NITISHRG15102007 commited on
Commit
d2e2ba0
·
verified ·
1 Parent(s): a484e09

sync: push from tools/sync_space_to_hub.py (no artifacts/)

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ web/dist/maps/bangalore_roads_full.geojson filter=lfs diff=lfs merge=lfs -text
37
+ web/public/maps/bangalore_roads_full.geojson filter=lfs diff=lfs merge=lfs -text
docs/superpowers/specs/2026-04-26-bangalore-osm-extract-design.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Bangalore OSM Offline Extract (Option B) — Design
2
+
3
+ ## Goal
4
+ Ship an **offline, pre-baked Bangalore road extract** that includes **all drivable roads** (not just arterials) while still rendering and routing smoothly on **HF Spaces CPU** with **100–500 vehicles**.
5
+
6
+ This is the “Option B” choice: **dense realism** for hackathon wow-factor, with **smart pruning** to keep performance predictable.
7
+
8
+ ## Non-goals
9
+ - No live Overpass/Mapbox calls at runtime (HF Spaces reliability).
10
+ - No full India/KA extract (too large and slow).
11
+ - No pedestrian/cycle/footpath routing (keep to drivable network).
12
+
13
+ ## Source of truth (repo files)
14
+ Committed assets under `web/public/maps/`:
15
+ - `bangalore_roads_full.geojson` — offline OSM extract (drivable ways only)
16
+ - `bangalore_roads_graph.json` — compact weighted graph for server routing (nodes/edges)
17
+ - (optional) `bangalore_roads_render.json` — simplified path list for deck.gl rendering if GeoJSON is too heavy
18
+
19
+ Runtime behavior:
20
+ - Frontend renders roads + routes from the offline files (no external APIs).
21
+ - Server loads `bangalore_roads_graph.json` once and returns road-following polylines.
22
+
23
+ ## Extract specification
24
+ ### Geographic coverage
25
+ - Bounding polygon: Bangalore Urban footprint (preferred) or bounding box around:
26
+ - lat: 12.75–13.18
27
+ - lng: 77.35–77.85
28
+
29
+ ### Road inclusion (drivable only)
30
+ Include OSM `highway` types:
31
+ - `motorway`, `trunk`, `primary`, `secondary`, `tertiary`, `residential`, `service`
32
+
33
+ Exclude:
34
+ - `footway`, `path`, `cycleway`, `steps`, `pedestrian`, `corridor`, `track` (unless explicitly needed later)
35
+
36
+ ### Pruning rules (performance)
37
+ - Drop disconnected components smaller than a threshold (e.g. < 200 edges).
38
+ - Optional geometry simplification (Douglas–Peucker) with a small tolerance to reduce vertex count **without straightening key curves**.
39
+ - Hard caps (enforced by tooling):
40
+ - `bangalore_roads_full.geojson` \(\le\) **60 MB**
41
+ - `bangalore_roads_graph.json` \(\le\) **25 MB**
42
+ - If caps are exceeded, generate `bangalore_roads_render.json` (simplified paths) and the frontend must prefer it for rendering.
43
+
44
+ ### Reproducibility (non-negotiable)
45
+ - The build must be deterministic given the same input:
46
+ - stable sorting of nodes/edges
47
+ - fixed float rounding for coordinates and weights
48
+ - JSON written with stable key ordering
49
+ - Record build metadata alongside outputs:
50
+ - input file SHA256
51
+ - bbox/polygon identifier
52
+ - snap precision, simplify tolerance, prune threshold
53
+ - tool versions
54
+ - output counts (nodes, edges, vertices)
55
+ - saved as `web/public/maps/bangalore_roads_build_meta.json`
56
+
57
+ ## Graph build specification
58
+ Input: `bangalore_roads_full.geojson`
59
+
60
+ Outputs:
61
+ - Nodes: snapped intersections/endpoints (coordinate snap, e.g. 5 decimals)
62
+ - Edges: **segment-to-segment** links that preserve curvature
63
+ - Edge weights:
64
+ - `dist_m` via haversine
65
+ - `speed_kmh` by road type (config table)
66
+ - `travel_s = dist_m / speed_mps`
67
+
68
+ Routing:
69
+ - Server shortest path by `travel_s`
70
+ - Polyline returned as `[lat, lng]` list with **>2 points** for typical trips
71
+
72
+ ### Graph invariants (must hold)
73
+ - Every edge references existing node ids.
74
+ - Every node has finite `lat/lng` within the selected polygon/bbox.
75
+ - Graph is explicitly **undirected** (for v1); neighbor relation must be symmetric.
76
+ - Largest connected component contains **\(\ge 95\%\)** of nodes after pruning (or fail the build).
77
+
78
+ ## Training integration
79
+ Training environment uses the same prebuilt graph:
80
+ - Action schema: `CURRENT_NODE`, `NEXT_NODE` (connected neighbor only)
81
+ - Anti-cheat:
82
+ - invalid current node → terminate with negative reward
83
+ - non-neighbor next node (“teleportation”) → terminate with negative reward
84
+ - ignore any agent attempts to redefine state; env is source of truth
85
+ - episode caps → stop deterministically
86
+ - Reward:
87
+ - time penalty (travel_s)
88
+ - distance-to-target shaping
89
+ - strong penalties for anti-cheat flags
90
+ - log reward breakdown every step (time, shaping, arrive, cheat)
91
+
92
+ ## Acceptance checks
93
+ 1. **Coverage**: drivable highways included: motorway→service; pedestrian-only ways excluded.
94
+ 2. **Routing realism**: median polyline length **\(\ge 20\)** points over a fixed OD benchmark set (e.g. 25 random station pairs).
95
+ 3. **Performance (HF Spaces CPU)**:
96
+ - graph load + router init \(\le\) **2.0s**
97
+ - frontend first interactive render \(\le\) **6.0s**
98
+ - 100 vehicles: no long-frame freezes (no single frame > 250ms during a 30s scripted run)
99
+ 4. **Determinism**: re-running build on the same input produces identical output SHA256 for `bangalore_roads_graph.json`.
100
+ 5. **No runtime fetch**: demo uses only committed road files; external calls limited to basemap tiles (or none if we later switch to offline tiles).
101
+
102
+ ## Rollout plan
103
+ 1. Add a one-time **extract tool** that takes a downloaded OSM extract (GeoJSON) and produces the committed outputs.
104
+ 2. Replace current `bangalore_roads_demo.geojson` with the full extract for rendering.
105
+ 3. Regenerate `bangalore_roads_graph.json` and ensure server/router loads it.
106
+ 4. Update training notebook to reflect the final graph size + constraints (already aligned).
107
+
server/road_router.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import json
 
