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:
- The robot's own
RobotFootprintentry, when that field is defined there (anullcounts as defined -- see suppression below). - Otherwise the
systementry's value for that field. - If, after that merge, neither
footprintnorradiusis defined at all (left unset by both entries, not merelynull), the robot's own reported outline is used instead, when it has reported one -- currently only ISO 21423 robots, fromidentity.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 id | Source | Update rate |
|---|---|---|
"0" | ISO globalPlan -- e.g. the flatland agent's /plan subscription, forwarding nav2's global plan | Ingest writes at most once per second per path (latest wins) |
"1" | ISO localTrajectory -- nav2's /local_plan | The 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).