@mapvx/web-js 3.6.0-dev.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +6 -1
  2. package/dist/cjs/index.js +2 -95
  3. package/dist/cjs/index.js.LICENSE.txt +1 -0
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/es/controllers/routeController.d.ts.map +1 -1
  6. package/dist/es/controllers/routeController.js +25 -8
  7. package/dist/es/controllers/routeController.js.map +1 -1
  8. package/dist/es/logger/sentry.js +1 -1
  9. package/dist/es/map/liveMapFitTransform.d.ts +11 -5
  10. package/dist/es/map/liveMapFitTransform.d.ts.map +1 -1
  11. package/dist/es/map/liveMapFitTransform.js +28 -19
  12. package/dist/es/map/liveMapFitTransform.js.map +1 -1
  13. package/dist/es/map/map.d.ts.map +1 -1
  14. package/dist/es/map/map.js +3 -6
  15. package/dist/es/map/map.js.map +1 -1
  16. package/dist/umd/index.js +932 -126
  17. package/dist/umd/index.js.map +1 -1
  18. package/dist/umd/styles.css +1 -1
  19. package/dist/umd/styles.css.map +1 -1
  20. package/package.json +7 -4
  21. package/dist/cjs/assets/icons.js +0 -85
  22. package/dist/cjs/assets/icons.js.map +0 -1
  23. package/dist/cjs/config/draco.js +0 -15
  24. package/dist/cjs/config/draco.js.map +0 -1
  25. package/dist/cjs/config/sdkConfig.js +0 -3
  26. package/dist/cjs/config/sdkConfig.js.map +0 -1
  27. package/dist/cjs/controllers/markerLayoutController.js +0 -359
  28. package/dist/cjs/controllers/markerLayoutController.js.map +0 -1
  29. package/dist/cjs/controllers/routeController.js +0 -1549
  30. package/dist/cjs/controllers/routeController.js.map +0 -1
  31. package/dist/cjs/domain/models/_internal.js +0 -14
  32. package/dist/cjs/domain/models/_internal.js.map +0 -1
  33. package/dist/cjs/domain/models/_rtl.js +0 -10
  34. package/dist/cjs/domain/models/_rtl.js.map +0 -1
  35. package/dist/cjs/domain/models/animation.js +0 -64
  36. package/dist/cjs/domain/models/animation.js.map +0 -1
  37. package/dist/cjs/domain/models/banner.js +0 -24
  38. package/dist/cjs/domain/models/banner.js.map +0 -1
  39. package/dist/cjs/domain/models/campaign.js +0 -73
  40. package/dist/cjs/domain/models/campaign.js.map +0 -1
  41. package/dist/cjs/domain/models/categories.js +0 -37
  42. package/dist/cjs/domain/models/categories.js.map +0 -1
  43. package/dist/cjs/domain/models/circle.js +0 -253
  44. package/dist/cjs/domain/models/circle.js.map +0 -1
  45. package/dist/cjs/domain/models/cityFilterOption.js +0 -3
  46. package/dist/cjs/domain/models/cityFilterOption.js.map +0 -1
  47. package/dist/cjs/domain/models/configuration.js +0 -3
  48. package/dist/cjs/domain/models/configuration.js.map +0 -1
  49. package/dist/cjs/domain/models/cssCustomization.js +0 -3
  50. package/dist/cjs/domain/models/cssCustomization.js.map +0 -1
  51. package/dist/cjs/domain/models/fitOptions.js +0 -3
  52. package/dist/cjs/domain/models/fitOptions.js.map +0 -1
  53. package/dist/cjs/domain/models/innerFloors.js +0 -3
  54. package/dist/cjs/domain/models/innerFloors.js.map +0 -1
  55. package/dist/cjs/domain/models/institution.js +0 -45
  56. package/dist/cjs/domain/models/institution.js.map +0 -1
  57. package/dist/cjs/domain/models/latLng.js +0 -3
  58. package/dist/cjs/domain/models/latLng.js.map +0 -1
  59. package/dist/cjs/domain/models/location.js +0 -3
  60. package/dist/cjs/domain/models/location.js.map +0 -1
  61. package/dist/cjs/domain/models/loggeable.js +0 -52
  62. package/dist/cjs/domain/models/loggeable.js.map +0 -1
  63. package/dist/cjs/domain/models/mapCamera.js +0 -3
  64. package/dist/cjs/domain/models/mapCamera.js.map +0 -1
  65. package/dist/cjs/domain/models/mapConfig.js +0 -37
  66. package/dist/cjs/domain/models/mapConfig.js.map +0 -1
  67. package/dist/cjs/domain/models/marker.js +0 -502
  68. package/dist/cjs/domain/models/marker.js.map +0 -1
  69. package/dist/cjs/domain/models/multilanguage.js +0 -17
  70. package/dist/cjs/domain/models/multilanguage.js.map +0 -1
  71. package/dist/cjs/domain/models/openingHours.js +0 -3
  72. package/dist/cjs/domain/models/openingHours.js.map +0 -1
  73. package/dist/cjs/domain/models/place.js +0 -127
  74. package/dist/cjs/domain/models/place.js.map +0 -1
  75. package/dist/cjs/domain/models/platform.js +0 -3
  76. package/dist/cjs/domain/models/platform.js.map +0 -1
  77. package/dist/cjs/domain/models/popover.js +0 -3
  78. package/dist/cjs/domain/models/popover.js.map +0 -1
  79. package/dist/cjs/domain/models/product.js +0 -3
  80. package/dist/cjs/domain/models/product.js.map +0 -1
  81. package/dist/cjs/domain/models/property.js +0 -104
  82. package/dist/cjs/domain/models/property.js.map +0 -1
  83. package/dist/cjs/domain/models/route.js +0 -166
  84. package/dist/cjs/domain/models/route.js.map +0 -1
  85. package/dist/cjs/domain/models/routeConfiguration.js +0 -82
  86. package/dist/cjs/domain/models/routeConfiguration.js.map +0 -1
  87. package/dist/cjs/domain/models/routeInstance.js +0 -30
  88. package/dist/cjs/domain/models/routeInstance.js.map +0 -1
  89. package/dist/cjs/domain/models/routeStyle.js +0 -3
  90. package/dist/cjs/domain/models/routeStyle.js.map +0 -1
  91. package/dist/cjs/domain/models/scene3D.js +0 -3
  92. package/dist/cjs/domain/models/scene3D.js.map +0 -1
  93. package/dist/cjs/domain/models/transport.js +0 -22
  94. package/dist/cjs/domain/models/transport.js.map +0 -1
  95. package/dist/cjs/domain/ports/ICache.js +0 -3
  96. package/dist/cjs/domain/ports/ICache.js.map +0 -1
  97. package/dist/cjs/domain/ports/IHttpClient.js +0 -3
  98. package/dist/cjs/domain/ports/IHttpClient.js.map +0 -1
  99. package/dist/cjs/domain/ports/ILogger.js +0 -23
  100. package/dist/cjs/domain/ports/ILogger.js.map +0 -1
  101. package/dist/cjs/domain/ports/index.js +0 -10
  102. package/dist/cjs/domain/ports/index.js.map +0 -1
  103. package/dist/cjs/infrastructure/cache/CacheManager.js +0 -258
  104. package/dist/cjs/infrastructure/cache/CacheManager.js.map +0 -1
  105. package/dist/cjs/infrastructure/cache/LRUCache.js +0 -153
  106. package/dist/cjs/infrastructure/cache/LRUCache.js.map +0 -1
  107. package/dist/cjs/infrastructure/cache/PersistentCache.js +0 -168
  108. package/dist/cjs/infrastructure/cache/PersistentCache.js.map +0 -1
  109. package/dist/cjs/infrastructure/cache/cacheConfig.js +0 -70
  110. package/dist/cjs/infrastructure/cache/cacheConfig.js.map +0 -1
  111. package/dist/cjs/infrastructure/http/FetchHttpClient.js +0 -75
  112. package/dist/cjs/infrastructure/http/FetchHttpClient.js.map +0 -1
  113. package/dist/cjs/infrastructure/logging/MVXLogger.js +0 -80
  114. package/dist/cjs/infrastructure/logging/MVXLogger.js.map +0 -1
  115. package/dist/cjs/interfaces/cache.js +0 -3
  116. package/dist/cjs/interfaces/cache.js.map +0 -1
  117. package/dist/cjs/interfaces/healthResponse.js +0 -3
  118. package/dist/cjs/interfaces/healthResponse.js.map +0 -1
  119. package/dist/cjs/interfaces/institutionResponse.js +0 -3
  120. package/dist/cjs/interfaces/institutionResponse.js.map +0 -1
  121. package/dist/cjs/interfaces/metadata.js +0 -3
  122. package/dist/cjs/interfaces/metadata.js.map +0 -1
  123. package/dist/cjs/interfaces/placeResponse.js +0 -3
  124. package/dist/cjs/interfaces/placeResponse.js.map +0 -1
  125. package/dist/cjs/interfaces/routeCacheResponse.js +0 -3
  126. package/dist/cjs/interfaces/routeCacheResponse.js.map +0 -1
  127. package/dist/cjs/interfaces/routeResponse.js +0 -3
  128. package/dist/cjs/interfaces/routeResponse.js.map +0 -1
  129. package/dist/cjs/interfaces/styleResponse.js +0 -3
  130. package/dist/cjs/interfaces/styleResponse.js.map +0 -1
  131. package/dist/cjs/logger/logger.js +0 -78
  132. package/dist/cjs/logger/logger.js.map +0 -1
  133. package/dist/cjs/logger/sentry.js +0 -81
  134. package/dist/cjs/logger/sentry.js.map +0 -1
  135. package/dist/cjs/map/fitCamera.js +0 -124
  136. package/dist/cjs/map/fitCamera.js.map +0 -1
  137. package/dist/cjs/map/liveMapFitTransform.js +0 -180
  138. package/dist/cjs/map/liveMapFitTransform.js.map +0 -1
  139. package/dist/cjs/map/map.js +0 -2766
  140. package/dist/cjs/map/map.js.map +0 -1
  141. package/dist/cjs/map/mapInteractionOptions.js +0 -73
  142. package/dist/cjs/map/mapInteractionOptions.js.map +0 -1
  143. package/dist/cjs/map/scene3DController.js +0 -849
  144. package/dist/cjs/map/scene3DController.js.map +0 -1
  145. package/dist/cjs/repository/repository.js +0 -694
  146. package/dist/cjs/repository/repository.js.map +0 -1
  147. package/dist/cjs/repository/requester.js +0 -771
  148. package/dist/cjs/repository/requester.js.map +0 -1
  149. package/dist/cjs/sdk.js +0 -438
  150. package/dist/cjs/sdk.js.map +0 -1
  151. package/dist/cjs/utils/3d.js +0 -245
  152. package/dist/cjs/utils/3d.js.map +0 -1
  153. package/dist/cjs/utils/deckLayers.js +0 -153
  154. package/dist/cjs/utils/deckLayers.js.map +0 -1
  155. package/dist/cjs/utils/error.js +0 -19
  156. package/dist/cjs/utils/error.js.map +0 -1
  157. package/dist/cjs/utils/images.js +0 -25
  158. package/dist/cjs/utils/images.js.map +0 -1
  159. package/dist/cjs/utils/loadGlb.js +0 -190
  160. package/dist/cjs/utils/loadGlb.js.map +0 -1
  161. package/dist/cjs/utils/mapvxHostname.js +0 -21
  162. package/dist/cjs/utils/mapvxHostname.js.map +0 -1
  163. package/dist/cjs/utils/nav-functions.js +0 -150
  164. package/dist/cjs/utils/nav-functions.js.map +0 -1
  165. package/dist/cjs/utils/opening-hours-helper.js +0 -914
  166. package/dist/cjs/utils/opening-hours-helper.js.map +0 -1
  167. package/dist/cjs/utils/preconnect.js +0 -131
  168. package/dist/cjs/utils/preconnect.js.map +0 -1
  169. package/dist/cjs/utils/route-utils.js +0 -57
  170. package/dist/cjs/utils/route-utils.js.map +0 -1
  171. package/dist/cjs/utils/semaphore.js +0 -143
  172. package/dist/cjs/utils/semaphore.js.map +0 -1
  173. package/dist/cjs/utils/three.js +0 -113
  174. package/dist/cjs/utils/three.js.map +0 -1
  175. package/dist/cjs/utils/threeScene.js +0 -126
  176. package/dist/cjs/utils/threeScene.js.map +0 -1
  177. package/dist/cjs/utils/update-css.js +0 -76
  178. package/dist/cjs/utils/update-css.js.map +0 -1
  179. package/dist/cjs/utils/utils.js +0 -104
  180. package/dist/cjs/utils/utils.js.map +0 -1
