67 lines
1.7 KiB
Python
67 lines
1.7 KiB
Python
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))
|