@mapmap/maps 0.1.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,4 +1,259 @@
1
- import maplibregl, { StyleSpecification, Map, MapOptions } from 'maplibre-gl';
1
+ import maplibregl, { CustomLayerInterface, Map, StyleSpecification, MapOptions, ExpressionSpecification } from 'maplibre-gl';
2
+
3
+ /**
4
+ * Shared public types for @mapmap/maps.
5
+ *
6
+ * These are runtime-free (type-only) so the pure logic modules that import
7
+ * them stay loadable in Node without a browser or maplibre-gl.
8
+ */
9
+ /** A longitude/latitude pair, accepted in the three common shapes. */
10
+ type LngLatLike = [number, number] | {
11
+ lng: number;
12
+ lat: number;
13
+ } | {
14
+ lon: number;
15
+ lat: number;
16
+ };
17
+ /** OSRM routing profile. MapMap ships `driving`, `walking` and `truck`. */
18
+ type RouteProfile = "driving" | "walking" | "truck" | (string & {});
19
+ /**
20
+ * Truck / ADR vehicle parameters forwarded to the gateway's OSRM truck
21
+ * vendor extensions. All optional; only `truck`-profile requests honour them.
22
+ */
23
+ interface TruckParams {
24
+ /** Vehicle height, metres. */
25
+ heightM?: number;
26
+ /** Vehicle width, metres. */
27
+ widthM?: number;
28
+ /** Vehicle length, metres. */
29
+ lengthM?: number;
30
+ /** Gross weight, tonnes. */
31
+ weightT?: number;
32
+ /** Carrying dangerous goods (ADR). */
33
+ hazmat?: boolean;
34
+ /**
35
+ * ADR 8.6.4 tunnel restriction code, e.g. `"C"` or `"B/D"`. The slash is
36
+ * URL-encoded automatically.
37
+ */
38
+ tunnelCode?: string;
39
+ }
40
+ /** Options for a single route request. */
41
+ interface RouteOptions {
42
+ /** Routing profile. Defaults to `"driving"`. */
43
+ profile?: RouteProfile;
44
+ /** Truck / ADR parameters (only meaningful with `profile: "truck"`). */
45
+ truck?: TruckParams;
46
+ /** Request spoken `voiceInstructions` on each step (see `guidance.ts`). */
47
+ voice?: boolean;
48
+ /** Request visual `bannerInstructions` (and lane data) on each step. */
49
+ banner?: boolean;
50
+ /** BCP 47 narration language, e.g. `"en-GB"`. */
51
+ language?: string;
52
+ }
53
+ /** A parsed GeoJSON LineString geometry (`[lng, lat]` positions). */
54
+ interface RouteGeometry {
55
+ type: "LineString";
56
+ coordinates: [number, number][];
57
+ }
58
+ /** A parsed OSRM route, normalised to the fields callers actually use. */
59
+ interface ParsedRoute {
60
+ /** Total distance, metres. */
61
+ distanceM: number;
62
+ /** Total duration, seconds. */
63
+ durationS: number;
64
+ /** Route line as GeoJSON, ready to hand to a MapLibre source. */
65
+ geometry: RouteGeometry;
66
+ /** The raw OSRM `routes[0]` object, for callers that need leg/step detail. */
67
+ raw: Record<string, unknown>;
68
+ }
69
+ /** The ADR category of a tunnel, `"A"` (least restrictive) … `"E"`. */
70
+ type AdrTunnelCategory = "A" | "B" | "C" | "D" | "E";
71
+ /**
72
+ * Physical truck dimensions for the ADR vehicle profile (mirrors
73
+ * `sn_adr::TruckDimensions`). All optional; unset fields default to the EU
74
+ * 96/53/EC maximum-authorised articulated vehicle (4.0 m high, 2.55 m wide,
75
+ * 16.5 m long, 40 t gross), matching the Rust `Default`.
76
+ */
77
+ interface AdrDimensions {
78
+ /** Vehicle height, metres. Defaults to `4.0`. */
79
+ heightM?: number;
80
+ /** Vehicle width, metres. Defaults to `2.55`. */
81
+ widthM?: number;
82
+ /** Vehicle length, metres. Defaults to `16.5`. */
83
+ lengthM?: number;
84
+ /** Gross combination weight, tonnes. Defaults to `40`. */
85
+ grossWeightT?: number;
86
+ /** Heaviest single-axle load, tonnes, if known. */
87
+ axleLoadT?: number;
88
+ /** Number of axles, if known. */
89
+ axleCount?: number;
90
+ }
91
+ /**
92
+ * Input for the gateway's `POST /adr/check` compliance endpoint: the
93
+ * vehicle's ADR profile plus the category of the tunnel to check. Serialised
94
+ * on the wire as `{"adr": <AdrVehicleProfile>, "tunnel_category": "A".."E"}`
95
+ * (see `sn-gateway` `routes/adr.rs`).
96
+ */
97
+ interface AdrCheckRequest {
98
+ /** Carrying dangerous goods. */
99
+ hazmat: boolean;
100
+ /**
101
+ * The *vehicle load's* ADR 8.6.4 tunnel restriction code, e.g. `"C"` or
102
+ * `"C/E"`. Omit when carrying dangerous goods of unknown code (the
103
+ * gateway then conservatively treats the load as code `B`). This is not
104
+ * the tunnel's category — that goes in {@link tunnelCategory}.
105
+ */
106
+ tunnelCode?: string;
107
+ /** Vehicle dimensions. Defaults to the EU standard artic maximums. */
108
+ dimensions?: AdrDimensions;
109
+ /** The ADR category of the *tunnel* to check, `"A"` … `"E"`. */
110
+ tunnelCategory: AdrTunnelCategory;
111
+ }
112
+ /** Parsed response from `POST /adr/check`. */
113
+ interface AdrCheckResult {
114
+ /** `"allowed"` or `"blocked"`. */
115
+ status: string;
116
+ /** Human-readable justification citing ADR 8.6.4 where blocked. */
117
+ reason?: string;
118
+ /** The raw response body. */
119
+ raw: Record<string, unknown>;
120
+ }
121
+
122
+ /**
123
+ * Effects engine - named, geometry-anchored visual effects rendered through
124
+ * MapLibre's stable `CustomLayerInterface` (first-party GLSL, no runtime
125
+ * dependencies). The first effect is `"flow"`: an animated energy ribbon
126
+ * along the active route line, drawn as a triangle strip with a flowing
127
+ * pulse gradient driven by a time uniform.
128
+ *
129
+ * Deliberately NOT full-map post-processing: MapLibre has no official
130
+ * post-process hook, so every effect here anchors to geometry and rides the
131
+ * supported custom-layer API. New effects register in {@link ROUTE_EFFECTS}.
132
+ *
133
+ * Accessibility and resilience:
134
+ * - `prefers-reduced-motion: reduce` freezes the ribbon to a static
135
+ * gradient (no animation loop, no repaints).
136
+ * - If WebGL setup for the custom layer fails (shader compile/link, lost
137
+ * context), the effect silently disables itself - the ordinary route
138
+ * line underneath is untouched - and logs a single console warning.
139
+ *
140
+ * The tessellation and colour helpers are pure and unit-tested in Node;
141
+ * only the layer object itself touches WebGL.
142
+ */
143
+
144
+ /** The names the effects registry knows. Grows as effects land. */
145
+ type RouteEffectName = "flow";
146
+ /** Options for the `"flow"` route effect (all optional). */
147
+ interface RouteFlowOptions {
148
+ /** Ribbon colour (hex or rgb()/rgba()). Defaults to MapMap signal blue. */
149
+ color?: string;
150
+ /** Ribbon width in CSS pixels. Defaults to `10`. */
151
+ width?: number;
152
+ /** Flow speed in pulse cycles per second. Defaults to `0.6`. */
153
+ speed?: number;
154
+ /** MapLibre layer id. Defaults to `"mapmap-route-effect"`. */
155
+ id?: string;
156
+ }
157
+ /** Floats per ribbon vertex: x, y (mercator), nx, ny (unit normal), progress. */
158
+ declare const RIBBON_FLOATS_PER_VERTEX = 5;
159
+ /** A tessellated route ribbon, ready for a `TRIANGLE_STRIP` draw. */
160
+ interface RibbonMesh {
161
+ /**
162
+ * Interleaved vertex data, {@link RIBBON_FLOATS_PER_VERTEX} floats per
163
+ * vertex: mercator position, the unit extrusion normal (the shader
164
+ * extrudes `pos + normal * halfWidth`, so width stays zoom-correct
165
+ * without re-tessellating), and progress along the route (0 at the
166
+ * start, 1 at the end - the flowing gradient's coordinate).
167
+ */
168
+ vertices: Float32Array;
169
+ /** Number of vertices (two per route point kept). */
170
+ vertexCount: number;
171
+ }
172
+ /**
173
+ * Project a `[lng, lat]` to web-mercator world coordinates in [0, 1]
174
+ * (MapLibre's `MercatorCoordinate` convention: x east, y south).
175
+ */
176
+ declare function lngLatToMercator(lngLat: [number, number]): [number, number];
177
+ /**
178
+ * Tessellate a route line into a centreline triangle strip: two vertices
179
+ * per point, each carrying the point's mercator position, a unit extrusion
180
+ * normal (averaged between adjacent segments at joins - a bevel-free miter
181
+ * that slightly thins very sharp corners, which is invisible on a soft
182
+ * glow ribbon), and normalised progress along the line.
183
+ *
184
+ * Pure: no WebGL, no map. Consecutive duplicate points are dropped; fewer
185
+ * than two distinct points yield an empty mesh.
186
+ */
187
+ declare function tessellateRouteRibbon(coordinates: [number, number][]): RibbonMesh;
188
+ /**
189
+ * Parse a CSS colour to premultiplication-ready `[r, g, b, a]` (0-1
190
+ * channels). Supports the hex forms and `rgb()`/`rgba()`; anything else
191
+ * (named colours, `hsl()`) returns `undefined` and the caller falls back
192
+ * to the default - this runs where no canvas is guaranteed, so no
193
+ * browser colour parser is available.
194
+ */
195
+ declare function parseCssColour(colour: string): [number, number, number, number] | undefined;
196
+ /** Whether the user has asked for reduced motion. Safe without a DOM. */
197
+ declare function prefersReducedMotion(): boolean;
198
+ /** Default flow parameters (exported so docs/tests state one truth). */
199
+ declare const FLOW_DEFAULTS: {
200
+ readonly color: "#3a86ff";
201
+ readonly width: 10;
202
+ readonly speed: 0.6;
203
+ };
204
+ /**
205
+ * A MapLibre custom layer (`CustomLayerInterface`) drawing the flowing
206
+ * route ribbon. Construct via {@link createRouteEffect} (the registry) or
207
+ * use `MapMapMap.setRouteEffect("flow")`, which manages the lifecycle.
208
+ */
209
+ declare class FlowRouteEffectLayer implements CustomLayerInterface {
210
+ readonly id: string;
211
+ readonly type: "custom";
212
+ readonly renderingMode: "2d";
213
+ private geometry;
214
+ private readonly colour;
215
+ private readonly widthPx;
216
+ private readonly speed;
217
+ private readonly animate;
218
+ private mapRef;
219
+ private glRef;
220
+ private program;
221
+ private buffer;
222
+ private vertexCount;
223
+ private repeats;
224
+ private startedAt;
225
+ private failed;
226
+ private aPos;
227
+ private aNormal;
228
+ private aProgress;
229
+ private uMatrix;
230
+ private uHalfWidth;
231
+ private uColor;
232
+ private uPhase;
233
+ private uRepeats;
234
+ constructor(geometry: RouteGeometry, options?: RouteFlowOptions);
235
+ /** Swap the ribbon onto a new route line (e.g. after a reroute). */
236
+ setGeometry(geometry: RouteGeometry): void;
237
+ onAdd(map: Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
238
+ onRemove(_map: Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
239
+ render(gl: WebGLRenderingContext | WebGL2RenderingContext, args: unknown): void;
240
+ private upload;
241
+ }
242
+ /** A route effect layer: a custom layer that can retarget its geometry. */
243
+ interface RouteEffectLayer extends CustomLayerInterface {
244
+ setGeometry(geometry: RouteGeometry): void;
245
+ }
246
+ /**
247
+ * The effect registry: name -> layer factory. New effects (ambient life,
248
+ * seasonal snow, fog-of-exploration) add an entry here and a name to
249
+ * {@link RouteEffectName}.
250
+ */
251
+ declare const ROUTE_EFFECTS: Record<RouteEffectName, (geometry: RouteGeometry, options?: RouteFlowOptions) => RouteEffectLayer>;
252
+ /**
253
+ * Create a named route effect layer. Throws an agent-readable error for
254
+ * unknown names (the accepted list included).
255
+ */
256
+ declare function createRouteEffect(name: RouteEffectName, geometry: RouteGeometry, options?: RouteFlowOptions): RouteEffectLayer;
2
257
 
3
258
  /**
4
259
  * The MapMap logo control: a small wordmark on the map, the same
@@ -104,6 +359,23 @@ interface LayerOverride {
104
359
  minzoom?: number;
105
360
  maxzoom?: number;
106
361
  }
362
+ /**
363
+ * The optional visual-effects block, mirroring `sn_style::Effects`. Unlike
364
+ * `extra`, this IS carried into the compiled style — as
365
+ * `metadata["mapmap:effects"]` — so any consumer of a compiled `style.json`
366
+ * (hosted style URL, baked territory package) can honour it without the
367
+ * theme document. Renderers that do not know the effects engine ignore the
368
+ * metadata and render the style unchanged.
369
+ */
370
+ interface ThemeEffects {
371
+ /** Route-line effect: `"flow"` (animated energy ribbon) or `"none"`. */
372
+ route?: "flow" | "none";
373
+ /**
374
+ * Effect parameters, validated per effect. For `flow`: `color` (CSS
375
+ * colour), `width` (px, 0.5-40), `speed` (cycles/s, 0-10).
376
+ */
377
+ params?: Record<string, unknown>;
378
+ }
107
379
  /**
108
380
  * A MapMap Studio theme: the unit users and agents edit, store and publish.
109
381
  * Mirrors `sn_style::Theme` (all fields carry serde defaults, so everything
@@ -146,22 +418,38 @@ interface Theme {
146
418
  * source-layer.
147
419
  */
148
420
  extra_layers?: JsonObject[];
421
+ /**
422
+ * Optional visual effects (route ribbon etc.). Compiled into
423
+ * `metadata["mapmap:effects"]` on the style — see {@link ThemeEffects}.
424
+ * Mirrors `Theme::effects`.
425
+ */
426
+ effects?: ThemeEffects;
149
427
  /**
150
428
  * Non-style extras carried by the THEME DOCUMENT, not the compiled
151
429
  * style. Studio stores its navigation design block here (`extra.nav`;
152
- * see `nav-design.ts`). {@link buildStyle} ignores it entirely - the
430
+ * see `nav-design.ts`) and its POI category design (`extra.poi`; see
431
+ * `poi-design.ts`). {@link buildStyle} ignores it entirely - the
153
432
  * compiled `style.json` is byte-identical with or without it. Mirrors
154
433
  * `Theme::extra` in the canonical Rust crate: the hosted styles API
155
434
  * stores the block verbatim (bounded at 256 KB serialised) and serves it
156
435
  * back from `GET /styles/{id}/theme`, so a Studio design survives
157
436
  * publishing - consume it via `createMap` with a theme document or
158
- * `navDesignFromThemeUrl` with the hosted theme URL.
437
+ * `navDesignFromThemeUrl` / `poiDesignFromThemeUrl` with the hosted
438
+ * theme URL.
159
439
  */
160
440
  extra?: {
161
441
  nav?: unknown;
442
+ poi?: unknown;
162
443
  };
163
444
  }
164
445
  interface BuildStyleOptions {
446
+ /**
447
+ * Label language: an ISO 639 code selects `name:<code>` with a
448
+ * local-name fallback; the literal `"local"` selects the local name
449
+ * outright; omitted keeps the default (English-first). The tiles carry
450
+ * the OSM `name:*` set, so switching involves no tile rebuild.
451
+ */
452
+ labelLanguage?: string;
165
453
  /**
166
454
  * Theme to build: a built-in name (`"light"` / `"dark"`) or a full
167
455
  * {@link Theme} document. Defaults to `"light"`.
@@ -175,6 +463,22 @@ interface BuildStyleOptions {
175
463
  }
176
464
  /** Ensure a tiles URL carries the `pmtiles://` protocol prefix. */
177
465
  declare function toPmtilesUrl(url: string): string;
466
+ /**
467
+ * The `metadata` key a compiled style carries its effects block under.
468
+ * Shared with `sn-style` (compile.rs) and read back by `MapMapMap` to
469
+ * auto-enable the route effect for styles that request one.
470
+ */
471
+ declare const EFFECTS_METADATA_KEY = "mapmap:effects";
472
+ /**
473
+ * The effects block from a compiled style's `metadata`, if the style
474
+ * carries a valid one (`{ route: "flow", params? }`). Lenient by design —
475
+ * it reads FOREIGN styles (hosted URLs, baked packages), so anything
476
+ * malformed returns `undefined` rather than throwing.
477
+ */
478
+ declare function effectsFromStyleMetadata(metadata: unknown): {
479
+ route: "flow";
480
+ params?: Record<string, unknown>;
481
+ } | undefined;
178
482
  declare function buildStyle(options?: BuildStyleOptions): StyleSpecification;
179
483
 
180
484
  /**
@@ -331,6 +635,128 @@ declare function navDesignFromTheme(theme: Theme | undefined | null): NavDesign
331
635
  */
332
636
  declare function navDesignFromThemeUrl(url: string, fetchImpl?: typeof fetch): Promise<NavDesign | undefined>;
333
637
 
638
+ /**
639
+ * Studio POI category-design block (`extra.poi`) - types, defaults, parser
640
+ * and appliers.
641
+ *
642
+ * MapMap Studio's POIs panel colours the ~8 POI categories - the coloured
643
+ * category dot drawn under a POI label, and the `poi-labels` text colour -
644
+ * and stores the result as a small design-token block under `extra.poi` in
645
+ * the theme JSON it downloads/copies/publishes, exactly like the navigation
646
+ * design under `extra.nav` (see nav-design.ts). This module reads that
647
+ * block and applies the label-text part to a live MapLibre map; the dot
648
+ * images themselves are runtime-rasterised by their host (the MapMap
649
+ * website today - sprite-based icons arrive with marker packs).
650
+ *
651
+ * LOCKSTEP: this file mirrors `website/lib/studio-poi.ts` (the Studio
652
+ * reference parser) - identical category ids, built-in colours, leniency
653
+ * and expression building. The class->category mapping mirrors
654
+ * `website/lib/poi-icons.ts` (`POI_CLASS_CATEGORIES`). Keep them in sync so
655
+ * a theme previews in Studio exactly as the SDK applies it. Not imported
656
+ * from the website because the packages do not depend on each other.
657
+ *
658
+ * PUBLISH NOTE: the block travels with the THEME DOCUMENT, never with the
659
+ * compiled `style.json`. The canonical `sn_style` crate stores `extra`
660
+ * verbatim (bounded at 256 KB serialised) and the gateway serves it back
661
+ * from `GET /styles/{id}/theme`, so a design survives Studio's publish
662
+ * flow. Read it from a theme file you pass to `createMap` / `buildStyle`,
663
+ * or fetch it from a hosted style with {@link poiDesignFromThemeUrl} - a
664
+ * compiled style URL alone never carries it.
665
+ */
666
+
667
+ /** One category's overrides. Both fields optional: absent = built-in. */
668
+ interface PoiCategoryDesign {
669
+ /** Dot fill colour (replaces the built-in category brand colour). */
670
+ color?: string;
671
+ /** Label text colour for this category (replaces `textSecondary`). */
672
+ textColor?: string;
673
+ }
674
+ /** The `extra.poi` block: POI category design. */
675
+ interface PoiDesign {
676
+ /** Block schema version; currently always 1. */
677
+ version: 1;
678
+ /** Overrides by category id; empty = all built-in. */
679
+ categories: Record<string, PoiCategoryDesign>;
680
+ }
681
+ /**
682
+ * The valid category ids with their built-in dot colours, in presentation
683
+ * order. LOCKSTEP: mirrors `POI_CATEGORIES` in `website/lib/poi-icons.ts`.
684
+ */
685
+ declare const POI_CATEGORY_COLORS: readonly (readonly [string, string])[];
686
+ /** The valid category ids, in presentation order. */
687
+ declare const POI_CATEGORY_IDS: readonly string[];
688
+ /**
689
+ * OpenMapTiles `poi` layer `class` values grouped into the categories
690
+ * above; every class not listed falls through to `services`. LOCKSTEP:
691
+ * mirrors `POI_CLASS_CATEGORIES` in `website/lib/poi-icons.ts`.
692
+ */
693
+ declare const POI_CLASS_CATEGORIES: Record<string, string[]>;
694
+ /** The built-in dot colour for a category id. */
695
+ declare function builtInPoiColor(categoryId: string): string | undefined;
696
+ /** No overrides: every category keeps its built-in dot and label colours. */
697
+ declare function defaultPoiDesign(): PoiDesign;
698
+ /** True when the design changes nothing. */
699
+ declare function poiDesignIsDefault(design: PoiDesign): boolean;
700
+ /**
701
+ * Lenient parse of an `extra.poi` value: unknown category ids and invalid
702
+ * colours are dropped rather than failing, so an imported theme never
703
+ * breaks on its POI block. LOCKSTEP: identical semantics to
704
+ * `parsePoiDesign` in `website/lib/studio-poi.ts`.
705
+ */
706
+ declare function parsePoiDesign(value: unknown): PoiDesign;
707
+ /**
708
+ * Reads and parses the `extra.poi` block from a Studio theme document.
709
+ * Returns `undefined` when the theme carries no poi block at all (so
710
+ * callers can tell "no design" apart from "default design"); a present but
711
+ * malformed block parses leniently to the defaults. Same contract as
712
+ * `navDesignFromTheme`.
713
+ */
714
+ declare function poiDesignFromTheme(theme: Theme | undefined | null): PoiDesign | undefined;
715
+ /**
716
+ * Fetches a hosted theme document and reads its `extra.poi` block - point
717
+ * it at the gateway's public, never-cached theme endpoint, e.g.
718
+ * `https://api.mapmap.ai/styles/midnight-fleet-a1b2c3/theme`. Returns
719
+ * `undefined` when the theme carries no poi block; throws on network
720
+ * failure, a non-2xx response or a non-JSON body. Same contract as
721
+ * {@link navDesignFromThemeUrl}.
722
+ *
723
+ * @param fetchImpl Optional `fetch` replacement (tests, Node polyfills).
724
+ */
725
+ declare function poiDesignFromThemeUrl(url: string, fetchImpl?: typeof fetch): Promise<PoiDesign | undefined>;
726
+ /**
727
+ * The `poi-labels` `text-color` value for a design: the plain fallback
728
+ * colour when no category overrides label text, otherwise a `match`
729
+ * expression keyed on the tile's `class` property. Every non-services
730
+ * category always gets an arm (override or fallback) so unmatched classes
731
+ * - the `services` fallback arm - never inherit another category's colour.
732
+ * LOCKSTEP: mirrors `poiTextColorExpression` in `website/lib/studio-poi.ts`.
733
+ */
734
+ declare function poiTextColorExpression(design: PoiDesign, fallback: string): unknown;
735
+ /** The subset of the MapLibre map surface {@link applyPoiDesign} touches. */
736
+ interface PoiPaintMap {
737
+ getStyle(): {
738
+ layers?: {
739
+ id: string;
740
+ type?: string;
741
+ }[];
742
+ } | undefined;
743
+ setPaintProperty(layerId: string, name: string, value: unknown): unknown;
744
+ }
745
+ /**
746
+ * Applies a design's per-category label text colours to every `poi-labels`
747
+ * layer on a live map (including per-territory clones named
748
+ * `poi-labels@<territory>`). `fallbackTextColor` is the colour
749
+ * non-overridden categories keep - pass the style's `poi-labels` text
750
+ * colour (the theme's `textSecondary` slot; defaults to the light-base
751
+ * value). A default design restores the plain fallback, so this is safe to
752
+ * call on every design change.
753
+ *
754
+ * NOTE: runtime paint changes do not survive a later `setStyle`; re-apply
755
+ * after style swaps (the MapMap website bakes the expression into the
756
+ * style object instead for exactly this reason).
757
+ */
758
+ declare function applyPoiDesign(map: PoiPaintMap, design: PoiDesign, fallbackTextColor?: string): void;
759
+
334
760
  /**
335
761
  * MapMapMap - the branded MapLibre GL map.
336
762
  *
@@ -378,6 +804,15 @@ interface MapMapOptions {
378
804
  */
379
805
  mapOptions?: Partial<Omit<MapOptions, "container" | "style">>;
380
806
  }
807
+ /** Options for {@link MapMapMap.setRouteEffect}. */
808
+ interface SetRouteEffectOptions extends RouteFlowOptions {
809
+ /**
810
+ * The route line to attach the effect to. Optional: when omitted the
811
+ * effect is ARMED and applies as soon as a `RouteLayer` draws a route
812
+ * (RouteLayer feeds every drawn geometry to the map automatically).
813
+ */
814
+ geometry?: RouteGeometry;
815
+ }
381
816
  /** The MapMap map: a branded, tiles-and-routing-ready MapLibre map. */
382
817
  declare class MapMapMap {
383
818
  /** The underlying MapLibre map. Use it for any native MapLibre call. */
@@ -393,7 +828,62 @@ declare class MapMapMap {
393
828
  * the theme had no nav block (or `style` was not a theme document).
394
829
  */
395
830
  readonly navDesign: NavDesign | undefined;
831
+ /**
832
+ * The Studio POI category design (`extra.poi`) parsed from the theme
833
+ * passed as `style`, when it carried one. Apply its label-text part with
834
+ * `applyPoiDesign(map.map, map.poiDesign)`; `undefined` means the theme
835
+ * had no poi block (or `style` was not a theme document).
836
+ */
837
+ readonly poiDesign: PoiDesign | undefined;
838
+ private effectName;
839
+ private effectOptions;
840
+ private effectGeometry;
841
+ private effectExplicit;
842
+ private effectLayer;
396
843
  constructor(options: MapMapOptions);
844
+ private readonly handleStyleLoadForEffects;
845
+ /** The current style's `metadata`, if it can be read yet. */
846
+ private styleMetadata;
847
+ /**
848
+ * Set (or clear) the visual effect on the active route line - the
849
+ * effects-engine entry point (see `effects.ts`). The first effect is
850
+ * `"flow"`: an animated energy ribbon flowing along the route, drawn in
851
+ * a MapLibre custom layer with first-party GLSL.
852
+ *
853
+ * ```ts
854
+ * const route = await routes.route(from, to);
855
+ * map.setRouteEffect("flow"); // uses the drawn route
856
+ * map.setRouteEffect("flow", { color: "#ff7a1f" }); // options
857
+ * map.setRouteEffect(null); // back to the plain line
858
+ * ```
859
+ *
860
+ * Geometry: pass `{ geometry }` explicitly, or omit it and the effect
861
+ * attaches to whatever route a `RouteLayer` on this map draws (current
862
+ * and future - RouteLayer reports every drawn line via
863
+ * {@link setRouteEffectGeometry}).
864
+ *
865
+ * Styles whose theme carried an `effects` block auto-enable their effect
866
+ * from the compiled style's metadata; calling this method (with any
867
+ * value, including `null`) overrides the style's wish for the lifetime
868
+ * of this map.
869
+ *
870
+ * Accessibility/resilience (see effects.ts): honours
871
+ * `prefers-reduced-motion` (static gradient, no animation) and falls
872
+ * back to the plain route line with a single console warning if WebGL
873
+ * setup fails.
874
+ */
875
+ setRouteEffect(effect: RouteEffectName | null, options?: SetRouteEffectOptions): void;
876
+ /**
877
+ * Attach the active route effect to a route line (or detach it with
878
+ * `null`). `RouteLayer` calls this on every `draw()`/`clear()`, so apps
879
+ * normally never do - pass `geometry` to {@link setRouteEffect} for
880
+ * routes drawn outside a RouteLayer.
881
+ */
882
+ setRouteEffectGeometry(geometry: RouteGeometry | null): void;
883
+ private effectLayerId;
884
+ private removeEffectLayer;
885
+ /** Reconcile the effect state with the map: install, update or remove. */
886
+ private applyRouteEffect;
397
887
  /**
398
888
  * Toggle 3D building extrusions at runtime — the live equivalent of
399
889
  * compiling the style with `buildings_3d: true` (see `Theme`): adds or
@@ -416,125 +906,6 @@ declare class MapMapMap {
416
906
  /** Functional alias for {@link MapMapMap}, mirroring the Mapbox `new Map` feel. */
417
907
  declare function createMap(options: MapMapOptions): MapMapMap;
418
908
 
419
- /**
420
- * Shared public types for @mapmap/maps.
421
- *
422
- * These are runtime-free (type-only) so the pure logic modules that import
423
- * them stay loadable in Node without a browser or maplibre-gl.
424
- */
425
- /** A longitude/latitude pair, accepted in the three common shapes. */
426
- type LngLatLike = [number, number] | {
427
- lng: number;
428
- lat: number;
429
- } | {
430
- lon: number;
431
- lat: number;
432
- };
433
- /** OSRM routing profile. MapMap ships `driving`, `walking` and `truck`. */
434
- type RouteProfile = "driving" | "walking" | "truck" | (string & {});
435
- /**
436
- * Truck / ADR vehicle parameters forwarded to the gateway's OSRM truck
437
- * vendor extensions. All optional; only `truck`-profile requests honour them.
438
- */
439
- interface TruckParams {
440
- /** Vehicle height, metres. */
441
- heightM?: number;
442
- /** Vehicle width, metres. */
443
- widthM?: number;
444
- /** Vehicle length, metres. */
445
- lengthM?: number;
446
- /** Gross weight, tonnes. */
447
- weightT?: number;
448
- /** Carrying dangerous goods (ADR). */
449
- hazmat?: boolean;
450
- /**
451
- * ADR 8.6.4 tunnel restriction code, e.g. `"C"` or `"B/D"`. The slash is
452
- * URL-encoded automatically.
453
- */
454
- tunnelCode?: string;
455
- }
456
- /** Options for a single route request. */
457
- interface RouteOptions {
458
- /** Routing profile. Defaults to `"driving"`. */
459
- profile?: RouteProfile;
460
- /** Truck / ADR parameters (only meaningful with `profile: "truck"`). */
461
- truck?: TruckParams;
462
- /** Request spoken `voiceInstructions` on each step (see `guidance.ts`). */
463
- voice?: boolean;
464
- /** Request visual `bannerInstructions` (and lane data) on each step. */
465
- banner?: boolean;
466
- /** BCP 47 narration language, e.g. `"en-GB"`. */
467
- language?: string;
468
- }
469
- /** A parsed GeoJSON LineString geometry (`[lng, lat]` positions). */
470
- interface RouteGeometry {
471
- type: "LineString";
472
- coordinates: [number, number][];
473
- }
474
- /** A parsed OSRM route, normalised to the fields callers actually use. */
475
- interface ParsedRoute {
476
- /** Total distance, metres. */
477
- distanceM: number;
478
- /** Total duration, seconds. */
479
- durationS: number;
480
- /** Route line as GeoJSON, ready to hand to a MapLibre source. */
481
- geometry: RouteGeometry;
482
- /** The raw OSRM `routes[0]` object, for callers that need leg/step detail. */
483
- raw: Record<string, unknown>;
484
- }
485
- /** The ADR category of a tunnel, `"A"` (least restrictive) … `"E"`. */
486
- type AdrTunnelCategory = "A" | "B" | "C" | "D" | "E";
487
- /**
488
- * Physical truck dimensions for the ADR vehicle profile (mirrors
489
- * `sn_adr::TruckDimensions`). All optional; unset fields default to the EU
490
- * 96/53/EC maximum-authorised articulated vehicle (4.0 m high, 2.55 m wide,
491
- * 16.5 m long, 40 t gross), matching the Rust `Default`.
492
- */
493
- interface AdrDimensions {
494
- /** Vehicle height, metres. Defaults to `4.0`. */
495
- heightM?: number;
496
- /** Vehicle width, metres. Defaults to `2.55`. */
497
- widthM?: number;
498
- /** Vehicle length, metres. Defaults to `16.5`. */
499
- lengthM?: number;
500
- /** Gross combination weight, tonnes. Defaults to `40`. */
501
- grossWeightT?: number;
502
- /** Heaviest single-axle load, tonnes, if known. */
503
- axleLoadT?: number;
504
- /** Number of axles, if known. */
505
- axleCount?: number;
506
- }
507
- /**
508
- * Input for the gateway's `POST /adr/check` compliance endpoint: the
509
- * vehicle's ADR profile plus the category of the tunnel to check. Serialised
510
- * on the wire as `{"adr": <AdrVehicleProfile>, "tunnel_category": "A".."E"}`
511
- * (see `sn-gateway` `routes/adr.rs`).
512
- */
513
- interface AdrCheckRequest {
514
- /** Carrying dangerous goods. */
515
- hazmat: boolean;
516
- /**
517
- * The *vehicle load's* ADR 8.6.4 tunnel restriction code, e.g. `"C"` or
518
- * `"C/E"`. Omit when carrying dangerous goods of unknown code (the
519
- * gateway then conservatively treats the load as code `B`). This is not
520
- * the tunnel's category — that goes in {@link tunnelCategory}.
521
- */
522
- tunnelCode?: string;
523
- /** Vehicle dimensions. Defaults to the EU standard artic maximums. */
524
- dimensions?: AdrDimensions;
525
- /** The ADR category of the *tunnel* to check, `"A"` … `"E"`. */
526
- tunnelCategory: AdrTunnelCategory;
527
- }
528
- /** Parsed response from `POST /adr/check`. */
529
- interface AdrCheckResult {
530
- /** `"allowed"` or `"blocked"`. */
531
- status: string;
532
- /** Human-readable justification citing ADR 8.6.4 where blocked. */
533
- reason?: string;
534
- /** The raw response body. */
535
- raw: Record<string, unknown>;
536
- }
537
-
538
909
  /**
539
910
  * RouteLayer - fetch an OSRM route from the MapMap gateway and draw it.
540
911
  *
@@ -562,6 +933,9 @@ interface RouteLayerOptions {
562
933
  /** Draws MapMap routes on a MapLibre map. */
563
934
  declare class RouteLayer {
564
935
  private readonly map;
936
+ /** The owning MapMapMap, when built from one - fed each drawn route so
937
+ * an armed route effect (`setRouteEffect`) attaches automatically. */
938
+ private readonly owner;
565
939
  private readonly baseUrl;
566
940
  private readonly apiKey;
567
941
  private readonly sourceId;
@@ -639,9 +1013,14 @@ interface Place {
639
1013
  /** Longitude, degrees. */
640
1014
  lon: number;
641
1015
  /**
642
- * Arbitrary extra data (opening hours, phone, …). Copied onto the GeoJSON
643
- * feature properties, so it is available to data-driven styling and comes
644
- * back on the place handed to `onPlaceClick` / `popup`.
1016
+ * Arbitrary extra data (opening hours, phone, …). Every key is copied
1017
+ * verbatim onto the top level of the GeoJSON feature's `properties`,
1018
+ * alongside the reserved `id`, `name` and `__mapmapIndex` keys (which win
1019
+ * on collision) - so scalar values are directly addressable from a
1020
+ * data-driven `color` expression, e.g. `["get", "category"]`. MapLibre
1021
+ * JSON-stringifies nested objects/arrays at render time, so keep anything
1022
+ * you want to style on as a top-level string/number/boolean. The whole
1023
+ * object also comes back on the place handed to `onPlaceClick` / `popup`.
645
1024
  */
646
1025
  properties?: Record<string, unknown>;
647
1026
  }
@@ -710,8 +1089,22 @@ interface PlacesLayerOptions {
710
1089
  clusterRadius?: number;
711
1090
  /** Max zoom to cluster at. Defaults to MapLibre's `14`. */
712
1091
  clusterMaxZoom?: number;
713
- /** Pin and cluster colour. Defaults to MapMap signal blue (`#3a86ff`). */
714
- color?: string;
1092
+ /**
1093
+ * Pin colour: a CSS colour string, or a MapLibre expression evaluated
1094
+ * against each feature's `properties` for per-category pins, e.g.
1095
+ * `["match", ["get", "category"], "food", "#e63946", "#3a86ff"]` (see
1096
+ * {@link Place.properties} for what an expression can `get`). Defaults to
1097
+ * MapMap signal blue (`#3a86ff`). Only styles the default circle pins -
1098
+ * ignored with a custom `icon`. A cluster mixes categories, so an
1099
+ * expression never applies to clusters: they use `clusterColor`.
1100
+ */
1101
+ color?: string | ExpressionSpecification;
1102
+ /**
1103
+ * Cluster circle colour. Defaults to `color` when that is a plain string,
1104
+ * else to MapMap signal blue (a cluster mixes categories, so a
1105
+ * data-driven `color` expression cannot apply to it).
1106
+ */
1107
+ clusterColor?: string;
715
1108
  /**
716
1109
  * Custom pin image for unclustered places (a symbol layer instead of the
717
1110
  * default circle). If the image fails to load, the circle look is used.
@@ -740,6 +1133,26 @@ interface PlacesLayerOptions {
740
1133
  */
741
1134
  popup?: (place: Place) => string | HTMLElement;
742
1135
  }
1136
+ /** The generated MapLibre source/layer ids (see {@link PlacesLayer.ids}). */
1137
+ interface PlacesLayerIds {
1138
+ /** The GeoJSON source id. */
1139
+ source: string;
1140
+ /** The unclustered-points layer id (circle, or symbol with `icon`). */
1141
+ points: string;
1142
+ /** The cluster circles layer id (only installed with `cluster: true`). */
1143
+ clusters: string;
1144
+ /** The cluster count badge layer id (only installed with `cluster: true`). */
1145
+ clusterCounts: string;
1146
+ }
1147
+ /** Options for {@link PlacesLayer.select}. */
1148
+ interface PlacesSelectOptions {
1149
+ /** Open the layer's configured `popup` at the place. Defaults to `true`. */
1150
+ popup?: boolean;
1151
+ /** Ease the camera to the place. Defaults to `true`. */
1152
+ flyTo?: boolean;
1153
+ /** Zoom for the camera move (with `flyTo`). Keeps the current zoom if omitted. */
1154
+ zoom?: number;
1155
+ }
743
1156
  /**
744
1157
  * Normalise a GeoJSON FeatureCollection of Points to `Place[]`. The id comes
745
1158
  * from `feature.id`, then `properties.id`, then the feature index; the name
@@ -768,6 +1181,7 @@ declare class PlacesLayer {
768
1181
  private readonly clusterRadius;
769
1182
  private readonly clusterMaxZoom;
770
1183
  private readonly color;
1184
+ private readonly clusterColor;
771
1185
  private readonly icon;
772
1186
  private readonly wantFitBounds;
773
1187
  private readonly onPlaceClick;
@@ -785,13 +1199,37 @@ declare class PlacesLayer {
785
1199
  private readonly handleClusterClick;
786
1200
  /**
787
1201
  * Replace the layer's places - a `Place[]` or a GeoJSON FeatureCollection
788
- * of Points. Updates the existing GeoJSON source in place; safe to call
789
- * before the style has loaded (installed on the next `style.load`). With
790
- * `fitBounds: true` the first non-empty set also fits the map view.
1202
+ * of Points. Never silently drops an update: once the source exists the
1203
+ * data is applied immediately, even while `isStyleLoaded()` is transiently
1204
+ * `false` mid-render (search-as-you-type just works); calls made before
1205
+ * the source has first been installed are stashed - the latest one wins -
1206
+ * and installed on the next `style.load`. With `fitBounds: true` the
1207
+ * first non-empty set also fits the map view.
791
1208
  */
792
1209
  setPlaces(places: PlacesInput): void;
793
1210
  /** The layer's current places (normalised to `Place[]`). */
794
1211
  get current(): Place[];
1212
+ /**
1213
+ * The generated MapLibre source/layer ids - public API for escape-hatch
1214
+ * styling (`map.setPaintProperty`, `queryRenderedFeatures`, …) beyond the
1215
+ * layer's options. Stable for the layer's lifetime, derived from the `id`
1216
+ * option (default `"mapmap-places"`). The cluster ids are only installed
1217
+ * on the map with `cluster: true` (the default), and `points` is a circle
1218
+ * layer by default or a symbol layer once a custom `icon` has loaded.
1219
+ */
1220
+ get ids(): PlacesLayerIds;
1221
+ /**
1222
+ * Programmatically select a place by id - list-to-map sync for a store
1223
+ * finder's results list. Opens the layer's configured `popup` at the
1224
+ * place (`popup: false` to skip, no-op without a `popup` option) and
1225
+ * eases the camera to it (`flyTo: false` to skip; `zoom` to also zoom).
1226
+ * Returns the selected place, or `undefined` for an unknown id (in which
1227
+ * case nothing happens). Does NOT invoke `onPlaceClick` - a programmatic
1228
+ * selection is not a user click.
1229
+ */
1230
+ select(id: string, options?: PlacesSelectOptions): Place | undefined;
1231
+ /** Close the popup opened by {@link select} or a place click, if any. */
1232
+ deselect(): void;
795
1233
  /**
796
1234
  * The nearest `n` places (default `1`) to `origin` by straight-line
797
1235
  * (haversine) distance, each with `distanceM` attached. Pure and instant -
@@ -969,6 +1407,255 @@ declare class NavigationCamera {
969
1407
  private scheduleRecentre;
970
1408
  private clearRecentreTimer;
971
1409
  }
1410
+ /**
1411
+ * Initial great-circle bearing from `a` to `b`, degrees clockwise from
1412
+ * north, normalised to `[0, 360)`.
1413
+ */
1414
+ declare function bearingBetween(a: [number, number], b: [number, number]): number;
1415
+ /**
1416
+ * The signed shortest-arc rotation from bearing `from` to bearing `to`,
1417
+ * in `(-180, 180]` degrees - the slerp-style delta the flythrough eases
1418
+ * along so the chase cam never spins the long way round.
1419
+ */
1420
+ declare function shortestArcDelta(from: number, to: number): number;
1421
+ /** A camera pose along a flythrough: where to look from, and which way. */
1422
+ interface FlythroughPose {
1423
+ /** Camera target on the route, `[lng, lat]`. */
1424
+ center: [number, number];
1425
+ /** Chase-cam bearing: towards a point `lookAheadM` up the road. */
1426
+ bearing: number;
1427
+ }
1428
+ /**
1429
+ * The camera pose at progress `t` (0-1) along a route: the interpolated
1430
+ * point that fraction of the total distance along the line, with a
1431
+ * chase-cam bearing towards the point `lookAheadM` further on. Pure -
1432
+ * exported for tests and for apps that drive the camera themselves.
1433
+ */
1434
+ declare function flythroughPose(coordinates: [number, number][], t: number, lookAheadM?: number): FlythroughPose;
1435
+ interface FlythroughOptions {
1436
+ /**
1437
+ * Camera tilt in degrees, clamped 0-85. Default `60` (the cinematic
1438
+ * chase look; see {@link NavigationCameraOptions.pitch} for caveats
1439
+ * above 60).
1440
+ */
1441
+ pitch?: number;
1442
+ /** Camera zoom held through the replay. Default `16`. */
1443
+ zoom?: number;
1444
+ /**
1445
+ * Total replay duration in milliseconds. Default `20000`. Ignored when
1446
+ * `speedMps` is given.
1447
+ */
1448
+ durationMs?: number;
1449
+ /**
1450
+ * Replay ground speed in metres/second along the route - the duration
1451
+ * becomes `routeLength / speedMps`. Wins over `durationMs`.
1452
+ */
1453
+ speedMps?: number;
1454
+ /**
1455
+ * How far up the road the chase cam looks to derive its bearing,
1456
+ * metres. Default `200`; larger = calmer bearing on wiggly roads.
1457
+ */
1458
+ lookAheadM?: number;
1459
+ /**
1460
+ * Bearing smoothing rate per second (the fraction of the remaining
1461
+ * shortest-arc turn closed each second). Default `3`; higher = snappier.
1462
+ */
1463
+ bearingEase?: number;
1464
+ }
1465
+ /** The handle a {@link flythrough} returns. */
1466
+ interface FlythroughController {
1467
+ /** Start (or resume) the replay. Restarts from 0 when it had finished. */
1468
+ play(): void;
1469
+ /** Freeze the replay where it is. */
1470
+ pause(): void;
1471
+ /** Pause and rewind to the start (the camera jumps to the start pose). */
1472
+ stop(): void;
1473
+ /** Scrub to progress `t` (0-1, clamped) and apply the pose immediately. */
1474
+ seek(t: number): void;
1475
+ /** Playback-rate multiplier (`1` = real duration, `2` = double). */
1476
+ speed: number;
1477
+ /** Current progress, 0-1. */
1478
+ readonly progress: number;
1479
+ /** Whether the replay is currently advancing. */
1480
+ readonly playing: boolean;
1481
+ /** Subscribe to progress updates; returns the unsubscribe function. */
1482
+ onProgress(listener: (t: number) => void): () => void;
1483
+ /** Pause and detach everything. The controller is dead afterwards. */
1484
+ destroy(): void;
1485
+ }
1486
+ /**
1487
+ * Cinematic route replay: fly the camera along a route line with a
1488
+ * chase-cam bearing (eased along the shortest arc, so junctions glide
1489
+ * instead of spinning), at a configurable pitch/zoom/speed. Returns a
1490
+ * transport-style controller; nothing moves until `play()` or `seek()`.
1491
+ *
1492
+ * ```ts
1493
+ * const route = await routes.route(from, to);
1494
+ * const replay = flythrough(map, route, { pitch: 60, durationMs: 15000 });
1495
+ * replay.onProgress((t) => scrubber.value = String(t));
1496
+ * replay.play();
1497
+ * ```
1498
+ *
1499
+ * Drives the camera with `jumpTo` once per animation frame (the standard
1500
+ * MapLibre scrub pattern). Runs under any projection; on globe the ride is
1501
+ * unanchored but valid. Accepts a `ParsedRoute` (from `RouteLayer`) or a
1502
+ * bare GeoJSON LineString geometry.
1503
+ */
1504
+ declare function flythrough(map: MapMapMap | Map, route: RouteGeometry | ParsedRoute, options?: FlythroughOptions): FlythroughController;
1505
+ /**
1506
+ * Scrollytelling: bind a flythrough to a scroll position, so scrolling a
1507
+ * story column scrubs the camera along the route. The binder pauses the
1508
+ * controller (scroll owns the timeline) and seeks it to the scrolled
1509
+ * fraction - immediately on bind, then on every scroll. Returns the
1510
+ * unbind function.
1511
+ *
1512
+ * ```ts
1513
+ * const replay = flythrough(map, route);
1514
+ * const unbind = bindFlythroughToScroll(replay, document.querySelector("#story")!);
1515
+ * ```
1516
+ */
1517
+ declare function bindFlythroughToScroll(controller: FlythroughController, target?: HTMLElement | Window): () => void;
1518
+
1519
+ /**
1520
+ * IsochroneLayer - walkability rings (reachability contours) on a MapMap
1521
+ * map, backed by the gateway's live `POST /isochrone` endpoint (Valhalla
1522
+ * isochrone; see docs/API.md).
1523
+ *
1524
+ * One call - `showReachability({ origin, mode: "walk", minutes: [5, 10, 15] })` -
1525
+ * renders concentric reachability rings: graduated-opacity fills (nearest
1526
+ * ring strongest), contour outlines, and "N min" labels along each
1527
+ * contour. The estate-agent / store-catchment / "how walkable is this
1528
+ * flat?" view in one line.
1529
+ *
1530
+ * Follows the RouteLayer pattern: gateway base URL/key picked up from a
1531
+ * `MapMapMap` automatically, layers survive `setStyle` theme swaps via
1532
+ * re-installation on `style.load`, and `clear()`/`destroy()` tidy up.
1533
+ */
1534
+
1535
+ /**
1536
+ * Travel mode for the rings. The friendly names map onto gateway costings
1537
+ * (`walk` → `pedestrian`, `cycle` → `bicycle`, `drive` → `auto`); any
1538
+ * other string is passed through as a raw Valhalla costing (e.g.
1539
+ * `"truck"`, `"motor_scooter"`).
1540
+ */
1541
+ type ReachabilityMode = "walk" | "cycle" | "drive" | "truck" | (string & {});
1542
+ interface ShowReachabilityOptions {
1543
+ /** Where the rings radiate from. */
1544
+ origin: LngLatLike;
1545
+ /** Travel mode. Defaults to `"walk"` - these are walkability rings. */
1546
+ mode?: ReachabilityMode;
1547
+ /** Ring contours in minutes, e.g. `[5, 10, 15]`. At least one. */
1548
+ minutes: number[];
1549
+ /** Ring colour. Defaults to MapMap signal blue. */
1550
+ color?: string;
1551
+ /**
1552
+ * Valhalla-style costing options forwarded verbatim, e.g.
1553
+ * `{ pedestrian: { use_lit: 1.0 } }`.
1554
+ */
1555
+ costingOptions?: Record<string, unknown>;
1556
+ }
1557
+ interface IsochroneLayerOptions {
1558
+ /** Gateway base URL. Defaults to the map's `baseUrl` when given a map. */
1559
+ baseUrl?: string;
1560
+ /** Gateway API key. Defaults to the map's `apiKey` when given a map. */
1561
+ apiKey?: string;
1562
+ /** Unique id prefix for the source/layers. Defaults to `"mapmap-isochrone"`. */
1563
+ id?: string;
1564
+ }
1565
+ /** The GeoJSON FeatureCollection the gateway returns, passed through. */
1566
+ interface IsochroneFeatureCollection {
1567
+ type: "FeatureCollection";
1568
+ features: {
1569
+ type: "Feature";
1570
+ geometry: {
1571
+ type: string;
1572
+ coordinates: unknown;
1573
+ };
1574
+ properties?: Record<string, unknown> | null;
1575
+ }[];
1576
+ }
1577
+ /** Renders reachability rings from the gateway's `POST /isochrone`. */
1578
+ declare class IsochroneLayer {
1579
+ private readonly map;
1580
+ private readonly baseUrl;
1581
+ private readonly apiKey;
1582
+ private readonly sourceId;
1583
+ private readonly fillLayerId;
1584
+ private readonly lineLayerId;
1585
+ private readonly labelLayerId;
1586
+ private lastData;
1587
+ private lastMinutes;
1588
+ private lastColor;
1589
+ constructor(map: MapMapMap | Map, options?: IsochroneLayerOptions);
1590
+ private readonly handleStyleLoad;
1591
+ /**
1592
+ * Fetch and render reachability rings. Returns the GeoJSON
1593
+ * FeatureCollection the gateway produced (each feature carries a
1594
+ * `contour` property in minutes), for callers who also want the raw
1595
+ * shapes. Replaces any rings already shown by this layer.
1596
+ */
1597
+ showReachability(options: ShowReachabilityOptions): Promise<IsochroneFeatureCollection>;
1598
+ /** The most recently drawn rings, if any. */
1599
+ get current(): IsochroneFeatureCollection | undefined;
1600
+ /** Draw (or update) a ring collection. Safe before the style has loaded. */
1601
+ private draw;
1602
+ /** Add-or-update the source and ring layers on the current style. */
1603
+ private install;
1604
+ /**
1605
+ * Graduated fill opacity: the nearest contour (fewest minutes) is the
1606
+ * most opaque, the farthest the faintest, interpolated on each
1607
+ * feature's `contour` property. With one contour it is a constant.
1608
+ */
1609
+ private fillOpacity;
1610
+ /** Remove the rings' layers and source from the map. */
1611
+ clear(): void;
1612
+ /** Remove the rings and detach the layer's `style.load` listener. */
1613
+ destroy(): void;
1614
+ }
1615
+
1616
+ /**
1617
+ * Self-diagnosing map errors - the SDK tells you what went wrong and how
1618
+ * to fix it, instead of a silently blank map.
1619
+ *
1620
+ * `MapMapMap` runs these checks at construction (and listens for tile
1621
+ * auth failures); each detected issue produces ONE `console.error` per
1622
+ * page with an actionable message and a docs link. Everything here is
1623
+ * best-effort and defensive: no check may ever throw or require a DOM
1624
+ * (server-side rendering and Node tests just skip the DOM checks).
1625
+ *
1626
+ * Issues covered - the documented gotchas that burn real integrations:
1627
+ * - a container whose height resolves to 0px (MapLibre renders into a
1628
+ * 0-tall canvas: no error, no map)
1629
+ * - a container element that is not attached to the DOM
1630
+ * - two maplibre-gl copies on one page (duplicate dependency /
1631
+ * micro-frontends - breaks the shared WebGL context and styles)
1632
+ * - WebGL unavailable (headless browsers, blocked GPU, old devices)
1633
+ * - missing/invalid API key (a 401 from the tile or routing endpoints)
1634
+ */
1635
+ /** The distinct issues the diagnostics can report. */
1636
+ type DiagnosticIssue = "container-zero-height" | "container-detached" | "duplicate-maplibre" | "webgl-unavailable" | "invalid-api-key";
1637
+ /** Reset the once-per-page dedup - for tests (and hot-reload tooling). */
1638
+ declare function resetDiagnostics(): void;
1639
+ /** What `runMapDiagnostics` needs; everything optional and structural. */
1640
+ interface MapDiagnosticsInput {
1641
+ /** The `container` option as given: an element or an element id. */
1642
+ container?: string | HTMLElement;
1643
+ /**
1644
+ * The constructed map (or any object with a MapLibre-style `on`). Used
1645
+ * to watch for tile-request auth failures.
1646
+ */
1647
+ map?: unknown;
1648
+ /** The maplibre-gl module the SDK is using (duplicate detection). */
1649
+ maplibre?: unknown;
1650
+ /** The gateway API key, if one was configured. */
1651
+ apiKey?: string | undefined;
1652
+ }
1653
+ /**
1654
+ * Run the construction-time checks and attach the 401 watcher. Called by
1655
+ * `MapMapMap` automatically; exported for apps wrapping a raw
1656
+ * `maplibregl.Map` that still want the self-diagnosis.
1657
+ */
1658
+ declare function runMapDiagnostics(input: MapDiagnosticsInput): void;
972
1659
 
973
1660
  /**
974
1661
  * AdrCheck - optional helper for the gateway's `POST /adr/check` endpoint.
@@ -1177,4 +1864,121 @@ declare class GuidanceBanner {
1177
1864
  update(banner: BannerInstruction | null): void;
1178
1865
  }
1179
1866
 
1180
- export { AdrCheck, type AdrCheckOptions, type AdrCheckRequest, type AdrCheckResult, type AdrDimensions, type AdrTunnelCategory, type BannerComponent, type BannerContent, type BannerInstruction, type BuildStyleOptions, type CameraFix, DEFAULT_GLYPHS_URL, DEFAULT_TERRITORY_TILES_URL, FULL_ATTRIBUTION, GuidanceBanner, LOGO_SVG, type LaneIndication, type LayerOverride, type LngLatLike, LogoControl, type LogoOptions, type LogoPosition, MAX_PUCK_IMAGE_BYTES, MapMapMap, type MapMapOptions, type MapMapTheme, NAV_CAMERA_DEFAULTS, type NavBannerDesign, type NavCameraDesign, type NavDesign, type NavPuckDesign, type NavRouteDesign, NavigationCamera, type NavigationCameraMode, type NavigationCameraOptions, type NearestByDriveTimeOptions, OPENMAPTILES_ATTRIBUTION, OSM_ATTRIBUTION, PALETTE_SLOTS, type ParsedRoute, type Place, type PlacePointFeature, type PlaceWithDistance, type PlaceWithDriveTime, type PlacesFeatureCollection, type PlacesIcon, type PlacesInput, PlacesLayer, type PlacesLayerOptions, PositionPuck, type RouteGeometry, RouteLayer, type RouteLayerOptions, type RouteOptions, type RouteProfile, SIGNAL_BLUE, SOURCE_LAYERS, type SpeakOptions, type StepGuidance, type Theme, type TruckParams, type VoiceInstruction, bannerLanes, buildAdrCheckBody, buildRouteQuery, buildRouteUrl, buildStyle, createMap, defaultNavDesign, directionArrow, extractGuidance, formatCoord, formatCoords, haversineDistanceM, navDesignFromTheme, navDesignFromThemeUrl, parseNavDesign, parseOsrmRoute, placesFromGeoJSON, registerPmtilesProtocol, speak, ssmlToText, toLngLat, toPmtilesUrl };
1867
+ /**
1868
+ * Spoken turn-by-turn guidance over the browser's `SpeechSynthesis`.
1869
+ *
1870
+ * {@link VoiceGuidance} consumes the guidance updates emitted by the
1871
+ * `@mapmap/core` wasm `GuidanceSession` (`{ state: "navigating" | "arrived"
1872
+ * | "offRoute", spoken?, severity?, … }`). The core already schedules each
1873
+ * spoken prompt by its trigger distance and repeats it on every update
1874
+ * until it is superseded, so the speaking rule is simple: announce each
1875
+ * `utteranceId` exactly once, the first time it appears.
1876
+ *
1877
+ * Severity (derived in the Rust core — see `InstructionSeverity`) maps to
1878
+ * prosody and earcons:
1879
+ *
1880
+ * - `info` — normal manoeuvres: base rate/pitch, no earcon by default;
1881
+ * - `warning` — approaching a restriction (toll, ferry): slight pitch
1882
+ * lift, warning earcon when configured;
1883
+ * - `critical` — prohibitions (ADR tunnel block, gate): pitch and rate
1884
+ * lift, pending speech cancelled so the prompt is never queued behind
1885
+ * chatter, critical earcon when configured.
1886
+ *
1887
+ * Off-route updates carry no instruction; they play the warning earcon
1888
+ * once per off-route episode (the app recalculates and speech resumes on
1889
+ * the new session).
1890
+ *
1891
+ * Earcons are lazy URLs (see `map-assets/earcons/`, generated by
1892
+ * `scripts/generate-earcons.py`) — nothing is bundled with the SDK.
1893
+ * Everything no-ops safely where `speechSynthesis` is unavailable
1894
+ * (non-browser contexts, some webviews).
1895
+ */
1896
+ /** Instruction urgency, as emitted by the core (`InstructionSeverity`). */
1897
+ type GuidanceSeverity = "info" | "warning" | "critical";
1898
+ /** The spoken prompt of a core guidance update. */
1899
+ interface GuidanceSpokenPrompt {
1900
+ /** Plain text for speech synthesis. */
1901
+ text: string;
1902
+ /** SSML form; browsers reject raw SSML, so it is stripped to text. */
1903
+ ssml?: string | null;
1904
+ /** Metres before the manoeuvre at which the core released the prompt. */
1905
+ triggerDistanceM: number;
1906
+ /** Unique utterance id — the de-duplication key. */
1907
+ utteranceId: string;
1908
+ }
1909
+ /** The subset of a core guidance update that {@link VoiceGuidance} reads. */
1910
+ interface VoiceGuidanceUpdate {
1911
+ /** Guidance state, as emitted by the wasm `GuidanceSession`. */
1912
+ state: "navigating" | "arrived" | "offRoute";
1913
+ /** Spoken prompt currently due (repeated until superseded). */
1914
+ spoken?: GuidanceSpokenPrompt | null;
1915
+ /** Urgency of the current step's instruction (default `info`). */
1916
+ severity?: GuidanceSeverity;
1917
+ /** BCP 47 locale of the instruction text, when the payload carries one. */
1918
+ voiceLocale?: string;
1919
+ }
1920
+ /** Options for {@link VoiceGuidance}. */
1921
+ interface VoiceGuidanceOptions {
1922
+ /** BCP 47 language for utterances, e.g. `en-GB`. An update's
1923
+ * `voiceLocale` takes precedence when present. */
1924
+ lang?: string;
1925
+ /** Base speech rate (0.1–10, default 1). Critical prompts speak
1926
+ * slightly faster than this base. */
1927
+ rate?: number;
1928
+ /** Volume for speech and earcons, 0–1 (default 1). */
1929
+ volume?: number;
1930
+ /** Start muted. */
1931
+ muted?: boolean;
1932
+ /** Severity → earcon audio URL. Absent entries play nothing. */
1933
+ earcons?: Partial<Record<GuidanceSeverity, string>>;
1934
+ }
1935
+ /** Speech parameters for one severity level. */
1936
+ interface SeverityProsody {
1937
+ /** Utterance rate. */
1938
+ rate: number;
1939
+ /** Utterance pitch (0–2, 1 = neutral). */
1940
+ pitch: number;
1941
+ /** Whether pending speech is cancelled before this prompt. */
1942
+ interrupt: boolean;
1943
+ }
1944
+ /**
1945
+ * Prosody for a severity level at a base rate. Pure — exported for tests
1946
+ * and for custom voice layers that want the same house style.
1947
+ */
1948
+ declare function severityProsody(severity: GuidanceSeverity, baseRate?: number): SeverityProsody;
1949
+ /**
1950
+ * Speaks core guidance updates through the browser's `SpeechSynthesis`,
1951
+ * with severity-aware prosody, optional earcons, and mute/volume
1952
+ * controls. Feed every update from the wasm `GuidanceSession` to
1953
+ * {@link VoiceGuidance.update}.
1954
+ */
1955
+ declare class VoiceGuidance {
1956
+ private readonly options;
1957
+ private volume;
1958
+ private mutedState;
1959
+ private lastUtteranceId;
1960
+ private offRouteAnnounced;
1961
+ constructor(options?: VoiceGuidanceOptions);
1962
+ /** Whether this environment can speak at all. */
1963
+ get available(): boolean;
1964
+ /** Whether announcements are currently muted. */
1965
+ get muted(): boolean;
1966
+ /** Mute announcements (also cancels anything mid-utterance). */
1967
+ mute(): void;
1968
+ /** Unmute announcements. Prompts seen while muted are not replayed. */
1969
+ unmute(): void;
1970
+ /** Set the volume for speech and earcons (clamped to 0–1). */
1971
+ setVolume(volume: number): void;
1972
+ /**
1973
+ * Consume one guidance update. Speaks the update's prompt the first
1974
+ * time its `utteranceId` appears; plays the warning earcon once per
1975
+ * off-route episode. Safe to call in any environment.
1976
+ */
1977
+ update(update: VoiceGuidanceUpdate): void;
1978
+ /** Cancel any speech in progress and forget the de-duplication state. */
1979
+ dispose(): void;
1980
+ private speak;
1981
+ private playEarcon;
1982
+ }
1983
+
1984
+ export { AdrCheck, type AdrCheckOptions, type AdrCheckRequest, type AdrCheckResult, type AdrDimensions, type AdrTunnelCategory, type BannerComponent, type BannerContent, type BannerInstruction, type BuildStyleOptions, type CameraFix, DEFAULT_GLYPHS_URL, DEFAULT_TERRITORY_TILES_URL, type DiagnosticIssue, EFFECTS_METADATA_KEY, FLOW_DEFAULTS, FULL_ATTRIBUTION, FlowRouteEffectLayer, type FlythroughController, type FlythroughOptions, type FlythroughPose, GuidanceBanner, type GuidanceSeverity, type GuidanceSpokenPrompt, type IsochroneFeatureCollection, IsochroneLayer, type IsochroneLayerOptions, LOGO_SVG, type LaneIndication, type LayerOverride, type LngLatLike, LogoControl, type LogoOptions, type LogoPosition, MAX_PUCK_IMAGE_BYTES, type MapDiagnosticsInput, MapMapMap, type MapMapOptions, type MapMapTheme, NAV_CAMERA_DEFAULTS, type NavBannerDesign, type NavCameraDesign, type NavDesign, type NavPuckDesign, type NavRouteDesign, NavigationCamera, type NavigationCameraMode, type NavigationCameraOptions, type NearestByDriveTimeOptions, OPENMAPTILES_ATTRIBUTION, OSM_ATTRIBUTION, PALETTE_SLOTS, POI_CATEGORY_COLORS, POI_CATEGORY_IDS, POI_CLASS_CATEGORIES, type ParsedRoute, type Place, type PlacePointFeature, type PlaceWithDistance, type PlaceWithDriveTime, type PlacesFeatureCollection, type PlacesIcon, type PlacesInput, PlacesLayer, type PlacesLayerIds, type PlacesLayerOptions, type PlacesSelectOptions, type PoiCategoryDesign, type PoiDesign, PositionPuck, RIBBON_FLOATS_PER_VERTEX, ROUTE_EFFECTS, type ReachabilityMode, type RibbonMesh, type RouteEffectLayer, type RouteEffectName, type RouteFlowOptions, type RouteGeometry, RouteLayer, type RouteLayerOptions, type RouteOptions, type RouteProfile, SIGNAL_BLUE, SOURCE_LAYERS, type SetRouteEffectOptions, type SeverityProsody, type ShowReachabilityOptions, type SpeakOptions, type StepGuidance, type Theme, type ThemeEffects, type TruckParams, VoiceGuidance, type VoiceGuidanceOptions, type VoiceGuidanceUpdate, type VoiceInstruction, applyPoiDesign, bannerLanes, bearingBetween, bindFlythroughToScroll, buildAdrCheckBody, buildRouteQuery, buildRouteUrl, buildStyle, builtInPoiColor, createMap, createRouteEffect, defaultNavDesign, defaultPoiDesign, directionArrow, effectsFromStyleMetadata, extractGuidance, flythrough, flythroughPose, formatCoord, formatCoords, haversineDistanceM, lngLatToMercator, navDesignFromTheme, navDesignFromThemeUrl, parseCssColour, parseNavDesign, parseOsrmRoute, parsePoiDesign, placesFromGeoJSON, poiDesignFromTheme, poiDesignFromThemeUrl, poiDesignIsDefault, poiTextColorExpression, prefersReducedMotion, registerPmtilesProtocol, resetDiagnostics, runMapDiagnostics, severityProsody, shortestArcDelta, speak, ssmlToText, tessellateRouteRibbon, toLngLat, toPmtilesUrl };