@@ -1,2766 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.InternalMapVXMap = void 0;
37
- const maplibre_gl_1 = __importStar(require("maplibre-gl"));
38
- const semaphore_1 = require("../utils/semaphore");
39
- /** Shared GeoJSON source holding every circle drawn through the circle API. */
40
- const CIRCLE_SOURCE_ID = "mapvx-circles";
41
- /** Fill layer rendering the translucent interior of the circles. */
42
- const CIRCLE_FILL_LAYER_ID = "mapvx-circles-fill";
43
- /** Line layer rendering the circle outlines. */
44
- const CIRCLE_LINE_LAYER_ID = "mapvx-circles-line";
45
- /**
46
- * Layer-id suffix per circle render order. The default placement keeps the
47
- * unsuffixed ids so existing consumers referencing them keep working.
48
- */
49
- const CIRCLE_LAYER_SUFFIXES = {
50
- aboveBasemap: "",
51
- belowLabels: "-below-labels",
52
- top: "-top",
53
- };
54
- /**
55
- * How many times a failed vector tile is reloaded before it is dropped from the retry
56
- * queue. Bounded so a permanently unavailable tile stops being requested, while a tile
57
- * that failed on a transient network error still gets several chances.
58
- */
59
- const MAX_TILE_RETRIES = 5;
60
- /**
61
- * Base delay for the retry backoff. Attempt N waits `BASE * 2^(N-1)` before it is
62
- * eligible again — 1s, 2s, 4s, 8s — so a tile that keeps failing is retried ever less
63
- * often instead of once per timer tick.
64
- */
65
- const TILE_RETRY_BASE_DELAY_MS = 1000;
66
- /**
67
- * Safety deadline for {@link InternalMapVXMap.setStyle}'s `onStyleReady` callback. The
68
- * callback normally fires on the map's `idle` event, but `idle` may never arrive (the
69
- * style request failed, the swap never settles). Callers hide the map behind a loading
70
- * overlay until the callback arrives, so it must be released no matter what.
71
- */
72
- const STYLE_READY_TIMEOUT_MS = 4000;
73
- /**
74
- * Frames a floor change waits before it will believe the indoor tiles are
75
- * already rebuilt (see `updateMarkersWhenFloorSettles`). Genuinely a frame
76
- * count: the reload is dispatched on the map's next render, so this is how
77
- * many renders it gets to be observed in.
78
- */
79
- const FLOOR_SETTLE_MIN_FRAMES = 3;
80
- /**
81
- * Longest a floor change waits for the new floor's tiles before switching the
82
- * HTML markers anyway, so a stalled source never hides markers for good.
83
- *
84
- * A deadline rather than a frame count. The panels this matters on run at
85
- * 20-30 fps, where a 60-frame cap is two to three seconds — and the wait also
86
- * gates `addMarker`, so overshooting it is not merely a late marker switch.
87
- */
88
- const FLOOR_SETTLE_MAX_MS = 1000;
89
- /** Fill/line layer ids for one circle render-order bucket. */
90
- function circleLayerIdsFor(order) {
91
- const suffix = CIRCLE_LAYER_SUFFIXES[order];
92
- return { fill: CIRCLE_FILL_LAYER_ID + suffix, line: CIRCLE_LINE_LAYER_ID + suffix };
93
- }
94
- /** True for the layer ids owned by the circle API, regardless of bucket. */
95
- function isCircleLayerId(id) {
96
- return id.startsWith(CIRCLE_FILL_LAYER_ID) || id.startsWith(CIRCLE_LINE_LAYER_ID);
97
- }
98
- // Flag to track if cached-tile protocol has been registered
99
- let cachedTileProtocolRegistered = false;
100
- /**
101
- * Deep clone helper that works in all environments.
102
- * Uses structuredClone if available, otherwise falls back to JSON serialization.
103
- * @param obj The object to clone
104
- * @returns A deep clone of the object
105
- */
106
- function deepClone(obj) {
107
- if (obj === undefined || obj === null) {
108
- return obj;
109
- }
110
- if (typeof structuredClone === "function") {
111
- return structuredClone(obj);
112
- }
113
- return JSON.parse(JSON.stringify(obj));
114
- }
115
- /**
116
- * Register a custom protocol for cached tiles that routes requests through the main thread.
117
- * This allows the service worker to intercept and cache tile requests.
118
- *
119
- * Wraps fetches in a {@link Semaphore} so tile CDN / WAFs are not hit with
120
- * unbounded parallel requests (same host as the service worker cache). The
121
- * semaphore is created once with the limit from the first map's config; later
122
- * calls are no-ops because the protocol can only be registered once globally
123
- * on MapLibre.
124
- *
125
- * @param maxConcurrentFetches - Maximum number of in-flight tile fetches.
126
- */
127
- function registerCachedTileProtocol(maxConcurrentFetches) {
128
- if (cachedTileProtocolRegistered)
129
- return;
130
- const semaphore = new semaphore_1.Semaphore(maxConcurrentFetches);
131
- maplibre_gl_1.default.addProtocol("cached-tile", (params, abortController) => {
132
- const url = params.url.replace("cached-tile://", "https://");
133
- // Pass the abort signal to acquire() so a request cancelled while still
134
- // queued (e.g. during rapid zoom) drops out of the FIFO queue instead of
135
- // waiting for a slot just to bail. release() runs only inside this chain,
136
- // i.e. only after a slot was actually granted.
137
- return semaphore.acquire(abortController.signal).then(() => fetch(url, { signal: abortController.signal })
138
- .then((response) => {
139
- if (!response.ok) {
140
- throw new Error(`HTTP error! status: ${response.status}`);
141
- }
142
- return response.arrayBuffer();
143
- })
144
- .then((data) => ({ data }))
145
- .finally(() => {
146
- semaphore.release();
147
- }));
148
- });
149
- cachedTileProtocolRegistered = true;
150
- }
151
- /**
152
- * Converts a PaddingValue to pixels based on the container and viewport dimensions.
153
- * @param value The padding value (number or string with unit)
154
- * @param dimension The reference dimension in pixels (container width/height)
155
- * @returns The padding value in pixels
156
- */
157
- function parsePaddingValue(value, dimension) {
158
- if (value === undefined) {
159
- return 0;
160
- }
161
- if (typeof value === "number") {
162
- return value;
163
- }
164
- const match = value.match(/^(-?\d*\.?\d+)(px|%|vh|vw)?$/);
165
- if (!match) {
166
- return 0;
167
- }
168
- const numericValue = Number.parseFloat(match[1]);
169
- const unit = match[2] || "px";
170
- switch (unit) {
171
- case "px":
172
- return numericValue;
173
- case "%":
174
- return (numericValue / 100) * dimension;
175
- case "vh":
176
- return (numericValue / 100) * window.innerHeight;
177
- case "vw":
178
- return (numericValue / 100) * window.innerWidth;
179
- default:
180
- return numericValue;
181
- }
182
- }
183
- /**
184
- * Converts PaddingOptions with mixed units to MapLibre's pixel-based padding format.
185
- * @param padding The padding options with optional fields and mixed units
186
- * @param containerWidth The map container width in pixels
187
- * @param containerHeight The map container height in pixels
188
- * @returns Padding object with all values in pixels
189
- */
190
- function convertPaddingToPixels(padding, containerWidth, containerHeight) {
191
- if (!padding) {
192
- return { top: 0, bottom: 0, left: 0, right: 0 };
193
- }
194
- return {
195
- top: parsePaddingValue(padding.top, containerHeight),
196
- bottom: parsePaddingValue(padding.bottom, containerHeight),
197
- left: parsePaddingValue(padding.left, containerWidth),
198
- right: parsePaddingValue(padding.right, containerWidth),
199
- };
200
- }
201
- const icons_1 = require("../assets/icons");
202
- const markerLayoutController_1 = require("../controllers/markerLayoutController");
203
- const routeController_1 = require("../controllers/routeController");
204
- const _rtl_1 = require("../domain/models/_rtl");
205
- const animation_1 = require("../domain/models/animation");
206
- const circle_1 = require("../domain/models/circle");
207
- const loggeable_1 = require("../domain/models/loggeable");
208
- const mapConfig_1 = require("../domain/models/mapConfig");
209
- const marker_1 = require("../domain/models/marker");
210
- const route_1 = require("../domain/models/route");
211
- const routeConfiguration_1 = require("../domain/models/routeConfiguration");
212
- const repository_1 = require("../repository/repository");
213
- const route_utils_1 = require("../utils/route-utils");
214
- const utils_1 = require("../utils/utils");
215
- const scene3DController_1 = require("./scene3DController");
216
- const fitCamera_1 = require("./fitCamera");
217
- const liveMapFitTransform_1 = require("./liveMapFitTransform");
218
- const mapInteractionOptions_1 = require("./mapInteractionOptions");
219
- /**
220
- * Class to interact with the map.
221
- * @category Map
222
- * @hidden
223
- */
224
- class InternalMapVXMap extends loggeable_1.Loggeable {
225
- /** Whether a floor change is still rebuilding, so markers wait for it. */
226
- get floorSettlePending() {
227
- return this.floorSettleDeadline != null && this.now() < this.floorSettleDeadline;
228
- }
229
- /** Wall clock for the settle deadline; `Date.now` where `performance` is absent. */
230
- now() {
231
- return typeof performance !== "undefined" ? performance.now() : Date.now();
232
- }
233
- /**
234
- * Creates a new map instance with the parameters specified in mapConfig and assigns it to the provided HTML container.
235
- * @param mapConfig Configuration parameters for the map.
236
- * @param container Container where the assignment of the created map is required.
237
- * @param parentPlace Parent place required for map floor mapping, along with the display of subplaces.
238
- * @param token The token provided for the creation and fetching of map information.
239
- * @returns A new instance of MapVXMap.
240
- */
241
- constructor(mapConfig, container, token) {
242
- var _a, _b, _c, _d;
243
- super();
244
- /**
245
- * Monotonic counter for {@link addLiveRoute} calls. Refreshes overlap (each one
246
- * awaits a fetch), so only the call holding the latest ticket may touch the
247
- * map — an older response resolving last is discarded instead of clobbering
248
- * the newer route.
249
- */
250
- this.liveRouteTicket = 0;
251
- this.potentialParentPlaces = [];
252
- this.innerFloors = [];
253
- this.subPlaces = [];
254
- this.currentFloor = "";
255
- this.baseFilters = {};
256
- /**
257
- * Layer ids whose declared (base) filter has already been resolved — either captured
258
- * into {@link baseFilters} or confirmed absent.
259
- *
260
- * `baseFilters[id] === undefined` cannot carry that information on its own: it means
261
- * both "not looked at yet" and "looked at, the layer declares no filter". Without the
262
- * distinction, a later pass re-reads the layer's LIVE filter — which by then is the
263
- * one this class wrote — and stores our own floor/public-place filter as if it were
264
- * the style's, so the layer keeps filtering by a stale floor forever.
265
- *
266
- * An id lands here the moment its declared filter is read from the style spec, or the
267
- * moment this class writes a filter to that layer, whichever happens first. Layers
268
- * added asynchronously after a `setStyle` swap are in neither situation, so their
269
- * genuinely pristine live filter is still captured on first touch.
270
- */
271
- this.capturedBaseFilters = new Set();
272
- /**
273
- * Monotonic id of the most recently STARTED style swap. Every {@link setStyle} call
274
- * captures its own value and checks it again in each async continuation, so a
275
- * superseded swap can neither touch the shared bookkeeping (`styleReadyTimer`,
276
- * `pendingStyleSwapListener`) nor apply its obsolete style to the live map.
277
- *
278
- * A per-call `readied` flag is not enough: the timer and the listener slot are
279
- * single-valued instance state, so two overlapping swaps otherwise clobber each
280
- * other's — one caller's overlay is released by the other's failure, or an
281
- * abandoned listener re-seeds filters from layer ids the current style lacks.
282
- */
283
- this.styleSwapGeneration = 0;
284
- this.markers = [];
285
- this.circles = [];
286
- this.enableHover = false;
287
- this.hoveredId = "unselected";
288
- this.failedTiles = new Set();
289
- /**
290
- * Retry bookkeeping per failed tile. A WeakMap so it can never outlive the tiles
291
- * themselves — entries disappear with the tile objects, which is why none of the
292
- * `failedTiles.clear()` paths have to reset it.
293
- */
294
- this.tileRetryState = new WeakMap();
295
- this.geoLocation = navigator.geolocation;
296
- /** Floor-change settle bookkeeping (see `updateMarkersWhenFloorSettles`). */
297
- this.floorSettleToken = 0;
298
- /**
299
- * When the floor-change settle stops holding markers back, or `null` when no
300
- * change is in flight.
301
- *
302
- * A deadline rather than a flag, because `addMarker` reads it: the poll that
303
- * would clear a flag runs on `requestAnimationFrame`, which does not fire in
304
- * a backgrounded tab and does not run at all for a map torn down through
305
- * MapLibre's own `remove()` rather than `destroyMap`. A flag left set there
306
- * makes every later `addMarker` skip the call that attaches the marker, for
307
- * good; a deadline expires on its own.
308
- */
309
- this.floorSettleDeadline = null;
310
- /** Owns the deck.gl overlay + GLB scene; created on style load only when 3D is active. */
311
- this.scene3D = undefined;
312
- if (_rtl_1.rtlLanguages.includes((_a = mapConfig.lang) !== null && _a !== void 0 ? _a : "")) {
313
- this.setRTLSupport();
314
- }
315
- this.repository = new repository_1.Repository(token, (_b = mapConfig.lang) !== null && _b !== void 0 ? _b : "en", mapConfig.apiUrl, undefined, mapConfig.mapvxRequestContext, {
316
- routeApiUrl: mapConfig.routeApiUrl,
317
- healthApiUrl: mapConfig.healthApiUrl,
318
- legacyApiUrl: mapConfig.legacyApiUrl,
319
- portalApiUrl: mapConfig.portalApiUrl,
320
- });
321
- this.token = token;
322
- this.authToken = mapConfig.authToken;
323
- this.currentLang = (_c = mapConfig.lang) !== null && _c !== void 0 ? _c : "en";
324
- this.parentPlaceId = mapConfig.parentPlaceId;
325
- this.routeController = new routeController_1.RouteController(this);
326
- this.userLocationMarker = undefined;
327
- this.watchPositionID = undefined;
328
- this.onFloorChange = mapConfig.onFloorChange;
329
- this.onParentPlaceChange = mapConfig.onParentPlaceChange;
330
- this.mode = (_d = mapConfig.mode) !== null && _d !== void 0 ? _d : "2D";
331
- this.styleRef = mapConfig.styleRef;
332
- this.styleTheme = mapConfig.styleTheme;
333
- this.scene3DConfig = mapConfig.scene3D;
334
- this.initialCenter = mapConfig.center;
335
- this.tileCacheConfig = (() => {
336
- const merged = Object.assign(Object.assign({}, mapConfig_1.DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
337
- merged.maxTiles = Math.min(merged.maxTiles, mapConfig_1.MAPLIBRE_MAX_TILE_CACHE_HARD_CAP);
338
- return merged;
339
- })();
340
- this.markerLayoutConfig = Object.assign(Object.assign({}, mapConfig_1.DEFAULT_MARKER_LAYOUT_CONFIG), mapConfig.markerLayout);
341
- this.markerLayoutController = new markerLayoutController_1.MarkerLayoutController(this, this.markerLayoutConfig);
342
- // `container` is the very element handed to MapLibre below, and every
343
- // marker is mounted inside it, so publishing the gap/width variables here
344
- // covers markers added before the map's "load" event fires.
345
- this.markerLayoutController.applyLayoutVariables(container);
346
- if (mapConfig.parentPlaceId != null) {
347
- this.initialPlaceDetailSetUp(mapConfig.parentPlaceId, mapConfig.authToken);
348
- }
349
- if (mapConfig.otherPotentialParentPlacesIds &&
350
- mapConfig.otherPotentialParentPlacesIds.length > 0) {
351
- this.loadOtherPotentialParentPlaces(mapConfig.otherPotentialParentPlacesIds);
352
- }
353
- this.enableHover = mapConfig.enableHover || false;
354
- this.initialMapStyleSetUp(mapConfig, container);
355
- }
356
- setRTLSupport() {
357
- (0, maplibre_gl_1.setRTLTextPlugin)("https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.3.0/dist/mapbox-gl-rtl-text.js", false).catch(() => {
358
- // RTL support may fail in some browsers - this is expected
359
- });
360
- }
361
- /**
362
- * Transform style sources to use cached-tile:// protocol for tile URLs.
363
- * This enables the service worker to intercept and cache tile requests.
364
- * @param style The original style specification
365
- * @returns A new style with transformed tile URLs
366
- */
367
- transformStyleForCaching(style) {
368
- if (!style) {
369
- return style;
370
- }
371
- const transformedStyle = deepClone(style);
372
- if (transformedStyle === null || transformedStyle === void 0 ? void 0 : transformedStyle.sources) {
373
- for (const sourceKey of Object.keys(transformedStyle.sources)) {
374
- const source = transformedStyle.sources[sourceKey];
375
- // Transform vector and raster tile sources
376
- // Only transform source.tiles (actual tile URLs), NOT source.url (TileJSON metadata URL)
377
- // source.url points to a TileJSON file that returns JSON, not binary tile data
378
- if (source.tiles && Array.isArray(source.tiles)) {
379
- source.tiles = source.tiles.map((tileUrl) => {
380
- if (tileUrl.startsWith("https://")) {
381
- return tileUrl.replace("https://", "cached-tile://");
382
- }
383
- return tileUrl;
384
- });
385
- }
386
- }
387
- }
388
- return transformedStyle;
389
- }
390
- initialPlaceDetailSetUp(parentPlaceId, withBearerToken) {
391
- if (withBearerToken) {
392
- this.repository
393
- .fetchAndParseAnyPlaceDetail({ id: parentPlaceId }, withBearerToken)
394
- .then((place) => {
395
- this.changeParentPlaceTo(place);
396
- this.potentialParentPlaces.push(place);
397
- })
398
- .catch(console.error);
399
- return;
400
- }
401
- this.repository
402
- .fetchAndParsePlaceDetail({ id: parentPlaceId })
403
- .then((place) => {
404
- this.changeParentPlaceTo(place);
405
- this.potentialParentPlaces.push(place);
406
- })
407
- .catch(console.error);
408
- }
409
- loadOtherPotentialParentPlaces(ids) {
410
- Promise.all(ids.map((id) => this.repository.fetchAndParsePlaceDetail({ id })))
411
- .then((places) => {
412
- this.potentialParentPlaces.push(...places);
413
- })
414
- .catch(console.error);
415
- }
416
- subPlacesLoad(parentPlaceId) {
417
- this.repository
418
- .fetchAndParseSubPlaces(parentPlaceId)
419
- .then((subPlaces) => {
420
- this.subPlaces = subPlaces;
421
- })
422
- .catch(console.error);
423
- }
424
- initialMapStyleSetUp(mapConfig, container) {
425
- if (mapConfig.institutionId && mapConfig.authToken) {
426
- this.repository
427
- .fetchAndParseAnyMapStyle(mapConfig.institutionId, mapConfig.authToken)
428
- .then((style) => {
429
- return this.onMapStyleLoaded(mapConfig, container, style);
430
- })
431
- .catch(console.error);
432
- return;
433
- }
434
- // Through the same helper as `setStyle` and `changeParentPlaceTo`. This PR
435
- // forwards `styleRef`/`styleTheme` to the initial load too, and a ref scoped
436
- // to the user's client rather than to the api key resolves only over bearer
437
- // — fetched over the api-key path it 4xx'd, and since this is the call that
438
- // creates the map, the map was never created at all.
439
- this.fetchVenueStyle(mapConfig.parentPlaceId, mapConfig.styleRef, mapConfig.styleTheme)
440
- .then((style) => {
441
- return this.onMapStyleLoaded(mapConfig, container, style);
442
- })
443
- .catch((error) => {
444
- this.logError(error, { functionName: "initialMapStyleSetUp" });
445
- });
446
- }
447
- onMapStyleLoaded(mapConfig, container, style) {
448
- var _a, _b, _c, _d, _e, _f, _g;
449
- // Determine if service worker caching should be enabled
450
- const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
451
- if (useServiceWorkerCaching) {
452
- registerCachedTileProtocol(this.tileCacheConfig.maxConcurrentTileFetches);
453
- }
454
- // Transform tile URLs only if service worker caching is enabled
455
- const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
456
- const mapOptions = Object.assign({ container, style: finalStyle, center: mapConfig.center, zoom: mapConfig.zoom, pitch: (_a = mapConfig.pitch) !== null && _a !== void 0 ? _a : 0, attributionControl: false, maplibreLogo: false, bearingSnap: (_b = mapConfig.bearingSnap) !== null && _b !== void 0 ? _b : 0, cancelPendingTileRequestsWhileZooming: true,
457
- // Use configured maxTiles for MapLibre's memory cache
458
- maxTileCacheSize: this.tileCacheConfig.maxTiles }, (0, mapInteractionOptions_1.buildInteractionOptions)(mapConfig));
459
- if (mapConfig.maxZoom)
460
- mapOptions.maxZoom = mapConfig.maxZoom;
461
- if (mapConfig.minZoom)
462
- mapOptions.minZoom = mapConfig.minZoom;
463
- // Reduced internal resolution on request (fill-rate relief for low-end GPUs);
464
- // MapLibre keeps devicePixelRatio when the option is left out.
465
- if (mapConfig.pixelRatio !== undefined)
466
- mapOptions.pixelRatio = mapConfig.pixelRatio;
467
- // `0` disables the post-gesture fade tail entirely; keep MapLibre's default otherwise.
468
- if (mapConfig.fadeDuration !== undefined)
469
- mapOptions.fadeDuration = mapConfig.fadeDuration;
470
- if (mapConfig.maxBounds && mapConfig.maxBounds.length > 1) {
471
- const boundingBox = (0, utils_1.getBoundingBox)(mapConfig.maxBounds);
472
- mapOptions.maxBounds = new maplibre_gl_1.LngLatBounds([boundingBox[0].lng, boundingBox[0].lat], [boundingBox[1].lng, boundingBox[1].lat]);
473
- }
474
- this.map = new maplibre_gl_1.Map(mapOptions);
475
- // Create the 3D controller only when the map opts into 3D. All heavy WebGL / deck.gl work is
476
- // owned by it; in 2D mode `this.scene3D` stays undefined and every hook below is a no-op.
477
- if (this.is3DSceneActive() && this.scene3DConfig) {
478
- this.scene3D = new scene3DController_1.Scene3DController(this.map, this.scene3DConfig, this.initialCenter, mapConfig.onScene3DModelsProgress);
479
- }
480
- // When rotation is disabled we still want pinch-to-zoom to work, so the
481
- // two-finger rotation is turned off here instead of via the constructor.
482
- if ((0, mapInteractionOptions_1.shouldDisableTouchRotation)(mapConfig)) {
483
- (_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.disableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
484
- }
485
- this.map.addControl(new maplibre_gl_1.NavigationControl({
486
- showCompass: (_e = mapConfig.showCompass) !== null && _e !== void 0 ? _e : true,
487
- showZoom: (_f = mapConfig.showZoom) !== null && _f !== void 0 ? _f : true,
488
- }), (_g = mapConfig.navigationPosition) !== null && _g !== void 0 ? _g : "top-right");
489
- this.map.on("load", () => {
490
- var _a, _b;
491
- // 3D libraries load asynchronously; the rest of the load flow does not wait on them.
492
- void ((_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.onMapLoad());
493
- this.whenStyleUpdates(style);
494
- if (mapConfig.lang)
495
- this.setLayersForLanguage(mapConfig.lang);
496
- (_b = mapConfig.onMapReady) === null || _b === void 0 ? void 0 : _b.call(mapConfig);
497
- this.onHover();
498
- this.subscribeToFailedTiles();
499
- this.markerLayoutController.attach();
500
- });
501
- // Self-healing for circles: a full style reload wipes custom sources and
502
- // layers, and not every restyle path goes through whenStyleUpdates.
503
- this.map.on("styledata", () => {
504
- if (this.circles.length > 0)
505
- this.ensureCircleLayers();
506
- });
507
- this.map.on("zoom", () => {
508
- var _a;
509
- (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.onZoom();
510
- });
511
- this.map.on("zoomend", () => {
512
- var _a;
513
- (_a = mapConfig.onZoomEnd) === null || _a === void 0 ? void 0 : _a.call(mapConfig, this.getZoomLevel());
514
- });
515
- this.map.on("rotate", () => {
516
- var _a;
517
- (_a = mapConfig.onRotate) === null || _a === void 0 ? void 0 : _a.call(mapConfig, this.map.getBearing());
518
- });
519
- this.defaultClickListener();
520
- }
521
- setBaseFilters(style) {
522
- if (!style)
523
- return;
524
- const publicPlaceFilter = ["==", ["get", "public_place"], true];
525
- style.layers
526
- .filter((layer) => layer.id.startsWith("indoor-"))
527
- .forEach((layer) => {
528
- // Seed from the STYLE SPEC — the only pristine source — and do it BEFORE the
529
- // setFilter below overwrites the live filter. Reading it back afterwards would
530
- // return our own `publicPlaceFilter` and store it as the layer's declared one,
531
- // turning `public_place` from an OR into an AND and hiding every private
532
- // feature on the selected floor. Every indoor layer is covered, not just
533
- // fill/line/symbol: a `circle` layer is discriminated by its filter just the
534
- // same, and dropping it makes the layer repaint every feature.
535
- // `filter` is absent from the union member for `background` layers, hence the
536
- // structural read rather than `layer.filter`.
537
- const declared = layer.filter;
538
- this.captureDeclaredBaseFilter(layer.id, declared);
539
- const baseFilter = this.baseFilters[layer.id];
540
- if (baseFilter != null) {
541
- const finalFilter = ["all", baseFilter, publicPlaceFilter];
542
- this.map.setFilter(layer.id, finalFilter);
543
- }
544
- else {
545
- this.map.setFilter(layer.id, publicPlaceFilter);
546
- }
547
- });
548
- }
549
- /**
550
- * Records a layer's declared (base) filter the first time the layer is seen, from an
551
- * explicit value (a style-spec `filter`) when one is available, otherwise by reading
552
- * the live filter — which is only pristine for layers this class has never written to.
553
- *
554
- * Marking happens even when no filter exists, so the layer is never revisited: on a
555
- * second visit its live filter is one we produced, and storing that as the "declared"
556
- * filter is what freezes a layer to a stale floor.
557
- *
558
- * @param layerId Id of the layer whose base filter should be resolved.
559
- * @param declared Declared filter straight from the style spec, when the caller has it.
560
- */
561
- captureDeclaredBaseFilter(layerId, declared) {
562
- if (this.capturedBaseFilters.has(layerId))
563
- return;
564
- this.capturedBaseFilters.add(layerId);
565
- const baseFilter = declared !== null && declared !== void 0 ? declared : this.safeGetFilter(layerId);
566
- if (baseFilter != null) {
567
- this.baseFilters[layerId] = baseFilter;
568
- }
569
- }
570
- setLayersForLanguage(lang) {
571
- var _a, _b;
572
- // outdoor layers
573
- const layersName = [
574
- "aerodrome_label",
575
- "mountain_peak",
576
- "park",
577
- "place",
578
- "poi",
579
- "transportation_name",
580
- "water_name",
581
- "waterway",
582
- ];
583
- const layers = (_b = (_a = this.map.getStyle()) === null || _a === void 0 ? void 0 : _a.layers) !== null && _b !== void 0 ? _b : [];
584
- const layersIds = layers
585
- .filter((layer) => {
586
- var _a;
587
- return layer.type === "symbol" &&
588
- layer.source === "openmaptiles" &&
589
- layersName.includes((_a = layer["source-layer"]) !== null && _a !== void 0 ? _a : "");
590
- })
591
- .map((layer) => {
592
- return layer.id;
593
- });
594
- layersIds.forEach((id) => {
595
- try {
596
- const property = this.map.getLayoutProperty(id, "symbol-placement");
597
- this.map.setLayoutProperty(id, "text-field", [
598
- "case",
599
- ["has", `name:${lang}`],
600
- ["get", `name:${lang}`],
601
- [
602
- "format",
603
- ["get", "name:latin"],
604
- property === "line" ? " " : "\n",
605
- ["get", "name:nonlatin"],
606
- ],
607
- ]);
608
- }
609
- catch (error) {
610
- this.debugLog("Layer language update failed (layer may not exist)", error);
611
- }
612
- });
613
- // indoor layers
614
- const indoorLayerIds = layers
615
- .filter((layer) => {
616
- var _a;
617
- return layer.type === "symbol" &&
618
- layer.source === "indoorequal" &&
619
- ((_a = layer.layout) === null || _a === void 0 ? void 0 : _a["text-field"]) !== undefined;
620
- })
621
- .map((layer) => {
622
- return layer.id;
623
- });
624
- indoorLayerIds.forEach((id) => {
625
- try {
626
- this.map.setLayoutProperty(id, "text-field", [
627
- "case",
628
- ["has", `name:${lang}`],
629
- ["get", `name:${lang}`],
630
- ["get", "name"],
631
- ]);
632
- }
633
- catch (error) {
634
- this.debugLog("Layer language update failed (layer may not exist)", error);
635
- }
636
- });
637
- }
638
- /**
639
- * Add values to identify the client.
640
- * @param segmentation The pair values to use as segmentation.
641
- * @returns The segmentation with the added values.
642
- */
643
- prepareSegmentation(segmentation) {
644
- var _a;
645
- return Object.assign(Object.assign({}, segmentation), { token: this.token, parentPlaceId: (_a = this.parentPlaceId) !== null && _a !== void 0 ? _a : "None" });
646
- }
647
- destroyMap() {
648
- var _a;
649
- this.circles = [];
650
- this.markerLayoutController.destroy();
651
- // Drop any in-flight `setStyle` bookkeeping while the map still exists: the
652
- // listener has to be detached before `map.remove()`, and the safety timer must not
653
- // outlive the map and call `onStyleReady` for a map that is already gone.
654
- this.clearPendingStyleSwapListener();
655
- if (this.styleReadyTimer) {
656
- clearTimeout(this.styleReadyTimer);
657
- this.styleReadyTimer = undefined;
658
- }
659
- // Drop any floor-settle poll in flight: bumping the token makes the next
660
- // `requestAnimationFrame` callback return instead of re-queueing itself and
661
- // re-attaching markers to a map that has already been removed.
662
- this.floorSettleToken++;
663
- this.floorSettleDeadline = null;
664
- // Dispose the 3D scene's GPU resources before the map (and its shared WebGL context) go away —
665
- // MapLibre's own teardown never calls the custom GLB layer's `onRemove`.
666
- (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.dispose();
667
- this.map.remove();
668
- this.unsubscribeFromFailedTiles();
669
- }
670
- clearTileCache() {
671
- try {
672
- // Clear the failed tiles tracking set
673
- this.failedTiles.clear();
674
- // Force MapLibre to reload all visible tiles by triggering a repaint
675
- // Note: MapLibre doesn't expose a direct cache clear method, so we
676
- // trigger a style refresh which causes tiles to be re-requested
677
- if (this.map) {
678
- this.map.triggerRepaint();
679
- }
680
- this.logEvent("clearTileCache");
681
- }
682
- catch (error) {
683
- this.debugLog("Tile cache clear failed (cache may not exist)", error);
684
- }
685
- }
686
- /**
687
- * Whether 3D scene features (deck.gl overlay, declarative GLB custom layer, etc.) are active.
688
- * Requires {@link MapConfig.mode} `"3D"` and {@link Scene3DConfig.enabled} not `false`.
689
- * The heavy WebGL / deck.gl work lives in {@link Scene3DController}; this only decides whether one
690
- * is created and wired up. When `scene3D` is omitted, 3D features remain available.
691
- */
692
- is3DSceneActive() {
693
- var _a;
694
- return this.mode === "3D" && ((_a = this.scene3DConfig) === null || _a === void 0 ? void 0 : _a.enabled) !== false;
695
- }
696
- getCurrentFloor() {
697
- var _a;
698
- return (_a = this.currentFloor) !== null && _a !== void 0 ? _a : "";
699
- }
700
- /**
701
- * Sets the route path used for GLB route-transparency collision, so 3D models a route passes
702
- * through fade and reveal the floor plan underneath. Called by {@link RouteController} whenever
703
- * the set of drawn routes changes. Points are tagged with the floor their segment is drawn on;
704
- * only points on the displayed floor participate. Pass an empty array to clear.
705
- *
706
- * No-op unless the 3D scene is active.
707
- */
708
- setRouteCollisionPath(points) {
709
- var _a;
710
- if (!this.is3DSceneActive()) {
711
- return;
712
- }
713
- (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.setRouteCollisionPath(points);
714
- }
715
- /**
716
- * Performs changes when parent place is updated
717
- * @param place new parent place
718
- * @param updateStyle defines if new parent place style must be set
719
- */
720
- changeParentPlaceTo(place, updateStyle = false, onStyleReady) {
721
- var _a, _b, _c, _d, _e, _f, _g;
722
- this.parentPlace = place;
723
- this.parentPlaceId = place === null || place === void 0 ? void 0 : place.mapvxId;
724
- this.innerFloors = (_b = (_a = place === null || place === void 0 ? void 0 : place.innerFloors) === null || _a === void 0 ? void 0 : _a.sort((a, b) => a.index - b.index)) !== null && _b !== void 0 ? _b : [];
725
- this.currentFloor =
726
- (_f = (_d = (_c = this.innerFloors.find((floor) => floor.defaultFloor)) === null || _c === void 0 ? void 0 : _c.key) !== null && _d !== void 0 ? _d : (_e = this.innerFloors[0]) === null || _e === void 0 ? void 0 : _e.key) !== null && _f !== void 0 ? _f : "";
727
- if (place) {
728
- this.subPlacesLoad(place.mapvxId);
729
- }
730
- (_g = this.scene3D) === null || _g === void 0 ? void 0 : _g.onFloorChange(this.currentFloor);
731
- if (updateStyle) {
732
- this.fetchVenueStyle(place === null || place === void 0 ? void 0 : place.mapvxId, this.styleRef, this.styleTheme)
733
- .then((style) => {
734
- const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
735
- const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
736
- this.map.setStyle(finalStyle);
737
- // The new place's style declares its own filters for the same `indoor-*` layer
738
- // ids. Without this reset every id is still marked captured from the previous
739
- // place, so `captureDeclaredBaseFilter` early-returns for all of them and the
740
- // previous place's expressions keep filtering the new style forever. (The
741
- // `switch (layer.type)` block that used to re-read them on every style load was
742
- // removed when capture became once-per-id, which is what made this necessary.)
743
- this.resetBaseFilters();
744
- this.whenStyleUpdates(finalStyle);
745
- onStyleReady === null || onStyleReady === void 0 ? void 0 : onStyleReady();
746
- })
747
- .catch((error) => {
748
- this.logError(error, { functionName: "changeParentPlaceTo" });
749
- // Release the caller either way. `onStyleReady` is what a consumer
750
- // hides its loading overlay on, so swallowing the failure here left
751
- // the overlay up for good rather than merely unstyled.
752
- onStyleReady === null || onStyleReady === void 0 ? void 0 : onStyleReady();
753
- });
754
- }
755
- }
756
- /**
757
- * Fetch the venue style over whichever endpoint this map is authorized on.
758
- *
759
- * A bearer token authorizes by the user's client access rather than the api
760
- * key's scopes, so a client's branded style resolves without granting that
761
- * place to a shared api key; api-key consumers keep the
762
- * `/api/sdk/v2/mapStyleJson` path.
763
- *
764
- * Both callers go through here so they cannot disagree. They used to: once
765
- * `setStyle` had stored a `styleRef` it resolved over bearer, the next
766
- * parent-place change refetched that same ref over the api-key path, whose
767
- * key does not scope it — a non-ok response, a rejected promise, and a style
768
- * that was never applied, with `onStyleReady` left unfired under a caller's
769
- * loading overlay.
770
- */
771
- fetchVenueStyle(placeId, styleRef, styleTheme) {
772
- return this.authToken
773
- ? this.repository.fetchAndParseMapStyleByBearer(placeId, this.authToken, styleRef, styleTheme)
774
- : this.repository.fetchAndParseMapStyle(placeId, styleRef, styleTheme);
775
- }
776
- /**
777
- * Update the parent place and the current floor.
778
- * Does not update layers, markers, etc.
779
- * @param parentPlaceId
780
- * @param floorId
781
- * @throws A error if the parent place is not one of the allowed places to show.
782
- */
783
- updatePlaceAndFloor(parentPlaceId, floorId) {
784
- var _a, _b, _c, _d, _e, _f;
785
- const place = this.potentialParentPlaces.find((place) => place.mapvxId === parentPlaceId);
786
- this.parentPlace = place;
787
- this.parentPlaceId = place === null || place === void 0 ? void 0 : place.mapvxId;
788
- this.innerFloors = (_a = place === null || place === void 0 ? void 0 : place.innerFloors.sort((a, b) => a.index - b.index)) !== null && _a !== void 0 ? _a : [];
789
- this.currentFloor =
790
- (_e = (_c = floorId !== null && floorId !== void 0 ? floorId : (_b = this.innerFloors.find((floor) => floor.defaultFloor)) === null || _b === void 0 ? void 0 : _b.key) !== null && _c !== void 0 ? _c : (_d = this.innerFloors[0]) === null || _d === void 0 ? void 0 : _d.key) !== null && _e !== void 0 ? _e : "";
791
- (_f = this.scene3D) === null || _f === void 0 ? void 0 : _f.onFloorChange(this.currentFloor);
792
- }
793
- updateParentPlaceAndFloor(parentPlaceId, floorId, options) {
794
- var _a, _b, _c, _d;
795
- try {
796
- this.updatePlaceAndFloor(parentPlaceId, floorId);
797
- this.filterByFloorKey(this.currentFloor);
798
- (_a = this.onParentPlaceChange) === null || _a === void 0 ? void 0 : _a.call(this, parentPlaceId);
799
- (_b = this.onFloorChange) === null || _b === void 0 ? void 0 : _b.call(this, (_c = this.currentFloor) !== null && _c !== void 0 ? _c : "");
800
- this.logEvent("updateParentPlaceAndFloor");
801
- (_d = options === null || options === void 0 ? void 0 : options.onComplete) === null || _d === void 0 ? void 0 : _d.call(options);
802
- }
803
- catch (error) {
804
- this.debugLog("Parent place/floor update failed", error);
805
- }
806
- }
807
- setLang(lang) {
808
- this.repository.setLang(lang);
809
- this.currentLang = lang;
810
- if (_rtl_1.rtlLanguages.includes(lang)) {
811
- this.setRTLSupport();
812
- }
813
- // setLayersForLanguage reads this.map.getStyle()?.layers, which is empty
814
- // until the style finishes loading. If setLang is called right after
815
- // createMap (before the "load" event), apply it once the style is ready
816
- // so the language change is not silently dropped.
817
- if (this.map.isStyleLoaded()) {
818
- this.setLayersForLanguage(lang);
819
- }
820
- else {
821
- this.map.once("load", () => this.setLayersForLanguage(lang));
822
- }
823
- }
824
- setParentPlace(place, updateStyle, onStyleReady) {
825
- this.changeParentPlaceTo(place, updateStyle, onStyleReady);
826
- }
827
- setStyle(styleRef, styleTheme, onStyleReady) {
828
- // Every swap gets a generation. All shared bookkeeping below is touched only while
829
- // this swap is still the current one, so two overlapping calls cannot release each
830
- // other's overlay or apply an obsolete style. `readied` stays per-call because each
831
- // caller's callback must still fire exactly once.
832
- const generation = ++this.styleSwapGeneration;
833
- const isCurrent = () => this.styleSwapGeneration === generation;
834
- let readied = false;
835
- const reportReady = (error) => {
836
- if (readied)
837
- return;
838
- readied = true;
839
- // Only the current swap owns the shared timer; a superseded swap clearing it here
840
- // would leave the live swap with no safety net (and its caller hidden forever).
841
- if (isCurrent() && this.styleReadyTimer) {
842
- clearTimeout(this.styleReadyTimer);
843
- this.styleReadyTimer = undefined;
844
- }
845
- onStyleReady === null || onStyleReady === void 0 ? void 0 : onStyleReady(error);
846
- };
847
- // `onStyleReady` must fire on EVERY path — success, a rejected style request, or a
848
- // swap that never settles — because callers keep the map hidden behind a loading
849
- // overlay until it arrives. The safety timer is therefore armed before the request
850
- // instead of inside `.then()`.
851
- if (this.styleReadyTimer)
852
- clearTimeout(this.styleReadyTimer);
853
- this.styleReadyTimer = setTimeout(reportReady, STYLE_READY_TIMEOUT_MS);
854
- // A previous swap may still hold a `styledata` listener whose style never reached the
855
- // loaded state. Drop it before starting a new one: it captured an obsolete style and
856
- // would otherwise fire on this swap, wiping `baseFilters` and calling `setFilter`
857
- // with layer ids the new style does not have.
858
- this.clearPendingStyleSwapListener();
859
- // `this.map` is created asynchronously (see `onMapStyleLoaded`), so a `setStyle` call
860
- // made right after `createMap` can land before it exists. Fail loudly instead of
861
- // letting `this.map.setStyle` throw into `.catch`, which reported READY and left the
862
- // caller revealing the previous style as if the swap had succeeded.
863
- if (!this.map) {
864
- const error = new Error("setStyle called before the map finished initializing; wait for the map's load event");
865
- this.logError(error, { functionName: "setStyle" });
866
- reportReady(error);
867
- return;
868
- }
869
- const stylePromise = this.fetchVenueStyle(this.parentPlaceId, styleRef, styleTheme);
870
- stylePromise
871
- .then((style) => {
872
- // A newer swap started while this style was in flight. Applying this one now
873
- // would fight the newer style layer by layer, so drop it and let the newer swap
874
- // own the map and its caller's callback.
875
- if (!isCurrent())
876
- return;
877
- // Remember what is actually on screen: `changeParentPlaceTo` refetches the
878
- // style with these, so without this an explicitly applied style would be
879
- // silently reverted to the constructor's on the next parent-place change.
880
- // After the generation check: a superseded swap never reached the map, so it
881
- // must not claim to be what is on it.
882
- this.styleRef = styleRef;
883
- this.styleTheme = styleTheme;
884
- const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
885
- const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
886
- // Full (non-diff) swap so the previewed style completely replaces the base —
887
- // a diff would keep old base layers and reappear on the next floor change.
888
- this.map.setStyle(finalStyle, { diff: false });
889
- // Drop the previous style's base filters SYNCHRONOUSLY, right where the layers
890
- // they describe stop existing. They are keyed by layer id and persist across
891
- // swaps, so any code reading them before the apply pass below (the `idle`
892
- // handler's `filterByFloorKey`, a floor change racing the swap) would AND the
893
- // previous style's discriminator into the new style's layers — the stale-filter
894
- // washout this reset exists to prevent. Gating it on `isStyleLoaded` left exactly
895
- // that window open. Fresh declared filters are re-captured lazily in
896
- // `applyLayerFilters` as each indoor layer is first touched.
897
- this.resetBaseFilters();
898
- // Re-apply floor filters, route/circle layers and markers ONLY once the new
899
- // style's sources/layers have finished loading; running them mid-reload throws
900
- // (`getSource` on an undefined style) and the floor filter would not stick.
901
- const applyOnceLoaded = () => {
902
- // Superseded: detach and leave the map to the newer swap. Without this the
903
- // listener stays attached (the slot it was tracked in has been overwritten)
904
- // and re-seeds filters from layer ids the current style does not have, which
905
- // makes maplibre throw and aborts the newer swap's own listener.
906
- if (!isCurrent()) {
907
- this.map.off("styledata", applyOnceLoaded);
908
- return;
909
- }
910
- if (!this.hasLoadedStyle())
911
- return;
912
- this.map.off("styledata", applyOnceLoaded);
913
- if (this.pendingStyleSwapListener === applyOnceLoaded) {
914
- this.pendingStyleSwapListener = undefined;
915
- }
916
- this.whenStyleUpdates(finalStyle);
917
- // A non-diff swap replaces every layer, so anything applied ON TOP of the
918
- // style — label language, caller-requested place highlights, the 3D scene's
919
- // GLB layer and hidden-layer overrides — is gone and has to be replayed.
920
- // `whenStyleUpdates` covers only floor filters, routes and circles.
921
- this.restoreStyleOverlays();
922
- this.logEvent("setStyle");
923
- };
924
- this.pendingStyleSwapListener = applyOnceLoaded;
925
- this.map.on("styledata", applyOnceLoaded);
926
- applyOnceLoaded();
927
- // Re-apply the floor filter and report ready once the map is idle. This is
928
- // intentionally OUTSIDE applyOnceLoaded's `isStyleLoaded` gate: the indoor
929
- // polygon layers settle a beat after the swap (so the floor filter in
930
- // whenStyleUpdates misses them and the venue paints every floor at once), and
931
- // `idle` only fires once the style is loaded and its first tiles rendered — so
932
- // filterByFloorKey is safe here and does not depend on the sometimes-flaky
933
- // isStyleLoaded check. onStyleReady lets callers reveal the map only once it is
934
- // settled; the timer armed above just clears a hanging overlay if idle never fires.
935
- this.map.once("idle", () => {
936
- // `once` cannot be cancelled, so a superseded swap's handler still fires —
937
- // filtering here keeps it from re-filtering the newer style's floors and from
938
- // reporting the newer swap's caller ready ahead of time.
939
- if (!isCurrent())
940
- return;
941
- this.filterByFloorKey(this.currentFloor);
942
- reportReady();
943
- });
944
- })
945
- .catch((error) => {
946
- // A superseded swap must not tear down the listener the CURRENT swap installed.
947
- if (isCurrent()) {
948
- this.clearPendingStyleSwapListener();
949
- }
950
- this.logError(error, { functionName: "setStyle" });
951
- // Release the caller immediately instead of making it wait out the safety timer:
952
- // the style will never arrive, so the overlay has nothing to wait for. The error
953
- // is handed to the callback so a caller can tell a failed swap from an applied
954
- // one — the previous style is still on screen either way.
955
- reportReady(error instanceof Error ? error : new Error(String(error)));
956
- });
957
- }
958
- /**
959
- * Forgets every captured base filter so the next style's declared filters are read
960
- * fresh from that style instead of inherited from the previous one.
961
- *
962
- * Both halves must move together: `baseFilters` holds the expressions and
963
- * `capturedBaseFilters` records which layer ids have already been resolved, so
964
- * clearing only the first would leave every id marked "already captured" and the new
965
- * style's filters would never be read at all.
966
- */
967
- resetBaseFilters() {
968
- this.baseFilters = {};
969
- this.capturedBaseFilters.clear();
970
- }
971
- /**
972
- * Replays everything that lives ON TOP of the map style after a full (non-diff) swap
973
- * has deleted it: label language expressions, caller-requested place highlights and
974
- * the 3D scene's GLB layer / hidden-layer overrides.
975
- *
976
- * These are deliberately not part of `whenStyleUpdates`, which restores only
977
- * style-derived state. These are decisions the SDK's callers made through `setLang`,
978
- * `setPlacesAsSelected`, `addBorderToPlaces` and the 3D config, and nothing else would
979
- * ever restore them.
980
- */
981
- restoreStyleOverlays() {
982
- // Each step is isolated: the style itself has already been applied by the time we get
983
- // here, so one failing restore must neither skip the others nor bubble out into
984
- // `setStyle`'s `.catch`, which would report the successful swap to `onStyleReady` as
985
- // a failure. `setPlacesAsSelected` / `addBorderToPlaces` throw by contract.
986
- const restore = (what, step) => {
987
- try {
988
- step();
989
- }
990
- catch (error) {
991
- this.logError(error, { functionName: "restoreStyleOverlays", step: what });
992
- }
993
- };
994
- restore("language", () => this.setLayersForLanguage(this.currentLang));
995
- if (this.coloredPlacesRequest) {
996
- const { placeIds, color } = this.coloredPlacesRequest;
997
- restore("coloredPlaces", () => this.setPlacesAsSelected(placeIds, color));
998
- }
999
- if (this.borderedPlacesRequest) {
1000
- const { placeIds, color, width } = this.borderedPlacesRequest;
1001
- restore("borderedPlaces", () => this.addBorderToPlaces(placeIds, color, width));
1002
- }
1003
- // The swap removed the GLB custom layer, and MapLibre called its `onRemove`, which
1004
- // disposes the whole three.js scene. Nothing else re-adds it, so without this every
1005
- // 3D model disappears permanently and `hideLayers` stops being applied.
1006
- restore("scene3D", () => { var _a; return (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.reattachToStyle(); });
1007
- }
1008
- /**
1009
- * Removes the `styledata` listener left by an in-flight {@link setStyle} swap, if any.
1010
- * Called before starting a new swap and when one fails, so a listener holding an
1011
- * obsolete style can never fire against a different style's layers.
1012
- */
1013
- clearPendingStyleSwapListener() {
1014
- var _a;
1015
- if (!this.pendingStyleSwapListener)
1016
- return;
1017
- // `this.map?` — a listener can only have been installed while the map existed, but
1018
- // this also runs from `destroyMap` and from `setStyle` before the map is created.
1019
- (_a = this.map) === null || _a === void 0 ? void 0 : _a.off("styledata", this.pendingStyleSwapListener);
1020
- this.pendingStyleSwapListener = undefined;
1021
- }
1022
- /**
1023
- * Removes current parent place and updates style to default
1024
- */
1025
- removeParentPlace() {
1026
- this.changeParentPlaceTo(undefined, true);
1027
- }
1028
- /**
1029
- * Applies filters and changes to layers according to given style.
1030
- * Expected to be called every time a new style is loaded
1031
- * @param newStyle
1032
- */
1033
- whenStyleUpdates(newStyle) {
1034
- var _a;
1035
- if (newStyle) {
1036
- this.setBaseFilters(newStyle);
1037
- }
1038
- this.routeController.addSourcesAndLayers();
1039
- this.refreshCircles();
1040
- this.filterByFloorKey(this.currentFloor);
1041
- (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.refreshOverlay(this.currentFloor);
1042
- }
1043
- /**
1044
- * Internal accessor for every marker currently on the map, used by
1045
- * {@link MarkerLayoutController} to compute collision avoidance. Not part
1046
- * of the public `MapVXMap` interface.
1047
- *
1048
- * The user-location marker and the route controller's markers live outside
1049
- * `this.markers`, but they still occupy screen space — leaving them out
1050
- * would let labels be placed straight on top of them. They carry no label
1051
- * of their own, so they only ever act as obstacles.
1052
- * @internal
1053
- */
1054
- getMarkers() {
1055
- const unmanaged = this.routeController.getRouteMarkers();
1056
- if (this.userLocationMarker)
1057
- unmanaged.push(this.userLocationMarker);
1058
- return unmanaged.length > 0 ? [...this.markers, ...unmanaged] : this.markers;
1059
- }
1060
- addMarker(marker) {
1061
- var _a;
1062
- try {
1063
- // Check if a marker with the same ID already exists and remove it
1064
- if (marker.id) {
1065
- const existingMarkerIndex = this.markers.findIndex((m) => m.id === marker.id);
1066
- if (existingMarkerIndex !== -1) {
1067
- const existingMarker = this.markers[existingMarkerIndex];
1068
- existingMarker.remove();
1069
- this.markers.splice(existingMarkerIndex, 1);
1070
- this.logEvent("removeMarker", {
1071
- markerId: marker.id,
1072
- reason: "duplicate",
1073
- });
1074
- }
1075
- }
1076
- const markerCreated = new marker_1.MarkerAttribute(Object.assign(Object.assign({}, marker), { maxLabelWidthPx: (_a = marker.maxLabelWidthPx) !== null && _a !== void 0 ? _a : this.markerLayoutConfig.maxLabelWidthPx }));
1077
- // While a floor change is still being rebuilt, the marker is shown by that
1078
- // change's settle step, together with the new floor.
1079
- if (!this.floorSettlePending) {
1080
- markerCreated.changeFloor(this.currentFloor, this.map, this.isOutdoorFloor(this.currentFloor));
1081
- }
1082
- this.markers.push(markerCreated);
1083
- this.markerLayoutController.scheduleRelayout();
1084
- this.logEvent("addMarker");
1085
- return markerCreated.id;
1086
- }
1087
- catch (error) {
1088
- throw new Error("Failed to add marker", { cause: error });
1089
- }
1090
- }
1091
- updateMarker(markerConfig) {
1092
- var _a;
1093
- try {
1094
- const marker = this.markers.find((e) => e.id === markerConfig.id);
1095
- if (marker !== undefined) {
1096
- marker.updateIcon(markerConfig);
1097
- this.markerLayoutController.invalidateMarker(marker);
1098
- this.markerLayoutController.scheduleRelayout();
1099
- }
1100
- this.logEvent("updateMarker");
1101
- return (_a = marker === null || marker === void 0 ? void 0 : marker.id) !== null && _a !== void 0 ? _a : "";
1102
- }
1103
- catch (error) {
1104
- throw new Error("Failed to update marker", { cause: error });
1105
- }
1106
- }
1107
- updateMarkerPosition(markerId, newPosition) {
1108
- try {
1109
- const marker = this.markers.find((e) => e.id === markerId);
1110
- if (marker !== undefined) {
1111
- marker.setLngLat(newPosition);
1112
- this.markerLayoutController.scheduleRelayout();
1113
- }
1114
- this.logEvent("updateMarkerPosition");
1115
- }
1116
- catch (error) {
1117
- throw new Error("Failed to update marker position", { cause: error });
1118
- }
1119
- }
1120
- showMarker(markerId) {
1121
- try {
1122
- const marker = this.markers.find((e) => e.id === markerId);
1123
- if (marker !== null && this.map) {
1124
- marker === null || marker === void 0 ? void 0 : marker.show(this.map);
1125
- this.markerLayoutController.scheduleRelayout();
1126
- }
1127
- this.logEvent("showMarker");
1128
- }
1129
- catch (error) {
1130
- this.debugLog("Show marker failed (marker may not exist)", error);
1131
- }
1132
- }
1133
- hideMarker(markerId) {
1134
- try {
1135
- const marker = this.markers.find((e) => e.id === markerId);
1136
- if (marker !== null) {
1137
- marker === null || marker === void 0 ? void 0 : marker.hide();
1138
- this.markerLayoutController.scheduleRelayout();
1139
- }
1140
- this.logEvent("hideMarker");
1141
- }
1142
- catch (error) {
1143
- this.debugLog("Hide marker failed (marker may not exist)", error);
1144
- }
1145
- }
1146
- removeMarker(markerId) {
1147
- try {
1148
- const marker = this.markers.find((e) => e.id === markerId);
1149
- if (marker !== null) {
1150
- marker === null || marker === void 0 ? void 0 : marker.remove();
1151
- this.markers = this.markers.filter((e) => e.id !== markerId);
1152
- this.markerLayoutController.scheduleRelayout();
1153
- }
1154
- this.logEvent("removeMarker");
1155
- }
1156
- catch (error) {
1157
- throw new Error("Failed to remove marker", { cause: error });
1158
- }
1159
- }
1160
- removeAllMarkers() {
1161
- try {
1162
- this.markers.forEach((marker) => {
1163
- marker.remove();
1164
- });
1165
- this.markers = [];
1166
- this.markerLayoutController.scheduleRelayout();
1167
- this.logEvent("removeAllMarkers");
1168
- }
1169
- catch (error) {
1170
- throw new Error("Failed to remove all markers", { cause: error });
1171
- }
1172
- }
1173
- addCircle(circle) {
1174
- try {
1175
- // Check if a circle with the same ID already exists and replace it
1176
- if (circle.id) {
1177
- this.circles = this.circles.filter((c) => c.id !== circle.id);
1178
- }
1179
- // resolveCircleConfig validates radiusMeters and coordinates
1180
- const record = (0, circle_1.resolveCircleConfig)(circle);
1181
- this.circles.push(record);
1182
- this.refreshCircles();
1183
- this.logEvent("addCircle");
1184
- return record.id;
1185
- }
1186
- catch (error) {
1187
- throw new Error(`Failed to add circle: ${error instanceof Error ? error.message : String(error)}`);
1188
- }
1189
- }
1190
- updateCircle(circleConfig) {
1191
- try {
1192
- const index = this.circles.findIndex((c) => c.id === circleConfig.id);
1193
- if (index === -1)
1194
- return null;
1195
- // resolveCircleConfig validates radiusMeters and coordinates
1196
- this.circles[index] = (0, circle_1.resolveCircleConfig)(circleConfig, this.circles[index].hidden);
1197
- this.refreshCircles();
1198
- this.logEvent("updateCircle");
1199
- return this.circles[index].id;
1200
- }
1201
- catch (error) {
1202
- throw new Error(`Failed to update circle: ${error instanceof Error ? error.message : String(error)}`);
1203
- }
1204
- }
1205
- getCircle(circleId) {
1206
- const circle = this.circles.find((c) => c.id === circleId);
1207
- return circle ? (0, circle_1.cloneCircleRecord)(circle) : undefined;
1208
- }
1209
- getCircles() {
1210
- return this.circles.map(circle_1.cloneCircleRecord);
1211
- }
1212
- hasCircle(circleId) {
1213
- return this.circles.some((c) => c.id === circleId);
1214
- }
1215
- updateCirclePosition(circleId, center, radiusMeters) {
1216
- try {
1217
- const circle = this.circles.find((c) => c.id === circleId);
1218
- if (circle === undefined)
1219
- return false;
1220
- // Validate inputs before modifying
1221
- if (!center) {
1222
- throw new Error("Circle center is required");
1223
- }
1224
- const { lat, lng } = center;
1225
- if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
1226
- throw new Error(`Invalid latitude: ${lat}. Must be between -90 and 90.`);
1227
- }
1228
- if (!Number.isFinite(lng) || lng < -180 || lng > 180) {
1229
- throw new Error(`Invalid longitude: ${lng}. Must be between -180 and 180.`);
1230
- }
1231
- if (radiusMeters !== undefined) {
1232
- if (!Number.isFinite(radiusMeters) || radiusMeters <= 0) {
1233
- throw new Error(`Invalid radiusMeters: ${radiusMeters}. Must be a positive finite number.`);
1234
- }
1235
- }
1236
- circle.coordinate = { lat: center.lat, lng: center.lng };
1237
- if (radiusMeters !== undefined) {
1238
- circle.radiusMeters = radiusMeters;
1239
- }
1240
- this.refreshCircles();
1241
- this.logEvent("updateCirclePosition");
1242
- return true;
1243
- }
1244
- catch (error) {
1245
- throw new Error(`Failed to update circle position: ${error instanceof Error ? error.message : String(error)}`);
1246
- }
1247
- }
1248
- updateCircleStyle(circleId, style) {
1249
- try {
1250
- const circle = this.circles.find((c) => c.id === circleId);
1251
- if (circle === undefined)
1252
- return false;
1253
- // Update only style properties, clamp opacities
1254
- if (style.fillColor !== undefined)
1255
- circle.fillColor = style.fillColor;
1256
- if (style.fillOpacity !== undefined)
1257
- circle.fillOpacity = Math.max(0, Math.min(1, style.fillOpacity));
1258
- if (style.strokeColor !== undefined)
1259
- circle.strokeColor = style.strokeColor;
1260
- if (style.strokeWidth !== undefined)
1261
- circle.strokeWidth = style.strokeWidth;
1262
- if (style.strokeOpacity !== undefined)
1263
- circle.strokeOpacity = Math.max(0, Math.min(1, style.strokeOpacity));
1264
- this.refreshCircles();
1265
- this.logEvent("updateCircleStyle");
1266
- return true;
1267
- }
1268
- catch (error) {
1269
- throw new Error(`Failed to update circle style: ${error instanceof Error ? error.message : String(error)}`);
1270
- }
1271
- }
1272
- removeCircle(circleId) {
1273
- try {
1274
- this.circles = this.circles.filter((c) => c.id !== circleId);
1275
- this.refreshCircles();
1276
- this.logEvent("removeCircle");
1277
- }
1278
- catch (error) {
1279
- throw new Error("Failed to remove circle", { cause: error });
1280
- }
1281
- }
1282
- removeAllCircles() {
1283
- try {
1284
- this.circles = [];
1285
- this.refreshCircles();
1286
- this.logEvent("removeAllCircles");
1287
- }
1288
- catch (error) {
1289
- throw new Error("Failed to remove all circles", { cause: error });
1290
- }
1291
- }
1292
- showCircle(circleId) {
1293
- try {
1294
- const circle = this.circles.find((c) => c.id === circleId);
1295
- if (circle === undefined) {
1296
- return false;
1297
- }
1298
- circle.hidden = false;
1299
- this.refreshCircles();
1300
- this.logEvent("showCircle");
1301
- return true;
1302
- }
1303
- catch (error) {
1304
- this.debugLog("Show circle failed", error);
1305
- return false;
1306
- }
1307
- }
1308
- hideCircle(circleId) {
1309
- try {
1310
- const circle = this.circles.find((c) => c.id === circleId);
1311
- if (circle === undefined) {
1312
- return false;
1313
- }
1314
- circle.hidden = true;
1315
- this.refreshCircles();
1316
- this.logEvent("hideCircle");
1317
- return true;
1318
- }
1319
- catch (error) {
1320
- this.debugLog("Hide circle failed", error);
1321
- return false;
1322
- }
1323
- }
1324
- /**
1325
- * Builds the GeoJSON FeatureCollection for every currently visible circle.
1326
- * Visibility mirrors marker semantics: a circle with a floor is shown only
1327
- * while that floor is displayed, and a circle without a floor is shown only
1328
- * in outdoor contexts. Hidden circles are always omitted.
1329
- */
1330
- circleFeatureCollection() {
1331
- var _a;
1332
- const floorId = (_a = this.currentFloor) !== null && _a !== void 0 ? _a : "";
1333
- const isOutdoor = this.isOutdoorFloor(floorId);
1334
- const features = this.circles
1335
- .filter((circle) => {
1336
- var _a;
1337
- if (circle.hidden)
1338
- return false;
1339
- const circleFloor = (_a = circle.floorId) !== null && _a !== void 0 ? _a : "";
1340
- return circleFloor === floorId || (isOutdoor && circleFloor === "");
1341
- })
1342
- .map((circle) => ({
1343
- type: "Feature",
1344
- properties: {
1345
- id: circle.id,
1346
- fillColor: circle.fillColor,
1347
- fillOpacity: circle.fillOpacity,
1348
- strokeColor: circle.strokeColor,
1349
- strokeWidth: circle.strokeWidth,
1350
- strokeOpacity: circle.strokeOpacity,
1351
- renderOrder: circle.renderOrder,
1352
- },
1353
- geometry: {
1354
- type: "Polygon",
1355
- coordinates: [
1356
- (0, circle_1.circleRing)(circle.coordinate.lng, circle.coordinate.lat, circle.radiusMeters),
1357
- ],
1358
- },
1359
- }));
1360
- return { type: "FeatureCollection", features };
1361
- }
1362
- /**
1363
- * Idempotently adds the shared circle source and its fill/line layers.
1364
- * Layers are inserted below the first symbol layer so place labels and
1365
- * markers stay readable above the translucent fill. Safe to call at any
1366
- * time: a style that is still loading simply rejects the calls, and the
1367
- * styledata listener retries once the style is ready.
1368
- */
1369
- /**
1370
- * Resolves the layer id to insert circle layers before, for one render
1371
- * order. `undefined` means "append on top".
1372
- *
1373
- * `aboveBasemap` finds the topmost non-symbol layer of the style and
1374
- * inserts before the first symbol layer that follows it. On indoor styles
1375
- * the floor-plate and building polygons are ordered after the first symbol
1376
- * layer, so anchoring on the topmost geometry layer — instead of the first
1377
- * symbol layer — guarantees circles are never occluded by basemap fills
1378
- * while still rendering below the labels that follow them.
1379
- */
1380
- circleBeforeIdFor(order) {
1381
- var _a, _b, _c, _d;
1382
- if (order === "top")
1383
- return undefined;
1384
- const layers = ((_b = (_a = this.map.getStyle()) === null || _a === void 0 ? void 0 : _a.layers) !== null && _b !== void 0 ? _b : []).filter((layer) => !isCircleLayerId(layer.id));
1385
- if (order === "belowLabels") {
1386
- return (_c = layers.find((layer) => layer.type === "symbol")) === null || _c === void 0 ? void 0 : _c.id;
1387
- }
1388
- // aboveBasemap
1389
- let lastNonSymbolIndex = -1;
1390
- layers.forEach((layer, index) => {
1391
- if (layer.type !== "symbol")
1392
- lastNonSymbolIndex = index;
1393
- });
1394
- return (_d = layers.slice(lastNonSymbolIndex + 1).find((layer) => layer.type === "symbol")) === null || _d === void 0 ? void 0 : _d.id;
1395
- }
1396
- /**
1397
- * Idempotently ensures the shared circle source and one fill/line layer
1398
- * pair per render order in use, at the placement that order requires.
1399
- * Placement is recomputed and re-asserted (via moveLayer) on every call,
1400
- * so circles regain their correct z-order after any style reload or
1401
- * floor/parent-place change. Safe to call at any time: a style that is
1402
- * still loading simply rejects the calls, and the styledata listener
1403
- * retries once the style is ready.
1404
- */
1405
- ensureCircleLayers() {
1406
- if (!this.map)
1407
- return;
1408
- try {
1409
- if (!this.map.getSource(CIRCLE_SOURCE_ID)) {
1410
- this.map.addSource(CIRCLE_SOURCE_ID, {
1411
- type: "geojson",
1412
- data: this.circleFeatureCollection(),
1413
- });
1414
- }
1415
- const ordersInUse = new Set(this.circles.map((c) => c.renderOrder));
1416
- // Always keep the default bucket alive so an empty map still renders
1417
- // newly added circles without a layer rebuild
1418
- ordersInUse.add("aboveBasemap");
1419
- // Process buckets lowest-first (CIRCLE_RENDER_ORDERS is canonical):
1420
- // when two buckets resolve to the same anchor, each addLayer/moveLayer
1421
- // lands immediately before it, so a bucket processed later paints above
1422
- // the ones processed earlier. Canonical order keeps
1423
- // belowLabels < aboveBasemap < top regardless of circle insertion order.
1424
- for (const order of circle_1.CIRCLE_RENDER_ORDERS) {
1425
- if (!ordersInUse.has(order))
1426
- continue;
1427
- const ids = circleLayerIdsFor(order);
1428
- const beforeId = this.circleBeforeIdFor(order);
1429
- const orderFilter = ["==", ["get", "renderOrder"], order];
1430
- if (!this.map.getLayer(ids.fill)) {
1431
- const fillLayer = {
1432
- id: ids.fill,
1433
- type: "fill",
1434
- source: CIRCLE_SOURCE_ID,
1435
- filter: orderFilter,
1436
- paint: {
1437
- "fill-color": ["get", "fillColor"],
1438
- "fill-opacity": ["get", "fillOpacity"],
1439
- },
1440
- };
1441
- this.map.addLayer(fillLayer, beforeId);
1442
- }
1443
- else {
1444
- this.map.moveLayer(ids.fill, beforeId);
1445
- }
1446
- if (!this.map.getLayer(ids.line)) {
1447
- const lineLayer = {
1448
- id: ids.line,
1449
- type: "line",
1450
- source: CIRCLE_SOURCE_ID,
1451
- filter: orderFilter,
1452
- paint: {
1453
- "line-color": ["get", "strokeColor"],
1454
- "line-width": ["get", "strokeWidth"],
1455
- "line-opacity": ["get", "strokeOpacity"],
1456
- },
1457
- };
1458
- this.map.addLayer(lineLayer, beforeId);
1459
- }
1460
- else {
1461
- this.map.moveLayer(ids.line, beforeId);
1462
- }
1463
- }
1464
- }
1465
- catch (error) {
1466
- this.debugLog("Layers not added: style not loaded yet, the styledata listener will re-add them", error);
1467
- }
1468
- }
1469
- /**
1470
- * Re-renders all circles: ensures the source and layers exist, then pushes
1471
- * the current FeatureCollection. Called after every mutation of the circle
1472
- * list, on floor changes, and when the map style reloads.
1473
- */
1474
- refreshCircles() {
1475
- if (!this.map)
1476
- return;
1477
- if (this.circles.length === 0 && !this.map.getSource(CIRCLE_SOURCE_ID))
1478
- return;
1479
- this.ensureCircleLayers();
1480
- try {
1481
- const source = this.map.getSource(CIRCLE_SOURCE_ID);
1482
- source === null || source === void 0 ? void 0 : source.setData(this.circleFeatureCollection());
1483
- }
1484
- catch (error) {
1485
- this.debugLog("Source not found (a new style may still be loading)", error);
1486
- }
1487
- }
1488
- /**
1489
- * Use it to change the current layer
1490
- * @param floorKey floor id
1491
- */
1492
- filterByFloorKey(floorKey) {
1493
- this.currentFloor = floorKey;
1494
- const floorKeyString = floorKey !== null && floorKey !== void 0 ? floorKey : "";
1495
- if (this.innerFloors.every((floor) => floor.key !== floorKey) && floorKey !== "") {
1496
- this.logEvent("invalidFloorKey", { floorKey: floorKeyString });
1497
- }
1498
- // One style read for the whole floor change. `getStyle()` serializes every
1499
- // source and layer of the venue style, and the second read is the expensive
1500
- // one: `updateFiltersTo` writes filters and layout properties in between,
1501
- // and each write nulls maplibre's `_serializedLayers` cache, so anything
1502
- // that reads the style afterwards rebuilds it from scratch.
1503
- const layers = this.safeStyleLayers();
1504
- this.updateFiltersTo(floorKeyString, layers);
1505
- this.updateMarkersWhenFloorSettles(floorKeyString, layers);
1506
- this.refreshCircles();
1507
- this.routeController.updateRouteLayers(floorKeyString);
1508
- this.routeController.updateRouteMarkerVisibility(floorKeyString);
1509
- }
1510
- /**
1511
- * Show/hide the HTML markers for `floorId` once the indoor tiles have been
1512
- * rebuilt with the new floor filters.
1513
- *
1514
- * A floor change is a `setFilter` on every indoor layer: MapLibre rebuilds the
1515
- * tile buckets in its worker, so the new floor appears a few frames later
1516
- * (hundreds of ms on a low-end panel) while a marker is a DOM element that
1517
- * would move instantly. Waiting for the source to report loaded makes floor
1518
- * polygons and markers switch in the same frame. The wait is bounded, is
1519
- * dropped when a newer floor change arrives, and is skipped outside a
1520
- * browser (no `requestAnimationFrame`).
1521
- */
1522
- updateMarkersWhenFloorSettles(floorId, styleLayers) {
1523
- const token = ++this.floorSettleToken;
1524
- // Resolved rather than probed with `hasStyle()`, which is another full
1525
- // `getStyle()` — and by this point the most expensive kind, because
1526
- // `updateFiltersTo` has just invalidated the serialized-layer cache.
1527
- const layers = styleLayers === undefined ? this.safeStyleLayers() : styleLayers;
1528
- if (typeof requestAnimationFrame !== "function" || layers === null) {
1529
- // No poll will run for this floor change, so nothing would ever clear the
1530
- // flag later: leaving it set makes every subsequent `addMarker` skip
1531
- // `changeFloor` and the marker is never attached to the map.
1532
- this.floorSettleDeadline = null;
1533
- this.updateMarkersTo(floorId);
1534
- return;
1535
- }
1536
- const sourceIds = this.floorSettleSourceIds(floorId, layers);
1537
- const deadline = this.now() + FLOOR_SETTLE_MAX_MS;
1538
- this.floorSettleDeadline = deadline;
1539
- let sawReloading = false;
1540
- let frames = 0;
1541
- const poll = () => {
1542
- if (token !== this.floorSettleToken)
1543
- return;
1544
- frames++;
1545
- const loaded = sourceIds.every((id) => this.isSourceSettled(id));
1546
- if (!loaded)
1547
- sawReloading = true;
1548
- // The reload is dispatched on the map's next render, which may run after
1549
- // this callback within the same frame: give it a couple of frames to be
1550
- // observed before concluding there was nothing to reload.
1551
- const settled = loaded && (sawReloading || frames >= FLOOR_SETTLE_MIN_FRAMES);
1552
- if (settled || this.now() >= deadline) {
1553
- this.floorSettleDeadline = null;
1554
- this.updateMarkersTo(floorId);
1555
- return;
1556
- }
1557
- requestAnimationFrame(poll);
1558
- };
1559
- requestAnimationFrame(poll);
1560
- }
1561
- /**
1562
- * Sources whose tiles a switch to `floorId` actually reloads, and which the
1563
- * marker settle therefore has to wait on.
1564
- *
1565
- * With a vector basemap that is the source the `indoor-*` layers draw from,
1566
- * because the floor change is a `setFilter` over those layers. An offline
1567
- * bundle's raster basemap switches floors by making `raster-floor-<key>`
1568
- * visible instead, and those tiles only begin loading at that moment — while
1569
- * its `indoor-*` layers carry labels and hover, are already loaded, and would
1570
- * report settled at once, letting the markers switch a floor before its image
1571
- * had painted. That is the configuration this wait exists for, so both are
1572
- * waited on.
1573
- */
1574
- floorSettleSourceIds(floorId, layers) {
1575
- const ids = new Set();
1576
- const rasterFloor = layers.find((layer) => layer.id === `raster-floor-${floorId}`);
1577
- if (rasterFloor && "source" in rasterFloor && typeof rasterFloor.source === "string") {
1578
- ids.add(rasterFloor.source);
1579
- }
1580
- ids.add(this.indoorSourceId(layers));
1581
- return [...ids];
1582
- }
1583
- /** The vector source the `indoor-*` layers draw from (`indoorequal` by default). */
1584
- indoorSourceId(layers) {
1585
- for (const layer of layers) {
1586
- if (layer.id.startsWith("indoor-") && "source" in layer && typeof layer.source === "string") {
1587
- return layer.source;
1588
- }
1589
- }
1590
- return "indoorequal";
1591
- }
1592
- isSourceSettled(sourceId) {
1593
- try {
1594
- if (!this.map.getSource(sourceId))
1595
- return true;
1596
- return this.map.isSourceLoaded(sourceId) !== false;
1597
- }
1598
- catch (_a) {
1599
- return true;
1600
- }
1601
- }
1602
- getContainer() {
1603
- return this.map.getContainer();
1604
- }
1605
- resize() {
1606
- try {
1607
- if (this.map) {
1608
- this.map.resize();
1609
- this.logEvent("resize");
1610
- }
1611
- }
1612
- catch (error) {
1613
- this.debugLog("Resize failed (map/container may not be ready)", error);
1614
- }
1615
- }
1616
- updateMarkersTo(floorId) {
1617
- const isOutdoor = this.isOutdoorFloor(floorId);
1618
- this.markers.forEach((e) => {
1619
- e.changeFloor(floorId, this.map, isOutdoor);
1620
- });
1621
- this.markerLayoutController.scheduleRelayout();
1622
- }
1623
- updateFiltersTo(floorId, styleLayers) {
1624
- var _a, _b, _c;
1625
- if (!this.map)
1626
- return;
1627
- // `null` means the caller looked and there was no style; only `undefined`
1628
- // means it did not look.
1629
- const layers = (_a = (styleLayers === undefined ? this.safeStyleLayers() : styleLayers)) !== null && _a !== void 0 ? _a : [];
1630
- // Raster basemap (offline bundles baked with one raster archive per floor):
1631
- // a floor is switched by showing its `raster-floor-<key>` layer and hiding
1632
- // the others. The remaining `indoor-*` layers (labels, hover, highlights)
1633
- // are still vector and keep going through the filter logic below.
1634
- const rasterFloorPrefix = "raster-floor-";
1635
- // Only switch the raster basemap when there is a baked layer to switch to.
1636
- // Hiding every raster floor leaves the basemap blank, and no id can match in
1637
- // two cases: no floor at all (an outdoor place, or a venue with no floors,
1638
- // where `currentFloor` is `""`), and a floor the bundle has no raster
1639
- // archive for — a venue where only some floors are baked, or a floor added
1640
- // after the bundle was built. Keeping the current one visible is the safe
1641
- // fallback for both.
1642
- const rasterFloorLayerIds = layers
1643
- .filter((l) => l.id.startsWith(rasterFloorPrefix))
1644
- .map((l) => l.id);
1645
- const targetRasterFloor = rasterFloorPrefix + floorId;
1646
- if (floorId && rasterFloorLayerIds.includes(targetRasterFloor)) {
1647
- rasterFloorLayerIds.forEach((id) => {
1648
- this.map.setLayoutProperty(id, "visibility", id === targetRasterFloor ? "visible" : "none");
1649
- });
1650
- }
1651
- layers
1652
- .filter((l) => l.id.startsWith("base-indoor-"))
1653
- .forEach((l) => {
1654
- this.map.removeLayer(l.id);
1655
- });
1656
- const baseFloorId = (_b = this.innerFloors.find((floor) => floor.key === floorId)) === null || _b === void 0 ? void 0 : _b.baseFloor;
1657
- const indoorLayers = layers.filter((layer) => layer.id.startsWith("indoor-"));
1658
- // Resolve each indoor layer's declared (base) filter before we start folding floor
1659
- // filters into it — and before cloning it into a `base-indoor-*` layer, which copies
1660
- // the base filter across. Layers added asynchronously after a `setStyle` swap are
1661
- // missing when `setBaseFilters` runs, so this is where their still-pristine filter
1662
- // is captured; without it the layer loses its own discrimination and repaints every
1663
- // feature (the washed-out / blue-outline look after changing floors). Layers already
1664
- // resolved are skipped, so a filter this class wrote is never mistaken for a
1665
- // declared one.
1666
- indoorLayers.forEach((layer) => this.captureDeclaredBaseFilter(layer.id));
1667
- if (baseFloorId != undefined) {
1668
- const firstLayerId = (_c = indoorLayers[0]) === null || _c === void 0 ? void 0 : _c.id;
1669
- indoorLayers
1670
- .filter((layer) => layer.type !== "fill-extrusion")
1671
- .forEach((layer) => {
1672
- const baseLayer = deepClone(layer);
1673
- baseLayer.id = "base-" + layer.id;
1674
- this.baseFilters[baseLayer.id] = this.baseFilters[layer.id];
1675
- this.map.addLayer(baseLayer, firstLayerId);
1676
- this.applyLayerFilters(baseLayer, baseFloorId);
1677
- });
1678
- }
1679
- indoorLayers.forEach((layer) => {
1680
- this.applyLayerFilters(layer, floorId);
1681
- });
1682
- }
1683
- /**
1684
- * Reads a layer's current filter, tolerating the layer having been removed or the
1685
- * style being mid-swap (maplibre `getFilter` throws in those cases). Returns
1686
- * `undefined` instead of throwing so base-filter capture never aborts floor filtering.
1687
- */
1688
- safeGetFilter(layerId) {
1689
- try {
1690
- return this.map.getFilter(layerId);
1691
- }
1692
- catch (_a) {
1693
- return undefined;
1694
- }
1695
- }
1696
- applyLayerFilters(layer, floorId) {
1697
- var _a;
1698
- // Resolve the layer's declared (base) filter the first time we touch it. Indoor
1699
- // layers such as `indoor-polygon-room` share one source and are told apart only by
1700
- // their own filter (e.g. `["in","room",["get","tags"]]`); we must fold that into the
1701
- // floor filter or the layer repaints every feature. Some of these layers are added
1702
- // asynchronously after a `setStyle` swap, so they are absent when `setBaseFilters`
1703
- // runs — capturing here, while their live filter is still pristine, is what keeps a
1704
- // floor change from wiping the style (the washed-out, blue-outline look).
1705
- // `base-indoor-*` clones inherit their base filter from the layer they were cloned
1706
- // from, so they are never read back.
1707
- if (!layer.id.startsWith("base-indoor-")) {
1708
- this.captureDeclaredBaseFilter(layer.id);
1709
- }
1710
- let baseFilter = this.baseFilters[layer.id];
1711
- const floorIdString = floorId !== null && floorId !== void 0 ? floorId : "";
1712
- const floorFilter = [
1713
- "any",
1714
- ["in", floorIdString, ["get", "floor_key"]],
1715
- ];
1716
- const publicPlaceFilter = ["==", ["get", "public_place"], true];
1717
- const subPlaceFilter = [
1718
- "!",
1719
- ["in", (_a = this.parentPlaceId) !== null && _a !== void 0 ? _a : "noRef", ["get", "is_subplace"]],
1720
- ];
1721
- const showInFloorFilter = [
1722
- "in",
1723
- floorIdString,
1724
- ["get", "show_in_floor"],
1725
- ];
1726
- let basicFilters = publicPlaceFilter;
1727
- if (this.parentPlaceId != null) {
1728
- basicFilters = ["any", floorFilter, publicPlaceFilter];
1729
- }
1730
- const customFilters = [
1731
- "all",
1732
- basicFilters,
1733
- ["any", subPlaceFilter, showInFloorFilter],
1734
- ];
1735
- if (layer.id.includes("hover")) {
1736
- baseFilter = ["in", this.hoveredId, ["get", "ref"]];
1737
- }
1738
- else if (!layer.id.includes("colored") &&
1739
- (layer.type == "fill" || layer.type === "fill-extrusion")) {
1740
- customFilters.push(["!", ["in", this.hoveredId, ["get", "ref"]]]);
1741
- }
1742
- // From here on this layer's live filter is ours, never the style's declared one.
1743
- // Marking it keeps any later pass from reading it back as a base filter — that is
1744
- // what used to AND the previous floor into the next one and blank the layer out.
1745
- this.capturedBaseFilters.add(layer.id);
1746
- if (baseFilter != null) {
1747
- const finalFilter = ["all", baseFilter, customFilters];
1748
- this.map.setFilter(layer.id, finalFilter);
1749
- }
1750
- else {
1751
- this.map.setFilter(layer.id, customFilters);
1752
- }
1753
- }
1754
- /**
1755
- * Floors of the parent place
1756
- * @returns Inner floors of the parent place
1757
- */
1758
- getInnerFloors() {
1759
- return this.innerFloors;
1760
- }
1761
- /**
1762
- * Whether `floorId` shows the venue's outdoors, and so whether entities that
1763
- * carry no floor of their own belong on screen with it.
1764
- *
1765
- * A map without a parent place is outdoors throughout; inside one, only a
1766
- * floor reachable from GPS is. Markers, circles and the route marker all hang
1767
- * their visibility on this, so they answer it the same way.
1768
- */
1769
- isOutdoorFloor(floorId) {
1770
- var _a, _b;
1771
- return (!this.parentPlace ||
1772
- ((_b = (_a = this.innerFloors.find((floor) => floor.key === floorId)) === null || _a === void 0 ? void 0 : _a.reachableFromGPS) !== null && _b !== void 0 ? _b : false));
1773
- }
1774
- updateCamera(newCamera, callback) {
1775
- void this.map.once("moveend", callback);
1776
- this.map.easeTo(Object.assign({}, newCamera));
1777
- }
1778
- setMaxZoom(zoomLvl, options) {
1779
- var _a;
1780
- this.map.setMaxZoom(zoomLvl);
1781
- (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
1782
- }
1783
- setMinZoom(zoomLvl, options) {
1784
- var _a;
1785
- this.map.setMinZoom(zoomLvl);
1786
- (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
1787
- }
1788
- setBearing(degrees, options) {
1789
- var _a;
1790
- if (!Number.isFinite(degrees))
1791
- return;
1792
- if (options === null || options === void 0 ? void 0 : options.animate) {
1793
- if (options.onComplete)
1794
- void this.map.once("moveend", options.onComplete);
1795
- this.map.rotateTo(degrees, { duration: 600 });
1796
- return;
1797
- }
1798
- this.map.setBearing(degrees);
1799
- (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
1800
- }
1801
- setRotationEnabled(enabled) {
1802
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1803
- if (enabled) {
1804
- (_b = (_a = this.map.dragRotate) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1805
- (_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.enableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
1806
- (_f = (_e = this.map.keyboard) === null || _e === void 0 ? void 0 : _e.enable) === null || _f === void 0 ? void 0 : _f.call(_e);
1807
- }
1808
- else {
1809
- (_h = (_g = this.map.dragRotate) === null || _g === void 0 ? void 0 : _g.disable) === null || _h === void 0 ? void 0 : _h.call(_g);
1810
- (_k = (_j = this.map.touchZoomRotate) === null || _j === void 0 ? void 0 : _j.disableRotation) === null || _k === void 0 ? void 0 : _k.call(_j);
1811
- }
1812
- }
1813
- setPanEnabled(enabled) {
1814
- var _a, _b, _c, _d;
1815
- if (enabled) {
1816
- (_b = (_a = this.map.dragPan) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1817
- }
1818
- else {
1819
- (_d = (_c = this.map.dragPan) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
1820
- }
1821
- }
1822
- setScrollZoomEnabled(enabled) {
1823
- var _a, _b, _c, _d;
1824
- if (enabled) {
1825
- (_b = (_a = this.map.scrollZoom) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1826
- }
1827
- else {
1828
- (_d = (_c = this.map.scrollZoom) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
1829
- }
1830
- }
1831
- isInsideBounds(point) {
1832
- const mapBounds = this.map.getMaxBounds();
1833
- if (mapBounds != null) {
1834
- return mapBounds.contains(point);
1835
- }
1836
- return true;
1837
- }
1838
- setMaxBounds(coordinates, options) {
1839
- var _a;
1840
- if (coordinates.length > 1) {
1841
- const boundingBox = (0, utils_1.getBoundingBox)(coordinates);
1842
- this.map.setMaxBounds(new maplibre_gl_1.LngLatBounds([boundingBox[0].lng, boundingBox[0].lat], [boundingBox[1].lng, boundingBox[1].lat]));
1843
- }
1844
- (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
1845
- }
1846
- fitCoordinates(coordinates, options) {
1847
- var _a, _b;
1848
- if (coordinates.length === 0) {
1849
- return;
1850
- }
1851
- const onComplete = options === null || options === void 0 ? void 0 : options.onComplete;
1852
- if (coordinates.length === 1) {
1853
- const [coord] = coordinates;
1854
- if (onComplete) {
1855
- void this.map.once("moveend", onComplete);
1856
- }
1857
- this.map.setCenter({
1858
- lat: coord.lat,
1859
- lng: coord.lng,
1860
- });
1861
- }
1862
- else {
1863
- let minLat;
1864
- let minLng;
1865
- let maxLat;
1866
- let maxLng;
1867
- for (const coord of coordinates) {
1868
- const { lat, lng } = coord;
1869
- minLat = minLat === undefined ? lat : Math.min(minLat, lat);
1870
- minLng = minLng === undefined ? lng : Math.min(minLng, lng);
1871
- maxLat = maxLat === undefined ? lat : Math.max(maxLat, lat);
1872
- maxLng = maxLng === undefined ? lng : Math.max(maxLng, lng);
1873
- }
1874
- const bounds = [
1875
- minLng !== null && minLng !== void 0 ? minLng : 0,
1876
- minLat !== null && minLat !== void 0 ? minLat : 0,
1877
- maxLng !== null && maxLng !== void 0 ? maxLng : 0,
1878
- maxLat !== null && maxLat !== void 0 ? maxLat : 0,
1879
- ];
1880
- // Get container dimensions for percentage-based padding calculations
1881
- const container = this.map.getContainer();
1882
- const containerWidth = container.clientWidth;
1883
- const containerHeight = container.clientHeight;
1884
- // Convert padding values with units to pixels
1885
- const padding = convertPaddingToPixels(options === null || options === void 0 ? void 0 : options.padding, containerWidth, containerHeight);
1886
- const fitOptions = Object.assign(Object.assign({}, options), { padding });
1887
- const bearing = this.map.getBearing();
1888
- const pitch = this.map.getPitch();
1889
- (_a = fitOptions.bearing) !== null && _a !== void 0 ? _a : (fitOptions.bearing = bearing);
1890
- (_b = fitOptions.pitch) !== null && _b !== void 0 ? _b : (fitOptions.pitch = pitch);
1891
- if (onComplete) {
1892
- void this.map.once("moveend", onComplete);
1893
- }
1894
- this.map.fitBounds(bounds, fitOptions);
1895
- }
1896
- }
1897
- easeToCoordinates(coordinates, options) {
1898
- var _a, _b, _c;
1899
- if (coordinates.length === 0) {
1900
- return;
1901
- }
1902
- const onComplete = options === null || options === void 0 ? void 0 : options.onComplete;
1903
- // Get container dimensions for percentage-based padding calculations
1904
- const container = this.map.getContainer();
1905
- const containerWidth = container.clientWidth;
1906
- const containerHeight = container.clientHeight;
1907
- // Convert padding values with units to pixels
1908
- const padding = convertPaddingToPixels(options === null || options === void 0 ? void 0 : options.padding, containerWidth, containerHeight);
1909
- if (coordinates.length === 1) {
1910
- const [coord] = coordinates;
1911
- if (onComplete) {
1912
- void this.map.once("moveend", onComplete);
1913
- }
1914
- this.map.easeTo(Object.assign(Object.assign({}, options), { center: { lat: coord.lat, lng: coord.lng }, padding }));
1915
- return;
1916
- }
1917
- const bearing = (_a = options === null || options === void 0 ? void 0 : options.bearing) !== null && _a !== void 0 ? _a : this.map.getBearing();
1918
- const pitch = (_b = options === null || options === void 0 ? void 0 : options.pitch) !== null && _b !== void 0 ? _b : this.map.getPitch();
1919
- const cameraParams = {
1920
- bearing,
1921
- padding,
1922
- containerWidth,
1923
- containerHeight,
1924
- maxZoom: (_c = options === null || options === void 0 ? void 0 : options.maxZoom) !== null && _c !== void 0 ? _c : this.map.getMaxZoom(),
1925
- minZoom: this.map.getMinZoom(),
1926
- };
1927
- // Orthographic oriented fit (exact for pitch 0). maplibre's fitBounds only
1928
- // fits a north-up LngLatBounds, which wastes viewport space for diagonal
1929
- // geometries under a bearing.
1930
- let camera = (0, fitCamera_1.computeOrientedCamera)(coordinates, cameraParams);
1931
- // With a tilted camera the orthographic zoom is off due to perspective, so
1932
- // refine it against maplibre's real projection on a cloned transform.
1933
- if (pitch > 0) {
1934
- // Simulated on the live map through its public API rather than on `map.transform.clone()`:
1935
- // MapLibre v6 removes the internal transform and exposes no replacement. The whole loop is
1936
- // synchronous and ends in `restore()`, so no intermediate frame is ever painted.
1937
- // See DEVELOPER_DOCS/MAPLIBRE_V6_MIGRATION.md.
1938
- const fitTransform = new liveMapFitTransform_1.LiveMapFitTransform(this.map);
1939
- try {
1940
- camera = (0, fitCamera_1.refineCameraForPitch)(fitTransform, coordinates, camera, Object.assign(Object.assign({}, cameraParams), { pitch }));
1941
- }
1942
- finally {
1943
- fitTransform.restore();
1944
- }
1945
- }
1946
- if (onComplete) {
1947
- void this.map.once("moveend", onComplete);
1948
- }
1949
- this.map.easeTo(Object.assign(Object.assign({}, options), { center: camera.center, zoom: camera.zoom, bearing,
1950
- pitch,
1951
- padding }));
1952
- }
1953
- fitRouteByPlace(route, placeId, options) {
1954
- // Filter steps that belong to the specified place
1955
- const stepsInPlace = route.steps.filter((step) => step.startInsidePlace === placeId || step.endInsidePlace === placeId);
1956
- if (stepsInPlace.length === 0) {
1957
- return;
1958
- }
1959
- // Extract all coordinates from the filtered steps
1960
- const coords = stepsInPlace.flatMap((step) => (0, route_utils_1.extractStepCoordinates)(step));
1961
- if (coords.length > 0) {
1962
- this.fitCoordinates(coords, options);
1963
- }
1964
- }
1965
- getZoomLevel() {
1966
- return this.map.getZoom();
1967
- }
1968
- updateFloor(floorId, options) {
1969
- var _a, _b, _c;
1970
- try {
1971
- this.filterByFloorKey(floorId);
1972
- (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.onFloorChange(this.currentFloor);
1973
- (_b = this.onFloorChange) === null || _b === void 0 ? void 0 : _b.call(this, floorId);
1974
- this.logEvent("updateFloor", { floorId });
1975
- (_c = options === null || options === void 0 ? void 0 : options.onComplete) === null || _c === void 0 ? void 0 : _c.call(options);
1976
- }
1977
- catch (error) {
1978
- this.debugLog("Floor update failed", error);
1979
- }
1980
- }
1981
- updatePlaceAndFloorForRouting(placeId, floorId) {
1982
- var _a;
1983
- try {
1984
- this.updatePlaceAndFloor(placeId, floorId);
1985
- const layers = this.safeStyleLayers();
1986
- this.updateFiltersTo(floorId, layers);
1987
- this.updateMarkersWhenFloorSettles(floorId, layers);
1988
- (_a = this.onFloorChange) === null || _a === void 0 ? void 0 : _a.call(this, floorId);
1989
- this.logEvent("updatePlaceAndFloorForRouting", { floorId });
1990
- }
1991
- catch (error) {
1992
- this.debugLog("Place/floor update for routing failed", error);
1993
- }
1994
- }
1995
- async addRoute(config, drawConfig = {}) {
1996
- try {
1997
- this.logEvent("addRoute");
1998
- const fetchRouteConfig = new routeConfiguration_1.InternalGetRouteConfiguration(config);
1999
- const drawRouteConfig = new routeConfiguration_1.InternalDrawRouteConfiguration(drawConfig);
2000
- const route = await this.repository.fetchAndParseRoute(fetchRouteConfig);
2001
- this.routeController.addRoute(route, drawRouteConfig);
2002
- return route;
2003
- }
2004
- catch (error) {
2005
- throw new Error("Error: Failed to add route", { cause: error });
2006
- }
2007
- }
2008
- async addLiveRoute(config, drawConfig = {}) {
2009
- try {
2010
- this.logEvent("addLiveRoute");
2011
- const fetchRouteConfig = new routeConfiguration_1.InternalGetRouteConfiguration(config);
2012
- const drawRouteConfig = new routeConfiguration_1.InternalDrawRouteConfiguration(drawConfig);
2013
- // Take a ticket BEFORE awaiting: refreshes overlap, and an older response
2014
- // resolving after a newer one must not clobber it (stale route drawn,
2015
- // liveRouteId pointing at the wrong instance).
2016
- const ticket = ++this.liveRouteTicket;
2017
- const route = await this.repository.fetchAndParseLiveRoute(fetchRouteConfig);
2018
- if (ticket !== this.liveRouteTicket) {
2019
- // Superseded while in flight — the map shows (or will show) the newer
2020
- // route. Return the fetched data without drawing it.
2021
- return route;
2022
- }
2023
- // Replace the previous live route so repeated refreshes don't stack paths
2024
- // on the map. Removal happens AFTER the fetch, so a failed refresh leaves
2025
- // the current route on screen. Targeted by id, so routes drawn with
2026
- // addRoute are untouched.
2027
- if (this.liveRouteId !== undefined && this.liveRouteId !== route.id) {
2028
- this.routeController.removeRoute(this.liveRouteId);
2029
- }
2030
- // Clear any instance already using this id (a same-id refresh, or the same
2031
- // route previously drawn via addRoute) — the controller derives its
2032
- // source/layer ids from the route id, so re-adding without removing would
2033
- // throw "Source already exists".
2034
- if (this.routeController.getRouteIds().includes(route.id)) {
2035
- this.routeController.removeRoute(route.id);
2036
- }
2037
- this.routeController.addRoute(route, drawRouteConfig);
2038
- this.liveRouteId = route.id;
2039
- return route;
2040
- }
2041
- catch (error) {
2042
- const reason = error instanceof Error ? error.message : String(error);
2043
- throw new Error(`Error: Failed to add route: ${reason}`, { cause: error });
2044
- }
2045
- }
2046
- updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#276EF1" }) {
2047
- try {
2048
- const behindConfig = new routeConfiguration_1.InternalDrawRouteConfiguration({
2049
- routeStyle: behindStyle,
2050
- });
2051
- this.routeController.updateRouteProgress(routeId, position, behindConfig);
2052
- }
2053
- catch (error) {
2054
- this.debugLog("Route update failed (route may not exist)", error);
2055
- }
2056
- }
2057
- removeRoute(routeId) {
2058
- try {
2059
- this.logEvent("removeRoute");
2060
- this.routeController.removeRoute(routeId);
2061
- }
2062
- catch (error) {
2063
- this.debugLog("Remove route failed (no route may exist)", error);
2064
- }
2065
- this.syncLiveRouteId();
2066
- }
2067
- removeRouteById(routeId) {
2068
- try {
2069
- this.logEvent("removeRouteById");
2070
- this.routeController.removeRoute(routeId);
2071
- }
2072
- catch (error) {
2073
- this.debugLog("Remove route failed (route may not exist)", error);
2074
- }
2075
- this.syncLiveRouteId();
2076
- }
2077
- removeAllRoutes() {
2078
- try {
2079
- this.logEvent("removeAllRoutes");
2080
- this.routeController.removeAllRoutes();
2081
- }
2082
- catch (error) {
2083
- this.debugLog("Remove all routes failed", error);
2084
- }
2085
- this.syncLiveRouteId();
2086
- }
2087
- /**
2088
- * Drop {@link liveRouteId} when the instance it points at is no longer on the
2089
- * map (removed via removeRoute/removeRouteById/removeAllRoutes — including the
2090
- * no-arg removeRoute, which removes the last-added route). Keeps the live
2091
- * bookkeeping in sync so a later {@link addLiveRoute} doesn't act on a
2092
- * dangling id.
2093
- */
2094
- syncLiveRouteId() {
2095
- if (this.liveRouteId !== undefined &&
2096
- !this.routeController.getRouteIds().includes(this.liveRouteId)) {
2097
- this.liveRouteId = undefined;
2098
- }
2099
- }
2100
- getActiveRouteIds() {
2101
- return this.routeController.getRouteIds();
2102
- }
2103
- addPopOver(popOverConfig) {
2104
- const placeId = popOverConfig.placeId;
2105
- if (placeId !== undefined) {
2106
- const pointedPlace = this.subPlaces.find((subplace) => {
2107
- return subplace.clientId === placeId || subplace.mapvxId === placeId;
2108
- });
2109
- if (pointedPlace !== undefined) {
2110
- new maplibre_gl_1.default.Popup()
2111
- .setLngLat(pointedPlace.position)
2112
- .setText(pointedPlace.title)
2113
- .addTo(this.map);
2114
- }
2115
- }
2116
- return "";
2117
- }
2118
- removePopOver(id) {
2119
- throw Error("Not implemented");
2120
- }
2121
- startClickListener(callback) {
2122
- this.clickCallback = callback;
2123
- }
2124
- stopClickListener() {
2125
- this.clickCallback = undefined;
2126
- }
2127
- defaultClickListener() {
2128
- this.map.on("click", async (e) => {
2129
- var _a, _b;
2130
- const features = this.map.queryRenderedFeatures(e.point);
2131
- // Filter room features by current floor
2132
- const roomFeatures = features.filter((feature) => { var _a; return feature.properties.class === "room" && ((_a = feature.properties) === null || _a === void 0 ? void 0 : _a.floor_key) === this.currentFloor; });
2133
- const touchedFeature = roomFeatures[0];
2134
- const ref = touchedFeature === null || touchedFeature === void 0 ? void 0 : touchedFeature.properties.ref;
2135
- if (ref != null && typeof ref === "string" && ref.length > 0) {
2136
- console.log("touchedFeature", ref);
2137
- // First, search in current subPlaces
2138
- let clickedPlace = this.subPlaces.find((subPlace) => subPlace.mapvxId === ref);
2139
- // If not found, search in subplaces of potential parent places
2140
- if (clickedPlace === undefined) {
2141
- for (const potentialParentPlace of this.potentialParentPlaces) {
2142
- const cachedSubPlaces = this.repository.getCachedSubPlaces(potentialParentPlace.mapvxId);
2143
- if (cachedSubPlaces) {
2144
- clickedPlace = cachedSubPlaces.find((subPlace) => subPlace.mapvxId === ref);
2145
- if (clickedPlace !== undefined) {
2146
- break;
2147
- }
2148
- }
2149
- }
2150
- }
2151
- // As last resort, search in general cache
2152
- if (clickedPlace === undefined) {
2153
- const cachedPlaces = this.repository.getCachedPlaces([ref]);
2154
- if (cachedPlaces.length === 0) {
2155
- console.log("fetching place detail from server", ref);
2156
- clickedPlace = await this.repository.fetchAndParsePlaceDetail({ id: ref });
2157
- }
2158
- else {
2159
- console.log("using cached place detail", ref);
2160
- clickedPlace = cachedPlaces === null || cachedPlaces === void 0 ? void 0 : cachedPlaces[0];
2161
- }
2162
- }
2163
- console.log("clickedPlace", clickedPlace === null || clickedPlace === void 0 ? void 0 : clickedPlace.title);
2164
- const clickedPlaceId = (_b = (_a = clickedPlace === null || clickedPlace === void 0 ? void 0 : clickedPlace.mapvxId) !== null && _a !== void 0 ? _a : clickedPlace === null || clickedPlace === void 0 ? void 0 : clickedPlace.clientId) !== null && _b !== void 0 ? _b : "";
2165
- if (clickedPlace !== undefined && this.clickCallback) {
2166
- this.clickCallback(clickedPlaceId);
2167
- }
2168
- }
2169
- });
2170
- }
2171
- onHover() {
2172
- if (!this.enableHover)
2173
- return;
2174
- this.map.on("mousemove", "hoverable", (e) => {
2175
- var _a;
2176
- const feature = (_a = e.features) === null || _a === void 0 ? void 0 : _a.find((feature) => { var _a, _b; return ((_a = feature.properties.ref) === null || _a === void 0 ? void 0 : _a.length) > 0 && ((_b = feature.properties) === null || _b === void 0 ? void 0 : _b.floor_key) === this.currentFloor; });
2177
- const ref = feature === null || feature === void 0 ? void 0 : feature.properties.ref;
2178
- if (ref != null && typeof ref === "string" && ref.length > 0) {
2179
- this.hoveredId = ref;
2180
- this.updateFiltersTo(this.currentFloor);
2181
- }
2182
- else if (ref === undefined && this.hoveredId !== "unselected") {
2183
- this.hoveredId = "unselected";
2184
- this.updateFiltersTo(this.currentFloor);
2185
- }
2186
- });
2187
- // When the mouse leaves the state-fill layer, update the feature state of the
2188
- // previously hovered feature.
2189
- this.map.on("mouseleave", "hoverable", () => {
2190
- this.hoveredId = "unselected";
2191
- this.updateFiltersTo(this.currentFloor);
2192
- });
2193
- }
2194
- setPlacesAsSelected(placeIds, color) {
2195
- try {
2196
- this.coloredPlacesRequest = { placeIds: [...placeIds], color };
2197
- if (!this.hasLayer("indoor-colored-places")) {
2198
- this.createColoredPlacesLayer();
2199
- }
2200
- const basicFilter = this.createBasicFilters();
2201
- const coloredPlacesFilters = placeIds.map((id) => {
2202
- const filter = ["==", ["get", "ref"], id];
2203
- return filter;
2204
- });
2205
- const placeFilter = ["any", ...coloredPlacesFilters];
2206
- const condition = ["all", placeFilter, basicFilter];
2207
- this.map.setFilter("indoor-colored-places", condition);
2208
- // Caller-defined base filter: mark it resolved so no later capture can
2209
- // overwrite this customization with the layer's style-declared filter.
2210
- this.capturedBaseFilters.add("indoor-colored-places");
2211
- this.baseFilters["indoor-colored-places"] = placeFilter;
2212
- if (color !== undefined) {
2213
- const layer = this.map.getLayer("indoor-colored-places");
2214
- switch (layer === null || layer === void 0 ? void 0 : layer.type) {
2215
- case "fill":
2216
- this.map.setPaintProperty("indoor-colored-places", "fill-color", color);
2217
- break;
2218
- case "fill-extrusion":
2219
- this.map.setPaintProperty("indoor-colored-places", "fill-extrusion-color", color);
2220
- break;
2221
- default:
2222
- break;
2223
- }
2224
- }
2225
- }
2226
- catch (error) {
2227
- this.debugLog("Place selection failed (layer may not exist)", error);
2228
- }
2229
- }
2230
- is3DStyle() {
2231
- const layers = this.map.getStyle().layers;
2232
- return layers.some((layer) => layer.type === "fill-extrusion");
2233
- }
2234
- createColoredPlacesLayer() {
2235
- if (this.is3DStyle()) {
2236
- const layer = {
2237
- id: "indoor-colored-places",
2238
- type: "fill-extrusion",
2239
- source: "indoorequal",
2240
- "source-layer": "area",
2241
- filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
2242
- paint: {
2243
- "fill-extrusion-height": 2.5,
2244
- "fill-extrusion-color": circle_1.MAPVX_BRAND_COLOR,
2245
- "fill-extrusion-opacity": 0.8,
2246
- },
2247
- };
2248
- this.map.addLayer(layer);
2249
- }
2250
- else {
2251
- const layer = {
2252
- id: "indoor-colored-places",
2253
- type: "fill",
2254
- source: "indoorequal",
2255
- "source-layer": "area",
2256
- filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
2257
- paint: {
2258
- "fill-color": circle_1.MAPVX_BRAND_COLOR,
2259
- },
2260
- };
2261
- this.map.addLayer(layer);
2262
- }
2263
- }
2264
- createBasicFilters() {
2265
- var _a, _b;
2266
- const publicPlaceFilter = ["==", ["get", "public_place"], true];
2267
- const subPlaceFilter = [
2268
- "!",
2269
- ["in", (_a = this.parentPlaceId) !== null && _a !== void 0 ? _a : "noRef", ["get", "is_subplace"]],
2270
- ];
2271
- const floorId = (_b = this.currentFloor) !== null && _b !== void 0 ? _b : "noRef";
2272
- const floorFilter = ["in", floorId, ["get", "floor_key"]];
2273
- const showInFloorFilter = [
2274
- "in",
2275
- floorId,
2276
- ["get", "show_in_floor"],
2277
- ];
2278
- let basicFilters = publicPlaceFilter;
2279
- if (this.parentPlaceId != null) {
2280
- basicFilters = ["any", floorFilter, publicPlaceFilter];
2281
- }
2282
- const customFilters = [
2283
- "all",
2284
- basicFilters,
2285
- ["any", subPlaceFilter, showInFloorFilter],
2286
- ];
2287
- return customFilters;
2288
- }
2289
- hasLayer(layerId) {
2290
- const layer = this.map.getLayer(layerId);
2291
- return layer !== undefined;
2292
- }
2293
- clearColoredPlaces() {
2294
- this.coloredPlacesRequest = undefined;
2295
- if (this.hasLayer("indoor-colored-places")) {
2296
- const condition = ["==", ["get", "ref"], "unselected"];
2297
- // Caller-defined base filter: mark it resolved so no later capture can
2298
- // overwrite this customization with the layer's style-declared filter.
2299
- this.capturedBaseFilters.add("indoor-colored-places");
2300
- this.baseFilters["indoor-colored-places"] = condition;
2301
- this.map.setFilter("indoor-colored-places", condition);
2302
- }
2303
- }
2304
- async startAnimateRoute(routeConfig, drawConfig = {}, animationConfig = {}) {
2305
- try {
2306
- let route;
2307
- if (routeConfig instanceof route_1.MVXRoute) {
2308
- route = routeConfig;
2309
- }
2310
- else {
2311
- const fetchRouteConfig = new routeConfiguration_1.InternalGetRouteConfiguration(routeConfig);
2312
- route = await this.repository.fetchAndParseRoute(fetchRouteConfig);
2313
- }
2314
- const drawRouteConfig = new animation_1.InternalAnimationDrawingConfig(drawConfig);
2315
- const animationRouteConfig = new animation_1.InternalAnimationConfig(animationConfig);
2316
- this.routeController.startAnimating(route, drawRouteConfig, animationRouteConfig);
2317
- return route;
2318
- }
2319
- catch (error) {
2320
- throw new Error("Error: Failed to start animating route", { cause: error });
2321
- }
2322
- }
2323
- async startAnimateRouteV2(routeConfig, drawConfig = {}, animationConfig = {}) {
2324
- try {
2325
- let route;
2326
- if (routeConfig instanceof route_1.MVXRoute) {
2327
- route = routeConfig;
2328
- }
2329
- else {
2330
- const fetchRouteConfig = new routeConfiguration_1.InternalGetRouteConfiguration(routeConfig);
2331
- route = await this.repository.fetchAndParseRouteV2(fetchRouteConfig);
2332
- }
2333
- const drawRouteConfig = new animation_1.InternalAnimationDrawingConfig(drawConfig);
2334
- const animationRouteConfig = new animation_1.InternalAnimationConfig(animationConfig);
2335
- this.routeController.startAnimating(route, drawRouteConfig, animationRouteConfig);
2336
- return route;
2337
- }
2338
- catch (error) {
2339
- throw new Error("Error: Failed to start animating route", { cause: error });
2340
- }
2341
- }
2342
- pauseAnimateRoute(routeId) {
2343
- try {
2344
- this.routeController.pauseAnimation(routeId);
2345
- this.logEvent("pauseAnimateRoute");
2346
- }
2347
- catch (error) {
2348
- this.debugLog("Pause animation failed (no animation may be running)", error);
2349
- }
2350
- }
2351
- resumeAnimateRoute(routeId) {
2352
- try {
2353
- this.routeController.resumeAnimation(routeId);
2354
- this.logEvent("resumeAnimateRoute");
2355
- }
2356
- catch (error) {
2357
- this.debugLog("Resume animation failed (no animation may be paused)", error);
2358
- }
2359
- }
2360
- restartAnimateRoute(routeId) {
2361
- try {
2362
- this.routeController.restartAnimation(routeId);
2363
- this.logEvent("restartAnimateRoute");
2364
- }
2365
- catch (error) {
2366
- this.debugLog("Restart animation failed (no animation may exist)", error);
2367
- }
2368
- }
2369
- stopAnimateRoute() {
2370
- this.logWarning(new Error("Not implemented"), {
2371
- functionName: "stopAnimateRoute",
2372
- });
2373
- }
2374
- async addStepAnimation(routeConfig, drawConfig = {}, animationConfig = {}) {
2375
- try {
2376
- let route;
2377
- if (routeConfig instanceof route_1.MVXRoute) {
2378
- route = routeConfig;
2379
- }
2380
- else {
2381
- const fetchRouteConfig = new routeConfiguration_1.InternalGetRouteConfiguration(routeConfig);
2382
- route = await this.repository.fetchAndParseRoute(fetchRouteConfig);
2383
- }
2384
- const drawRouteConfig = new animation_1.InternalAnimationDrawingConfig(drawConfig);
2385
- const animationRouteConfig = new animation_1.InternalAnimationConfig(animationConfig);
2386
- this.routeController.addStepAnimation(route, drawRouteConfig, animationRouteConfig);
2387
- this.logEvent("addStepAnimation");
2388
- return route;
2389
- }
2390
- catch (error) {
2391
- throw new Error("Error: Failed to add step animation", { cause: error });
2392
- }
2393
- }
2394
- async addStepAnimationV2(routeConfig, drawConfig = {}, animationConfig = {}) {
2395
- try {
2396
- let route;
2397
- if (routeConfig instanceof route_1.MVXRoute) {
2398
- route = routeConfig;
2399
- }
2400
- else {
2401
- const fetchRouteConfig = new routeConfiguration_1.InternalGetRouteConfiguration(routeConfig);
2402
- route = await this.repository.fetchAndParseRouteV2(fetchRouteConfig);
2403
- }
2404
- const drawRouteConfig = new animation_1.InternalAnimationDrawingConfig(drawConfig);
2405
- const animationRouteConfig = new animation_1.InternalAnimationConfig(animationConfig);
2406
- this.routeController.addStepAnimation(route, drawRouteConfig, animationRouteConfig);
2407
- this.logEvent("addStepAnimationV2");
2408
- return route;
2409
- }
2410
- catch (error) {
2411
- throw new Error("Error: Failed to add step animation", { cause: error });
2412
- }
2413
- }
2414
- nextStepAnimation(routeId) {
2415
- try {
2416
- this.routeController.nextStepAnimation(routeId);
2417
- this.logEvent("nextStepAnimation");
2418
- }
2419
- catch (error) {
2420
- throw new Error("Error: Failed to animate step", { cause: error });
2421
- }
2422
- }
2423
- previousStepAnimation(routeId) {
2424
- try {
2425
- this.routeController.previousStepAnimation(routeId);
2426
- this.logEvent("previousStepAnimation");
2427
- }
2428
- catch (error) {
2429
- const errorMessage = error instanceof Error ? error.message : String(error);
2430
- throw new Error("Error: Failed to prepare to animate previous step, " + errorMessage);
2431
- }
2432
- }
2433
- centerOnCurrentStepAnimation(routeId) {
2434
- try {
2435
- this.routeController.centerOnCurrentStep(routeId);
2436
- this.logEvent("centerOnCurrentStepAnimation");
2437
- }
2438
- catch (error) {
2439
- const errorMessage = error instanceof Error ? error.message : String(error);
2440
- throw new Error("Error: Failed to center on current step animation, " + errorMessage);
2441
- }
2442
- }
2443
- addUserLocationTracking(enableHighAccuracy, geoLocation, icon) {
2444
- const initialCoordinate = {
2445
- lat: 0.0,
2446
- lng: 0.0,
2447
- };
2448
- let markerIcon;
2449
- if (icon != null) {
2450
- markerIcon = icon;
2451
- }
2452
- else {
2453
- const image = new Image(40, 40);
2454
- image.src = icons_1.userLocationDataUrl;
2455
- markerIcon = image;
2456
- }
2457
- const markerConfig = {
2458
- coordinate: initialCoordinate,
2459
- icon: markerIcon,
2460
- text: "",
2461
- };
2462
- const marker = new marker_1.MarkerAttribute(markerConfig);
2463
- marker.addToMap(this.map);
2464
- this.userLocationMarker = marker;
2465
- this.markerLayoutController.scheduleRelayout();
2466
- const updateMarker = (position) => {
2467
- marker.setLngLat([position.coords.longitude, position.coords.latitude]);
2468
- this.markerLayoutController.scheduleRelayout();
2469
- };
2470
- this.geoLocation = geoLocation !== null && geoLocation !== void 0 ? geoLocation : navigator.geolocation;
2471
- this.watchPositionID = this.geoLocation.watchPosition(updateMarker, (error) => {
2472
- console.error("Error getting user location:", error);
2473
- }, {
2474
- enableHighAccuracy: enableHighAccuracy,
2475
- });
2476
- }
2477
- removeUserLocationTracking() {
2478
- if (this.userLocationMarker !== undefined) {
2479
- this.userLocationMarker.remove();
2480
- this.userLocationMarker = undefined;
2481
- this.markerLayoutController.scheduleRelayout();
2482
- // Check if watchPosition ID is valid and stop tracking
2483
- if (this.watchPositionID) {
2484
- this.geoLocation.clearWatch(this.watchPositionID);
2485
- this.watchPositionID = undefined;
2486
- }
2487
- }
2488
- }
2489
- getBearing() {
2490
- return this.map.getBearing();
2491
- }
2492
- addBorderToPlaces(placeIds, color, width) {
2493
- try {
2494
- this.borderedPlacesRequest = { placeIds: [...placeIds], color, width };
2495
- if (!this.hasLayer("indoor-bordered-places")) {
2496
- this.createBorderedPlacesLayer();
2497
- }
2498
- const basicFilter = this.createBasicFilters();
2499
- const coloredPlacesFilters = placeIds.map((id) => {
2500
- const filter = ["==", ["get", "ref"], id];
2501
- return filter;
2502
- });
2503
- const placeFilter = ["any", ...coloredPlacesFilters];
2504
- const condition = ["all", placeFilter, basicFilter];
2505
- this.map.setFilter("indoor-bordered-places", condition);
2506
- // Caller-defined base filter: mark it resolved so no later capture can
2507
- // overwrite this customization with the layer's style-declared filter.
2508
- this.capturedBaseFilters.add("indoor-bordered-places");
2509
- this.baseFilters["indoor-bordered-places"] = placeFilter;
2510
- if (color !== undefined && width !== undefined) {
2511
- this.map.setPaintProperty("indoor-bordered-places", "line-color", color);
2512
- this.map.setPaintProperty("indoor-bordered-places", "line-width", width);
2513
- }
2514
- }
2515
- catch (error) {
2516
- throw new Error("Failed to add border to places", { cause: error });
2517
- }
2518
- }
2519
- createBorderedPlacesLayer() {
2520
- const layer = {
2521
- id: "indoor-bordered-places",
2522
- type: "line",
2523
- source: "indoorequal",
2524
- "source-layer": "area",
2525
- filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
2526
- paint: {
2527
- "line-color": circle_1.MAPVX_BRAND_COLOR,
2528
- "line-width": 4,
2529
- },
2530
- };
2531
- this.map.addLayer(layer);
2532
- }
2533
- clearBorderedPlaces() {
2534
- this.borderedPlacesRequest = undefined;
2535
- if (this.hasLayer("indoor-bordered-places")) {
2536
- const condition = ["==", ["get", "ref"], "unselected"];
2537
- // Caller-defined base filter: mark it resolved so no later capture can
2538
- // overwrite this customization with the layer's style-declared filter.
2539
- this.capturedBaseFilters.add("indoor-bordered-places");
2540
- this.baseFilters["indoor-bordered-places"] = condition;
2541
- this.map.setFilter("indoor-bordered-places", condition);
2542
- }
2543
- }
2544
- onWebGLContextLost(e) {
2545
- var _a, _b;
2546
- e.originalEvent.preventDefault();
2547
- console.warn("Contexto WebGL perdido 😢");
2548
- // Intentar restaurar el contexto automáticamente
2549
- const canvas = (_a = this === null || this === void 0 ? void 0 : this.map) === null || _a === void 0 ? void 0 : _a.getCanvas();
2550
- const gl = (canvas === null || canvas === void 0 ? void 0 : canvas.getContext("webgl")) || (canvas === null || canvas === void 0 ? void 0 : canvas.getContext("webgl2"));
2551
- if (gl && gl.isContextLost()) {
2552
- canvas.addEventListener("webglcontextrestored", () => {
2553
- console.debug("Contexto WebGL restaurado, forzando re-render");
2554
- this.map.triggerRepaint(); // forzar repaint
2555
- this.failedTiles.clear();
2556
- }, { once: true });
2557
- const ext = gl.getExtension("WEBGL_lose_context");
2558
- (_b = ext === null || ext === void 0 ? void 0 : ext.restoreContext) === null || _b === void 0 ? void 0 : _b.call(ext); // si aplica
2559
- }
2560
- }
2561
- onWebGLContextRestored() {
2562
- console.debug("Evento webglcontextrestored recibido, haciendo map.redraw()");
2563
- this.map.redraw(); // fuerza un redraw síncrono [oai_citation:0‡maplibre.org](https://www.maplibre.org/maplibre-gl-js/docs/API/classes/Map/?utm_source=chatgpt.com)
2564
- this.failedTiles.clear();
2565
- }
2566
- onSourceData(e) {
2567
- var _a;
2568
- // Only track tiles that actually failed to load (not aborted or never loaded)
2569
- if (e.source.type === "vector" && e.sourceId === "indoorequal" && e.tile && !e.isSourceLoaded) {
2570
- // Check if tile has an error state before adding to retry queue
2571
- // Access state property safely as it may not always be available
2572
- const tileState = (_a = e.tile) === null || _a === void 0 ? void 0 : _a.state;
2573
- if (tileState === "errored" || tileState === "expired") {
2574
- this.failedTiles.add(e.tile);
2575
- }
2576
- }
2577
- }
2578
- onSourceDataAbort(e) {
2579
- // Remove aborted tiles from retry queue as they shouldn't be retried
2580
- if (e.source.type === "vector" && e.sourceId === "indoorequal" && e.tile) {
2581
- this.failedTiles.delete(e.tile);
2582
- }
2583
- }
2584
- /**
2585
- * True when a style object is attached to the map. During a full `setStyle` swap the
2586
- * map is momentarily style-less and any `getSource`/`getFilter` call throws
2587
- * ("Cannot read properties of undefined"), so call sites that only need the style to
2588
- * exist should gate on this rather than on `isStyleLoaded()`.
2589
- */
2590
- /**
2591
- * This style's layers, or `null` when the map has no style right now.
2592
- *
2593
- * The one place the style is read for a floor change, so callers can pass the
2594
- * result on instead of each probing again — `getStyle()` serializes every
2595
- * source and layer, and `Style._updateLayer` drops the cache behind it on
2596
- * every filter or layout write.
2597
- */
2598
- safeStyleLayers() {
2599
- var _a, _b, _c;
2600
- try {
2601
- return (_c = (_b = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getStyle()) === null || _b === void 0 ? void 0 : _b.layers) !== null && _c !== void 0 ? _c : null;
2602
- }
2603
- catch (_d) {
2604
- return null;
2605
- }
2606
- }
2607
- hasStyle() {
2608
- var _a;
2609
- // `map.getStyle()` is the PUBLIC probe: it returns the serialized style, or throws
2610
- // while the map is momentarily style-less mid-swap. Reading maplibre's internal
2611
- // `style` field instead (it is absent from the public typings, so it needed a double
2612
- // cast) would fail closed and silently forever the moment a maplibre upgrade renames
2613
- // or privatizes it — disabling both the tile-retry loop and `setStyle`'s apply pass
2614
- // with no throw and no log. `safeGetFilter` already uses this try/catch shape.
2615
- try {
2616
- return ((_a = this.map) === null || _a === void 0 ? void 0 : _a.getStyle()) != null;
2617
- }
2618
- catch (_b) {
2619
- return false;
2620
- }
2621
- }
2622
- /**
2623
- * True when the map has a style object attached and fully loaded. Unlike
2624
- * `map.isStyleLoaded()`, this does not emit maplibre's "There is no style
2625
- * added to the map" warning when the style is momentarily absent (e.g. during
2626
- * a `setStyle` swap), because it checks for the style object first.
2627
- */
2628
- hasLoadedStyle() {
2629
- if (!this.hasStyle())
2630
- return false;
2631
- return this.map.isStyleLoaded() === true;
2632
- }
2633
- subscribeToFailedTiles() {
2634
- if (this.retryTimer)
2635
- return;
2636
- // Store bound functions for proper cleanup
2637
- this.boundOnSourceData = this.onSourceData.bind(this);
2638
- this.boundOnSourceDataAbort = this.onSourceDataAbort.bind(this);
2639
- this.boundOnWebGLContextLost = this.onWebGLContextLost.bind(this);
2640
- this.boundOnWebGLContextRestored = this.onWebGLContextRestored.bind(this);
2641
- this.retryTimer = setInterval(() => {
2642
- // During a full style swap (`setStyle` with `diff: false`) the map can
2643
- // momentarily have no style object attached. Calling `getSource` then
2644
- // throws ("Cannot read properties of undefined (reading 'getSource')").
2645
- // Only the style's PRESENCE is checked here, never `isStyleLoaded()`:
2646
- // that is false while any source still has tiles in flight, which is
2647
- // exactly when tiles fail — gating on it would skip every tick during a
2648
- // bad-network burst. The queue is left untouched so the tiles queued
2649
- // mid-swap are still retried on the next tick.
2650
- if (!this.hasStyle()) {
2651
- return;
2652
- }
2653
- if (!this.map.getSource("indoorequal")) {
2654
- this.failedTiles.clear();
2655
- return;
2656
- }
2657
- const source = this.map.getSource("indoorequal");
2658
- if (!source || source.type !== "vector") {
2659
- this.failedTiles.clear();
2660
- return;
2661
- }
2662
- // Process tiles and remove invalid ones
2663
- const tilesToRetry = [];
2664
- const tilesToRemove = [];
2665
- this.failedTiles.forEach((tile) => {
2666
- // Check if tile is still valid before attempting reload
2667
- if (!tile || tile.state === "unloaded" || tile.state === "loading") {
2668
- tilesToRemove.push(tile);
2669
- return;
2670
- }
2671
- // Only retry tiles that are in an error state
2672
- if (tile.state === "errored" || tile.state === "expired") {
2673
- tilesToRetry.push(tile);
2674
- }
2675
- else {
2676
- // Tile is in a valid state, remove from retry queue
2677
- tilesToRemove.push(tile);
2678
- }
2679
- });
2680
- // Remove invalid tiles from the set
2681
- tilesToRemove.forEach((tile) => this.failedTiles.delete(tile));
2682
- // Attempt to reload valid failed tiles
2683
- const now = Date.now();
2684
- tilesToRetry.forEach((tile) => {
2685
- var _a;
2686
- const retry = (_a = this.tileRetryState.get(tile)) !== null && _a !== void 0 ? _a : {
2687
- attempts: 0,
2688
- nextAttemptAt: 0,
2689
- pending: false,
2690
- };
2691
- // Skip tiles with a reload still in flight or still inside their backoff window.
2692
- if (retry.pending || now < retry.nextAttemptAt)
2693
- return;
2694
- const onFailure = (error) => {
2695
- const attempts = retry.attempts + 1;
2696
- if (attempts >= MAX_TILE_RETRIES) {
2697
- this.debugLog(`Tile reload failed ${attempts} times, dropping it from the retry queue`, error);
2698
- this.failedTiles.delete(tile);
2699
- this.tileRetryState.delete(tile);
2700
- return;
2701
- }
2702
- // Stay in the queue and try again once the backoff has elapsed.
2703
- const backoffMs = TILE_RETRY_BASE_DELAY_MS * 2 ** (attempts - 1);
2704
- this.debugLog(`Tile reload failed (attempt ${attempts}/${MAX_TILE_RETRIES}), retrying in ${backoffMs}ms`, error);
2705
- this.tileRetryState.set(tile, {
2706
- attempts,
2707
- nextAttemptAt: Date.now() + backoffMs,
2708
- pending: false,
2709
- });
2710
- };
2711
- try {
2712
- // Verify tile still exists and is in a retryable state
2713
- if (tile && (tile.state === "errored" || tile.state === "expired")) {
2714
- this.tileRetryState.set(tile, Object.assign(Object.assign({}, retry), { pending: true }));
2715
- // loadTile is async, so a rejection escapes this try/catch and has to be handled
2716
- // on the promise itself - otherwise it surfaces as an unhandled rejection and the
2717
- // tile is retried on every tick regardless of how often it has already failed.
2718
- source
2719
- .loadTile(tile)
2720
- .then(() => {
2721
- // Loaded: forget the attempt history so a later failure starts from zero. The
2722
- // tile leaves the queue through the tilesToRemove branch on the next tick,
2723
- // once its state is no longer errored/expired.
2724
- this.tileRetryState.delete(tile);
2725
- })
2726
- .catch(onFailure);
2727
- }
2728
- }
2729
- catch (error) {
2730
- onFailure(error);
2731
- }
2732
- });
2733
- }, 1000);
2734
- this.map.on("sourcedata", this.boundOnSourceData);
2735
- this.map.on("sourcedataabort", this.boundOnSourceDataAbort);
2736
- // Escucha el evento de contexto perdido
2737
- this.map.on("webglcontextlost", this.boundOnWebGLContextLost);
2738
- // Adicional: escucha evento restauración
2739
- this.map.on("webglcontextrestored", this.boundOnWebGLContextRestored);
2740
- }
2741
- unsubscribeFromFailedTiles() {
2742
- if (this.retryTimer) {
2743
- clearInterval(this.retryTimer);
2744
- this.retryTimer = undefined;
2745
- }
2746
- if (this.boundOnSourceData) {
2747
- this.map.off("sourcedata", this.boundOnSourceData);
2748
- this.boundOnSourceData = undefined;
2749
- }
2750
- if (this.boundOnSourceDataAbort) {
2751
- this.map.off("sourcedataabort", this.boundOnSourceDataAbort);
2752
- this.boundOnSourceDataAbort = undefined;
2753
- }
2754
- if (this.boundOnWebGLContextLost) {
2755
- this.map.off("webglcontextlost", this.boundOnWebGLContextLost);
2756
- this.boundOnWebGLContextLost = undefined;
2757
- }
2758
- if (this.boundOnWebGLContextRestored) {
2759
- this.map.off("webglcontextrestored", this.boundOnWebGLContextRestored);
2760
- this.boundOnWebGLContextRestored = undefined;
2761
- }
2762
- this.failedTiles.clear();
2763
- }
2764
- }
2765
- exports.InternalMapVXMap = InternalMapVXMap;
2766
- //# sourceMappingURL=map.js.map