from __future__ import annotations from dataclasses import dataclass from math import asin, cos, radians, sin, sqrt from typing import Protocol def haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float: radius_km = 6371.0088 dlat = radians(lat2 - lat1) dlon = radians(lon2 - lon1) a = sin(dlat / 2) ** 2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon / 2) ** 2 return 2 * radius_km * asin(sqrt(a)) @dataclass(frozen=True) class CommuteEstimate: minutes: int km: float source: str source_version: str confidence: float = 0.55 is_estimate: bool = True class CommuteEstimator(Protocol): name: str version: str def estimate(self, distance_km: float) -> CommuteEstimate: ... @dataclass(frozen=True) class ConservativeRoadEstimator: name: str = "road" version: str = "offline-heuristic-1" avg_kmh: float = 34.0 route_factor: float = 1.35 confidence: float = 0.55 def estimate(self, distance_km: float) -> CommuteEstimate: if distance_km <= 0: minutes = 0 else: minutes = int(((distance_km / self.avg_kmh) * 60.0) * self.route_factor) return CommuteEstimate( minutes=max(minutes, 1), km=distance_km, source=self.name, source_version=self.version, confidence=self.confidence, is_estimate=True, ) def estimate_commute( distance_km: float | None, *, estimator: CommuteEstimator | None = None, ) -> CommuteEstimate | None: if distance_km is None: return None if estimator is None: estimator = ConservativeRoadEstimator() return estimator.estimate(float(distance_km))