#!/usr/bin/env python3 """Generate a compact road-class grid for the RED4ext GPS edge-cost shim.""" from __future__ import annotations import argparse import json import math from dataclasses import dataclass from pathlib import Path from typing import Any, Iterable FLAGS = { "Pavement": 0x0008, "Road": 0x0010, "GPSOnly": 0x0200, "Highway": 0x4000, } CATEGORY_CODES = { "unknown": ".", "pavement": "P", "gpsonly": "G", "road": "R", "highway": "H", } CATEGORY_PRIORITY = { ".": 0, "P": 1, "G": 2, "R": 3, "H": 4, } @dataclass(frozen=True) class GridSpec: min_x: float min_y: float width: int height: int cell_size: float def flag_value(value: Any) -> int: if isinstance(value, int): return value if isinstance(value, str): return int(value) if isinstance(value, dict): for key in ("Value", "$value", "value"): if key in value: return flag_value(value[key]) return int(value or 0) def lane_category(flags: int) -> str: if flags & FLAGS["Highway"]: return "highway" if flags & FLAGS["GPSOnly"]: return "gpsonly" if flags & FLAGS["Road"]: return "road" if flags & FLAGS["Pavement"]: return "pavement" return "unknown" def load_lanes(path: Path) -> list[dict[str, Any]]: data = json.loads(path.read_text(encoding="utf-8")) return data["Data"]["RootChunk"]["data"]["lanes"] def load_lane_polygons(path: Path) -> list[list[tuple[float, float]]]: data = json.loads(path.read_text(encoding="utf-8")) rows = data["Data"]["RootChunk"]["data"] polygons: list[list[tuple[float, float]]] = [] for row in rows: polygon = row.get("value", {}).get("polygon") or [] polygons.append([(float(point["X"]), float(point["Y"])) for point in polygon]) return polygons def point_in_polygon(x: float, y: float, polygon: list[tuple[float, float]]) -> bool: inside = False count = len(polygon) if count < 3: return False previous_x, previous_y = polygon[-1] for current_x, current_y in polygon: if (current_y > y) != (previous_y > y): edge_x = (previous_x - current_x) * (y - current_y) / (previous_y - current_y) + current_x if x < edge_x: inside = not inside previous_x, previous_y = current_x, current_y return inside def orientation(ax: float, ay: float, bx: float, by: float, cx: float, cy: float) -> float: return (by - ay) * (cx - bx) - (bx - ax) * (cy - by) def on_segment(ax: float, ay: float, bx: float, by: float, cx: float, cy: float) -> bool: return min(ax, cx) <= bx <= max(ax, cx) and min(ay, cy) <= by <= max(ay, cy) def segments_intersect( ax: float, ay: float, bx: float, by: float, cx: float, cy: float, dx: float, dy: float, ) -> bool: o1 = orientation(ax, ay, bx, by, cx, cy) o2 = orientation(ax, ay, bx, by, dx, dy) o3 = orientation(cx, cy, dx, dy, ax, ay) o4 = orientation(cx, cy, dx, dy, bx, by) if (o1 > 0) != (o2 > 0) and (o3 > 0) != (o4 > 0): return True eps = 1e-5 if abs(o1) <= eps and on_segment(ax, ay, cx, cy, bx, by): return True if abs(o2) <= eps and on_segment(ax, ay, dx, dy, bx, by): return True if abs(o3) <= eps and on_segment(cx, cy, ax, ay, dx, dy): return True if abs(o4) <= eps and on_segment(cx, cy, bx, by, dx, dy): return True return False def polygon_intersects_cell( polygon: list[tuple[float, float]], min_x: float, min_y: float, max_x: float, max_y: float, ) -> bool: center_x = (min_x + max_x) * 0.5 center_y = (min_y + max_y) * 0.5 if point_in_polygon(center_x, center_y, polygon): return True corners = ((min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)) if any(point_in_polygon(x, y, polygon) for x, y in corners): return True if any(min_x <= x <= max_x and min_y <= y <= max_y for x, y in polygon): return True cell_edges = ( (min_x, min_y, max_x, min_y), (max_x, min_y, max_x, max_y), (max_x, max_y, min_x, max_y), (min_x, max_y, min_x, min_y), ) previous_x, previous_y = polygon[-1] for current_x, current_y in polygon: for edge in cell_edges: if segments_intersect(previous_x, previous_y, current_x, current_y, *edge): return True previous_x, previous_y = current_x, current_y return False def grid_index(spec: GridSpec, x: float, y: float) -> tuple[int, int]: column = math.floor((x - spec.min_x) / spec.cell_size) row = math.floor((y - spec.min_y) / spec.cell_size) return int(column), int(row) def mark_cell(grid: list[list[str]], column: int, row: int, code: str) -> bool: if row < 0 or row >= len(grid) or column < 0 or column >= len(grid[row]): return False if CATEGORY_PRIORITY[code] <= CATEGORY_PRIORITY[grid[row][column]]: return False grid[row][column] = code return True def iter_cells_for_polygon(spec: GridSpec, polygon: list[tuple[float, float]]) -> Iterable[tuple[int, int]]: xs = [point[0] for point in polygon] ys = [point[1] for point in polygon] min_col, min_row = grid_index(spec, min(xs), min(ys)) max_col, max_row = grid_index(spec, max(xs), max(ys)) for row in range(max(0, min_row), min(spec.height - 1, max_row) + 1): for column in range(max(0, min_col), min(spec.width - 1, max_col) + 1): cell_min_x = spec.min_x + column * spec.cell_size cell_min_y = spec.min_y + row * spec.cell_size if polygon_intersects_cell( polygon, cell_min_x, cell_min_y, cell_min_x + spec.cell_size, cell_min_y + spec.cell_size, ): yield column, row def build_grid( lanes: list[dict[str, Any]], polygons: list[list[tuple[float, float]]], cell_size: float, inflate_cells: int, ) -> tuple[GridSpec, list[list[str]], dict[str, int]]: points = [point for polygon in polygons for point in polygon] if not points: raise ValueError("no polygon points found") min_x = math.floor(min(point[0] for point in points) / cell_size) * cell_size min_y = math.floor(min(point[1] for point in points) / cell_size) * cell_size max_x = math.ceil(max(point[0] for point in points) / cell_size) * cell_size max_y = math.ceil(max(point[1] for point in points) / cell_size) * cell_size spec = GridSpec( min_x=min_x, min_y=min_y, width=int(round((max_x - min_x) / cell_size)), height=int(round((max_y - min_y) / cell_size)), cell_size=cell_size, ) grid = [[CATEGORY_CODES["unknown"] for _ in range(spec.width)] for _ in range(spec.height)] for index, (lane, polygon) in enumerate(zip(lanes, polygons, strict=True)): if len(polygon) < 3: continue code = CATEGORY_CODES[lane_category(flag_value(lane.get("flags", 0)))] if code == CATEGORY_CODES["unknown"]: continue changed_cells: set[tuple[int, int]] = set() for column, row in iter_cells_for_polygon(spec, polygon): for dy in range(-inflate_cells, inflate_cells + 1): for dx in range(-inflate_cells, inflate_cells + 1): changed_cells.add((column + dx, row + dy)) for column, row in changed_cells: mark_cell(grid, column, row, code) counts = {code: sum(row.count(code) for row in grid) for code in CATEGORY_PRIORITY} return spec, grid, counts def write_header(path: Path, spec: GridSpec, grid: list[list[str]], counts: dict[str, int]) -> None: lines = [ "#pragma once", "", "#include ", "#include ", "#include ", "", "namespace EdgeWeightGPS::Generated", "{", f"constexpr float kSpatialRoadGridMinX = {spec.min_x:.1f}f;", f"constexpr float kSpatialRoadGridMinY = {spec.min_y:.1f}f;", f"constexpr float kSpatialRoadGridCellSize = {spec.cell_size:.1f}f;", f"constexpr uint32_t kSpatialRoadGridWidth = {spec.width};", f"constexpr uint32_t kSpatialRoadGridHeight = {spec.height};", "", "// Cell codes: H=highway, R=road, G=GPS-only, P=pavement, .=unknown.", "// Generated by tools/generate_spatial_edge_grid.py from all.traffic_persistent and all.lane_polygons.", f"// Counts: H={counts['H']} R={counts['R']} G={counts['G']} P={counts['P']} unknown={counts['.']}.", "constexpr std::array kSpatialRoadGridRows = {{", ] for row in grid: lines.append(f' "{("").join(row)}",') lines.extend( [ "}};", "", "} // namespace EdgeWeightGPS::Generated", "", ] ) path.write_text("\n".join(lines), encoding="utf-8") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--traffic-json", type=Path, default=Path("work/raw-segment-json/all.traffic_persistent.json")) parser.add_argument( "--lane-polygons-json", type=Path, default=Path("work/traffic-companions/json/all.lane_polygons.json"), ) parser.add_argument( "--output", type=Path, default=Path("red4ext/EdgeWeightGPS/src/GeneratedSpatialRoadGrid.hpp"), ) parser.add_argument("--cell-size", type=float, default=16.0) parser.add_argument("--inflate-cells", type=int, default=0) args = parser.parse_args() lanes = load_lanes(args.traffic_json) polygons = load_lane_polygons(args.lane_polygons_json) if len(lanes) != len(polygons): raise ValueError(f"lane/polygon count mismatch: {len(lanes)} != {len(polygons)}") spec, grid, counts = build_grid(lanes, polygons, args.cell_size, args.inflate_cells) args.output.parent.mkdir(parents=True, exist_ok=True) write_header(args.output, spec, grid, counts) total = spec.width * spec.height covered = total - sum(row.count(".") for row in grid) print( f"wrote {args.output} cells={total} covered={covered} " f"size={spec.width}x{spec.height} origin=({spec.min_x},{spec.min_y}) cell={spec.cell_size}" ) print("counts " + " ".join(f"{code}={counts[code]}" for code in ("H", "R", "G", "P", "."))) return 0 if __name__ == "__main__": raise SystemExit(main())