Packer and routing
obc-pack converts OpenStreetMap PBF extracts to the OBCM map format. It also builds POIs, opening-hours data, contours, and a navigation graph. The device uses this graph for route planning.
The GPX converter creates OBCR route files. The route matcher maps live positions to an active route.
Packing a map
The pipeline performs these operations:
- Ingest and optionally crop or merge PBF sources.
- Calculate the content bounding box.
- Add land, sea, and optional contours.
- Build POIs and the navigation graph.
- Build each level of detail (LOD).
- Write the map and release the completed LOD.
The packer keeps approximately one LOD quadtree in memory at a time. It removes a partial output file after an error or cancellation.
Styling: first match wins
The features object in config.json is ordered. The packer checks tag keys in document order. The first matching key and value supplies the style. An exact value has priority over the "*" catch-all. The packer drops a way when no style matches.
Style IDs start at 1 and follow document order. A style contains color, paint order, width, priority, minimum LOD, and line properties. The style table can contain at most 254 entries.
Ingest: two passes, then assemble
The ingester normally reads each PBF twice. Pass 1 stores node coordinates and collects renderable area relations. Pass 2 resolves ways and captures relation-member geometry. The ingester then uses GEOS to assemble polygons and holes.
A closed way becomes a polygon only when its tags identify an area. A closed road loop remains a line.
Cropping to a box
Use --bbox W,S,E,N to select data during ingest. This option adds an ID-selection pass before the two normal passes.
The selection keeps each selected way complete. It also keeps all required nodes and renderable area-relation members. Thus, roads do not stop at artificial box edges. Area relations do not lose required geometry.
Complete objects can extend outside the requested box. Therefore, the map header bounding box can be larger than the requested box. Do not use the output bounding box as the next crop request.
Merging several regions
The ingester can read multiple PBF files in the same passes. It does not create an intermediate merged file. For a duplicate object type and ID, the first listed file wins. The ingester emits surviving objects in ascending ID order for each type. These rules make the result deterministic.
Land and sea
OpenStreetMap supplies coastlines, but not a complete land fill. The packer downloads and caches a global land-polygon dataset. It clips this dataset to the content bounding box. When land is the backdrop, the packer stores only the sea complement. Use --no-land to skip this stage.
Contours, traced from the terrain
The packer can trace contours from OBCT terrain. Set contours.enabled and supply --terrain. The packer uses marching squares on the terrain lattice. It skips a lattice square if one corner has no height.
The packer creates contour.major and contour.index line features. The defaults are a 100 m interval and an index at every fifth contour. The default pre-LOD simplify tolerance is 15 m. The configuration can change these values.
A contour class needs a matching style rule. Contours then use the normal LOD and quadtree pipeline. The terrain-layer flag lets the renderer hide their ink. It does not remove their bytes from the map.
Extracting POIs
The packer uses a fixed table of 18 OSM tag mappings. The table covers water, campsites, accommodation, resupply, pharmacies, and bicycle shops. The first matching table row supplies the subtype.
POI subtype IDs, categories, and fallback labels are normative. The packer stores names as at most 24 printable ASCII bytes. It transliterates supported Latin characters. An unnamed POI uses its subtype fallback label.
The packer deduplicates nearby candidates within each category. It gives node POIs priority over area centroids. It then builds one spatial index for each category.
Parsing opening hours
The packer parses a subset of the OSM opening_hours grammar. It stores a 29-byte weekly schedule in a shared pool. The device does not parse the source text.
The subset supports these forms:
- Weekday ranges and lists.
- Up to two intervals per day.
24/7,off, andclosed.- Time-only rules for all days.
- Overnight intervals.
- Representative seasonal weeks.
The parser rounds times to the nearest 15 minutes with half-to-even rounding. It flags a partial result when it drops an unsupported rule. A fully unsupported value produces no schedule.
Building the navigation graph
The packer always builds the navigation graph. It keeps OSM node IDs for routable ways. Shared node IDs form junctions. The packer splits ways at junctions and removes duplicate edges. It removes small disconnected components with the configured threshold.
The legality filter rejects private access, motor roads, and bicycle prohibitions. It classifies each accepted edge by highway and surface. The packed way_kind byte contains both classes. See OBCM section 8 for the normative tables and layout.
The serializer writes tiled nodes, adjacency records, edge geometry, and snap anchors. It densifies and splits long geometry so each record fits its chunk.
Weighting the graph: bike profiles
Each map contains one to eight bike profiles. The defaults are Road, Gravel, MTB, and Touring. Each profile supplies highway, surface, and climb weights.
Highway and surface multipliers use 1/16 fixed-point values. A value of 16 means 1.0. Zero forbids the class. Every nonzero multiplier must be at least 1.0. This limit keeps the A* distance heuristic admissible.
The router starts weighted A* with epsilon 1.3. If the fixed search table fills, it retries with 2.0 and then 3.0. The successful epsilon bounds the returned profile-weighted cost.
Weighting the climb
Each adjacency stores ascent for its travel direction. The packer calculates ascent along the edge polyline. It samples terrain at intervals of at most 50 m. The shared integrator uses a 3 m dead band. A missing sample pauses the dead band without using zero elevation. Valid samples after the gap start a new ascent segment. The gap contributes no ascent. An edge with no valid sample has zero ascent.
edge_cost = weighted_distance + ascent_m × climb_weight
The climb weight is a profile value from 0 through 255. Zero disables climb cost. The defaults are Road 10, Gravel 8, MTB 6, and Touring 8. A descent never reduces edge cost.
Building the LOD pyramid
Each style defines the coarsest permitted LOD. Each LOD defines its simplify and size thresholds. The packer builds each LOD independently.
min_area_pxremoves small polygons and holes from coarse LODs.merge_fillscombines fills with the same render identity.merge_linesjoins connected lines with the same render identity.min_line_kmremoves short joined lines.- Coverage simplification creates coarse semantic coverage.
min_line_km requires merge_lines. The finest LOD does not use min_area_px. The packer joins lines before it applies the line-length filter.
The quadtree: packing geometry into chunks
Each LOD uses a quadtree over the global bounding box. A node becomes a leaf when both conditions are true:
- Its estimated feature bytes fit the configured chunk size.
- Each feature has at most 32 rings.
Otherwise, the node splits into four children. The packer clips features to each child box. A feature that crosses a boundary can occur in multiple children. The renderer walks the same flat quadtree to select visible chunks.
The builder
The product builder assembles published cells. It does not run the packer. A selection can contain named regions, boxes, lassos, and GPX corridors. The builder unions all parts before it prices or downloads cells. Thus, overlapping parts do not duplicate cells.
The builder verifies each object length and SHA-256 digest. The assembler verifies the completed map before it exposes the output. If the catalog supplies terrain, the builder downloads the required terrain cells. The assembler puts them in the final map terrain region. The builder does not provide a terrain switch.
Editing a skin
The product skin editor changes presentation fields only. It does not change feature types, style IDs, LODs, or routing profiles. The editor uses the production reader and renderer for its preview. The builder rejects a saved skin that does not match the current schema.
One source, three hosts
One Svelte application supplies the website, desktop app, and maintainer server. Host modules supply transport and storage capabilities. They do not supply separate selection or assembly algorithms.
| Capability | Website | Desktop | Maintainer server |
|---|---|---|---|
| Coverage selection | Yes | Yes | Yes |
| WebAssembly assembly | Yes | Yes | Yes |
| Product skin editor | Yes | Yes | Yes |
| Managed ride library | No | Yes | No |
| Advanced schema editor | No | No | Yes |
| Product PBF build | No | No | No |
The maintainer server can pack a fixed reference crop for schema previews. This preview is not a product build path. Published cells still require an explicit maintainer bake.
Device and ride surfaces
The builder outputs one .obcm file. A device transfer commits the complete map or no map. The device verifies a whole-object CRC-32 before it commits the map. The desktop app also manages routes and recorded rides. It acknowledges an imported ride only after durable storage.
Where the hosted tier lives
The website is a static application. The catalog and cells use separate object storage. A catalog update does not require a website deployment. The publisher uploads content before it publishes the new catalog root.
Following a route
The GPX converter creates an OBCR route. It keeps exact source statistics before it reduces display geometry. It chunks the route and shares the seam point between adjacent chunks. The same no_std converter runs on the device, simulator, and web host.
Map-matching: a forward-biased cursor
The matcher keeps a cursor on the active route. For each position fix, it searches a bounded segment window around that cursor. The window has more forward range than backward range. This bias prevents a loop from matching an earlier pass.
The first fix searches the complete route. Normal tracking searches 3 segments backward and 64 segments forward. Off-route tracking searches 3 segments backward and 320 segments forward. A caller can request the wide window after an unmatched interval.
The matcher marks the rider off-route at 25 m or more. It clears this state below 15 m. This hysteresis prevents state changes from GPS noise. While off-route, cross-track distance stays live and route progress stays fixed.
Attribution and share-alike
OpenStreetMap data uses the Open Database License 1.0. A rendered map is a Produced Work. The device provides the required attribution on its About page.
A published .obcm map is a Derivative Database. The catalog declares ODbL-1.0 and publishes the license text. A distributor of this map data must follow the same license terms. Maps with terrain-derived contours also require the Copernicus attribution.
Implementation
- Packer pipeline:
pipeline.rs - Configuration:
config.rs - OSM ingest:
ingest.rs - POIs and opening hours:
poi.rs,hours.rs - Navigation graph:
nav.rs - Quadtree:
quadtree.rs - Builder:
builder/ - Web assembler:
obc-web-assemble - Device router:
nav.rs - Route matcher:
matcher.rs - GPX converter:
convert.rs