Skip to main content

simple_lanelet2

A reimplementation of the Lanelet2 Python API with a Rust core and PyO3 bindings.

The goal is a drop-in replacement: import lanelet2 keeps working, unchanged, but installation is a single wheel with no Boost, no GeographicLib and no C++ toolchain.

import lanelet2
from lanelet2.core import Point3d, LineString3d, Lanelet, getId

left = LineString3d(getId(), [Point3d(getId(), 0, 0, 0), Point3d(getId(), 1, 0, 0)])
right = LineString3d(getId(), [Point3d(getId(), 0, 1, 0), Point3d(getId(), 1, 1, 0)])
lanelet = Lanelet(getId(), left, right)

Status. All seven submodules are implemented. The library exposes 100% of the reference's public API — 633 names across every module and class — and upstream's own test suite passes against it unmodified, in both modes.

Bug-compatibility mode

Upstream has a number of outright defects — a __hash__ that contradicts __eq__, a misnamed keyword argument on Origin, a routing filter that silently does nothing. By default this library fixes them. Setting LANELET2_BUG_COMPAT restores upstream's behaviour exactly:

LANELET2_BUG_COMPAT=1 python my_script.py   # byte-for-byte upstream behaviour
python my_script.py                         # repaired behaviour (default)

The flag is read once at import time and is reported as lanelet2.BUG_COMPAT. Every switched behaviour is listed in docs/DIVERGENCE.md and enforced by the test harness.

Container image

A runtime image is published to GHCR for linux/amd64 and linux/arm64:

# an interpreter with `import lanelet2` already working
docker run --rm -it ghcr.io/hakuturu583/simple_lanelet2:latest

# the working directory is /work, so a map on the host is one mount away
docker run --rm -v "$PWD:/work" ghcr.io/hakuturu583/simple_lanelet2:latest python3 -c "
import lanelet2
from lanelet2.projection import UtmProjector
from lanelet2.io import Origin
m = lanelet2.io.load('map.osm', UtmProjector(Origin(49.0, 8.4)))
print(len(m.laneletLayer), 'lanelets')"

Tags are latest, X.Y.Z, X.Y and X from releases, plus main and sha-<commit> from the tip of the default branch.

It carries a CPython and the wheel and nothing else: no Rust toolchain, no source tree, no pip — around 40 MB unpacked, of which the extension module is 2.4 MB and almost all of the rest is CPython itself. That is enforced rather than intended; the build fails if pip, cargo, rustc or a C compiler can be found in the image, and its smoke test exercises the map, geometry, projection, routing and I/O paths through the extension before anything is pushed.

Dockerfile builds it in three stages: a Rust toolchain produces one musl wheel, a second stage unpacks that wheel against the interpreter that will load it, and the third takes the finished filesystem onto scratch — rm in a layer above a base image writes a whiteout and ships the deleted bytes anyway, so copying what survived is what actually makes it smaller. The image runs as an unprivileged user with /work as its working directory.

The wheel inside it is musllinux and is built only for this image; the manylinux wheels on PyPI are the ones to install with pip.

Map viewer

https://hakuturu583.github.io/simple_lanelet2/ — drop a Lanelet2 .osm on the page and look at it. The file is parsed and styled by this library compiled to WebAssembly, in the tab; nothing is uploaded anywhere.

It is a component rather than a page, so it embeds: a Foxglove panel extension or any application that runs your JavaScript imports web/viewer.js and mounts <lanelet2-viewer> — shadow DOM, ResizeObserver, no globals, a real destroy() — and a host that can only place a URL, such as a wandb HTML panel or a notebook cell, frames web/embed.html and drives it over postMessage. web/EMBEDDING.md has both, and both are live.

None of this is part of the drop-in Lanelet2 surface. Upstream has no viewer to be compatible with, so ll2-viz, ll2-wasm and ll2-rerun are outside the compatibility claim and have no Python bindings — nothing in the wheel imports them, and the diff harness does not touch them.

Three crates carry it, and none of them needs a browser:

  • ll2-viz turns a LaneletMap into a Scene — a list of styled polylines and polygons in map coordinates — and renders one to SVG. It classifies primitives the way the Lanelet2 tagging document and Autoware's lanelet2_extension describe them, so line_thin/dashed comes out as a dashed hairline, stop_line as a red bar, a crosswalk lanelet in its own colour. Those coordinates keep each node's elevation, and a View decides where the map is seen from: straight down by default, or tilted, which is what shows an overpass as an overpass rather than as a road crossing another at the same height.
  • ll2-wasm hands a scene across the WebAssembly boundary as typed arrays, which is what lets the demo's <canvas> draw a city-scale map from a few dozen calls per frame.
  • ll2-rerun logs the map into Rerun as 3D archetypes, for a Spatial3D view. It reads the same tag-to-style table, so a map is coloured the same there as it is here — but it keeps every node's ele instead of flattening it, because the point of that viewer is having the map in the same space as the lidar sweep, the ego pose and the trajectory beside it.
