# services/external/app/registry/city_registry.py """ City Registry - Configuration-driven multi-city support """ from dataclasses import dataclass from typing import List, Optional, Dict, Any from enum import Enum import math class Country(str, Enum): SPAIN = "ES" FRANCE = "FR" class WeatherProvider(str, Enum): AEMET = "aemet" METEO_FRANCE = "meteo_france" OPEN_WEATHER = "open_weather" class TrafficProvider(str, Enum): MADRID_OPENDATA = "madrid_opendata" VALENCIA_OPENDATA = "valencia_opendata" BARCELONA_OPENDATA = "barcelona_opendata" @dataclass class CityDefinition: """City configuration with data source specifications""" city_id: str name: str country: Country latitude: float longitude: float radius_km: float weather_provider: WeatherProvider weather_config: Dict[str, Any] traffic_provider: TrafficProvider traffic_config: Dict[str, Any] timezone: str population: int enabled: bool = True class CityRegistry: """Central registry of supported cities""" CITIES: List[CityDefinition] = [ CityDefinition( city_id="madrid", name="Madrid", country=Country.SPAIN, latitude=40.4168, longitude=-3.7038, radius_km=30.0, weather_provider=WeatherProvider.AEMET, weather_config={ "station_ids": ["3195", "3129", "3197"], "municipality_code": "28079" }, traffic_provider=TrafficProvider.MADRID_OPENDATA, traffic_config={ "current_xml_url": "https://datos.madrid.es/egob/catalogo/...", "historical_base_url": "https://datos.madrid.es/...", "measurement_points_csv": "https://datos.madrid.es/..." }, timezone="Europe/Madrid", population=3_200_000 ), CityDefinition( city_id="valencia", name="Valencia", country=Country.SPAIN, latitude=39.4699, longitude=-0.3763, radius_km=25.0, weather_provider=WeatherProvider.AEMET, weather_config={ "station_ids": ["8416"], "municipality_code": "46250" }, traffic_provider=TrafficProvider.VALENCIA_OPENDATA, traffic_config={ "api_endpoint": "https://valencia.opendatasoft.com/api/..." }, timezone="Europe/Madrid", population=800_000, enabled=False ), CityDefinition( city_id="barcelona", name="Barcelona", country=Country.SPAIN, latitude=41.3851, longitude=2.1734, radius_km=30.0, weather_provider=WeatherProvider.AEMET, weather_config={ "station_ids": ["0076"], "municipality_code": "08019" }, traffic_provider=TrafficProvider.BARCELONA_OPENDATA, traffic_config={ "api_endpoint": "https://opendata-ajuntament.barcelona.cat/..." }, timezone="Europe/Madrid", population=1_600_000, enabled=False ) ] @classmethod def get_enabled_cities(cls) -> List[CityDefinition]: """Get all enabled cities""" return [city for city in cls.CITIES if city.enabled] @classmethod def get_city(cls, city_id: str) -> Optional[CityDefinition]: """Get city by ID""" for city in cls.CITIES: if city.city_id == city_id: return city return None @classmethod def find_nearest_city(cls, latitude: float, longitude: float) -> Optional[CityDefinition]: """Find nearest enabled city to coordinates""" enabled_cities = cls.get_enabled_cities() if not enabled_cities: return None min_distance = float('inf') nearest_city = None for city in enabled_cities: distance = cls._haversine_distance( latitude, longitude, city.latitude, city.longitude ) if distance <= city.radius_km and distance < min_distance: min_distance = distance nearest_city = city return nearest_city @staticmethod def _haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: """Calculate distance in km between two coordinates""" R = 6371 dlat = math.radians(lat2 - lat1) dlon = math.radians(lon2 - lon1) a = (math.sin(dlat/2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2) ** 2) c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) return R * c