localgreenchain/lib/services/geolocation.ts
Claude 1e14a700c7
Implement LocalGreenChain: Plant Cloning Blockchain System
This commit implements a complete blockchain-based plant tracking system
that preserves lineage across clones, seeds, and all plant offspring while
connecting growers through geographic proximity.

Features implemented:
- Custom blockchain with proof-of-work consensus
- Plant registration and cloning with lineage tracking
- Geographic discovery to find nearby plants and growers
- Integration with plants.net API for plant identification
- Comprehensive web UI for plant management
- RESTful API endpoints for all operations
- Network statistics and visualization

Core Components:
- lib/blockchain/: PlantBlock, PlantChain, and blockchain manager
- lib/services/: plants.net API and geolocation services
- pages/api/plants/: REST API endpoints for all operations
- pages/: Frontend UI pages for registration, exploration, and lineage

Technical Details:
- TypeScript for type safety
- Next.js for server-side rendering
- Tailwind CSS for responsive design
- JSON file-based blockchain storage
- Haversine distance calculations for geolocation
- OpenStreetMap integration for geocoding

This system enables large-scale adoption by:
- Making plant lineage tracking accessible to everyone
- Connecting local communities through plant sharing
- Providing immutable proof of plant provenance
- Supporting unlimited generations of plant propagation
- Scaling from individual growers to global networks

Documentation includes comprehensive README with:
- Quick start guide
- API reference
- Architecture details
- Scaling recommendations
- Use cases for various audiences
- Roadmap for future enhancements
2025-11-16 05:11:55 +00:00

302 lines
8 KiB
TypeScript

