"""Pre-bake per-handle GPS tracks for the Explore page. Reads all activity GeoJSON files for a handle, applies RDP simplification, and writes a single tracks.json for fast client-side heatmap rendering. """ from __future__ import annotations import json import time from pathlib import Path from bincio.extract.simplify import _rdp_mask _VERSION = 1 _RDP_EPSILON = 0.0001 # ~10 m on the ground _SPORT_MAP: dict[str, str] = { "cycling": "cycling", "road_cycling": "cycling", "gravel_cycling": "cycling", "mountain_biking": "cycling", "e_biking": "cycling", "indoor_cycling": "cycling", "biking": "cycling", "bike": "cycling", "ride": "cycling", "running": "running", "trail_running": "running", "treadmill_running": "running", "jogging": "running", "hiking": "hiking", "walking": "hiking", "trekking": "hiking", "mountaineering": "hiking", "skiing": "skiing", "cross_country_skiing": "skiing", "alpine_skiing": "skiing", "snowboarding": "skiing", } def _sport_to_type(sport: str | None) -> str: if not sport: return "other" return _SPORT_MAP.get(sport.lower(), "other") def bake_tracks(handle: str, data_dir: Path) -> int: """Build tracks.json for handle. Returns number of tracks included.""" acts_dir = data_dir / handle / "activities" if not acts_dir.exists(): return 0 tracks = [] for gj_path in sorted(acts_dir.glob("*.geojson")): act_id = gj_path.stem meta: dict = {} meta_path = acts_dir / f"{act_id}.json" if meta_path.exists(): try: meta = json.loads(meta_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): pass if meta.get("virtual"): continue try: gj = json.loads(gj_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): continue raw_coords = gj.get("geometry", {}).get("coordinates") or [] if len(raw_coords) < 2: continue lng_lat = [[float(c[0]), float(c[1])] for c in raw_coords if len(c) >= 2] if len(lng_lat) < 2: continue mask = _rdp_mask(lng_lat, epsilon=_RDP_EPSILON) simplified = [pt for pt, keep in zip(lng_lat, mask) if keep] if len(simplified) < 2: continue tracks.append({ "id": act_id, "date": (meta.get("started_at") or "")[:10], "type": _sport_to_type(meta.get("sport")), "name": meta.get("title") or act_id, "dist": int(meta.get("distance_m") or 0), "coords": simplified, }) tracks.sort(key=lambda t: t["date"], reverse=True) out = data_dir / handle / "tracks.json" out.write_text( json.dumps({ "v": _VERSION, "handle": handle, "generated_at": int(time.time()), "tracks": tracks, }), encoding="utf-8", ) return len(tracks)