Spaces:
Running
Running
hotfix: replace Google Maps API with local estimation in optimizer
Browse files- Disabled Google Maps `Compute Routes` and `Distance Matrix` APIs in the optimization loop to prevent billing overages.
- Swapped the underlying algorithm with a local Haversine-based distance estimator.
- This is an emergency fix to resolve the high API usage incident.
src/services/local_route_estimator.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from typing import List, Dict, Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class LocalRouteEstimator:
|
| 10 |
+
"""
|
| 11 |
+
一個本地的距離與時間估算器,用於取代昂貴的 Google Distance Matrix API。
|
| 12 |
+
使用 Haversine 公式 + 曲折係數進行估算。
|
| 13 |
+
"""
|
| 14 |
+
# 曲折係數 (Circuity Factor): 直線距離 x 此係數 = 估算道路距離
|
| 15 |
+
# 一般城市道路約為 1.3 ~ 1.5
|
| 16 |
+
TORTUOSITY_FACTOR = 1.4
|
| 17 |
+
|
| 18 |
+
# 預設行駛速度 (km/h) -> 轉換為 m/s
|
| 19 |
+
AVERAGE_SPEED_KMH = 35.0
|
| 20 |
+
AVERAGE_SPEED_MPS = AVERAGE_SPEED_KMH * 1000 / 3600
|
| 21 |
+
|
| 22 |
+
def compute_route_matrix(
|
| 23 |
+
self,
|
| 24 |
+
origins: List[Dict[str, float]],
|
| 25 |
+
destinations: List[Dict[str, float]],
|
| 26 |
+
*args, **kwargs
|
| 27 |
+
) -> Dict[str, np.ndarray]:
|
| 28 |
+
"""
|
| 29 |
+
本地版矩陣計算 (Drop-in replacement for Google API)
|
| 30 |
+
|
| 31 |
+
完全不發送 HTTP 請求,純數學計算。
|
| 32 |
+
"""
|
| 33 |
+
O_len = len(origins)
|
| 34 |
+
D_len = len(destinations)
|
| 35 |
+
|
| 36 |
+
logger.info(
|
| 37 |
+
f"🧮 Computing LOCAL route matrix: {O_len}×{D_len} "
|
| 38 |
+
f"(Speed assumed: {self.AVERAGE_SPEED_KMH} km/h)"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# 1. 準備座標數據 (轉成 numpy array)
|
| 42 |
+
# origins structure: [{'lat': x, 'lng': y}, ...]
|
| 43 |
+
o_lats = np.array([o['lat'] for o in origins], dtype=np.float64)
|
| 44 |
+
o_lngs = np.array([o['lng'] for o in origins], dtype=np.float64)
|
| 45 |
+
|
| 46 |
+
d_lats = np.array([d['lat'] for d in destinations], dtype=np.float64)
|
| 47 |
+
d_lngs = np.array([d['lng'] for d in destinations], dtype=np.float64)
|
| 48 |
+
|
| 49 |
+
# 2. 向量化 Haversine 計算 (利用 Broadcasting 建立矩陣)
|
| 50 |
+
# 將維度調整為 (O, 1) 和 (1, D) 以便廣播運算
|
| 51 |
+
lat1 = np.radians(o_lats)[:, np.newaxis]
|
| 52 |
+
lon1 = np.radians(o_lngs)[:, np.newaxis]
|
| 53 |
+
lat2 = np.radians(d_lats)[np.newaxis, :]
|
| 54 |
+
lon2 = np.radians(d_lngs)[np.newaxis, :]
|
| 55 |
+
|
| 56 |
+
# Haversine 公式
|
| 57 |
+
dlon = lon2 - lon1
|
| 58 |
+
dlat = lat2 - lat1
|
| 59 |
+
a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2
|
| 60 |
+
c = 2 * np.arcsin(np.sqrt(a))
|
| 61 |
+
r = 6371000 # 地球半徑 (公尺)
|
| 62 |
+
|
| 63 |
+
# 得到直線距離 (Great Circle Distance)
|
| 64 |
+
straight_distance = c * r
|
| 65 |
+
|
| 66 |
+
# 3. 估算道路距離 (Road Distance)
|
| 67 |
+
# 乘以曲折係數
|
| 68 |
+
estimated_distance = straight_distance * self.TORTUOSITY_FACTOR
|
| 69 |
+
|
| 70 |
+
# 4. 估算時間 (Duration)
|
| 71 |
+
# 時間 = 距離 / 速度
|
| 72 |
+
# 為了避免除以零,我們假設最小時間
|
| 73 |
+
estimated_duration = estimated_distance / self.AVERAGE_SPEED_MPS
|
| 74 |
+
|
| 75 |
+
# 轉換為 int32 (與 Google API 回傳格式保持一致)
|
| 76 |
+
distance_matrix = estimated_distance.astype(np.int32)
|
| 77 |
+
duration_matrix = estimated_duration.astype(np.int32)
|
| 78 |
+
|
| 79 |
+
# 5. 自我對角線處理 (如果是同一個點,距離時間設為 0)
|
| 80 |
+
# 只有當 origins 和 destinations 列表完全一樣時才需要檢查
|
| 81 |
+
# 這裡簡單處理:如果距離極小 (< 10米),設為 0
|
| 82 |
+
mask = distance_matrix < 10
|
| 83 |
+
distance_matrix[mask] = 0
|
| 84 |
+
duration_matrix[mask] = 0
|
| 85 |
+
|
| 86 |
+
logger.debug("✅ Local matrix computation done.")
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"duration_matrix": duration_matrix,
|
| 90 |
+
"distance_matrix": distance_matrix,
|
| 91 |
+
}
|