Add reverse geocoding functionality and enhance driver details retrieval with location information
18f4b6b
| """ | |
| Geocoding service for FleetMind | |
| Handles address validation with Google Maps API and smart mock fallback | |
| """ | |
| import os | |
| import googlemaps | |
| import logging | |
| from typing import Dict, Optional | |
| logger = logging.getLogger(__name__) | |
| # Common city coordinates for mock geocoding | |
| CITY_COORDINATES = { | |
| "san francisco": (37.7749, -122.4194), | |
| "sf": (37.7749, -122.4194), | |
| "new york": (40.7128, -74.0060), | |
| "nyc": (40.7128, -74.0060), | |
| "los angeles": (34.0522, -118.2437), | |
| "la": (34.0522, -118.2437), | |
| "chicago": (41.8781, -87.6298), | |
| "houston": (29.7604, -95.3698), | |
| "phoenix": (33.4484, -112.0740), | |
| "philadelphia": (39.9526, -75.1652), | |
| "san antonio": (29.4241, -98.4936), | |
| "san diego": (32.7157, -117.1611), | |
| "dallas": (32.7767, -96.7970), | |
| "austin": (30.2672, -97.7431), | |
| "seattle": (47.6062, -122.3321), | |
| "boston": (42.3601, -71.0589), | |
| "denver": (39.7392, -104.9903), | |
| "miami": (25.7617, -80.1918), | |
| "atlanta": (33.7490, -84.3880), | |
| "portland": (45.5152, -122.6784), | |
| } | |
| class GeocodingService: | |
| """Handle address geocoding with Google Maps API and smart mock fallback""" | |
| def __init__(self): | |
| self.google_maps_key = os.getenv("GOOGLE_MAPS_API_KEY", "") | |
| self.use_mock = not self.google_maps_key or self.google_maps_key.startswith("your_") | |
| if self.use_mock: | |
| logger.info("Geocoding: Using mock (GOOGLE_MAPS_API_KEY not configured)") | |
| self.gmaps_client = None | |
| else: | |
| try: | |
| self.gmaps_client = googlemaps.Client(key=self.google_maps_key) | |
| logger.info("Geocoding: Using Google Maps API") | |
| except Exception as e: | |
| logger.error(f"Failed to initialize Google Maps client: {e}") | |
| self.use_mock = True | |
| self.gmaps_client = None | |
| def geocode(self, address: str) -> Dict: | |
| """ | |
| Geocode address, using mock if API unavailable | |
| Args: | |
| address: Street address to geocode | |
| Returns: | |
| Dict with keys: lat, lng, formatted_address, confidence | |
| """ | |
| if self.use_mock: | |
| return self._geocode_mock(address) | |
| else: | |
| try: | |
| return self._geocode_google(address) | |
| except Exception as e: | |
| logger.error(f"Google Maps API failed: {e}, falling back to mock") | |
| return self._geocode_mock(address) | |
| def _geocode_google(self, address: str) -> Dict: | |
| """Real Google Maps API geocoding""" | |
| try: | |
| # Call Google Maps Geocoding API | |
| result = self.gmaps_client.geocode(address) | |
| if not result: | |
| # No results found, fall back to mock | |
| logger.warning(f"Google Maps API found no results for: {address}") | |
| return self._geocode_mock(address) | |
| # Get first result | |
| first_result = result[0] | |
| location = first_result['geometry']['location'] | |
| return { | |
| "lat": location['lat'], | |
| "lng": location['lng'], | |
| "formatted_address": first_result.get('formatted_address', address), | |
| "confidence": "high (Google Maps API)" | |
| } | |
| except Exception as e: | |
| logger.error(f"Google Maps geocoding error: {e}") | |
| raise | |
| def _geocode_mock(self, address: str) -> Dict: | |
| """ | |
| Smart mock geocoding for testing | |
| Tries to detect city name and use approximate coordinates | |
| """ | |
| address_lower = address.lower() | |
| # Try to find a city match | |
| for city, coords in CITY_COORDINATES.items(): | |
| if city in address_lower: | |
| logger.info(f"Mock geocoding detected city: {city}") | |
| return { | |
| "lat": coords[0], | |
| "lng": coords[1], | |
| "formatted_address": address, | |
| "confidence": f"medium (mock - {city})" | |
| } | |
| # Default to San Francisco if no city detected | |
| logger.info("Mock geocoding: Using default SF coordinates") | |
| return { | |
| "lat": 37.7749, | |
| "lng": -122.4194, | |
| "formatted_address": address, | |
| "confidence": "low (mock - default)" | |
| } | |
| def reverse_geocode(self, lat: float, lng: float) -> Dict: | |
| """ | |
| Reverse geocode coordinates to address | |
| Args: | |
| lat: Latitude | |
| lng: Longitude | |
| Returns: | |
| Dict with keys: address, city, state, country, formatted_address | |
| """ | |
| if self.use_mock: | |
| return self._reverse_geocode_mock(lat, lng) | |
| else: | |
| try: | |
| return self._reverse_geocode_google(lat, lng) | |
| except Exception as e: | |
| logger.error(f"Google Maps reverse geocoding failed: {e}, falling back to mock") | |
| return self._reverse_geocode_mock(lat, lng) | |
| def _reverse_geocode_google(self, lat: float, lng: float) -> Dict: | |
| """Real Google Maps API reverse geocoding""" | |
| try: | |
| # Call Google Maps Reverse Geocoding API | |
| result = self.gmaps_client.reverse_geocode((lat, lng)) | |
| if not result: | |
| logger.warning(f"Google Maps API found no results for: ({lat}, {lng})") | |
| return self._reverse_geocode_mock(lat, lng) | |
| # Get first result | |
| first_result = result[0] | |
| # Extract address components | |
| address_components = first_result.get('address_components', []) | |
| city = "" | |
| state = "" | |
| country = "" | |
| for component in address_components: | |
| types = component.get('types', []) | |
| if 'locality' in types: | |
| city = component.get('long_name', '') | |
| elif 'administrative_area_level_1' in types: | |
| state = component.get('short_name', '') | |
| elif 'country' in types: | |
| country = component.get('long_name', '') | |
| return { | |
| "address": first_result.get('formatted_address', f"{lat}, {lng}"), | |
| "city": city, | |
| "state": state, | |
| "country": country, | |
| "formatted_address": first_result.get('formatted_address', f"{lat}, {lng}"), | |
| "confidence": "high (Google Maps API)" | |
| } | |
| except Exception as e: | |
| logger.error(f"Google Maps reverse geocoding error: {e}") | |
| raise | |
| def _reverse_geocode_mock(self, lat: float, lng: float) -> Dict: | |
| """ | |
| Mock reverse geocoding | |
| Tries to match coordinates to known cities | |
| """ | |
| # Find closest city | |
| min_distance = float('inf') | |
| closest_city = "Unknown Location" | |
| for city, coords in CITY_COORDINATES.items(): | |
| # Simple distance calculation | |
| distance = ((lat - coords[0]) ** 2 + (lng - coords[1]) ** 2) ** 0.5 | |
| if distance < min_distance: | |
| min_distance = distance | |
| closest_city = city | |
| # If very close to a known city (within ~0.1 degrees, roughly 11km) | |
| if min_distance < 0.1: | |
| logger.info(f"Mock reverse geocoding: Matched to {closest_city}") | |
| return { | |
| "address": f"Near {closest_city.title()}", | |
| "city": closest_city.title(), | |
| "state": "CA" if "san francisco" in closest_city or "la" in closest_city else "", | |
| "country": "USA", | |
| "formatted_address": f"Near {closest_city.title()}, USA", | |
| "confidence": f"medium (mock - near {closest_city})" | |
| } | |
| else: | |
| logger.info(f"Mock reverse geocoding: Unknown location at ({lat}, {lng})") | |
| return { | |
| "address": f"{lat}, {lng}", | |
| "city": "", | |
| "state": "", | |
| "country": "", | |
| "formatted_address": f"Coordinates: {lat}, {lng}", | |
| "confidence": "low (mock - no match)" | |
| } | |
| def get_status(self) -> str: | |
| """Get geocoding service status""" | |
| if self.use_mock: | |
| return "⚠️ Using mock geocoding (add GOOGLE_MAPS_API_KEY for real)" | |
| else: | |
| return "✅ Google Maps API connected" | |