Path planning
The pathsolver module provides standalone planning capabilities — no ROS 2 context is required.
Use the map_data_plan CLI to process waypoints against a .mapdata file, import the
planners directly as a Python library, or run the route_planner ROS 2 action server.
Offline route planning: map_data_plan and route_planner
The viewer's Planner screen (POST /api/create_replan), the map_data_plan command-line
tool, and the route_planner ROS 2 action server all call map_data.pathsolver.route.plan_route
on the same map data: the .mapdata file with its <stem>.annotations.json sidecar merged in
(map_data.annotations.load_mapdata_with_annotations). Whatever is drawn, deleted, or split
in the viewer is therefore reflected during planning.
Both tools support an explicit annotation choice: --annotations auto|none|FILE on the command
line, or the annotations parameter on the node. none plans on the unedited OSM map, which
is useful when an annotation store was pruned for another area.
Planner defaults come from config/planner_defaults.yaml (cell_size, inflate_obstacles,
simplify_path, smooth_path, max_path_dist, grid_cost_weight). route_planner exposes
the first four as ROS 2 parameters, re-read on every goal.
Goal QR codes (Planner screen)
Robotour provides goals as QR codes with a geo:lat,lon payload. In the Planner screen,
right-click any waypoint and choose QR code, or press QR goal for the last waypoint:
the code opens full screen on a white background (size slider) so a laptop or tablet can be
held to the robot camera, with Download SVG (or PNG) for printing and Copy geo text for typing the
goal in with qr_goal_send. The code comes from GET /api/qr.svg?lat=…&lon=…[&scale=12][&download=1][&caption=]
as vector art, so it stays sharp at any size; GET /api/qr?… serves the same code as a PNG.
Command line: map_data_plan
# Bundled demo map (the two waypoints of demo/buchlovice.gpx, ~420 m)
map_data_plan -f demo/buchlovice.mapdata --start 49.084416,17.336309 --goal 49.083106,17.338527
# Paths only (graph planner on footways), 3 m waypoint spacing, saved as a GPX track
map_data_plan -f coords.mapdata --start 50.1038,14.4294 --goal geo:50.1067,14.4193 \
--spacing 3 --save route.gpx
# All-terrain (grid A*) through intermediate via points, printed as JSON
map_data_plan -f coords.mapdata -p 50.1038,14.4294 -p 50.1050,14.4250 -p 50.1067,14.4193 \
--algorithm astar --cell-size 0.5 --json
Command-line options
| Flag | Default | Description |
|---|---|---|
-h, --help |
Show help message and exit | |
-f FILE, --file FILE |
required | .mapdata file path or filename in data/ (or the source checkout's demo/) |
--annotations auto\|none\|FILE |
auto |
Annotation store to merge: auto (<file>.annotations.json), none, or explicit file |
--start START |
Start coordinates as lat,lon |
|
--goal GOAL |
Goal coordinates as lat,lon or geo:lat,lon |
|
-p POINTS, --point POINTS |
Intermediate via point lat,lon (repeat for multiple points between start and goal) |
|
--algorithm {graph,astar,rrt} |
graph |
Planning algorithm: graph (on ways only), astar or rrt (all-terrain grid) |
--ways TYPE [TYPE ...] |
footway |
Allowed way types: footway, road, or both (--ways footway road) |
--traversability FILE |
package default | Tag rule file determining drivable ways and costs (config/traversability.yaml) |
--no-traversability |
false |
Ignore traversability rules entirely (stairs remain excluded) |
--spacing SPACING |
0.0 |
Maximum meters between output waypoints (0.0 keeps planner vertices) |
--max-snap-distance DIST |
100.0 |
Graph: reject waypoints farther than this (m) from every allowed way |
--cell-size CELL_SIZE |
0.25 |
Grid: cell resolution in meters |
--inflate-obstacles DIST |
0.25 |
Grid: barrier inflation margin in meters |
--simplify, --no-simplify |
--simplify |
Grid: simplify path vertices using Douglas-Peucker (--no-simplify retains all cells) |
--smooth, --no-smooth |
--no-smooth |
Grid: smooth path using gradient-descent optimization |
--save SAVE |
Write route to GPX file | |
--wpt |
false |
Write GPX <wpt> elements instead of a <trk> track |
--json |
false |
Print planning result as JSON |
-v, --verbose |
false |
Enable verbose debug logging |
ROS 2 action server: route_planner
The route_planner node exposes an action server on /route_planner/plan_route (map_data_interfaces/action/PlanRoute).
ros2 launch map_data route_planner.launch.py mapdata_file:=coords.mapdata
ros2 launch map_data route_planner.launch.py mapdata_file:=coords.mapdata highway_types:=footway,road
ros2 action send_goal /route_planner/plan_route map_data_interfaces/action/PlanRoute \
"{waypoints: [{latitude: 50.1067, longitude: 14.4193}], start_from_robot: true}"
Launch arguments (route_planner.launch.py)
| Argument | Default | Description |
|---|---|---|
params_file |
route_planner.yaml |
Parameter file: absolute path or filename in config/ |
mapdata_file |
"" |
Default .mapdata filename or path |
mapdata_path |
"" |
Directory where bare .mapdata names are resolved (default: package share data) |
annotations |
"" |
Annotation store: auto, none, or explicit file path |
traversability |
"" |
Tag rule file deciding traversable ways (empty = from params_file) |
mission_dir |
"" |
Directory where GPX routes are saved |
gps_fix_topic |
"" |
Topic for sensor_msgs/NavSatFix |
earth_frame |
"" |
ECEF frame name for exact TF transform |
local_frame |
"" |
Local frame name for nav_msgs/Path |
algorithm |
"" |
Default algorithm: graph, astar, or rrt |
highway_types |
"" |
Allowed way types: footway, road, or footway,road |
spacing |
"" |
Meters between output waypoints (0 = raw) |
preload |
"" |
Load map and graph at startup (true or false) |
use_sim_time |
false |
Use simulation time |
ROS 2 parameters (route_planner)
Parameters in PLANNER_PARAMETERS (algorithm, highway_types, spacing, max_snap_distance,
cell_size, inflate_obstacles, simplify_path, smooth_path) are re-read on every goal,
allowing dynamic updates via ros2 param set.
| Parameter | Default | Description |
|---|---|---|
mapdata_file |
"" |
Default .mapdata filename or absolute path |
data_dir |
package share data |
Directory where bare .mapdata filenames are resolved (then the package demo/) |
annotations |
auto |
Annotation store merged into the map: auto, none, or file path |
exclude_highway |
["steps"] |
Non-routable highway types excluded from graph and map |
traversability_file |
"" |
Tag rule file ("" = package config/traversability.yaml). Traversability rules warning |
preload |
true |
Load map and build graph at startup |
mission_dir |
~/missions |
Directory where planned GPX tracks are saved |
gps_fix_topic |
/fixposition/odometry_llh |
NavSatFix topic for start_from_robot |
goal_topic |
~/goal |
GeoPointStamped topic that triggers planning from current robot fix |
route_topic |
~/route |
Latched geographic_msgs/GeoPath topic |
route_path_topic |
~/route_path |
Latched nav_msgs/Path topic in local_frame |
status_topic |
~/status |
Latched std_msgs/String status topic |
earth_frame |
FP_ECEF |
ECEF frame name for exact coordinate conversion |
local_frame |
FP_ENU0 |
Target local frame for route_path |
fix_max_age |
10.0 |
Maximum age (s) before GPS fix is considered stale |
algorithm |
graph |
Fallback algorithm (graph, astar, rrt) |
highway_types |
["footway"] |
Fallback way types (footway, road, or both) |
spacing |
3.0 |
Fallback waypoint spacing in meters |
max_snap_distance |
100.0 |
Maximum snap distance (m) from start to nearest way |
goal_max_snap_distance |
30.0 |
Maximum snap distance (m) for goal; farther fails with snap_too_far |
outside_map_tolerance |
100.0 |
Allowed distance (m) endpoints may lie outside map bounds |
keep_goal |
true |
End route at exact goal coordinate rather than projected point |
cell_size |
0.25 |
Grid cell size (m), defaults from planner_defaults.yaml |
inflate_obstacles |
0.25 |
Obstacle inflation buffer (m), defaults from planner_defaults.yaml |
simplify_path |
true |
Simplify grid path, defaults from planner_defaults.yaml |
smooth_path |
false |
Smooth grid path, defaults from planner_defaults.yaml |
Goal execution, GPX saving, and waypoint spacing
- Cancellable goals: Action goals can be cancelled during execution. When cancelled, the action returns status
CANCELLEDand failure reasoncancelled. - Boolean goal fields: Goal booleans (
simplify_path,smooth_path) are evaluated as sent without falling back to node defaults. save_gpxfield:- Empty string (
""): automatically writes a timestamped track tomission_dir/route_<timestamp>.gpx. "-": disables GPX file output.- A
.gpxfile name: writesmission_dir/<name>. A path,..or another suffix is refused (logged, nothing saved), since any host on the network can send goals.
- Empty string (
spacingsemantics:spacing > 0: resamples the path so consecutive waypoints are at mostspacingmeters apart. Below0.1fails withbad_parameter.spacing == 0: uses the node's default spacing (3.0 m).- Negative spacing (
spacing < 0, e.g.-1.0): preserves raw planner vertices without densification.
Action failure reasons
When route planning fails, PlanRoute.Result sets success = false and provides a machine-readable reason:
| Reason | Meaning |
|---|---|
bad_parameter |
Unknown algorithm or sub_algorithm, non-positive or non-finite cell_size, negative or non-finite inflate_obstacles, a positive spacing below 0.1 or infinite, or a waypoint UTM cannot express (latitude outside -80..84, non-finite) |
start_blocked |
Grid start coordinate lies inside an obstacle and cannot be snapped to free space |
goal_blocked |
Grid goal coordinate lies inside an obstacle and cannot be snapped to free space |
cancelled |
Planning request was cancelled |
busy |
Another plan is still running; the goal is not queued, send it again |
start_outside_map |
Start coordinate lies farther than outside_map_tolerance outside the map area |
goal_outside_map |
Goal coordinate lies farther than outside_map_tolerance outside the map area |
snap_too_far |
Waypoint exceeds maximum snap distance to the nearest allowed way |
unreachable |
Graph planner found no connection between endpoints in the way network |
no_path |
Grid planner found no collision-free path between endpoints |
grid_too_large |
Requested bounding area exceeds max_grid_cells |
route_too_long |
The route would have more than 200 000 waypoints (MAX_ROUTE_POINTS) |
too_few_points |
Fewer than two waypoints supplied without start_from_robot |
file_not_found |
Map data file could not be found or loaded; a goal's mapdata_file must be a relative name inside data_dir |
internal_error |
A bug in the planner; the node logs the traceback and keeps running |
no_fix |
start_from_robot requested but no recent GNSS fix was received on gps_fix_topic |
Python library usage
from map_data.annotations import load_mapdata_with_annotations
from map_data.pathsolver.route import RoutePlanningError, plan_route
from map_data.utils.gpx import create_gpx_track
md, _ = load_mapdata_with_annotations("demo/buchlovice.mapdata")
try:
route = plan_route(md, [(49.084416, 17.336309), (49.083106, 17.338527)], spacing=3.0)
except RoutePlanningError as e:
print(f"Planning failed ({e.reason}): {e.message}")
else:
with open("route.gpx", "w") as f:
f.write(create_gpx_track(route.latlon))
Traversability rules
Which OSM ways the robot may drive on is decided from their tags by
config/traversability.yaml, the single file to edit when routing rules change. The rules are
applied when a map is loaded for planning (load_mapdata_with_annotations), ensuring that
route_planner, osm_cloud, and intersection generation all observe the same network.
Keep traversability rules in sync between nodes
Both route_planner and osm_cloud must be configured with the same traversability file. If they differ, osm_cloud may publish intersection rings and cost clouds for ways that route_planner refuses to route over. route_planner detects rule file modifications automatically on each new goal, whereas osm_cloud reads the rule file once at startup and must be restarted after edits.
default:
traversable: true
cost: 0.0
rules:
- match: {highway: steps} # exact value
traversable: false
reason: stairs
- match: {surface: [grass, mud, sand]} # any of these values
traversable: false
reason: soft surface
- match: {bridge: boardwalk} # boardwalk bridges refused
traversable: false
reason: bridge
- match: {smoothness: [bad, very_bad, horrible, very_horrible, impassable]}
traversable: false
reason: rough surface
- match: {access: [no, private]}
traversable: false
reason: no access
- match: {highway: path, informal: yes} # multiple keys: all must match (AND)
cost: 1.0
reason: informal path
- Rules are evaluated in order and the first match wins. A way that matches no rule receives
default. traversable: falseremoves the way: it is never routed over, its junctions receive no intersection rings, and it is excluded fromosm_cloud.cost:is optional and additive on top ofhighway/surfacecosts fromconfig/planner_defaults.yaml: graph edge weights are(1 + way cost + rule cost) × length.- Rule and tag values are compared as strings (
informal: yesmatchesinformal: "yes"). - Shipped defaults refuse stairs, soft surfaces (
grass,mud,sand), boardwalk bridges (bridge: boardwalk), rough surfaces, and non-accessible ways.
Available algorithms
Graph planning
Plans a path by searching the OSM road and footway network using A*. The route is constrained
to follow existing ways, making it suitable for on-road navigation. Its edge weights are
length × (1 + way cost) using highway_costs/surface_costs tables plus any additive cost
from traversability rules.
Walkable areas — closed area=yes (or multipolygon) ways such as pedestrian squares — are
crossed directly rather than circumnavigated. The planner computes the shortest line-of-sight
path within the polygon boundary between entry and exit points.
Grid A*
Discretizes the area around the route into a uniform grid and runs A* on it.
Each cell carries a cost (see Cost grid); cells inside barrier polygons are impassable.
Grid A* is complete (finds a path if one exists) and provides predictable runtime. Resolution
and obstacle inflation are tuned via --cell-size and --inflate-obstacles.
RRT*
Rapidly-exploring Random Tree Star is a sampling-based planner that builds a tree of collision-free waypoints by randomly sampling the free space. RRT* is well suited for large open areas. If RRT* exceeds its iteration budget without finding a path, Grid A* runs on the same grid as a fallback.
Cost grid
ReplanPath.fill_grid() converts OSM map data into a 2-D NumPy array of floating-point costs,
one value per grid cell representing a cell_size × cell_size meter patch.
Cell cost assignment:
- Barrier polygons (inflated by
inflate_obstacles, minus the ways ofhighway_typesthat cross them, such as a footway through a gate) are rasterized as impassable (cost = inf). - For each remaining cell, the distance to the nearest way centerline is computed.
-
If that distance is $\le$
max_path_dist, the cell receives a blended cost:$$\text{cost} = \text{way_cost} + (\text{default_off_path_cost} - \text{way_cost}) \times \left(\frac{\text{dist}}{\text{max_path_dist}}\right)^2$$
where $\text{way_cost} = \min(\text{path_cost_cap}, \text{highway_cost} + \text{surface_cost})$.
-
Cells beyond
max_path_distfrom any way receivedefault_off_path_cost(default0.9).
Obstacle thresholds:
- In Grid A*, only cells with cost
inf(np.isinf) are treated as impassable obstacles. Cells with finite costs (even $\ge 1.0$) are traversable with weighted traversal cost. - In RRT*, cells with cost $\ge$
traversability_threshold(default10.0) orinfare treated as obstacles.
Performance tuning
Grid resolution (cell_size)
cell_size is the dominant factor in memory consumption and planning time.
cell_size |
Grid cells per 100 m² | Typical use case |
|---|---|---|
0.5 m |
400 | Coarse survey, large open areas |
0.25 m |
1 600 | Default — balanced resolution for pedestrian routes |
0.1 m |
10 000 | Narrow gates, tight corridors |
Obstacle inflation (inflate_obstacles)
Increasing inflate_obstacles adds a safety margin around barriers at the expense of
potentially closing narrow corridors. If planning returns None, consider reducing this
value.
Path post-processing
simplify_path(Douglas-Peucker) drops collinear and redundant vertices, reducing waypoint count without altering path geometry.smooth_pathapplies gradient-descent smoothing to round sharp corners.
Algorithm choice
| Situation | Recommended algorithm |
|---|---|
| Mapped pedestrian or road network, robot must stay on ways | Graph planning |
| Urban route with well-defined barrier obstacles | Grid A* |
| Large open area, sparse obstacles | RRT* |
Using astar_search directly
astar_search is the generic A* implementation GraphPlanner uses (grid_astar has its
own array-based loop). You can use it directly to build a custom planner for any graph-like problem.
from map_data.pathsolver.astar import astar_search
# Example: plan over a simple weighted grid
def neighbors(node):
r, c = node
candidates = [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]
return [(n, 1.0) for n in candidates if 0 <= n[0] < 10 and 0 <= n[1] < 10]
def heuristic(node):
goal = (9, 9)
return abs(node[0] - goal[0]) + abs(node[1] - goal[1])
path = astar_search(
start_node=(0, 0),
goal_node=(9, 9),
get_neighbors_func=neighbors,
heuristic_func=heuristic,
)
# path is a list of (row, col) tuples from start to goal, or None if unreachable
get_neighbors_func must return an iterable of (neighbor_node, edge_cost) tuples.
heuristic_func must be admissible (never overestimate the true cost to the goal) for A* to
return an optimal path.