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

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