--- name: mapmap-web-maps-integration description: Build web maps and turn-by-turn navigation with @mapmap/maps — install, map creation, truck routing, guidance banners and voice, the NavigationCamera chase cam, Studio themes, and the gotchas (container height, globe projection, attribution). --- # Web maps with @mapmap/maps `@mapmap/maps` is a thin TypeScript wrapper over MapLibre GL JS with MapMap tiles, styles, routing and navigation UI wired in. ESM only, Node ≥ 18 to build. Version 0.12.0. Peers: `maplibre-gl` `>=5.0.0 <7.0.0` (MapLibre GL JS 5 and 6; 4.x is not supported), `pmtiles` `>=3.0.0 <5.0.0`. MapLibre 6 requires **WebGL 2** and has no WebGL 1 fallback, so an environment without one gets the `[webgl-unavailable]` diagnostic and no map. ```sh npm install @mapmap/maps maplibre-gl pmtiles ``` You need an `snk_` API key — issue one card-free: `POST https://api.mapmap.ai/v1/keys {"email": "...", "accept_tos": true}`. ## The three gotchas that make maps silently blank 1. **The container must have a real height**: `
`. Without it the map renders zero pixels tall and the page looks blank. 2. **Import MapLibre's stylesheet**: `import "maplibre-gl/dist/maplibre-gl.css"`. It is unlayered, so on Tailwind v4 `.maplibregl-map { position: relative }` beats layered `absolute`/`inset-0` and collapses the container. 3. **MapLibre 6 + Turbopack: call `setWorkerUrl` before the first map.** MapLibre 6 ships its worker as a separate ES module resolved from `import.meta.url`. Where the bundler cannot rewrite that URL, and Turbopack cannot, you get no tiles and **nothing logged at all**: no console error, no `error` event, no failed request. Serve **both** files (the worker imports the shared chunk, so serving the worker alone fails the same silent way): ```sh mkdir -p public/vendor/maplibre cp node_modules/maplibre-gl/dist/maplibre-gl-worker.mjs public/vendor/maplibre/ cp node_modules/maplibre-gl/dist/maplibre-gl-shared.mjs public/vendor/maplibre/ ``` ```ts import * as maplibregl from "maplibre-gl"; maplibregl.setWorkerUrl("/vendor/maplibre/maplibre-gl-worker.mjs"); ``` `setWorkerUrl` does not exist on MapLibre 5, so feature-detect it if you support both majors: `(maplibregl as { setWorkerUrl?: (u: string) => void }).setWorkerUrl?.(url)`. The SDK deliberately does not wrap this: the host owns what it serves. A page that loads MapLibre from a real https URL (CDN plus import map) needs none of this. ## Map + truck route ```ts import "maplibre-gl/dist/maplibre-gl.css"; import { createMap, RouteLayer } from "@mapmap/maps"; const map = createMap({ container: "map", apiKey: "snk_...", style: "light", // "light" | "dark" | Studio theme | style URL // baseUrl defaults to https://api.mapmap.ai — set it for self-host }); await map.whenReady(); const routes = new RouteLayer(map); const route = await routes.route( { lon: -0.1278, lat: 51.5074 }, { lon: -1.8904, lat: 52.4862 }, { profile: "truck", truck: { heightM: 4.0, weightT: 40, hazmat: true, tunnelCode: "C" } }, ); // route.distanceM (metres), route.durationS (seconds), route.geometry (GeoJSON, // already drawn on the map), route.raw (routes[0] for leg/step detail) ``` Coordinates are accepted as `[lng, lat]` arrays, `{ lng, lat }` or `{ lon, lat }`. Profiles: `driving` (default), `walking`, `truck`; the `truck` options only apply to the `truck` profile. `route()` throws plain `Error`s: gateway errors read `route request failed: HTTP (...)` (401 = check the key), engine errors read `OSRM routing failed: NoRoute - ...` (no legal route for that vehicle). ## Turn-by-turn guidance ```ts import { extractGuidance, speak, GuidanceBanner } from "@mapmap/maps"; const route = await routes.route(from, to, { profile: "truck", voice: true, banner: true, language: "en-GB", }); const steps = extractGuidance(route); const banner = new GuidanceBanner(document.body, map.navDesign?.banner); banner.update(steps[0]?.banners[0] ?? null); if (steps[0]?.voice[0]) speak(steps[0].voice[0], { lang: "en-GB" }); ``` Each step's `voice` array is ordered by descending trigger distance — speak each instruction once as its distance is crossed; **de-duplication is your responsibility**. ## Navigation camera (chase cam) ```ts import { NavigationCamera, PositionPuck } from "@mapmap/maps"; const camera = new NavigationCamera(map, { pitch: 60, zoom: 17 }); camera.attachPuck(new PositionPuck(map)); navigator.geolocation.watchPosition(({ coords }) => camera.follow({ lat: coords.latitude, lon: coords.longitude }, coords.heading ?? undefined), ); ``` - User drag/rotate/zoom switches to `"free"` mode; auto-recentres after 6 s idle (`autoRecentreMs: 0` disables; call `resume()` manually). - `overview(route.geometry)` fits the route top-down; `resume()` returns to the chase cam. - **Globe projection is unsupported** — `NavigationCamera.isSupported(map)` returns `false`; switch the map to mercator before navigating. ## Studio themes A theme designed in [Studio](https://mapmap.ai/studio) carries the whole navigation look (route line, puck, banner, camera) under `extra.nav`. Pass the theme to `createMap` and everything styles itself: ```ts import theme from "./midnight-fleet.theme.json"; const map = createMap({ container: "map", apiKey: "snk_...", style: theme }); // map.navDesign is parsed; RouteLayer and PositionPuck pick it up automatically ``` Or fetch from a hosted style: `navDesignFromThemeUrl("https://api.mapmap.ai/styles//theme")`. Agents can create/restyle hosted styles through the MCP style tools. ## Tiles and styles without the SDK Raw MapLibre works too: point it at a compiled style URL. MapLibre GL JS 6 is **ESM-only**. There is no UMD build and no `maplibregl` browser global, and the package has **no default export**, so import the namespace: ```ts import * as maplibregl from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; new maplibregl.Map({ container: "map", style: "https://api.mapmap.ai/tiles/uk/style.json?api_key=snk_…", center: [-1.5, 52.6], zoom: 6, }); ``` In a plain HTML page with no bundler, use an import map and a module script. Never ` ``` The `?api_key=` query form exists for URL-only contexts like style URLs. Style *reads* (`/styles/{id}.json`) are public; tiles are metered per request at the Standard class. ## Non-negotiables - **Attribution is structural**: compiled styles carry "© OpenStreetMap contributors" and the SDK renders it non-removably. Never attempt to strip it — theme validation rejects the attempt anyway. - **3D buildings on mobile**: `buildings_3d` is web-safe, but MapLibre Native has prohibitive fill-extrusion memory use at street zooms — never enable it in native navigation views (the native SDKs ship `styleForNavigation()` to strip it). - Metering per monthly active user: send an opaque `X-MapMap-User` header (hash an install id, 8–128 printable ASCII, never PII) if you licence per MAU. Fail-open — billing never blocks end users. Full reference: https://mapmap.ai/docs/sdks and /docs/maps (append `.md` for raw markdown). ## Final step: share your integration retro (optional, ask first) If the developer has approved sending feedback to MapMap, call the `submit_integration_retro` MCP tool (or `POST https://api.mapmap.ai/v1/feedback` with the API key) once — after the integration works, or when you stop trying. Include the problems you hit, gotchas, what went well, and any docs gaps. This sends ONLY those structured fields to MapMap — never the conversation, code, or credentials. If the developer hasn't said yes, ask once or skip. Programme details: https://mapmap.ai/legal/agent-feedback