agriflow-api / data_sources /google_maps.py
masterAAA123's picture
Space deploy v5: orphan snapshot of main, zero binary files
b81a86b
Raw
History Blame Contribute Delete
6.6 kB
"""
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)