new changes in the api
This commit is contained in:
150
app/services/routing/gps_smoother.py
Normal file
150
app/services/routing/gps_smoother.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
GPS location smoothing — rider-api
|
||||
|
||||
Smooths noisy rider GPS pings (typical error +-5-15m, worse on poor signal,
|
||||
occasional bad-fix "jumps") using a per-rider exponential moving average
|
||||
(EMA): each new reading is blended with the running estimate so a single bad
|
||||
ping can't yank the rider's position, while the estimate still tracks real
|
||||
movement.
|
||||
|
||||
This used to be implemented as a full Kalman filter (per-coordinate process/
|
||||
measurement covariance, gain computed every update). For a "constant
|
||||
position" state model with no velocity term — which is what this is, since
|
||||
we're smoothing noisy pings, not tracking motion — the Kalman update reduces
|
||||
mathematically to an EMA once the gain reaches steady state, which happens
|
||||
within the first couple of updates. The EMA below is the same behavior with
|
||||
one constant instead of two, and no covariance bookkeeping to explain to the
|
||||
next person reading this file.
|
||||
|
||||
Only two things are actually used elsewhere in the app:
|
||||
smooth_rider_locations(riders) — per-rider EMA, stateful across calls
|
||||
smooth_order_coordinates(orders) — validates/normalises delivery coords
|
||||
(NOT smoothed — see its docstring)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Smoothing factor: how much weight a new GPS reading gets against the
|
||||
# running estimate. Lower = smoother/slower to react, higher = trusts each
|
||||
# new ping more. 0.1 matches the steady-state behavior of the Kalman filter
|
||||
# this replaced (process_noise=1e-4, measurement_noise=0.01).
|
||||
_ALPHA = 0.1
|
||||
_STALE_SECONDS = 1800.0 # reset a rider's running estimate after 30 min silence
|
||||
|
||||
|
||||
def _is_valid_coord(lat: float, lon: float) -> bool:
|
||||
try:
|
||||
lat, lon = float(lat), float(lon)
|
||||
return (
|
||||
-90.0 <= lat <= 90.0
|
||||
and -180.0 <= lon <= 180.0
|
||||
and not (lat == 0.0 and lon == 0.0)
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
class _RiderEstimate:
|
||||
"""Running EMA estimate of one rider's position."""
|
||||
|
||||
def __init__(self):
|
||||
self.lat: Optional[float] = None
|
||||
self.lon: Optional[float] = None
|
||||
self.last_updated: float = time.time()
|
||||
|
||||
def update(self, lat: float, lon: float) -> Tuple[float, float]:
|
||||
if not _is_valid_coord(lat, lon):
|
||||
return (self.lat, self.lon) if self.lat is not None else (lat, lon)
|
||||
|
||||
if time.time() - self.last_updated > _STALE_SECONDS:
|
||||
self.lat = self.lon = None # stale — start fresh
|
||||
|
||||
if self.lat is None:
|
||||
self.lat, self.lon = lat, lon
|
||||
else:
|
||||
self.lat += _ALPHA * (lat - self.lat)
|
||||
self.lon += _ALPHA * (lon - self.lon)
|
||||
|
||||
self.last_updated = time.time()
|
||||
return self.lat, self.lon
|
||||
|
||||
|
||||
_rider_estimates: Dict[str, _RiderEstimate] = {}
|
||||
|
||||
|
||||
def smooth_rider_locations(riders: list) -> list:
|
||||
"""
|
||||
Apply EMA smoothing to a list of rider dicts in-place, keyed by rider id
|
||||
(history preserved across calls via a process-level registry).
|
||||
|
||||
Reads/writes: latitude, longitude (and currentlat/currentlong if present).
|
||||
Adds: _location_smoothed = True on each processed rider.
|
||||
"""
|
||||
for rider in riders:
|
||||
try:
|
||||
rider_id = str(
|
||||
rider.get("userid") or rider.get("riderid") or
|
||||
rider.get("id") or "unknown"
|
||||
)
|
||||
raw_lat = float(rider.get("latitude") or rider.get("currentlat") or 0)
|
||||
raw_lon = float(rider.get("longitude") or rider.get("currentlong") or 0)
|
||||
if raw_lat == 0.0 and raw_lon == 0.0:
|
||||
continue
|
||||
|
||||
estimate = _rider_estimates.setdefault(rider_id, _RiderEstimate())
|
||||
smooth_lat, smooth_lon = estimate.update(raw_lat, raw_lon)
|
||||
|
||||
# Cast back to string for Go compatibility
|
||||
s_lat, s_lon = str(round(smooth_lat, 8)), str(round(smooth_lon, 8))
|
||||
rider["latitude"] = s_lat
|
||||
rider["longitude"] = s_lon
|
||||
if "currentlat" in rider:
|
||||
rider["currentlat"] = s_lat
|
||||
if "currentlong" in rider:
|
||||
rider["currentlong"] = s_lon
|
||||
rider["_location_smoothed"] = True
|
||||
except Exception as e:
|
||||
logger.debug(f"Rider location smoothing skipped: {e}")
|
||||
return riders
|
||||
|
||||
|
||||
def smooth_order_coordinates(orders: list) -> list:
|
||||
"""
|
||||
Validate and lightly normalise delivery coordinates in a list of order dicts.
|
||||
|
||||
DESIGN NOTE — why these are NOT smoothed:
|
||||
Smoothing blends successive measurements from the same source over time.
|
||||
Delivery coordinates are a single static point (one measurement) — there
|
||||
is nothing to blend. Per-customer GPS accuracy is handled upstream by the
|
||||
FAISS coordinate store (verified historical rider-confirmed delivery
|
||||
points). This function only normalises the coordinate fields to floats
|
||||
so downstream code never sees raw strings or None values.
|
||||
|
||||
Modifies orders in-place. Returns the same list.
|
||||
"""
|
||||
for order in orders:
|
||||
try:
|
||||
dlat_raw = order.get("deliverylat") or order.get("droplat")
|
||||
dlon_raw = order.get("deliverylong") or order.get("droplon")
|
||||
if dlat_raw is None or dlon_raw is None:
|
||||
continue
|
||||
dlat = float(dlat_raw)
|
||||
dlon = float(dlon_raw)
|
||||
if not _is_valid_coord(dlat, dlon):
|
||||
continue
|
||||
# Normalise to string (Go service expects string coordinates)
|
||||
s_lat = str(round(dlat, 8))
|
||||
s_lon = str(round(dlon, 8))
|
||||
order["deliverylat"] = s_lat
|
||||
order["deliverylong"] = s_lon
|
||||
if "droplat" in order:
|
||||
order["droplat"] = s_lat
|
||||
if "droplon" in order:
|
||||
order["droplon"] = s_lon
|
||||
except Exception as e:
|
||||
logger.debug(f"Coordinate normalisation skipped: {e}")
|
||||
return orders
|
||||
Reference in New Issue
Block a user