257 lines
9.9 KiB
Python
257 lines
9.9 KiB
Python
# ================================================================
|
|
# services/data/app/external/apis/traffic.py
|
|
# ================================================================
|
|
"""
|
|
Traffic API abstraction layer for multiple cities
|
|
"""
|
|
|
|
import asyncio
|
|
from abc import ABC, abstractmethod
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
import structlog
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class SupportedCity(Enum):
|
|
"""Supported cities for traffic data collection"""
|
|
MADRID = "madrid"
|
|
BARCELONA = "barcelona"
|
|
VALENCIA = "valencia"
|
|
|
|
|
|
class BaseTrafficClient(ABC):
|
|
"""
|
|
Abstract base class for city-specific traffic clients
|
|
Defines the contract that all traffic clients must implement
|
|
"""
|
|
|
|
def __init__(self, city: SupportedCity):
|
|
self.city = city
|
|
self.logger = structlog.get_logger().bind(city=city.value)
|
|
|
|
@abstractmethod
|
|
async def get_current_traffic(self, latitude: float, longitude: float) -> Optional[Dict[str, Any]]:
|
|
"""Get current traffic data for location"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def get_historical_traffic(self, latitude: float, longitude: float,
|
|
start_date: datetime, end_date: datetime) -> List[Dict[str, Any]]:
|
|
"""Get historical traffic data"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def get_events(self, latitude: float, longitude: float, radius_km: float = 5.0) -> List[Dict[str, Any]]:
|
|
"""Get traffic incidents and events"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def supports_location(self, latitude: float, longitude: float) -> bool:
|
|
"""Check if this client supports the given location"""
|
|
pass
|
|
|
|
|
|
class TrafficAPIClientFactory:
|
|
"""
|
|
Factory class to create appropriate traffic clients based on location
|
|
"""
|
|
|
|
# City geographical bounds
|
|
CITY_BOUNDS = {
|
|
SupportedCity.MADRID: {
|
|
'lat_min': 40.31, 'lat_max': 40.56,
|
|
'lon_min': -3.89, 'lon_max': -3.51
|
|
},
|
|
SupportedCity.BARCELONA: {
|
|
'lat_min': 41.32, 'lat_max': 41.47,
|
|
'lon_min': 2.05, 'lon_max': 2.25
|
|
},
|
|
SupportedCity.VALENCIA: {
|
|
'lat_min': 39.42, 'lat_max': 39.52,
|
|
'lon_min': -0.42, 'lon_max': -0.32
|
|
}
|
|
}
|
|
|
|
@classmethod
|
|
def get_client_for_location(cls, latitude: float, longitude: float) -> Optional[BaseTrafficClient]:
|
|
"""
|
|
Get appropriate traffic client for given location
|
|
|
|
Args:
|
|
latitude: Query location latitude
|
|
longitude: Query location longitude
|
|
|
|
Returns:
|
|
BaseTrafficClient instance or None if location not supported
|
|
"""
|
|
try:
|
|
# Check each city's bounds
|
|
for city, bounds in cls.CITY_BOUNDS.items():
|
|
if (bounds['lat_min'] <= latitude <= bounds['lat_max'] and
|
|
bounds['lon_min'] <= longitude <= bounds['lon_max']):
|
|
|
|
logger.info("Location matched to city",
|
|
city=city.value, lat=latitude, lon=longitude)
|
|
return cls._create_client(city)
|
|
|
|
# If no specific city matches, try to find closest supported city
|
|
closest_city = cls._find_closest_city(latitude, longitude)
|
|
if closest_city:
|
|
logger.info("Using closest city for location",
|
|
closest_city=closest_city.value, lat=latitude, lon=longitude)
|
|
return cls._create_client(closest_city)
|
|
|
|
logger.warning("No traffic client available for location",
|
|
lat=latitude, lon=longitude)
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting traffic client for location",
|
|
lat=latitude, lon=longitude, error=str(e))
|
|
return None
|
|
|
|
@classmethod
|
|
def _create_client(cls, city: SupportedCity) -> BaseTrafficClient:
|
|
"""Create traffic client for specific city"""
|
|
if city == SupportedCity.MADRID:
|
|
from .madrid_traffic_client import MadridTrafficClient
|
|
return MadridTrafficClient()
|
|
elif city == SupportedCity.BARCELONA:
|
|
# Future implementation
|
|
raise NotImplementedError(f"Traffic client for {city.value} not yet implemented")
|
|
elif city == SupportedCity.VALENCIA:
|
|
# Future implementation
|
|
raise NotImplementedError(f"Traffic client for {city.value} not yet implemented")
|
|
else:
|
|
raise ValueError(f"Unsupported city: {city}")
|
|
|
|
@classmethod
|
|
def _find_closest_city(cls, latitude: float, longitude: float) -> Optional[SupportedCity]:
|
|
"""Find closest supported city to given coordinates"""
|
|
import math
|
|
|
|
def distance(lat1, lon1, lat2, lon2):
|
|
"""Calculate distance between two coordinates"""
|
|
R = 6371 # Earth's radius in km
|
|
dlat = math.radians(lat2 - lat1)
|
|
dlon = math.radians(lon2 - lon1)
|
|
a = (math.sin(dlat/2) * math.sin(dlat/2) +
|
|
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
|
|
math.sin(dlon/2) * math.sin(dlon/2))
|
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
|
return R * c
|
|
|
|
min_distance = float('inf')
|
|
closest_city = None
|
|
|
|
# City centers for distance calculation
|
|
city_centers = {
|
|
SupportedCity.MADRID: (40.4168, -3.7038),
|
|
SupportedCity.BARCELONA: (41.3851, 2.1734),
|
|
SupportedCity.VALENCIA: (39.4699, -0.3763)
|
|
}
|
|
|
|
for city, (city_lat, city_lon) in city_centers.items():
|
|
dist = distance(latitude, longitude, city_lat, city_lon)
|
|
if dist < min_distance and dist < 100: # Within 100km
|
|
min_distance = dist
|
|
closest_city = city
|
|
|
|
return closest_city
|
|
|
|
@classmethod
|
|
def get_supported_cities(cls) -> List[Dict[str, Any]]:
|
|
"""Get list of supported cities with their bounds"""
|
|
cities = []
|
|
for city, bounds in cls.CITY_BOUNDS.items():
|
|
cities.append({
|
|
"city": city.value,
|
|
"bounds": bounds,
|
|
"status": "active" if city == SupportedCity.MADRID else "planned"
|
|
})
|
|
return cities
|
|
|
|
|
|
class UniversalTrafficClient:
|
|
"""
|
|
Universal traffic client that delegates to appropriate city-specific clients
|
|
This is the main interface that external services should use
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.factory = TrafficAPIClientFactory()
|
|
self.client_cache = {} # Cache clients for performance
|
|
|
|
async def get_current_traffic(self, latitude: float, longitude: float) -> Optional[Dict[str, Any]]:
|
|
"""Get current traffic data for any supported location"""
|
|
try:
|
|
client = self._get_client_for_location(latitude, longitude)
|
|
if client:
|
|
return await client.get_current_traffic(latitude, longitude)
|
|
else:
|
|
logger.warning("No traffic data available for location",
|
|
lat=latitude, lon=longitude)
|
|
return None
|
|
except Exception as e:
|
|
logger.error("Error getting current traffic",
|
|
lat=latitude, lon=longitude, error=str(e))
|
|
return None
|
|
|
|
async def get_historical_traffic(self, latitude: float, longitude: float,
|
|
start_date: datetime, end_date: datetime) -> List[Dict[str, Any]]:
|
|
"""Get historical traffic data for any supported location"""
|
|
try:
|
|
client = self._get_client_for_location(latitude, longitude)
|
|
if client:
|
|
return await client.get_historical_traffic(latitude, longitude, start_date, end_date)
|
|
else:
|
|
logger.warning("No historical traffic data available for location",
|
|
lat=latitude, lon=longitude)
|
|
return []
|
|
except Exception as e:
|
|
logger.error("Error getting historical traffic",
|
|
lat=latitude, lon=longitude, error=str(e))
|
|
return []
|
|
|
|
async def get_events(self, latitude: float, longitude: float, radius_km: float = 5.0) -> List[Dict[str, Any]]:
|
|
"""Get traffic events for any supported location"""
|
|
try:
|
|
client = self._get_client_for_location(latitude, longitude)
|
|
if client:
|
|
return await client.get_events(latitude, longitude, radius_km)
|
|
else:
|
|
return []
|
|
except Exception as e:
|
|
logger.error("Error getting traffic events",
|
|
lat=latitude, lon=longitude, error=str(e))
|
|
return []
|
|
|
|
def _get_client_for_location(self, latitude: float, longitude: float) -> Optional[BaseTrafficClient]:
|
|
"""Get cached or create new client for location"""
|
|
cache_key = f"{latitude:.4f},{longitude:.4f}"
|
|
|
|
if cache_key not in self.client_cache:
|
|
client = self.factory.get_client_for_location(latitude, longitude)
|
|
self.client_cache[cache_key] = client
|
|
|
|
return self.client_cache[cache_key]
|
|
|
|
def get_location_info(self, latitude: float, longitude: float) -> Dict[str, Any]:
|
|
"""Get information about traffic data availability for location"""
|
|
client = self._get_client_for_location(latitude, longitude)
|
|
if client:
|
|
return {
|
|
"supported": True,
|
|
"city": client.city.value,
|
|
"features": ["current_traffic", "historical_traffic", "events"]
|
|
}
|
|
else:
|
|
return {
|
|
"supported": False,
|
|
"city": None,
|
|
"features": [],
|
|
"message": "No traffic data available for this location"
|
|
} |