/** * 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(); 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> { 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; }