The web maps SDK wraps MapLibre, it does not fork it

# The web maps SDK wraps MapLibre, it does not fork it `@mapmap/maps` is a TypeScript wrapper over MapLibre GL JS, published on npm at version 0.1.0, that gives you a MapMap map with our vector tiles, styles and truck routing already wired in. It is a wrapper and not a fork. MapLibre does every pixel of the rendering, `maplibre-gl` stays a peer dependency your app owns, and `map.map` hands you the raw `maplibregl.Map` the moment our helpers stop being enough. Everything below is additive: delete the wrapper and you still have a working MapLibre app. ## What does one npm install actually give you? It gives you MapLibre plus five things you would otherwise write yourself: the MapMap style, the `pmtiles://` protocol handler, typed routing against our gateway, a navigation camera and a position puck that does not teleport. ```sh npm install @mapmap/maps maplibre-gl pmtiles ``` `maplibre-gl` and `pmtiles` are peer dependencies on purpose. Your app owns a single shared copy, because two MapLibre instances on one page break the WebGL context. The accepted ranges are `maplibre-gl` >= 4 < 6 and `pmtiles` >= 3 < 5. | Export | What it is | |---|---| | `MapMapMap` / `createMap` | The wrapper over `maplibregl.Map`. `.map` is the native instance | | `RouteLayer` | Typed routing (`driving`, `walking`, `truck`) that draws the line for you | | `NavigationCamera` | Course-up chase cam, one call per GPS fix | | `PositionPuck` | Current-position dot and heading arrow, interpolated between fixes | | `GuidanceBanner`, `extractGuidance`, `speak` | Visual and spoken turn-by-turn | | `ThemeScheduler`, `setMapLanguage` | Auto day/night and runtime label language | | `AdrCheck` | Typed client for `POST /adr/check` | | `PlacesLayer` | Clustered pins, popups and nearest-by-drive-time | The package exports around 30 symbols in total, covering map, routing, ADR, guidance, styling and coordinate helpers, and a generated TypeDoc reference ships with the npm release. One gotcha before anything else: the container needs an explicit height. A bare `
` renders a zero-pixel-tall canvas with no error and no map. Set `height: 100vh` (or any real height) before you debug anything else. ## How do you point the map at a style? Pass `style` a keyword, a URL or a Studio theme document, and the SDK compiles the rest. ```ts import { createMap } from "@mapmap/maps"; import "maplibre-gl/dist/maplibre-gl.css"; const map = createMap({ container: "map", apiKey: "snk_…", // baseUrl defaults to https://api.mapmap.ai style: "light", // "light" | "dark" | a Studio theme | a style URL center: [-1.5, 52.6], // lon, lat zoom: 6, }); await map.whenReady(); ``` The accepted shapes are `"light"`, `"dark"`, a Studio theme document, a full MapLibre `StyleSpecification` or a hosted style URL such as `https://api.mapmap.ai/styles/midnight-fleet-9f3a2c@1.json`. A theme document downloaded from [Studio](/studio) drops straight in, and its `extra.nav` block (route line, puck and banner design) is parsed onto `map.navDesign` and picked up automatically by `RouteLayer` and `PositionPuck`. How the tiles and the theme engine work underneath is covered in [vector tiles, styles and the theme engine](/news/vector-tiles-and-styles). The `apiKey` you pass to `createMap` is reused by `RouteLayer` and `AdrCheck`, so you set it once. ## Why did we wrap MapLibre instead of forking it? Because a fork means we own every rendering bug for the rest of the product's life, and you lose the ability to walk away from us. The tempting version of this SDK is a fork of MapLibre with our features patched into the renderer. We did not build that, for two reasons. - **We get upstream fixes for free.** MapLibre ships renderer work we have no business duplicating. A fork means every upstream release becomes a merge, and merges get skipped when they are inconvenient. A wrapper means you bump `maplibre-gl` in your own `package.json` and you are done. - **You get an escape hatch.** `map.map` is the real `maplibregl.Map`. Every native call works: `setPaintProperty`, `queryRenderedFeatures`, `addLayer`, the lot. `mapOptions` passes native `MapOptions` straight through. `PlacesLayer` even publishes its generated layer ids as `stores.ids` so you can style them with raw MapLibre when our options do not reach far enough. If you decide our wrapper is wrong for you, the exit is a refactor, not a rewrite. The cost of this decision is real and we will name it: anything MapLibre cannot do, we cannot do either. The globe projection limit below is exactly that bill arriving. ## What does NavigationCamera do that raw MapLibre does not? It turns one GPS fix into a tilted, course-up chase cam with the puck anchored low on the screen, and it handles the four things that make hand-rolled cameras feel wrong. ```ts import { NavigationCamera, PositionPuck } from "@mapmap/maps"; const camera = new NavigationCamera(map, { pitch: 60, zoom: 17 }); camera.attachPuck(new PositionPuck(map)); // one call now moves both navigator.geolocation.watchPosition(({ coords }) => camera.follow({ lat: coords.latitude, lon: coords.longitude }, coords.heading ?? undefined), ); ``` | Option | Default | What it does | |---|---|---| | `pitch` | 60 | Tilt in degrees, clamped 0 to 85 | | `zoom` | 17 | Follow zoom | | `anchorY` | 0.72 | Puck's vertical screen position, so the camera looks up the road | | `easeMs` | 900 | Ease per fix, capped by the observed fix interval | | `autoRecentreMs` | 6000 | Idle time before auto-recentre; `0` disables it | Each `follow()` is an interruptible linear ease rather than a queued animation, so a fast fix cadence glides instead of stuttering. Any user drag, rotate, pitch or zoom switches `camera.mode` to `"free"` and the camera recentres itself after the idle timeout. `camera.overview(route.geometry)` fits the whole route top-down, `camera.resume()` returns to the chase cam. `PositionPuck` interpolates between fixes by default, gliding along the shortest arc to each new fix over a duration matched to the observed fix interval, so the puck and camera arrive together instead of the puck jumping once a second. Pass `{ interpolate: false }` to snap. The camera design is written up in full in [the 3D navigation camera](/news/navigation-camera-3d). Precedence for `pitch` and `zoom` is option, then Studio design (`extra.nav.camera`), then built-in default. ## Can you dim the travelled route and drop an arrow at the next turn? Yes, in two calls, and both survive a light/dark style swap. ```ts import { RouteLayer } from "@mapmap/maps"; const routes = new RouteLayer(map); await routes.route( { lon: -0.1278, lat: 51.5074 }, // London { lon: -1.8904, lat: 52.4862 }, // Birmingham { profile: "truck", truck: { heightM: 4.0, weightT: 40, hazmat: true, tunnelCode: "C" } }, ); routes.setProgress(0.42); // 0-1 of the route length, travelled routes.setManeuver([-1.8904, 52.4862], 135); // arrow at the next turn, bearing in degrees // …later routes.setProgress(0); // restore the plain line routes.clearManeuver(); ``` `setProgress` renders a `line-gradient` over the route source (built with `lineMetrics`), which is the vanishing route line effect. Pass `progressColor` in the layer options to change the dimmed colour. Derive the fraction from the guidance module's distance-remaining. ## Can you change label language without new tiles? Yes. MapMap tiles carry the OpenMapTiles multilingual `name:*` fields, so `setMapLanguage` rewrites the label expressions in place. ```ts import { ThemeScheduler, setMapLanguage } from "@mapmap/maps"; setMapLanguage(map, "de"); // relabel in German; null restores the default new ThemeScheduler({ lat: 51.5, lng: -0.13, onLight: () => map.setStyle("light"), onDark: () => map.setStyle("dark"), }); ``` `setMapLanguage` rewrites only layers whose `text-field` reads name properties, so road ref shields and house numbers are left alone. It validates the tag and throws on a malformed one, and it returns the ids of the layers it changed. It is a runtime layout-property rewrite, not a tile fetch, so it is instant and works offline. `ThemeScheduler` flips light and dark at local sunrise and sunset using a dependency-free solar calculation, re-arming at each boundary, and it handles polar day and night. `resolveTheme(date, lat, lng)` is the one-shot form. Call `dispose()` to stop it. Territory packages ship paired light/dark styles, so this works offline too. ## What does it not do? Four honest limits, all of them things we would rather you heard from us than found at 2am. - **`NavigationCamera` does not support the globe projection.** `NavigationCamera.isSupported(map)` returns `false` under globe or vertical-perspective, because the low-anchor offset maths and overview framing assume a mercator camera. Switch with `map.map.setProjection({ type: "mercator" })` before navigating. - **Voice de-duplication is your job.** Each step's `voice` array is ordered by descending trigger distance. Speak each instruction once as its distance is crossed. We do not track that for you. - **ESM only.** The package ships `dist/index.js` and `dist/index.d.ts`, and needs Node >= 18 to build. There is no CommonJS build. - **Pitch above 60 is allowed but not advised.** It is clamped at 85 and raises MapLibre's `maxPitch` for you, but MapLibre marks it experimental and DOM-marker pucks flatten at extreme tilt. Tiles and style writes are metered per request at the Standard price class, 0.05p per call, and the free tier is 50,000 included calls a month after you verify an email, with commercial use allowed. Style reads are public and unmetered. The map renders a small MapMap wordmark bottom-right, which you can remove with `logo: false`. Attribution is separate and is not removable by configuration: every compiled style carries "© OpenStreetMap contributors © OpenMapTiles" on its tile source, because the data licence requires it. ## Try it Install the package, point it at a style and route something: ```sh npm install @mapmap/maps maplibre-gl pmtiles ``` The [maps documentation](/docs/maps) covers tiles, hosted styles, Studio and the theme document. The [SDKs reference](/docs/sdks) has the full `@mapmap/maps` surface, the error shapes and the licensing rules. Every call shown here works identically against the hosted gateway and against your own self-hosted deployment.