Changelog
[Unreleased]
[3.0.0] — 2026-09-21
A major version because public helpers and a config file are removed.
Added
.mapdatafiles carry aformat_versionmarker (FORMAT_VERSIONinmap_data/utils/serialization.py, currently1). Files written before the marker load as before, a file from a newer version is rejected with a message naming both versions instead of half-loading, and every key of a stored way is optional on load (theWaydefaults fill in), so a key added later does not break older filesplan_route()androute_plannerreject a start or goal more thanoutside_map_tolerance(100 m) outside the map's area withstart_outside_map/goal_outside_map(any algorithm, checked before the snap distances;route_plannernames the loaded file), so a wrong map file or a QR code for another map fails at once instead of planning towards the map's edge- The viewer's tracker topics can be changed without a restart: Topics… in the Tracker
sidebar resubscribes the running tracker and can save the topics back to its config file,
comments kept (
GET/PUT /api/tracker/settings).map_data_viewer --config <file>names that file, a ROS 2 parameter file with amap_data_trackersection. The default is the newconfig/tracker.yaml, which holds the built-in defaults, so nothing changes for existing setups config/helhest_jr.yaml: tracker topics of helhest-jr (map_data_viewer --config config/helhest_jr.yaml)- Tracker
tf_static_only(config file only, defaulttrue): frames are read from/tf_staticalone. Set it tofalsewhen a frame above the robot is published on/tf, which costs a lot of CPU - The Tracker sidebar keeps the last five speech messages instead of only the newest, so a message that scrolls by while you look elsewhere is still readable
- Tracker Clear Trail: drops the recorded trail on the node
(
DELETE /api/tracker/trail), so it stays gone instead of coming back with the next fix - Planner Start at robot: with a robot position on the tracker's telemetry, the toggle pins waypoint 0 to the robot and refreshes it when Replan Path runs, so a route is planned from where the robot actually is instead of a clicked start
osm_cloudtakes ahighway_typesparameter (andhighway_types:=launch argument), asroute_plannerdoes: the grid can be drawn from roads as well as footways, so it shows the network the graph planner routes on when roads are allowed. The default stays["footway"]- The graph planner crosses walkable areas (closed
area=yesor multipolygon ways, e.g. pedestrian squares) along the shortest path inside them, holes and concave corners respected, instead of walking their rim. Any network node on, inside or within 1 m of the area enters it; a waypoint inside an area is no longer snapped to its rim, and one snapped onto (or within 1 m of) an area crosses it from there. The crossings live with the area (map_data.pathsolver.walkable_area) and add no graph edges
Removed
config/helhest.yaml: helhest-jr is the only Fixposition robot, and its topics (tracker andosm_cloud) are inconfig/helhest_jr.yaml, now the defaultconfig_fileofosm_cloud.launch.py- Migrations for annotation stores written before v2 (
migrate_change_log()and the legacy store normalization inmap_data/viewer/helpers.py): a store from that era now loads as whatever it literally contains. Re-export such a map from a v2 viewer before upgrading find_config()(map_data/utils/config.py) andsave_mapdata()(map_data/utils/serialization.py), each with a single caller; useMapData.save()- The
requestsdependency (python3-requests).map_data/utils/overpass.pyposts throughurllib.requestand identifies itself asmap_data/<version>
Changed
smooth_pathruns as a vectorized Jacobi sweep over the whole path instead of a scalar Gauss-Seidel triple loop: ~100x faster on a 1000-waypoint path (40 ms to 0.3 ms). Both schemes share the same fixed point, so the smoothed path is the same up to the stopping tolerance/api/mapdataserves the merged GeoJSON from a cache keyed on the.mapdatafile's identity (mtime, size, inode) and a digest of the annotation store, so repeat calls no longer copy the map, re-apply every edit and rebuild the FeatureCollection. The response carries anETagand honorsIf-None-Matchwith a 304. Any edit or a change to the file on disk invalidates the entry- Loading a map for planning no longer
deepcopys it.apply_annotationscopies the map shallowly and only the ways an edit touches, so an annotated map costs a copy of the edited ways instead of the whole node table - The grid A* inner loop runs on plain Python floats and an
array("f")of g scores instead of numpy scalars, andmathreplacesnp.sqrtfor the heuristic. Every g score is rounded back to float32, so the expansion order and the resulting path are bit-identical to before - Obstacles are marked in the point grid by index range: each obstacle's bounds become a sub-block of the lattice and only that block is tested, instead of masking the whole grid once per obstacle
- The graph planner measures distances with
math.hypotrather thannp.linalg.norm(the A* heuristic, the snap costs and the spur collapse), andlatlon_to_utm_path/utm_path_to_latlon_pairsconvert whole arrays in oneutmcall instead of point by point osm_cloudqueries its kd-tree in 2-D over all cores at once and splits ways with numpy instead of a per-node loop- The viewer's tracker spins on the C++
EventsExecutorwhen the distro has it (measured 6 % vs 60 % CPU against a 250 Hz robot), falling back torclpy.spin; TF comes from/tf_staticonly (seetf_static_only), and telemetry updates less often route_plannerno longer blocks 0.5 s per goal waiting for theearth_frame -> local_frametransform: the lookup does not wait, the warning is throttled to 30 s, and the one-shot preload timer is destroyed instead of only cancelled- CI pins ruff to 0.16.6 (matching
pyproject.tomland.pre-commit-config.yaml), builds the docs with--strict, smoke-tests the demo on pull requests, and fails under 72 % coverage
Fixed
- RRT* no longer samples from the module-global
random. EveryRRTStargets its ownrandom.Random(injectable asRRTStar(..., rng=...)/ReplanPath(..., rng=...), or via aseedunderrrt:inconfig/planner_defaults.yaml), so concurrent planners in different request threads cannot interleave each other's draws, a failing run can be replayed, and tests no longer have to seed the global stream - Moving a node that several ways share now moves it for the planner the way the viewer draws
it. Moves are stored per way, but the merged map resolved them per node id: the last
override won, the other ways were dragged along in the graph and the junction survived.
The merge (
apply_way_edits) now gives the moved way its own copy of the node (a fresh negative id) and leaves the original where it was, so the junction is broken exactly where the viewer shows a gap; undoing the move joins it again. Nothing changes in the store - Ways that meet on the map now meet in the graph. The graph planner connects ways by node id
only, and an edit never produced a shared id by itself. The annotation merge now ends with
join_ways(), which gives such ways a real node to meet at:- an end node dragged onto another way (within 5 m,
JOIN_DISTANCE_M) joins it there: at a node of that way when one is within 1 m, else at a new node on the edge. Before, the end kept the junctions it had and gained none; since the shared-node fix above it was left loose - a drawn path joins every way it crosses, and a way that ends on it. Only its two ends were connected before, and only by the planner, so a path drawn across a footway showed a crossroad the planner could not turn at
- splitting a drawn path that an export had baked into the map no longer cuts it off the
network (the planner's own stitching skipped the
-18:1style ids of its segments), and a path drawn on such a map no longer reuses the baked path's way id The junction is only added to the node lists, so no geometry moves; being real nodes, the junctions survive an export. The planner's own stitching of drawn-path ends stays for maps merged elsewhere and skips an end that is already a node of another way
- an end node dragged onto another way (within 5 m,
- A junction node moved to the same spot (0.5 m) in every way that uses it stays one junction, instead of one copy per way lying on top of each other, unconnected
- A move in one way no longer leaks into the others: a way rebuilt for a deleted node or a split took a shared node's position from the way that had moved it, and a move recorded on a way deleted afterwards still moved the node for everyone
osm_cloudpublished an empty grid that only a restart cured. A Fixposition unit without a fusion fix publishesFP_ECEF -> FP_ENU0as all zeros, and the node latched it at start-up:utm_to_local_via_ecefthen worked at an altitude of -6378 km and collapsed the whole map into a metre-wide box, so no grid cell landed withinmax_path_distof a way (7 of the last 25 field runs,(0, 4)or(30, 4)points,FP_ENU0 origin at lat=180.0in the log). Such a transform is now rejected and waited out; the/tf_staticsubscription is kept for the node's life, so a placement transform that arrives late or moves (the ENU0 origin changes whenever the GNSS/INS driver restarts) rebuilds and re-publishes the grid and the intersections instead of needing the node restarted; and a grid that comes out empty over a map that has ways is logged as an error rather than latched onto every late subscriber- The tracker kept showing a plan and goal that no longer existed. The plan and the goal are
dropped once nothing publishes their topics (a planner that stopped or restarted), or once
the planner has been sending empty plans for 3 s (
PLAN_STALE_TIMEOUT) — crl_commander republishes at 20 Hz and publishes an empty plan while it has no goal, so STOP, goal reached and goal aborted all clear the display. Silence alone never clears anything, so a publish-once planner (nav2) keeps its plan - The tracker drew no high-level route on helhest-jr:
config/helhest_jr.yamlpointedsequence_path_topicat/crl_commander/goal_sequence, which the commander only publishes in SEQUENCE mode, whileroad_followerdrives it goal by goal. It now reads/road_follower/route_path, the route actually being followed whatever its source. A route is also no longer decimated to every 10th pose (which cut every corner of a waypoint route); it is thinned only above 200 poses - The viewer's telemetry Socket.IO connection failed with
property 'session' of 'RequestContext' object has no setteron Flask 3.0.2 with Flask-SocketIO 5.3 (Ubuntu 24.04 packages), so the tracker received nothing. The Socket.IO server no longer manages a Flask session - Route relations (hiking, bike, bus) are no longer merged like multipolygons.
Their member ways were chained into one negative-id way carrying every
member's tags, so a single bridge or stairs member made the whole route
non-traversable and the planner avoided kilometres of ordinary footway. Only
multipolygonandboundaryrelations are merged now. Existing.mapdatafiles keep the merged ways until they are parsed again
[2.0.0] — 2026-09-13
A major version because node parameters and public helpers are removed.
Removed
osm_cloud: theauto_utmandutm_to_localparameters and thepublish_static_tflaunch argument;transform_modecovers themMapData: theflipandcurrent_robot_positionarguments andexclude_ways(); useapply_traversability(TraversabilityRules().extend(...)). The nodes'exclude_highwayparameter is unchangedutils.gpx:parse_path,parse_gpx_file,parse_yaml_file,convert_waypointandutm_path_to_latlon.MapDatareads GPX itself; YAML waypoint files are no longer accepted- The
python -m map_data.pathsolver.replanCLI and its matplotlib visualizer;map_data_planis the offline planner Way.to_pcd_pointsandutils.points_to_graph_pointsTraversabilityRules.describe,summaryandedge_factor,OverpassClient.query, andutils.launch.package_config_dir(nowutils.config.package_share)- The
tqdmandmatplotlibdependencies
Changed
- RRTruns as Informed RRT: once a path is found it keeps improving it for up
to
rrt.improve_iter(200) iterations, sampling the informed ellipse with an adaptive rewiring radius. The settings live in a newrrt:section ofconfig/planner_defaults.yaml - Grid Aand RRT paths are simplified once, over the whole route, instead of per segment and then again at the end
- Obstacles are rasterised with
shapely.contains_xy; the demo map's grid is identical - Viewer: collapsible panels are native
<details>(the whole header toggles), the wormhole share dialog is a Bootstrap modal, and the duplicated frontend and route code is consolidated - Annotation stores in legacy formats are normalised once, when loaded
Fixed
- Crossroad detection no longer invents junctions where one corridor is mapped twice. A node was a crossroad when several ways used it, so a cycleway or an annotated path drawn along an existing footway — which reuses its node ids — made a junction of every node of the shared run. A node is now a crossroad when more than two distinct neighbouring nodes leave it: two ways passing between the same neighbours are one path, three directions are a fork. On the full Stromovka map this drops 64 junctions, every one of them mid-corridor
- Roads take part in crossroad detection. Only footways were considered, so a
footway meeting a service road was not a junction at all and the same physical
junction was found or missed depending on how the through way happened to be
tagged — 93 of them on the full Stromovka map, 42 on one drawn road alone.
This matters now that the graph planner routes over roads
(
highway_types: [footway, road]) - The geometric detector used for annotated paths matches centre line against
centre line. It tested the drawn line against the other way's buffered
geometry, which yields the stretch of line inside a 3 m (footway) or 7 m
(road) corridor rather than a crossing: a path merely running alongside a way
reported a junction it never reaches, every real crossing was placed half a
corridor — up to 3.5 m, most of the follower's 5 m enter radius — from where
it happens, and the T-junction tolerance reached half a corridor too far.
MapData.centre_linerebuilds the unbuffered line from a way's node ids MapData.loadrecomputes the node-based crossroads instead of trusting the ones in the file, so a.mapdatawritten by an older version is corrected on load; crossroads that cannot be recomputed from node ids (an annotated path's) are kept as saved
[1.4.0] — 2026-09-12
Added
- Traversability rules:
config/traversability.yamldecides from OSM tags which ways the robot may drive on (first matching rule wins, each with areasonthe operator sees in the node log) and what they cost.MapDataapplies the rules when a map is loaded for planning —exclude_waysis now the shortcut for rules that only denyhighwayvalues — andGraphPlannerweighs its edges by the same highway/surface cost tables as the grid planner through the sharedpathsolver.way_costhelper, while the reported route length stays geometric. The file is selectable per run (traversability_fileparameter,traversability:=,--traversability) and is part of the map and planner cache keys, mtime included - Offline route planning:
map_data.pathsolver.route.plan_route(the Planner screen's graph/grid planning as a library call, withdensify, failure reasons and snap distances),map_data.annotations(annotation-store merge without the web app,load_mapdata_with_annotations), themap_data_planCLI, theroute_plannerROS 2 action server with the newmap_data_interfacespackage (PlanRoute.action),route_planner.launch.py,GraphPlanner.snap_distanceandgpx.create_gpx_track. The viewer's/api/create_replannow delegates toplan_routeand reports areasonon failure config/route_planner.yaml: everyroute_plannerparameter in one documented file, loaded byroute_planner.launch.py(params_file:=takes an absolute path or a name inconfig/). The launch arguments now default to empty and are applied on top of the file, so an argument left unset keeps the file's value;highway_types:=footway,roadlets the graph planner use roads as well as footways.map_data.utils.launchholds the argument helperskeep_goal(default on) ends a route at the requested goal itself rather than at its projection onto the network, which can be tens of metres short; the final off-network leg is densified like the rest. The goal has its own snap limit (goal_max_snap_distance, 30 m, failing withsnap_too_far) separate from the start's 100 m, which is the robot's own fixexclude_highway(defaultsteps): stairs stay in the saved map for the viewer, butMapData.exclude_waysdrops them at load time andGraphPlannerfilters them for callers passing a raw map, so no route is ever planned over them- Explicit annotations switch —
--annotations auto|none|FILEand theannotationsparameter — because a store may delete a large part of the map (Stromovka's deletes 838 ways) and planning has to be able to opt out of it - Goal QR codes for Robotour:
map_data.utils.qrencodes a goal as a geo URI, and the viewer serves one per waypoint (/api/qr,/api/qr.svg) with a full-screen modal and PNG download osm_cloudgeodetic placement of map data via the ECEF → local TF: newtransform_mode(tf|auto|geodetic) andearth_frameparameters. In geodetic mode UTM points are converted to lat/lon → ECEF and placed inlocal_framethrough theearth_frame→local_frametransform (FP_ECEF→FP_ENU0on Helhest), which is exact where a UTM translation is off by grid convergence (5–8 m per km). Grid bounds use all four UTM corners and the frames are launch arguments- Latched
osm_cloudpublishing (the grid and intersections are published once at start-up and after parameter rebuilds via transient-local publishers;republish_periodrestores periodic re-publishing) andMapData.geometric_intersections(), which finds crossings and endpoint T-junctions geometrically so that annotated viewer paths — which share no OSM node ids and were therefore invisible to node-based detection — reach export andosm_cloudas crossroads - Viewer tracker for the Helhest field stack: poses in any TF frame are converted
to lat/lon through
earth_frame(ECEF) instead of requiring autmframe; new inputsBatteryState,Temperature, e-stopBool,DiagnosticArray,Joy, crl_commander state string,road_followerstate, goal (PoseStamped), waypoint sequence (Path/PoseArray) and visual road path; heading fromImu,Vector3Stampedyaw orOdometry(heading_type). The sidebar gained E-Stop / Follower / Diagnostics rows and battery percentage; the map draws the sequence (blue), road path (cyan) and goal (orange).config/helhest.yamlnow targets this stack (verified against the Stromovka bags) - Tracker map:
osm_cloudintersections, theroad_followeractive intersection with its enter/exit radii, the follower's waypoint window, a robot trail and a fix-age / stale indicator; the sidebar has a map legend geodesy.ecef_to_latlon_array- Documentation: offline route planning, the
route_plannerconfig file, the traversability rules and the cost tables the graph planner now shares, and the goal QR codes
Changed
route_plannerpreloads the map and caches its graph planners, converts ECEF in a vectorised form and subscribes to static TF only, so a plan request no longer pays for the map on every callsetup.pyinstalls.mapdatafiles and annotation stores, so a planning node finds its map in an installed workspacedata/is no longer tracked (the directory is kept): local working datasets do not belong in the packageconfig/helhest.yamluses the real crl_commander topic names, and the follower threshold now lives inroad_and_gps_follower.yaml- Nodes log through
warning(); on Kiltedwarn()shares one caller id, which collapses distinct warnings into one throttled message - Repo hygiene: the stale
todo_fixes_plan.mdis gone, the unusedpython3-joblibdependency is dropped frompackage.xml,.mypy_cache/is ignored, and a test now asserts that the version stays in step acrosspyproject.toml,package.xml,map_data_interfaces/package.xmlandCITATION.cffand that the released version has a dated changelog section
Fixed
GraphPlanner.plan()re-inserted the raw clicked coordinate at both ends of every segment, so each via point became a degenerate out-and-back off the network. It now returns on-network points only and collapses coincident vertices and sub-metre spurs — a 900 m loop goes from 49 to 37 vertices — whilekeep_start(start_from_robot) still keeps waypoint 0 verbatim, because planning from the robot's pose has to begin where the robot isannotations:apply_added_nodesinserts each added node after its anchor instead of at a running offset, so added nodes land in the right place when a map is loaded for planning- The viewer re-maps segment annotations when a way's split points change
- The viewer's sidebar mode panels scroll when their body is too long: as
flex:1childrenmin-height:autolet them grow past the sidebar and#mainclipped the overflow, leaving the bottom of the planner unreachable osm_cloudno longer clips intersections to the auto grid bounds: those bounds come from the map's query bbox, but ways crossing it are downloaded whole, so crossroads on the route were silently droppedosm_cloudloads the same map as the planner (annotations,exclude_highway); a junction on a retagged or deleted way published a ring nothing routes over- Two field fixes ported from the robot:
TransformListener(..., spin_thread=True)so TF lookups do not depend on the node's own executor, and theutm_to_localparameter reshaped to 4×4 — a flat 16-element list otherwise breaks every transform - Goal codes are served as vectors (
/api/qr.svg) and the modal caption renders as HTML - CI: the OpenCV runtime dependency is declared and the new mypy errors are fixed
[1.3.0] — 2026-08-24
Security
- Fixed two stored-XSS vectors in the viewer: virtual way IDs are now strictly
validated server-side (
<int>or<int>:<int>only) before being stored in the annotation store or change log, the changes/hidden panels build rows via DOM APIs instead of inlineonclickstrings, andescHtmlescapes quotes so attribute-context interpolation (e.g. the tag editor'svalue="...") cannot break out - CSRF protection for cookie-based auth: with
MAP_DATA_ACCESS_TOKENset, the cookie alone now authenticates only safe methods — state-changing requests must carry theX-Requested-Withheader (attached automatically by the viewer's same-origin fetches) or the token header/query parameter; threat model documented in the viewer docs - Request bodies are capped at 100 MB (
MAX_CONTENT_LENGTH), so an oversized upload can no longer fill the data directory cost_gridandcreate_replanvalidate every client parameter up front (bbox sanity, cell budget of at mostMAX_GRID_CELLS, numeric ranges forcell_size/inflate_obstacles/grid_cost_weight, cost-dict shapes, path points within UTM-supported ranges), closing a compute-DoS lever and turning deep planner 500s into clean 400s
Fixed
- Annotation edits no longer race: a shared
annotation_storecontext manager holds the per-file lock across the whole load → mutate → save cycle in all 16 mutating viewer routes, fixing lost updates between concurrent editors and duplicate synthetic node IDs from concurrentadd_way_nodecalls GraphPlannerapplies annotation splits per way in descending segment order (junctions no longer land between the wrong nodes when one way is split in several places) and splices into per-planner node-list copies instead of mutating the sharedMapData(a second planner on the same map no longer raisesKeyError)grid_astardiagonal moves require both edge-adjacent cells to be free, so paths can no longer cut through the corner where two blocked cells touch; grid cells are sampled at their centers, removing the half-cell obstacle-membership bias- Douglas–Peucker simplification in
replan/grid_astarcollision-checks every shortcut it introduces and keeps the original vertices where a chord would cross an obstacle - RRT* rewiring propagates cost changes to descendants and keeps the informed ellipse synced with the goal's true cost, instead of sampling from a stale, oversized ellipse
GraphPlanner.plan()returnsNonefor fewer than two waypoints (per its documented contract) and rejects waypoints farther than a configurablemax_snap_distance(default 100 m) from the network instead of silently snapping to an arbitrarily distant edge- GPX/YAML parsing forces every waypoint into the first point's UTM zone, so a route crossing a zone boundary no longer produces a ~400 km easting discontinuity
- Overpass HTTP-200 error bodies (a
remarkruntime error or an HTML page from a busy mirror) now rotate to the next mirror and retry like server errors instead of crashing the fetch; unparseable bodies — including a corrupt cached response — degrade gracefully - OSM relation inner rings are subtracted from the outer polygons, so courtyards and islands are no longer covered by the merged barrier; member ways consumed by a relation merge are dropped from the parse, eliminating duplicated barrier geometry
- Degenerate OSM ways (too few coordinates for their geometry type) are skipped with a warning instead of aborting the whole parse
.mapdataand OSM-cache writes are atomic (temp file +os.replace), so a crash or full disk mid-save can no longer truncate a previously good filemap_data_inforeports the footway centerline length instead of the buffered-polygon perimeter (which was roughly double the walked distance)MapDataaccepts a 1-Dcurrent_robot_positionand reconciles its elevation column against the waypoints;_csv_to_dicthandles single-row CSVs; loaded instances restore thepointsattribute- Viewer:
cancel_replan,create_wormhole, andcancel_wormholereturn a clean 400 on a missing/non-JSON body (previously an unhandled 500); the two cancel endpoints also 400 on a missingtransfer_id - Abandoned fetch tasks are swept from the registry after the 60 s retention window on every new fetch; cancelled replan transfer IDs are discarded on entry and exit, so a late cancel cannot poison the next replan with the same transfer ID
Added
- Regression tests for every fix above plus coverage for the previously
untested riskiest paths: Overpass body-level failures,
GraphPlannerannotation splicing, the auth/CSRF gate,cost_grid/create_replanvalidation,upload_mapdata, native export, mocked wormhole transfers,info.get_stats,combine_waysbackward-prepend and closed-ring merges, grid A* cost-weighting, and replan cancellation (~130 new tests; suite now 311 tests in ~6 s) tests/conftest.pywith a shared mocked-Overpass fixture, and pytest configuration centralized in[tool.pytest.ini_options]so a plainpytestruns with the same coverage flags locally and in CI
[1.2.1] — 2026-07-15
Security
- Viewer no longer leaks exception detail in HTTP 500 responses (the
upload_gpxandcreate_wormholehandlers now return a generic message and log the real error server-side) - SocketIO CORS now defaults to same-origin instead of a hardcoded
*;MAP_DATA_CORS_ORIGINSopts into a specific origin list - Optional access-token gate via
MAP_DATA_ACCESS_TOKEN(off by default) for network deployments; documented the--host 0.0.0.0attack surface in the viewer docs
Fixed
- Guarded three latent
None-dereference paths surfaced by mypy:buffer_lineraises a clear error for a way with no geometry;osm_cloudraises a clear error if the UTM-to-local transform is still unresolved at startup (rclpy shutdown race) instead of crashing later;upload_gpxrejects a file part with no filename (HTTP 400) instead of raisingTypeError - Fixed
TrackerNode.num_waypointsnever being updated in the ROS node - Fixed polygon rasterization re-blocking holes when drawing obstacles
- Fixed inconsistent UTM zone forcing during map parsing
- Fixed
combine_wayscrash when OSM relation merging yields disconnected MultiLineString geometries - Fixed
Way.to_pcd_pointsignoring thedensityparameter for linestring geometries - Fixed
Way.to_pcd_pointscache returning stale results when called with different arguments - Fixed
parse_gpx_file/parse_yaml_filereturning inconsistent shapes on empty input - Fixed
parse_gpx_fileonly reading waypoints — now falls back to tracks and routes - Fixed RRT*
__main__demo crashing due to tuple start/goal (requiresnp.array) - Synced hardcoded fallback
sandsurface cost (0.7→0.4) withplanner_defaults.yaml - Wired up the dead
mapdata_pathlaunch argument inosm_cloud.launch.py(baremapdata_file/gpx_filenames now resolve against the data directory)
Changed
- Overpass
run_queriesnow filters server-side by the tag families the parser inspects instead of downloading every way/node in the bounding box; matching multipolygon relations and their member ways are recursed in so those obstacles are still classified - Removed the no-op
joblibthreading parallelism fromReplanPath.replan(the A/RRT segment work is GIL-bound); segments run sequentially and the grid cache is warmed once up front, removing a redundant per-segment cache-build race - Removed the now-unused
joblibruntime dependency frompyproject.toml - Deduplicated the triplicated way-resolution pipeline in
viewer/routes.pyinto a shared_resolve_wayhelper, and documented theMapDatashallow-vs-deep copy invariant - Removed the unused multi-robot
robot_idscaffolding from the tracker (parameter, telemetry field, andhelhest.yamlentry) — the viewer targets a single robot
Added
- Static type checking with
mypy([tool.mypy]inpyproject.toml), enforced in CI (typecheckjob) and as a pre-commit hook, to catch bugs like keyword-only-argument mismatches and inconsistent Flask route return types - Interactive viewer:
GET /api/export/geojsonendpoint and toolbar button to download the merged (annotation-resolved) map as a.geojsonfile, for use in QGIS/geojson.io - Unit tests for
osm_cloudpure helper functions (create_grid,points_near_ref,transform_points,split_ways_to_points) pytest-covcoverage reporting in CI.pre-commit-config.yamlfor local ruff lint/format checks- Documented the
map_data_infoCLI (including--validate), the viewer's--data-dir/--host/--portflags, and theTHUNDERFOREST_API_KEY/SEZNAM_API_KEYenvironment variables in the README and viewer docs - Unit test for the
osm_cloudROS node initialization (parameter wiring, publishers, timer) - Tests for
parse_osm_relsmultipolygon member-way tagging,combine_waysdisconnected members,Way.to_pcd_pointscache invalidation, empty-GPX return shape, the off-path zero-cost regression, and launch-argument consumption - NumPy-style docstrings for
pathsolver/grid_constructor.py,viewer/helpers.py, and theviewer/routes.pyhandlers
[1.2.0] — 2026-07-14
Fixed
- Fixed critical positional argument crash in
osm_cloud.py'srun_all - Fixed custom planner costs being silently ignored by the viewer and path planner
- Fixed application crash when refreshing OSM data over a loaded
.mapdatafile - Fixed GPX launch mode being unreachable in
osm_cloud.launch.py - Fixed phantom zero-cost cells appearing off-path due to spacing mismatches
- Fixed non-atomic annotation saves and missing locks causing corruption
- Fixed
__version__single-sourcing (now reads dynamically viaimportlib.metadata) - Fixed double-normalization in quadratic neighbor cost logic
- Fixed viewer routes re-raising HTTP exceptions properly and applying zero-coordinate checks
[1.1.0] — 2026-07-07
Added
- Interactive viewer: base-layer switcher with a satellite imagery option (Esri World Imagery); the selection persists across sessions
- Interactive viewer: copy-to-clipboard buttons for lat/lon in the node inspector
- Interactive viewer: per-annotation "revert geometry" action (↺ in the Annotations panel) to undo geometry drags back to the last loaded state
- Interactive viewer: the planner distance readout now labels whether it shows straight-line waypoint distance or actual planned path length
map_data_viewer --telemetry-rateflag to configure the Tracker telemetry broadcast rate (default 2 Hz)map_data_info --validatechecks a.mapdatafile for structural issues (missing metadata or geometry, duplicate way IDs, nodes missing from the cache, disconnected footway networks) and exits non-zero when any are found
Changed
- The package now targets ROS2 Jazzy or later and Python 3.12+
create_mapdatano longer requires ROS2 — it falls back to the repo data directory whenament_index_pythonis unavailable- Packaging metadata (version, dependencies) is single-sourced in
pyproject.toml;requirements.txtwas replaced by the[dev]extra - CI runs ruff lint/format checks and tests on Python 3.12 and 3.13
Fixed
astar_searchused syntax unavailable on the previously documented minimum Python version- Stale version numbers in
setup.pyand the Overpass User-Agent
[1.0.0] — 2026-05-29
Added
- Interactive viewer: way splitting, node deletion, and node position drag editing
- Interactive viewer: nodes can be inserted into OSM ways by dragging the blue midpoint handles between existing nodes in Edit mode
- Interactive viewer: barrier ways can now be split (✂️ button on nodes), matching the existing road and footway split behaviour; closed barriers are excluded
- Interactive viewer: vertex-level editing for manual annotations in Edit mode
- Interactive viewer: tag editing with save and undo support
- Interactive viewer: audit change log stored in
.annotations.json - Interactive viewer: feature search by ID or name in the sidebar; search now also matches all OSM tag values and, for annotations, the annotation type and all extra property keys and values (e.g. searching "wall" or "barrier" finds matching annotations)
- Interactive viewer: category and subtype visibility toggles
- Interactive viewer: Leaflet.Snap integration for precise annotation alignment
- Interactive viewer: GPX file import for waypoint overlays
- Interactive viewer: GPX upload modal with map name input (previously a stub)
- Interactive viewer: drag-and-drop
.mapdatafile upload;/api/upload_mapdatavalidates viaMapData.loadand saves to the data directory with collision-safe naming - Interactive viewer: Tracker mode for live robot position via ROS2
- Interactive viewer: planner mode can download and parse OSM data on demand
- Interactive viewer: planning parameters (
grid_margin,obstacle_radius,buffer_widths,grid_cost_weight) configurable in all three map creation dialogs (fetch area, GPX upload, planner fetch) via collapsible advanced-options panels; server defaults populate fields on load - Path planning module (
pathsolver) with A* graph search and cost-grid support - Path planning: annotated paths take priority over obstacle cells
- Path planning: paths-only planning mode (constrained to mapped ways)
- Path planning: cancellable planning requests
- New planner config parameters in
planner_defaults.yaml:grid_cost_weight,obstacle_radius, and per-typebuffer_widths(road,footway,barrier) — previously hardcoded in source - OSM response caching: Overpass query results are persisted to a
.osm_cache.jsonsidecar file and reused on subsequent loads when the bounding box matches, avoiding redundant network requests - YAML waypoint format as an alternative to GPX
map_data_infoCLI tool to print statistics about a.mapdatafileosm_cloudlaunch file with configurable grid topic and static transform publishingosm_cloud: dynamic reconfigure support for runtime tuning ofmax_path_dist,neighbor_cost, andgrid_res- Documentation site (MkDocs Material)
- Dedicated
Testingdocumentation page (docs/dev/testing.md) with per-module design notes and guidance for adding new tests pyproject.tomlwith ruff configuration for code style enforcement, compatible with ROS2 builds- Type hints across core and utility modules
- pytest suite covering core logic and path planning edge cases
- Expanded test suite:
test_overpass.py(retry logic, rate limiting, status polling),test_errors.py(malformed GPX, corrupt files, Overpass timeouts, planning failures),test_parsing.py(OSM element classification and buffering),test_fill_grid.py(footway cost assignment, barrier cell marking),test_viewer_helpers.py(GeoJSON roundtrip, way splitting, change log migration),test_viewer_routes.py(annotation CRUD, path-traversal security, way operations), and extendedtest_integration.py(OSM cache roundtrip, bbox mismatch,parse_intersections)
Changed
- Interactive viewer: GPX download now exports a
<trk><trkseg><trkpt>track by default; a Track / Waypoints toggle in the planner panel switches back to the legacy<wpt>format - Interactive viewer: annotation vertex handles in Edit mode now only appear when the annotation is clicked, not for all annotations at once
- Interactive viewer: annotation vertex handles now use the same orange circle + midpoint style as OSM node editing, replacing Leaflet.draw square handles
- Interactive viewer: transparent overlay markers provide larger click/drag hit targets for OSM nodes, annotation vertices, and planner waypoints without changing their visual appearance
- Interactive viewer: node drag in Edit mode is more responsive
visualize_mapdataCLI tool removed; the browser-based viewer supersedes itgrid_cost_weight,obstacle_radius, andbuffer_widthsare now per-run arguments togrid_astar,RRTStar, andReplanPath;osm_margin + reserve_marginreplaced with a singlegrid_marginconstant (150 m default) accepted as a per-call override inMapData.__init__,parse_osm_nodes, andseparate_waysMapData.get_points()now vectorizes UTM conversion by passing full lat/lon arrays toutm.from_latlonin one call instead of looping per node; all nodes are projected into the map's zone for consistencyRRTStarnow supports Informed RRT* sampling (informed=True, default): once an initial path is found, random samples are drawn from an ellipsoidal subset defined by the current best cost, accelerating convergence toward the optimumRRTStarnow supports adaptive neighbor radius (adaptive_radius=True, default): the rewiring radius shrinks asγ·√(log n / n)so the number of rewiring checks stays bounded while preserving asymptotic optimalitysmooth_pathnow returns the best collision-free intermediate state instead of reverting to the original path entirely when a smoothed segment collides with an obstacleReplanPathrefactored into focused sub-modules: grid construction moved toPathGrid(pathsolver/grid_constructor.py), path smoothing tosmooth_path(pathsolver/smoothing.py), and matplotlib debug visualization tovisualize_replan(pathsolver/visualizer.py)- Core architecture split into modular components:
OverpassClient,parsing,serialization - Config loading centralized to
map_data/utils/config.py, eliminating duplicated YAML-loading logic frommap_data.pyandreplan.py - Logging configuration centralized in
setup_logging()(map_data/utils/config.py);info.py,create_mapdata.py, andviewer/app.pynow call it insidemain()instead of invokinglogging.basicConfigat module level with inconsistent formats .mapdataserialisation migrated from pickle to JSON + WKT; legacy pickle support was subsequently removed for security reasons- Overpass queries parallelised for faster map data loading
Wayclass refactored to a@dataclasswith full type hintsos.pathreplaced withpathlib.Paththroughout the codebase- Code formatting standardized via ruff across all source modules
pyproject.tomlandsetup.pyaligned for ROS2 build compatibility
Fixed
visualizer.py: swapped X/Y axis labels corrected — Easting is now on the X axis, Northing on Y- Interactive viewer: node drag in Edit mode missed clicks intermittently because canvas node markers rendered below the SVG ghost layer; node markers now use the SVG renderer so events reach them directly
- Interactive viewer: adding a node to a buffered (Polygon) way drew a faint spike from the buffer outline back to the new node position because
apply_added_nodesinserted the centerline coordinate into the exterior ring of the buffer polygon; geometry is now left unchanged for Polygon ways and onlyway.nodesis updated - Interactive viewer: split segments of buffered footways and barriers were rendered thinner than the original after a node had been added to the way; the spike inserted into the buffer polygon corrupted its area/perimeter, causing
split_wayto calculate a smaller-than-correct buffer radius - Interactive viewer: splitting a way at a synthetic (user-added) node failed silently because
apply_added_nodeswas not called beforesplit_wayin the segments and node-list endpoints; synthetic nodes are now inserted into the way geometry and an extended nodes cache is built so the split resolves correctly - Interactive viewer: added nodes did not appear anywhere in the sidebar; the
add_nodeevent is now written to the change log on creation and removed on undo, and added-node entries are shown in the Annotations panel with an undo button - Interactive viewer: deleting one segment of a split way caused the entire original way to disappear on the next file reload; segment deletions are now stored under their virtual ID (
"<id>:<index>") so only the deleted segment is suppressed while the remaining segments survive - Interactive viewer: reverting any annotation edit (tag override, node deletion, node move) on a way that had also been split left the stale pre-revert geometry on the map alongside the newly restored geometry until page reload;
_reloadWaynow always uses the segments endpoint so split virtual layers (id:n) are atomically replaced - Interactive viewer: Closed roads and footways (e.g. roundabouts) were rendered as filled polygons instead of an annular ring:
buffer_linenow converts a closed-loopPolygonto aLineStringbefore buffering, unless the way carriesarea=yes - Interactive viewer: tag change-log entries silently disappeared from the changes panel after any metadata refresh (
refreshMetadata/loadMapData) becausetagMapused numeric keys while the server'schange_logstores tag ids as strings; both call-sites now normalise to string keys - Interactive viewer:
_reselectFeatureandfocusFeatureByIdnow compareString(_featureId) === String(wayId); previously strict equality failed to reselect real OSM ways (numeric_featureId) after a reload that converted the ID to a string, leaving the feature visually deselected - Interactive viewer: duplicate
clicklisteners onway-edit-saveandway-edit-add-prop-btncaused double row insertion and double API calls on each action - Interactive viewer: planner
mousemove/mouseuphandlers accumulated on the map with eachredraw()call; handlers are now tracked in_mapDragListenersand removed viamap.off()before each redraw - Interactive viewer:
fetch_areaand OSM data parsing now run in a background thread; the route returns a task ID immediately and the client polls/api/fetch_area/<task_id>for completion, preventing UI hangs and WebSocket timeouts during long Overpass fetches - Annotation deletion via the Del key in the viewer
- Path traversal vulnerability in viewer API: user-supplied
fileparameter is now validated against the resolved data directory before any file access /api/fetch_areanow rejects requests wheremin_lat >= max_latormin_lon >= max_lonparse_yaml_filenow wraps all parse errors in a try/except and returns[]with a log message, matching the error contract ofparse_gpx_file- Path planning with split ways
- Thread-safe cancellation in the replanning module
create_mapdatanode: existing file load was missing the.mapdatasuffix, causingMapData.loadto receive an incorrect pathcreate_mapdatanode:--downloadflag was passed as a positional argument toprocess_map_data, now correctly passed as a keyword argument-
Multi-zone UTM boundary warning when loaded area spans two UTM zones
-
route_plannerpreloads its map and caches oneGraphPlannerper map / way set / snap distance (plan_route(planner=...)); a request plans in milliseconds instead of rebuilding the graph. -
Planner screen: Robotour goal QR codes per waypoint (
/api/qr,map_data.utils.qr) shown full screen for the robot camera or downloaded as PNG. -
Goal QR codes are vector art (
/api/qr.svg,qr_svg): the caption used to be rasterised into the PNG, so it went blocky when the viewer scaled the code up to fill the screen. The modal now shows an SVG with the caption as HTML text beside it, and offers an SVG download for printing.