""" 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"