@mapmap/maps 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +411 -0
- package/dist/index.d.ts +1180 -0
- package/dist/index.js +1827 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MapMap Maps SDK (@mapmap/maps)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Precode Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This package is proprietary first-party software of Precode Ltd, distributed
|
|
6
|
+
under a commercial licence. It is part of the MapMap
|
|
7
|
+
product. Use of this package requires a written commercial agreement with
|
|
8
|
+
Precode Ltd and a valid MapMap gateway API key; it is licensed per fleet
|
|
9
|
+
vehicle or per monthly-active-user (see docs/SDK-DISTRIBUTION.md). No licence,
|
|
10
|
+
express or implied, is granted except under that agreement.
|
|
11
|
+
|
|
12
|
+
This SDK is a thin wrapper over third-party open-source components, used under
|
|
13
|
+
their own permissive licences and not relicensed by this file:
|
|
14
|
+
|
|
15
|
+
- MapLibre GL JS (BSD-2-Clause) - the map renderer, a peer dependency.
|
|
16
|
+
- pmtiles (BSD-3-Clause) - the PMTiles protocol handler, a peer dependency.
|
|
17
|
+
|
|
18
|
+
Map data rendered through this SDK is derived from OpenStreetMap and is
|
|
19
|
+
subject to the Open Database License (ODbL) 1.0. The "© OpenStreetMap
|
|
20
|
+
contributors" attribution is legally required and must not be removed from
|
|
21
|
+
maps produced with this SDK.
|
|
22
|
+
|
|
23
|
+
See the repository root LICENSE, THIRD-PARTY-NOTICES.md and docs/LINEAGE.md.
|
package/README.md
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# @mapmap/maps - MapMap Maps SDK
|
|
2
|
+
|
|
3
|
+
A thin, well-typed TypeScript wrapper over [MapLibre GL JS](https://maplibre.org/)
|
|
4
|
+
that drops a **MapMap-branded map** (signed territory PMTiles) plus
|
|
5
|
+
**OSRM-compatible truck / ADR routing** into any web app - the way developers
|
|
6
|
+
use Mapbox GL, but on MapMap's own tiles and routing gateway.
|
|
7
|
+
|
|
8
|
+
The SDK does **not** reimplement MapLibre. MapLibre does all the rendering;
|
|
9
|
+
this package wires in the MapMap default style, registers the `pmtiles`
|
|
10
|
+
protocol, and gives you typed helpers for routing and ADR tunnel compliance
|
|
11
|
+
against the MapMap gateway.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @mapmap/maps maplibre-gl pmtiles
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`maplibre-gl` and `pmtiles` are **peer dependencies** - your app owns a single
|
|
20
|
+
shared copy (two MapLibre instances on one page break the WebGL context).
|
|
21
|
+
|
|
22
|
+
You also need a MapMap gateway API key (`snk_…`). See
|
|
23
|
+
[`docs/SDK-DISTRIBUTION.md`](../docs/SDK-DISTRIBUTION.md) for licensing.
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { MapMapMap, RouteLayer } from "@mapmap/maps";
|
|
29
|
+
import "maplibre-gl/dist/maplibre-gl.css";
|
|
30
|
+
|
|
31
|
+
const map = new MapMapMap({
|
|
32
|
+
container: "map",
|
|
33
|
+
apiKey: "snk_…",
|
|
34
|
+
style: "light", // "light" | "dark" | a StyleSpecification | a style URL
|
|
35
|
+
center: [-1.5, 52.6],
|
|
36
|
+
zoom: 6,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await map.whenReady();
|
|
40
|
+
|
|
41
|
+
const routes = new RouteLayer(map);
|
|
42
|
+
const route = await routes.route(
|
|
43
|
+
{ lng: -0.1278, lat: 51.5074 }, // London
|
|
44
|
+
{ lng: -1.5106, lat: 52.4081 }, // Birmingham
|
|
45
|
+
{
|
|
46
|
+
profile: "truck",
|
|
47
|
+
truck: { heightM: 4.0, weightT: 40, hazmat: true, tunnelCode: "C" },
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
console.log(route.distanceM, route.durationS); // metres, seconds
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The route line is drawn automatically in MapMap signal blue (`#3a86ff`)
|
|
55
|
+
with a darker casing. Call `routes.clear()` to remove it, or
|
|
56
|
+
`routes.route(...)` again to replace it.
|
|
57
|
+
|
|
58
|
+
## API surface
|
|
59
|
+
|
|
60
|
+
### `class MapMapMap` (alias: `createMap(options)`)
|
|
61
|
+
|
|
62
|
+
Wraps `maplibregl.Map`.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
new MapMapMap({
|
|
66
|
+
container, // string id or HTMLElement (required)
|
|
67
|
+
apiKey?, // gateway key, reused by RouteLayer/AdrCheck
|
|
68
|
+
baseUrl?, // gateway origin, default https://api.mapmap.ai
|
|
69
|
+
style?, // "light" | "dark" | Theme | StyleSpecification | URL
|
|
70
|
+
territoryTilesUrl?, // override the PMTiles URL in the default styles
|
|
71
|
+
center?, zoom?, // initial view ([lng, lat], number)
|
|
72
|
+
mapOptions?, // extra native MapLibre MapOptions (escape hatch)
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- `.map` - the underlying `maplibregl.Map`; use it for any native call.
|
|
77
|
+
- `.whenReady()` - resolves after the style and first tiles load.
|
|
78
|
+
- `.destroy()` - removes the map and frees the WebGL context.
|
|
79
|
+
- `.apiKey`, `.baseUrl` - read back for your own gateway calls.
|
|
80
|
+
- `.navDesign` - the parsed Studio `extra.nav` block when the `style` theme
|
|
81
|
+
carried one (see "Using your Studio design").
|
|
82
|
+
|
|
83
|
+
### `class RouteLayer`
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const routes = new RouteLayer(map /* MapMapMap or maplibregl.Map */, {
|
|
87
|
+
baseUrl?, apiKey?, id?, design?, // design: extra.nav route block
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await routes.route(from, to, { profile, truck }); // two-point
|
|
91
|
+
await routes.routePath([a, b, c], { profile, truck }); // multi-point
|
|
92
|
+
routes.current; // last ParsedRoute
|
|
93
|
+
routes.clear(); // remove the drawn line
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Coordinates accept `[lng, lat]`, `{ lng, lat }` or `{ lon, lat }`. A route
|
|
97
|
+
resolves to `{ distanceM, durationS, geometry (GeoJSON LineString), raw }`.
|
|
98
|
+
|
|
99
|
+
`truck` params map onto the gateway's OSRM truck vendor extensions:
|
|
100
|
+
`heightM`, `widthM`, `lengthM`, `weightT`, `hazmat`, and `tunnelCode` (ADR
|
|
101
|
+
8.6.4, e.g. `"C"` or `"B/D"` - the slash is URL-encoded for you). They only
|
|
102
|
+
take effect with `profile: "truck"`.
|
|
103
|
+
|
|
104
|
+
### `class PositionPuck`
|
|
105
|
+
|
|
106
|
+
The current-position marker: a coloured dot with a heading arrow, or a
|
|
107
|
+
custom image, styled by a Studio design (see "Using your Studio design").
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const puck = new PositionPuck(map /* MapMapMap or maplibregl.Map */, design?);
|
|
111
|
+
puck.setLocation({ lat, lon }, headingDeg?); // adds it on first call
|
|
112
|
+
puck.remove();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `class PlacesLayer` - places / store finder
|
|
116
|
+
|
|
117
|
+
Drop your own places (e.g. 300 store locations) onto the map: clustered
|
|
118
|
+
pins, popups, and "nearest branch" answers by straight line or by drive
|
|
119
|
+
time (via the gateway's `POST /matrix`).
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { PlacesLayer } from "@mapmap/maps";
|
|
123
|
+
|
|
124
|
+
const stores = new PlacesLayer(map /* MapMapMap or maplibregl.Map */, {
|
|
125
|
+
places: [
|
|
126
|
+
{ id: "bhm-01", name: "Birmingham", lat: 52.4862, lon: -1.8904 },
|
|
127
|
+
{ id: "man-01", name: "Manchester", lat: 53.4808, lon: -2.2426 },
|
|
128
|
+
// …or a plain GeoJSON FeatureCollection of Points
|
|
129
|
+
],
|
|
130
|
+
cluster: true, // default; clusterRadius?, clusterMaxZoom?
|
|
131
|
+
color: "#ff6b35", // pin + cluster colour (default signal blue)
|
|
132
|
+
fitBounds: true, // fit the view to the places on first set
|
|
133
|
+
popup: (place) => `<strong>${place.name}</strong>`,
|
|
134
|
+
onPlaceClick: (place, lngLat) => console.log(place.id, lngLat),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
stores.setPlaces(nextPlaces); // replace data in place
|
|
138
|
+
stores.nearest({ lat: 51.5, lon: -0.13 }, 3); // haversine, adds distanceM
|
|
139
|
+
await stores.nearestByDriveTime({ lat: 51.5, lon: -0.13 }, { n: 3, costing: "truck" });
|
|
140
|
+
stores.clear(); // remove pins; stores.destroy() also detaches listeners
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Clicking a cluster zooms in to expand it; the cursor becomes a pointer over
|
|
144
|
+
pins. Pass `icon: { url, size? }` for a custom pin image (falls back to the
|
|
145
|
+
circle pin if it fails to load). `nearestByDriveTime` sorts by `durationS`
|
|
146
|
+
(seconds, driven `distanceM` attached, unreachable places dropped) and reuses
|
|
147
|
+
the map's `baseUrl`/`apiKey` - or pass them as layer options.
|
|
148
|
+
|
|
149
|
+
### `class NavigationCamera`
|
|
150
|
+
|
|
151
|
+
The turnkey chase cam: follows each GPS fix course-up, tilted, with the
|
|
152
|
+
puck anchored low-centre (see "Navigation camera" below).
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const camera = new NavigationCamera(map /* MapMapMap or maplibregl.Map */, {
|
|
156
|
+
pitch?, // tilt in degrees, default 60, clamped 0-85
|
|
157
|
+
zoom?, // follow zoom, default 17
|
|
158
|
+
anchorY?, // puck's vertical screen position 0-1, default 0.72
|
|
159
|
+
easeMs?, // ease per fix, default 900 (capped by the fix interval)
|
|
160
|
+
autoRecentreMs?, // idle time before auto-recentre, default 6000; 0 = never
|
|
161
|
+
});
|
|
162
|
+
// pitch/zoom precedence: option > Studio design (extra.nav.camera, when the
|
|
163
|
+
// map is a MapMapMap built from a theme carrying one) > built-in default.
|
|
164
|
+
|
|
165
|
+
camera.follow({ lat, lon }, courseDeg?); // glide to a fix, course-up
|
|
166
|
+
camera.attachPuck(puck); // follow() then co-drives the puck
|
|
167
|
+
camera.overview(route.geometry); // whole route, top-down
|
|
168
|
+
camera.resume(); // back to the chase cam
|
|
169
|
+
camera.mode; // "follow" | "overview" | "free"
|
|
170
|
+
camera.destroy(); // remove listeners
|
|
171
|
+
NavigationCamera.isSupported(map); // false under globe projection
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `class AdrCheck`
|
|
175
|
+
|
|
176
|
+
Direct access to the gateway's `POST /adr/check` tunnel compliance engine:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { AdrCheck } from "@mapmap/maps";
|
|
180
|
+
|
|
181
|
+
const adr = new AdrCheck({ baseUrl: "https://api.mapmap.ai", apiKey: "snk_…" });
|
|
182
|
+
const decision = await adr.check({ hazmat: true, tunnelCode: "C", tunnelCategory: "D" });
|
|
183
|
+
// { status: "allowed" | "blocked", reason?, raw }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Pure helpers (no browser required)
|
|
187
|
+
|
|
188
|
+
Exported for server-side or test use - none of these touch MapLibre:
|
|
189
|
+
|
|
190
|
+
- `buildStyle({ theme, territoryTilesUrl })` → a MapLibre `StyleSpecification`.
|
|
191
|
+
- `buildRouteUrl(baseUrl, profile, points, truck?)` / `buildRouteQuery(truck?)`.
|
|
192
|
+
- `parseOsrmRoute(body)` → `ParsedRoute`.
|
|
193
|
+
- `toLngLat` / `formatCoord` / `formatCoords` - coordinate normalisation.
|
|
194
|
+
- `haversineDistanceM(a, b)` - straight-line distance in metres.
|
|
195
|
+
- `placesFromGeoJSON(collection)` - GeoJSON Points → `Place[]`.
|
|
196
|
+
- `registerPmtilesProtocol(gl?)` - install the `pmtiles://` handler yourself.
|
|
197
|
+
- `OSM_ATTRIBUTION`, `FULL_ATTRIBUTION`, `DEFAULT_TERRITORY_TILES_URL`,
|
|
198
|
+
`PALETTE_SLOTS`, `SOURCE_LAYERS`, `SIGNAL_BLUE`.
|
|
199
|
+
|
|
200
|
+
### Themes (MapMap Studio)
|
|
201
|
+
|
|
202
|
+
`buildStyle` (and `MapMapMap`'s `style` option) accepts `"light"`, `"dark"`,
|
|
203
|
+
or a **MapMap Studio `Theme`** document - the same theme JSON the `sn-style`
|
|
204
|
+
crate compiles server-side, so styles render identically in the browser and
|
|
205
|
+
in signed territory packages:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const map = new MapMapMap({
|
|
209
|
+
container: "map",
|
|
210
|
+
style: {
|
|
211
|
+
name: "midnight-fleet",
|
|
212
|
+
base: "dark",
|
|
213
|
+
palette: { water: "#0b2038", roadMajor: "#8a6d3b" },
|
|
214
|
+
layers: {
|
|
215
|
+
building: { visible: false },
|
|
216
|
+
"road-minor": { paint: { "line-width": 2 }, minzoom: 10 },
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- **Palette slots** (17): `background water waterway landcover landuse park
|
|
223
|
+
building aeroway road roadMajor path rail boundary boundaryMinor
|
|
224
|
+
textPrimary textSecondary textHalo`.
|
|
225
|
+
- **`extra.nav`** (Studio's navigation design block) is carried by the theme
|
|
226
|
+
file and ignored by style compilation - see "Using your Studio design"
|
|
227
|
+
below.
|
|
228
|
+
- **Layer ids** (paint order): `background landcover landuse park water
|
|
229
|
+
waterway aeroway building rail road-path road-minor road-major
|
|
230
|
+
boundary-minor boundary housenumber road-labels water-name poi-labels
|
|
231
|
+
mountain-peak-labels aerodrome-labels place-labels`.
|
|
232
|
+
- Per-layer overrides: `visible`, `paint`/`layout` (per-key merge), `filter`
|
|
233
|
+
(replace), `minzoom`/`maxzoom`. `extra_layers` (full MapLibre layers over
|
|
234
|
+
the `territory` source) are inserted above the base map, below labels.
|
|
235
|
+
- Unknown slots/layer ids throw with the accepted values listed.
|
|
236
|
+
- See `docs/STUDIO.md` for the full theme document reference.
|
|
237
|
+
|
|
238
|
+
### Using your Studio design
|
|
239
|
+
|
|
240
|
+
Studio's Navigation panel designs the turn-by-turn look - route line,
|
|
241
|
+
current-position puck and banner instruction - and stores it under
|
|
242
|
+
`extra.nav` in the theme JSON you download or copy. The block also survives
|
|
243
|
+
hosted publishing: Studio's publish sends it with the theme, and the gateway
|
|
244
|
+
serves it back from `GET /styles/{id}/theme`. Pass a theme file to
|
|
245
|
+
`createMap` and the whole navigation UI styles itself:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { createMap, RouteLayer, GuidanceBanner, PositionPuck } from "@mapmap/maps";
|
|
249
|
+
import theme from "./midnight-fleet.theme.json"; // downloaded from Studio
|
|
250
|
+
|
|
251
|
+
const map = createMap({ container: "map", apiKey: "snk_…", style: theme });
|
|
252
|
+
await map.whenReady();
|
|
253
|
+
|
|
254
|
+
// Route line: colour, width, opacity and casing from extra.nav.route.
|
|
255
|
+
const routes = new RouteLayer(map);
|
|
256
|
+
await routes.route({ lng: -0.1278, lat: 51.5074 }, { lng: -1.5106, lat: 52.4081 });
|
|
257
|
+
|
|
258
|
+
// Banner: colours, font size, padding, radius, max width, height and the
|
|
259
|
+
// lane row from extra.nav.banner.
|
|
260
|
+
const banner = new GuidanceBanner(document.body, map.navDesign?.banner);
|
|
261
|
+
|
|
262
|
+
// Puck: dot + heading arrow (or your custom image) from extra.nav.puck.
|
|
263
|
+
const puck = new PositionPuck(map);
|
|
264
|
+
puck.setLocation({ lat: 51.5074, lon: -0.1278 }, 45); // heading in degrees
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- `map.navDesign` is the parsed `extra.nav` block (`undefined` when the
|
|
268
|
+
theme has none); `navDesignFromTheme(theme)` / `parseNavDesign(value)` are
|
|
269
|
+
exported for standalone use, and `defaultNavDesign()` matches the SDK's
|
|
270
|
+
built-in look.
|
|
271
|
+
- `RouteLayer` and `PositionPuck` pick the design up from a `MapMapMap`
|
|
272
|
+
automatically; pass `{ design }` / a second argument to override. Without
|
|
273
|
+
a design everything renders exactly as before.
|
|
274
|
+
- `extra.nav.camera` (optional since v1, whole block and each field) holds
|
|
275
|
+
the drive-camera tokens: `pitch` (degrees, 0-85, default 60), `zoom`
|
|
276
|
+
(14-20, default 17.5) and `speedMps` (drive speed in m/s, 2-40, default
|
|
277
|
+
12 - used by Studio's demo drive and route simulators; a camera following
|
|
278
|
+
real GPS fixes ignores it). `NavigationCamera` reads the block's
|
|
279
|
+
pitch/zoom as its option defaults when built from a `MapMapMap`; explicit
|
|
280
|
+
options still win (option > design > built-in). Themes saved without the
|
|
281
|
+
block load unchanged and stay `"version": 1`.
|
|
282
|
+
- The nav block travels with the **theme document**, never the compiled
|
|
283
|
+
style: `extra` round-trips through `POST /styles` and
|
|
284
|
+
`GET /styles/{id}/theme` (bounded at 256 KB serialised), but a compiled
|
|
285
|
+
`style.json` URL alone never carries it. For a hosted style, fetch the
|
|
286
|
+
design from the theme endpoint (public, uncached):
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { navDesignFromThemeUrl } from "@mapmap/maps";
|
|
290
|
+
|
|
291
|
+
const design = await navDesignFromThemeUrl(
|
|
292
|
+
"https://api.mapmap.ai/styles/midnight-fleet-a1b2c3/theme",
|
|
293
|
+
);
|
|
294
|
+
const banner = new GuidanceBanner(document.body, design?.banner);
|
|
295
|
+
const routes = new RouteLayer(map, { design: design?.route });
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`navDesignFromThemeUrl(url, fetchImpl?)` returns `undefined` when the
|
|
299
|
+
theme has no nav block and throws on HTTP/network failure; pass your own
|
|
300
|
+
`fetch` for tests or polyfills.
|
|
301
|
+
- Parsing is lenient and clamped (widths 0.5-20, opacity 0-1, banner height
|
|
302
|
+
40-120 or unset for auto, puck images `https:`/`data:` only, data
|
|
303
|
+
payloads capped at 64 KB) - a malformed block falls back per-field to the
|
|
304
|
+
defaults rather than failing.
|
|
305
|
+
|
|
306
|
+
## The MapMap mark
|
|
307
|
+
|
|
308
|
+
Maps render a small MapMap wordmark bottom-right (the same convention as
|
|
309
|
+
Google Maps and Mapbox). It links to mapmap.ai, needs no network fetch,
|
|
310
|
+
and is on by default: pass `logo: false` to `createMap` to remove it, or
|
|
311
|
+
`logo: { position, href }` to customise. Attribution is separate and not
|
|
312
|
+
removable.
|
|
313
|
+
|
|
314
|
+
## Turn-by-turn guidance
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { GuidanceBanner, extractGuidance, speak } from "@mapmap/maps";
|
|
318
|
+
|
|
319
|
+
const route = await routes.route(from, to, {
|
|
320
|
+
costing: "truck",
|
|
321
|
+
voice: true,
|
|
322
|
+
banner: true,
|
|
323
|
+
language: "en-GB",
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const steps = extractGuidance(route);
|
|
327
|
+
const banner = new GuidanceBanner(document.body, map.navDesign?.banner);
|
|
328
|
+
banner.update(steps[0].banners[0] ?? null);
|
|
329
|
+
// As the driver advances, fire each voice prompt once when its
|
|
330
|
+
// distanceAlongGeometry trigger is crossed:
|
|
331
|
+
speak(steps[0].voice[0]);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Voice prompts arrive plain and as SSML; banners carry the maneuver glyph
|
|
335
|
+
and, where OSM has `turn:lanes` data, a lane diagram (sprite icons
|
|
336
|
+
`lane-left`, `lane-straight`, … ship with the map assets).
|
|
337
|
+
|
|
338
|
+
## Navigation camera
|
|
339
|
+
|
|
340
|
+
`NavigationCamera` gives the Google-Maps-style chase cam in one call per
|
|
341
|
+
fix: tilted (pitch 60), course-up, the puck anchored low-centre so the
|
|
342
|
+
camera looks up the road, each fix gliding into the next. Feed it from the
|
|
343
|
+
same loop that drives your guidance:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import { NavigationCamera, PositionPuck } from "@mapmap/maps";
|
|
347
|
+
|
|
348
|
+
const camera = new NavigationCamera(map, { pitch: 60, zoom: 17 });
|
|
349
|
+
camera.attachPuck(new PositionPuck(map)); // one call now moves both
|
|
350
|
+
navigator.geolocation.watchPosition(({ coords }) =>
|
|
351
|
+
camera.follow({ lat: coords.latitude, lon: coords.longitude }, coords.heading ?? undefined),
|
|
352
|
+
);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- **Smoothness:** every `follow()` is an interruptible linear `easeTo`
|
|
356
|
+
whose duration is `easeMs` capped by the observed fix interval, so a
|
|
357
|
+
fast fix cadence glides continuously instead of queueing animations.
|
|
358
|
+
- **User gestures win:** any drag / rotate / pitch / zoom switches the
|
|
359
|
+
camera to `"free"` mode (easing stops, fixes keep being recorded and the
|
|
360
|
+
puck keeps moving). It recentres itself after `autoRecentreMs` of idle
|
|
361
|
+
(default 6 s; `0` disables it - call `camera.resume()` yourself).
|
|
362
|
+
- **Overview:** `camera.overview(route.geometry)` fits the whole route
|
|
363
|
+
top-down (pitch 0, north-up); `camera.resume()` returns to the chase cam
|
|
364
|
+
at the last fix. `camera.mode` reports `"follow" | "overview" | "free"`.
|
|
365
|
+
- **Globe projection:** `NavigationCamera.isSupported(map)` returns
|
|
366
|
+
`false` when the map runs the globe (or vertical-perspective)
|
|
367
|
+
projection - the low-anchor offset maths and overview framing assume a
|
|
368
|
+
mercator camera. Switch to mercator
|
|
369
|
+
(`map.map.setProjection({ type: "mercator" })`) before navigating.
|
|
370
|
+
- **Pitch above 60** is allowed (clamped at 85) and raises the map's
|
|
371
|
+
`maxPitch` for you, but MapLibre marks it experimental and DOM-marker
|
|
372
|
+
pucks flatten at extreme tilt - stay at 60 unless you have a reason.
|
|
373
|
+
|
|
374
|
+
## Tiles, attribution and the verifying key
|
|
375
|
+
|
|
376
|
+
The default light/dark styles load MapMap territory tiles as **PMTiles** over
|
|
377
|
+
the `pmtiles://` protocol. Override the source with `territoryTilesUrl` (a
|
|
378
|
+
`pmtiles://…` or plain `https://…` URL - the prefix is added for you).
|
|
379
|
+
|
|
380
|
+
`© OpenStreetMap contributors © OpenMapTiles` attribution is **legally
|
|
381
|
+
required** (ODbL + CC-BY 4.0) and is baked into every compiled style's tile
|
|
382
|
+
source. It is not themable - no theme field can remove it.
|
|
383
|
+
|
|
384
|
+
MapMap **territory packages** (the offline `.snpkg` bundles the mobile SDKs
|
|
385
|
+
consume) are ed25519-signed and the app pins the factory public key. This web
|
|
386
|
+
SDK renders tiles served by the gateway/CDN over HTTPS and does not verify
|
|
387
|
+
package signatures in the browser; the signing/verifying-key story for the
|
|
388
|
+
signed territory channel is documented in
|
|
389
|
+
[`docs/SDK-DISTRIBUTION.md`](../docs/SDK-DISTRIBUTION.md) and
|
|
390
|
+
[`docs/TERRITORY-UPDATES.md`](../docs/TERRITORY-UPDATES.md).
|
|
391
|
+
|
|
392
|
+
## Bundler notes
|
|
393
|
+
|
|
394
|
+
- ESM only. Ships `dist/index.js` + `dist/index.d.ts`.
|
|
395
|
+
- `maplibre-gl` and `pmtiles` stay external; provide them in your app.
|
|
396
|
+
- No bundler? See [`examples/index.html`](./examples/index.html) - it uses a
|
|
397
|
+
native import map and the unpkg ESM CDN.
|
|
398
|
+
|
|
399
|
+
## Develop
|
|
400
|
+
|
|
401
|
+
```sh
|
|
402
|
+
npm install
|
|
403
|
+
npm run typecheck # tsc --noEmit
|
|
404
|
+
npm run build # tsup → dist/ (ESM + .d.ts)
|
|
405
|
+
npm test # vitest (pure-logic unit tests, no browser)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Licence
|
|
409
|
+
|
|
410
|
+
Proprietary - © 2026 Precode Ltd, distributed under a commercial licence.
|
|
411
|
+
See [`LICENSE`](./LICENSE).
|