// a .osm to an .svg, no browser involved
let svg = ll2_viz::svg_from_osm(&text, &ll2_viz::VizOptions::default())?;

// the same map drawn in relief, from a camera 55 degrees above the horizon
let scene = ll2_viz::Scene::from_map(&map, &ll2_viz::VizOptions::default());
let svg = ll2_viz::render_svg(&scene, &ll2_viz::SvgOptions {
    view: ll2_viz::View::three_quarter(),
    ..Default::default()
});

// the same map in 3D, in a Rerun viewer it opens for you
ll2_rerun::spawn_osm(&text, &ll2_rerun::MapOptions::default())?;
just svg tests/data/mapping_example.osm map.svg   # the same thing from a shell
just svg3d tests/data/mapping_example.osm 3d.svg  # or in relief, elevation and all
just scene tests/data/mapping_example.osm s.json  # or the scene itself, to draw elsewhere
just rrd tests/data/mapping_example.osm map.rrd   # or a Rerun recording, to open in 3D
just web-serve                                    # the demo, on localhost:8000

A Scene being renderer-agnostic is meant literally: the SVG writer, the demo's <canvas> and anything else are peers, and the third one costs a serialiser. examples/scene2json.rs is that serialiser — it writes the styled shapes, both palettes and the layer table as JSON, which is enough to draw the map somewhere this repository has never heard of.

The third dimension is in the Scene, not in the renderers: every vertex carries the elevation its node had, and flattening happens once, at the very end, through the View a renderer is given. So the SVG writer and the demo's <canvas> both draw the map in relief without either of them knowing how — what crosses to the browser is still two floats per vertex, already projected and already in painter's order, and the canvas needs no depth buffer to show a bridge over a road. Turning the camera rebuilds the scene rather than redrawing it, which is the right trade for a viewpoint that is chosen and then looked at. The JSON serialiser is the exception, and deliberately so: it hands over the unprojected three dimensions and leaves the camera to whatever is drawing them.

ll2-rerun is the one that does not read a Scene, and the reason survives that: it builds surfaces rather than outlines — ear-clipped rings, lanelets stitched into ribbons — and hands Rerun the map in three dimensions rather than a projection of it, one entity per layer under map/, so the eight groups the SVG writer emits are eight checkboxes in the viewer's blueprint tree. Painter's order becomes two centimetres of real separation per layer, which is what stops a lane marking and the road it is painted on from fighting over the same depth value.

The viewer differs from lanelet2.io.load in one deliberate way. It has no origin to be given, so it takes the median of the file's own latitudes and longitudes and projects through UTM from there — and if that collapses the map to a point while the file's local_x/local_y tags do not, it uses those instead. That is the case for Autoware maps written with a placeholder lat/lon, which would otherwise draw as nothing. The Python API makes no such guess; see Autoware maps below. Both behaviours are reachable from the viewer's Coordinates control.

web/README.md covers building and deploying it.

Verification

Compatibility is not asserted, it is measured. Every case in tests/cases/ is run three ways and the JSON-Lines output is compared:

run interpreter environment
REF .venv-ref the real lanelet2==1.2.3 from PyPI
COMPAT .venv ours, LANELET2_BUG_COMPAT=1
FIXED .venv ours, default

REF and COMPAT must agree exactly. COMPAT and FIXED must differ in exactly the places listed in tests/compat_matrix.toml — no more, and no fewer, so neither an accidental behaviour change nor an unwired repair can slip through.

just venvs           # create both virtualenvs
just build           # build and install into .venv
just diff            # run the harness
just upstream-tests  # upstream's own tests, unmodified, in both modes
just test-rust       # the Rust unit tests

What the harness checks, beyond "it runs":

  • the 594 KB example map from the Lanelet2 repository loads, and writing it back reproduces the reference's file byte for byte — every node, way, relation and tag, in the same order — and a second pass is identical again;
  • every lanelet's full centerline matches, on that map and on forty procedurally generated shapes chosen to exercise the parts of the algorithm a rectangular lanelet never reaches;
  • the projections agree with GeographicLib to 7e-15 m across zone edges, the Norway and Svalbard zone irregularities, both hemispheres and the antimeridian;
  • the traffic-rule tables are swept exhaustively — every participant against every way subtype, both directions, both locations — rather than spot-checked;
  • the routing graph's whole edge list is compared before any query is.

Autoware maps

autoware_lanelet2_extension's regulatory elements and its transverse Mercator projector are provided under the upstream import path, in this same wheel:

import lanelet2
import autoware_lanelet2_extension_python.regulatory_elements  # registers the subtypes
from autoware_lanelet2_extension_python.projection import TransverseMercatorProjector

Importing regulatory_elements is what makes road_marking, crosswalk, detection_area and the rest resolvable, and it makes traffic_light resolve to AutowareTrafficLight. Before that import a map carrying them is refused, exactly as stock Lanelet2 refuses it. That is upstream's behaviour, not an accident of packaging: registration there happens when the extension's shared library loads. Note that it is process-wide and cannot be undone, so an unrelated module importing the extension changes what lanelet2.io.load produces from that point on.

