@minmaps-dev/mm-web-sdk 1.0.0-rc.5 → 1.0.0-rc.51

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,4 @@
1
- import { IControl, ControlPosition, Map } from 'maplibre-gl';
1
+ import { Map, StyleSpecification, IControl, ControlPosition } from 'maplibre-gl';
2
2
 
3
3
  /**
4
4
  * Represents a floor in a building
@@ -6,8 +6,18 @@ import { IControl, ControlPosition, Map } from 'maplibre-gl';
6
6
  interface Floor {
7
7
  /** Unique floor identifier */
8
8
  id: string | number;
9
+ /**
10
+ * Underlying map entity id. Separate from `id` (the floor entity id) — used
11
+ * by waypoints (`wp.mapId`) and floor-geojson endpoints. Optional because
12
+ * not all providers populate it.
13
+ */
14
+ mapId?: string | number;
9
15
  /** Display name of the floor */
10
16
  name: string;
17
+ /** Short display name of the floor (e.g. "L2"). Optional: JACS leaves it
18
+ * unset on plenty of floors, and `JacsDataProvider` maps a blank one to
19
+ * `undefined` rather than inventing a value. */
20
+ shortName?: string;
11
21
  /** Floor sequence/order (e.g., -1 for basement, 0 for ground, 1 for first floor) */
12
22
  sequence?: number;
13
23
  /** Associated map data */