/**
* Geolocation Service
* Provides location-based features for connecting plant owners
*/
import { PlantData, PlantLocation } from '../blockchain/types';
export interface PlantCluster {
centerLat: number;
centerLon: number;
plantCount: number;
plants: PlantData[];
radius: number; // in km
dominantSpecies: string[];
}
export interface ConnectionSuggestion {
plant1: PlantData;
plant2: PlantData;
distance: number;
matchReason: string; // e.g., "same species", "same lineage", "nearby location"
compatibilityScore: number; // 0-100
}
export class GeolocationService {
/**
* Calculate distance between two coordinates using Haversine formula
*/
calculateDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371; // Earth's radius in km
const dLat = this.toRadians(lat2 - lat1);
const dLon = this.toRadians(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRadians(lat1)) *
Math.cos(this.toRadians(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
private toRadians(degrees: number): number {
return degrees * (Math.PI / 180);
}
/**
* Find plant clusters in a region
* Groups nearby plants together to show hotspots of activity
*/
findPlantClusters(
plants: PlantData[],
clusterRadius: number = 10 // km
): PlantCluster[] {
const clusters: PlantCluster[] = [];
const processed = new Set<string>();
for (const plant of plants) {
if (processed.has(plant.id)) continue;
// Find all plants within cluster radius
const clusterPlants: PlantData[] = [plant];
processed.add(plant.id);
for (const otherPlant of plants) {
if (processed.has(otherPlant.id)) continue;
const distance = this.calculateDistance(
plant.location.latitude,
plant.location.longitude,
otherPlant.location.latitude,
otherPlant.location.longitude
);
if (distance <= clusterRadius) {
clusterPlants.push(otherPlant);
processed.add(otherPlant.id);
}
}
// Calculate cluster center (average of all positions)
const centerLat =
clusterPlants.reduce((sum, p) => sum + p.location.latitude, 0) /
clusterPlants.length;
const centerLon =
clusterPlants.reduce((sum, p) => sum + p.location.longitude, 0) /
clusterPlants.length;
// Find dominant species
const speciesCount: { [key: string]: number } = {};
for (const p of clusterPlants) {
if (p.scientificName) {
speciesCount[p.scientificName] =
(speciesCount[p.scientificName] || 0) + 1;
}
}
const dominantSpecies = Object.entries(speciesCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([species]) => species);
clusters.push({
centerLat,
centerLon,
plantCount: clusterPlants.length,
plants: clusterPlants,
radius: clusterRadius,
dominantSpecies,
});
}
return clusters.sort((a, b) => b.plantCount - a.plantCount);
}
/**
* Suggest connections between plant owners
* Finds compatible plants for sharing/trading
*/
suggestConnections(
userPlant: PlantData,
allPlants: PlantData[],
maxDistance: number = 50 // km
): ConnectionSuggestion[] {
const suggestions: ConnectionSuggestion[] = [];
for (const otherPlant of allPlants) {
// Skip if same owner
if (otherPlant.owner.id === userPlant.owner.id) continue;
// Skip if same plant
if (otherPlant.id === userPlant.id) continue;
const distance = this.calculateDistance(
userPlant.location.latitude,
userPlant.location.longitude,
otherPlant.location.latitude,
otherPlant.location.longitude
);
// Skip if too far
if (distance > maxDistance) continue;
let matchReason = '';
let compatibilityScore = 0;
// Check for same species
if (
userPlant.scientificName &&
userPlant.scientificName === otherPlant.scientificName
) {
matchReason = 'Same species';
compatibilityScore += 40;
}
// Check for same lineage
if (
userPlant.parentPlantId === otherPlant.parentPlantId &&
userPlant.parentPlantId
) {
matchReason =
matchReason + (matchReason ? ', ' : '') + 'Same parent plant';
compatibilityScore += 30;
}
// Check for same genus
if (userPlant.genus && userPlant.genus === otherPlant.genus) {
if (!matchReason) matchReason = 'Same genus';
compatibilityScore += 20;
}
// Proximity bonus
const proximityScore = Math.max(0, 20 - distance / 2.5);
compatibilityScore += proximityScore;
// Only suggest if there's some compatibility
if (compatibilityScore > 20) {
if (!matchReason) matchReason = 'Nearby location';
suggestions.push({
plant1: userPlant,
plant2: otherPlant,
distance,
matchReason,
compatibilityScore: Math.min(100, compatibilityScore),
});
}
}
return suggestions.sort((a, b) => b.compatibilityScore - a.compatibilityScore);
}
/**
* Get address from coordinates using reverse geocoding
* Note: In production, you'd use a service like Google Maps or OpenStreetMap
*/
async reverseGeocode(
latitude: number,
longitude: number
): Promise<Partial<PlantLocation>> {
try {
// Using OpenStreetMap Nominatim (free, but rate-limited)
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`,
{
headers: {
'User-Agent': 'LocalGreenChain/1.0',
},
}
);
if (!response.ok) {
console.error('Reverse geocoding error:', response.statusText);
return { latitude, longitude };
}
const data = await response.json();
return {
latitude,
longitude,
address: data.display_name,
city: data.address?.city || data.address?.town || data.address?.village,
country: data.address?.country,
};
} catch (error) {
console.error('Error in reverse geocoding:', error);
return { latitude, longitude };
}
}
/**
* Get coordinates from address using forward geocoding
*/
async geocode(
address: string
): Promise<{ latitude: number; longitude: number } | null> {
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`,
{
headers: {
'User-Agent': 'LocalGreenChain/1.0',
},
}
);
if (!response.ok) {
console.error('Geocoding error:', response.statusText);
return null;
}
const data = await response.json();
if (data.length === 0) return null;
return {
latitude: parseFloat(data[0].lat),
longitude: parseFloat(data[0].lon),
};
} catch (error) {
console.error('Error in geocoding:', error);
return null;
}
}
/**
* Check if a location is within a given boundary
*/
isWithinBounds(
location: { latitude: number; longitude: number },
bounds: {
north: number;
south: number;
east: number;
west: number;
}
): boolean {
return (
location.latitude <= bounds.north &&
location.latitude >= bounds.south &&
location.longitude <= bounds.east &&
location.longitude >= bounds.west
);
}
}
// Singleton instance
let geolocationService: GeolocationService | null = null;
export function getGeolocationService(): GeolocationService {
if (!geolocationService) {
geolocationService = new GeolocationService();
}
return geolocationService;
}