Coordinates come from latitude and longitude, not from local_x/local_y. Autoware's C++ AutowareOsmParser prefers those tags, and a real Autoware map is full of them — the Nishi-Shinjuku example carries 36,936. That parser is not reachable from Python in the reference either, so lanelet2.io.load behaves the same in both; our output on that map is byte-identical to the reference's. But it does mean the numbers differ from what Autoware's own C++ tooling produces.

utility.query and utility.utilities are provided apart from their ROS-dependent halves, which are defined but raise when called — upstream imports geometry_msgs and rclpy at module top, so its versions cannot be imported at all without ROS. Several of upstream's own bindings do not work; those are repaired here and reproduced under LANELET2_BUG_COMPAT=1. See docs/DIVERGENCE.md for the list.

CI

Four jobs, arranged around the compatibility claim rather than around the test suite:

job what it proves needs
check it builds, lints, and the wheel installs into an empty venv and imports nothing
diff 20 cases against the PyPI lanelet2==1.2.3, plus upstream's vendored tests a wheel
upstream upstream's own tests, cloned at HEAD each run, unmodified, both modes network
oracle 31 cases, including the Autoware extension and the two-reference skew check pixi + colcon

The map viewer likewise has its own workflow, pages.yml: it builds the wasm module, instantiates it in node and puts the example map through it, drives the demo and both embedding routes in a real Chromium, and deploys to GitHub Pages from main. Pull requests build and test without deploying. Both test scripts earn their place: cargo test runs on the host, where wasm-bindgen's glue is inert, so it cannot catch a module that fails to instantiate — and instantiating is not the same as a canvas in a shadow root receiving a wheel event.

The container image has its own workflow rather than a fifth job here, because it costs a full Rust build per architecture and is wanted on a different set of events — see docker.yml. It builds on any pull request that touches the crates, the Python package or the Dockerfile, and publishes only from main and from releases.

One case is not in that count. 1150_aw_map loads a real 10.5 MB Autoware map and checks it writes back byte for byte; the map it was developed against is CC BY-NC licensed, so it can neither be vendored here nor fetched by CI. Point SIMPLE_LL2_AW_MAP at a real Autoware map to run it. 1160_aw_synthetic_map covers the same behaviour on a map small enough to write inline, and does run in CI.

One caveat on what upstream proves. Lanelet2's own tests are substantive; the Autoware extension's Python tests are import smoke tests, and its real suite is C++ gtest that cannot run against a Python implementation. The extension is verified by the # ORACLE: aw diff cases instead — see tests/upstream-awext/README.md.

upstream fetching at HEAD rather than a pin is deliberate. A vendored copy proves compatibility with whatever upstream looked like the day it was copied; fetching proves it against upstream as it stands, and a test they change becomes a signal rather than a silent drift. It runs on a schedule too, since upstream moves without anyone touching this repository. Locally: just upstream-fresh.

Licence

BSD-3-Clause, matching upstream Lanelet2. See NOTICE for vendored assets.

Metadata

Release files for simple-lanelet2 1.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for simple-lanelet2 1.1.3
File Size Uploaded Provenance Actions
simple_lanelet2-1.1.3.tar.gz 268.5 kB With provenance Details for simple_lanelet2-1.1.3.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for simple-lanelet2 1.1.3
File Interpreter ABI Platform Actions
simple_lanelet2-1.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.9 abi3 Linux glibc 2.17+ x86-64 Details for simple_lanelet2-1.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
simple_lanelet2-1.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details for simple_lanelet2-1.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Total release size: 2.2 MB

Release files / simple_lanelet2-1.1.3.tar.gz

Download URL simple_lanelet2-1.1.3.tar.gz
Size 268.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a9cb6989884a88a27d30067277f9a1872edd4020943b4edf00885aba5f6df2c5
BLAKE2b-256 checksum
How to use checksums
ab81dc147b8ead4756c98be8df3e19cec339924e1df94b495f2d45aa0d856934
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / simple_lanelet2-1.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL simple_lanelet2-1.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 981.5 kB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
76470aab1144a6a91887f4a929a463d104fad0f41b0278ae08c90c4ac8e782cf
BLAKE2b-256 checksum
How to use checksums
1e87c9ee9d40bf303c7fe302eec0511d4b1a83019e9595b1716236459430a3b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / simple_lanelet2-1.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL simple_lanelet2-1.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 954.7 kB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
5e5fe2b45413f4bff648a460d426e8f5b2863d24051e8e3c142da2423ac1b14b
BLAKE2b-256 checksum
How to use checksums
f033cd6c295734f26947cd3082eae5c99be72ef0d4ddb2099698f78f0f194289
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Security for simple-lanelet2 1.1.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 3 files in this release have been uploaded with provenance metadata.

Select from 3 files

Attestation for simple_lanelet2-1.1.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page