@minmaps-dev/mm-web-sdk 1.0.0-rc.34 → 1.0.0-rc.36

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
@@ -14,6 +14,10 @@ interface Floor {
14
14
  mapId?: string | number;
15
15
  /** Display name of the floor */
16
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;
17
21
  /** Floor sequence/order (e.g., -1 for basement, 0 for ground, 1 for first floor) */
18
22
  sequence?: number;
19
23
  /** Associated map data */
@@ -29,6 +33,7 @@ interface Floor {
29
33
  interface FloorMetadata {
30
34
  id: string | number;
31
35
  name: string;
36
+ shortName?: string;
32
37
  sequence?: number;
33
38
  isDefault?: boolean;
34
39
  isActive?: boolean;
@@ -51,12 +56,17 @@ interface POI {
51
56
  floorId: string | number;
52
57
  /** Category/type of amenity (e.g., 'restroom', 'elevator') */
53
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;
54
64
  /** Which amenity icon layer this POI renders in — `'connector'`,
55
- * `'entrance'`, `'parking'`, `'bus'` or `'other'`. Derived from the
56
- * amenity's connector flag, name, type and keywords; emitted as the
57
- * `amenityCategory` feature property the theme filters on. Amenities
58
- * only. See `amenityCategoryFor`. */
59
- amenityCategory?: 'connector' | 'entrance' | 'parking' | 'bus' | 'other';
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';
60
70
  /** Search keywords */
61
71
  keywords?: string[];
62
72
  /** Additional properties */
@@ -70,6 +80,12 @@ interface POI {
70
80
  /** For kiosk POIs only: compass heading the device physically faces, in
71
81
  * degrees clockwise from north. Sourced from JACS `Device.heading`. */
72
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;
73
89
  }
74
90
  /**
75
91
  * Amenity - a specific type of POI (facilities, services)
@@ -104,6 +120,17 @@ interface Amenity {
104
120
  waypoints?: Waypoint[];
105
121
  /** Amenity type/category */
106
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;
107
134
  /** Extended properties */
108
135
  extensors?: Record<string, unknown>;
109
136
  }
@@ -119,6 +146,12 @@ interface Destination {
119
146
  waypoints?: Waypoint[];
120
147
  /** Category/classification */
121
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[];
122
155
  /** Additional properties */
123
156
  properties?: Record<string, unknown>;
124
157
  /** The CMS "Location Image" — a photo or logo for the popover, not a map
@@ -158,6 +191,15 @@ interface Waypoint {
158
191
  * link between a location and its department — read at runtime across the
159
192
  * data provider (see the package CLAUDE.md zone landmine). */
160
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
+ }>;
161
203
  }
162
204
  /**
163
205
  * Amenity enriched with the floor it belongs to
@@ -218,17 +260,16 @@ type WayfindCenterMode = 'none' | 'destination' | 'route';
218
260
  /**
219
261
  * Per-call routing preferences forwarded to the wayfinding provider.
220
262
  *
221
- * - `accessible` — prefer accessible paths; providers should drop or heavily
222
- * penalize edges marked inaccessible (stairs by default, anything else
223
- * the provider's data flags).
224
- * - `avoidStairs` — hard-filter stairs from the graph.
263
+ * - `avoidStairs` — hard-filter stairs from the graph. Accessibility
264
+ * preference otherwise comes from the CMS's per-path-type `weight`
265
+ * (elevators score low, stairs/escalators score high) — see
266
+ * `data/wayfinding/weights.ts` — not from a caller-supplied flag.
225
267
  *
226
268
  * The bundled stub provider doesn't compute routes; consumers wire a real
227
- * provider (see `mm-web-sdk-example/lib/wayfinding`) that reads these
228
- * flags when building edge weights.
269
+ * provider (see `mm-web-sdk-example/lib/wayfinding`) that reads this
270
+ * flag when building edge weights.
229
271
  */
230
272
  type RoutingOptions = {
231
- accessible?: boolean;
232
273
  avoidStairs?: boolean;
233
274
  };
234
275
 
@@ -266,13 +307,18 @@ type StepBase = {
266
307
  floorId: number | null;
267
308
  /** Localized instruction text. */
268
309
  text: string;
269
- /** Total walking distance within this step, in meters (rounded). */
270
- distanceMeters?: number;
310
+ /** Total walking distance within this step, in feet (rounded). */
311
+ distanceFeet?: number;
271
312
  /** Landmark name used for anchoring, if any. */
272
313
  landmark?: string;
273
314
  };
274
315
  type DepartStep = StepBase & {
275
316
  type: 'depart';
317
+ /** Which way the traveller turns before walking off, relative to the
318
+ * way they're facing at the origin. Absent when `originFacingBearing`
319
+ * wasn't supplied (direction unknowable) or when the route heads
320
+ * straight ahead. Consumers use it to pick the step's icon. */
321
+ initialTurn?: 'left' | 'right' | 'around';
276
322
  };
277
323
  type TurnStep = StepBase & {
278
324
  type: 'turn-left' | 'turn-right' | 'u-turn';
@@ -350,6 +396,11 @@ type AmenityBadgeStyle = {
350
396
  ringColor?: string;
351
397
  /** Ring width in logical px. Default `2`. */
352
398
  ringWidth?: number;
399
+ /** Extra inset between the icon and the badge's inner edge, in logical px
400
+ * (applied on all sides). Default `0` — the icon fills ~85% of the disc
401
+ * as before. Raise this if an icon's own artwork reaches close to its
402
+ * viewBox edges and ends up touching the ring. */
403
+ padding?: number;
353
404
  };
354
405
  interface SDKOptions {
355
406
  debug?: boolean;
@@ -387,18 +438,67 @@ interface SDKOptions {
387
438
  * controls the automatic highlight on `routeReady`.
388
439
  */
389
440
  routeStepHighlight?: boolean;
441
+ /**
442
+ * Whether restroom pins that are too close together to draw individually
443
+ * collapse into a single Material `wc` badge. Defaults to `true`.
444
+ *
445
+ * Venues author men's and women's restrooms as two amenities a few metres
446
+ * apart. Both are wayfinding-critical (rank 0) and both draw on a layer that
447
+ * doesn't allow icon overlap, so without this MapLibre resolves the collision
448
+ * by hiding one of them outright until the camera is very close in. Grouping
449
+ * shows one badge in their place and hands back *all* the members when it's
450
+ * tapped, so the consumer can offer directions to each.
451
+ *
452
+ * Set `false` to opt out and get the raw collision behaviour back.
453
+ */
454
+ groupRestroomIcons?: boolean;
390
455
  initialFloor?: string | number;
391
456
  enableInteractions?: boolean;
392
457
  customSprite?: string;
393
458
  minIndoorZoom?: number;
394
459
  /**
395
- * Interior unit-wall thickness in feet (default 1). Each wall is built by
396
- * insetting a ring into each unit by `wallThickness / 2`, so adjacent units
397
- * share a wall (their rings abut at the common edge). Floored at 0.5 — a
398
- * thinner value collapses the negative buffer and drops the wall.
460
+ * How far, in **metres** (default `0.3`, ≈1 ft), extruded interior polygons
461
+ * (restrooms, back-of-house, obstacles, connectors) are inset so their edges
462
+ * don't z-fight the neighbouring room block. A polygon too thin to survive
463
+ * the inset (it would keep under half its area, e.g. a ~0.3 m back-of-house
464
+ * perimeter band) is left at its original size rather than erased. Metres to match the theme's
465
+ * `fill-extrusion-height` values, which MapLibre defines in metres — unlike
466
+ * the distances shown to visitors, which are in feet. Was feet before this
467
+ * option changed units; a value of `1` is now ~3.3× thicker.
399
468
  */
400
469
  wallThickness?: number;
470
+ /**
471
+ * How much costlier a candidate on a **different floor** from the kiosk is
472
+ * treated when choosing the closest instance of an amenity (default `1.5`).
473
+ * Used by `findClosestByWalkTime`, and so by `navigateFromKioskToClosestAmenity`
474
+ * and `highlightAmenity`.
475
+ *
476
+ * A multiplier on the trip's effort — walk time plus the elevator wait — not
477
+ * a distance: `1.5` means another floor's instance has to be a third cheaper
478
+ * to beat one on the kiosk's floor. It is a *preference* for not making the
479
+ * visitor change floors, layered on top of the physical cost of doing so
480
+ * (which is already in the effort). `1` turns it off and ranks by effort
481
+ * alone; a value below `1` is treated as `1`. It only affects which instance
482
+ * is chosen — never a route.
483
+ *
484
+ * For scale: at the Las Vegas VA the nearest other-floor Lactation Pod costs
485
+ * ~400 ft-equivalent against ~530 for the one on the kiosk's floor, so it
486
+ * takes a value above ~1.32 to keep the visitor on their floor.
487
+ */
488
+ otherFloorPenalty?: number;
401
489
  boundsPadding?: BoundsPadding;
490
+ /**
491
+ * Caps how far a visitor can pan/zoom out. By default the SDK derives this
492
+ * from the venue's own bounds, scaled to 3x its width/height around the
493
+ * same center — enough room to pan around the building without drifting
494
+ * into an empty, un-tiled region. The default box is never smaller than
495
+ * ~3.7 km a side, so a small venue isn't forced to zoom in tighter than a
496
+ * large one. Pass a number to use a different
497
+ * multiplier (e.g. `1.5` for a tighter leash), explicit `[[west, south],
498
+ * [east, north]]` bounds to override entirely, or `false` to disable
499
+ * max-bounds clamping.
500
+ */
501
+ maxBounds?: Bounds | number | false;
402
502
  /**
403
503
  * Pitch (deg) for the opening view, applied centred on the kiosk ("You
404
504
  * are here"). Omit for top-down.
@@ -419,6 +519,16 @@ interface SDKOptions {
419
519
  * massing/region. `0` locks zoom-out exactly to the opening view.
420
520
  */
421
521
  minZoomBelowInitialFit?: number;
522
+ /**
523
+ * Zoom-out floor expressed as ground scale: the most metres one CSS pixel
524
+ * may span. The SDK converts it to a `minZoom` at the venue's latitude, so
525
+ * every venue shows the same amount of surrounding map when fully zoomed
526
+ * out — a plain zoom number doesn't, because Web Mercator shows less ground
527
+ * per zoom level the further a venue is from the equator. Takes precedence
528
+ * over `minZoomBelowInitialFit`. For scale, `1.207` is zoom 15.8 at Orlando
529
+ * (about 2.3 km x 1.3 km on a 1920x1080 screen).
530
+ */
531
+ minZoomMetersPerPixel?: number;
422
532
  /**
423
533
  * Override the colour the SDK recolours every amenity SVG to before it
424
534
  * composites the badge. `none` / `transparent` fills are preserved so
@@ -449,6 +559,21 @@ interface SDKOptions {
449
559
  * unless this is set explicitly.
450
560
  */
451
561
  connectorBadge?: AmenityBadgeStyle | false;
562
+ /**
563
+ * Badge for information-desk amenities (curated `information`/`info`
564
+ * icon). These render as their own visual class — a green disc with a
565
+ * white glyph by default — so information reads distinct from the gold
566
+ * service amenities. Pass `false` to fall them back into the regular gold
567
+ * badge, or an object to tune the disc / ring. When `amenityBadge` is
568
+ * `false`, information icons are badge-less too unless this is set
569
+ * explicitly.
570
+ */
571
+ informationBadge?: AmenityBadgeStyle | false;
572
+ /**
573
+ * Glyph colour for information icons on their badge. Default white
574
+ * (matches `informationBadge`'s default green disc).
575
+ */
576
+ informationIconColor?: string;
452
577
  styleMode?: 'venueStyleUrl' | 'sdkTemplate';
453
578
  templateOverrideMode?: 'colorsOnly' | 'colorsAndConstants' | 'all';
454
579
  }
@@ -748,8 +873,13 @@ declare class AmenityManager {
748
873
  * three times in a venue list); this collapses those into a single record
749
874
  * with all its waypoints merged across floors.
750
875
  *
751
- * Also folds any duplicate-id records from the provider (a CMS data bug)
752
- * into a single entry so React lists don't trip on dup keys.
876
+ * Also folds any duplicate-id records from the provider into a single entry
877
+ * so React lists don't trip on dup keys. This used to say those duplicates
878
+ * were "a CMS data bug" — they weren't. JACS returns one record per amenity;
879
+ * the SDK's own floor index pushed it once per waypoint, so an amenity with
880
+ * four placements on a floor came back four times. Fixed at the source in
881
+ * `jacsDataProvider.floorIdsForWaypoints`; the fold stays as a cheap guard
882
+ * for a provider that genuinely repeats an id.
753
883
  *
754
884
  * Use this for venue-wide browse UIs; pair it with `findClosestWaypoint`
755
885
  * (on the SDK) to resolve a tap to the nearest physical instance.
@@ -761,6 +891,175 @@ declare class AmenityManager {
761
891
  logKioskForFloor(floorId: Floor['id']): void;
762
892
  }
763
893
 
894
+ /**
895
+ * The `mm_`-prefixed extensor keys the CMS reserves for fields JACS has no
896
+ * column for yet.
897
+ *
898
+ * This file is the read half of a cross-repo table: the write half is
899
+ * `cms/packages/map-manager/app/utils/reserved-extensors.js`, which owns the key
900
+ * names, the value encodings, and the rule that a default value is deleted
901
+ * rather than stored. Change them together.
902
+ *
903
+ * The `mm_` prefix is load-bearing on both sides. Authors can type anything into
904
+ * the CMS's free-form Properties editor, so an unprefixed key like
905
+ * `point_of_interest` could collide with a hand-entered one; the prefix also
906
+ * makes "is this key reserved?" a prefix scan rather than a hand-maintained list
907
+ * once JACS grows real columns and these are migrated away.
908
+ *
909
+ * Two things that look like they belong here and don't:
910
+ *
911
+ * - **Room number.** The CMS labels it "Room Number", but it writes the real
912
+ * `Destination.unitNumber` field, not an extensor. `getPOIDetails().roomNumber`
913
+ * reads that field (see `sdk.ts`).
914
+ * - **`mm_point_of_interest`.** Registered by the CMS and parsed here for
915
+ * completeness, but deliberately not surfaced on `getPOIDetails` — nothing has
916
+ * pinned down whether an unflagged destination should drop out of search, out
917
+ * of the map, or neither, and guessing wrong is worse than omitting it.
918
+ */
919
+ declare const RESERVED_EXTENSOR_PREFIX = "mm_";
920
+ /** Weekly opening hours. Value shape and parser live in `openHours.ts`. */
921
+ declare const OPEN_HOURS_EXTENSOR_KEY = "mm_open_hours";
922
+ /** Phone number, with the extension folded into the same value. */
923
+ declare const PHONE_EXTENSOR_KEY = "mm_phone";
924
+ /** Alt text for the destination's uploaded Location Image. */
925
+ declare const IMAGE_ALT_EXTENSOR_KEY = "mm_image_alt";
926
+ /** Author-set "this is a point of interest" flag. Parsed, not surfaced. */
927
+ declare const POINT_OF_INTEREST_EXTENSOR_KEY = "mm_point_of_interest";
928
+ /**
929
+ * Which building on the campus the destination stands in.
930
+ *
931
+ * The venue's own `Building` record can't answer this: the CMS binds a venue to
932
+ * a single building and hangs the floor list off it, so a campus with a dozen
933
+ * numbered buildings has one `Building` and no way to say which one a room is
934
+ * in. The author types it as free text, and that text is what a kiosk prints —
935
+ * it is not a reference to anything, so don't try to resolve it against
936
+ * `venue.buildings`.
937
+ */
938
+ declare const BUILDING_NAME_EXTENSOR_KEY = "mm_building_name";
939
+ interface DestinationPhone {
940
+ /** Exactly as the author typed it — `"(408) 300-9294"`. Deliberately not
941
+ * normalised: doing that properly across international formats needs a real
942
+ * phone library, and the CMS makes the same choice on the way in. */
943
+ number: string;
944
+ /** Internal extension digits, when there is one. */
945
+ ext?: string;
946
+ }
947
+ /**
948
+ * `"(408) 300-9294;ext=123"` → `{ number, ext }`. `null` when there's no number
949
+ * — including for the empty string, which is how JACS spells a deleted key.
950
+ */
951
+ declare function parsePhone(raw: unknown): DestinationPhone | null;
952
+ /** Alt text for the Location Image, or `undefined` when the author set none. */
953
+ declare function parseImageAlt(raw: unknown): string | undefined;
954
+ /** The building name, or `undefined` when the author set none. */
955
+ declare function parseBuildingName(raw: unknown): string | undefined;
956
+ /** The point-of-interest flag. Absence means false — only the checked state is
957
+ * ever stored. */
958
+ declare function parsePointOfInterest(raw: unknown): boolean;
959
+
960
+ /**
961
+ * Weekly opening hours for a destination.
962
+ *
963
+ * JACS has no hours column, so the CMS ships the whole week in a single
964
+ * destination extensor — key `mm_open_hours`, value a JSON string:
965
+ *
966
+ * {"v":1,"days":{"mon":[["09:00","17:30"]],"sat":[],"sun":[["00:00","24:00"]]}}
967
+ *
968
+ * - Times are 24-hour `"HH:MM"` and **venue-local**; no timezone is recorded.
969
+ * - Each day maps to an array of `[open, close]` ranges. The editor exposes one
970
+ * range per day today, but the array shape leaves room for split shifts (a
971
+ * cafeteria that closes over the afternoon) and extra ranges round-trip.
972
+ * - An empty array — or a missing day — means closed.
973
+ * - `[["00:00","24:00"]]` means open 24 hours.
974
+ *
975
+ * This module owns the read half: parse the value (`parseOpenHours`) and derive
976
+ * the live open/closed state from it (`getOpenStatus`). The write half lives in
977
+ * the CMS (`map-manager`'s `utils/open-hours.js`) — the two must agree on the
978
+ * wire shape above, so change them together. The key itself is registered in
979
+ * `reservedExtensors.ts` alongside the CMS's other stand-in fields, mirroring
980
+ * the same split on the authoring side.
981
+ */
982
+
983
+ /** Wire-format version this module understands. */
984
+ declare const OPEN_HOURS_VERSION = 1;
985
+ type DayKey = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
986
+ /** Monday-first, matching the wire format's own day order. `getOpenStatus`
987
+ * reports `OpenHoursTransition.day` as one of these. */
988
+ declare const DAY_KEYS: readonly DayKey[];
989
+ /** One open→close stretch within a single day. 24-hour `"HH:MM"`, venue-local.
990
+ * `close` may be `"24:00"` (end of day); `open` never is. */
991
+ interface HoursRange {
992
+ open: string;
993
+ close: string;
994
+ }
995
+ /** A parsed week. Every day is present; a closed day is an empty array. */
996
+ interface OpenHours {
997
+ days: Record<DayKey, HoursRange[]>;
998
+ /** Open every minute of the week (a 24/7 emergency department, say). The
999
+ * status has no `closesAt` in this case — there is nothing to count down to. */
1000
+ alwaysOpen: boolean;
1001
+ }
1002
+ /** A point in the week the status is counting toward. */
1003
+ interface OpenHoursTransition {
1004
+ /** Day the transition falls on, venue-local. */
1005
+ day: DayKey;
1006
+ /** 24-hour `"HH:MM"`, venue-local. Midnight is always `"00:00"` on the
1007
+ * following day — `"24:00"` never surfaces here. */
1008
+ time: string;
1009
+ /** Whole minutes from `now`. */
1010
+ minutesUntil: number;
1011
+ /** Calendar days ahead: `0` today, `1` tomorrow. */
1012
+ daysAhead: number;
1013
+ }
1014
+ interface OpenStatus {
1015
+ /** Open right now. */
1016
+ open: boolean;
1017
+ /** Open every minute of the week — no closing time to show. */
1018
+ alwaysOpen: boolean;
1019
+ /** Open, and closing within `soonMinutes`. */
1020
+ closingSoon: boolean;
1021
+ /** Closed, and opening within `soonMinutes`. */
1022
+ openingSoon: boolean;
1023
+ /** When open: the end of the current stretch. Absent when `alwaysOpen`. */
1024
+ closesAt?: OpenHoursTransition;
1025
+ /** When closed: the next time it opens. Absent when no day is open. */
1026
+ opensAt?: OpenHoursTransition;
1027
+ }
1028
+ interface OpenStatusOptions {
1029
+ /** How near a transition counts as "soon". Default `30`. */
1030
+ soonMinutes?: number;
1031
+ /** IANA zone the hours are expressed in (`'America/Denver'`). Omit — the
1032
+ * normal case for a kiosk standing in the venue — to read the host clock.
1033
+ * An unrecognised zone falls back to the host clock rather than throwing. */
1034
+ timeZone?: string;
1035
+ }
1036
+ /**
1037
+ * Extensor value → a parsed week, or `null` when there is nothing usable.
1038
+ *
1039
+ * Accepts the stored JSON string or an already-parsed object. Anything
1040
+ * unreadable — absent key, malformed JSON, no open day — is `null` rather than
1041
+ * a throw, so a bad value costs the consumer an omitted section, not a crash.
1042
+ *
1043
+ * An unknown `v` is also `null`, deliberately. A future version could add
1044
+ * holiday overrides or a timezone; reading only the `days` it recognises would
1045
+ * let the SDK announce "Open" on a day the venue marked closed, and a confidently
1046
+ * wrong answer at a kiosk is worse than a missing one.
1047
+ */
1048
+ declare function parseOpenHours(raw: unknown): OpenHours | null;
1049
+ /**
1050
+ * The live open/closed state for a parsed week.
1051
+ *
1052
+ * Pure and time-explicit: pass the `now` you want evaluated (default: the
1053
+ * moment of the call). Nothing is cached, so a consumer that wants a status
1054
+ * that stays true — a kiosk card sitting open — re-calls this on a tick rather
1055
+ * than holding the result.
1056
+ *
1057
+ * Hours carry no timezone of their own, so they are read against the host clock
1058
+ * unless `options.timeZone` names the venue's zone. The kiosk case needs no
1059
+ * option: the machine stands in the building it is describing.
1060
+ */
1061
+ declare function getOpenStatus(hours: OpenHours, now?: Date, options?: OpenStatusOptions): OpenStatus;
1062
+
764
1063
  declare class MinuteMaps {
765
1064
  private config;
766
1065
  private map;
@@ -799,19 +1098,37 @@ declare class MinuteMaps {
799
1098
  /** The `window` key `debug` mode installed this instance under, if any. */
800
1099
  private debugGlobalKey;
801
1100
  private defaultCamera;
1101
+ /** This venue's opening zoom, captured once after the initial fit resolves.
1102
+ * Anchors the pinned zoom profile's icon/label reveal thresholds (idle
1103
+ * spotlight, active route) to this venue's own opening view rather than an
1104
+ * absolute MapLibre zoom, and the roof fill's fade-in. See `zoomViews`. */
1105
+ private iconZoomBase;
1106
+ /** Which layers show at which zoom: the Campus / Building / Kiosk views, or
1107
+ * the pinned profile while the idle spotlight or a route is up. Re-applied
1108
+ * after every `setTheme()` swap, since `setStyle({ diff: false })`
1109
+ * recreates layers from the pristine theme. */
1110
+ private zoomViews;
802
1111
  private viewModes;
803
1112
  /** Mirror of `config.options.reducedMotion`, mutable via `setReducedMotion`. */
804
1113
  private reducedMotion;
1114
+ /** Whether map labels render in OpenDyslexic, mutable via `setDyslexicFont`.
1115
+ * Re-applied after every `setTheme()` swap, same reason as `iconZoomBase`. */
1116
+ private dyslexicFont;
805
1117
  /** Tracks the active theme name so `setTheme()` is a no-op when re-requested. */
806
1118
  private activeThemeName;
807
1119
  /** Tracks the active locale so `setLocale()` is a no-op when re-requested.
808
1120
  * Seeded from `config.jmap.locale` at construction; `undefined` means
809
1121
  * whatever the customer's default locale is on the JACS side. */
810
1122
  private currentLocale;
1123
+ /** The destination of the last successfully computed route — kept so
1124
+ * `setLocale()` can rebuild + re-emit `routeReady` with retranslated
1125
+ * step text without recomputing the path. Cleared on route failure. */
1126
+ private lastRouteDestinationPoi;
811
1127
  private amenityManager;
812
1128
  private wayfinding;
813
1129
  private highlightManager;
814
1130
  private selectionManager;
1131
+ private restroomGroups;
815
1132
  /** Captured unit paint values to restore when the selection clears, or
816
1133
  * `null` when no room is currently focused. See `UNIT_DIM_TARGETS`. */
817
1134
  private unitDimRestore;
@@ -823,6 +1140,23 @@ declare class MinuteMaps {
823
1140
  * *previous* filters, not the ids) so `setTheme` can re-apply the same focus
824
1141
  * to the new style's layers after a swap discards the old ones. */
825
1142
  private destFocusIds;
1143
+ /** The predicate currently narrowing the POI marker layers to a curated set,
1144
+ * or `null` when no spotlight is active. See `setPOISpotlight`. Held as the
1145
+ * predicate, not a resolved id list, so a floor change or locale redraw can
1146
+ * re-resolve it against the POIs now on screen. */
1147
+ private poiSpotlight;
1148
+ /** Layer filters as they were before the spotlight narrowed them, put back
1149
+ * verbatim when it releases. `null` when no spotlight filter is applied. */
1150
+ private poiSpotlightRestore;
1151
+ /** Coordinates of the current tap/Info selection, or `null` when nothing is
1152
+ * selected. `easeToSelection` reads `getBoundsPadding()` at the moment a
1153
+ * selection is made, which is often *before* the consumer's info card has
1154
+ * mounted and been measured into that padding — so the first pan can land
1155
+ * the POI behind the card. Kept here so `setBoundsPadding` can re-nudge the
1156
+ * camera once the consumer measures its now-mounted chrome and reports
1157
+ * fresh insets, the same way it already re-fits an active route. Cleared by
1158
+ * `clearHighlight`. */
1159
+ private selectedCoordinates;
826
1160
  private youAreHerePulse;
827
1161
  /** Theme layers whose features represent a tappable destination/amenity.
828
1162
  * A click hit-tests these (see `SelectionManager`) → `poiSelected`. The
@@ -836,6 +1170,15 @@ declare class MinuteMaps {
836
1170
  * identically. Kept separate from the pin list because these carry no POI
837
1171
  * identity of their own — they resolve spatially. */
838
1172
  private static readonly CLICKABLE_UNIT_LAYERS;
1173
+ /** The kiosk's own "you are here" pin — icon, directional heading wedge,
1174
+ * and the ring drawn behind them. Deliberately **not** part of
1175
+ * `CLICKABLE_POI_LAYERS`: that list feeds `emitPoiSelected`, which opens
1176
+ * a selection card for a destination/amenity, and this pin isn't one —
1177
+ * it's where the kiosk itself is standing. A tap here means "where am
1178
+ * I", so it gets its own delegated listener emitting `youAreHereClicked`
1179
+ * instead, for a consumer to wire to the same recenter action as its
1180
+ * own recenter control. */
1181
+ private static readonly YOU_ARE_HERE_LAYERS;
839
1182
  /** While a room is selected, the other units are recolored to a flat, muted
840
1183
  * gray so the raised, full-strength highlight block reads as the focus. We
841
1184
  * recolor rather than lower opacity on purpose: translucent extrusions blend
@@ -857,7 +1200,20 @@ declare class MinuteMaps {
857
1200
  * `poi-highlight-*` layers — are intentionally unaffected; they're
858
1201
  * anchors or transient focus state, not categorical content. */
859
1202
  private static readonly POI_CATEGORY_LAYERS;
1203
+ /** Marker layers `setPOISpotlight` narrows. Every categorical POI layer
1204
+ * except `poi-other-amenity-icons`, whose filter the restroom-group manager
1205
+ * and the zoom-range pass rewrite as the camera moves — a spotlight
1206
+ * narrowing would be silently overwritten mid-orbit — plus the per-instance
1207
+ * amenity label, which is a marker in every sense but isn't a category. */
1208
+ private static readonly POI_SPOTLIGHT_LAYERS;
860
1209
  private poiVisibleTypes;
1210
+ /** Independent show/hide state for destination dots and name labels, set
1211
+ * by `setDestinationCirclesVisible`/`setDestinationLabelsVisible`. Kept
1212
+ * separate from `poiVisibleTypes` so the two controls compose instead of
1213
+ * clobbering each other — `applyPOIFilter` and `applyDestinationDisplay`
1214
+ * each own one paint/layout axis of the same layers. */
1215
+ private destinationCirclesVisible;
1216
+ private destinationLabelsVisible;
861
1217
  constructor(config: SDKConfig);
862
1218
  init(): Promise<void>;
863
1219
  /**
@@ -883,6 +1239,16 @@ declare class MinuteMaps {
883
1239
  off(event: string, cb?: (e: MapEvent) => void): void;
884
1240
  addControl(control: IControl, position?: ControlPosition): void;
885
1241
  setView(options: ViewOptions): void;
1242
+ /**
1243
+ * Force a camera move to land instantly while reduced motion is on.
1244
+ *
1245
+ * The single gate every consumer-driven move goes through — zoom
1246
+ * buttons, compass reset, go-home, recenter, route fits — so the one
1247
+ * toggle covers them all instead of each call site remembering to ask.
1248
+ * Returns `opts` untouched when the preference is off, so a caller that
1249
+ * asked for `animate: false` keeps it either way.
1250
+ */
1251
+ private motionGated;
886
1252
  get amenities(): AmenityManager;
887
1253
  resetView(opts?: {
888
1254
  animate?: boolean;
@@ -921,10 +1287,13 @@ declare class MinuteMaps {
921
1287
  * `styleimagemissing`; everything else must be re-run explicitly here or
922
1288
  * it silently goes blank after a theme swap.
923
1289
  *
924
- * `themeName === 'high-contrast'` recolors the amenity badge disc to
925
- * `HIGH_CONTRAST_AMENITY_BADGE_COLOR`, leaving ring/glyph colors and
926
- * connector (elevator/stairs/escalator) badges untouched. Failures per
927
- * icon set are logged and skipped so one bad icon doesn't block the rest.
1290
+ * `themeName === 'high-contrast'` recolors both the amenity badge disc
1291
+ * and the connector (elevator/stairs/escalator) badge disc to
1292
+ * `HIGH_CONTRAST_AMENITY_BADGE_COLOR`, and forces both the regular and
1293
+ * connector glyph to `HIGH_CONTRAST_AMENITY_ICON_COLOR` so neither
1294
+ * washes out against its now-yellow disc and the two match each other.
1295
+ * Ring colors are left untouched. Failures per icon set are logged and
1296
+ * skipped so one bad icon doesn't block the rest.
928
1297
  */
929
1298
  private registerRuntimeIcons;
930
1299
  /**
@@ -939,12 +1308,39 @@ declare class MinuteMaps {
939
1308
  /** Currently active theme name (or `'custom'` when a raw style was passed). */
940
1309
  getActiveTheme(): 'default' | 'high-contrast' | 'custom';
941
1310
  /**
942
- * Update the SDK's reduced-motion preference at runtime. When true,
943
- * imperative camera animations (route fits, idle re-frames) and the
944
- * route line-draw animation run with `duration: 0`.
1311
+ * Update the SDK's reduced-motion preference at runtime. When true, every
1312
+ * imperative camera move (`setView`, `resetView`, `refit`, recenter, route
1313
+ * fits, selection pans, floor fits) runs with `duration: 0`, and every
1314
+ * looping animation the SDK drives — the route line-draw, the flowing route
1315
+ * arrows, the "You are here" pulse, the selection ring's ping — is stopped.
1316
+ *
1317
+ * Anything already on screen is switched over here, not just on the next
1318
+ * action: a visitor who flips the toggle mid-route is doing it *because* of
1319
+ * the motion they can see, so waiting for the next event would be the one
1320
+ * moment the preference doesn't work.
945
1321
  */
946
1322
  setReducedMotion(enabled: boolean): void;
947
1323
  getReducedMotion(): boolean;
1324
+ /**
1325
+ * Swap every map label (street names, POI names, building labels, "You are
1326
+ * here") between the bundled themes' default font and OpenDyslexic. Mirrors
1327
+ * the DOM-side dyslexic-font accommodation onto the canvas, which CSS can't
1328
+ * reach.
1329
+ *
1330
+ * Goes through the same local-glyph fallback the themes already use for
1331
+ * `Material Icons` (`LOCAL_GLYPH_FONTS`): `text-font` values are swapped to
1332
+ * fontstack names intentionally absent from the glyph server
1333
+ * (`OpenDyslexic Bold` etc.), so MapLibre rasterizes them from a
1334
+ * page-loaded `@font-face` instead. The consuming app owns loading that
1335
+ * font — the SDK ships no assets — so this is a no-op glyph-wise (labels
1336
+ * fall back to tofu-free default rendering) if the app never declared it.
1337
+ *
1338
+ * Re-applied after `setTheme()`, since `setStyle({ diff: false })`
1339
+ * recreates every layer from the pristine (non-dyslexic) theme JSON.
1340
+ */
1341
+ setDyslexicFont(enabled: boolean): Promise<void>;
1342
+ getDyslexicFont(): boolean;
1343
+ private applyDyslexicFontToLabels;
948
1344
  /**
949
1345
  * Switch the locale used for POI / amenity / destination / floor names at
950
1346
  * runtime. The SDK refetches localized names from JACS (per-entity
@@ -958,9 +1354,12 @@ declare class MinuteMaps {
958
1354
  * Emits a `localeChanged` event with the new code after the patch
959
1355
  * completes. No-op when the code matches the current locale.
960
1356
  *
961
- * Throws if the underlying data provider doesn't implement locale support
962
- * (only `JacsDataProvider` does today), or if the JACS fetch fails — the
963
- * prior locale is left intact in that case.
1357
+ * Throws only on programmer error — a provider without locale support
1358
+ * (only `JacsDataProvider` has it today) or an empty code. A **failed
1359
+ * JACS fetch is not fatal**: the locale still switches and is reported,
1360
+ * it's only the localized names that stay on the prior language. That
1361
+ * split is deliberate — generated directions text ships inside the SDK
1362
+ * and must not be held hostage to a network round-trip.
964
1363
  */
965
1364
  setLocale(locale: string): Promise<void>;
966
1365
  /** Currently active locale (BCP-47), or `undefined` when no locale has
@@ -992,15 +1391,53 @@ declare class MinuteMaps {
992
1391
  type: 'amenity' | 'destination' | 'kiosk';
993
1392
  category?: string;
994
1393
  floorName?: string;
1394
+ floorShortName?: string;
995
1395
  zoneName?: string;
996
1396
  zoneDescription?: string;
1397
+ /** The department's authored hex colour (`Zone.color`) — the same value
1398
+ * that tints this room's polygon on the map, so a consumer can print a
1399
+ * swatch beside the department name and connect the two. Absent when the
1400
+ * author never picked one, which is also when the map leaves the room
1401
+ * untinted; treat "no colour" as "this department isn't colour-coded",
1402
+ * not as "fall back to a default swatch". */
1403
+ zoneColor?: string;
997
1404
  keywords?: string[];
998
1405
  description?: string;
1406
+ /** CMS-authored extras from the entity's `extensors` bag (opening hours,
1407
+ * days, room, building). Free-form — the consumer decides what to render.
1408
+ * Artwork is not in here; see `imageUrl`. */
1409
+ properties?: Record<string, unknown>;
999
1410
  /** The destination's uploaded location image, ready for an `<img src>`.
1000
1411
  * Destinations only — amenities use sprite glyphs, not photos. Inline SVG
1001
1412
  * from `/all` comes back as a data URI; otherwise it's the uploaded uri
1002
1413
  * path. Absent when nothing was uploaded in the CMS. */
1003
1414
  imageUrl?: string;
1415
+ /** Author-written alt text for `imageUrl` (`mm_image_alt`) — a description
1416
+ * of the place, for screen readers and for anyone the image fails to load
1417
+ * for. Absent when the author wrote none, in which case the image is
1418
+ * decorative and belongs in an `<img alt="">`, not an unlabelled one. */
1419
+ imageAlt?: string;
1420
+ /** Room number — the CMS's "Room Number". Reads the real
1421
+ * `Destination.unitNumber` column, falling back to a legacy hand-typed
1422
+ * `room_number` property for venues authored before that field existed. */
1423
+ roomNumber?: string;
1424
+ /** Which building on the campus this destination is in (`mm_building_name`),
1425
+ * as the author typed it — "200", "Ambulatory Care". Free text, not a
1426
+ * reference to a `Building` record; render it, don't resolve it. Falls back
1427
+ * to the legacy hand-typed `building_name` property, which is unprefixed
1428
+ * and therefore a different key. */
1429
+ buildingName?: string;
1430
+ /** Phone number and internal extension (`mm_phone`). The number is exactly
1431
+ * as the author typed it, not normalised — see `parsePhone`. */
1432
+ phone?: DestinationPhone;
1433
+ /** The CMS-authored opening hours (`mm_open_hours`), parsed. Absent when
1434
+ * the author set none, or the stored value is unreadable.
1435
+ *
1436
+ * Static — it describes the week, not this moment. Pass it to
1437
+ * `getOpenStatus()` for the live open/closed state, and re-call that on a
1438
+ * tick if the UI stays on screen. The raw string also remains in
1439
+ * `properties` for consumers that would rather parse it themselves. */
1440
+ openHours?: OpenHours;
1004
1441
  };
1005
1442
  /** Resolve the source Destination/Amenity record backing a rendered POI,
1006
1443
  * for detail fields (category, localized description) that don't live on
@@ -1028,6 +1465,40 @@ declare class MinuteMaps {
1028
1465
  private reportLayerAudit;
1029
1466
  getFloorMapTemplate3d(floorId: string | number): any[];
1030
1467
  getAllPOIs(floor?: Floor): POI[];
1468
+ /**
1469
+ * Put the map on the kiosk's floor before drawing a route from it.
1470
+ *
1471
+ * **A route from the kiosk always starts on the kiosk's floor**, so that is
1472
+ * the floor the visitor has to be looking at when it appears. The renderer
1473
+ * agrees with that already — `wayfinding` reveals and frames *the active
1474
+ * floor's* slice of the route — which is exactly why this matters: leave the
1475
+ * map where the visitor had wandered to and the route arrives framed on the
1476
+ * wrong leg, or on no leg at all.
1477
+ *
1478
+ * Both halves of that were reported from a real kiosk. Browse up to Level 3,
1479
+ * search, press Directions: if Level 3 isn't on the route the fit falls back
1480
+ * to the union of every floor and nothing reads as a path from here; if it
1481
+ * *is* on the route — because the destination is up there — the visitor is
1482
+ * looking at the last leg of a walk they haven't started, with no indication
1483
+ * that the first one is two floors down.
1484
+ *
1485
+ * Called before the route is computed rather than after it's drawn, so the
1486
+ * camera makes one move instead of fitting the wrong floor and correcting.
1487
+ * No-op when the venue has no kiosk anchor (those routes fail anyway) or the
1488
+ * map is already there.
1489
+ */
1490
+ private showKioskFloorForRoute;
1491
+ /**
1492
+ * The floor the kiosk is physically anchored to — not necessarily the
1493
+ * floor currently displayed (the visitor may have panned the floor
1494
+ * selector to preview a destination on another level, and the SDK opens
1495
+ * on the venue's configured default floor rather than the kiosk's own —
1496
+ * see `FloorManager.selectInitialFloor`). Anchor resolution (routing,
1497
+ * recentering, closest-waypoint) must resolve against this floor, never
1498
+ * `getCurrentFloor()`, or it silently misses the "you are here" POI
1499
+ * whenever the two diverge. `null` when the venue has no kiosk anchor.
1500
+ */
1501
+ getKioskFloor(): Floor | null;
1031
1502
  getYouAreHerePOI(floor?: Floor): POI | null;
1032
1503
  getYouAreHereCoordinates(floor?: Floor): [number, number] | null;
1033
1504
  /**
@@ -1042,6 +1513,14 @@ declare class MinuteMaps {
1042
1513
  * cone rendered next to the "You are here" marker.
1043
1514
  */
1044
1515
  getKioskHeading(): number | null;
1516
+ /**
1517
+ * Bearing (degrees clockwise from north) the visitor at the kiosk is
1518
+ * FACING — the device heading rotated 180°, since they stand in front
1519
+ * of the screen looking back at it. `null` when the device record
1520
+ * carries no heading, in which case direction-relative wording ("turn
1521
+ * right and walk 40 ft") is suppressed rather than guessed.
1522
+ */
1523
+ private getVisitorFacingBearing;
1045
1524
  /**
1046
1525
  * Display name of the JACS device the kiosk is anchored to (e.g.
1047
1526
  * "Main Lobby Kiosk", "Information Desk Kiosk"). `null` when the venue
@@ -1064,8 +1543,14 @@ declare class MinuteMaps {
1064
1543
  * `{ id, mapId? }` shape. Returns `null` when the kiosk isn't anchored,
1065
1544
  * the target waypoint isn't on the graph, or no path resolves.
1066
1545
  *
1067
- * Walking speed defaults to 1.2 m/s (indoor wayfinding norm). Pass
1068
- * `walkingSpeedMps` to estimate for accessibility (e.g. 0.9).
1546
+ * Walking speed defaults to 2.953 ft/s (0.9 m/s, average adult walking speed —
1547
+ * PMC2967707). Pass `walkingSpeedFps` to estimate for accessibility (e.g. 2).
1548
+ *
1549
+ * `feet` / `seconds` are the *physical* walk — horizontal legs only, what a
1550
+ * visitor is shown. `effortSeconds` adds the router's charge for each floor
1551
+ * change (the elevator wait) and is what to *rank* candidates by; see
1552
+ * `findClosestByWalkTime`. Pass `avoidStairs` to price the path as a route
1553
+ * with that option would be.
1069
1554
  */
1070
1555
  getWalkTimeFromKiosk(target: number | string | {
1071
1556
  id?: number | string;
@@ -1075,10 +1560,12 @@ declare class MinuteMaps {
1075
1560
  id?: number | string;
1076
1561
  }>;
1077
1562
  }, opts?: {
1078
- walkingSpeedMps?: number;
1563
+ walkingSpeedFps?: number;
1564
+ avoidStairs?: boolean;
1079
1565
  }): {
1080
- meters: number;
1566
+ feet: number;
1081
1567
  seconds: number;
1568
+ effortSeconds: number;
1082
1569
  pathNodeCount: number;
1083
1570
  } | null;
1084
1571
  /**
@@ -1112,8 +1599,6 @@ declare class MinuteMaps {
1112
1599
  wayfindBetweenWaypoints(fromWaypoint: any, toWaypoint: any, options?: {
1113
1600
  centerMode?: 'none' | 'destination' | 'route';
1114
1601
  zoom?: number;
1115
- /** Prefer accessible paths (drops or penalizes stairs / inaccessible edges). */
1116
- accessible?: boolean;
1117
1602
  /** Hard-filter stairs from the graph. */
1118
1603
  avoidStairs?: boolean;
1119
1604
  }): Promise<any>;
@@ -1126,12 +1611,23 @@ declare class MinuteMaps {
1126
1611
  * you want a visual focus rather than a route.
1127
1612
  */
1128
1613
  navigateFromKioskToPOI(poi: POI, options?: {
1129
- accessible?: boolean;
1130
1614
  avoidStairs?: boolean;
1131
1615
  }): Promise<any>;
1132
1616
  /** Build the landmark / floor-name context the directions module needs.
1133
1617
  * Pulled out so `setLocale()` or `setActiveStep()` can rebuild it on
1134
1618
  * demand if we ever surface a locale-aware variant. */
1619
+ /**
1620
+ * Rewrite every source whose features carry venue-authored *text* for a
1621
+ * floor: POI labels and map labels. Both read their names straight out of
1622
+ * the venue model, so anything that mutates that model in place — today
1623
+ * `setLocale`'s translation patch — has to call this or the map keeps
1624
+ * rendering the old strings until the next floor change.
1625
+ *
1626
+ * Map labels ride the same lifecycle as POIs and carry the `allLevels`
1627
+ * flag, which the projector has already honored by duplicating the
1628
+ * instance under every floor's list.
1629
+ */
1630
+ private redrawFloorText;
1135
1631
  private buildDirectionsContext;
1136
1632
  /**
1137
1633
  * Drive the turn-by-turn UI. Two effects:
@@ -1160,6 +1656,42 @@ declare class MinuteMaps {
1160
1656
  * it lives on another one. Pass a POI / amenity / destination id.
1161
1657
  */
1162
1658
  highlightPOI(id: string | number): Promise<void>;
1659
+ /**
1660
+ * Select a POI programmatically — the same end state as tapping it on the
1661
+ * map, from a list or any other chrome.
1662
+ *
1663
+ * Switches the active floor first when the POI lives on another one, raises
1664
+ * its room as a highlighted block, mutes the neighbouring units, declutters
1665
+ * to the selected destination(s), pans it into the unobscured map area and
1666
+ * emits **`poiSelected`** carrying every co-located POI. A consumer that
1667
+ * already renders an info card off that event therefore needs no second code
1668
+ * path: a tap and a list pick arrive the same way.
1669
+ *
1670
+ * This is deliberately *not* `highlightPOI`, which is the search-locate
1671
+ * gesture — it zooms and recenters and draws a pulsing ring, and emits
1672
+ * nothing. Selection is the quieter one: current zoom kept, no ring, the
1673
+ * raised room is the indicator.
1674
+ *
1675
+ * Accepts a `POI` (preferred — its `type` and `waypointId` pin down the exact
1676
+ * instance, which matters for an amenity that shares one id across every
1677
+ * physical pin) or a bare id.
1678
+ *
1679
+ * **`includeCoLocated: false` emits only the requested POI.** By default the
1680
+ * payload carries everything sharing the room, which is right when the
1681
+ * gesture was a tap — the visitor pointed at a place on a floor plan and the
1682
+ * consumer has to show them what's there. It's wrong when they picked one
1683
+ * entry out of a named list: they have already disambiguated, and handing
1684
+ * back four places would ask them to do it again, on a card that opened
1685
+ * *because* they were specific. The map treatment is identical either way
1686
+ * (same room raised, same neighbours muted, same pan); only the payload —
1687
+ * and so the declutter's allow-list — narrows.
1688
+ *
1689
+ * Returns the selected POIs, `pois[0]` being the requested one; an empty
1690
+ * array if no POI matched, which is also when nothing on the map changes.
1691
+ */
1692
+ selectPOI(target: POI | string | number, options?: {
1693
+ includeCoLocated?: boolean;
1694
+ }): Promise<POI[]>;
1163
1695
  /** Remove the POI highlight set by `highlightPOI`, un-dim the other units,
1164
1696
  * and bring back the destination markers hidden by a room selection. */
1165
1697
  clearHighlight(): void;
@@ -1217,7 +1749,69 @@ declare class MinuteMaps {
1217
1749
  setPOIFilter(types: Array<'amenity' | 'destination'>): void;
1218
1750
  /** Restore the default — every POI category renders. */
1219
1751
  clearPOIFilter(): void;
1752
+ /**
1753
+ * Narrow the POI markers to a curated set: only POIs for which `match`
1754
+ * returns true keep their icon and label, every other pin on the categorical
1755
+ * marker layers (destinations, entrances, parking, bus, connectors,
1756
+ * information, and the per-instance amenity names) is filtered out. Built for
1757
+ * an attract loop that wants a few chosen places on screen instead of either
1758
+ * every marker or none.
1759
+ *
1760
+ * `match` runs against the current floor's POIs — the same objects
1761
+ * `getAllPOIs()` returns, `instanceName` and `amenityCategory` included — and
1762
+ * is re-run on every floor change, theme swap and locale redraw, so the
1763
+ * spotlight follows the map rather than the floor it was set on. Kiosk
1764
+ * "You are here" is never touched.
1765
+ *
1766
+ * Two things it deliberately leaves alone:
1767
+ * - `poi-other-amenity-icons` and the restroom badge. Their filters are owned
1768
+ * by the restroom-group manager and rewritten as the camera moves, so a
1769
+ * narrowing there would not survive a frame. Hide them with `hideLayers`
1770
+ * if the spotlight should exclude them.
1771
+ * - `setPOIFilter`'s category visibility, which composes: a category the
1772
+ * filter hides stays hidden whatever `match` says.
1773
+ *
1774
+ * Selecting a place (`selectPOI`, a map tap) releases the spotlight. Pass
1775
+ * `null` — or call `clearPOISpotlight()` — to restore every marker.
1776
+ */
1777
+ setPOISpotlight(match: ((poi: POI) => boolean) | null): void;
1778
+ /** Re-apply the zoom profile if the idle spotlight or a route just changed
1779
+ * which one is in force, and refresh what hangs off it. */
1780
+ private syncZoomViews;
1781
+ /** Restore every POI marker narrowed by `setPOISpotlight`. */
1782
+ clearPOISpotlight(): void;
1783
+ private applyPOISpotlight;
1220
1784
  private applyPOIFilter;
1785
+ /**
1786
+ * Show or hide destination dots, independently of their name labels and of
1787
+ * `setPOIFilter`'s combined `'destination'` category (which still toggles
1788
+ * both together). Affects every destination dot — both the dot+label
1789
+ * marker on `poi-destination-labels` and the dot-only fallback on
1790
+ * `poi-destination-circles` that a `showLabel: false` POI renders on.
1791
+ *
1792
+ * Implemented as `icon-opacity`, not a layer swap: dot and label are
1793
+ * placed as one collision unit on `poi-destination-labels` (`icon-optional:
1794
+ * false` / `text-optional: true`), which is what guarantees a label never
1795
+ * renders without a dot beneath it. Splitting them into independently
1796
+ * *placed* layers would let MapLibre's collision index resolve each half
1797
+ * on its own, so a label could win placement while its dot lost — the
1798
+ * "floating name" bug this theme's combined layer was built to fix (see
1799
+ * `themes.test.ts` :: "a destination is one marker").
1800
+ */
1801
+ setDestinationCirclesVisible(visible: boolean): void;
1802
+ /**
1803
+ * Show or hide destination name labels, independently of their dots. See
1804
+ * `setDestinationCirclesVisible` for why this is an opacity toggle on the
1805
+ * shared marker layer rather than a separate label layer.
1806
+ *
1807
+ * `visible: true` (the default) doesn't mean "always on" — labels still
1808
+ * fade in only once the visitor has zoomed in past the venue's opening
1809
+ * view (`DESTINATION_LABEL_REVEAL_OFFSET` in `iconZoomRanges.ts`), so a
1810
+ * zoomed-out visitor sees dots without the screen filling with names. Pass
1811
+ * `false` to suppress labels outright regardless of zoom.
1812
+ */
1813
+ setDestinationLabelsVisible(visible: boolean): void;
1814
+ private applyDestinationDisplay;
1221
1815
  /**
1222
1816
  * Return the waypoint in `waypoints` closest to `from`. Useful for
1223
1817
  * "route to the nearest X" against a venue-wide amenity record that has
@@ -1228,26 +1822,50 @@ declare class MinuteMaps {
1228
1822
  * single-venue scale, and avoids a turf dependency on the hot path.
1229
1823
  */
1230
1824
  findClosestWaypoint(waypoints: Waypoint[], from?: [number, number]): Waypoint | null;
1825
+ /**
1826
+ * Pick the candidate cheapest to reach from the kiosk — the one rule for
1827
+ * "closest instance of an amenity". `navigateFromKioskToClosestAmenity` and
1828
+ * `highlightAmenity` use it, and a consumer collapsing a venue-wide amenity
1829
+ * to a single search row should too, so a tap and a search result can never
1830
+ * resolve the same amenity to different floors.
1831
+ *
1832
+ * Each candidate is scored by `getWalkTimeFromKiosk().effortSeconds` — walk
1833
+ * time over the JACS path graph **plus the elevator wait** for any floor
1834
+ * change — and a candidate on a *different floor* from the kiosk has that
1835
+ * score multiplied by `options.otherFloorPenalty` (default `1.5`; `1`
1836
+ * disables it). The wait is the physical cost of leaving the floor; the
1837
+ * multiplier is a preference for not making the visitor do it, and only
1838
+ * applies here — it never changes a route.
1839
+ *
1840
+ * `toWaypoint` maps a candidate to the waypoint it stands for (identity for
1841
+ * a `Waypoint[]`, `poi => poi.waypoint` for a `POI[]`). Candidates whose
1842
+ * waypoint is missing or has no coordinates are ignored, as are waypoints
1843
+ * the graph can't reach from the kiosk. If none resolve a cost, falls back
1844
+ * to the planar `findClosestWaypoint`, **restricted to the kiosk's floor
1845
+ * when any candidate is on it** (planar distance is meaningless across
1846
+ * overlaid floors). Returns `null` for no usable candidate. Pass the same
1847
+ * `avoidStairs` the route will use so the pick and the route agree.
1848
+ */
1849
+ findClosestByWalkTime<T>(items: readonly T[], toWaypoint: (item: T) => Waypoint | null | undefined, opts?: {
1850
+ avoidStairs?: boolean;
1851
+ }): T | null;
1231
1852
  /**
1232
1853
  * Route from the kiosk to the closest physical instance of an amenity.
1233
1854
  * Resolves the amenity venue-wide, picks the instance with the smallest
1234
- * path-graph walk time from the kiosk (falling back to planar Euclidean
1235
- * when the walk-time graph can't resolve any waypoint), switches the
1855
+ * path-graph cost from the kiosk (`findClosestByWalkTime`: walk time plus
1856
+ * the elevator wait, with a preference for the kiosk's floor; falling back
1857
+ * to planar Euclidean when the walk-time graph can't resolve any waypoint), switches the
1236
1858
  * active floor if needed, and routes via the same plumbing as
1237
1859
  * `navigateFromKioskToPOI` — so step-by-step directions, the
1238
1860
  * `routeReady` event, and the active-floor camera fit all "just work."
1239
1861
  *
1240
- * Pass `accessible` / `avoidStairs` to weight the *route* (the SDK's
1241
- * routing engine reads these per call). Note: closest-instance picking
1242
- * itself is currently distance-based and does not yet honor those
1243
- * weights — the underlying walk-time graph applies pixel-length only.
1244
- * In practice this matters when an amenity has multiple instances and
1245
- * the geometrically closest is reachable only via stairs; the picked
1246
- * instance won't change today, but the *route* to it will avoid stairs
1247
- * (or fail gracefully) if `accessible`/`avoidStairs` is set.
1862
+ * Pass `avoidStairs` to hard-filter stairs from the *route* (the SDK's
1863
+ * routing engine reads it per call; the CMS's per-path-type weight
1864
+ * otherwise handles accessibility preference automatically). The same
1865
+ * flag is passed to the closest-instance pick, so an instance reachable
1866
+ * only by stairs is skipped in favour of one the route can actually reach.
1248
1867
  */
1249
1868
  navigateFromKioskToClosestAmenity(amenityId: string | number, options?: {
1250
- accessible?: boolean;
1251
1869
  avoidStairs?: boolean;
1252
1870
  }): Promise<any>;
1253
1871
  /**
@@ -1268,6 +1886,32 @@ declare class MinuteMaps {
1268
1886
  private findFloorForWaypoint;
1269
1887
  getCameraPosition(): CameraState | null;
1270
1888
  getMap(): Map | null;
1889
+ /**
1890
+ * Hide one or more layers by id — a public, always-live counterpart to
1891
+ * `debug.hide()` for consumers that want to toggle arbitrary layers
1892
+ * without reaching into the console-facing debug API. Sticky: the
1893
+ * override is re-asserted across floor changes, `setPOIFilter`, view-mode
1894
+ * toggles and theme swaps until `showLayers` undoes it. Ids absent from
1895
+ * the active style are skipped silently. Returns the ids actually found
1896
+ * and hidden.
1897
+ */
1898
+ hideLayers(layerIds: string[]): string[];
1899
+ /**
1900
+ * Undo `hideLayers` for these ids. Each layer returns to whatever
1901
+ * visibility the SDK's own state — current floor, POI filter, view mode —
1902
+ * says it should have; it is **not** forced to `visible`. A layer the SDK
1903
+ * is independently keeping hidden (another floor's layers, an
1904
+ * off-view-mode extrusion) stays hidden rather than being force-revealed.
1905
+ * This is the correct inverse of `hideLayers` (mirrors `debug.restore()` —
1906
+ * see `debugLayers.ts` for why a blanket "show" is usually the wrong
1907
+ * undo). Returns the ids that actually had an override to release.
1908
+ */
1909
+ showLayers(layerIds: string[]): string[];
1910
+ /**
1911
+ * Whether a layer is currently rendering (`visibility !== 'none'`).
1912
+ * `false` for a layer absent from the active style.
1913
+ */
1914
+ isLayerVisible(layerId: string): boolean;
1271
1915
  /**
1272
1916
  * Put this instance on `window` so a kiosk running in a browser can be
1273
1917
  * driven from devtools — `mm.debug.help()`, `mm.getMap()`, `mm.setTheme(…)`.
@@ -1311,6 +1955,19 @@ declare class MinuteMaps {
1311
1955
  */
1312
1956
  private clampPadding;
1313
1957
  private getVenueBounds;
1958
+ /**
1959
+ * Resolve `options.maxBounds` into the actual pan/zoom-out cap. `false`
1960
+ * disables it, explicit bounds pass through as-is, and a number (or the
1961
+ * unset default of 3) scales the venue's own bounds outward around its
1962
+ * center — see `scaleBounds`.
1963
+ *
1964
+ * The unset default is also floored at `DEFAULT_MIN_MAX_BOUNDS_SPAN_M` per
1965
+ * side. MapLibre won't zoom out past the point where `maxBounds` fills the
1966
+ * viewport, so a small venue's 3x box would otherwise raise the effective
1967
+ * zoom-out floor above `minZoom` and show far less surrounding map than a
1968
+ * large venue does. An explicit number is a deliberate leash and is left as-is.
1969
+ */
1970
+ private getMaxBounds;
1314
1971
  /**
1315
1972
  * Opening-framing policy applied once after the initial floor fit.
1316
1973
  * - `initialPitch`: tilt to this and re-fit the building footprint so the
@@ -1364,5 +2021,76 @@ declare function groupStepsIntoFloorSections(steps: WayfindStep[]): RouteFloorSe
1364
2021
  */
1365
2022
  declare function activeSectionIndex(sections: RouteFloorSection[], activeStepIndex: number): number;
1366
2023
 
1367
- export { MinuteMaps, activeSectionIndex, createMinuteMapsSDK, groupStepsIntoFloorSections };
1368
- export type { Amenity, AmenityBadgeStyle, AmenityWithFloor, Bounds, BoundsPadding, CameraState, Destination, EventCallback, Floor, FloorMetadata, JMapAuth, JMapConfig, JacsAuth, JacsConfig, MapEvent, POI, POISearchResult, RouteFloorSection, RoutePoint, RoutingOptions, SDKConfig, SDKOptions, ThemeName, TransitionStep, ViewOptions, WayfindCenterMode, WayfindStep, Waypoint, Zone };
2024
+ /** True when an amenity is a vertical-circulation connector. The provider
2025
+ * stamps `_connector` on every synthetic elevator / stairs / escalator, which
2026
+ * is the reliable signal — `extensors.iconName` is only the *display* glyph
2027
+ * and may be any CMS-assigned icon (we still honour it as a fallback for
2028
+ * amenities authored with a connector icon name directly). */
2029
+ declare function isConnectorAmenity(amenity: AmenityLike): boolean;
2030
+ type LocaleField = {
2031
+ locale?: string;
2032
+ uriPath?: string;
2033
+ };
2034
+ type UriField = {
2035
+ mimeType?: string;
2036
+ resourceType?: string;
2037
+ locales?: LocaleField[];
2038
+ };
2039
+ type AmenityLike = {
2040
+ id: number | string;
2041
+ iconId?: string;
2042
+ uris?: UriField[] | null;
2043
+ /** Inline SVG markup. Used by the SDK for synthetic connector
2044
+ * amenities (elevator / stairs / escalator) where the CMS has no
2045
+ * uploaded icon to fetch. When set, this short-circuits the
2046
+ * `uris[].locales[].uriPath` lookup. */
2047
+ svg?: string;
2048
+ /** Free-form CMS metadata. When `extensors.iconName` names a curated
2049
+ * Material Symbols icon, the SDK renders it from the bundled registry
2050
+ * (no upload, no network) — see `pickMaterialSvg`. */
2051
+ extensors?: Record<string, unknown> | null;
2052
+ /** Set by the JACS provider on synthetic vertical-circulation amenities
2053
+ * (elevator / stairs / escalator built from path types). The reliable
2054
+ * connector signal — `extensors.iconName` is the *display* glyph, which
2055
+ * may be any CMS-assigned icon, not the connector kind. */
2056
+ _connector?: boolean;
2057
+ };
2058
+ type ResolvedIconSource = {
2059
+ /** Map image id to register/reference. Shared across amenities for
2060
+ * connectors (preset) and curated Material icons (`material-<name>`). */
2061
+ iconId: string;
2062
+ /** Source for `composeAmenityBitmap` — exactly one of url / inlineSvg. */
2063
+ source: {
2064
+ url?: string;
2065
+ inlineSvg?: string;
2066
+ };
2067
+ /**
2068
+ * Where the artwork came from. The map doesn't care — it rasterises all
2069
+ * three the same way — but a DOM consumer must, because provenance decides
2070
+ * both trust and styling:
2071
+ *
2072
+ * - `material` — from the SDK's bundled registry. Ours, so it's safe to
2073
+ * inline into the DOM, and safe to recolour (it's a monochrome glyph).
2074
+ * - `inline` — an `svg` string off the CMS record. **Author-supplied
2075
+ * markup**: an inline `<svg>` can carry `<script>`, so render it through
2076
+ * an `<img src="data:…">` (which never executes script), not
2077
+ * `dangerouslySetInnerHTML`.
2078
+ * - `upload` — a URL to a CMS-uploaded file. Also author-supplied, also an
2079
+ * `<img>`; and it arrives in whatever colours the author chose, so don't
2080
+ * assume you can tint it to match a badge.
2081
+ */
2082
+ kind: 'material' | 'inline' | 'upload';
2083
+ };
2084
+ /**
2085
+ * Pure icon-source resolution by precedence — no map/DOM side effects, so it's
2086
+ * unit-testable:
2087
+ * 1. inline svg — explicit inline-SVG override (rare)
2088
+ * 2. extensors.iconName — curated Material icon (bundled, shared bitmap);
2089
+ * also how synthetic connectors render now
2090
+ * 3. uris[].uriPath — legacy / custom-uploaded SVG (fetched)
2091
+ * Returns null when the amenity has no resolvable icon.
2092
+ */
2093
+ declare function resolveAmenityIconSource(amenity: AmenityLike, preferredLocale?: string): ResolvedIconSource | null;
2094
+
2095
+ 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, activeSectionIndex, createMinuteMapsSDK, getOpenStatus, groupStepsIntoFloorSections, isConnectorAmenity, parseBuildingName, parseImageAlt, parseOpenHours, parsePhone, parsePointOfInterest, resolveAmenityIconSource };
2096
+ export type { Amenity, AmenityBadgeStyle, AmenityWithFloor, Bounds, BoundsPadding, CameraState, DayKey, Destination, DestinationPhone, EventCallback, Floor, FloorMetadata, HoursRange, JMapAuth, JMapConfig, JacsAuth, JacsConfig, MapEvent, OpenHours, OpenHoursTransition, OpenStatus, OpenStatusOptions, POI, POISearchResult, ResolvedIconSource, RouteFloorSection, RoutePoint, RoutingOptions, SDKConfig, SDKOptions, ThemeName, TransitionStep, ViewOptions, WayfindCenterMode, WayfindStep, Waypoint, Zone };