import numpy as np from typing import List, Dict, Optional from datetime import datetime import logging logger = logging.getLogger(__name__) class LocalRouteEstimator: """ 一個本地的距離與時間估算器,用於取代昂貴的 Google Distance Matrix API。 使用 Haversine 公式 + 曲折係數進行估算。 """ # 曲折係數 (Circuity Factor): 直線距離 x 此係數 = 估算道路距離 # 一般城市道路約為 1.3 ~ 1.5 TORTUOSITY_FACTOR = 1.55 # 預設行駛速度 (km/h) -> 轉換為 m/s AVERAGE_SPEED_KMH = 32.5 AVERAGE_SPEED_MPS = AVERAGE_SPEED_KMH * 1000 / 3600 def compute_route_matrix( self, origins: List[Dict[str, float]], destinations: List[Dict[str, float]], *args, **kwargs ) -> Dict[str, np.ndarray]: """ 本地版矩陣計算 (Drop-in replacement for Google API) 完全不發送 HTTP 請求,純數學計算。 """ O_len = len(origins) D_len = len(destinations) logger.info( f"🧮 Computing LOCAL route matrix: {O_len}×{D_len} " f"(Speed assumed: {self.AVERAGE_SPEED_KMH} km/h)" ) # 1. 準備座標數據 (轉成 numpy array) # origins structure: [{'lat': x, 'lng': y}, ...] o_lats = np.array([o['lat'] for o in origins], dtype=np.float64) o_lngs = np.array([o['lng'] for o in origins], dtype=np.float64) d_lats = np.array([d['lat'] for d in destinations], dtype=np.float64) d_lngs = np.array([d['lng'] for d in destinations], dtype=np.float64) # 2. 向量化 Haversine 計算 (利用 Broadcasting 建立矩陣) # 將維度調整為 (O, 1) 和 (1, D) 以便廣播運算 lat1 = np.radians(o_lats)[:, np.newaxis] lon1 = np.radians(o_lngs)[:, np.newaxis] lat2 = np.radians(d_lats)[np.newaxis, :] lon2 = np.radians(d_lngs)[np.newaxis, :] # Haversine 公式 dlon = lon2 - lon1 dlat = lat2 - lat1 a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2 c = 2 * np.arcsin(np.sqrt(a)) r = 6371000 # 地球半徑 (公尺) # 得到直線距離 (Great Circle Distance) straight_distance = c * r # 3. 估算道路距離 (Road Distance) # 乘以曲折係數 estimated_distance = straight_distance * self.TORTUOSITY_FACTOR # 4. 估算時間 (Duration) # 時間 = 距離 / 速度 # 為了避免除以零,我們假設最小時間 estimated_duration = estimated_distance / self.AVERAGE_SPEED_MPS # 轉換為 int32 (與 Google API 回傳格式保持一致) distance_matrix = estimated_distance.astype(np.int32) duration_matrix = estimated_duration.astype(np.int32) # 5. 自我對角線處理 (如果是同一個點,距離時間設為 0) # 只有當 origins 和 destinations 列表完全一樣時才需要檢查 # 這裡簡單處理:如果距離極小 (< 10米),設為 0 mask = distance_matrix < 10 distance_matrix[mask] = 0 duration_matrix[mask] = 0 logger.debug("✅ Local matrix computation done.") return { "duration_matrix": duration_matrix, "distance_matrix": distance_matrix, }