4
  from dataclasses import dataclass
5
  from math import asin, cos, radians, sin, sqrt
6
  from pathlib import Path
@@ -18,14 +19,49 @@ def haversine_m(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
18
  return r * c
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  @dataclass(frozen=True)
22
  class RoadRouter:
23
  g: nx.Graph
24
  nodes: list[tuple[float, float]] # (lat,lng) by node id
 
25
 
26
  @classmethod
27
  def load(cls, path: Path) -> "RoadRouter":
28
- obj = json.loads(path.read_text(encoding="utf-8"))
 
 
 
 
29
  nodes_in = obj.get("nodes", [])
30
  edges_in = obj.get("edges", [])
31
  if not isinstance(nodes_in, list) or not isinstance(edges_in, list):
@@ -38,10 +74,12 @@ class RoadRouter:
38
  g = nx.Graph()
39
  for i, (lat, lng) in enumerate(nodes):
40
  g.add_node(i, lat=lat, lng=lng)
 
41
  for e in edges_in:
42
  a = int(e["a"])
43
  b = int(e["b"])
44
  w = float(e.get("travel_s") or 0.0)
 
45
  if a == b:
46
  continue
47
  # keep smallest weight if duplicates
@@ -50,8 +88,14 @@ class RoadRouter:
50
  g.edges[a, b]["weight"] = w
51
  else:
52
  g.add_edge(a, b, weight=w)
 
 
 
 
 
 
53
 
54
- return cls(g=g, nodes=nodes)
55
 
56
  def nearest_node(self, *, lat: float, lng: float) -> int:
57
  best = 0
@@ -70,7 +114,20 @@ class RoadRouter:
70
  path = nx.shortest_path(self.g, a, b, weight="weight") # type: ignore[arg-type]
71
  except Exception:
72
  return None
73
- return [[self.nodes[i][0], self.nodes[i][1]] for i in path]
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
 
76
  _ROUTER: RoadRouter | None = None
@@ -81,7 +138,9 @@ def get_router() -> RoadRouter:
81
  if _ROUTER is not None:
82
  return _ROUTER
83
  root = Path(__file__).resolve().parents[1]
84
- p = root / "web" / "public" / "maps" / "bangalore_roads_graph.json"
 
 
85
  _ROUTER = RoadRouter.load(p)
86
  return _ROUTER
87
 
 
1
  from __future__ import annotations
2
 
3
  import json
4
+ import gzip
5
  from dataclasses import dataclass
6
  from math import asin, cos, radians, sin, sqrt
7
  from pathlib import Path
 
19
  return r * c
20
 
21
 
22
+ def decode_polyline_latlng(s: str, *, precision: int = 5) -> list[list[float]]:
23
+ if not s:
24
+ return []
25
+ factor = 10**precision
26
+ idx = 0
27
+ lat = 0
28
+ lng = 0
29
+ out: list[list[float]] = []
30
+
31
+ def _next() -> int:
32
+ nonlocal idx
33
+ shift = 0
34
+ result = 0
35
+ while True:
36
+ b = ord(s[idx]) - 63
37
+ idx += 1
38
+ result |= (b & 0x1F) << shift
39
+ shift += 5
40
+ if b < 0x20:
41
+ break
42
+ d = ~(result >> 1) if (result & 1) else (result >> 1)
43
+ return int(d)
44
+
45
+ while idx < len(s):
46
+ lat += _next()
47
+ lng += _next()
48
+ out.append([lat / factor, lng / factor])
49
+ return out
50
+
51
+
52
  @dataclass(frozen=True)
53
  class RoadRouter:
54
  g: nx.Graph
55
  nodes: list[tuple[float, float]] # (lat,lng) by node id
56
+ edge_geom: dict[tuple[int, int], list[list[float]]]
57
 
58
  @classmethod
59
  def load(cls, path: Path) -> "RoadRouter":
60
+ if str(path).endswith(".gz"):
61
+ with gzip.open(path, "rb") as f:
62
+ obj = json.loads(f.read().decode("utf-8"))
63
+ else:
64
+ obj = json.loads(path.read_text(encoding="utf-8"))
65
  nodes_in = obj.get("nodes", [])
66
  edges_in = obj.get("edges", [])
67
  if not isinstance(nodes_in, list) or not isinstance(edges_in, list):
 
74
  g = nx.Graph()
75
  for i, (lat, lng) in enumerate(nodes):
76
  g.add_node(i, lat=lat, lng=lng)
77
+ edge_geom: dict[tuple[int, int], list[list[float]]] = {}
78
  for e in edges_in:
79
  a = int(e["a"])
80
  b = int(e["b"])
81
  w = float(e.get("travel_s") or 0.0)
82
+ geom_poly = e.get("geom_poly")
83
  if a == b:
84
  continue
85
  # keep smallest weight if duplicates
 
88
  g.edges[a, b]["weight"] = w
89
  else:
90
  g.add_edge(a, b, weight=w)
91
+ if isinstance(geom_poly, str) and geom_poly:
92
+ precision = int(obj.get("meta", {}).get("snap_decimals", 5) or 5)
93
+ geom = decode_polyline_latlng(geom_poly, precision=precision)
94
+ if len(geom) >= 2:
95
+ edge_geom[(a, b)] = geom
96
+ edge_geom[(b, a)] = list(reversed(geom))
97
 
98
+ return cls(g=g, nodes=nodes, edge_geom=edge_geom)
99
 
100
  def nearest_node(self, *, lat: float, lng: float) -> int:
101
  best = 0
 
114
  path = nx.shortest_path(self.g, a, b, weight="weight") # type: ignore[arg-type]
115
  except Exception:
116
  return None
117
+ poly: list[list[float]] = []
118
+ for u, v in zip(path, path[1:]):
119
+ seg = self.edge_geom.get((int(u), int(v)))
120
+ if seg and len(seg) >= 2:
121
+ if poly:
122
+ poly.extend(seg[1:])
123
+ else:
124
+ poly.extend(seg)
125
+ else:
126
+ # fallback: straight segment
127
+ if not poly:
128
+ poly.append([self.nodes[int(u)][0], self.nodes[int(u)][1]])
129
+ poly.append([self.nodes[int(v)][0], self.nodes[int(v)][1]])
130
+ return poly
131
 
132
 
133
  _ROUTER: RoadRouter | None = None
 
138
  if _ROUTER is not None:
139
  return _ROUTER
140
  root = Path(__file__).resolve().parents[1]
141
+ # Prefer gz (smaller repo + faster downloads on HF).
142
+ p_gz = root / "web" / "public" / "maps" / "bangalore_roads_graph.json.gz"
143
+ p = p_gz if p_gz.exists() else (root / "web" / "public" / "maps" / "bangalore_roads_graph.json")
144
  _ROUTER = RoadRouter.load(p)
145
  return _ROUTER
146
 
tools/build_road_graph.py CHANGED
@@ -2,9 +2,14 @@ from __future__ import annotations
2
 
3
  import argparse
4
  import json
 
 
5
  from dataclasses import dataclass
6
  from math import asin, cos, radians, sin, sqrt
7
  from pathlib import Path
 
 
 
8
 
9
 
10
  ROOT = Path(__file__).resolve().parents[1]
@@ -19,6 +24,39 @@ def haversine_m(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
19
  return r * c
20
 
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  SPEED_KMH = {
23
  "motorway": 65,
24
  "trunk": 60,
@@ -44,28 +82,73 @@ def snap(lat: float, lng: float, *, decimals: int) -> tuple[float, float]:
44
  return (round(lat, decimals), round(lng, decimals))
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def main() -> int:
48
  ap = argparse.ArgumentParser()
49
- ap.add_argument("--in", dest="inp", default="web/public/maps/bangalore_roads_demo.geojson")
50
  ap.add_argument("--out", dest="out", default="web/public/maps/bangalore_roads_graph.json")
 
51
  ap.add_argument("--snap-decimals", type=int, default=5, help="Coordinate snapping for intersection merging")
 
 
 
52
  args = ap.parse_args()
53
 
54
  inp = (ROOT / args.inp).resolve()
55
  out = (ROOT / args.out).resolve()
 
56
  out.parent.mkdir(parents=True, exist_ok=True)
57
 
58
- gj = json.loads(inp.read_text(encoding="utf-8"))
 
59
  feats = gj.get("features", [])
60
  if not isinstance(feats, list):
61
  raise SystemExit("invalid geojson: features[] missing")
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  node_id: dict[tuple[float, float], int] = {}
64
  nodes: list[Node] = []
65
- edges: list[dict] = []
66
 
67
- def get_node(lat: float, lng: float) -> int:
68
- k = snap(lat, lng, decimals=args.snap_decimals)
69
  if k in node_id:
70
  return node_id[k]
71
  nid = len(nodes)
@@ -73,6 +156,9 @@ def main() -> int:
73
  nodes.append(Node(lat=float(k[0]), lng=float(k[1])))
74
  return nid
75
 
 
 
 
76
  for f in feats:
77
  if not isinstance(f, dict):
78
  continue
@@ -80,48 +166,163 @@ def main() -> int:
80
  if not isinstance(geom, dict) or geom.get("type") != "LineString":
81
  continue
82
  coords = geom.get("coordinates")
83
- if not isinstance(coords, list) or len(coords) < 2:
 
84
  continue
85
  props = f.get("properties") or {}
86
  highway = str((props.get("highway") if isinstance(props, dict) else "") or "")
87
  name = str((props.get("name") if isinstance(props, dict) else "") or "")
88
 
89
- # Coordinates are [lng,lat] in GeoJSON.
90
- pts = [(float(c[1]), float(c[0])) for c in coords if isinstance(c, list) and len(c) >= 2]
91
- if len(pts) < 2:
92
- continue
 
 
 
 
93
 
94
- # Build segment edges between consecutive points. This preserves curvature.
95
- for (lat1, lng1), (lat2, lng2) in zip(pts, pts[1:]):
96
- a = get_node(lat1, lng1)
97
- b = get_node(lat2, lng2)
98
- if a == b:
99
- continue
100
- dist_m = haversine_m(lat1, lng1, lat2, lng2)
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  v_kmh = speed_kmh(highway)
102
  travel_s = dist_m / max(1e-3, (v_kmh * 1000.0 / 3600.0))
 
103
  edges.append(
104
  {
105
- "a": a,
106
- "b": b,
107
  "highway": highway,
108
  "name": name,
109
- "dist_m": dist_m,
110
- "travel_s": travel_s,
 
 
 
 
 
 
 
 
111
  }
112
  )
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  payload = {
115
  "meta": {
116
  "source": str(inp.relative_to(ROOT)).replace("\\", "/"),
117
- "snap_decimals": args.snap_decimals,
 
118
  "speed_kmh": SPEED_KMH,
 
119
  },
120
- "nodes": [{"lat": n.lat, "lng": n.lng} for n in nodes],
121
  "edges": edges,
122
  }
123
- out.write_text(json.dumps(payload), encoding="utf-8")
124
- print(f"Wrote {out} nodes={len(nodes)} edges={len(edges)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  return 0
126
 
127
 
 
2
 
3
  import argparse
4
  import json
5
+ import hashlib
6
+ import gzip
7
  from dataclasses import dataclass
8
  from math import asin, cos, radians, sin, sqrt
9
  from pathlib import Path
10
+ from typing import Any
11
+
12
+ import networkx as nx
13
 
14
 
15
  ROOT = Path(__file__).resolve().parents[1]
 
24
  return r * c
25
 
26
 
27
+ def _encode_signed(num: int) -> str:
28
+ num = num << 1
29
+ if num < 0:
30
+ num = ~num
31
+ out = ""
32
+ while num >= 0x20:
33
+ out += chr((0x20 | (num & 0x1F)) + 63)
34
+ num >>= 5
35
+ out += chr(num + 63)
36
+ return out
37
+
38
+
39
+ def encode_polyline_latlng(points: list[list[float]], *, precision: int = 5) -> str:
40
+ """
41
+ Google polyline encoding for [lat,lng] points.
42
+ Stored as a compact ASCII string to shrink graph artifacts.
43
+ """
44
+ if not points:
45
+ return ""
46
+ factor = 10**precision
47
+ prev_lat = 0
48
+ prev_lng = 0
49
+ out = ""
50
+ for lat, lng in points:
51
+ ilat = int(round(float(lat) * factor))
52
+ ilng = int(round(float(lng) * factor))
53
+ out += _encode_signed(ilat - prev_lat)
54
+ out += _encode_signed(ilng - prev_lng)
55
+ prev_lat = ilat
56
+ prev_lng = ilng
57
+ return out
58
+
59
+
60
  SPEED_KMH = {
61
  "motorway": 65,
62
  "trunk": 60,
 
82
  return (round(lat, decimals), round(lng, decimals))
83
 
84
 
85
+ def _coords_latlng_from_geojson_line(coords: Any) -> list[tuple[float, float]]:
86
+ if not isinstance(coords, list) or len(coords) < 2:
87
+ return []
88
+ out: list[tuple[float, float]] = []
89
+ for c in coords:
90
+ if not isinstance(c, list) or len(c) < 2:
91
+ continue
92
+ lng = float(c[0])
93
+ lat = float(c[1])
94
+ out.append((lat, lng))
95
+ return out
96
+
97
+
98
  def main() -> int:
99
  ap = argparse.ArgumentParser()
100
+ ap.add_argument("--in", dest="inp", default="web/public/maps/bangalore_roads_full.geojson")
101
  ap.add_argument("--out", dest="out", default="web/public/maps/bangalore_roads_graph.json")
102
+ ap.add_argument("--meta-out", dest="meta_out", default="web/public/maps/bangalore_roads_build_meta.json")
103
  ap.add_argument("--snap-decimals", type=int, default=5, help="Coordinate snapping for intersection merging")
104
+ ap.add_argument("--geom-every", type=int, default=3, help="keep every Nth point in edge geometry (>=1)")
105
+ ap.add_argument("--keep-only-largest-component", action="store_true", default=True)
106
+ ap.add_argument("--gzip", action="store_true", help="write graph output as .gz (recommended)")
107
  args = ap.parse_args()
108
 
109
  inp = (ROOT / args.inp).resolve()
110
  out = (ROOT / args.out).resolve()
111
+ meta_out = (ROOT / args.meta_out).resolve()
112
  out.parent.mkdir(parents=True, exist_ok=True)
113
 
114
+ raw_text = inp.read_text(encoding="utf-8")
115
+ gj = json.loads(raw_text)
116
  feats = gj.get("features", [])
117
  if not isinstance(feats, list):
118
  raise SystemExit("invalid geojson: features[] missing")
119
 
120
+ snap_decimals = int(args.snap_decimals)
121
+ geom_every = max(1, int(args.geom_every))
122
+
123
+ # Pass 1: build point adjacency over snapped coordinates.
124
+ adj: dict[tuple[float, float], set[tuple[float, float]]] = {}
125
+
126
+ def add_neighbor(a: tuple[float, float], b: tuple[float, float]):
127
+ if a == b:
128
+ return
129
+ adj.setdefault(a, set()).add(b)
130
+ adj.setdefault(b, set()).add(a)
131
+
132
+ for f in feats:
133
+ if not isinstance(f, dict):
134
+ continue
135
+ geom = f.get("geometry") or {}
136
+ if not isinstance(geom, dict) or geom.get("type") != "LineString":
137
+ continue
138
+ pts = _coords_latlng_from_geojson_line(geom.get("coordinates"))
139
+ if len(pts) < 2:
140
+ continue
141
+ snapped = [snap(lat, lng, decimals=snap_decimals) for (lat, lng) in pts]
142
+ for a, b in zip(snapped, snapped[1:]):
143
+ add_neighbor(a, b)
144
+
145
+ # Intersections/endpoints are nodes where degree != 2.
146
+ is_node: dict[tuple[float, float], bool] = {k: (len(v) != 2) for k, v in adj.items()}
147
+
148
  node_id: dict[tuple[float, float], int] = {}
149
  nodes: list[Node] = []
 
150
 
151
+ def get_node(k: tuple[float, float]) -> int:
 
152
  if k in node_id:
153
  return node_id[k]
154
  nid = len(nodes)
 
156
  nodes.append(Node(lat=float(k[0]), lng=float(k[1])))
157
  return nid
158
 
159
+ edges: list[dict[str, Any]] = []
160
+
161
+ # Pass 2: for each way, contract degree-2 chains into intersection-to-intersection edges
162
  for f in feats:
163
  if not isinstance(f, dict):
164
  continue
 
166
  if not isinstance(geom, dict) or geom.get("type") != "LineString":
167
  continue
168
  coords = geom.get("coordinates")
169
+ pts = _coords_latlng_from_geojson_line(coords)
170
+ if len(pts) < 2:
171
  continue
172
  props = f.get("properties") or {}
173
  highway = str((props.get("highway") if isinstance(props, dict) else "") or "")
174
  name = str((props.get("name") if isinstance(props, dict) else "") or "")
175
 
176
+ snapped = [snap(lat, lng, decimals=snap_decimals) for (lat, lng) in pts]
177
+ # Ensure endpoints are treated as nodes.
178
+ if snapped:
179
+ is_node[snapped[0]] = True
180
+ is_node[snapped[-1]] = True
181
+
182
+ last_node_k: tuple[float, float] | None = None
183
+ seg_geom: list[list[float]] = [] # [[lat,lng],...]
184
 
185
+ def flush(to_k: tuple[float, float]):
186
+ nonlocal last_node_k, seg_geom
187
+ if last_node_k is None:
188
+ last_node_k = to_k
189
+ seg_geom = [[float(to_k[0]), float(to_k[1])]]
190
+ return
191
+ if to_k == last_node_k:
192
+ return
193
+ if len(seg_geom) < 2:
194
+ seg_geom.append([float(to_k[0]), float(to_k[1])])
195
+ else:
196
+ seg_geom[-1] = [float(to_k[0]), float(to_k[1])]
197
+
198
+ a_id = get_node(last_node_k)
199
+ b_id = get_node(to_k)
200
+
201
+ # Distance along the segment geometry
202
+ dist_m = 0.0
203
+ for (la1, lo1), (la2, lo2) in zip(seg_geom, seg_geom[1:]):
204
+ dist_m += haversine_m(float(la1), float(lo1), float(la2), float(lo2))
205
  v_kmh = speed_kmh(highway)
206
  travel_s = dist_m / max(1e-3, (v_kmh * 1000.0 / 3600.0))
207
+
208
  edges.append(
209
  {
210
+ "a": int(a_id),
211
+ "b": int(b_id),
212
  "highway": highway,
213
  "name": name,
214
+ "dist_m": round(dist_m, 3),
215
+ "travel_s": round(travel_s, 4),
216
+ "geom_poly": encode_polyline_latlng(
217
+ [
218
+ [round(float(p[0]), snap_decimals), round(float(p[1]), snap_decimals)]
219
+ for idx, p in enumerate(seg_geom)
220
+ if geom_every <= 1 or idx in (0, len(seg_geom) - 1) or (idx % geom_every) == 0
221
+ ],
222
+ precision=snap_decimals,
223
+ ),
224
  }
225
  )
226
 
227
+ last_node_k = to_k
228
+ seg_geom = [[float(to_k[0]), float(to_k[1])]]
229
+
230
+ # Build contracted segments
231
+ if not snapped:
232
+ continue
233
+ # Start at first point
234
+ last_node_k = snapped[0]
235
+ seg_geom = [[float(snapped[0][0]), float(snapped[0][1])]]
236
+ for k in snapped[1:]:
237
+ seg_geom.append([float(k[0]), float(k[1])])
238
+ if is_node.get(k, False):
239
+ flush(k)
240
+
241
  payload = {
242
  "meta": {
243
  "source": str(inp.relative_to(ROOT)).replace("\\", "/"),
244
+ "snap_decimals": snap_decimals,
245
+ "geom_every": geom_every,
246
  "speed_kmh": SPEED_KMH,
247
+ "keep_only_largest_component": True,
248
  },
249
+ "nodes": [{"lat": round(n.lat, snap_decimals), "lng": round(n.lng, snap_decimals)} for n in nodes],
250
  "edges": edges,
251
  }
252
+
253
+ # Keep only the largest connected component (by node count) to satisfy routing coverage.
254
+ g3 = nx.Graph()
255
+ g3.add_nodes_from(range(len(nodes)))
256
+ for e in edges:
257
+ g3.add_edge(int(e["a"]), int(e["b"]))
258
+ comps3 = list(nx.connected_components(g3))
259
+ keep_nodes = max(comps3, key=lambda c: len(c)) if comps3 else set()
260
+ keep_nodes_set = set(int(x) for x in keep_nodes)
261
+
262
+ # Remap nodes to a compact id space.
263
+ id_map: dict[int, int] = {}
264
+ new_nodes: list[dict[str, float]] = []
265
+ for old_id in sorted(keep_nodes_set):
266
+ id_map[old_id] = len(new_nodes)
267
+ n = nodes[old_id]
268
+ new_nodes.append({"lat": round(float(n.lat), snap_decimals), "lng": round(float(n.lng), snap_decimals)})
269
+
270
+ new_edges: list[dict[str, Any]] = []
271
+ for e in edges:
272
+ a = int(e["a"])
273
+ b = int(e["b"])
274
+ if a not in id_map or b not in id_map:
275
+ continue
276
+ ee = dict(e)
277
+ ee["a"] = id_map[a]
278
+ ee["b"] = id_map[b]
279
+ new_edges.append(ee)
280
+
281
+ new_edges.sort(key=lambda e: (int(e["a"]), int(e["b"]), str(e.get("highway") or ""), str(e.get("name") or "")))
282
+
283
+ payload["nodes"] = new_nodes
284
+ payload["edges"] = new_edges
285
+
286
+ out_text = json.dumps(payload, sort_keys=True, separators=(",", ":"))
287
+ if args.gzip:
288
+ gz_path = Path(str(out) + ".gz")
289
+ with gzip.open(gz_path, "wb") as f:
290
+ f.write(out_text.encode("utf-8"))
291
+ out_written = gz_path
292
+ out_bytes = gz_path.read_bytes()
293
+ sha_out = hashlib.sha256(out_bytes).hexdigest()
294
+ else:
295
+ out.write_text(out_text, encoding="utf-8")
296
+ out_written = out
297
+ sha_out = hashlib.sha256(out_text.encode("utf-8")).hexdigest()
298
+
299
+ sha_in = hashlib.sha256(raw_text.encode("utf-8")).hexdigest()
300
+ # Largest component coverage is 1.0 by construction (remapped), but report ratios vs raw.
301
+ raw_node_count = len(nodes)
302
+ kept_node_count = len(new_nodes)
303
+ coverage = (kept_node_count / max(1, raw_node_count)) if raw_node_count else 0.0
304
+
305
+ meta = {
306
+ "input": {"path": str(inp.relative_to(ROOT)).replace("\\", "/"), "sha256": sha_in},
307
+ "output": {"path": str(out_written.relative_to(ROOT)).replace("\\", "/"), "sha256": sha_out},
308
+ "params": {
309
+ "snap_decimals": snap_decimals,
310
+ "geom_every": geom_every,
311
+ "keep_only_largest_component": True,
312
+ "speed_kmh": SPEED_KMH,
313
+ },
314
+ "counts": {
315
+ "features": len(feats),
316
+ "nodes_raw": raw_node_count,
317
+ "nodes_kept": kept_node_count,
318
+ "edges": len(new_edges),
319
+ "kept_node_ratio": round(float(coverage), 6),
320
+ },
321
+ }
322
+ meta_out.write_text(json.dumps(meta, indent=2, sort_keys=True), encoding="utf-8")
323
+
324
+ print(f"Wrote {out_written} nodes={kept_node_count} edges={len(new_edges)} kept_ratio={coverage:.3f}")
325
+ print(f"Wrote {meta_out}")
326
  return 0
327
 
328
 
tools/build_roads_render.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+
11
+
12
+ KEEP_HIGHWAYS_RENDER = {
13
+ "motorway",
14
+ "trunk",
15
+ "primary",
16
+ "secondary",
17
+ "tertiary",
18
+ "residential",
19
+ # drop "service" by default to reduce clutter/size
20
+ }
21
+
22
+
23
+ def main() -> int:
24
+ ap = argparse.ArgumentParser()
25
+ ap.add_argument("--in", dest="inp", default="web/public/maps/bangalore_roads_full.geojson")
26
+ ap.add_argument("--out", dest="out", default="web/public/maps/bangalore_roads_render.json")
27
+ ap.add_argument("--every", type=int, default=4, help="keep every Nth point in each linestring (>=1)")
28
+ ap.add_argument("--max-features", type=int, default=120_000)
29
+ args = ap.parse_args()
30
+
31
+ inp = (ROOT / args.inp).resolve()
32
+ out = (ROOT / args.out).resolve()
33
+ out.parent.mkdir(parents=True, exist_ok=True)
34
+
35
+ obj = json.loads(inp.read_text(encoding="utf-8"))
36
+ feats = obj.get("features", [])
37
+ if not isinstance(feats, list):
38
+ raise SystemExit("invalid geojson: missing features")
39
+
40
+ every = max(1, int(args.every))
41
+ max_features = int(args.max_features)
42
+
43
+ rows: list[dict[str, Any]] = []
44
+ for f in feats:
45
+ if len(rows) >= max_features:
46
+ break
47
+ if not isinstance(f, dict):
48
+ continue
49
+ geom = f.get("geometry") or {}
50
+ if not isinstance(geom, dict) or geom.get("type") != "LineString":
51
+ continue
52
+ props = f.get("properties") or {}
53
+ hw = str((props.get("highway") if isinstance(props, dict) else "") or "")
54
+ if hw and hw not in KEEP_HIGHWAYS_RENDER:
55
+ continue
56
+ coords = geom.get("coordinates")
57
+ if not isinstance(coords, list) or len(coords) < 2:
58
+ continue
59
+ out_coords = []
60
+ for i, c in enumerate(coords):
61
+ if not isinstance(c, list) or len(c) < 2:
62
+ continue
63
+ if every > 1 and (i % every) != 0 and i not in (0, len(coords) - 1):
64
+ continue
65
+ out_coords.append([float(c[0]), float(c[1])])
66
+ if len(out_coords) < 2:
67
+ continue
68
+ rows.append({"highway": hw, "path": out_coords})
69
+
70
+ out.write_text(json.dumps(rows, separators=(",", ":")), encoding="utf-8")
71
+ print(f"Wrote {out} paths={len(rows)} every={every}")
72
+ return 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(main())
77
+
tools/fetch_bangalore_roads_overpass.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import time
6
+ import urllib.parse
7
+ import urllib.request
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+
14
+
15
+ DEFAULT_HIGHWAYS = [
16
+ "motorway",
17
+ "trunk",
18
+ "primary",
19
+ "secondary",
20
+ "tertiary",
21
+ "residential",
22
+ "service",
23
+ ]
24
+
25
+
26
+ def _chunk(xs: list[str], n: int) -> list[list[str]]:
27
+ out: list[list[str]] = []
28
+ for i in range(0, len(xs), n):
29
+ out.append(xs[i : i + n])
30
+ return out
31
+
32
+
33
+ def _overpass_query(bbox: tuple[float, float, float, float], highways: list[str]) -> str:
34
+ south, west, north, east = bbox
35
+ # Use `out geom` so each way includes geometry points (no extra node fetch).
36
+ # Split the highway filter into OR clauses to avoid huge regexes.
37
+ clauses = "".join([f'way["highway"="{h}"]({south},{west},{north},{east});' for h in highways])
38
+ return f"[out:json][timeout:180];({clauses});out geom;"
39
+
40
+
41
+ def _tile_bbox(bbox: tuple[float, float, float, float], tiles: int) -> list[tuple[float, float, float, float]]:
42
+ if tiles <= 1:
43
+ return [bbox]
44
+ south, west, north, east = bbox
45
+ lat_step = (north - south) / tiles
46
+ lng_step = (east - west) / tiles
47
+ out: list[tuple[float, float, float, float]] = []
48
+ for i in range(tiles):
49
+ for j in range(tiles):
50
+ s = south + lat_step * i
51
+ n = south + lat_step * (i + 1)
52
+ w = west + lng_step * j
53
+ e = west + lng_step * (j + 1)
54
+ out.append((s, w, n, e))
55
+ return out
56
+
57
+
58
+ def _http_post(url: str, data: dict[str, str], *, retries: int = 3) -> bytes:
59
+ body = urllib.parse.urlencode(data).encode("utf-8")
60
+ req = urllib.request.Request(url, data=body, method="POST")
61
+ req.add_header("content-type", "application/x-www-form-urlencoded")
62
+ req.add_header("accept", "application/json,text/plain,*/*")
63
+ req.add_header(
64
+ "user-agent",
65
+ "EV-Grid-Oracle/1.0 (offline-extract; contact: hackathon-demo)",
66
+ )
67
+ last_err: Exception | None = None
68
+ for attempt in range(1, retries + 1):
69
+ try:
70
+ with urllib.request.urlopen(req, timeout=240) as r:
71
+ return r.read()
72
+ except Exception as e: # noqa: BLE001
73
+ last_err = e
74
+ if attempt >= retries:
75
+ raise
76
+ time.sleep(1.25 * attempt)
77
+ raise RuntimeError("overpass request failed") from last_err
78
+
79
+
80
+ def _to_geojson(overpass_json: dict[str, Any], *, simplify_every: int) -> dict[str, Any]:
81
+ feats: list[dict[str, Any]] = []
82
+ for el in overpass_json.get("elements", []):
83
+ if not isinstance(el, dict):
84
+ continue
85
+ if el.get("type") != "way":
86
+ continue
87
+ tags = el.get("tags") or {}
88
+ if not isinstance(tags, dict):
89
+ tags = {}
90
+ hw = str(tags.get("highway") or "")
91
+ name = str(tags.get("name") or "")
92
+ geom = el.get("geometry")
93
+ if not isinstance(geom, list) or len(geom) < 2:
94
+ continue
95
+
96
+ coords = []
97
+ for i, p in enumerate(geom):
98
+ if simplify_every > 1 and (i % simplify_every) != 0 and i not in (0, len(geom) - 1):
99
+ continue
100
+ if not isinstance(p, dict):
101
+ continue
102
+ lat = p.get("lat")
103
+ lon = p.get("lon")
104
+ if lat is None or lon is None:
105
+ continue
106
+ coords.append([float(lon), float(lat)]) # GeoJSON: [lng,lat]
107
+ if len(coords) < 2:
108
+ continue
109
+
110
+ feats.append(
111
+ {
112
+ "type": "Feature",
113
+ "properties": {"highway": hw, "name": name, "osm_id": el.get("id")},
114
+ "geometry": {"type": "LineString", "coordinates": coords},
115
+ }
116
+ )
117
+
118
+ return {"type": "FeatureCollection", "features": feats}
119
+
120
+
121
+ def main() -> int:
122
+ ap = argparse.ArgumentParser()
123
+ ap.add_argument("--out", default="web/public/maps/bangalore_roads_full.geojson")
124
+ ap.add_argument("--bbox", default="12.75,77.35,13.18,77.85", help="south,west,north,east")
125
+ ap.add_argument("--highways", default=",".join(DEFAULT_HIGHWAYS))
126
+ ap.add_argument("--simplify-every", type=int, default=2, help="keep every Nth point per way (>=1)")
127
+ ap.add_argument("--endpoint", default="https://overpass-api.de/api/interpreter")
128
+ ap.add_argument("--tiles", type=int, default=3, help="split bbox into NxN tiles to avoid huge queries")
129
+ args = ap.parse_args()
130
+
131
+ south, west, north, east = [float(x.strip()) for x in str(args.bbox).split(",")]
132
+ bbox = (south, west, north, east)
133
+ highways = [h.strip() for h in str(args.highways).split(",") if h.strip()]
134
+
135
+ simplify_every = max(1, int(args.simplify_every))
136
+
137
+ # Tile the bbox to keep Overpass responses under server limits.
138
+ features_by_id: dict[str, dict[str, Any]] = {}
139
+ total_tiles = int(args.tiles)
140
+ tiles = _tile_bbox(bbox, total_tiles)
141
+ for idx, tb in enumerate(tiles):
142
+ q = _overpass_query(tb, highways)
143
+ raw = _http_post(args.endpoint, {"data": q})
144
+ obj = json.loads(raw.decode("utf-8"))
145
+ gj_part = _to_geojson(obj, simplify_every=simplify_every)
146
+ for f in gj_part.get("features", []):
147
+ props = f.get("properties") or {}
148
+ oid = str(props.get("osm_id") or "")
149
+ # Merge by OSM way id (tile overlap duplicates).
150
+ if oid and oid not in features_by_id:
151
+ features_by_id[oid] = f
152
+ print(f"tile {idx+1}/{len(tiles)} features={len(gj_part.get('features', []))} unique_total={len(features_by_id)}")
153
+
154
+ gj = {"type": "FeatureCollection", "features": list(features_by_id.values())}
155
+
156
+ out = (ROOT / args.out).resolve()
157
+ out.parent.mkdir(parents=True, exist_ok=True)
158
+ out.write_text(json.dumps(gj), encoding="utf-8")
159
+ print(f"Wrote {out} features={len(gj.get('features', []))} tiles={total_tiles}x{total_tiles} simplify_every={simplify_every}")
160
+ return 0
161
+
162
+
163
+ if __name__ == "__main__":
164
+ raise SystemExit(main())
165
+
web/dist/assets/index-CNGk-9eE.js ADDED
The diff for this file is too large to render. See raw diff
 
web/dist/index.html CHANGED
@@ -5,7 +5,7 @@
5
  <link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>EV Grid Oracle — Pixel City</title>
8
- <script type="module" crossorigin src="/ui/assets/index-DZaVyPvU.js"></script>
9
  <link rel="stylesheet" crossorigin href="/ui/assets/index-DhnTSNm-.css">
10
  </head>
11
  <body>
 
5
  <link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>EV Grid Oracle — Pixel City</title>
8
+ <script type="module" crossorigin src="/ui/assets/index-CNGk-9eE.js"></script>
9
  <link rel="stylesheet" crossorigin href="/ui/assets/index-DhnTSNm-.css">
10
  </head>
11
  <body>
web/dist/maps/bangalore_roads_build_meta.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "counts": {
3
+ "edges": 201510,
4
+ "features": 218355,
5
+ "kept_node_ratio": 0.522341,
6
+ "nodes_kept": 185883,
7
+ "nodes_raw": 355865
8
+ },
9
+ "input": {
10
+ "path": "web/public/maps/bangalore_roads_full.geojson",
11
+ "sha256": "3ff919bfed1d055c5a9ab096f2bfa3c5eb4fc580ff0ac84084a563360f26edc4"
12
+ },
13
+ "output": {
14
+ "path": "web/public/maps/bangalore_roads_graph.json.gz",
15
+ "sha256": "f91aa66d5eecfe6c10bfbc974d38b18d547e145967a0a3ef3b5ce5dc038f2f9b"
16
+ },
17
+ "params": {
18
+ "geom_every": 4,
19
+ "keep_only_largest_component": true,
20
+ "snap_decimals": 5,
21
+ "speed_kmh": {
22
+ "motorway": 65,
23
+ "primary": 45,
24
+ "residential": 22,
25
+ "secondary": 35,
26
+ "service": 16,
27
+ "tertiary": 28,
28
+ "trunk": 60
29
+ }
30
+ }
31
+ }
web/dist/maps/bangalore_roads_full.geojson ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ff919bfed1d055c5a9ab096f2bfa3c5eb4fc580ff0ac84084a563360f26edc4
3
+ size 51801935
web/dist/maps/bangalore_roads_graph.json.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f91aa66d5eecfe6c10bfbc974d38b18d547e145967a0a3ef3b5ce5dc038f2f9b
3
+ size 6175675
web/dist/maps/bangalore_roads_render.json ADDED
The diff for this file is too large to render. See raw diff
 
web/public/maps/bangalore_roads_build_meta.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "counts": {
3
+ "edges": 201510,
4
+ "features": 218355,
5
+ "kept_node_ratio": 0.522341,
6
+ "nodes_kept": 185883,
7
+ "nodes_raw": 355865
8
+ },
9
+ "input": {
10
+ "path": "web/public/maps/bangalore_roads_full.geojson",
11
+ "sha256": "3ff919bfed1d055c5a9ab096f2bfa3c5eb4fc580ff0ac84084a563360f26edc4"
12
+ },
13
+ "output": {
14
+ "path": "web/public/maps/bangalore_roads_graph.json.gz",
15
+ "sha256": "f91aa66d5eecfe6c10bfbc974d38b18d547e145967a0a3ef3b5ce5dc038f2f9b"
16
+ },
17
+ "params": {
18
+ "geom_every": 4,
19
+ "keep_only_largest_component": true,
20
+ "snap_decimals": 5,
21
+ "speed_kmh": {
22
+ "motorway": 65,
23
+ "primary": 45,
24
+ "residential": 22,
25
+ "secondary": 35,
26
+ "service": 16,
27
+ "tertiary": 28,
28
+ "trunk": 60
29
+ }
30
+ }
31
+ }
web/public/maps/bangalore_roads_full.geojson ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ff919bfed1d055c5a9ab096f2bfa3c5eb4fc580ff0ac84084a563360f26edc4
3
+ size 51801935
web/public/maps/bangalore_roads_graph.json.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f91aa66d5eecfe6c10bfbc974d38b18d547e145967a0a3ef3b5ce5dc038f2f9b
3
+ size 6175675
web/public/maps/bangalore_roads_render.json ADDED
The diff for this file is too large to render. See raw diff
 
web/src/map/MapView.ts CHANGED
@@ -109,18 +109,31 @@ export class MapView {
109
  ];
110
  this.map.fitBounds(bounds as any, { padding: 60, duration: 600, maxZoom: 13.8 });
111
 
112
- // Load roads GeoJSON as a deck PathLayer (thin background mesh)
113
- const roadsUrl = staticAssetUrl("maps/bangalore_roads_demo.geojson");
114
- const gj = await fetch(roadsUrl).then((r) => r.json());
115
- const feats = Array.isArray(gj?.features) ? gj.features : [];
116
  const paths: { path: [number, number][]; highway: string }[] = [];
117
- for (const f of feats) {
118
- if (f?.geometry?.type !== "LineString") continue;
119
- const hw = String(f?.properties?.highway || "");
120
- const coords = f?.geometry?.coordinates;
121
- if (!Array.isArray(coords) || coords.length < 2) continue;
122
- // geojson is [lng,lat] already
123
- paths.push({ path: coords as [number, number][], highway: hw });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
 
126
  (this as any)._roads = paths;
 
109
  ];
110
  this.map.fitBounds(bounds as any, { padding: 60, duration: 600, maxZoom: 13.8 });
111
 
112
+ // Load simplified render paths (much smaller than GeoJSON).
113
+ // Fallback to GeoJSON only if render file is missing.
 
 
114
  const paths: { path: [number, number][]; highway: string }[] = [];
115
+ try {
116
+ const renderUrl = staticAssetUrl("maps/bangalore_roads_render.json");
117
+ const rows = (await fetch(renderUrl).then((r) => r.json())) as any[];
118
+ if (Array.isArray(rows)) {
119
+ for (const row of rows) {
120
+ const hw = String(row?.highway || "");
121
+ const coords = row?.path;
122
+ if (!Array.isArray(coords) || coords.length < 2) continue;
123
+ paths.push({ path: coords as [number, number][], highway: hw });
124
+ }
125
+ }
126
+ } catch {
127
+ const roadsUrl = staticAssetUrl("maps/bangalore_roads_full.geojson");
128
+ const gj = await fetch(roadsUrl).then((r) => r.json());
129
+ const feats = Array.isArray(gj?.features) ? gj.features : [];
130
+ for (const f of feats) {
131
+ if (f?.geometry?.type !== "LineString") continue;
132
+ const hw = String(f?.properties?.highway || "");
133
+ const coords = f?.geometry?.coordinates;
134
+ if (!Array.isArray(coords) || coords.length < 2) continue;
135
+ paths.push({ path: coords as [number, number][], highway: hw });
136
+ }
137
  }
138
 
139
  (this as any)._roads = paths;