Skip to content

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():

  1. parse_osm_ways builds a Way per OSM way (closed ways become polygons, except linear barriers such as fences and walls).
  2. parse_osm_rels merges multipolygon relation members into rings, stamps the relation's tags onto untagged outers and subtracts inner rings.
  3. parse_osm_nodes turns standalone obstacle nodes into circles.
  4. separate_ways sorts ways into roads, footways and barriers (see Tag configuration).
  5. parse_intersections adds 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: True when line is a polygon;
  • nodes: ordered OSM node IDs;
  • tags: OSM tags;
  • line: Shapely geometry in UTM. Roads, footways and open barrier lines are buffered to Polygons 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 locked annotation_store() context manager) and the way-edit pipeline that rebuilds a way's nodes and line from deleted, moved, added and split nodes.
  • map_data/annotations.py: apply_store(md, store) merges a store into a MapData (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_planner and osm_cloud all 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

  1. Create the map: create_mapdata -f <file>.gpx -d, or Fetch area / GPX upload in the viewer, writes <name>.mapdata.
  2. Inspect and annotate: the viewer renders the map; edits go to <name>.annotations.json.
  3. Plan: the viewer's Planner, map_data_plan or a PlanRoute goal to route_planner load the map with its annotations and traversability rules and call plan_route.
  4. Drive: osm_cloud publishes 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