Skip to main content

Maps

The Navigation widget displays a robot on a map: an image, a resolution, and an origin that place the robot's localization data in pixel space. OpenRobOps supports two kinds of map.

Robot maps vs shared maps

A robot map is the occupancy grid a robot itself publishes over MQTT as it maps or localizes; it belongs to that robot and no other robot can use it. A shared map is uploaded once through the Config API's SpatialAnnotation kind at scope: system and is available to every robot that lists it — for example a facility floor plan drawn outside of SLAM, or a map shared by robots that can't produce their own (see ISO robots below). SpatialAnnotation can also store a robot-owned map (scope: <robotId>), but the common case is a shared map at system scope.

Uploading a shared map

tools/png2map.py turns a PNG into the YAML SpatialAnnotation document and prints it to stdout:

tools/png2map.py warehouse.png --id warehouse-floor-1 --frame map \
--resolution 0.05 --x 0 --y 0 --label "Warehouse Floor 1" \
> warehouse-floor-1.yaml

--from-ros-yaml map.yaml lifts resolution and origin from a ROS map_server YAML file when you're converting an existing ROS map (values also given on the command line win). Apply the result like any other Config API object:

inorbit apply -f warehouse-floor-1.yaml # or POST /api/configuration/apply

Only PNG images are accepted, capped at 12 MB decoded.

Frames and transforms

Every map has a frameId — the coordinate frame its image is drawn in. Robot localization grids are normally in the map frame, and most shared maps declare frameId: map too, so no extra setup is needed.

When a shared map's frameId differs from a robot's own frame, the widget needs a SpatialTransformation linking the two frames (either an explicit matrix or ≥3 reference point pairs). Without one, the map is still shown but the robot is hidden and a banner explains that no transform links the two frames.

To obtain that transform for a floor plan drawn outside of SLAM, either pick three or more landmarks visible in both images and give their coordinates as reference points, or let tools/align_maps.py fit it: it registers the walls of the robot's grid onto the plan (coarse search plus ICP) and prints the SpatialTransformation document, fit statistics and an overlay image to check by eye. The step-by-step guide, including the formatVersion y-axis convention that must match each image, is in docs/map-alignment.md.

This is exactly how the ISO 21423 facility coordinate system (CCS) works: the CCS is a frame in its own right, and the system map → <ccs.id> transform — described in the ISO Robots setup guide — is what lets ORO place ISO robots (whose poses arrive in the CCS) on a map drawn in the map frame, or vice versa.

Robot footprint

The Navigation widget draws every robot as an outline: a polygon, a plain circle sized by a radius, or a default 0.45 m ring when nothing else is configured or reported. Configure it with the RobotFootprint kind, at system scope (fleet default) or a robot id (per-robot override).

resolveFootprint resolves each field (footprint, bufferFootprint, radius, primaryColor, secondaryColor, opacity) independently:

  1. The robot's own RobotFootprint entry, when that field is defined there (a null counts as defined -- see suppression below).
  2. Otherwise the system entry's value for that field.
  3. If, after that merge, neither footprint nor radius is defined at all (left unset by both entries, not merely null), the robot's own reported outline is used instead, when it has reported one -- currently only ISO 21423 robots, from identity.details.imrFootprint (see ISO Robots setup). Otherwise the widget falls back to its default ring.

A robot can hide a fleet-wide footprint by applying spec: null for its own id: this writes footprint, bufferFootprint and radius as null, which step 1 treats as defined (so the system value is skipped for those fields) but which is dropped before rendering -- landing the robot on the widget's default ring, not its reported outline (step 3 only applies when a field is left undefined, and suppression defines it as null). Removing the entry entirely with clear, instead of suppressing it, is what restores the system → reported → default chain. primaryColor/secondaryColor/opacity are unaffected by suppression.

bufferFootprint is stored and returned by the REST endpoint below, but not yet drawn by the widget.

Query the resolved footprint for one robot with GET /api/robots/{robotId}/footprint.

Robot paths

The Navigation widget draws each robot's paths from localization.paths.<pathId>, one line/point set per path id, fading as the data ages. Style them with the RobotPath kind, at system scope (fleet default) or a robot id (per-robot override); a robot's entry for a path id replaces the system entry for that same id outright, not merged field by field.

ISO 21423 robots populate two paths:

Path idSourceUpdate rate
"0"ISO globalPlan -- e.g. the flatland agent's /plan subscription, forwarding nav2's global planIngest writes at most once per second per path (latest wins)
"1"ISO localTrajectory -- nav2's /local_planThe agent publishes it at ISO_LOCAL_TRAJECTORY_HZ (2 Hz by default); ingest still caps writes to once per second

Path id "0" matches the id the InOrbit ROS2 agent uses for its own /plan topic, so one RobotPath config styles both wire and ISO robots. ISO's globalPath (a NURBS curve, distinct from globalPlan) is a separate resource and is not consumed -- see ISO Robots setup: known limitations. Wire robots keep whatever path ids their own agent already uses; RobotPath styles by path id, independent of robot type.

Fleet view

The Navigation and Map widgets draw every robot the user can see, not just the selected one -- online robots the user has view access to; offline robots aren't drawn. Click any robot to select it.

Each robot is placed independently: its own frame (localization.map.frameId, or map when unset) is transformed into the displayed map's frame the same way as in Frames and transforms above -- a robot-scope SpatialTransformation first, then the system-scope one, or the identity transform when the frames already match. A robot with no transform to the map's frame is left off the map and counted in a small note, N robot(s) not shown: no transform to "<frame>", instead of blanking the whole map. The "No transform ... for the selected robot" banner -- and the map interactions it hides -- is shown only when the selected robot itself can't be placed.

Name labels are drawn by the Names layer, on by default. Layer visibility is remembered per dashboard section, but there's no layers menu in the control bar yet to toggle it from the UI.

Only the selected robot gets full detail -- lasers, paths, costmap -- and live MQTT updates. Every other robot is updated from the server at ingest rate, pose only.

Switching maps in the Navigation widget

When a robot can display two or more maps, a map switcher dropdown appears in the Navigation widget's control bar. Shared maps are labelled "(shared)" in that list to distinguish them from the robot's own maps.

ISO robots

ISO 21423 robots have no occupancy-grid resource of their own — the standard doesn't define one — so they never have a robot map. Shared maps are the only maps an ISO robot can display. Ingest converts every ISO pose into the map frame on the way in, so a map drawn in the facility CCS frame (frameId: <ccs.id>) still needs the system map → <ccs.id> transform to place the robot on it — the same transform CcsConverter.load() seeds from settings.iso21423.ccs when no SpatialTransformation exists yet (see Frames and transforms above).