164 lines
4.8 KiB
Python
164 lines
4.8 KiB
Python
# 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
|