Droid210/FleetVision
0
1"""Routing utility powered by OpenRouteService (ORS)."""2from __future__ import annotations3 4import os5from dataclasses import dataclass6from pathlib import Path7from typing import Tuple8 9import openrouteservice10 11 12class RoutingServiceError(RuntimeError):13 """Raised when ORS routing or geocoding fails."""14 15 16@dataclass(frozen=True)17class RouteSummary:18 distance_km: float19 duration_mins: float20 formatted_eta: str21 22 23def _load_env_file() -> None:24 """Lightweight .env loader to avoid hard dependency on dotenv package."""25 env_path = Path(__file__).resolve().parents[1] / ".env"26 if not env_path.exists():27 return28 29 for line in env_path.read_text(encoding="utf-8").splitlines():30 entry = line.strip()31 if not entry or entry.startswith("#") or "=" not in entry:32 continue33 key, value = entry.split("=", 1)34 key = key.strip()35 value = value.strip().strip('"').strip("'")36 if key and key not in os.environ:37 os.environ[key] = value38 39 40def _get_ors_client() -> openrouteservice.Client:41 _load_env_file()42 api_key = os.getenv("ORS_API_KEY", "").strip()43 if not api_key:44 raise RoutingServiceError("Missing ORS_API_KEY. Set it in .env or environment variables.")45 return openrouteservice.Client(key=api_key)46 47 48def _format_eta(duration_minutes: float) -> str:49 total_mins = int(round(duration_minutes))50 hours, mins = divmod(total_mins, 60)51 if hours:52 return f"{hours}h {mins}m"53 return f"{mins}m"54 55 56def geocode_destination(destination: str) -> Tuple[float, float]:57 """Convert a destination string into ORS coordinates (lon, lat)."""58 if not destination or not destination.strip():59 raise RoutingServiceError("Destination text is empty.")60 61 client = _get_ors_client()62 try:63 result = client.pelias_search(text=destination.strip(), size=1)64 features = result.get("features", [])65 if not features:66 raise RoutingServiceError(f"No geocoding result for destination: {destination}")67 coords = features[0]["geometry"]["coordinates"]68 return float(coords[0]), float(coords[1])69 except RoutingServiceError:70 raise71 except Exception as exc: # noqa: BLE00172 raise RoutingServiceError(f"Geocoding failed: {exc}") from exc73 74 75def get_ride_details(start_coords: Tuple[float, float], end_coords: Tuple[float, float]) -> dict:76 """Get driving distance and ETA via ORS `driving-car` profile.77 78 Args:79 start_coords: Tuple(lon, lat) for origin.80 end_coords: Tuple(lon, lat) for destination.81 82 Returns:83 Dictionary with distance_km, duration_mins, formatted_eta.84 """85 client = _get_ors_client()86 87 try:88 route = client.directions(89 coordinates=[list(start_coords), list(end_coords)],90 profile="driving-car",91 format="geojson",92 )93 segment = route["features"][0]["properties"]["segments"][0]94 distance_km = float(segment["distance"]) / 1000.095 duration_mins = float(segment["duration"]) / 60.096 97 summary = RouteSummary(98 distance_km=round(distance_km, 2),99 duration_mins=round(duration_mins, 2),100 formatted_eta=_format_eta(duration_mins),101 )102 return {103 "distance_km": summary.distance_km,104 "duration_mins": summary.duration_mins,105 "formatted_eta": summary.formatted_eta,106 }107 except Exception as exc: # noqa: BLE001108 raise RoutingServiceError(f"Routing request failed: {exc}") from exc109 