Spaces:
Sleeping
Sleeping
| """ | |
| Data Source: Google Maps Routes API + OSRM (fallback) | |
| ====================================================== | |
| Routing untuk distance/duration antar kabupaten — lebih akurat dari haversine | |
| yang dipakai default (geodesic distance, tidak tahu jalan/macet/ferry). | |
| Primary: Google Maps Routes API | |
| URL: https://routes.googleapis.com/directions/v2:computeRoutes | |
| Authentication: API key required (paid setelah free tier). | |
| Free tier: $200/bulan credits. | |
| Fallback: OSRM Demo Server | |
| URL: https://router.project-osrm.org/route/v1/driving/ | |
| Free, public demo. Rate limit ~1000 req/day. | |
| Production: deploy OSRM sendiri di internal server. | |
| Untuk hackathon: default ke OSRM (gratis), Google sebagai opsi. | |
| Author: AgriFlow Team | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from typing import Optional | |
| try: | |
| import requests | |
| HAS_REQUESTS = True | |
| except ImportError: | |
| HAS_REQUESTS = False | |
| class RouteConnector: | |
| """ | |
| Routing connector dengan Google → OSRM fallback. | |
| Usage: | |
| rc = RouteConnector() | |
| result = rc.get_distance_duration( | |
| origin_lat=-7.79, origin_lon=112.17, | |
| dest_lat=-7.25, dest_lon=112.75, | |
| ) | |
| # → {"distance_km": 134.2, "duration_minutes": 168, "source": "OSRM"} | |
| """ | |
| GOOGLE_URL = "https://routes.googleapis.com/directions/v2:computeRoutes" | |
| OSRM_URL = "https://router.project-osrm.org/route/v1/driving" | |
| DEFAULT_TIMEOUT = 10 | |
| def __init__( | |
| self, | |
| google_api_key: Optional[str] = None, | |
| prefer_google: bool = False, | |
| timeout: int = DEFAULT_TIMEOUT, | |
| ): | |
| self.google_api_key = google_api_key or os.getenv("GOOGLE_MAPS_API_KEY", "") | |
| self.prefer_google = prefer_google and bool(self.google_api_key) | |
| self.timeout = timeout | |
| def get_distance_duration( | |
| self, | |
| origin_lat: float, origin_lon: float, | |
| dest_lat: float, dest_lon: float, | |
| ) -> dict: | |
| """ | |
| Get driving distance & duration between 2 points. | |
| Returns: | |
| { | |
| "distance_km": float, | |
| "duration_minutes": float, | |
| "source": "GOOGLE" | "OSRM" | |
| } | |
| """ | |
| if self.prefer_google: | |
| result = self._fetch_google(origin_lat, origin_lon, dest_lat, dest_lon) | |
| if result: | |
| return result | |
| # Fallback ke OSRM | |
| return self._fetch_osrm(origin_lat, origin_lon, dest_lat, dest_lon) | |
| def _fetch_google( | |
| self, | |
| origin_lat: float, origin_lon: float, | |
| dest_lat: float, dest_lon: float, | |
| ) -> Optional[dict]: | |
| if not HAS_REQUESTS or not self.google_api_key: | |
| return None | |
| try: | |
| payload = { | |
| "origin": {"location": {"latLng": { | |
| "latitude": origin_lat, "longitude": origin_lon | |
| }}}, | |
| "destination": {"location": {"latLng": { | |
| "latitude": dest_lat, "longitude": dest_lon | |
| }}}, | |
| "travelMode": "DRIVE", | |
| "routingPreference": "TRAFFIC_AWARE", | |
| } | |
| headers = { | |
| "Content-Type": "application/json", | |
| "X-Goog-Api-Key": self.google_api_key, | |
| "X-Goog-FieldMask": "routes.distanceMeters,routes.duration", | |
| } | |
| r = requests.post(self.GOOGLE_URL, json=payload, headers=headers, | |
| timeout=self.timeout) | |
| r.raise_for_status() | |
| data = r.json() | |
| routes = data.get("routes", []) | |
| if not routes: | |
| return None | |
| route = routes[0] | |
| distance_m = route.get("distanceMeters", 0) | |
| duration_str = route.get("duration", "0s") | |
| duration_s = int(duration_str.rstrip("s")) | |
| return { | |
| "distance_km": distance_m / 1000.0, | |
| "duration_minutes": duration_s / 60.0, | |
| "source": "GOOGLE", | |
| } | |
| except (requests.RequestException, ValueError, KeyError) as e: | |
| print(f"⚠ Google Maps error: {e}") | |
| return None | |
| def _fetch_osrm( | |
| self, | |
| origin_lat: float, origin_lon: float, | |
| dest_lat: float, dest_lon: float, | |
| ) -> dict: | |
| """OSRM demo server. Format: /route/v1/driving/lon1,lat1;lon2,lat2""" | |
| if not HAS_REQUESTS: | |
| # Tanpa requests → return haversine estimate | |
| return self._fallback_haversine( | |
| origin_lat, origin_lon, dest_lat, dest_lon | |
| ) | |
| try: | |
| url = (f"{self.OSRM_URL}/" | |
| f"{origin_lon},{origin_lat};{dest_lon},{dest_lat}" | |
| f"?overview=false") | |
| r = requests.get(url, timeout=self.timeout) | |
| r.raise_for_status() | |
| data = r.json() | |
| if data.get("code") != "Ok" or not data.get("routes"): | |
| return self._fallback_haversine( | |
| origin_lat, origin_lon, dest_lat, dest_lon | |
| ) | |
| route = data["routes"][0] | |
| return { | |
| "distance_km": route["distance"] / 1000.0, | |
| "duration_minutes": route["duration"] / 60.0, | |
| "source": "OSRM", | |
| } | |
| except (requests.RequestException, ValueError, KeyError) as e: | |
| print(f"⚠ OSRM error: {e}, fallback ke haversine") | |
| return self._fallback_haversine( | |
| origin_lat, origin_lon, dest_lat, dest_lon | |
| ) | |
| def _fallback_haversine( | |
| self, | |
| origin_lat: float, origin_lon: float, | |
| dest_lat: float, dest_lon: float, | |
| ) -> dict: | |
| """Last-resort fallback: haversine + asumsi 60 km/h.""" | |
| from math import asin, cos, radians, sin, sqrt | |
| R = 6371.0 | |
| dlat = radians(dest_lat - origin_lat) | |
| dlon = radians(dest_lon - origin_lon) | |
| a = (sin(dlat / 2) ** 2 + | |
| cos(radians(origin_lat)) * cos(radians(dest_lat)) * sin(dlon / 2) ** 2) | |
| dist_km = 2 * R * asin(sqrt(a)) | |
| return { | |
| "distance_km": dist_km, | |
| "duration_minutes": (dist_km / 60.0) * 60.0, # 60 km/h | |
| "source": "HAVERSINE_FALLBACK", | |
| } | |
| def get_route(lat1: float, lon1: float, lat2: float, lon2: float) -> dict: | |
| """Helper helper.""" | |
| return RouteConnector().get_distance_duration(lat1, lon1, lat2, lon2) | |
| if __name__ == "__main__": | |
| # Smoke test: Kediri → Surabaya | |
| result = get_route(-7.796, 112.170, -7.2575, 112.7521) | |
| print("Kediri → Surabaya route:", result) | |