Skip to content

Annotations and edits

Load a map the way the viewer shows it: with its annotation store merged and the traversability rules applied.

from map_data.annotations import load_mapdata_with_annotations

md, store = load_mapdata_with_annotations("my_area.mapdata")

Merging a store

Apply a viewer annotation store to a :class:~map_data.map_data.MapData.

The viewer keeps every user edit of a map (deleted ways/nodes, splits, moved nodes, tag overrides, drawn paths and obstacles) in <stem>.annotations.json next to the .mapdata file. Merging it here, outside the web app, lets scripts, map_data_plan and the ROS nodes use the map the viewer shows. :func:load_mapdata_with_annotations is the one-call entry point and :func:apply_store the merge itself.

load_mapdata_with_annotations(mapdata_path, annotations_path=None, exclude_highway=NON_ROUTABLE_HIGHWAY_VALUES, traversability=None)

Load a .mapdata file and merge its annotation store.

annotations_path defaults to :func:annotation_path_for; a missing store yields the unedited map, :data:NO_ANNOTATIONS skips it.

traversability is a :class:~map_data.traversability.TraversabilityRules, a rule file, or None for the package's config/traversability.yaml. It is applied after the store, so tag overrides affect it. exclude_highway values become one leading deny rule. Pass an empty iterable and TraversabilityRules() to keep every way.

Returns (map_data, store).

edit_mapdata(md, source_path, annotations_path=None, exclude_highway=NON_ROUTABLE_HIGHWAY_VALUES, traversability=None)

Apply the traversability rules and the annotation store to a freshly built md.

:func:load_mapdata_with_annotations for a map not read from a .mapdata file (see there for the arguments). source_path names the default store.

apply_store(md, store)

Apply every kind of edit in store to a copy of md and return it.

No pass mutates a Way, a node entry or the waypoints; they rebind the map's attributes, except :func:merge_annotations, which appends to the way lists and adds nodes to nodes_cache. A shallow copy with its own containers therefore leaves md untouched, far cheaper than a deepcopy.

annotation_path_for(mapdata_path)

<dir>/<stem>.annotations.json for a .mapdata file.

load_summary(path, md, store, annotations, exclude_highway, trav_path, *, crossroads=False)

One-line "loaded : ..." summary of a loaded map.

annotations is the store argument used when loading (None for the default sidecar), trav_path the resolved traversability rule file.

Store and way edits

The JSON annotation store and the way-edit pipeline.

The pipeline rebuilds a :class:~map_data.utils.way.Way's nodes and line from stored edits (deleted/moved/added/split nodes), for both LineString ways and buffered-Polygon ways.

AnnotationStoreError

Bases: Exception

The annotation store exists but is not a readable JSON object.

load_annotations(path)

Load the annotation store from path, or {"version": 1, "annotations": []} if absent.

Raises:

Type Description
AnnotationStoreError

If path exists but cannot be read, is not valid JSON, or is not a JSON object. Treating it as empty would let the next edit overwrite every edit it holds.

annotation_store(path)

Yield the store at path for in-place mutation, under its lock, and save it on exit.

The per-path lock is held across load -> mutate -> save, so concurrent edits cannot overwrite each other. The store is written (atomically) only when the body exits normally; if it raises (including abort()) nothing is saved. An unreadable store raises :class:AnnotationStoreError before the body runs, so it is never overwritten.

quarantine_annotation_store(path)

Move an unreadable store at path aside to <path>.corrupt-<timestamp>.

Runs under the store's lock and re-checks the file first, so a store another request already replaced with a good one is left alone.

Returns:

Type Description
Path or None

The backup path, or None if the store is readable (or gone).

split_way(way, split_nids, zone_number, zone_letter, nodes_cache, detached=None)

Split a way into segments at the given interior node IDs.

A LineString way is cut along its own coordinates; a buffered Polygon way's centerline is rebuilt from nodes_cache and each segment re-buffered by :func:_buffer_radius. Returns [way] unchanged when there is nothing to split: no split_nids, no geometry or nodes, a closed way, or fewer than two resulting segments. Split IDs at the first or last node are ignored.

Parameters:

Name Type Description Default
way Way

The way to split.

required
split_nids list of int

Interior node IDs at which to split the way.

required
zone_number int

UTM zone number.

required
zone_letter str

UTM zone letter.

required
nodes_cache dict or None

{node_id: {"lat": ..., "lon": ...}}; needed for Polygon ways and detached split ends.

required
detached dict of {int: int}

Split node ID -> synthetic node ID that starts the following segment instead (see :func:get_detached_node_ids).

None

Returns:

Type Description
list of Way

Shallow copies of way with their own nodes and line and virtual IDs f"{way.id}:{index}", or [way].

apply_node_position_overrides(way, overrides, zone_number, zone_letter, nodes_cache=None, category=None)

Return a copy of way with node positions moved per overrides.

Each node's position comes from overrides, else nodes_cache, else way.line (least reliable: a Polygon's ring is the buffer outline, not the centerline). A LineString is rebuilt from the new coordinates, a Polygon by :func:_rebuild_polygon. A way without nodes (a point barrier) is translated so its centroid lands on the first override.

Parameters:

Name Type Description Default
way Way

The way whose node positions to override.

required
overrides dict of {int: dict}

{node_id: {"lat": float, "lon": float}}.

required
zone_number int

UTM zone number.

required
zone_letter str

UTM zone letter.

required
nodes_cache dict or None

{node_id: {"lat": ..., "lon": ...}} cache for node positions.

None
category str

"barrier" makes a closed Polygon a flat area rather than a buffered ring, unless it is a linear barrier (fence, wall, ...) without area=yes.

None

Returns:

Type Description
Way or None

The edited copy; way itself when there is nothing to apply or it cannot be rebuilt; None when a Polygon can be neither re-buffered nor rebuilt as a flat ring (callers use result or way).

apply_added_nodes(way, store, zone_number, zone_letter)

Return a copy of way with its user-added synthetic nodes spliced in.

Each store["added_nodes"] entry of the original way ({"id", "after_node_id", "lat", "lon"}) is inserted after after_node_id, at its moved position if it has one; an entry whose anchor is not in the node list is skipped. A LineString gets the coordinate too; a Polygon keeps its geometry, since its ring is the buffer outline. Returns way itself when nothing was inserted, the geometry is another type, or the new line is invalid.

rebuild_way_without_nodes(way, del_nids, zone_number, zone_letter, nodes_cache, category=None)

Return a copy of way with the nodes del_nids removed and line rebuilt.

A LineString keeps the coordinates of the remaining nodes (re-closed if the way was closed). A Polygon is rebuilt from the remaining nodes' nodes_cache positions by :func:_rebuild_polygon, falling back to the bare centerline if re-buffering fails.

Parameters:

Name Type Description Default
way Way

The way to rebuild.

required
del_nids set of int or list of int

Node IDs to remove from the way.

required
zone_number int

UTM zone number.

required
zone_letter str

UTM zone letter.

required
nodes_cache dict or None

{node_id: {"lat": ..., "lon": ...}} cache for node positions.

required
category str

Way category (e.g. "barrier").

None

Returns:

Type Description
Way or None

The edited copy, or None when the way became degenerate (too few nodes or coordinates, or an unsupported geometry type) and should be dropped.