Architecture overview
The map_data package has four layers: data ingestion, representation, planning and visualization.
Component diagram
MapData
MapData (map_data/map_data.py) is the central data class. It takes a .gpx file (waypoints or a track) or a UTM coordinate array, downloads the OSM data around it and parses it into Way lists.
Bounding box. The waypoints' axis-aligned UTM bounding box, expanded by grid_margin (default 150 m, config/planner_defaults.yaml) on every side. MapData(..., grid_margin=...) overrides it.
Overpass query. run_queries() sends one union query for the box: every way, its nodes, the relations that reference it, and every node. Nothing is filtered server-side; run_parse() classifies client-side. OverpassClient (map_data/utils/overpass.py) rotates endpoints, retries, and rejects error bodies.
Parsing. run_parse():
parse_osm_waysbuilds aWayper OSM way (closed ways become polygons, except linear barriers such as fences and walls).parse_osm_relsmerges multipolygon relation members into rings, stamps the relation's tags onto untagged outers and subtracts inner rings.parse_osm_nodesturns standalone obstacle nodes into circles.separate_wayssorts ways into roads, footways and barriers (see Tag configuration).parse_intersectionsadds a crossroad wherever a footway/road node branches.
Attributes after parsing:
| Attribute | Type | Description |
|---|---|---|
roads_list |
list[Way] |
Non-footway highway ways, buffered to 7 m wide polygons |
footways_list |
list[Way] |
Footway-class highway ways, buffered to 3 m wide polygons |
barriers_list |
list[Way] |
Barrier polygons, buffered barrier lines and obstacle circles |
crossroads_list |
list[Way] |
Junction circles of radius CROSSROAD_RADIUS (1.5 m) |
nodes_cache |
dict[int, dict] |
OSM node ID → {lat, lon, tags} |
zone_number, zone_letter |
int, str |
UTM zone of the input waypoints |
waypoints |
np.ndarray |
(N, 2) UTM input waypoints |
min_x, max_x, min_y, max_y |
float |
UTM bounding box including grid_margin |
OSM cache. The raw Overpass response is saved as <stem>.osm_cache.json next to the source GPX. It is reused when the hash of the query (which contains the bounding box) matches and it is younger than 30 days (OSM_CACHE_MAX_AGE). run_queries() uses it by default (the viewer's fetch and GPX upload); run_all() bypasses it, and create_mapdata uses it only with --cache.
Serialization. MapData.save() writes JSON (.mapdata) atomically; MapData.load() reads it and recomputes the node-based crossroads. See Data formats.
Traversability. MapData.apply_traversability(rules) removes the roads and footways the traversability rules refuse and recomputes the crossroads. It runs at load time (load_mapdata_with_annotations), not at parse time, so the .mapdata file keeps the full OSM classification.
Way objects
Every OSM feature is a Way (map_data/utils/way.py):
id: OSM way or node ID; negative or string IDs for merged relation rings, viewer-drawn annotations and split segments;is_area:Truewhenlineis a polygon;nodes: ordered OSM node IDs;tags: OSM tags;line: Shapely geometry in UTM. Roads, footways and open barrier lines are buffered toPolygons during parsing;MapData.centre_line(way)recovers a footway's centerline;in_out:"outer"or"inner"for multipolygon members, else"".
Annotations and edits
Viewer edits live in <stem>.annotations.json next to the .mapdata file, which is never modified.
map_data/edits.py: the annotation store (load, quarantine of an unreadable store, the lockedannotation_store()context manager) and the way-edit pipeline that rebuilds a way'snodesandlinefrom deleted, moved, added and split nodes.map_data/annotations.py:apply_store(md, store)merges a store into aMapData(way edits, tag overrides, drawn paths and obstacles,join_ways).load_mapdata_with_annotations()loads a map, merges its store and applies the traversability rules in one call;edit_mapdata()does the same for an already loaded map. The viewer,map_data_plan,route_plannerandosm_cloudall use it, so they see the same map.
join_ways gives ways that meet without a shared node ID a junction node: a drawn path's loose end or a moved end node within JOIN_DISTANCE_M (5 m) of another way, and every crossing of a drawn path.
Pathsolvers
map_data/pathsolver/route.py is the shared entry point: plan_route(md, points_latlon, algorithm=...) validates the request, runs the graph or grid planner and returns a RouteResult, or raises RoutePlanningError with a machine-readable reason. The viewer, map_data_plan and route_planner all call it.
GraphPlanner
map_data/pathsolver/graph_planner.py
A* over the OSM way network (footway and/or road). Ways connect by shared node ID. An edge weighs length × (1 + way cost + rule cost) (see Planner configuration); reported lengths stay geometric. Waypoints snap to the nearest edge through an STRtree; a waypoint farther than max_snap_distance fails with snap_too_far. Open pedestrian areas (WalkableArea, walkable_area.py) are crossed through a visibility graph instead of walked around their rim.
ReplanPath
map_data/pathsolver/replan.py
Cost-grid planning over a box around the waypoints:
| Module | Class / function | Responsibility |
|---|---|---|
grid_constructor.py |
PathGrid |
Cost raster at cell_size; cells near a way take its cost, obstacle cells are inf |
grid_astar.py |
grid_astar |
8-connected A* on the raster |
rrt_star.py |
RRTStar |
Sampling planner with informed sampling and an adaptive rewiring radius |
smoothing.py |
smooth_path |
Gradient-descent smoothing with collision checks |
way_cost.py |
way_cost, load_cost_tables |
highway/surface cost shared with GraphPlanner |
replan() plans each leg between consecutive waypoints. A straight leg that hits no obstacle is kept. Otherwise blocked ends are moved to the nearest free cell within SNAP_RADIUS_M (2 m), and the leg fails fast when start and goal lie in disconnected free regions. RRT falls back to A when it finds no path. The result is then smoothed (optional) and simplified. On failure it returns None and sets failure_reason (no_path, start_blocked, goal_blocked, cancelled).
Defaults come from grid_defaults(); see Planner configuration.
Viewer
map_data/viewer/ is a single-page Flask + Leaflet application.
Back-end:
| Module | Responsibility |
|---|---|
app.py |
App factory, Socket.IO, CLI (map_data_viewer), optional ROS 2 thread |
routes/ |
The viewer blueprint: files (page, file list, map, fetch, uploads, export), annotations, ways (way and node edits), planning (planner defaults, traversability, cost grid, replan), sharing (goal QR codes, wormhole), common (JSON errors, path helpers, way resolution, validators, get_merged_mapdata) |
helpers.py |
GeoJSON conversion |
cache.py |
Read caches keyed on file signature and annotation-store digest |
ros_node.py |
TrackerNode, the Tracker's ROS 2 subscriber |
tracker_routes.py, tracker_config.py |
Tracker settings API and its parameter file |
Every error is JSON {"error": message}; see Viewer REST API.
Front-end modes (static/js/):
| Mode | Entry point | Purpose |
|---|---|---|
| Viewer | ui_handlers.js, layers.js, draw_handlers.js |
Inspect, edit, annotate and search OSM features |
| Planner | planner_mode.js |
Waypoints, route planning, GPX download, goal QR codes |
| Tracker | tracker_mode.js |
Live robot telemetry over Socket.IO |
Executables
| Executable | File | Kind | Purpose |
|---|---|---|---|
create_mapdata |
map_data/create_mapdata.py |
CLI | Build a .mapdata from a GPX file |
map_data_info |
map_data/info.py |
CLI | Statistics and validation of a .mapdata file |
map_data_plan |
map_data/plan_route_cli.py |
CLI | Plan a route offline (plan_route) |
map_data_viewer |
map_data/viewer/app.py |
CLI, optional ROS 2 | Web viewer; with ROS 2 also runs TrackerNode |
osm_cloud |
map_data/osm_cloud.py |
ROS 2 node | Publishes the way network as a cost-grid PointCloud2, intersections as PoseArray and markers |
route_planner |
map_data/route_planner.py |
ROS 2 node | PlanRoute action server on ~/plan_route; publishes the route latched |
The PlanRoute action is defined in the nested map_data_interfaces/ package.
Data flow
- Create the map:
create_mapdata -f <file>.gpx -d, or Fetch area / GPX upload in the viewer, writes<name>.mapdata. - Inspect and annotate: the viewer renders the map; edits go to
<name>.annotations.json. - Plan: the viewer's Planner,
map_data_planor aPlanRoutegoal toroute_plannerload the map with its annotations and traversability rules and callplan_route. - Drive:
osm_cloudpublishes the same map for the navigation stack; the viewer's Tracker shows the robot and its route.
Project structure
map_data/ # repository root
├── map_data/ # Python package
│ ├── map_data.py # MapData
│ ├── annotations.py # apply an annotation store to a MapData
│ ├── edits.py # annotation store IO, way-edit pipeline
│ ├── traversability.py # tag rules: which ways may be driven on
│ ├── create_mapdata.py # create_mapdata CLI
│ ├── info.py # map_data_info CLI
│ ├── plan_route_cli.py # map_data_plan CLI
│ ├── osm_cloud.py # osm_cloud ROS 2 node
│ ├── route_planner.py # route_planner ROS 2 action server
│ ├── pathsolver/
│ │ ├── route.py # plan_route, RoutePlanningError, goal_plan_kwargs
│ │ ├── graph_planner.py # GraphPlanner (on-network A*)
│ │ ├── walkable_area.py # crossing open pedestrian areas
│ │ ├── astar.py # generic A*
│ │ ├── way_cost.py # highway/surface cost tables
│ │ ├── replan.py # ReplanPath, grid_defaults
│ │ ├── grid_constructor.py # PathGrid cost raster
│ │ ├── grid_astar.py # grid A*
│ │ ├── rrt_star.py # RRTStar
│ │ └── smoothing.py # gradient-descent smoother
│ ├── utils/
│ │ ├── config.py # config lookup, setup_logging
│ │ ├── densify.py # resample ways to a max step
│ │ ├── geodesy.py # UTM / WGS84 / ECEF
│ │ ├── gpx.py # GPX writing
│ │ ├── launch.py # launch-file helpers
│ │ ├── overpass.py # OverpassClient
│ │ ├── parsing.py # OSM parsing and classification
│ │ ├── qr.py # goal QR codes
│ │ ├── serialization.py # .mapdata JSON, atomic writes
│ │ └── way.py # Way
│ └── viewer/
│ ├── app.py # Flask app, Socket.IO, CLI
│ ├── routes/ # REST API, one module per concern
│ ├── helpers.py # GeoJSON conversion
│ ├── cache.py # read caches
│ ├── ros_node.py # TrackerNode
│ ├── tracker_routes.py # Tracker settings API
│ ├── tracker_config.py # Tracker parameter file
│ ├── templates/index.html # page shell
│ └── static/ # css/, js/ (one module per mode and concern)
├── map_data_interfaces/ # PlanRoute action (separate ament_cmake package)
├── config/ # planner_defaults, traversability, route_planner, osm_grid, tracker, helhest_jr
├── parameters/ # tag classification CSVs
├── launch/ # osm_cloud.launch.py, route_planner.launch.py
├── demo/ # demo map, installed to share/map_data/data
├── data/ # local maps (untracked)
├── scripts/
│ ├── setup_ws.sh # symlink map_data_interfaces into the workspace
│ ├── ros_smoke.sh # ROS 2 runtime smoke test
│ └── build_static_demo.sh # static viewer demo for GitHub Pages
├── tests/ # pytest suite
│ └── data/ # Stromovka .mapdata fixture (ODbL)
├── docs/ # MkDocs source
├── pyproject.toml, uv.lock # Python metadata, locked dev environment
├── package.xml, setup.py, setup.cfg, resource/ # ament_python packaging
└── mkdocs.yml