@mapmap/core
v0.2.0
Published
WASM navigation intelligence for browsers and agent runtimes: Ferrostar-derived guidance, ADR tunnel compliance, offline route-request building, polyline codecs (npm: @mapmap/core)
Readme
sn-nav-web — @mapmap/core
The on-device navigation intelligence of MapMap, compiled to
WebAssembly for browsers and agent runtimes. A thin wasm-bindgen shell over
sn-nav-core (built with default-features = false, features = ["wasm"]):
nothing is reimplemented — web apps and agents run the same Rust code
that ships inside the Android and iOS SDKs.
What runs in WASM — and what deliberately does not
| In this package (client-side) | Server-side (hosted API or any Valhalla/OSRM endpoint) |
|---|---|
| Guidance state machine (Ferrostar-derived): route snapping, step advance, deviation detection, arrival | Valhalla graph routing (route computation over the road network) |
| ADR 8.6.4 tunnel compliance (worst-case reading, same sn-adr matrix as the gateway and the mobile SDKs) | Map data, tiles, geocoding |
| Valhalla route-request building with ADR costing merge (conflicts rejected, never silently overridden) | |
| Route response parsing / SI summaries | |
| Encoded-polyline codec (precision 5 and 6) | |
There is no Valhalla WASM port. Valhalla is a C++ engine over a
multi-gigabyte routing tile graph; no browser build exists — Valhalla's own
web-app is a UI that calls a hosted
Valhalla server. The architecture here is the same as the mobile SDKs',
with the engine seam moved across the network: fetch a route JSON from the
MapMap hosted API (or any Valhalla-compatible endpoint you run —
see docs/SELF-HOSTING.md), then run guidance, compliance and replay
locally at full speed, offline-tolerant, in the page or the agent process.
That split is what ferrostar's own published web package does for guidance
(its wasm_js feature set is exactly what sn-nav-core/wasm mirrors), and
it is why this package exists: the intelligence layer is client-side; the
graph stays on the server.
Browser usage
<script type="module">
import init, { GuidanceSession, checkTunnel, buildOfflineRouteRequest }
from "./pkg-web/sn_nav_web.js";
await init();
// 1. Build the request (ADR profile merged into truck costing options).
const request = buildOfflineRouteRequest(
[{ lat: 51.5074, lon: -0.1278 }, { lat: 52.4081, lon: -1.5106 }],
"truck",
{ hazmat: true, tunnelCode: "C", heightM: 4.0, grossWeightT: 40.0 },
);
// 2. Route on the server (hosted API or any Valhalla endpoint).
const routeJson = await (await fetch("https://api.example.com/route", {
method: "POST", body: request,
})).text();
// 3. Everything else is local.
const session = new GuidanceSession(routeJson, { stepAdvanceDistanceM: 20 });
navigator.geolocation.watchPosition((p) => {
const update = session.updateLocation(
p.coords.latitude, p.coords.longitude, p.timestamp,
p.coords.speed ?? undefined, p.coords.heading ?? undefined,
p.coords.accuracy,
);
if (update.state === "navigating") render(update.currentInstruction, update.distanceToNextManeuverM);
if (update.state === "offRoute") recalculate();
});
</script>A complete self-contained example — synthetic drive replay, live
instruction/distance rendering and an ADR compliance panel — is in
examples/browser-demo.html:
wasm-pack build . --release --target web --scope MapMap --out-dir pkg-web
python3 -m http.server 8080 # then open /examples/browser-demo.htmlAgent runtime usage (Node)
The nodejs target needs no init() and no DOM — an agent can plan,
check compliance and replay a drive entirely in-process:
const nav = require("@mapmap/core"); // pkg-node build
// Is this load allowed through a category D tunnel?
nav.checkTunnel({ hazmat: true, tunnelCode: "C" }, "D");
// → { status: "blocked", reason: "ADR 8.6.4: tunnel restriction code C forbids…" }
nav.forbiddenCategories({ hazmat: true, tunnelCode: "C" }); // → ["C", "D", "E"]
// Build a Valhalla request, route via any endpoint, then replay locally.
const request = nav.buildOfflineRouteRequest(waypoints, "truck", profile);
const routeJson = await routeViaApi(request);
console.log(nav.parseRouteSummary(routeJson)); // { distanceM, durationS, … }
const session = new nav.GuidanceSession(routeJson);
for (const fix of telemetry) {
const update = session.updateLocation(fix.lat, fix.lon, fix.t, fix.speed, fix.course, fix.accuracy);
if (update.state === "arrived") break;
}
session.advanceToNextStep(); // manual stepping (tunnels, simulations)API surface
new GuidanceSession(routeResponseJson, config?)—configkeys (all optional):stepAdvanceDistanceM,minimumHorizontalAccuracyM,routeDeviationThresholdM,snapCourseToRoute,waypointAdvanceRangeM(defaults identical to the mobile SDKs).updateLocation(lat, lon, timestampMs, speedMps?, courseDeg?, accuracyM?)→{ state: "navigating", stepIndex, distanceToNextManeuverM, distanceRemainingM, durationRemainingS, currentInstruction, deviationM }|{ state: "arrived" }|{ state: "offRoute", deviationM }.advanceToNextStep()→ same shape.totalSteps(getter)
checkTunnel(profile, category)→{ status: "allowed" }or{ status: "blocked", reason }(reason cites ADR 8.6.4)forbiddenCategories(profile)→["C", "D", "E"]-style arraybuildOfflineRouteRequest(locations, costing, adrProfile?)→ Valhalla request JSON string; ADR merge conflicts are errors, never overridesparseRouteSummary(routeResponseJson)→{ distanceM, durationS, hasToll, hasHighway, hasFerry }polylineDecode(str, precision)→Float64Arrayof[lat, lon, …]pairs;polylineEncode(float64Array, precision)→ stringversion()→ package version
ADR profile keys (all optional; defaults are the EU 96/53/EC maxima,
non-hazmat): heightM, widthM, lengthM, grossWeightT, axleLoadT,
axleCount, hazmat, tunnelCode ("B", "B1000C", "C/E", …).
All inputs are plain JSON-serialisable objects. Unknown keys are
rejected (serde-wasm-bindgen cannot see them, so the crate checks keys
explicitly): a misspelled hazMat throws instead of silently routing a
hazmat load as clean. Errors are real JS Errors carrying the underlying
typed Rust error messages.
Building and testing
rustup target add wasm32-unknown-unknown
cargo install wasm-pack --locked
cargo check -p sn-nav-web --target wasm32-unknown-unknown
wasm-pack test --node crates/sn-nav-web # 13 wasm-bindgen tests
# Browser package (ES module) and Node package:
wasm-pack build crates/sn-nav-web --release --target web --scope MapMap --out-dir pkg-web
wasm-pack build crates/sn-nav-web --release --target nodejs --scope MapMap --out-dir pkg-node
node crates/sn-nav-web/scripts/set-npm-name.mjs crates/sn-nav-web/pkg-web # → @mapmap/core
node crates/sn-nav-web/scripts/set-npm-name.mjs crates/sn-nav-web/pkg-node(wasm-pack names the package after the crate; set-npm-name.mjs rewrites
it to @mapmap/core and bundles the licence.)
CI: .github/workflows/wasm.yml runs the wasm32 check, the Node test
suite, both builds, a Node smoke test and uploads the packages.
Size
Measured on the release build (wasm-opt -O via wasm-pack, rustc 1.97):
sn_nav_web_bg.wasm: 987 KiB (1,010,521 bytes), 359 KiB gzipped (366,932 bytes) — the price of carrying the full Ferrostar guidance core, geo maths and serde. Loaded once and cached, it is comparable to a mid-sized JS map library, and there is no JS re-implementation to keep in sync with the devices.
Licensing
First-party proprietary code (see repository LICENSE), bundled into the
package by set-npm-name.mjs. Third-party: ferrostar (BSD-3-Clause),
wasm-bindgen/js-sys/serde-wasm-bindgen (MIT/Apache-2.0) — all permissive;
the UniFFI (MPL-2.0) mobile surface is not compiled into the wasm
build (sn-nav-core is consumed with default-features = false).
