Skip to content

Data Formats

This page documents every file format produced or consumed by the map_data package.


.mapdata file format

A .mapdata file is a JSON text file written by MapData.save() via json.dump. It is not a pickle file and it is not binary. Legacy files created before the JSON migration were Python pickle — support for these has been removed for security reasons. Legacy files must be re-parsed from their source GPX/YAML file to be converted to the JSON format.

Read a .mapdata file with:

from map_data.map_data import MapData

md = MapData.load("route.mapdata")

Never open .mapdata files with pickle.load directly — use MapData.load().

Top-level JSON structure

{
  "format_version": 1,
  "metadata": { ... },
  "waypoints": [[x0, y0], [x1, y1], ...],
  "roads": [ <way object>, ... ],
  "footways": [ <way object>, ... ],
  "barriers": [ <way object>, ... ],
  "crossroads": [ <way object>, ... ],
  "nodes_cache": { "<node_id>": {"lat": 50.1, "lon": 14.5, "tags": {}}, ... }
}

format_version is the schema version (currently 1, see FORMAT_VERSION in map_data/utils/serialization.py). Files written before the marker existed have no such key and are read as the pre-versioning schema; a file whose version is newer than the one this build knows is rejected with a ValueError rather than half-read. Every way key is optional on load, so a file written by an older version falls back to the Way defaults for the keys added since.

metadata object

Field Type Description
zone_number int UTM zone number
zone_letter str UTM zone letter
min_x / max_x float UTM easting bounding box (includes margins)
min_y / max_y float UTM northing bounding box (includes margins)
min_lat / max_lat float WGS-84 latitude bounding box
min_long / max_long float WGS-84 longitude bounding box
coords_file str or null Path of the source GPX/YAML file, or null if constructed from an array

Way object (within roads, footways, barriers, crossroads)

Field Type Description
id int or str OSM way/node ID, or a negative synthetic ID for merged segments
is_area bool true if the geometry is a closed polygon
nodes list[int] Ordered list of OSM node IDs
tags object OSM tag key-value pairs
line str Shapely WKT representation of the geometry (UTM coordinates)
in_out str or null "outer" or "inner" for multipolygon relation members; null otherwise

Human-readable export

The viewer's Export button writes <stem>.exported.mapdata — a JSON file with the same schema as above but with all viewer annotations (deleted ways, tag overrides, node position overrides, drawn annotations) applied. This file can be used as a clean, annotation-resolved snapshot for downstream tools.

The GeoJSON button next to it writes the same annotation-resolved data as <stem>.geojson — a standard GeoJSON FeatureCollection (see mapdata_to_geojson in map_data/viewer/helpers.py) for use in QGIS, geojson.io, or other GIS tools.


.annotations.json sidecar format

Annotation files are written alongside the .mapdata file as <stem>.annotations.json. They store all edits made in the viewer without modifying the original map data. The viewer merges the sidecar at load time.

Top-level keys

Key Type Description
version int Schema version (currently 1)
annotations list Drawn geometry annotations (obstacles, path segments)
deleted_ways list Ways removed from the map view
hidden_ways list Ways temporarily hidden but not deleted
tag_overrides object Per-way OSM tag edits
split_ways object Node IDs at which a way is split into segments
detached_nodes list Synthetic node that starts the segment after a split
deleted_nodes object Node IDs deleted from specific ways
node_position_overrides object Dragged node position corrections
change_log list Audit log of all user-initiated changes
change_log_migration str Internal migration version tag

annotations list

Each entry is a GeoJSON Feature-like object:

{
  "id": "ann_1715600000000",
  "type": "obstacle",
  "geometry": {
    "type": "Polygon",
    "coordinates": [[[14.5678, 50.1234], [14.5680, 50.1234],
                     [14.5680, 50.1236], [14.5678, 50.1234]]]
  },
  "properties": {}
}

type is either "obstacle" (drawn barrier polygon) or "path" (drawn navigable path segment).

deleted_ways list

[
  {"id": 123456789, "category": "barrier", "label": "building"}
]

hidden_ways list

Same structure as deleted_ways. Hidden ways are still included in export but are not rendered in the viewer.

tag_overrides object

Keys are way IDs as strings; values are dicts of tag key-value pairs that replace the original OSM tags for that way:

{
  "987654321": {"highway": "footway", "surface": "asphalt"}
}

split_ways object

Keys are way IDs as strings; values are lists of OSM node IDs at which the way is split:

{
  "123456789": [9876543, 9876544]
}

detached_nodes list

A split made in the viewer detaches the segments. The segment after the split starts at its own copy of the split node, with a negative synthetic id. This means each end can be moved on its own, and the planner does not route across the split. Splits without an entry (older stores) keep sharing the node. Undoing the split removes the entry and the position overrides of both ends.

[
  {"way_id": 123456789, "node_id": 9876543, "id": -3}
]

Positions of moved nodes, including detached ends, are also what the graph planner uses.

deleted_nodes object

Keys are way IDs as strings; values are lists of OSM node IDs deleted from that way:

{
  "123456789": [9876540, 9876541]
}

node_position_overrides object

Keys are way IDs as strings; values are dicts mapping node IDs (as strings) to corrected {lat, lon} positions:

{
  "123456789": {
    "9876542": {"lat": 50.1235, "lon": 14.5679}
  }
}

A move applies to that way only. When the node is also used by another way, the merged map gives the moved way its own copy of the node under a fresh negative id (minted at merge time, not stored) and the other ways keep the original position, so the ways no longer join there. Ways that put the node within 0.5 m of each other keep sharing it; when all of them do, the node simply moves. A move recorded on a way that no longer uses the node is ignored.

A moved end node that lands within 5 m of another way is joined to it at merge time (join_ways in map_data/annotations.py): the junction, a node of that way or a new one on its edge, is put after the end in the way's node list. The stored position is not changed.

change_log list

An ordered audit log. Each entry has a type field and optional ts (ISO timestamp for user-initiated changes):

[
  {"type": "way",  "id": 123456789, "category": "barrier", "label": "building", "ts": "2025-11-01T10:00:00Z"},
  {"type": "node", "way_id": 123456789, "node_id": 9876540, "ts": "2025-11-01T10:01:00Z"},
  {"type": "tag",  "id": 987654321},
  {"type": "move", "id": 123456789, "category": "footway", "label": ""},
  {"type": "split","way_id": 123456789, "node_id": 9876543}
]

GPX waypoint format

MapData reads standard GPX 1.1 files (used for map creation, e.g. create_mapdata and osm_cloud's gpx_file parameter). It reads <wpt> elements if the file has any; otherwise it falls back to track points (<trkpt>) from <trk> elements, and then to route points (<rtept>) from <rte> elements. Loading fails if none of the three are present.

<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="MapData Planner">
  <wpt lat="50.1234" lon="14.5678"></wpt>
  <wpt lat="50.1240" lon="14.5690"></wpt>
  <wpt lat="50.1250" lon="14.5700"></wpt>
</gpx>

YAML waypoint files are not supported.