@@ -23,6 +33,7 @@ interface Floor {
23
33
  interface FloorMetadata {
24
34
  id: string | number;
25
35
  name: string;
36
+ shortName?: string;
26
37
  sequence?: number;
27
38
  isDefault?: boolean;
28
39
  isActive?: boolean;
@@ -32,7 +43,7 @@ interface POI {
32
43
  /** Unique identifier */
33
44
  id: string | number;
34
45
  /** POI type */
35
- type: 'amenity' | 'destination';
46
+ type: 'amenity' | 'destination' | 'kiosk';
36
47
  /** Display name */
37
48
  name: string;
38
49
  /** Geographic coordinates [longitude, latitude] */
@@ -45,6 +56,17 @@ interface POI {
45
56
  floorId: string | number;
46
57
  /** Category/type of amenity (e.g., 'restroom', 'elevator') */
47
58
  amenityType?: string;
59
+ /** Locale-stable classification text (name + type + keywords, frozen at
60
+ * load before any locale patch can translate them). Copied from the
61
+ * source `Amenity.classifyText`; used by `amenityCategoryFor`/rank
62
+ * instead of the live, possibly-translated fields. See `Amenity.classifyText`. */
63
+ classifyText?: string;
64
+ /** Which amenity icon layer this POI renders in — `'connector'`,
65
+ * `'entrance'`, `'parking'`, `'bus'`, `'information'` or `'other'`.
66
+ * Derived from the amenity's connector flag, name, type and keywords;
67
+ * emitted as the `amenityCategory` feature property the theme filters on.
68
+ * Amenities only. See `amenityCategoryFor`. */
69
+ amenityCategory?: 'connector' | 'entrance' | 'parking' | 'bus' | 'information' | 'other';
48
70
  /** Search keywords */
49
71
  keywords?: string[];
50
72
  /** Additional properties */
@@ -55,6 +77,15 @@ interface POI {
55
77
  waypoint?: Waypoint;
56
78
  /** Marks this POI as the "You are here" kiosk */
57
79
  isYouAreHere?: boolean;
80
+ /** For kiosk POIs only: compass heading the device physically faces, in
81
+ * degrees clockwise from north. Sourced from JACS `Device.heading`. */
82
+ heading?: number | null;
83
+ /** Per-instance name for this specific placement of an amenity (e.g.
84
+ * "Parking Lot A" for a "Parking" amenity), resolved from the waypoint's
85
+ * `amenityAssociations`. Distinct from `name`, which is the amenity's
86
+ * type-level name shared by every instance — see `Amenity.name`. Amenities
87
+ * only, and only set when JACS actually returned an instance name. */
88
+ instanceName?: string;
58
89
  }
59
90
  /**
60
91
  * Amenity - a specific type of POI (facilities, services)
@@ -64,14 +95,42 @@ interface Amenity {
64
95
  id: string | number;
65
96
  /** Display name */
66
97
  name: string;
67
- /** SVG icon data */
98
+ /** Inline SVG markup (legacy fullcall DTO). When absent, icon is resolved from `uris`. */
68
99
  svg?: string;
100
+ /**
101
+ * Hosted icon descriptors from JACS (`uris[].locales[].uriPath`). Used by
102
+ * the SDK's runtime icon fetcher to register a map image at init time.
103
+ */
104
+ uris?: Array<{
105
+ mimeType?: string;
106
+ resourceType?: string;
107
+ locales?: Array<{
108
+ locale?: string;
109
+ uriPath?: string;
110
+ }>;
111
+ }>;
112
+ /**
113
+ * Map-image id used as `icon-image` in the POI style layer. Decorated
114
+ * in-place by AmenityIcons.register once the SVG is fetched and registered.
115
+ */
116
+ iconId?: string;
69
117
  /** Search keywords */
70
118
  keywords?: string[];
71
119
  /** Associated waypoints */
72
120
  waypoints?: Waypoint[];
73
121
  /** Amenity type/category */
74
122
  type?: string;
123
+ /**
124
+ * Locale-stable snapshot of `name + type + keywords`, captured once from
125
+ * the `/all` payload before `applyLocale` ever runs. `amenityCategoryFor`
126
+ * and the icon-rank heuristics in `poi.ts` match hard-coded English
127
+ * vocabulary against those fields; `name`/`keywords` are legitimately
128
+ * overwritten with translated text on a language switch
129
+ * (`LOCALIZED_FIELDS.amenity`), which reclassified every translated
130
+ * amenity to `'other'`/tier-3 and dropped its icon until the next zoom
131
+ * recomputed the rank gate. Classification reads this frozen copy instead.
132
+ */
133
+ classifyText?: string;
75
134
  /** Extended properties */
76
135
  extensors?: Record<string, unknown>;
77
136
  }
@@ -87,8 +146,34 @@ interface Destination {
87
146
  waypoints?: Waypoint[];
88
147
  /** Category/classification */
89
148
  category?: string;
149
+ /** Room number — the CMS's "Room Number" field. A **real JACS column**, not
150
+ * one of the `mm_` stand-in extensors, so it arrives on the entity rather
151
+ * than in `extensors`. Surfaced as `getPOIDetails().roomNumber`. */
152
+ unitNumber?: string | null;
153
+ /** Search keywords */
154
+ keywords?: string[];
90
155
  /** Additional properties */
91
156
  properties?: Record<string, unknown>;
157
+ /** The CMS "Location Image" — a photo or logo for the popover, not a map
158
+ * marker. JACS `/all` fullcall serves this as
159
+ * `{ items: [{ resourceType, mimeType, path }] }`; the legacy/basic shape is
160
+ * `[{ locales: [{ uriPath }] }]`. `destinationImageSrc` handles both, and
161
+ * surfaces the result on `sdk.getPOIDetails(poi).imageUrl`. */
162
+ uris?: unknown;
163
+ /** Inline SVG for the uploaded image, embedded by JACS `/all`. Preferred over
164
+ * fetching `uris[].path` — no network round-trip. */
165
+ svg?: string;
166
+ /** Free-form CMS metadata. */
167
+ extensors?: Record<string, unknown>;
168
+ /** @deprecated Read by nothing in the SDK as of rc.34.
169
+ *
170
+ * JACS still serves the old map-manager "Label Display" mode
171
+ * (`0` Hidden · `1` Label · `2` Image), but the CMS removed the picker and
172
+ * pins every write to `1`. Rows authored before that keep a stale `0`/`2`
173
+ * the author can no longer change, so honouring the field would hide labels
174
+ * with no way to bring them back. Destinations always render dot + text
175
+ * label; the uploaded image goes to the popover instead. */
176
+ displayMode?: number;
92
177
  }
93
178
  /**
94
179
  * Waypoint - a specific point location
@@ -102,6 +187,19 @@ interface Waypoint {
102
187
  floorId?: string | number;
103
188
  /** Whether this is the primary/default waypoint */
104
189
  isPrimary?: boolean;
190
+ /** CMS zone (department) this waypoint belongs to, if any. The only backend
191
+ * link between a location and its department — read at runtime across the
192
+ * data provider (see the package CLAUDE.md zone landmine). */
193
+ zoneId?: string | number;
194
+ /** Per-instance names JACS attaches to this waypoint's amenities (e.g. a
195
+ * "Parking" amenity placed twice as "Parking Lot A" / "Parking Lot B").
196
+ * `id` matches an `Amenity.id`, not this waypoint — a waypoint can carry
197
+ * more than one amenity, so a consumer must find the entry for the
198
+ * specific amenity it's rendering rather than assuming index 0. */
199
+ amenityAssociations?: Array<{
200
+ id: string | number;
201
+ name: string;
202
+ }>;
105
203
  }
106
204
  /**
107
205
  * Amenity enriched with the floor it belongs to
@@ -121,6 +219,28 @@ interface POISearchResult {
121
219
  matchedFields: string[];
122
220
  }
123
221
 
222
+ /**
223
+ * A Zone is a CMS-authored department category — a named, colored grouping
224
+ * (e.g. "Cardiology", "Surgical", "Admin"). Zones are venue-wide (not
225
+ * floor-scoped) in the JACS `/all` payload and carry no geometry of their own;
226
+ * their spatial footprint is derived from the waypoints that reference them
227
+ * (`waypoint.zoneId`), which lets the SDK color-code the room/unit polygons a
228
+ * zone covers. See `map/zones.ts` for the derivation.
229
+ */
230
+ interface Zone {
231
+ id: number;
232
+ name: string;
233
+ /** Authored hex color (e.g. "#d32f2f"); may be empty when the author
234
+ * hasn't picked one — such zones are skipped for color-coding. */
235
+ color: string;
236
+ /** The color this zone's rooms are drawn in under the active theme — `color`
237
+ * in the default theme, its high-contrast palette entry under high contrast.
238
+ * Use this, not `color`, for a legend swatch, and re-read it on
239
+ * `themeChanged`. Set on `getZones()` results only. */
240
+ displayColor?: string;
241
+ description?: string;
242
+ }
243
+
124
244
  /** Captured camera state (center, zoom, pitch, bearing) */
125
245
  type CameraState = {
126
246
  center: [number, number];
@@ -140,25 +260,447 @@ type ViewOptions = {
140
260
  /** SW/NE bounding box */
141
261
  type Bounds = [[number, number], [number, number]];
142
262
 
263
+ /** How to center the map after computing a route */
264
+ type WayfindCenterMode = 'none' | 'destination' | 'route';
265
+ /**
266
+ * Per-call routing preferences forwarded to the wayfinding provider.
267
+ *
268
+ * - `avoidStairs` — hard-filter stairs from the graph. Accessibility
269
+ * preference otherwise comes from the CMS's per-path-type `weight`
270
+ * (elevators score low, stairs/escalators score high) — see
271
+ * `data/wayfinding/weights.ts` — not from a caller-supplied flag.
272
+ *
273
+ * The bundled stub provider doesn't compute routes; consumers wire a real
274
+ * provider (see `mm-web-sdk-example/lib/wayfinding`) that reads this
275
+ * flag when building edge weights.
276
+ */
277
+ type RoutingOptions = {
278
+ avoidStairs?: boolean;
279
+ };
280
+ /**
281
+ * A spot on a floor that need not be a POI — a tap in the middle of a
282
+ * corridor. Routes and walk times accept one wherever they accept a POI:
283
+ * it's snapped to the closest point along a path on its floor and routed
284
+ * from (or to) there, without any CMS waypoint at that spot.
285
+ *
286
+ * `kind` tells it apart from a `POI`, which also has `floorId` and
287
+ * `coordinates`. Build one with `sdk.snapToPath`, or by hand from a stored
288
+ * point (a shared link) — the SDK snaps it again either way.
289
+ */
290
+ type MapLocation = {
291
+ kind: 'location';
292
+ floorId: string | number;
293
+ /** `[lng, lat]`. */
294
+ coordinates: [number, number];
295
+ /** What the route calls it — the arrive step's landmark and the end
296
+ * flag's label. Omitted, the arrive step reads "You have arrived". */
297
+ label?: string;
298
+ /** Where the visitor actually tapped, when it's off the walking network and
299
+ * `coordinates` (where the route starts or ends) had to be moved onto it.
300
+ * Set by `snapToPath`; a location built from a stored point is snapped from
301
+ * `tapped ?? coordinates`, so store this to rebuild it later. */
302
+ tapped?: [number, number];
303
+ /** The walkable way from `tapped` to `coordinates` — around rooms, back-of-
304
+ * house areas and other obstacles, leaving a room by its door — drawn as a
305
+ * dotted line with a marker on the tap. `tapped` without `approach` means
306
+ * no walkable way was found (a room with no known door, somewhere nothing
307
+ * walks), so the point was simply moved to the nearest path. */
308
+ approach?: [number, number][];
309
+ /** `tapped` is inside a building or room, so `approach` leads to a hallway
310
+ * rather than an outdoor walkway — it picks the approach step's wording.
311
+ * Set by `snapToPath` alongside `approach`. */
312
+ indoors?: boolean;
313
+ /** `tapped` was in a hallway, a little way off its path line: the point was
314
+ * moved to the middle of the hall, with no `approach` to draw. Set by
315
+ * `snapToPath` with `tapped`; a "choose on map" UI can say so. */
316
+ centered?: boolean;
317
+ };
318
+ /** True for a `MapLocation`, false for a `POI`. */
319
+ declare function isMapLocation(value: unknown): value is MapLocation;
320
+
321
+ /**
322
+ * A single point along the rendered route. `mapId` lets the renderer
323
+ * split the polyline by floor (so other-floor segments don't paint
324
+ * through the visible floor's geometry); `waypointId` and `pathTypeId`
325
+ * let downstream layers (text-directions, step UI) classify
326
+ * transitions — e.g. "Take the elevator" vs "Walk straight". The two
327
+ * endpoint points are the kiosk and destination anchors (`isEndpoint`
328
+ * marks them so they can render endpoint pins). All fields except
329
+ * `coordinates` are optional for backward-compat with the straight-line
330
+ * fallback.
331
+ */
332
+ type RoutePoint = {
333
+ coordinates: [number, number];
334
+ /** Floor mapId this point sits on. Missing on the straight-line fallback. */
335
+ mapId?: number;
336
+ /** JACS waypoint id this point resolved to, if any. */
337
+ waypointId?: number;
338
+ /** Path type of the edge that LEADS INTO this point (i.e. the edge
339
+ * whose `toKey` is this node). Used to detect elevator / stair /
340
+ * escalator transitions. Missing on the start point and on the
341
+ * straight-line fallback. */
342
+ pathTypeId?: number;
343
+ /** True for the snapped kiosk + destination anchors. */
344
+ isEndpoint?: boolean;
345
+ };
346
+
347
+ type WayfindStep = DepartStep | TurnStep | ContinueStep | TransitionStep$1 | ApproachStep | ArriveStep;
348
+ type StepBase = {
349
+ /** Indices into the input `points` array that this step spans. */
350
+ pointRange: [startInclusive: number, endInclusive: number];
351
+ /** Floor this step is on. `null` for cross-floor transitions. */
352
+ floorId: number | null;
353
+ /** Localized instruction text. */
354
+ text: string;
355
+ /** Total walking distance within this step, in feet (rounded). */
356
+ distanceFeet?: number;
357
+ /** Landmark name used for anchoring, if any. */
358
+ landmark?: string;
359
+ };
360
+ type DepartStep = StepBase & {
361
+ type: 'depart';
362
+ /** Which way the traveller turns before walking off, relative to the
363
+ * way they're facing at the origin. Absent when `originFacingBearing`
364
+ * wasn't supplied (direction unknowable) or when the route heads
365
+ * straight ahead. Consumers use it to pick the step's icon. */
366
+ initialTurn?: 'left' | 'right' | 'around';
367
+ };
368
+ type TurnStep = StepBase & {
369
+ type: 'turn-left' | 'turn-right' | 'u-turn';
370
+ };
371
+ type ContinueStep = StepBase & {
372
+ type: 'continue';
373
+ };
374
+ type TransitionStep$1 = StepBase & {
375
+ type: 'transition';
376
+ transition: 'elevator' | 'stairs' | 'escalator';
377
+ fromFloorId: number | null;
378
+ toFloorId: number | null;
379
+ };
380
+ type ArriveStep = StepBase & {
381
+ type: 'arrive';
382
+ };
383
+ /**
384
+ * The walk between a tapped spot and the walking network (`MapLocation.approach`):
385
+ * `'start'` from where the visitor tapped to the hallway, before the depart
386
+ * step; `'end'` from the hallway to the tapped destination, before arrive.
387
+ * Its geometry isn't on the route line, so it carries its own `path` (in
388
+ * walking order) — highlight and frame that, not `pointRange`, which only
389
+ * marks the route point it joins.
390
+ */
391
+ type ApproachStep = StepBase & {
392
+ type: 'approach';
393
+ approach: 'start' | 'end';
394
+ path: [number, number][];
395
+ };
396
+
143
397
  interface MapEvent {
144
398
  floor?: Floor;
399
+ /** `routeReady`: the destination routed to — the exact instance, so for
400
+ * `navigateFromKioskToClosestAmenity` it carries the picked `waypointId`. */
145
401
  poi?: POI;
402
+ /** All POIs co-located at a clicked point (overlapping pins / a shared
403
+ * suite). Emitted by `poiSelected`; length ≥ 1, and `poi` mirrors
404
+ * `pois[0]` for single-POI consumers. */
405
+ pois?: POI[];
146
406
  coordinates?: [number, number];
407
+ /** `locationPicked`: the tap snapped to the closest point along a path, or
408
+ * `null` when no path on the floor was within reach (`coordinates` is the
409
+ * raw tap either way). `routeFailed`: the location that didn't resolve.
410
+ * `routeReady`: the tapped spot routed to, in place of `poi`. */
411
+ location?: MapLocation | null;
412
+ /** `locationPicked`: the tap was outside every building and room on the
413
+ * floor. With `hasOutdoorPaths(floor.id)` false, nothing outdoors on this
414
+ * floor is walkable — the visitor wants a floor that has outdoor paths. */
415
+ outdoors?: boolean;
147
416
  error?: any;
148
417
  venue?: any;
149
418
  camera?: CameraState;
419
+ /** Emitted by `themeChanged`; `'custom'` when consumer passed a raw style. */
420
+ theme?: 'default' | 'high-contrast' | 'color-blind' | 'custom';
421
+ /** Emitted by `localeChanged` after `setLocale()` finishes patching the
422
+ * venue model. BCP-47 code (`'en'`, `'es'`, `'es-MX'`). */
423
+ locale?: string;
424
+ /** Emitted by `routeReady` after a successful `navigateFromKioskToPOI`.
425
+ * Carries the raw geometry (`points`) and the human-readable
426
+ * step-by-step directions. Subscribe to drive a turn-by-turn UI. */
427
+ route?: {
428
+ points: RoutePoint[];
429
+ steps: WayfindStep[];
430
+ /** Floor mapId of each segment, in render order. `null` for
431
+ * segments emitted by the straight-line fallback. */
432
+ floorIds: Array<number | null>;
433
+ };
150
434
  }
151
435
  type EventCallback = (event: MapEvent) => void;
152
436
 
437
+ /**
438
+ * Padding kept clear inside the viewport when the SDK frames bounds —
439
+ * the initial venue/floor fit and the wayfinding route fit. A plain
440
+ * number pads all sides equally; the object form lets a kiosk reserve
441
+ * space for fixed UI overlays (header, dock, side rails) so framed
442
+ * content is never hidden behind them.
443
+ */
444
+ type BoundsPadding = number | {
445
+ top: number;
446
+ bottom: number;
447
+ left: number;
448
+ right: number;
449
+ };
450
+ /**
451
+ * Built-in theme names accepted by `MinuteMaps.setTheme()` and the
452
+ * `options.theme` init option. Pass a `StyleSpecification` directly for
453
+ * custom themes.
454
+ */
455
+ type ThemeName = 'default' | 'high-contrast' | 'color-blind';
456
+ /**
457
+ * Style of the disc + ring rendered behind each amenity icon. The SDK
458
+ * composites this into the icon bitmap at registration time, so badge +
459
+ * icon participate in symbol collision as one unit.
460
+ */
461
+ type AmenityBadgeStyle = {
462
+ /** Fill colour of the badge disc. Default `'#fdb81e'`. */
463
+ color?: string;
464
+ /** Stroke colour of the ring around the disc. Default `'#FFFFFF'`. */
465
+ ringColor?: string;
466
+ /** Ring width in logical px. Default `2`. */
467
+ ringWidth?: number;
468
+ /** Extra inset between the icon and the badge's inner edge, in logical px
469
+ * (applied on all sides). Default `0` — the icon fills ~85% of the disc
470
+ * as before. Raise this if an icon's own artwork reaches close to its
471
+ * viewBox edges and ends up touching the ring. */
472
+ padding?: number;
473
+ };
153
474
  interface SDKOptions {
154
475
  debug?: boolean;
155
- theme?: 'light' | 'dark' | 'hybrid' | any;
476
+ /**
477
+ * `window` key the SDK instance is published under while `debug` is on, so
478
+ * a running kiosk can be driven from devtools (`mm.debug.hide('labels')`,
479
+ * `mm.getMap()`). Defaults to `'mm'`. Pass another name to avoid a clash,
480
+ * or `false` to publish nothing even in debug mode. Ignored entirely when
481
+ * `debug` is off.
482
+ */
483
+ debugGlobal?: string | false;
484
+ /**
485
+ * Initial theme. `'default'` uses the bundled hybrid 3D theme.
486
+ * `'high-contrast'` uses the WCAG-AA tuned theme. `'color-blind'` keeps the
487
+ * default look but swaps the colors that red-green and blue-yellow color
488
+ * blindness merge (the route, room categories and department zones,
489
+ * restrooms, parks). Pass a
490
+ * `StyleSpecification` for fully custom styling.
491
+ */
492
+ theme?: ThemeName | any;
493
+ /**
494
+ * When true, the SDK skips animations on imperative camera calls
495
+ * (wayfinding fits, `setView`, idle re-frames). Consumers should
496
+ * mirror their app's `prefers-reduced-motion` state into this.
497
+ */
498
+ reducedMotion?: boolean;
499
+ /**
500
+ * Whether a freshly computed route auto-highlights its first step's
501
+ * segment (the brighter `route-line-active` overlay). Defaults to
502
+ * `true`, preserving the turn-by-turn segment highlight that
503
+ * `setActiveStep` drives.
504
+ *
505
+ * Set `false` for kiosk-style "show the whole route at once" UIs:
506
+ * the entire route line is shown without singling out one segment,
507
+ * which reads more clearly at a glance and avoids implying the visitor
508
+ * must step through the route. `setActiveStep` still works when called
509
+ * explicitly (e.g. a paginated QR-handoff / mobile flow) — this only
510
+ * controls the automatic highlight on `routeReady`.
511
+ */
512
+ routeStepHighlight?: boolean;
513
+ /**
514
+ * Which way is up while a route is on screen. Defaults to `'kiosk'`.
515
+ *
516
+ * - `'kiosk'` — the kiosk's heading rotated 180°, so the map matches the
517
+ * direction a visitor walks away from the screen (180°, south-up, when
518
+ * the venue has no kiosk heading).
519
+ * - `'first-step'` — the direction of the route's first step: the tapped
520
+ * start's approach when there is one, otherwise the first stretch of
521
+ * the path. For UIs viewed away from any kiosk (a phone, a home
522
+ * computer), where the kiosk's facing means nothing. Taken once per
523
+ * route, so stepping through it or toggling the full route keeps that
524
+ * orientation.
525
+ */
526
+ routeBearing?: 'kiosk' | 'first-step';
527
+ /**
528
+ * Whether restroom pins that are too close together to draw individually
529
+ * collapse into a single Material `wc` badge. Defaults to `true`.
530
+ *
531
+ * Venues author men's and women's restrooms as two amenities a few metres
532
+ * apart. Both are wayfinding-critical (rank 0) and both draw on a layer that
533
+ * doesn't allow icon overlap, so without this MapLibre resolves the collision
534
+ * by hiding one of them outright until the camera is very close in. Grouping
535
+ * shows one badge in their place and hands back *all* the members when it's
536
+ * tapped, so the consumer can offer directions to each.
537
+ *
538
+ * Set `false` to opt out and get the raw collision behaviour back.
539
+ */
540
+ groupRestroomIcons?: boolean;
156
541
  initialFloor?: string | number;
157
542
  enableInteractions?: boolean;
158
543
  customSprite?: string;
159
544
  minIndoorZoom?: number;
545
+ /**
546
+ * How far, in **metres** (default `0.3`, ≈1 ft), extruded interior polygons
547
+ * (restrooms, back-of-house, obstacles, connectors) are inset so their edges
548
+ * don't z-fight the neighbouring room block. A polygon too thin to survive
549
+ * the inset (it would keep under half its area, e.g. a ~0.3 m back-of-house
550
+ * perimeter band) is left at its original size rather than erased. Metres to match the theme's
551
+ * `fill-extrusion-height` values, which MapLibre defines in metres — unlike
552
+ * the distances shown to visitors, which are in feet. Was feet before this
553
+ * option changed units; a value of `1` is now ~3.3× thicker.
554
+ */
160
555
  wallThickness?: number;
161
- boundsPadding?: number;
556
+ /**
557
+ * How much costlier a candidate on a **different floor** from the kiosk is
558
+ * treated when choosing the closest instance of an amenity (default `1.5`).
559
+ * Used by `findClosestByWalkTime`, and so by `navigateFromKioskToClosestAmenity`
560
+ * and `highlightAmenity`.
561
+ *
562
+ * A multiplier on the trip's effort — walk time plus the elevator wait — not
563
+ * a distance: `1.5` means another floor's instance has to be a third cheaper
564
+ * to beat one on the kiosk's floor. It is a *preference* for not making the
565
+ * visitor change floors, layered on top of the physical cost of doing so
566
+ * (which is already in the effort). `1` turns it off and ranks by effort
567
+ * alone; a value below `1` is treated as `1`. It only affects which instance
568
+ * is chosen — never a route.
569
+ *
570
+ * For scale: at the Las Vegas VA the nearest other-floor Lactation Pod costs
571
+ * ~400 ft-equivalent against ~530 for the one on the kiosk's floor, so it
572
+ * takes a value above ~1.32 to keep the visitor on their floor.
573
+ */
574
+ otherFloorPenalty?: number;
575
+ boundsPadding?: BoundsPadding;
576
+ /**
577
+ * Caps how far a visitor can pan/zoom out. By default the SDK derives this
578
+ * from the venue's own bounds, scaled to 3x its width/height around the
579
+ * same center — enough room to pan around the building without drifting
580
+ * into an empty, un-tiled region. The default box is never smaller than
581
+ * ~3.7 km a side, so a small venue isn't forced to zoom in tighter than a
582
+ * large one. Pass a number to use a different
583
+ * multiplier (e.g. `1.5` for a tighter leash), explicit `[[west, south],
584
+ * [east, north]]` bounds to override entirely, or `false` to disable
585
+ * max-bounds clamping.
586
+ */
587
+ maxBounds?: Bounds | number | false;
588
+ /**
589
+ * Hides basemap points of interest (civic and marine icons, park names) that
590
+ * fall within this many metres of the venue's bounds, so an OSM library or
591
+ * chapel on the campus isn't drawn underneath the venue's own map. Defaults
592
+ * to `50`. Street labels, road shields and water labels are never hidden.
593
+ * Pass `false` to show every basemap POI.
594
+ */
595
+ hideBasemapPoisNearVenue?: number | false;
596
+ /**
597
+ * Whether the venue's kiosk anchors a "You are here" marker, and the opening
598
+ * view / default route origin. Defaults to `true`. Set `false` for non-kiosk
599
+ * consumers (e.g. a browser map): no device is resolved, even via the
600
+ * "first placed device" fallback, so the marker is never drawn.
601
+ */
602
+ kiosk?: boolean;
603
+ /**
604
+ * Pitch (deg) for the opening view, applied centred on the kiosk ("You
605
+ * are here"). Omit for top-down.
606
+ */
607
+ initialPitch?: number;
608
+ /**
609
+ * Zoom for the opening view, centred on the kiosk. Set this above the
610
+ * theme's unit-walls breakpoint (~16.5 in the bundled hybrid theme) so the
611
+ * kiosk opens on the building's interior floor plan (its contents) rather
612
+ * than a zoomed-out 3D massing outline. Omit to keep the post-floor-fit
613
+ * zoom.
614
+ */
615
+ initialZoom?: number;
616
+ /**
617
+ * Zoom floor for selecting a place — a map tap or `selectPOI` (default
618
+ * `19.25`, one level into the Kiosk view). A selection made from further out
619
+ * zooms in to this; a closer zoom is kept. At the Kiosk view's edge (18.25)
620
+ * the selected dot and name are drawn but still lose collision to their
621
+ * neighbours, and in the Building view (a room tapped from 16.5 up) they
622
+ * aren't drawn at all — either way the visitor selects a place they can't
623
+ * see. `0` keeps the current zoom on every selection.
624
+ */
625
+ selectionZoom?: number;
626
+ /**
627
+ * Clamp zoom-out relative to the opening view. When set, the map's
628
+ * `minZoom` becomes `(initialZoom − this)`, so visitors can nudge out by
629
+ * this many zoom levels but never pull back below the interior into the
630
+ * massing/region. `0` locks zoom-out exactly to the opening view.
631
+ */
632
+ minZoomBelowInitialFit?: number;
633
+ /**
634
+ * Zoom-out floor expressed as ground scale: the most metres one CSS pixel
635
+ * may span. The SDK converts it to a `minZoom` at the venue's latitude, so
636
+ * every venue shows the same amount of surrounding map when fully zoomed
637
+ * out — a plain zoom number doesn't, because Web Mercator shows less ground
638
+ * per zoom level the further a venue is from the equator. Takes precedence
639
+ * over `minZoomBelowInitialFit`. For scale, `1.207` is zoom 15.8 at Orlando
640
+ * (about 2.3 km x 1.3 km on a 1920x1080 screen).
641
+ */
642
+ minZoomMetersPerPixel?: number;
643
+ /**
644
+ * Override the colour the SDK recolours every amenity SVG to before it
645
+ * composites the badge. `none` / `transparent` fills are preserved so
646
+ * cut-outs stay. When the badge is enabled (the default), this defaults
647
+ * to navy (`#162e51`) so icons read on gold. Set explicitly for a
648
+ * different look, or set `amenityBadge: false` to disable recolouring
649
+ * altogether and keep the CMS-uploaded colours.
650
+ */
651
+ amenityIconColor?: string;
652
+ /**
653
+ * Badge composited behind each amenity icon. Pass `false` to render the
654
+ * icon alone (no badge — useful for high-contrast or 2D themes where the
655
+ * gold disc would compete with the floor). Pass an object to tune the
656
+ * disc / ring style. Defaults to a VA-gold disc with a 2px white ring.
657
+ *
658
+ * The badge is baked into the icon bitmap (canvas composite) rather than
659
+ * drawn as a separate circle layer, so badge + icon participate in symbol
660
+ * collision together — overlapping amenities hide as one unit instead of
661
+ * the icon hiding while the disc stays painted.
662
+ */
663
+ amenityBadge?: AmenityBadgeStyle | false;
664
+ /**
665
+ * Badge for vertical-circulation connectors (elevator / stairs / escalator).
666
+ * These render as their own visual class — a navy disc with a white glyph —
667
+ * so circulation reads distinct from the gold service amenities. Pass `false`
668
+ * to fall them back into the regular gold badge, or an object to tune the
669
+ * disc / ring. When `amenityBadge` is `false`, connectors are badge-less too
670
+ * unless this is set explicitly.
671
+ */
672
+ connectorBadge?: AmenityBadgeStyle | false;
673
+ /**
674
+ * Badge for information-desk amenities (curated `information`/`info`
675
+ * icon). These render as their own visual class — a green disc with a
676
+ * white glyph by default — so information reads distinct from the gold
677
+ * service amenities. Pass `false` to fall them back into the regular gold
678
+ * badge, or an object to tune the disc / ring. When `amenityBadge` is
679
+ * `false`, information icons are badge-less too unless this is set
680
+ * explicitly.
681
+ */
682
+ informationBadge?: AmenityBadgeStyle | false;
683
+ /**
684
+ * Glyph colour for information icons on their badge. Default white
685
+ * (matches `informationBadge`'s default green disc).
686
+ */
687
+ informationIconColor?: string;
688
+ /**
689
+ * Badge for the end-of-route flag (`poi-route-end-flag`) shown in place of a
690
+ * destination's dot while a route to it is displayed. Has its own defaults
691
+ * (green disc, white ring) that don't follow `amenityBadge`. Pass an object
692
+ * to tune the disc / ring, or `false` for the bare flag glyph with no badge.
693
+ * When `amenityBadge` is `false`, the flag is badge-less too unless this is
694
+ * set explicitly. Under the high-contrast theme the disc colour is forced to
695
+ * the high-contrast amenity yellow, like the amenity and connector badges.
696
+ */
697
+ routeEndBadge?: AmenityBadgeStyle | false;
698
+ /**
699
+ * Flag glyph colour on the end-of-route badge. Default white (contrasts the
700
+ * default green disc); doesn't follow `amenityIconColor`. Ignored under the
701
+ * high-contrast theme, which forces its own glyph colour.
702
+ */
703
+ routeEndIconColor?: string;
162
704
  styleMode?: 'venueStyleUrl' | 'sdkTemplate';
163
705
  templateOverrideMode?: 'colorsOnly' | 'colorsAndConstants' | 'all';
164
706
  }
@@ -190,9 +732,19 @@ type SDKConfig = {
190
732
  venueId: number;
191
733
  locale?: string;
192
734
  auth?: JMapAuth;
735
+ /**
736
+ * Identifies which physical kiosk this instance is, so the SDK can pin
737
+ * the "You are here" marker. Matched against either the numeric device
738
+ * `id` or the device `uuid` from the venue's `devices`. The device's
739
+ * attached waypoint (`waypoint.deviceIds`) is the kiosk's location.
740
+ */
741
+ deviceId?: string | number;
193
742
  };
194
743
  jacs: {
195
- mode: 'proxy' | 'direct';
744
+ /** `snapshot` reads published venue snapshots from a CDN — what clients
745
+ * should use. `proxy`/`direct` read live JACS (the draft): local dev and
746
+ * CMS preview. */
747
+ mode: 'proxy' | 'direct' | 'snapshot';
196
748
  host?: string;
197
749
  auth?: {
198
750
  clientId: string;
@@ -200,12 +752,240 @@ type SDKConfig = {
200
752
  password: string;
201
753
  };
202
754
  proxyBaseUrl?: string;
755
+ baseUrl?: string;
203
756
  };
204
757
  options?: SDKOptions;
205
758
  };
206
759
 
207
- /** How to center the map after computing a route */
208
- type WayfindCenterMode = 'none' | 'destination' | 'route';
760
+ /** The chosen ends; either may be missing. */
761
+ type RouteEndPreview = {
762
+ start?: POI | MapLocation | null;
763
+ destination?: POI | MapLocation | null;
764
+ };
765
+
766
+ type Visibility = 'visible' | 'none';
767
+ type DebugLayersDeps = {
768
+ getMap: () => Map | null;
769
+ /**
770
+ * Re-assert the visibility the SDK itself owns — floor layers, the POI
771
+ * category filter, the 2D/3D view mode. Called at the end of `reset()`,
772
+ * because restoring a captured `visibility` can't distinguish "the theme
773
+ * ships this visible" from "the SDK hid it because it belongs to another
774
+ * floor", and the naive restore would reveal every floor at once.
775
+ */
776
+ reapplySdkState?: () => void;
777
+ log?: (...args: unknown[]) => void;
778
+ };
779
+ type LayerRow = {
780
+ id: string;
781
+ type: string;
782
+ visibility: Visibility;
783
+ /**
784
+ * What this layer's visibility was before we touched it — i.e. what the
785
+ * theme and the SDK want it to be. Equal to `visibility` for any layer we
786
+ * hold no override on. A UI wanting to show "is this group on" should judge
787
+ * only the layers whose `base` is `'visible'`: the rest (the 3D extrusions
788
+ * in flat mode, the 2D outlines outside it) aren't rendering by design, so
789
+ * counting them makes a healthy group look half-off.
790
+ */
791
+ base: Visibility;
792
+ };
793
+ /**
794
+ * Console-facing layer inspector for demos, screenshots and debugging.
795
+ *
796
+ * Reachable as `sdk.debug`, and as `window.mm.debug` when
797
+ * `options.debug` is on (see `options.debugGlobal`).
798
+ *
799
+ * Two things make it more than a wrapper around `setLayoutProperty`:
800
+ *
801
+ * 1. **Selectors.** `hide('amenities')`, `hide('route-')`, `hide('/^poi-/')`
802
+ * and `hide('type:fill-extrusion')` all work, so you don't have to know
803
+ * the theme's layer ids to start poking at it.
804
+ * 2. **Overrides are sticky.** The SDK re-asserts layer visibility on floor
805
+ * changes, `setPOIFilter`, flat-mode toggles and theme swaps — all of which
806
+ * would otherwise wipe a manual toggle mid-demo. Every override is re-applied
807
+ * on `styledata`, so what you hid stays hidden until `reset()`.
808
+ */
809
+ declare class DebugLayers {
810
+ private deps;
811
+ /** layer id → visibility this API is asserting. */
812
+ private overrides;
813
+ /** layer id → visibility before we first touched it. */
814
+ private originals;
815
+ /** `${layer}|${prop}` → paint value before we first touched it. */
816
+ private paintOriginals;
817
+ private sticky;
818
+ private attachedTo;
819
+ /** Re-entrancy guard: our own `setLayoutProperty` fires `styledata`. */
820
+ private applying;
821
+ private readonly onStyleData;
822
+ constructor(deps: DebugLayersDeps);
823
+ /** Begin re-asserting overrides across style/floor changes. */
824
+ attach(map: Map): void;
825
+ detach(): void;
826
+ /**
827
+ * Every layer in the active style, newest-on-top last, with its current
828
+ * visibility. Pass a selector to narrow it (same grammar as `hide`).
829
+ */
830
+ list(selector?: string): LayerRow[];
831
+ /** Just the ids currently rendering — the quickest "what am I looking at". */
832
+ visible(): string[];
833
+ /** Group name → the ids it resolves to *in the active style*. */
834
+ groups(): Record<string, string[]>;
835
+ /** Hide every layer the selectors resolve to. Returns the ids affected. */
836
+ hide(...selectors: string[]): string[];
837
+ /**
838
+ * **Force** every resolved layer visible — including ones the SDK
839
+ * deliberately keeps off: the 3D extrusions while flat mode is on, the 2D
840
+ * outlines while it isn't, every floor but the active one. Forcing those
841
+ * renders two representations of the same geometry at once, or every floor
842
+ * stacked, and the map looks wrong.
843
+ *
844
+ * So this is *not* the way to undo a `hide()` — use `restore()`, which puts
845
+ * each layer back to what it was rather than blanket-revealing the group.
846
+ * Reach for `show()` only when you actually mean "reveal this regardless".
847
+ */
848
+ show(...selectors: string[]): string[];
849
+ /**
850
+ * Undo our overrides on these layers: each goes back to the visibility it
851
+ * had before we first touched it, and control returns to the SDK (floor
852
+ * layers, POI filter, view mode re-assert afterwards).
853
+ *
854
+ * This is the correct inverse of `hide()`. A group is rarely uniformly
855
+ * visible to begin with — `rooms` hides either the 2D outline or the 3D
856
+ * extrusion depending on view mode, and every floor layer but the active
857
+ * one is off — so `hide()` then `show()` does not round-trip, but `hide()`
858
+ * then `restore()` does.
859
+ *
860
+ * Returns the ids that actually had an override; layers we never touched
861
+ * are left alone.
862
+ */
863
+ restore(...selectors: string[]): string[];
864
+ /** Flip each resolved layer independently. Returns the ids affected. */
865
+ toggle(...selectors: string[]): string[];
866
+ /**
867
+ * Show only what the selectors resolve to — everything else is hidden
868
+ * (except `background`, which is just the canvas colour). The one-liner for
869
+ * "which layer is drawing that thing".
870
+ *
871
+ * The kept set is *restored*, not forced: anything we'd previously hidden
872
+ * comes back, but a layer the theme ships hidden stays hidden. Soloing must
873
+ * not invent geometry that never renders normally — see `show()`.
874
+ */
875
+ solo(...selectors: string[]): string[];
876
+ /**
877
+ * Set a paint property — `paint('units-fill', 'fill-color', '#f00')`. The
878
+ * original value is captured so `reset()` puts it back. Not sticky: unlike
879
+ * visibility, a theme swap rebuilds paint from the new style and re-applying
880
+ * a captured expression across themes is exactly the bug `setTheme` had to
881
+ * fix. Re-run it after a swap if you need it back.
882
+ */
883
+ paint(selector: string, prop: string, value: unknown): string[];
884
+ /**
885
+ * Drop every override and paint change, then let the SDK re-assert the
886
+ * visibility it owns (floor layers, POI filter, view mode). Back to the map
887
+ * you'd have had without touching anything.
888
+ */
889
+ reset(): void;
890
+ /**
891
+ * Hand the layers we just released back to the SDK, then re-assert whatever
892
+ * overrides are still standing.
893
+ *
894
+ * The second half is not belt-and-braces. The SDK's re-assert passes are
895
+ * all-or-nothing: with no POI filter active, `applyPOIFilter` writes
896
+ * `visible` to *every* POI category layer, and the floor / view-mode passes
897
+ * sweep their whole layer sets the same way. So releasing one group would
898
+ * un-hide every other group you'd hidden — hide labels, destinations and
899
+ * amenities, re-check one, and all three came back.
900
+ *
901
+ * Doing it here rather than waiting for the `styledata` the SDK's writes
902
+ * trigger also keeps it synchronous: MapLibre fires that on a later frame,
903
+ * so a UI reading visibility straight after this call would otherwise see —
904
+ * and render — the clobbered state.
905
+ */
906
+ private handBackToSdk;
907
+ /**
908
+ * The current override set as a plain object — save it, paste it back with
909
+ * `apply()` to reproduce the exact same framing for a screenshot later.
910
+ */
911
+ snapshot(): Record<string, Visibility>;
912
+ /** Apply a `snapshot()` (or any layer-id → visibility map). */
913
+ apply(snapshot: Record<string, Visibility>): string[];
914
+ /**
915
+ * Stop (or resume) re-asserting overrides when the style changes. Turn it
916
+ * off if you want a floor switch or `setPOIFilter` to win over what you
917
+ * toggled by hand.
918
+ */
919
+ setSticky(enabled: boolean): boolean;
920
+ /** Console cheat sheet. */
921
+ help(): Array<{
922
+ call: string;
923
+ does: string;
924
+ }>;
925
+ /**
926
+ * Resolve one selector to layer ids present in the active style, tried in
927
+ * order: group name → exact layer id → `type:<layer type>` → `/regex/` →
928
+ * case-insensitive substring on the id.
929
+ */
930
+ resolve(selector: string): string[];
931
+ private resolveAll;
932
+ private set;
933
+ /**
934
+ * Put these layers back to their pre-override visibility and forget them.
935
+ * Callers decide whether to follow up with `reapplySdkState` — `restore()`
936
+ * and `reset()` do, `solo()` deliberately doesn't.
937
+ */
938
+ private restoreVisibility;
939
+ /** Write one layer's visibility and remember it as an override. */
940
+ private write;
941
+ /**
942
+ * Re-assert overrides after something else rewrote layer visibility. Only
943
+ * writes when the current value actually differs, so this doesn't feed
944
+ * itself through the `styledata` it triggers.
945
+ */
946
+ private reapply;
947
+ private styleLayers;
948
+ private visibilityOf;
949
+ /**
950
+ * MapLibre throws from these getters/setters while a style is mid-swap (the
951
+ * layer exists in `getStyle()` but its owner is being replaced). A debug tool
952
+ * that can break the map it's inspecting is worse than useless.
953
+ */
954
+ private safe;
955
+ }
956
+
957
+ type LayerAudit = {
958
+ /** `featureType` values the CMS permits for this customer. */
959
+ allowed: string[];
960
+ /** `featureType` values actually present in the loaded floors. */
961
+ inData: string[];
962
+ /** `featureType` values some theme layer filters on. */
963
+ inStyle: string[];
964
+ /**
965
+ * Drawn on a map but not in the customer's allowed list — a mis-authored
966
+ * or renamed layer. These render only by accident (if the theme happens to
967
+ * match the string) and are invisible otherwise.
968
+ */
969
+ unauthorized: string[];
970
+ /**
971
+ * Present in the data and allowed, but no theme layer filters on it. The
972
+ * author drew something the map will never show.
973
+ */
974
+ unstyled: string[];
975
+ /**
976
+ * The theme filters on it, but no loaded floor contains it. Dead weight in
977
+ * the style *for this venue* — another venue may well use it, so this is a
978
+ * prompt to check, not a delete list. Cross-reference `allowed`: a value
979
+ * that isn't in there either is dead everywhere for this customer.
980
+ */
981
+ unused: string[];
982
+ /**
983
+ * Allowed by the CMS but no theme rule matches it, whether or not this
984
+ * venue happens to draw it. The SDK's total blind spot — an author can
985
+ * legitimately draw any of these and get nothing.
986
+ */
987
+ allowedNotStyled: string[];
988
+ };
209
989
 
210
990
  type LoggerFn = (...args: unknown[]) => void;
211
991
  type AmenityManagerDeps = {
@@ -224,71 +1004,1896 @@ declare class AmenityManager {
224
1004
  private log;
225
1005
  private loadForFloor;
226
1006
  getAll(): AmenityWithFloor[];
1007
+ /**
1008
+ * Venue-wide amenities, one entry per id. `getAll()` returns each amenity
1009
+ * once per floor it appears on (and so renders a multi-floor "Bathroom"
1010
+ * three times in a venue list); this collapses those into a single record
1011
+ * with all its waypoints merged across floors.
1012
+ *
1013
+ * Also folds any duplicate-id records from the provider into a single entry
1014
+ * so React lists don't trip on dup keys. This used to say those duplicates
1015
+ * were "a CMS data bug" — they weren't. JACS returns one record per amenity;
1016
+ * the SDK's own floor index pushed it once per waypoint, so an amenity with
1017
+ * four placements on a floor came back four times. Fixed at the source in
1018
+ * `jacsDataProvider.floorIdsForWaypoints`; the fold stays as a cheap guard
1019
+ * for a provider that genuinely repeats an id.
1020
+ *
1021
+ * Use this for venue-wide browse UIs; pair it with `findClosestWaypoint`
1022
+ * (on the SDK) to resolve a tap to the nearest physical instance.
1023
+ */
1024
+ getDistinct(): Amenity[];
227
1025
  getByFloorId(floorId: Floor['id']): AmenityWithFloor[];
228
1026
  getAllKiosks(): AmenityWithFloor[];
229
1027
  getKioskForFloor(floorId: Floor['id']): AmenityWithFloor | null;
230
1028
  logKioskForFloor(floorId: Floor['id']): void;
231
1029
  }
232
1030
 
1031
+ /**
1032
+ * The `mm_`-prefixed extensor keys the CMS reserves for fields JACS has no
1033
+ * column for yet.
1034
+ *
1035
+ * This file is the read half of a cross-repo table: the write half is
1036
+ * `cms/packages/map-manager/app/utils/reserved-extensors.js`, which owns the key
1037
+ * names, the value encodings, and the rule that a default value is deleted
1038
+ * rather than stored. Change them together.
1039
+ *
1040
+ * The `mm_` prefix is load-bearing on both sides. Authors can type anything into
1041
+ * the CMS's free-form Properties editor, so an unprefixed key like
1042
+ * `point_of_interest` could collide with a hand-entered one; the prefix also
1043
+ * makes "is this key reserved?" a prefix scan rather than a hand-maintained list
1044
+ * once JACS grows real columns and these are migrated away.
1045
+ *
1046
+ * Two things that look like they belong here and don't:
1047
+ *
1048
+ * - **Room number.** The CMS labels it "Room Number", but it writes the real
1049
+ * `Destination.unitNumber` field, not an extensor. `getPOIDetails().roomNumber`
1050
+ * reads that field (see `sdk.ts`).
1051
+ * - **`mm_point_of_interest`.** Registered by the CMS and parsed here for
1052
+ * completeness, but deliberately not surfaced on `getPOIDetails` — nothing has
1053
+ * pinned down whether an unflagged destination should drop out of search, out
1054
+ * of the map, or neither, and guessing wrong is worse than omitting it.
1055
+ */
1056
+ declare const RESERVED_EXTENSOR_PREFIX = "mm_";
1057
+ /** Weekly opening hours. Value shape and parser live in `openHours.ts`. */
1058
+ declare const OPEN_HOURS_EXTENSOR_KEY = "mm_open_hours";
1059
+ /** Phone number, with the extension folded into the same value. */
1060
+ declare const PHONE_EXTENSOR_KEY = "mm_phone";
1061
+ /** Alt text for the destination's uploaded Location Image. */
1062
+ declare const IMAGE_ALT_EXTENSOR_KEY = "mm_image_alt";
1063
+ /** Author-set "this is a point of interest" flag. Parsed, not surfaced. */
1064
+ declare const POINT_OF_INTEREST_EXTENSOR_KEY = "mm_point_of_interest";
1065
+ /**
1066
+ * Which building on the campus the destination stands in.
1067
+ *
1068
+ * The venue's own `Building` record can't answer this: the CMS binds a venue to
1069
+ * a single building and hangs the floor list off it, so a campus with a dozen
1070
+ * numbered buildings has one `Building` and no way to say which one a room is
1071
+ * in. The author types it as free text, and that text is what a kiosk prints —
1072
+ * it is not a reference to anything, so don't try to resolve it against
1073
+ * `venue.buildings`.
1074
+ */
1075
+ declare const BUILDING_NAME_EXTENSOR_KEY = "mm_building_name";
1076
+ interface DestinationPhone {
1077
+ /** Exactly as the author typed it — `"(408) 300-9294"`. Deliberately not
1078
+ * normalised: doing that properly across international formats needs a real
1079
+ * phone library, and the CMS makes the same choice on the way in. */
1080
+ number: string;
1081
+ /** Internal extension digits, when there is one. */
1082
+ ext?: string;
1083
+ }
1084
+ /**
1085
+ * `"(408) 300-9294;ext=123"` → `{ number, ext }`. `null` when there's no number
1086
+ * — including for the empty string, which is how JACS spells a deleted key.
1087
+ */
1088
+ declare function parsePhone(raw: unknown): DestinationPhone | null;
1089
+ /** Alt text for the Location Image, or `undefined` when the author set none. */
1090
+ declare function parseImageAlt(raw: unknown): string | undefined;
1091
+ /** The building name, or `undefined` when the author set none. */
1092
+ declare function parseBuildingName(raw: unknown): string | undefined;
1093
+ /** The point-of-interest flag. Absence means false — only the checked state is
1094
+ * ever stored. */
1095
+ declare function parsePointOfInterest(raw: unknown): boolean;
1096
+
1097
+ /**
1098
+ * Weekly opening hours for a destination.
1099
+ *
1100
+ * JACS has no hours column, so the CMS ships the whole week in a single
1101
+ * destination extensor — key `mm_open_hours`, value a JSON string:
1102
+ *
1103
+ * {"v":1,"days":{"mon":[["09:00","17:30"]],"sat":[],"sun":[["00:00","24:00"]]}}
1104
+ *
1105
+ * - Times are 24-hour `"HH:MM"` and **venue-local**; no timezone is recorded.
1106
+ * - Each day maps to an array of `[open, close]` ranges. The editor exposes one
1107
+ * range per day today, but the array shape leaves room for split shifts (a
1108
+ * cafeteria that closes over the afternoon) and extra ranges round-trip.
1109
+ * - An empty array — or a missing day — means closed.
1110
+ * - `[["00:00","24:00"]]` means open 24 hours.
1111
+ *
1112
+ * This module owns the read half: parse the value (`parseOpenHours`) and derive
1113
+ * the live open/closed state from it (`getOpenStatus`). The write half lives in
1114
+ * the CMS (`map-manager`'s `utils/open-hours.js`) — the two must agree on the
1115
+ * wire shape above, so change them together. The key itself is registered in
1116
+ * `reservedExtensors.ts` alongside the CMS's other stand-in fields, mirroring
1117
+ * the same split on the authoring side.
1118
+ */
1119
+
1120
+ /** Wire-format version this module understands. */
1121
+ declare const OPEN_HOURS_VERSION = 1;
1122
+ type DayKey = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
1123
+ /** Monday-first, matching the wire format's own day order. `getOpenStatus`
1124
+ * reports `OpenHoursTransition.day` as one of these. */
1125
+ declare const DAY_KEYS: readonly DayKey[];
1126
+ /** One open→close stretch within a single day. 24-hour `"HH:MM"`, venue-local.
1127
+ * `close` may be `"24:00"` (end of day); `open` never is. */
1128
+ interface HoursRange {
1129
+ open: string;
1130
+ close: string;
1131
+ }
1132
+ /** A parsed week. Every day is present; a closed day is an empty array. */
1133
+ interface OpenHours {
1134
+ days: Record<DayKey, HoursRange[]>;
1135
+ /** Open every minute of the week (a 24/7 emergency department, say). The
1136
+ * status has no `closesAt` in this case — there is nothing to count down to. */
1137
+ alwaysOpen: boolean;
1138
+ }
1139
+ /** A point in the week the status is counting toward. */
1140
+ interface OpenHoursTransition {
1141
+ /** Day the transition falls on, venue-local. */
1142
+ day: DayKey;
1143
+ /** 24-hour `"HH:MM"`, venue-local. Midnight is always `"00:00"` on the
1144
+ * following day — `"24:00"` never surfaces here. */
1145
+ time: string;
1146
+ /** Whole minutes from `now`. */
1147
+ minutesUntil: number;
1148
+ /** Calendar days ahead: `0` today, `1` tomorrow. */
1149
+ daysAhead: number;
1150
+ }
1151
+ interface OpenStatus {
1152
+ /** Open right now. */
1153
+ open: boolean;
1154
+ /** Open every minute of the week — no closing time to show. */
1155
+ alwaysOpen: boolean;
1156
+ /** Open, and closing within `soonMinutes`. */
1157
+ closingSoon: boolean;
1158
+ /** Closed, and opening within `soonMinutes`. */
1159
+ openingSoon: boolean;
1160
+ /** When open: the end of the current stretch. Absent when `alwaysOpen`. */
1161
+ closesAt?: OpenHoursTransition;
1162
+ /** When closed: the next time it opens. Absent when no day is open. */
1163
+ opensAt?: OpenHoursTransition;
1164
+ }
1165
+ interface OpenStatusOptions {
1166
+ /** How near a transition counts as "soon". Default `30`. */
1167
+ soonMinutes?: number;
1168
+ /** IANA zone the hours are expressed in (`'America/Denver'`). Omit — the
1169
+ * normal case for a kiosk standing in the venue — to read the host clock.
1170
+ * An unrecognised zone falls back to the host clock rather than throwing. */
1171
+ timeZone?: string;
1172
+ }
1173
+ /**
1174
+ * Extensor value → a parsed week, or `null` when there is nothing usable.
1175
+ *
1176
+ * Accepts the stored JSON string or an already-parsed object. Anything
1177
+ * unreadable — absent key, malformed JSON, no open day — is `null` rather than
1178
+ * a throw, so a bad value costs the consumer an omitted section, not a crash.
1179
+ *
1180
+ * An unknown `v` is also `null`, deliberately. A future version could add
1181
+ * holiday overrides or a timezone; reading only the `days` it recognises would
1182
+ * let the SDK announce "Open" on a day the venue marked closed, and a confidently
1183
+ * wrong answer at a kiosk is worse than a missing one.
1184
+ */
1185
+ declare function parseOpenHours(raw: unknown): OpenHours | null;
1186
+ /**
1187
+ * The weekday `now` falls on in `timeZone` (host clock when omitted or
1188
+ * unrecognised) — the same day `getOpenStatus` evaluates against. Exported so
1189
+ * a week table can highlight "today" without disagreeing with the status line
1190
+ * around the venue's midnight.
1191
+ */
1192
+ declare function dayKeyAt(now: Date, timeZone?: string): DayKey;
1193
+ /**
1194
+ * A short label for `timeZone` (host zone when omitted or unrecognised) — the
1195
+ * zone `getOpenStatus` reads hours against — so a UI can say "Opens 9:00 AM ET".
1196
+ *
1197
+ * US zones get the generic form (`ET`, `CT`, `MT`, `PT`, `AKT`), which doesn't
1198
+ * flip with daylight saving. Elsewhere the engine has no generic abbreviation
1199
+ * (it would spell out "Germany Time"), so this falls back to the specific short
1200
+ * name, which may be an offset (`GMT+2`). `now` only matters for that fallback
1201
+ * and for zones like Phoenix that print `MST` year-round.
1202
+ */
1203
+ declare function timeZoneAbbreviation(now?: Date, timeZone?: string): string;
1204
+ /**
1205
+ * The live open/closed state for a parsed week.
1206
+ *
1207
+ * Pure and time-explicit: pass the `now` you want evaluated (default: the
1208
+ * moment of the call). Nothing is cached, so a consumer that wants a status
1209
+ * that stays true — a kiosk card sitting open — re-calls this on a tick rather
1210
+ * than holding the result.
1211
+ *
1212
+ * Hours carry no timezone of their own, so they are read against the host clock
1213
+ * unless `options.timeZone` names the venue's zone. The kiosk case needs no
1214
+ * option: the machine stands in the building it is describing. A web consumer
1215
+ * whose visitors may be elsewhere should resolve the venue's zone (e.g. from
1216
+ * `MinuteMaps.getVenueCenter()`) and pass it.
1217
+ */
1218
+ declare function getOpenStatus(hours: OpenHours, now?: Date, options?: OpenStatusOptions): OpenStatus;
1219
+
1220
+ /** One floor of the active route, drawn on its own for a printout. */
1221
+ type RouteFloorImage = {
1222
+ floor: Floor;
1223
+ /** The bare map canvas (no DOM chrome), as a `data:` URL. */
1224
+ dataUrl: string;
1225
+ };
1226
+ type RouteImageOptions = {
1227
+ /** CSS-pixel size the map is laid out at while capturing. Default 1200×800. */
1228
+ width?: number;
1229
+ height?: number;
1230
+ /** Canvas pixels per CSS pixel. Default 2, crisp enough for paper. */
1231
+ pixelRatio?: number;
1232
+ /** Padding around each floor's route segment, in CSS px. Default 40. */
1233
+ padding?: number;
1234
+ /** Draw in flat (2D) mode, which reads better on paper. The previous mode
1235
+ * is put back afterwards. Default `true`. */
1236
+ flat?: boolean;
1237
+ /** Image MIME type passed to `toDataURL`. Default `'image/png'`. */
1238
+ type?: string;
1239
+ /** Encoder quality for lossy types (`'image/jpeg'`, `'image/webp'`). */
1240
+ quality?: number;
1241
+ };
1242
+
1243
+ type JacsProviderConfig = {
1244
+ mode: 'proxy';
1245
+ proxyBaseUrl?: string;
1246
+ fetchImpl?: typeof fetch;
1247
+ } | {
1248
+ mode: 'direct';
1249
+ host: string;
1250
+ clientId: string;
1251
+ username: string;
1252
+ password: string;
1253
+ fetchImpl?: typeof fetch;
1254
+ };
1255
+ type GetBuildingsArgs = {
1256
+ customerId: number | string;
1257
+ venueId: number | string;
1258
+ };
1259
+ type GetFloorsArgs = {
1260
+ customerId: number | string;
1261
+ venueId: number | string;
1262
+ buildingId: number | string;
1263
+ /** BCP-47 code (`es`, `en-US`). When set, JACS resolves floor `name` /
1264
+ * `description` to that locale instead of the customer's default. */
1265
+ locale?: string;
1266
+ };
1267
+ type GetDestinationsArgs = {
1268
+ customerId: number | string;
1269
+ venueId: number | string;
1270
+ /** See {@link GetFloorsArgs.locale}. */
1271
+ locale?: string;
1272
+ };
1273
+ type GetAmenitiesArgs = {
1274
+ customerId: number | string;
1275
+ /** See {@link GetFloorsArgs.locale}. */
1276
+ locale?: string;
1277
+ };
1278
+ type GetZonesArgs = {
1279
+ customerId: number | string;
1280
+ venueId: number | string;
1281
+ /** See {@link GetFloorsArgs.locale}. */
1282
+ locale?: string;
1283
+ };
1284
+ type GetMapLabelsArgs = {
1285
+ customerId: number | string;
1286
+ venueId: number | string;
1287
+ /** See {@link GetFloorsArgs.locale}. */
1288
+ locale?: string;
1289
+ };
1290
+ type GetVenueArgs = {
1291
+ customerId: number | string;
1292
+ venueId: number | string;
1293
+ };
1294
+ type JacsVenue = Record<string, any>;
1295
+ type GetBuildingFullArgs = {
1296
+ customerId: number | string;
1297
+ venueId: number | string;
1298
+ buildingId: number | string;
1299
+ mapProfile?: string;
1300
+ geojson?: boolean;
1301
+ };
1302
+ type JacsBuildingFull = Record<string, any>;
1303
+ type JacsVenueAll = Record<string, any>;
1304
+ type GetStylesheet3dArgs = {
1305
+ customerId: number | string;
1306
+ venueId: number | string;
1307
+ buildingId: number | string;
1308
+ };
1309
+ type GetFloorMapGeoJsonArgs = {
1310
+ customerId: number | string;
1311
+ venueId: number | string;
1312
+ buildingId: number | string;
1313
+ floorId: number | string;
1314
+ };
1315
+ type GetPolygonLayersArgs = {
1316
+ customerId: number | string;
1317
+ };
1318
+ type GetFloorMapTemplate3dArgs = {
1319
+ customerId: number | string;
1320
+ venueId: number | string;
1321
+ buildingId: number | string;
1322
+ floorId: number | string;
1323
+ };
1324
+ type JacsBuilding = {
1325
+ id?: number | string;
1326
+ buildingId?: number | string;
1327
+ name?: string;
1328
+ isDefault?: boolean;
1329
+ default?: boolean;
1330
+ [key: string]: any;
1331
+ };
1332
+ type JacsFloor = Record<string, any>;
1333
+ type JacsDestination = Record<string, any>;
1334
+ type JacsAmenity = Record<string, any>;
1335
+ type JacsZone = Record<string, any>;
1336
+ type JacsMapLabel = Record<string, any>;
1337
+ type JacsPolygonLayer = Record<string, any>;
1338
+ /**
1339
+ * This matches the “map-template/3d” shape you pasted earlier:
1340
+ * rows keyed by layerName with paint-ish properties like fill, line-color, etc.
1341
+ */
1342
+ type JacsMapTemplate3dRow = {
1343
+ layerName: string;
1344
+ fill?: string | null;
1345
+ 'fill-opacity'?: number | null;
1346
+ fillAntialias?: boolean | null;
1347
+ 'fill-pattern'?: string | null;
1348
+ 'line-color'?: string | null;
1349
+ 'line-width'?: number | null;
1350
+ lineOpacity?: number | null;
1351
+ strokeWidth?: number | null;
1352
+ strokeOpacity?: number | null;
1353
+ 'fill-extrusion-color'?: string | null;
1354
+ 'fill-extrusion-height'?: number | null;
1355
+ 'fill-extrusion-opacity'?: number | null;
1356
+ 'fill-extrusion-base'?: number | null;
1357
+ 'fill-extrusion-pattern'?: string | null;
1358
+ [key: string]: any;
1359
+ };
1360
+ declare class JacsProvider {
1361
+ private mode;
1362
+ private proxyBaseUrl;
1363
+ private direct;
1364
+ private fetchImpl;
1365
+ private tokenCache;
1366
+ constructor(config: JacsProviderConfig);
1367
+ /** `/customer/all` — every customer with its venues, for a venue picker. Each
1368
+ * entry is `{ customer, venues[] }`. Not venue-scoped. */
1369
+ getCustomersAll(): Promise<Record<string, any>[]>;
1370
+ getPolygonLayers(args: GetPolygonLayersArgs): Promise<JacsPolygonLayer[]>;
1371
+ getFloorMapTemplate3d(args: GetFloorMapTemplate3dArgs): Promise<JacsMapTemplate3dRow[]>;
1372
+ getFloors(args: GetFloorsArgs): Promise<JacsFloor[]>;
1373
+ /**
1374
+ * Hits the JEM venue full-call aggregator at /customer/{cid}/venue/{vid}/all.
1375
+ * Unlike /building/{bid}/full this endpoint does NOT go through Redis — JACS
1376
+ * assembles the response server-side from many controllers on every call, so
1377
+ * SDK consumers always see fresh data regardless of cache invalidation state.
1378
+ *
1379
+ * Response shape: { customer, venue, buildings: [{ floors }], waypoints[],
1380
+ * destinations[], amenities[], paths[], mapLabels[], devices[], zones[],
1381
+ * pathTypes[], mapProfiles[] }. Floor geojson is NOT included — the SDK
1382
+ * lazy-loads it via ensureFloorGeojson.
1383
+ */
1384
+ getVenueAll(args: GetVenueArgs): Promise<JacsVenueAll>;
1385
+ getVenue(args: GetVenueArgs): Promise<JacsVenue>;
1386
+ getFloorMapGeojson(args: GetFloorMapGeoJsonArgs): Promise<GeoJSON.FeatureCollection>;
1387
+ getBuildingFull(args: GetBuildingFullArgs): Promise<JacsBuildingFull>;
1388
+ getDestinations(args: GetDestinationsArgs): Promise<JacsDestination[]>;
1389
+ /** Zones (department groupings). `name` / `description` are localized. */
1390
+ getZones(args: GetZonesArgs): Promise<JacsZone[]>;
1391
+ /**
1392
+ * CMS-authored map labels (street / area / wing names). The endpoint is
1393
+ * `/label`, not `/map-label` — the latter 404s. The localized `name` is on
1394
+ * the label template; each `instances[]` placement carries its own internal
1395
+ * `name` ("Parking Lot E - Instance 1") which is authoring metadata and is
1396
+ * never displayed.
1397
+ */
1398
+ getMapLabels(args: GetMapLabelsArgs): Promise<JacsMapLabel[]>;
1399
+ /**
1400
+ * Amenities are **customer-scoped**, not venue-scoped. A
1401
+ * `/customer/{id}/venue/{id}/amenity` path also exists and returns the same
1402
+ * records, but it silently ignores `?locale=` — it answers with the base
1403
+ * (en-US) names for every locale, which reads as "translations aren't
1404
+ * working" rather than as a wrong URL. Only `/customer/{id}/amenity`
1405
+ * resolves the locale. Don't reintroduce the venue segment.
1406
+ */
1407
+ getAmenities(args: GetAmenitiesArgs): Promise<JacsAmenity[]>;
1408
+ getBuildings(args: GetBuildingsArgs): Promise<JacsBuilding[]>;
1409
+ getStylesheet3d(args: GetStylesheet3dArgs): Promise<StyleSpecification>;
1410
+ invalidateToken(): void;
1411
+ getJson<T>(path: string): Promise<T>;
1412
+ private extractArray;
1413
+ private getJwtToken;
1414
+ private normalizeHost;
1415
+ private joinUrl;
1416
+ private safeReadText;
1417
+ }
1418
+
1419
+ /** The JacsProvider methods the SDK's data layer reads. Both JacsProvider (live
1420
+ * JACS) and SnapshotProvider (published snapshots) implement them. */
1421
+ type JacsReader = Pick<JacsProvider, 'getVenueAll' | 'getFloorMapGeojson' | 'getDestinations' | 'getFloors' | 'getZones' | 'getMapLabels' | 'getAmenities' | 'getPolygonLayers' | 'getCustomersAll'>;
1422
+ type SnapshotProviderConfig = {
1423
+ /** Root of the snapshot host, e.g. `https://snapshots.example.com`. */
1424
+ baseUrl: string;
1425
+ fetchImpl?: typeof fetch;
1426
+ };
1427
+ /** Highest manifest `schemaVersion` this SDK can read. */
1428
+ declare const SNAPSHOT_SCHEMA_VERSION = 1;
1429
+ /**
1430
+ * The venue (or its customer) can't be shown from snapshots: it was never
1431
+ * published, or it was withdrawn (unpublished, disabled or deleted). Thrown
1432
+ * from `init()`; check `reason` to show "not published" vs "no longer
1433
+ * available".
1434
+ */
1435
+ declare class SnapshotUnavailableError extends Error {
1436
+ readonly reason: 'not-published' | 'withdrawn';
1437
+ readonly customerId: string;
1438
+ readonly venueId: string | null;
1439
+ constructor(reason: 'not-published' | 'withdrawn', customerId: string, venueId: string | null);
1440
+ }
1441
+ /** The customer and venue snapshot versions a load is using. */
1442
+ type SnapshotVersions = {
1443
+ customer: number;
1444
+ venue: number;
1445
+ };
1446
+ /**
1447
+ * Result of `checkForUpdate`: whether what's published now differs from what
1448
+ * this instance loaded.
1449
+ *
1450
+ * - `current` — nothing newer is published.
1451
+ * - `available` — a newer customer or venue version is published; reload to
1452
+ * show it.
1453
+ * - `unavailable` — the venue (or its customer) has since been withdrawn or
1454
+ * was never published; a reload would fail with `SnapshotUnavailableError`.
1455
+ * - `incompatible` — a newer version is published in a schema this SDK can't
1456
+ * read; don't reload until the SDK is updated.
1457
+ */
1458
+ type SnapshotUpdateCheck = {
1459
+ status: 'current';
1460
+ loaded: SnapshotVersions;
1461
+ } | {
1462
+ status: 'available';
1463
+ loaded: SnapshotVersions;
1464
+ latest: SnapshotVersions;
1465
+ } | {
1466
+ status: 'unavailable';
1467
+ loaded: SnapshotVersions;
1468
+ reason: 'not-published' | 'withdrawn';
1469
+ } | {
1470
+ status: 'incompatible';
1471
+ loaded: SnapshotVersions;
1472
+ schemaVersion: number;
1473
+ };
1474
+ /**
1475
+ * Picks the locale folder to read. Exact (case-insensitive) match first, then a
1476
+ * same-language match (`es` → `es-US`), then `fallback` — so a locale the
1477
+ * customer added after this venue was last published falls back to the default
1478
+ * language until the venue is republished.
1479
+ */
1480
+ declare function resolveSnapshotLocale(requested: string | undefined, available: string[], fallback: string): string;
1481
+ declare class SnapshotProvider implements JacsReader {
1482
+ private baseUrl;
1483
+ private fetchImpl;
1484
+ private snapshots;
1485
+ private files;
1486
+ constructor(config: SnapshotProviderConfig);
1487
+ /** `directory.json` — published, public customers with their venues, in
1488
+ * `/customer/all`'s `{ customer, venues[] }` shape. */
1489
+ getCustomersAll(): Promise<Record<string, any>[]>;
1490
+ /** venue.json with the customer-wide pieces put back where `/all` has them. */
1491
+ getVenueAll(args: {
1492
+ customerId: number | string;
1493
+ venueId: number | string;
1494
+ }): Promise<JacsVenueAll>;
1495
+ getFloorMapGeojson(args: {
1496
+ customerId: number | string;
1497
+ venueId: number | string;
1498
+ floorId: number | string;
1499
+ }): Promise<GeoJSON.FeatureCollection>;
1500
+ getDestinations(args: {
1501
+ customerId: number | string;
1502
+ venueId: number | string;
1503
+ locale?: string;
1504
+ }): Promise<JacsDestination[]>;
1505
+ /** The snapshot holds the first building's floors only — the one the SDK reads. */
1506
+ getFloors(args: {
1507
+ customerId: number | string;
1508
+ venueId: number | string;
1509
+ locale?: string;
1510
+ }): Promise<JacsFloor[]>;
1511
+ getZones(args: {
1512
+ customerId: number | string;
1513
+ venueId: number | string;
1514
+ locale?: string;
1515
+ }): Promise<JacsZone[]>;
1516
+ getMapLabels(args: {
1517
+ customerId: number | string;
1518
+ venueId: number | string;
1519
+ locale?: string;
1520
+ }): Promise<JacsMapLabel[]>;
1521
+ getAmenities(args: {
1522
+ customerId: number | string;
1523
+ locale?: string;
1524
+ }): Promise<JacsAmenity[]>;
1525
+ getPolygonLayers(args: {
1526
+ customerId: number | string;
1527
+ }): Promise<JacsPolygonLayer[]>;
1528
+ /** The manifest versions this instance is reading, once loaded. */
1529
+ getVersions(customerId: number | string, venueId: number | string): Promise<{
1530
+ customer: number;
1531
+ venue: number;
1532
+ }>;
1533
+ /**
1534
+ * Re-reads both manifests (without changing what this instance reads) and
1535
+ * compares them with the versions it loaded. Rejects when a manifest can't
1536
+ * be fetched — a network failure says nothing about what's published, so
1537
+ * try again later.
1538
+ */
1539
+ checkForUpdate(customerId: number | string, venueId: number | string): Promise<SnapshotUpdateCheck>;
1540
+ private venueLocalized;
1541
+ private customerRef;
1542
+ private venueRef;
1543
+ /**
1544
+ * Runs `read` against the pinned snapshots. The publisher keeps old versions
1545
+ * for a while, but a long-running client (a kiosk) can outlive one: if a
1546
+ * pinned version's file is gone, re-read that manifest and retry once on the
1547
+ * current version. Data already loaded from the old version stays as it was;
1548
+ * files fetched from now on come from the new one.
1549
+ */
1550
+ private pinned;
1551
+ /** The current manifest, read once per instance: a loaded venue keeps
1552
+ * reading the version it started with (see `pinned` for when it can't). */
1553
+ private snapshot;
1554
+ /** Versioned files are immutable, so one fetch per file per instance. */
1555
+ private file;
1556
+ private list;
1557
+ private fetchJson;
1558
+ }
1559
+
1560
+ /** A built-in theme name, or `'custom'` when a raw style was passed. */
1561
+ type ActiveThemeName = ThemeName | 'custom';
233
1562
  declare class MinuteMaps {
234
1563
  private config;
235
1564
  private map;
1565
+ /**
1566
+ * Generation token for `init()`. Bumped by every `init()` and by `destroy()`,
1567
+ * so an init suspended on a network await can tell it has been superseded and
1568
+ * bail instead of creating a map nobody holds a reference to.
1569
+ *
1570
+ * This is what makes `destroy()` safe *during* init: `destroy()` only removes
1571
+ * `this.map`, which is still null until step 3, so a teardown that lands
1572
+ * mid-fetch has nothing to clean up — and without this token the init would
1573
+ * resume and build a fully live orphan (WebGL context, render loop, map-level
1574
+ * listeners, YAH pulse) that can never be reached to shut down. React
1575
+ * StrictMode's mount → unmount → mount does exactly this on every dev load.
1576
+ *
1577
+ * A counter, not a sticky `destroyed` flag: `destroy()` → `init()` re-init is
1578
+ * a supported lifecycle (see the manager-class note in CLAUDE.md), so the
1579
+ * instance has to stay usable after teardown.
1580
+ */
1581
+ private initGeneration;
236
1582
  private data;
237
1583
  private wayfindingProvider;
238
1584
  private events;
239
1585
  private floorsApi;
240
1586
  private venue;
241
1587
  private spriteKeys;
242
- private readonly debug;
1588
+ private readonly debugEnabled;
243
1589
  private readonly logger;
1590
+ /**
1591
+ * Console-facing layer inspector — `sdk.debug.hide('labels')`,
1592
+ * `sdk.debug.solo('route')`, `sdk.debug.reset()`. Always present so demo
1593
+ * code can drive it; additionally reachable as `window.mm.debug` when
1594
+ * `options.debug` is on. See `DebugLayers`.
1595
+ */
1596
+ readonly debug: DebugLayers;
1597
+ /** The `window` key `debug` mode installed this instance under, if any. */
1598
+ private debugGlobalKey;
244
1599
  private defaultCamera;
1600
+ /** This venue's opening zoom, captured once after the initial fit resolves.
1601
+ * Anchors the pinned zoom profile's icon/label reveal thresholds (idle
1602
+ * spotlight, active route) to this venue's own opening view rather than an
1603
+ * absolute MapLibre zoom, and the roof fill's fade-in. See `zoomViews`. */
1604
+ private iconZoomBase;
1605
+ /** The current floor's building-label points, as written to
1606
+ * `MAP_LABELS_SOURCE_ID` — what a building-label tap hit-tests against. */
1607
+ private buildingLabelPoints;
1608
+ /** Which layers show at which zoom: the Campus / Building / Kiosk views, or
1609
+ * the pinned profile while the idle spotlight or a route is up. Re-applied
1610
+ * after every `setTheme()` swap, since `setStyle({ diff: false })`
1611
+ * recreates layers from the pristine theme. */
1612
+ private zoomViews;
245
1613
  private viewModes;
1614
+ /** Mirror of `config.options.reducedMotion`, mutable via `setReducedMotion`. */
1615
+ private reducedMotion;
1616
+ /** Whether map labels render in OpenDyslexic, mutable via `setDyslexicFont`.
1617
+ * Re-applied after every `setTheme()` swap, same reason as `iconZoomBase`. */
1618
+ private dyslexicFont;
1619
+ /** Multiplier applied to every theme label's `text-size`, mutable via
1620
+ * `setTextScale`. Re-applied after every `setTheme()` swap. */
1621
+ private textScale;
1622
+ /** Each label layer's unscaled `text-size` and em offsets, captured by
1623
+ * `setTextScale` and keyed `"<layer id>|<property>"`. Cleared on
1624
+ * `setTheme()` — the recreated layers carry new base values. */
1625
+ private baseTextSizes;
1626
+ /** Tracks the active theme name so `setTheme()` is a no-op when re-requested. */
1627
+ private activeThemeName;
1628
+ /** Tracks the active locale so `setLocale()` is a no-op when re-requested.
1629
+ * Seeded from `config.jmap.locale` at construction; `undefined` means
1630
+ * whatever the customer's default locale is on the JACS side. */
1631
+ private currentLocale;
1632
+ /** The destination of the last successfully computed route — kept so
1633
+ * `setLocale()` can rebuild + re-emit `routeReady` with retranslated
1634
+ * step text without recomputing the path. Cleared on route failure. */
1635
+ private lastRouteDestination;
1636
+ /** `setLocationPicking` is on: a tap snaps to a path and emits
1637
+ * `locationPicked` instead of selecting what's under it. */
1638
+ private locationPicking;
1639
+ /** The active route's two ends, for redrawing its approaches after a
1640
+ * location-pick preview borrowed their source. */
1641
+ private routeApproachEnds;
1642
+ /** The running "moved to the nearest path" animation frame, if any. */
1643
+ private pickAnimationFrame;
1644
+ /** Waypoint ids (stringified) of the connector(s) the active transition
1645
+ * step rides — empty unless `setActiveStep` is on a transition. Drives
1646
+ * `applyRouteConnectorHighlight`. */
1647
+ private activeTransitionWaypointIds;
1648
+ /** Set while `captureRouteFloorImages` runs; holds the bounds padding to
1649
+ * put back afterwards (updated by any `setBoundsPadding` in between). */
1650
+ private routeCapture;
246
1651
  private amenityManager;
247
1652
  private wayfinding;
1653
+ private highlightManager;
1654
+ private selectionManager;
1655
+ private restroomGroups;
1656
+ /** Captured unit paint values to restore when the selection clears, or
1657
+ * `null` when no room is currently focused. See `UNIT_DIM_TARGETS`. */
1658
+ private unitDimRestore;
1659
+ /** Captured destination-layer filters to restore when the selection clears,
1660
+ * or `null` when no destination focus is active. See `setDestinationFocus`. */
1661
+ private destFocusRestore;
1662
+ /** The allow-list currently narrowing the destination layers, or `null` when
1663
+ * no focus is active. Kept alongside `destFocusRestore` (which holds the
1664
+ * *previous* filters, not the ids) so `setTheme` can re-apply the same focus
1665
+ * to the new style's layers after a swap discards the old ones. */
1666
+ private destFocusIds;
1667
+ /** The predicate currently narrowing the POI marker layers to a curated set,
1668
+ * or `null` when no spotlight is active. See `setPOISpotlight`. Held as the
1669
+ * predicate, not a resolved id list, so a floor change or locale redraw can
1670
+ * re-resolve it against the POIs now on screen. */
1671
+ private poiSpotlight;
1672
+ /** Layer filters as they were before the spotlight narrowed them, put back
1673
+ * verbatim when it releases. `null` when no spotlight filter is applied. */
1674
+ private poiSpotlightRestore;
1675
+ /** Coordinates of the current tap/Info selection, or `null` when nothing is
1676
+ * selected. `easeToSelection` reads `getBoundsPadding()` at the moment a
1677
+ * selection is made, which is often *before* the consumer's info card has
1678
+ * mounted and been measured into that padding — so the first pan can land
1679
+ * the POI behind the card. Kept here so `setBoundsPadding` can re-nudge the
1680
+ * camera once the consumer measures its now-mounted chrome and reports
1681
+ * fresh insets, the same way it already re-fits an active route. Cleared by
1682
+ * `clearHighlight`. */
1683
+ private selectedCoordinates;
1684
+ /** Zoom floor for the current selection's camera move, or `null` to keep
1685
+ * the current zoom. Set by `selectPOI` and map taps (`selectionZoom()` — a
1686
+ * pick can arrive from the Campus or Building view, where the selected
1687
+ * marker isn't drawn); kept beside
1688
+ * `selectedCoordinates` so a `setBoundsPadding` re-nudge carries the same
1689
+ * zoom instead of cancelling the zoom-in halfway. Cleared with it. */
1690
+ private selectedMinZoom;
1691
+ private youAreHerePulse;
1692
+ /** Theme layers whose features represent a tappable destination/amenity.
1693
+ * A click hit-tests these (see `SelectionManager`) → `poiSelected`. The
1694
+ * "You are here" anchor and the transient highlight layers are excluded;
1695
+ * a POI drawn across the circle/label/icon layers is de-duped downstream
1696
+ * in `resolveClickedPOIs`. */
1697
+ private static readonly CLICKABLE_POI_LAYERS;
1698
+ /** Room-body layers that are also tappable: a click on a `Units` polygon
1699
+ * resolves to the destination pin inside it (see `resolvePOIsFromFeatures`).
1700
+ * Queried alongside the pin layers so tapping the room or its dot behaves
1701
+ * identically. Kept separate from the pin list because these carry no POI
1702
+ * identity of their own — they resolve spatially. */
1703
+ private static readonly CLICKABLE_UNIT_LAYERS;
1704
+ /** The kiosk's own "you are here" pin — icon, directional heading wedge,
1705
+ * and the ring drawn behind them. Deliberately **not** part of
1706
+ * `CLICKABLE_POI_LAYERS`: that list feeds `emitPoiSelected`, which opens
1707
+ * a selection card for a destination/amenity, and this pin isn't one —
1708
+ * it's where the kiosk itself is standing. A tap here means "where am
1709
+ * I", so it gets its own delegated listener emitting `youAreHereClicked`
1710
+ * instead, for a consumer to wire to the same recenter action as its
1711
+ * own recenter control. */
1712
+ private static readonly YOU_ARE_HERE_LAYERS;
1713
+ /** While a room is selected, the other units are recolored to a flat, muted
1714
+ * gray so the raised, full-strength highlight block reads as the focus. We
1715
+ * recolor rather than lower opacity on purpose: translucent extrusions blend
1716
+ * through their neighbours and look murky. The blocks stay fully opaque (and
1717
+ * keep their vertical-gradient shading, so they still read as rooms) — just
1718
+ * colorless. The selected room's own base grays too, but its highlight cap
1719
+ * sits opaque and taller on top, so it still stands out. The original paint
1720
+ * value (the data-driven color expression) is captured at mute time and put
1721
+ * back on `clearHighlight`, so department tinting returns intact. */
1722
+ private static readonly UNIT_DIM_TARGETS;
1723
+ /** Destination marker layers hidden down to just the selected destination
1724
+ * while a room is focused, so the map declutters to the one place in view.
1725
+ * Amenity/connector layers (`poiType: 'amenity'`), the "you are here" anchor,
1726
+ * and room name labels (`map-labels`) are deliberately absent — they stay
1727
+ * visible as wayfinding reference. Restored on `clearHighlight`. */
1728
+ private static readonly DESTINATION_FOCUS_LAYERS;
1729
+ /** Categorical POI layers in the bundled theme that `setPOIFilter`
1730
+ * toggles. Layers absent from this list — `poi-you-are-here-*` and the
1731
+ * `poi-highlight-*` layers — are intentionally unaffected; they're
1732
+ * anchors or transient focus state, not categorical content. */
1733
+ private static readonly POI_CATEGORY_LAYERS;
1734
+ /** Marker layers `setPOISpotlight` narrows. Every categorical POI layer
1735
+ * except `poi-other-amenity-icons`, whose filter the restroom-group manager
1736
+ * and the zoom-range pass rewrite as the camera moves — a spotlight
1737
+ * narrowing would be silently overwritten mid-orbit — plus the per-instance
1738
+ * amenity label, which is a marker in every sense but isn't a category. */
1739
+ private static readonly POI_SPOTLIGHT_LAYERS;
1740
+ private poiVisibleTypes;
1741
+ /** Independent show/hide state for destination dots and name labels, set
1742
+ * by `setDestinationCirclesVisible`/`setDestinationLabelsVisible`. Kept
1743
+ * separate from `poiVisibleTypes` so the two controls compose instead of
1744
+ * clobbering each other — `applyPOIFilter` and `applyDestinationDisplay`
1745
+ * each own one paint/layout axis of the same layers. */
1746
+ private destinationCirclesVisible;
1747
+ private destinationLabelsVisible;
248
1748
  constructor(config: SDKConfig);
249
1749
  init(): Promise<void>;
1750
+ /**
1751
+ * Supply an image the current style has requested but doesn't have.
1752
+ *
1753
+ * Every imperatively-registered image is served from here, because a theme
1754
+ * swap (`setStyle` with `diff: false`) drops all of them and the code that
1755
+ * registered them doesn't run again:
1756
+ *
1757
+ * - the **route arrow** isn't in the sprite sheet at all — it's canvas-drawn
1758
+ * on demand (this was already the case);
1759
+ * - **amenity / destination icons** come back from `iconCache`. Their
1760
+ * registrars run only during `init`, so before this they went missing on
1761
+ * the first theme swap and never returned — the map kept the dots and
1762
+ * labels but lost every badge and logo.
1763
+ *
1764
+ * Serving lazily (rather than re-registering everything on `style.load`)
1765
+ * means only the ids the new style actually asks for are restored, with no
1766
+ * refetch and no re-compositing.
1767
+ */
1768
+ private serveMissingStyleImage;
250
1769
  on(event: string, cb: (e: MapEvent) => void): void;
251
1770
  off(event: string, cb?: (e: MapEvent) => void): void;
252
1771
  addControl(control: IControl, position?: ControlPosition): void;
253
1772
  setView(options: ViewOptions): void;
1773
+ /**
1774
+ * Force a camera move to land instantly while reduced motion is on.
1775
+ *
1776
+ * The single gate every consumer-driven move goes through — zoom
1777
+ * buttons, compass reset, go-home, recenter, route fits — so the one
1778
+ * toggle covers them all instead of each call site remembering to ask.
1779
+ * Returns `opts` untouched when the preference is off, so a caller that
1780
+ * asked for `animate: false` keeps it either way.
1781
+ */
1782
+ private motionGated;
254
1783
  get amenities(): AmenityManager;
255
1784
  resetView(opts?: {
256
1785
  animate?: boolean;
257
1786
  duration?: number;
258
1787
  }): void;
1788
+ /** Whether the "You are here" pin, its name or its ring is drawn under
1789
+ * `point` — the same box the `youAreHereClicked` handler queries, so a tap
1790
+ * that recenters never also selects the room or pins beneath the pin.
1791
+ * Synchronous, and `false` if the query throws: `poi-data` doesn't show the
1792
+ * persistent failure `map/symbolHitTest.ts` works around, only the
1793
+ * one-frame one, and a dropped claim then just lets the tap select. */
1794
+ private isYouAreHereAt;
1795
+ /** The venue marker or building label drawn under `point`, as the point to
1796
+ * zoom in on, or `null`. */
1797
+ private zoomTapTargetAt;
1798
+ /** A venue marker or building label tap: zoom in on that symbol to the Kiosk view,
1799
+ * centered in the area the chrome leaves free (`boundsPadding`). Keeps the
1800
+ * current pitch and bearing. */
1801
+ private zoomToKioskView;
1802
+ /**
1803
+ * Content-aware re-frame: fit an active route if one is displayed,
1804
+ * otherwise fit the current floor's geojson. Unlike `resetView()`, this
1805
+ * preserves the user's pitch and bearing — it only adjusts center/zoom
1806
+ * to bring the scene back into view. Intended use is after the map
1807
+ * container resizes (e.g. a side drawer opens beside the map and the
1808
+ * pane gets narrower), so what matters stays framed without a yank.
1809
+ */
1810
+ refit(opts?: {
1811
+ animate?: boolean;
1812
+ duration?: number;
1813
+ }): void;
1814
+ /**
1815
+ * Update the bounds padding (camera insets) used by route + floor fits.
1816
+ * Consumers measure their on-screen chrome (panels, rails) and pass the
1817
+ * insets so a fit frames the path in the visible, unobscured area rather
1818
+ * than under the UI. Stored; the next fit / `refit()` uses it (clamped so
1819
+ * a fit always lands — see `clampPadding`). While the camera still sits on
1820
+ * the opening view, that view is re-framed with the new insets too (and
1821
+ * becomes `resetView()`'s target) — the opening fit runs before a consumer
1822
+ * can measure chrome that only mounts once the map is ready.
1823
+ */
1824
+ setBoundsPadding(p: BoundsPadding): void;
1825
+ /** Whether the camera is still exactly on the opening view — no pan, zoom,
1826
+ * tilt or rotate since init (or since the last re-frame). */
1827
+ private isAtOpeningView;
1828
+ /** Redo the opening fit (the current floor's footprint, or the venue's
1829
+ * bounds without floor geometry) with the current bounds padding, then the
1830
+ * opening framing policy, and make the result the new `resetView()` target.
1831
+ * Z0 (`iconZoomBase`) is left as captured at init. */
1832
+ private reframeOpeningView;
1833
+ private fitToBounds;
259
1834
  set3dEnabled(enabled: boolean): void;
260
1835
  toggle3d(): void;
261
1836
  getIs3dEnabled(): boolean;
1837
+ /**
1838
+ * (Re-)register per-amenity icons, per-destination logos, and the kiosk
1839
+ * "you are here" icon. Needed both at init and after every `setTheme()`
1840
+ * call — `map.setStyle(..., { diff: false })` tears down and recreates
1841
+ * the whole `Style` object, which drops every image added at runtime via
1842
+ * `map.addImage`. Only `route-arrow` re-registers itself lazily via
1843
+ * `styleimagemissing`; everything else must be re-run explicitly here or
1844
+ * it silently goes blank after a theme swap.
1845
+ *
1846
+ * `themeName === 'high-contrast'` recolors both the amenity badge disc
1847
+ * and the connector (elevator/stairs/escalator) badge disc to
1848
+ * `HIGH_CONTRAST_AMENITY_BADGE_COLOR`, and forces both the regular and
1849
+ * connector glyph to `HIGH_CONTRAST_AMENITY_ICON_COLOR` so neither
1850
+ * washes out against its now-yellow disc and the two match each other.
1851
+ * Ring colors are left untouched. Failures per icon set are logged and
1852
+ * skipped so one bad icon doesn't block the rest.
1853
+ */
1854
+ private registerRuntimeIcons;
1855
+ /**
1856
+ * Set the active map theme by name (`'default'` / `'high-contrast'` /
1857
+ * `'color-blind'`) or by
1858
+ * passing a custom `StyleSpecification`. The current floor, route, POIs
1859
+ * and camera are preserved across the swap — only layer styling changes.
1860
+ *
1861
+ * Used by kiosk consumers to wire the OS-level / popover high-contrast
1862
+ * toggle to the map. See `KIOSK.md` for the pattern.
1863
+ *
1864
+ * Switching between the two built-in themes happens in place (paint
1865
+ * properties plus the sprite — see `swapBuiltinThemeInPlace`); a custom style,
1866
+ * or leaving one, rebuilds the style with `setStyle`. Calls run one at a time
1867
+ * in the order made, so a quick double toggle can't interleave two swaps.
1868
+ */
1869
+ setTheme(theme: ThemeName | StyleSpecification): Promise<void>;
1870
+ /** Tail of the `setTheme` queue. */
1871
+ private themeQueue;
1872
+ private applyTheme;
1873
+ /**
1874
+ * Switch between built-in themes without `setStyle`. They share every layer
1875
+ * and differ only in paint and sprite (high contrast and color-blind are
1876
+ * derived from hybrid — see `themes/`), so this rewrites the paint
1877
+ * properties that differ and points the map at the other sprite sheet.
1878
+ * Sources, filters, zoom ranges, layout overrides (text scale, dyslexic font)
1879
+ * and the selection's destination focus are untouched, so unlike the
1880
+ * `setStyle` path there is nothing to capture and restore.
1881
+ *
1882
+ * Returns `false`, having changed nothing, when the two themes differ in
1883
+ * anything paint can't express; the caller then falls back to `setStyle`.
1884
+ */
1885
+ private swapBuiltinThemeInPlace;
1886
+ /** Room layers whose color a zone palette replaces. The
1887
+ * highlight pair is here so a selected room's raised block keeps its
1888
+ * palette color instead of reverting to the authored one. */
1889
+ private static readonly ZONE_PAINT_TARGETS;
1890
+ /** Zone id → palette fill for `themeName`, over the zones in use (see
1891
+ * `assignZonePaletteColors` for why only those), or `null` for a theme that
1892
+ * draws the authored colors. */
1893
+ private paletteZoneColors;
1894
+ /**
1895
+ * Under a theme with a zone palette (high contrast, color-blind), color each
1896
+ * zoned room from it, keeping the theme's own value for everything else.
1897
+ * Under the default theme, write the theme's own value back: its expression
1898
+ * already tints by the authored `zoneColor`, and an in-place swap away from
1899
+ * color-blind wouldn't reset these layers otherwise, since the two static
1900
+ * themes' unit fills are identical and `diffThemePaint` skips them.
1901
+ *
1902
+ * Writes the room layers' paint, so it must run before `setUnitFocusDim(true)`
1903
+ * captures them — otherwise clearing the selection would hand back the
1904
+ * palette-less value.
1905
+ */
1906
+ private applyZonePaint;
1907
+ /** The color a zone's rooms are drawn in under the active theme: its palette
1908
+ * entry under high contrast or color-blind, its authored color otherwise. */
1909
+ private zoneDisplayColor;
1910
+ /** Currently active theme name (or `'custom'` when a raw style was passed). */
1911
+ getActiveTheme(): ActiveThemeName;
1912
+ /**
1913
+ * Update the SDK's reduced-motion preference at runtime. When true, every
1914
+ * imperative camera move (`setView`, `resetView`, `refit`, recenter, route
1915
+ * fits, selection pans, floor fits) runs with `duration: 0`, and every
1916
+ * looping animation the SDK drives — the route line-draw, the flowing route
1917
+ * arrows, the "You are here" pulse, the selection ring's ping — is stopped.
1918
+ *
1919
+ * Anything already on screen is switched over here, not just on the next
1920
+ * action: a visitor who flips the toggle mid-route is doing it *because* of
1921
+ * the motion they can see, so waiting for the next event would be the one
1922
+ * moment the preference doesn't work.
1923
+ */
1924
+ setReducedMotion(enabled: boolean): void;
1925
+ getReducedMotion(): boolean;
1926
+ /**
1927
+ * Swap every map label (street names, POI names, building labels, "You are
1928
+ * here") between the bundled themes' default font and OpenDyslexic. Mirrors
1929
+ * the DOM-side dyslexic-font accommodation onto the canvas, which CSS can't
1930
+ * reach.
1931
+ *
1932
+ * Goes through MapLibre's local-glyph fallback: `text-font` values are swapped to
1933
+ * fontstack names intentionally absent from the glyph server
1934
+ * (`OpenDyslexic Bold` etc.), so MapLibre rasterizes them from a
1935
+ * page-loaded `@font-face` instead. The consuming app owns loading that
1936
+ * font — the SDK ships no assets — so this is a no-op glyph-wise (labels
1937
+ * fall back to tofu-free default rendering) if the app never declared it.
1938
+ *
1939
+ * Re-applied after `setTheme()`, since `setStyle({ diff: false })`
1940
+ * recreates every layer from the pristine (non-dyslexic) theme JSON.
1941
+ */
1942
+ setDyslexicFont(enabled: boolean): Promise<void>;
1943
+ getDyslexicFont(): boolean;
1944
+ private applyDyslexicFontToLabels;
1945
+ /**
1946
+ * Scale every map label's `text-size` by `scale` (1 = theme default; the
1947
+ * example kiosk passes 1.15 for its "large text" setting). Mirrors the
1948
+ * DOM-side text-size accommodation onto the canvas, which CSS can't reach.
1949
+ *
1950
+ * Multiplies the output values inside each layer's `text-size` (MapLibre
1951
+ * requires `zoom` to stay at the top of the expression, so it can't be
1952
+ * wrapped), so zoom interpolation and data-driven sizing keep working. Re-applied after
1953
+ * `setTheme()`, since `setStyle({ diff: false })` recreates every layer from
1954
+ * the pristine theme JSON. Non-finite or non-positive values reset to 1.
1955
+ *
1956
+ * The em-based `text-offset` / `text-variable-anchor-offset` are divided by
1957
+ * the same factor, so each label keeps its pixel distance from an icon that
1958
+ * doesn't scale (see `scaleTextEmOffset`).
1959
+ */
1960
+ setTextScale(scale: number): void;
1961
+ getTextScale(): number;
1962
+ private applyTextScaleToLabels;
1963
+ /**
1964
+ * Switch the locale used for POI / amenity / destination / floor names at
1965
+ * runtime. The SDK refetches localized names from JACS (per-entity
1966
+ * endpoints — `/all` does not honor `?locale=`), patches the in-memory
1967
+ * venue model in place, then redraws the POI source for the current floor
1968
+ * so map labels render in the new language.
1969
+ *
1970
+ * Structural data (waypoints, coordinates, iconIds) is preserved — only
1971
+ * the localized fields (`name`, `description`) move.
1972
+ *
1973
+ * Emits a `localeChanged` event with the new code after the patch
1974
+ * completes. No-op when the code matches the current locale.
1975
+ *
1976
+ * Throws only on programmer error — a provider without locale support
1977
+ * (only `JacsDataProvider` has it today) or an empty code. A **failed
1978
+ * JACS fetch is not fatal**: the locale still switches and is reported,
1979
+ * it's only the localized names that stay on the prior language. That
1980
+ * split is deliberate — generated directions text ships inside the SDK
1981
+ * and must not be held hostage to a network round-trip.
1982
+ */
1983
+ setLocale(locale: string): Promise<void>;
1984
+ /**
1985
+ * Start fetching the localized names for `locale` in the background so a
1986
+ * later `setLocale(locale)` only has the in-memory patch and redraw left to
1987
+ * do. Call it once after `ready` for each alternate language the UI offers.
1988
+ *
1989
+ * Never throws and never changes the active locale or the map — a provider
1990
+ * without locale support, an empty code or a failed fetch is a silent no-op
1991
+ * (a failed fetch is retried when `setLocale` runs). Safe to call
1992
+ * repeatedly; the fetch for a given code happens once.
1993
+ */
1994
+ prefetchLocale(locale: string): Promise<void>;
1995
+ /**
1996
+ * Whether newer data has been published since this venue loaded, for
1997
+ * long-running displays (kiosks) that should pick up a publish. Snapshot
1998
+ * mode only: it re-reads the venue's and customer's manifests and compares
1999
+ * them with the versions in use; `{ status: 'unsupported' }` in live
2000
+ * (`proxy`/`direct`) modes. A loaded venue never switches versions by
2001
+ * itself — on `available`, reload (ideally while nobody is using the
2002
+ * screen). Rejects when the manifests can't be fetched; try again later.
2003
+ */
2004
+ checkForUpdate(): Promise<SnapshotUpdateCheck | {
2005
+ status: 'unsupported';
2006
+ }>;
2007
+ /** Currently active locale (BCP-47), or `undefined` when no locale has
2008
+ * been set — meaning JACS resolves names to the customer's default. */
2009
+ getActiveLocale(): string | undefined;
262
2010
  setUnits2dEnabled(enabled: boolean): void;
263
2011
  toggleUnits2d(): void;
264
2012
  getIsUnits2dEnabled(): boolean;
265
2013
  setFlatMode(enabled: boolean): void;
266
2014
  toggleFlatMode(): void;
267
2015
  getIsFlatMode(): boolean;
2016
+ /**
2017
+ * Street address of the venue, from its first JACS building's `address`.
2018
+ * The CMS's Edit Venue form saves its Address field there, since it binds a
2019
+ * venue to a single building. `null` before the venue loads or when the
2020
+ * address is blank. In the venue's default language: `/all` ignores
2021
+ * `?locale=` and the locale refetch doesn't cover buildings.
2022
+ */
2023
+ getVenueAddress(): string | null;
2024
+ /**
2025
+ * Description of the venue, from its first JACS building's `description` —
2026
+ * stored there by the CMS's Edit Venue form, like `getVenueAddress()`.
2027
+ * `null` before the venue loads or when the description is blank. Same
2028
+ * locale caveat: always the venue's default language.
2029
+ */
2030
+ getVenueDescription(): string | null;
2031
+ /**
2032
+ * Center of the venue's bounds as `[lng, lat]`. `null` before the venue
2033
+ * loads or when it has no coordinates. JACS records no time zone, so a
2034
+ * consumer whose visitors may be outside the venue's zone resolves one from
2035
+ * this point and passes it to `getOpenStatus` / `dayKeyAt`.
2036
+ */
2037
+ getVenueCenter(): [number, number] | null;
268
2038
  getFloors(): Floor[];
269
2039
  getCurrentFloor(): Floor | null;
270
2040
  getDefaultFloor(): Floor | null;
271
2041
  getDestinations(floor?: Floor): Destination[];
2042
+ getZones(): Zone[];
2043
+ /** The department (zone) a POI belongs to, resolved via its waypoint's
2044
+ * `zoneId` — the only backend link between a location and a department
2045
+ * (see the zone landmine in the package CLAUDE.md). `null` when the POI
2046
+ * has no waypoint, no zone, or the zone id is unknown. */
2047
+ getZoneForPOI(poi: POI): Zone | null;
2048
+ /** Human-readable detail fields for a POI info popover. Only surfaces
2049
+ * fields that exist on the current data — `category`, `floorName`,
2050
+ * `zoneName`, `keywords`, `description`, and `imageUrl` are each omitted
2051
+ * when absent. Keeps the display-field derivation (incl. the zone lookup)
2052
+ * in the SDK so consumers stay thin. */
2053
+ getPOIDetails(poi: POI): {
2054
+ name: string;
2055
+ type: 'amenity' | 'destination' | 'kiosk';
2056
+ category?: string;
2057
+ floorName?: string;
2058
+ floorShortName?: string;
2059
+ zoneName?: string;
2060
+ zoneDescription?: string;
2061
+ /** The colour that tints this room's polygon on the map under the active
2062
+ * theme — the department's authored `Zone.color`, or its high-contrast
2063
+ * palette entry under high contrast (`Zone.displayColor`) — so a consumer
2064
+ * can print a swatch beside the department name and connect the two.
2065
+ * Re-read it on `themeChanged`. Absent when the author never picked one,
2066
+ * which is also when the map leaves the room untinted; treat "no colour"
2067
+ * as "this department isn't colour-coded", not as "fall back to a default
2068
+ * swatch". */
2069
+ zoneColor?: string;
2070
+ keywords?: string[];
2071
+ description?: string;
2072
+ /** CMS-authored extras from the entity's `extensors` bag (opening hours,
2073
+ * days, room, building). Free-form — the consumer decides what to render.
2074
+ * Artwork is not in here; see `imageUrl`. */
2075
+ properties?: Record<string, unknown>;
2076
+ /** The destination's uploaded location image, ready for an `<img src>`.
2077
+ * Destinations only — amenities use sprite glyphs, not photos. Inline SVG
2078
+ * from `/all` comes back as a data URI; otherwise it's the uploaded uri
2079
+ * path. Absent when nothing was uploaded in the CMS. */
2080
+ imageUrl?: string;
2081
+ /** Author-written alt text for `imageUrl` (`mm_image_alt`) — a description
2082
+ * of the place, for screen readers and for anyone the image fails to load
2083
+ * for. Absent when the author wrote none, in which case the image is
2084
+ * decorative and belongs in an `<img alt="">`, not an unlabelled one. */
2085
+ imageAlt?: string;
2086
+ /** Room number — the CMS's "Room Number". Reads the real
2087
+ * `Destination.unitNumber` column, falling back to a legacy hand-typed
2088
+ * `room_number` property for venues authored before that field existed. */
2089
+ roomNumber?: string;
2090
+ /** Which building on the campus this destination is in (`mm_building_name`),
2091
+ * as the author typed it — "200", "Ambulatory Care". Free text, not a
2092
+ * reference to a `Building` record; render it, don't resolve it. Falls back
2093
+ * to the legacy hand-typed `building_name` property, which is unprefixed
2094
+ * and therefore a different key. */
2095
+ buildingName?: string;
2096
+ /** Phone number and internal extension (`mm_phone`). The number is exactly
2097
+ * as the author typed it, not normalised — see `parsePhone`. */
2098
+ phone?: DestinationPhone;
2099
+ /** The CMS-authored opening hours (`mm_open_hours`), parsed. Absent when
2100
+ * the author set none, or the stored value is unreadable.
2101
+ *
2102
+ * Static — it describes the week, not this moment. Pass it to
2103
+ * `getOpenStatus()` for the live open/closed state, and re-call that on a
2104
+ * tick if the UI stays on screen. The raw string also remains in
2105
+ * `properties` for consumers that would rather parse it themselves. */
2106
+ openHours?: OpenHours;
2107
+ };
2108
+ /** Resolve the source Destination/Amenity record backing a rendered POI,
2109
+ * for detail fields (category, localized description) that don't live on
2110
+ * the built POI object. */
2111
+ private getSourceEntity;
272
2112
  getPolygonLayers(): any[];
2113
+ /**
2114
+ * Cross-reference the venue's drawn layers against the customer's allowed
2115
+ * layer list (JACS `GET /customer/{id}/polygon-layer`) and against what the
2116
+ * active theme can actually render.
2117
+ *
2118
+ * Three failure modes, none of which is visible by looking at the map:
2119
+ * a layer drawn under a name the CMS doesn't list (`unauthorized`), a layer
2120
+ * legitimately drawn that no theme rule matches (`unstyled`), and a theme
2121
+ * rule with nothing to draw at this venue (`unused`). Runs automatically
2122
+ * once after `ready` when `options.debug` is on; call it directly —
2123
+ * `mm.validateMapLayers()` — any time.
2124
+ *
2125
+ * Only counts floors whose geojson has loaded. Init preloads the rest in
2126
+ * the background, so a call in the first moments after `ready` may report
2127
+ * fewer `inData` types than a call a second later.
2128
+ */
2129
+ validateMapLayers(): Promise<LayerAudit>;
2130
+ /** Debug-mode auto-run: log findings once, stay silent when clean. */
2131
+ private reportLayerAudit;
273
2132
  getFloorMapTemplate3d(floorId: string | number): any[];
274
2133
  getAllPOIs(floor?: Floor): POI[];
2134
+ /**
2135
+ * Put the map on the kiosk's floor before drawing a route from it.
2136
+ *
2137
+ * **A route from the kiosk always starts on the kiosk's floor**, so that is
2138
+ * the floor the visitor has to be looking at when it appears. The renderer
2139
+ * agrees with that already — `wayfinding` reveals and frames *the active
2140
+ * floor's* slice of the route — which is exactly why this matters: leave the
2141
+ * map where the visitor had wandered to and the route arrives framed on the
2142
+ * wrong leg, or on no leg at all.
2143
+ *
2144
+ * Both halves of that were reported from a real kiosk. Browse up to Level 3,
2145
+ * search, press Directions: if Level 3 isn't on the route the fit falls back
2146
+ * to the union of every floor and nothing reads as a path from here; if it
2147
+ * *is* on the route — because the destination is up there — the visitor is
2148
+ * looking at the last leg of a walk they haven't started, with no indication
2149
+ * that the first one is two floors down.
2150
+ *
2151
+ * Called before the route is computed rather than after it's drawn, so the
2152
+ * camera makes one move instead of fitting the wrong floor and correcting.
2153
+ * No-op when the venue has no kiosk anchor (those routes fail anyway) or the
2154
+ * map is already there.
2155
+ */
2156
+ private showKioskFloorForRoute;
2157
+ /**
2158
+ * The floor the kiosk is physically anchored to — not necessarily the
2159
+ * floor currently displayed (the visitor may have panned the floor
2160
+ * selector to preview a destination on another level, and the SDK opens
2161
+ * on the venue's configured default floor rather than the kiosk's own —
2162
+ * see `FloorManager.selectInitialFloor`). Anchor resolution (routing,
2163
+ * recentering, closest-waypoint) must resolve against this floor, never
2164
+ * `getCurrentFloor()`, or it silently misses the "you are here" POI
2165
+ * whenever the two diverge. `null` when the venue has no kiosk anchor.
2166
+ */
2167
+ getKioskFloor(): Floor | null;
275
2168
  getYouAreHerePOI(floor?: Floor): POI | null;
276
2169
  getYouAreHereCoordinates(floor?: Floor): [number, number] | null;
2170
+ /**
2171
+ * Compass heading the kiosk's physical hardware faces, in degrees
2172
+ * clockwise from north. Sourced from JACS `Device.heading`. `null` when
2173
+ * the venue's device record has no heading set.
2174
+ *
2175
+ * In order to rotate the map so it is oriented towards the user's gaze,
2176
+ * 180 is subtracted from `Device.heading`.
2177
+ *
2178
+ * Drives `recenterOnKiosk({ useHeading: true })` and the directional
2179
+ * cone rendered next to the "You are here" marker.
2180
+ */
2181
+ getKioskHeading(): number | null;
2182
+ /**
2183
+ * Bearing (degrees clockwise from north) the visitor at the kiosk is
2184
+ * FACING — the device heading rotated 180°, since they stand in front
2185
+ * of the screen looking back at it. `null` when the device record
2186
+ * carries no heading, in which case direction-relative wording ("turn
2187
+ * right and walk 40 ft") is suppressed rather than guessed.
2188
+ */
2189
+ private getVisitorFacingBearing;
2190
+ /**
2191
+ * Display name of the JACS device the kiosk is anchored to (e.g.
2192
+ * "Main Lobby Kiosk", "Information Desk Kiosk"). `null` when the venue
2193
+ * has no kiosk anchor or the device record carries no `name`.
2194
+ *
2195
+ * Distinct from `getYouAreHerePOI()?.name`, which is always the
2196
+ * literal "You are here" label rendered on the map. Use this for chrome
2197
+ * that needs the kiosk's identity — e.g. the directions overlay's
2198
+ * "From <kiosk>" header.
2199
+ */
2200
+ getKioskName(): string | null;
2201
+ /**
2202
+ * Distance + walking time from the kiosk's anchor waypoint to a target,
2203
+ * computed over the JACS path graph (the same graph the routing engine
2204
+ * uses). Designed for "Closest: 30 sec walk" subtitles in the consumer
2205
+ * UI — no second graph build, no second Dijkstra implementation.
2206
+ *
2207
+ * Accepts a raw waypoint id, an amenity / destination object (the first
2208
+ * entry of its `waypoints` array is treated as the entry point), or any
2209
+ * `{ id, mapId? }` shape. Returns `null` when the kiosk isn't anchored,
2210
+ * the target waypoint isn't on the graph, or no path resolves.
2211
+ *
2212
+ * Walking speed defaults to 2.953 ft/s (0.9 m/s, average adult walking speed —
2213
+ * PMC2967707). Pass `walkingSpeedFps` to estimate for accessibility (e.g. 2).
2214
+ *
2215
+ * `feet` / `seconds` are the *physical* walk — horizontal legs only, what a
2216
+ * visitor is shown. `effortSeconds` adds the router's charge for each floor
2217
+ * change (the elevator wait) and is what to *rank* candidates by; see
2218
+ * `findClosestByWalkTime`. Pass `avoidStairs` to price the path as a route
2219
+ * with that option would be.
2220
+ */
2221
+ getWalkTimeFromKiosk(target: number | string | {
2222
+ id?: number | string;
2223
+ mapId?: number;
2224
+ } | {
2225
+ waypoints?: Array<number | string | {
2226
+ id?: number | string;
2227
+ }>;
2228
+ }, opts?: {
2229
+ walkingSpeedFps?: number;
2230
+ avoidStairs?: boolean;
2231
+ }): {
2232
+ feet: number;
2233
+ seconds: number;
2234
+ effortSeconds: number;
2235
+ pathNodeCount: number;
2236
+ } | null;
2237
+ /**
2238
+ * Center the camera on the kiosk's "You are here" position. Use this
2239
+ * (not `resetView`) for a chrome "Recenter" button — `resetView` snaps
2240
+ * to the SDK's captured default camera, which may have been a venue
2241
+ * overview rather than the kiosk's spot.
2242
+ *
2243
+ * When `useHeading` is true (default) and the kiosk device has a
2244
+ * `heading` on file, the camera's bearing is rotated so the direction
2245
+ * the kiosk physically faces ends up at the top of the screen. That
2246
+ * way "what the user sees in front of them" matches "what's up on the
2247
+ * map" — the single largest readability win for stressed indoor users.
2248
+ */
2249
+ recenterOnKiosk(opts?: {
2250
+ useHeading?: boolean;
2251
+ zoom?: number;
2252
+ pitch?: number;
2253
+ animate?: boolean;
2254
+ duration?: number;
2255
+ }): void;
2256
+ /**
2257
+ * Find the POI placed on a given waypoint, searching every floor. A
2258
+ * waypoint is a single placement, so this resolves the exact amenity
2259
+ * instance or destination a kiosk routed to — unlike an amenity id, which
2260
+ * is shared by all of its instances. The kiosk's own "You are here" POI is
2261
+ * skipped. Returns `null` when nothing is placed on that waypoint (stale id,
2262
+ * or a floor whose geometry hasn't loaded).
2263
+ */
2264
+ getPOIByWaypointId(waypointId: string | number): POI | null;
277
2265
  searchPOIs(query: string, floor?: Floor): POI[];
2266
+ /**
2267
+ * Search POIs across every floor in the venue. Returns raw matches —
2268
+ * an amenity that exists on multiple floors appears multiple times,
2269
+ * once per instance. Consumers that need one row per logical amenity
2270
+ * should dedupe by id (and, for "nearest", pick the instance closest
2271
+ * to the kiosk via planar distance on `coordinates`).
2272
+ */
2273
+ searchAllPOIs(query: string): POI[];
278
2274
  wayfindBetweenWaypoints(fromWaypoint: any, toWaypoint: any, options?: {
279
2275
  centerMode?: 'none' | 'destination' | 'route';
280
2276
  zoom?: number;
2277
+ /** Hard-filter stairs from the graph. */
2278
+ avoidStairs?: boolean;
281
2279
  }): Promise<any>;
282
2280
  navigateFromKioskToDestination(destination: any): Promise<any>;
2281
+ /**
2282
+ * Route from the kiosk to a specific POI's waypoint. Use this when you
2283
+ * already hold a resolved POI (e.g. from `searchPOIs` / `searchAllPOIs`).
2284
+ * For a venue-wide amenity record with multiple instances, resolve via
2285
+ * `findClosestWaypoint` first — or use `highlightAmenity` instead, if
2286
+ * you want a visual focus rather than a route.
2287
+ */
2288
+ navigateFromKioskToPOI(poi: POI, options?: {
2289
+ avoidStairs?: boolean;
2290
+ }): Promise<any>;
2291
+ /**
2292
+ * Route between two arbitrary POIs — the browser/mobile counterpart of
2293
+ * `navigateFromKioskToPOI`, for visitors who aren't standing at a kiosk
2294
+ * and pick their own starting point. `navigateBetween` with POIs only.
2295
+ */
2296
+ navigateBetweenPOIs(from: POI, to: POI, options?: {
2297
+ avoidStairs?: boolean;
2298
+ }): Promise<any>;
2299
+ /**
2300
+ * Route between two picks, each a POI or a `MapLocation` (a tapped spot
2301
+ * that isn't a POI — see `snapToPath`). A location is snapped to the
2302
+ * closest point along a path on its floor and routed from there, so a
2303
+ * visitor can start mid-corridor with no waypoint authored at that spot.
2304
+ * Same events (`routeReady` / `routeFailed`) and the same
2305
+ * `centerMode: 'route'` framing as the kiosk routes. Switches to the
2306
+ * origin's floor first, because the route starts there.
2307
+ *
2308
+ * Emits `routeFailed` with `origin_unavailable` / `destination_unavailable`
2309
+ * when a POI has no waypoint or a location has no path within reach,
2310
+ * `no_route` when no path connects them.
2311
+ */
2312
+ navigateBetween(from: POI | MapLocation, to: POI | MapLocation, options?: {
2313
+ avoidStairs?: boolean;
2314
+ }): Promise<any>;
2315
+ /**
2316
+ * What a tap on the map stands for as one end of a route — the rule "choose
2317
+ * on map" (`setLocationPicking`) applies, and the one to apply to a stored
2318
+ * tap so it resolves the same way:
2319
+ *
2320
+ * - **Inside a room that holds a destination** → that destination (`POI`),
2321
+ * routed to its entrance. A clinic's examination rooms and the like aren't
2322
+ * somewhere to walk a visitor through, so a tap there never becomes a spot
2323
+ * with an approach (`destinationInRoomAt`: smallest such room, nearest
2324
+ * destination in it).
2325
+ * - **Anywhere else** — an unplotted corridor, outdoors, a room with no
2326
+ * destination → `snapToPath`.
2327
+ *
2328
+ * `null` when the floor is unknown or no path is within reach.
2329
+ */
2330
+ resolveMapTap(location: Omit<MapLocation, 'kind'> | MapLocation): POI | MapLocation | null;
2331
+ /** `destinationInRoomAt` against a floor's own geometry and destinations. */
2332
+ private destinationInRoomAtTap;
2333
+ /**
2334
+ * Move a spot on a floor onto the walking network — where a route from (or
2335
+ * to) it will actually start (or end). The path point chosen is the one
2336
+ * closest **to walk to**, not the closest in a straight line: rooms,
2337
+ * back-of-house areas, stairs, elevators, obstacles and other buildings are
2338
+ * walked around, and a tap inside a room leaves by its door (inferred where
2339
+ * a path crosses the room's outline) rather than through the wall to the
2340
+ * corridor behind it. See `data/wayfinding/approach.ts`.
2341
+ *
2342
+ * Returns a `MapLocation` with `coordinates` on a path (`label` carried
2343
+ * over) and, when the tap was off the network, `tapped` plus — if a
2344
+ * walkable way was found — `approach`, the dotted line to draw. `tapped`
2345
+ * without `approach` means the point was simply moved (a room with no known
2346
+ * door, somewhere nothing walks) — or, with `centered`, moved from the side
2347
+ * of a hallway to its middle. `null` when the floor is unknown or no path
2348
+ * is within `maxDistanceMeters` (default 30 m).
2349
+ *
2350
+ * Planned from `tapped ?? coordinates`, so re-snapping a result (or a stored
2351
+ * tap) gives the same answer. Until the floor's geometry has loaded it's the
2352
+ * plain nearest point. Cross-floor paths are never snapped to.
2353
+ */
2354
+ snapToPath(location: Omit<MapLocation, 'kind'> | MapLocation, opts?: {
2355
+ maxDistanceMeters?: number;
2356
+ }): MapLocation | null;
2357
+ /**
2358
+ * Whether floor `floorId` has walkways outdoors — a path edge outside every
2359
+ * building and room. A multi-storey campus usually draws its outdoor paths
2360
+ * on the ground floor only, so a tap outside on another floor
2361
+ * (`locationPicked`'s `outdoors`) can't reach one: a "choose on map" UI can
2362
+ * point the visitor to a floor where this is `true`. `false` when the floor
2363
+ * is unknown or its geometry hasn't loaded.
2364
+ */
2365
+ hasOutdoorPaths(floorId: Floor['id']): boolean;
2366
+ /**
2367
+ * Turn map taps into location picks — for a "choose on map" control. While
2368
+ * on, a tap selects nothing; it's snapped to the closest point along a path
2369
+ * on the visible floor (`snapToPath`), a ring marks the snapped spot, and
2370
+ * **`locationPicked`** is emitted with `location` (the snapped
2371
+ * `MapLocation`, or `null` when no path was in reach — the ring is taken
2372
+ * down), `coordinates` (the raw tap), `floor`, and `outdoors` (the tap is
2373
+ * outside every building and room — see `hasOutdoorPaths`). Turning it off removes
2374
+ * the ring.
2375
+ */
2376
+ setLocationPicking(enabled: boolean): void;
2377
+ /**
2378
+ * Pin the ends a visitor has chosen for a route that isn't drawn yet — a
2379
+ * route form's start and destination — so each choice reads as saved: the
2380
+ * sprite's `marker-start` on `start`, `marker-destination` on
2381
+ * `destination`. A location tapped off the walking network is pinned where
2382
+ * it was tapped. Shown on the end's own floor only. Replaces the previous
2383
+ * preview; `{}` clears it. Independent of the route — clear it once the
2384
+ * route is drawn, so the route's own markers stand alone.
2385
+ */
2386
+ setRouteEndPreview(ends: RouteEndPreview): void;
2387
+ /**
2388
+ * A pick on the map: the ring on the on-path point, and the approach to it
2389
+ * from the tap. A tap that had to be *moved* (no walkable way) is shown
2390
+ * moving — its marker slides from the tap onto the path before the ring
2391
+ * appears — so the jump reads as deliberate. With reduced motion it's
2392
+ * simply there.
2393
+ */
2394
+ private showPickedLocation;
2395
+ /** A pick that resolved to a destination (`resolveMapTap`): its room
2396
+ * raised and the ring on its pin, as `highlightPOI` locates one. */
2397
+ private showPickedDestination;
2398
+ private stopPickAnimation;
2399
+ /** The floor's path-graph map id: for JACS venues the floor id *is* the
2400
+ * mapId, but read `mapId` first rather than lean on that. */
2401
+ private mapIdForFloorId;
2402
+ /** What the router takes for one end of a route: a POI's waypoint, or a
2403
+ * location's floor + on-path point (spliced in by the provider as a
2404
+ * virtual node). A location must already be through `snapToPath`, so its
2405
+ * `coordinates` are on a path. `null` when it can't be routed. */
2406
+ private routeEndpointWaypoint;
2407
+ /**
2408
+ * Distance + walking time between two arbitrary picks over the path graph —
2409
+ * `getWalkTimeFromKiosk` with a caller-chosen origin. Either end may be a
2410
+ * `MapLocation`, snapped exactly as `navigateBetween` snaps it, and its
2411
+ * `approach` (the walk from the tap to the path) is counted in `feet` /
2412
+ * `seconds` / `effortSeconds`. `null` when a POI has no waypoint on the
2413
+ * graph, a location has no path within reach, or no path resolves.
2414
+ */
2415
+ getWalkTimeBetween(from: POI | MapLocation, target: Parameters<MinuteMaps['getWalkTimeFromKiosk']>[0] | MapLocation, opts?: {
2416
+ walkingSpeedFps?: number;
2417
+ avoidStairs?: boolean;
2418
+ }): {
2419
+ feet: number;
2420
+ seconds: number;
2421
+ effortSeconds: number;
2422
+ pathNodeCount: number;
2423
+ } | null;
2424
+ /** One end of a walk-time query: a POI's waypoint, or a location's
2425
+ * on-path point (already through `snapToPath`). */
2426
+ private walkTimeRef;
2427
+ /** `routeReady`'s destination: `poi` for a place — the exact instance
2428
+ * routed to, so `navigateFromKioskToClosestAmenity`'s pick is visible —
2429
+ * or `location` for a tapped spot. */
2430
+ private routeDestinationEventField;
2431
+ /** Shared tail of the route entry points: path, camera fit, steps, events.
2432
+ * `originPoi` is the start a visitor picked (`navigateBetween`), which
2433
+ * gets a start pin; a kiosk route passes none — "You are here" marks it. */
2434
+ private routeTo;
2435
+ /** Build the landmark / floor-name context the directions module needs.
2436
+ * Pulled out so `setLocale()` or `setActiveStep()` can rebuild it on
2437
+ * demand if we ever surface a locale-aware variant. */
2438
+ /**
2439
+ * Rewrite every source whose features carry venue-authored *text* for a
2440
+ * floor: POI labels and map labels. Both read their names straight out of
2441
+ * the venue model, so anything that mutates that model in place — today
2442
+ * `setLocale`'s translation patch — has to call this or the map keeps
2443
+ * rendering the old strings until the next floor change.
2444
+ *
2445
+ * Map labels ride the same lifecycle as POIs and carry the `allLevels`
2446
+ * flag, which the projector has already honored by duplicating the
2447
+ * instance under every floor's list.
2448
+ */
2449
+ private redrawFloorText;
2450
+ /** The route's directions: `buildDirections` over the path, plus a step
2451
+ * for each tapped end's walk on to / off the network (`withApproachSteps`). */
2452
+ private buildRouteSteps;
2453
+ private buildDirectionsContext;
2454
+ /**
2455
+ * Drive the turn-by-turn UI. Two effects:
2456
+ *
2457
+ * 1. When the step's `floorId` differs from the active floor, switch
2458
+ * floors so the segment for the step's leg becomes visible.
2459
+ * 2. Highlight the step's slice of the route line by writing the
2460
+ * point range to the `route-active-step` source; the bundled
2461
+ * theme's `route-line-active` layer paints it in gold over the
2462
+ * muted base route.
2463
+ *
2464
+ * Transition steps (cross-floor elevator / stair hops) clear the line
2465
+ * highlight — there's no meaningful on-floor segment to paint — and
2466
+ * instead swap the connector(s) the step rides to their highlighted
2467
+ * sprite badge (`CONNECTOR_HIGHLIGHTED_ICON_SUFFIX`).
2468
+ *
2469
+ * `opts.fit` moves the camera too:
2470
+ * - `'step'` frames just this step (zoomed no closer than
2471
+ * `ROUTE_STEP_FIT_MAX_ZOOM`) instead of the floor's whole segment, and
2472
+ * keeps framing it through later re-fits — `setBoundsPadding` as the
2473
+ * consumer's directions card resizes, a floor switch back to the step's
2474
+ * floor — until another fit or a new route.
2475
+ * - `'route'` goes back to the route's own framing (its floor segment),
2476
+ * as when it was first drawn — e.g. a "Restart" on the first step.
2477
+ *
2478
+ * Omitted, the camera is left alone, so a route's first framing stays
2479
+ * the whole route.
2480
+ *
2481
+ * Pass `null` to clear the highlight without changing the floor or
2482
+ * tearing down the route.
2483
+ */
2484
+ setActiveStep(step: WayfindStep | null, opts?: {
2485
+ fit?: 'step' | 'route';
2486
+ }): Promise<void>;
2487
+ /** The coordinates of `step`'s point range, for the camera to frame. */
2488
+ private stepFocusFor;
2489
+ private writeActiveStepHighlight;
2490
+ private clearActiveStepHighlight;
2491
+ private clearRouteConnectorHighlight;
2492
+ /**
2493
+ * Point `ROUTE_CONNECTOR_LAYER_ID`'s `icon-image` at the highlighted badge
2494
+ * for the connector(s) the active transition step rides, and back at the
2495
+ * plain `iconId` otherwise. Imperative like the layer's filter, so it's
2496
+ * re-run from `updateRouteFloorFilter` after a theme swap drops it.
2497
+ *
2498
+ * `coalesce` over `image` falls back to the plain badge when the sprite has
2499
+ * no `-highlighted` copy of an id (a custom CMS connector icon, or a sprite
2500
+ * that predates the variant) rather than drawing nothing.
2501
+ */
2502
+ private applyRouteConnectorHighlight;
2503
+ /**
2504
+ * Draw each floor the active route crosses, in route order, and return
2505
+ * one image per floor — for printing directions.
2506
+ *
2507
+ * Images are the map canvas alone, so the consumer's DOM chrome (panels,
2508
+ * buttons, the floor picker) never appears in them. For the duration the
2509
+ * map container is laid out at a fixed `width` × `height`, the camera
2510
+ * fits each floor's segment of the route with uniform `padding`, and the
2511
+ * line is drawn solid (no draw-on sweep, no step highlight). The map is
2512
+ * drawn flat (2D) unless `flat: false`. Afterwards the container size,
2513
+ * floor, camera, flat/3D mode and padding are put back. Hide the
2514
+ * map behind a loading screen while this runs; the visitor would
2515
+ * otherwise see it jump between floors.
2516
+ *
2517
+ * The step highlight is cleared, not restored: call `setActiveStep`
2518
+ * again with the step your UI is showing. Resolves to `[]` when no route
2519
+ * is displayed.
2520
+ */
2521
+ captureRouteFloorImages(opts?: RouteImageOptions): Promise<RouteFloorImage[]>;
283
2522
  clearRoute(): void;
2523
+ /**
2524
+ * Put the end-of-route marker on `poi`, or take it down (`null`). With
2525
+ * `start`, the route's start also gets the sprite's `marker-start` pin.
2526
+ *
2527
+ * A destination gets a flag in place of its dot; an amenity instance gets an
2528
+ * enlarged copy of its own icon (`map/routeEnd.ts`). Both are drawn from a
2529
+ * source of their own, so this never touches the POI layers — clearing the
2530
+ * route empties the source and the ordinary dot / icon are what remain.
2531
+ *
2532
+ * The routed restroom is also kept out of restroom grouping while the marker
2533
+ * is up; see `RestroomGroupManager.setExcludedKey`.
2534
+ */
2535
+ private setRouteEnd;
2536
+ /**
2537
+ * Highlight a single POI on the map — a pulsing ring — and bring it into
2538
+ * view. Resolves the POI across all floors, switching the active floor if
2539
+ * it lives on another one. Pass a POI / amenity / destination id.
2540
+ */
2541
+ highlightPOI(id: string | number): Promise<void>;
2542
+ /**
2543
+ * Select a POI programmatically — the same end state as tapping it on the
2544
+ * map, from a list or any other chrome.
2545
+ *
2546
+ * Switches the active floor first when the POI lives on another one, raises
2547
+ * its room as a highlighted block, mutes the neighbouring units, declutters
2548
+ * to the selected destination(s), pans it into the unobscured map area and
2549
+ * emits **`poiSelected`** carrying every co-located POI. A consumer that
2550
+ * already renders an info card off that event therefore needs no second code
2551
+ * path: a tap and a list pick arrive the same way.
2552
+ *
2553
+ * This is deliberately *not* `highlightPOI`, which is the search-locate
2554
+ * gesture — it zooms and recenters and draws a pulsing ring, and emits
2555
+ * nothing. Selection is the quieter one: no ring, the raised room is the
2556
+ * indicator, and the current zoom is kept — unless it is further out than
2557
+ * `options.selectionZoom` (default 19.25, just inside the Kiosk view), where
2558
+ * the room or its marker may not be drawn, in which case the camera zooms in
2559
+ * to it. A map tap gets the same floor.
2560
+ *
2561
+ * Accepts a `POI` (preferred — its `type` and `waypointId` pin down the exact
2562
+ * instance, which matters for an amenity that shares one id across every
2563
+ * physical pin) or a bare id.
2564
+ *
2565
+ * **`includeCoLocated: false` emits only the requested POI.** By default the
2566
+ * payload carries everything sharing the room, which is right when the
2567
+ * gesture was a tap — the visitor pointed at a place on a floor plan and the
2568
+ * consumer has to show them what's there. It's wrong when they picked one
2569
+ * entry out of a named list: they have already disambiguated, and handing
2570
+ * back four places would ask them to do it again, on a card that opened
2571
+ * *because* they were specific. The map treatment is identical either way
2572
+ * (same room raised, same neighbours muted, same pan); only the payload —
2573
+ * and so the declutter's allow-list — narrows.
2574
+ *
2575
+ * Returns the selected POIs, `pois[0]` being the requested one; an empty
2576
+ * array if no POI matched, which is also when nothing on the map changes.
2577
+ */
2578
+ selectPOI(target: POI | string | number, options?: {
2579
+ includeCoLocated?: boolean;
2580
+ }): Promise<POI[]>;
2581
+ /** Zoom floor for a tap or `selectPOI` camera move (`options.selectionZoom`),
2582
+ * or `null` to keep the current zoom. The default sits a level past the
2583
+ * Kiosk view's edge: right at `KIOSK_VIEW_MIN_ZOOM` the selected dot and
2584
+ * name are drawn but routinely lose collision to amenities and room names. */
2585
+ private selectionZoom;
2586
+ /** Remove the POI highlight set by `highlightPOI`, un-dim the other units,
2587
+ * and bring back the destination markers hidden by a room selection. */
2588
+ clearHighlight(): void;
2589
+ /** Map a click's raw feature hits to the full, de-duplicated POIs at that
2590
+ * point (see `resolvePOIsFromFeatures`). Injected into `SelectionManager`.
2591
+ * `getAllPOIs()` is the current floor's rendered POIs — the only clickable
2592
+ * ones — and carries each POI's `waypoint`, needed for directions. */
2593
+ private resolveClickedPOIs;
2594
+ /** Emit `poiSelected` for the co-located POIs at a clicked point. Focuses the
2595
+ * selection in place (no recenter — the user already sees where they
2596
+ * tapped): rings the tapped dot and, when the tap landed in a room, raises
2597
+ * that room as a highlighted block so the eye goes to the selected unit.
2598
+ * `clearHighlight` removes both. `poi` mirrors `pois[0]` for single-POI
2599
+ * consumers. */
2600
+ private emitPoiSelected;
2601
+ /**
2602
+ * Build the room-highlight payload for a selection. The colour comes from the
2603
+ * *clicked* feature (the rendered truth), but the geometry comes from the
2604
+ * floor's full source polygon — the clicked feature is from
2605
+ * `queryRenderedFeatures`, whose geometry is clipped to on-screen tiles, so
2606
+ * highlighting it directly would leave any off-screen part of the room grey
2607
+ * (its base extrusion is muted). Falls back to the clipped geometry if the
2608
+ * full polygon can't be found. Returns `null` for a pin with no room.
2609
+ */
2610
+ private unitHighlightFor;
2611
+ /** The full, unclipped `Units` polygon on the current floor that contains
2612
+ * `coord`, straight from the floor source (not the tile-clipped render). */
2613
+ private fullUnitPolygonAt;
2614
+ /**
2615
+ * Hide every destination marker except the given ids (pass `null` to
2616
+ * restore all). Narrows each destination layer's own filter with an id
2617
+ * allow-list; amenity/connector/"you are here" layers are untouched, so they
2618
+ * stay on the map as reference. Captures the original filters once and puts
2619
+ * them back verbatim, so the layers' base rules (poiType, showLabel, iconId)
2620
+ * survive.
2621
+ */
2622
+ private setDestinationFocus;
2623
+ /**
2624
+ * Select every destination inside a tapped cluster bubble, so a consumer's
2625
+ * multi-place card lists them — the same `poiSelected` a tap on stacked pins
2626
+ * emits.
2627
+ *
2628
+ * Unlike `emitPoiSelected` this raises no room and narrows no markers. A
2629
+ * cluster spans rooms, so there is no one unit to raise or dim around, and
2630
+ * the destination focus would filter out the bubble that was just tapped
2631
+ * (its features carry no `id`) and the members hidden inside it with it.
2632
+ * Any earlier selection is cleared first, so a room left raised by the last
2633
+ * tap doesn't outlive this one. `clearHighlight` afterwards is a harmless
2634
+ * no-op for the consumer to call on dismiss.
2635
+ *
2636
+ * The leaves are read back from the cluster source (`getClusterLeaves`: a
2637
+ * promise on MapLibre v5, a callback on v4 — `peerDependencies` allows both)
2638
+ * and resolved against the floor's POIs, since a source feature only carries
2639
+ * enough identity to look the full POI (with its waypoint) back up.
2640
+ */
2641
+ private selectCluster;
2642
+ /** Gentle pan so the selection sits in the centre of the map area MapLibre
2643
+ * frames within `boundsPadding` (which the consumer sets to reserve for its
2644
+ * bottom-docked chrome / info card). A straight `easeTo`, current zoom kept,
2645
+ * so it reads as a nudge, not a recenter — unless `selectedMinZoom` is set
2646
+ * and the camera is further out, in which case it zooms in to that floor. */
2647
+ private easeToSelection;
2648
+ /** Dim every unit's paint (or restore it) so a selected room reads as the
2649
+ * focus. Idempotent: capturing only happens on the first activate, and the
2650
+ * captured originals are put back verbatim on deactivate. */
2651
+ private setUnitFocusDim;
2652
+ /**
2653
+ * Show only the named POI categories on the map. Pass `['amenity']` to
2654
+ * emphasize amenities (e.g. while an Amenities drawer is open) — destination
2655
+ * POIs hide. Call `clearPOIFilter()` to restore the default (all categories).
2656
+ *
2657
+ * The "You are here" marker, the wheelchair-accessibility badge, and any
2658
+ * active highlight are not affected and always render.
2659
+ */
2660
+ setPOIFilter(types: Array<'amenity' | 'destination'>): void;
2661
+ /** Restore the default — every POI category renders. */
2662
+ clearPOIFilter(): void;
2663
+ /**
2664
+ * Narrow the POI markers to a curated set: only POIs for which `match`
2665
+ * returns true keep their icon and label, every other pin on the categorical
2666
+ * marker layers (destinations, entrances, parking, bus, connectors,
2667
+ * information, and the per-instance amenity names) is filtered out. Built for
2668
+ * an attract loop that wants a few chosen places on screen instead of either
2669
+ * every marker or none.
2670
+ *
2671
+ * `match` runs against the current floor's POIs — the same objects
2672
+ * `getAllPOIs()` returns, `instanceName` and `amenityCategory` included — and
2673
+ * is re-run on every floor change, theme swap and locale redraw, so the
2674
+ * spotlight follows the map rather than the floor it was set on. Kiosk
2675
+ * "You are here" is never touched.
2676
+ *
2677
+ * Two things it deliberately leaves alone:
2678
+ * - `poi-other-amenity-icons` and the restroom badge. Their filters are owned
2679
+ * by the restroom-group manager and rewritten as the camera moves, so a
2680
+ * narrowing there would not survive a frame. Hide them with `hideLayers`
2681
+ * if the spotlight should exclude them.
2682
+ * - `setPOIFilter`'s category visibility, which composes: a category the
2683
+ * filter hides stays hidden whatever `match` says.
2684
+ *
2685
+ * Selecting a place (`selectPOI`, a map tap) releases the spotlight. Pass
2686
+ * `null` — or call `clearPOISpotlight()` — to restore every marker.
2687
+ */
2688
+ setPOISpotlight(match: ((poi: POI) => boolean) | null): void;
2689
+ /** Re-apply the zoom profile if the idle spotlight or a route just changed
2690
+ * which one is in force, and refresh what hangs off it. */
2691
+ private syncZoomViews;
2692
+ /** Restore every POI marker narrowed by `setPOISpotlight`. */
2693
+ clearPOISpotlight(): void;
2694
+ private applyPOISpotlight;
2695
+ private applyPOIFilter;
2696
+ /**
2697
+ * Show or hide destination dots, independently of their name labels and of
2698
+ * `setPOIFilter`'s combined `'destination'` category (which still toggles
2699
+ * both together). Affects every destination dot — both the dot+label
2700
+ * marker on `poi-destination-labels` and the dot-only fallback on
2701
+ * `poi-destination-circles` that a `showLabel: false` POI renders on.
2702
+ *
2703
+ * Implemented as `icon-opacity`, not a layer swap: dot and label are
2704
+ * placed as one collision unit on `poi-destination-labels` (`icon-optional:
2705
+ * false` / `text-optional: true`), which is what guarantees a label never
2706
+ * renders without a dot beneath it. Splitting them into independently
2707
+ * *placed* layers would let MapLibre's collision index resolve each half
2708
+ * on its own, so a label could win placement while its dot lost — the
2709
+ * "floating name" bug this theme's combined layer was built to fix (see
2710
+ * `themes.test.ts` :: "a destination is one marker").
2711
+ */
2712
+ setDestinationCirclesVisible(visible: boolean): void;
2713
+ /**
2714
+ * Show or hide destination name labels, independently of their dots. See
2715
+ * `setDestinationCirclesVisible` for why this is an opacity toggle on the
2716
+ * shared marker layer rather than a separate label layer.
2717
+ *
2718
+ * `visible: true` (the default) doesn't mean "always on" — labels still
2719
+ * fade in only once the visitor has zoomed in past the venue's opening
2720
+ * view (`DESTINATION_LABEL_REVEAL_OFFSET` in `iconZoomRanges.ts`), so a
2721
+ * zoomed-out visitor sees dots without the screen filling with names. Pass
2722
+ * `false` to suppress labels outright regardless of zoom.
2723
+ */
2724
+ setDestinationLabelsVisible(visible: boolean): void;
2725
+ private applyDestinationDisplay;
2726
+ /**
2727
+ * Return the waypoint in `waypoints` closest to `from`. Useful for
2728
+ * "route to the nearest X" against a venue-wide amenity record that has
2729
+ * multiple physical instances. Defaults `from` to the kiosk's
2730
+ * "You are here" coordinates.
2731
+ *
2732
+ * Distance is planar Euclidean on lng/lat — sufficient for ordering at
2733
+ * single-venue scale, and avoids a turf dependency on the hot path.
2734
+ */
2735
+ findClosestWaypoint(waypoints: Waypoint[], from?: [number, number]): Waypoint | null;
2736
+ /**
2737
+ * Pick the candidate cheapest to reach from the kiosk — the one rule for
2738
+ * "closest instance of an amenity". `navigateFromKioskToClosestAmenity` and
2739
+ * `highlightAmenity` use it, and a consumer collapsing a venue-wide amenity
2740
+ * to a single search row should too, so a tap and a search result can never
2741
+ * resolve the same amenity to different floors.
2742
+ *
2743
+ * Each candidate is scored by `getWalkTimeFromKiosk().effortSeconds` — walk
2744
+ * time over the JACS path graph **plus the elevator wait** for any floor
2745
+ * change — and a candidate on a *different floor* from the kiosk has that
2746
+ * score multiplied by `options.otherFloorPenalty` (default `1.5`; `1`
2747
+ * disables it). The wait is the physical cost of leaving the floor; the
2748
+ * multiplier is a preference for not making the visitor do it, and only
2749
+ * applies here — it never changes a route.
2750
+ *
2751
+ * `toWaypoint` maps a candidate to the waypoint it stands for (identity for
2752
+ * a `Waypoint[]`, `poi => poi.waypoint` for a `POI[]`). Candidates whose
2753
+ * waypoint is missing or has no coordinates are ignored, as are waypoints
2754
+ * the graph can't reach from the kiosk. If none resolve a cost, falls back
2755
+ * to the planar `findClosestWaypoint`, **restricted to the kiosk's floor
2756
+ * when any candidate is on it** (planar distance is meaningless across
2757
+ * overlaid floors). Returns `null` for no usable candidate. Pass the same
2758
+ * `avoidStairs` the route will use so the pick and the route agree.
2759
+ */
2760
+ findClosestByWalkTime<T>(items: readonly T[], toWaypoint: (item: T) => Waypoint | null | undefined, opts?: {
2761
+ avoidStairs?: boolean;
2762
+ }): T | null;
2763
+ /**
2764
+ * Route from the kiosk to the closest physical instance of an amenity.
2765
+ * Resolves the amenity venue-wide, picks the instance with the smallest
2766
+ * path-graph cost from the kiosk (`findClosestByWalkTime`: walk time plus
2767
+ * the elevator wait, with a preference for the kiosk's floor; falling back
2768
+ * to planar Euclidean when the walk-time graph can't resolve any waypoint), switches the
2769
+ * active floor if needed, and routes via the same plumbing as
2770
+ * `navigateFromKioskToPOI` — so step-by-step directions, the
2771
+ * `routeReady` event, and the active-floor camera fit all "just work."
2772
+ *
2773
+ * Pass `avoidStairs` to hard-filter stairs from the *route* (the SDK's
2774
+ * routing engine reads it per call; the CMS's per-path-type weight
2775
+ * otherwise handles accessibility preference automatically). The same
2776
+ * flag is passed to the closest-instance pick, so an instance reachable
2777
+ * only by stairs is skipped in favour of one the route can actually reach.
2778
+ */
2779
+ navigateFromKioskToClosestAmenity(amenityId: string | number, options?: {
2780
+ avoidStairs?: boolean;
2781
+ }): Promise<any>;
2782
+ /**
2783
+ * Highlight + center on the closest physical instance of an amenity.
2784
+ * The amenity is resolved against `amenities.getDistinct()` (venue-wide,
2785
+ * deduped); the closest waypoint to the kiosk is picked; the active floor
2786
+ * is switched if that instance lives on another one.
2787
+ *
2788
+ * This is the right call from a venue-wide list ("here are all the
2789
+ * bathrooms in the building, take me to the closest one"). For taps on
2790
+ * a specific resolved POI (e.g. a search result that already names one
2791
+ * instance), use `highlightPOI` instead.
2792
+ */
2793
+ highlightAmenity(amenityId: string | number): Promise<void>;
2794
+ /** Which floor owns this waypoint, by matching mapId against the floor's
2795
+ * `mapId` (or `id` as fallback). Used to switch floors when routing /
2796
+ * highlighting hits an instance on a different one. */
2797
+ private findFloorForWaypoint;
284
2798
  getCameraPosition(): CameraState | null;
285
2799
  getMap(): Map | null;
2800
+ /**
2801
+ * Hide one or more layers by id — a public, always-live counterpart to
2802
+ * `debug.hide()` for consumers that want to toggle arbitrary layers
2803
+ * without reaching into the console-facing debug API. Sticky: the
2804
+ * override is re-asserted across floor changes, `setPOIFilter`, view-mode
2805
+ * toggles and theme swaps until `showLayers` undoes it. Ids absent from
2806
+ * the active style are skipped silently. Returns the ids actually found
2807
+ * and hidden.
2808
+ */
2809
+ hideLayers(layerIds: string[]): string[];
2810
+ /**
2811
+ * Undo `hideLayers` for these ids. Each layer returns to whatever
2812
+ * visibility the SDK's own state — current floor, POI filter, view mode —
2813
+ * says it should have; it is **not** forced to `visible`. A layer the SDK
2814
+ * is independently keeping hidden (another floor's layers, an
2815
+ * off-view-mode extrusion) stays hidden rather than being force-revealed.
2816
+ * This is the correct inverse of `hideLayers` (mirrors `debug.restore()` —
2817
+ * see `debugLayers.ts` for why a blanket "show" is usually the wrong
2818
+ * undo). Returns the ids that actually had an override to release.
2819
+ */
2820
+ showLayers(layerIds: string[]): string[];
2821
+ /**
2822
+ * Whether a layer is currently rendering (`visibility !== 'none'`).
2823
+ * `false` for a layer absent from the active style.
2824
+ */
2825
+ isLayerVisible(layerId: string): boolean;
2826
+ /**
2827
+ * Put this instance on `window` so a kiosk running in a browser can be
2828
+ * driven from devtools — `mm.debug.help()`, `mm.getMap()`, `mm.setTheme(…)`.
2829
+ * Debug builds only: nothing is exposed unless `options.debug` is on, and
2830
+ * `options.debugGlobal` renames the key (or `false` opts out entirely).
2831
+ *
2832
+ * Refuses to clobber a key that already holds something that isn't a
2833
+ * MinuteMaps — a kiosk shell with its own `window.mm` shouldn't lose it to
2834
+ * a dev-mode convenience.
2835
+ */
2836
+ private exposeDebugGlobal;
2837
+ /** Drop the `window` handle, but only if it still points at this instance. */
2838
+ private releaseDebugGlobal;
286
2839
  destroy(): void;
287
2840
  setCurrentFloor(floor: Floor): Promise<void>;
2841
+ /**
2842
+ * Filter the route-line / route-halo layers to only render segments
2843
+ * whose `floorId` matches the active floor (or features that carry no
2844
+ * `floorId` at all — the straight-line fallback, which we want visible
2845
+ * on every floor since it has no floor membership to filter against).
2846
+ *
2847
+ * Features are tagged with the JACS pixel `mapId` (that's what flows
2848
+ * through the route point's `mapId`), so the filter compares against
2849
+ * the active floor's `mapId`, NOT its `id` — those are two different
2850
+ * JACS identifiers (`floor.id` is the building-floor record; `mapId`
2851
+ * is the SVG asset). We accept the floor's `id` here for convenience
2852
+ * and resolve to `mapId` via `getFloors()`.
2853
+ *
2854
+ * Applied imperatively rather than baked into the theme JSON so the
2855
+ * filter tracks runtime floor changes without restyling the map.
2856
+ */
2857
+ private updateRouteFloorFilter;
288
2858
  isReady(): boolean;
289
2859
  private setFloorLayerVisibility;
290
2860
  private getBoundsPadding;
2861
+ /**
2862
+ * Shrink per-side padding if the requested insets would leave no room.
2863
+ * `cameraForBounds` / `fitBounds` silently fail ("Map cannot fit within
2864
+ * canvas") when an axis's total padding meets or exceeds that axis — cap
2865
+ * each axis's total at 80% so a fit always has space to land.
2866
+ */
2867
+ private clampPadding;
291
2868
  private getVenueBounds;
2869
+ /**
2870
+ * The polygon `options.hideBasemapPoisNearVenue` hides basemap POIs inside:
2871
+ * the venue's bounds padded outward. `null` when disabled or before the
2872
+ * venue loads.
2873
+ */
2874
+ private getBasemapPoiMask;
2875
+ /**
2876
+ * Resolve `options.maxBounds` into the actual pan/zoom-out cap. `false`
2877
+ * disables it, explicit bounds pass through as-is, and a number (or the
2878
+ * unset default of 3) scales the venue's own bounds outward around its
2879
+ * center — see `scaleBounds`.
2880
+ *
2881
+ * The unset default is also floored at `DEFAULT_MIN_MAX_BOUNDS_SPAN_M` per
2882
+ * side. MapLibre won't zoom out past the point where `maxBounds` fills the
2883
+ * viewport, so a small venue's 3x box would otherwise raise the effective
2884
+ * zoom-out floor above `minZoom` and show far less surrounding map than a
2885
+ * large venue does. An explicit number is a deliberate leash and is left as-is.
2886
+ */
2887
+ private getMaxBounds;
2888
+ /**
2889
+ * Opening-framing policy applied once after the initial floor fit.
2890
+ * - `initialPitch`: tilt to this and re-fit the building footprint so the
2891
+ * kiosk opens on a tilted building instead of a flat top-down plan.
2892
+ * - `minZoomBelowInitialFit`: clamp the map's minZoom to
2893
+ * `(fitZoom − margin)` so visitors can't pull back to the empty region.
2894
+ * Both are no-ops when their option is unset.
2895
+ */
2896
+ private applyInitialFraming;
292
2897
  private applyInitialViewFromVenue;
293
2898
  private loadAndPatchVenueStyle;
294
2899
  private ensureFloorLayersFromStyle;
@@ -296,5 +2901,204 @@ declare class MinuteMaps {
296
2901
  }
297
2902
  declare function createMinuteMapsSDK(config: SDKConfig): MinuteMaps;
298
2903
 
299
- export { MinuteMaps, createMinuteMapsSDK };
300
- export type { Amenity, AmenityWithFloor, Bounds, CameraState, Destination, EventCallback, Floor, FloorMetadata, JMapAuth, JMapConfig, JacsAuth, JacsConfig, MapEvent, POI, POISearchResult, SDKConfig, SDKOptions, ViewOptions, WayfindCenterMode, Waypoint };
2904
+ /** Where a `VenueDirectory` reads from: live JACS (`proxy`/`direct`, which
2905
+ * reads `/customer/all`) or published snapshots (`snapshot`, which reads
2906
+ * `directory.json` — published venues only). */
2907
+ type VenueDirectoryConfig = JacsProviderConfig | ({
2908
+ mode: 'snapshot';
2909
+ } & SnapshotProviderConfig);
2910
+ /** The slice of `/customer/all`'s `customer` object a client acts on. Audit
2911
+ * fields, quotas and SSO config are dropped on purpose. */
2912
+ type Customer = {
2913
+ id: number | string;
2914
+ name: string;
2915
+ /** `null` when JACS has no localized name; use {@link customerDisplayName}. */
2916
+ localizedName: string | null;
2917
+ /** Environment/partition the customer lives in; test customers carry a
2918
+ * different one, which is what the `tenantId` filter keys on. */
2919
+ tenantId: number | null;
2920
+ uuid: string | null;
2921
+ /** BCP-47 code of `defaultLocale.locale`. */
2922
+ defaultLocale: string | null;
2923
+ };
2924
+ /** A venue as listed under a customer — a summary, not the loaded venue model. */
2925
+ type VenueSummary = {
2926
+ id: number | string;
2927
+ customerId: number | string;
2928
+ name: string;
2929
+ /** South-west corner (`bottomLng`, `bottomLat`); `null` if JACS omitted coordinates. */
2930
+ southWest: [number, number] | null;
2931
+ /** North-east corner (`topLng`, `topLat`). */
2932
+ northEast: [number, number] | null;
2933
+ /** JACS's `status` flag. Not filtered here — a picker decides whether to hide
2934
+ * `false` (unpublished) venues. */
2935
+ status: boolean;
2936
+ locked: boolean;
2937
+ uuid: string | null;
2938
+ /** JACS's `externalId`; `null` when the venue has none. */
2939
+ externalId: string | null;
2940
+ /** JACS `extensors` — free-form per-venue key/values (`GO_LIVE`, `MOBILE_APP`,
2941
+ * `KIOSK_LOCATIONS`, analytics ids, …). Values are the raw strings JACS sends
2942
+ * (`KIOSK_LOCATIONS` is a JSON-encoded string); non-scalar values are dropped. */
2943
+ extensors: Record<string, string>;
2944
+ /** UUID of the venue's default map template; `null` when not set. */
2945
+ defaultMapTemplateId: string | null;
2946
+ /** JACS `labelType` enum value; `null` if omitted. */
2947
+ labelType: number | null;
2948
+ /** Whether the venue's map uses marker clustering. */
2949
+ clustering: boolean;
2950
+ /** Whether JACS publishes the venue's edits automatically. */
2951
+ autoPublish: boolean;
2952
+ /** The venue's street address (its first building's `address`); `null` when
2953
+ * blank or unknown. Only snapshot directories carry it — `/customer/all`
2954
+ * has no buildings — so it is always `null` in `proxy`/`direct` mode. Same
2955
+ * value `MinuteMaps.getVenueAddress()` returns once the venue loads. */
2956
+ address: string | null;
2957
+ /** The venue's description, as with `address`. Matches
2958
+ * `MinuteMaps.getVenueDescription()`. */
2959
+ description: string | null;
2960
+ /** Whether the venue has a map to open: `false` when the publisher listed it
2961
+ * with no building, floors or uploaded floor maps, so a picker can show it
2962
+ * without loading it. `null` when the source doesn't say — always in
2963
+ * `proxy`/`direct` mode, and in snapshots published before the field. */
2964
+ mapped: boolean | null;
2965
+ };
2966
+ /** One entry of `/customer/all`: a customer and the venues it owns. Hand a
2967
+ * venue's `customerId` and `id` to the SDK's `jacs`/`venue` config to load it. */
2968
+ type CustomerVenues = {
2969
+ customer: Customer;
2970
+ venues: VenueSummary[];
2971
+ };
2972
+ type ListCustomerVenuesOptions = {
2973
+ /** Keep only customers with this `tenantId`, so test customers living in
2974
+ * other tenants are left out. A customer with no `tenantId` never matches.
2975
+ * Omit to return every customer the proxy exposes. */
2976
+ tenantId?: number;
2977
+ };
2978
+ /** `customer.localizedName` when non-blank, else `customer.name`. */
2979
+ declare function customerDisplayName(customer: Customer): string;
2980
+ /**
2981
+ * Lists the customers and venues a user can pick from — the data behind a
2982
+ * "select your hospital" screen. Unlike the `MinuteMaps` class this isn't
2983
+ * bound to a venue: it runs *before* one is chosen.
2984
+ *
2985
+ * Accepts JACS config (proxy or direct), snapshot config, or an existing
2986
+ * `JacsProvider` / `SnapshotProvider`.
2987
+ */
2988
+ declare class VenueDirectory {
2989
+ private jacs;
2990
+ constructor(config: VenueDirectoryConfig | JacsReader);
2991
+ /** `/customer/all`, optionally narrowed to one `tenantId`. */
2992
+ listCustomerVenues(options?: ListCustomerVenuesOptions): Promise<CustomerVenues[]>;
2993
+ }
2994
+
2995
+ type TransitionStep = Extract<WayfindStep, {
2996
+ type: 'transition';
2997
+ }>;
2998
+ type RouteFloorSection = {
2999
+ /** Floor (JACS `mapId`) this section is on. `null` only for a degenerate
3000
+ * floorless route (straight-line fallback with no `mapId`). */
3001
+ floorId: number | null;
3002
+ /** This floor's steps, each with its index into the original flat array
3003
+ * so the consumer can call `setActiveStep` / highlight by index. */
3004
+ steps: Array<{
3005
+ step: WayfindStep;
3006
+ index: number;
3007
+ }>;
3008
+ /** Index of this section's first step in the original array — the target
3009
+ * for `setActiveStep` when advancing INTO this section. */
3010
+ firstStepIndex: number;
3011
+ /** The transition step that leaves this floor for the next section, if
3012
+ * any. Absent on the final section. Its `text` ("Take the elevator to
3013
+ * Basement") makes a good "continue" button label. */
3014
+ exit?: {
3015
+ step: TransitionStep;
3016
+ index: number;
3017
+ };
3018
+ };
3019
+ /**
3020
+ * Group steps into per-floor sections. Single-floor routes return one
3021
+ * section (so a consumer can keep its whole-route view for that case).
3022
+ */
3023
+ declare function groupStepsIntoFloorSections(steps: WayfindStep[]): RouteFloorSection[];
3024
+ /**
3025
+ * Which section is "active" given the active step index — the last section
3026
+ * whose `firstStepIndex` is at or before `activeStepIndex`. Returns 0 when
3027
+ * nothing matches (e.g. index points at the leading transition, which
3028
+ * shouldn't happen since routes start with a depart step).
3029
+ */
3030
+ declare function activeSectionIndex(sections: RouteFloorSection[], activeStepIndex: number): number;
3031
+
3032
+ /** True when an amenity is a vertical-circulation connector. The provider
3033
+ * stamps `_connector` on every synthetic elevator / stairs / escalator, which
3034
+ * is the reliable signal — `extensors.iconName` is only the *display* glyph
3035
+ * and may be any CMS-assigned icon (we still honour it as a fallback for
3036
+ * amenities authored with a connector icon name directly). */
3037
+ declare function isConnectorAmenity(amenity: AmenityLike): boolean;
3038
+ type LocaleField = {
3039
+ locale?: string;
3040
+ uriPath?: string;
3041
+ };
3042
+ type UriField = {
3043
+ mimeType?: string;
3044
+ resourceType?: string;
3045
+ locales?: LocaleField[];
3046
+ };
3047
+ type AmenityLike = {
3048
+ id: number | string;
3049
+ iconId?: string;
3050
+ uris?: UriField[] | null;
3051
+ /** Inline SVG markup. Used by the SDK for synthetic connector
3052
+ * amenities (elevator / stairs / escalator) where the CMS has no
3053
+ * uploaded icon to fetch. When set, this short-circuits the
3054
+ * `uris[].locales[].uriPath` lookup. */
3055
+ svg?: string;
3056
+ /** Free-form CMS metadata. When `extensors.iconName` names a curated
3057
+ * Material Symbols icon, the SDK renders it from the bundled registry
3058
+ * (no upload, no network) — see `pickMaterialSvg`. */
3059
+ extensors?: Record<string, unknown> | null;
3060
+ /** Set by the JACS provider on synthetic vertical-circulation amenities
3061
+ * (elevator / stairs / escalator built from path types). The reliable
3062
+ * connector signal — `extensors.iconName` is the *display* glyph, which
3063
+ * may be any CMS-assigned icon, not the connector kind. */
3064
+ _connector?: boolean;
3065
+ };
3066
+ type ResolvedIconSource = {
3067
+ /** Map image id to register/reference. Shared across amenities for
3068
+ * connectors (preset) and curated Material icons (`material-<name>`). */
3069
+ iconId: string;
3070
+ /** Source for `composeAmenityBitmap` — exactly one of url / inlineSvg. */
3071
+ source: {
3072
+ url?: string;
3073
+ inlineSvg?: string;
3074
+ };
3075
+ /**
3076
+ * Where the artwork came from. The map doesn't care — it rasterises all
3077
+ * three the same way — but a DOM consumer must, because provenance decides
3078
+ * both trust and styling:
3079
+ *
3080
+ * - `material` — from the SDK's bundled registry. Ours, so it's safe to
3081
+ * inline into the DOM, and safe to recolour (it's a monochrome glyph).
3082
+ * - `inline` — an `svg` string off the CMS record. **Author-supplied
3083
+ * markup**: an inline `<svg>` can carry `<script>`, so render it through
3084
+ * an `<img src="data:…">` (which never executes script), not
3085
+ * `dangerouslySetInnerHTML`.
3086
+ * - `upload` — a URL to a CMS-uploaded file. Also author-supplied, also an
3087
+ * `<img>`; and it arrives in whatever colours the author chose, so don't
3088
+ * assume you can tint it to match a badge.
3089
+ */
3090
+ kind: 'material' | 'inline' | 'upload';
3091
+ };
3092
+ /**
3093
+ * Pure icon-source resolution by precedence — no map/DOM side effects, so it's
3094
+ * unit-testable:
3095
+ * 1. inline svg — explicit inline-SVG override (rare)
3096
+ * 2. extensors.iconName — curated Material icon (bundled, shared bitmap);
3097
+ * also how synthetic connectors render now
3098
+ * 3. uris[].uriPath — legacy / custom-uploaded SVG (fetched)
3099
+ * Returns null when the amenity has no resolvable icon.
3100
+ */
3101
+ declare function resolveAmenityIconSource(amenity: AmenityLike, preferredLocale?: string): ResolvedIconSource | null;
3102
+
3103
+ export { BUILDING_NAME_EXTENSOR_KEY, DAY_KEYS, IMAGE_ALT_EXTENSOR_KEY, MinuteMaps, OPEN_HOURS_EXTENSOR_KEY, OPEN_HOURS_VERSION, PHONE_EXTENSOR_KEY, POINT_OF_INTEREST_EXTENSOR_KEY, RESERVED_EXTENSOR_PREFIX, SNAPSHOT_SCHEMA_VERSION, SnapshotProvider, SnapshotUnavailableError, VenueDirectory, activeSectionIndex, createMinuteMapsSDK, customerDisplayName, dayKeyAt, getOpenStatus, groupStepsIntoFloorSections, isConnectorAmenity, isMapLocation, parseBuildingName, parseImageAlt, parseOpenHours, parsePhone, parsePointOfInterest, resolveAmenityIconSource, resolveSnapshotLocale, timeZoneAbbreviation };
3104
+ export type { Amenity, AmenityBadgeStyle, AmenityWithFloor, Bounds, BoundsPadding, CameraState, Customer, CustomerVenues, DayKey, Destination, DestinationPhone, EventCallback, Floor, FloorMetadata, HoursRange, JMapAuth, JMapConfig, JacsAuth, JacsConfig, JacsProviderConfig, JacsReader, ListCustomerVenuesOptions, MapEvent, MapLocation, OpenHours, OpenHoursTransition, OpenStatus, OpenStatusOptions, POI, POISearchResult, ResolvedIconSource, RouteEndPreview, RouteFloorImage, RouteFloorSection, RouteImageOptions, RoutePoint, RoutingOptions, SDKConfig, SDKOptions, SnapshotProviderConfig, SnapshotUpdateCheck, SnapshotVersions, ThemeName, TransitionStep, VenueDirectoryConfig, VenueSummary, ViewOptions, WayfindCenterMode, WayfindStep, Waypoint, Zone };