@mapvx/web-js 1.1.0 → 1.1.2-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +2 -2
- package/README.md +12 -9
- package/dist/cjs/assets/icons.js +6 -8
- package/dist/cjs/assets/icons.js.map +1 -1
- package/dist/cjs/controllers/routeController.js +19 -19
- package/dist/cjs/controllers/routeController.js.map +1 -1
- package/dist/cjs/domain/models/animation.js +2 -2
- package/dist/cjs/domain/models/categories.js +23 -10
- package/dist/cjs/domain/models/categories.js.map +1 -1
- package/dist/cjs/domain/models/circle.js +253 -0
- package/dist/cjs/domain/models/circle.js.map +1 -0
- package/dist/cjs/domain/models/mapConfig.js +10 -1
- package/dist/cjs/domain/models/mapConfig.js.map +1 -1
- package/dist/cjs/domain/models/marker.js +92 -80
- package/dist/cjs/domain/models/marker.js.map +1 -1
- package/dist/cjs/domain/models/routeConfiguration.js +3 -1
- package/dist/cjs/domain/models/routeConfiguration.js.map +1 -1
- package/dist/cjs/index.js +24 -10
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/logger/logger.js +21 -9
- package/dist/cjs/logger/logger.js.map +1 -1
- package/dist/cjs/logger/rollbar.js +21 -8
- package/dist/cjs/logger/rollbar.js.map +1 -1
- package/dist/cjs/map/map.js +446 -28
- package/dist/cjs/map/map.js.map +1 -1
- package/dist/cjs/map/mapInteractionOptions.js +56 -0
- package/dist/cjs/map/mapInteractionOptions.js.map +1 -0
- package/dist/cjs/repository/repository.js +25 -26
- package/dist/cjs/repository/repository.js.map +1 -1
- package/dist/cjs/repository/requester.js +71 -91
- package/dist/cjs/repository/requester.js.map +1 -1
- package/dist/cjs/sdk.js +18 -1
- package/dist/cjs/sdk.js.map +1 -1
- package/dist/cjs/utils/preconnect.js +131 -0
- package/dist/cjs/utils/preconnect.js.map +1 -0
- package/dist/cjs/utils/semaphore.js +143 -0
- package/dist/cjs/utils/semaphore.js.map +1 -0
- package/dist/es/assets/icons.d.ts +4 -4
- package/dist/es/assets/icons.d.ts.map +1 -1
- package/dist/es/assets/icons.js +6 -8
- package/dist/es/assets/icons.js.map +1 -1
- package/dist/es/controllers/routeController.d.ts.map +1 -1
- package/dist/es/controllers/routeController.js +19 -19
- package/dist/es/controllers/routeController.js.map +1 -1
- package/dist/es/domain/models/animation.d.ts +3 -3
- package/dist/es/domain/models/animation.js +2 -2
- package/dist/es/domain/models/categories.d.ts +34 -10
- package/dist/es/domain/models/categories.d.ts.map +1 -1
- package/dist/es/domain/models/categories.js +21 -9
- package/dist/es/domain/models/categories.js.map +1 -1
- package/dist/es/domain/models/circle.d.ts +222 -0
- package/dist/es/domain/models/circle.d.ts.map +1 -0
- package/dist/es/domain/models/circle.js +246 -0
- package/dist/es/domain/models/circle.js.map +1 -0
- package/dist/es/domain/models/configuration.d.ts +8 -0
- package/dist/es/domain/models/configuration.d.ts.map +1 -1
- package/dist/es/domain/models/mapConfig.d.ts +118 -3
- package/dist/es/domain/models/mapConfig.d.ts.map +1 -1
- package/dist/es/domain/models/mapConfig.js +9 -0
- package/dist/es/domain/models/mapConfig.js.map +1 -1
- package/dist/es/domain/models/marker.d.ts +16 -0
- package/dist/es/domain/models/marker.d.ts.map +1 -1
- package/dist/es/domain/models/marker.js +92 -80
- package/dist/es/domain/models/marker.js.map +1 -1
- package/dist/es/domain/models/routeConfiguration.d.ts +47 -0
- package/dist/es/domain/models/routeConfiguration.d.ts.map +1 -1
- package/dist/es/domain/models/routeConfiguration.js +3 -1
- package/dist/es/domain/models/routeConfiguration.js.map +1 -1
- package/dist/es/index.d.ts +15 -11
- package/dist/es/index.d.ts.map +1 -1
- package/dist/es/index.js +10 -5
- package/dist/es/index.js.map +1 -1
- package/dist/es/interfaces/routeCacheResponse.d.ts.map +1 -1
- package/dist/es/logger/logger.d.ts.map +1 -1
- package/dist/es/logger/logger.js +21 -9
- package/dist/es/logger/logger.js.map +1 -1
- package/dist/es/logger/rollbar.d.ts.map +1 -1
- package/dist/es/logger/rollbar.js +21 -8
- package/dist/es/logger/rollbar.js.map +1 -1
- package/dist/es/map/map.d.ts +298 -0
- package/dist/es/map/map.d.ts.map +1 -1
- package/dist/es/map/map.js +447 -29
- package/dist/es/map/map.js.map +1 -1
- package/dist/es/map/mapInteractionOptions.d.ts +37 -0
- package/dist/es/map/mapInteractionOptions.d.ts.map +1 -0
- package/dist/es/map/mapInteractionOptions.js +51 -0
- package/dist/es/map/mapInteractionOptions.js.map +1 -0
- package/dist/es/repository/repository.d.ts +0 -1
- package/dist/es/repository/repository.d.ts.map +1 -1
- package/dist/es/repository/repository.js +25 -26
- package/dist/es/repository/repository.js.map +1 -1
- package/dist/es/repository/requester.d.ts +12 -2
- package/dist/es/repository/requester.d.ts.map +1 -1
- package/dist/es/repository/requester.js +71 -91
- package/dist/es/repository/requester.js.map +1 -1
- package/dist/es/sdk.d.ts +2 -0
- package/dist/es/sdk.d.ts.map +1 -1
- package/dist/es/sdk.js +18 -1
- package/dist/es/sdk.js.map +1 -1
- package/dist/es/utils/preconnect.d.ts +45 -0
- package/dist/es/utils/preconnect.d.ts.map +1 -0
- package/dist/es/utils/preconnect.js +127 -0
- package/dist/es/utils/preconnect.js.map +1 -0
- package/dist/es/utils/semaphore.d.ts +70 -0
- package/dist/es/utils/semaphore.d.ts.map +1 -0
- package/dist/es/utils/semaphore.js +139 -0
- package/dist/es/utils/semaphore.js.map +1 -0
- package/dist/umd/index.js +1968 -669
- package/dist/umd/index.js.map +1 -1
- package/dist/umd/styles.css +32 -14
- package/dist/umd/styles.css.map +1 -1
- package/package.json +63 -49
- package/dist/cjs/assets/route_animation_icon.svg +0 -15
- package/dist/cjs/assets/user-dot-icon.svg +0 -3
- package/dist/es/assets/route_animation_icon.svg +0 -15
- package/dist/es/assets/user-dot-icon.svg +0 -3
package/dist/cjs/map/map.js
CHANGED
|
@@ -25,6 +25,31 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.InternalMapVXMap = void 0;
|
|
27
27
|
const maplibre_gl_1 = __importStar(require("maplibre-gl"));
|
|
28
|
+
const semaphore_1 = require("../utils/semaphore");
|
|
29
|
+
/** Shared GeoJSON source holding every circle drawn through the circle API. */
|
|
30
|
+
const CIRCLE_SOURCE_ID = "mapvx-circles";
|
|
31
|
+
/** Fill layer rendering the translucent interior of the circles. */
|
|
32
|
+
const CIRCLE_FILL_LAYER_ID = "mapvx-circles-fill";
|
|
33
|
+
/** Line layer rendering the circle outlines. */
|
|
34
|
+
const CIRCLE_LINE_LAYER_ID = "mapvx-circles-line";
|
|
35
|
+
/**
|
|
36
|
+
* Layer-id suffix per circle render order. The default placement keeps the
|
|
37
|
+
* unsuffixed ids so existing consumers referencing them keep working.
|
|
38
|
+
*/
|
|
39
|
+
const CIRCLE_LAYER_SUFFIXES = {
|
|
40
|
+
aboveBasemap: "",
|
|
41
|
+
belowLabels: "-below-labels",
|
|
42
|
+
top: "-top",
|
|
43
|
+
};
|
|
44
|
+
/** Fill/line layer ids for one circle render-order bucket. */
|
|
45
|
+
function circleLayerIdsFor(order) {
|
|
46
|
+
const suffix = CIRCLE_LAYER_SUFFIXES[order];
|
|
47
|
+
return { fill: CIRCLE_FILL_LAYER_ID + suffix, line: CIRCLE_LINE_LAYER_ID + suffix };
|
|
48
|
+
}
|
|
49
|
+
/** True for the layer ids owned by the circle API, regardless of bucket. */
|
|
50
|
+
function isCircleLayerId(id) {
|
|
51
|
+
return id.startsWith(CIRCLE_FILL_LAYER_ID) || id.startsWith(CIRCLE_LINE_LAYER_ID);
|
|
52
|
+
}
|
|
28
53
|
// Flag to track if cached-tile protocol has been registered
|
|
29
54
|
let cachedTileProtocolRegistered = false;
|
|
30
55
|
/**
|
|
@@ -45,21 +70,36 @@ function deepClone(obj) {
|
|
|
45
70
|
/**
|
|
46
71
|
* Register a custom protocol for cached tiles that routes requests through the main thread.
|
|
47
72
|
* This allows the service worker to intercept and cache tile requests.
|
|
73
|
+
*
|
|
74
|
+
* Wraps fetches in a {@link Semaphore} so tile CDN / WAFs are not hit with
|
|
75
|
+
* unbounded parallel requests (same host as the service worker cache). The
|
|
76
|
+
* semaphore is created once with the limit from the first map's config; later
|
|
77
|
+
* calls are no-ops because the protocol can only be registered once globally
|
|
78
|
+
* on MapLibre.
|
|
79
|
+
*
|
|
80
|
+
* @param maxConcurrentFetches - Maximum number of in-flight tile fetches.
|
|
48
81
|
*/
|
|
49
|
-
function registerCachedTileProtocol() {
|
|
82
|
+
function registerCachedTileProtocol(maxConcurrentFetches) {
|
|
50
83
|
if (cachedTileProtocolRegistered)
|
|
51
84
|
return;
|
|
85
|
+
const semaphore = new semaphore_1.Semaphore(maxConcurrentFetches);
|
|
52
86
|
maplibre_gl_1.default.addProtocol("cached-tile", (params, abortController) => {
|
|
53
|
-
// Convert cached-tile:// URL back to https://
|
|
54
87
|
const url = params.url.replace("cached-tile://", "https://");
|
|
55
|
-
|
|
88
|
+
// Pass the abort signal to acquire() so a request cancelled while still
|
|
89
|
+
// queued (e.g. during rapid zoom) drops out of the FIFO queue instead of
|
|
90
|
+
// waiting for a slot just to bail. release() runs only inside this chain,
|
|
91
|
+
// i.e. only after a slot was actually granted.
|
|
92
|
+
return semaphore.acquire(abortController.signal).then(() => fetch(url, { signal: abortController.signal })
|
|
56
93
|
.then((response) => {
|
|
57
94
|
if (!response.ok) {
|
|
58
95
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
59
96
|
}
|
|
60
97
|
return response.arrayBuffer();
|
|
61
98
|
})
|
|
62
|
-
.then((data) => ({ data }))
|
|
99
|
+
.then((data) => ({ data }))
|
|
100
|
+
.finally(() => {
|
|
101
|
+
semaphore.release();
|
|
102
|
+
}));
|
|
63
103
|
});
|
|
64
104
|
cachedTileProtocolRegistered = true;
|
|
65
105
|
}
|
|
@@ -119,12 +159,14 @@ const _rtl_1 = require("../domain/models/_rtl");
|
|
|
119
159
|
const animation_1 = require("../domain/models/animation");
|
|
120
160
|
const loggeable_1 = require("../domain/models/loggeable");
|
|
121
161
|
const mapConfig_1 = require("../domain/models/mapConfig");
|
|
162
|
+
const circle_1 = require("../domain/models/circle");
|
|
122
163
|
const marker_1 = require("../domain/models/marker");
|
|
123
164
|
const route_1 = require("../domain/models/route");
|
|
124
165
|
const routeConfiguration_1 = require("../domain/models/routeConfiguration");
|
|
125
166
|
const repository_1 = require("../repository/repository");
|
|
126
167
|
const utils_1 = require("../utils/utils");
|
|
127
168
|
const route_utils_1 = require("../utils/route-utils");
|
|
169
|
+
const mapInteractionOptions_1 = require("./mapInteractionOptions");
|
|
128
170
|
/**
|
|
129
171
|
* Class to interact with the map.
|
|
130
172
|
* @category Map
|
|
@@ -148,6 +190,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
148
190
|
this.currentFloor = "";
|
|
149
191
|
this.baseFilters = {};
|
|
150
192
|
this.markers = [];
|
|
193
|
+
this.circles = [];
|
|
151
194
|
this.enableHover = false;
|
|
152
195
|
this.hoveredId = "unselected";
|
|
153
196
|
this.failedTiles = new Set();
|
|
@@ -163,8 +206,11 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
163
206
|
this.watchPositionID = undefined;
|
|
164
207
|
this.onFloorChange = mapConfig.onFloorChange;
|
|
165
208
|
this.onParentPlaceChange = mapConfig.onParentPlaceChange;
|
|
166
|
-
|
|
167
|
-
|
|
209
|
+
this.tileCacheConfig = (() => {
|
|
210
|
+
const merged = Object.assign(Object.assign({}, mapConfig_1.DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
|
|
211
|
+
merged.maxTiles = Math.min(merged.maxTiles, mapConfig_1.MAPLIBRE_MAX_TILE_CACHE_HARD_CAP);
|
|
212
|
+
return merged;
|
|
213
|
+
})();
|
|
168
214
|
if (mapConfig.parentPlaceId != null) {
|
|
169
215
|
this.initialPlaceDetailSetUp(mapConfig.parentPlaceId, mapConfig.authToken);
|
|
170
216
|
}
|
|
@@ -261,28 +307,17 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
261
307
|
.catch(console.error);
|
|
262
308
|
}
|
|
263
309
|
onMapStyleLoaded(mapConfig, container, style) {
|
|
264
|
-
var _a, _b, _c;
|
|
310
|
+
var _a, _b, _c, _d, _e;
|
|
265
311
|
// Determine if service worker caching should be enabled
|
|
266
312
|
const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
|
|
267
|
-
// Register cached-tile protocol only if service worker caching is enabled
|
|
268
313
|
if (useServiceWorkerCaching) {
|
|
269
|
-
registerCachedTileProtocol();
|
|
314
|
+
registerCachedTileProtocol(this.tileCacheConfig.maxConcurrentTileFetches);
|
|
270
315
|
}
|
|
271
316
|
// Transform tile URLs only if service worker caching is enabled
|
|
272
317
|
const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
|
|
273
|
-
const mapOptions = {
|
|
274
|
-
container,
|
|
275
|
-
style: finalStyle,
|
|
276
|
-
center: mapConfig.center,
|
|
277
|
-
zoom: mapConfig.zoom,
|
|
278
|
-
pitch: (_a = mapConfig.pitch) !== null && _a !== void 0 ? _a : 0,
|
|
279
|
-
attributionControl: false,
|
|
280
|
-
maplibreLogo: false,
|
|
281
|
-
bearingSnap: (_b = mapConfig.bearingSnap) !== null && _b !== void 0 ? _b : 0,
|
|
282
|
-
cancelPendingTileRequestsWhileZooming: false,
|
|
318
|
+
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,
|
|
283
319
|
// Use configured maxTiles for MapLibre's memory cache
|
|
284
|
-
maxTileCacheSize: this.tileCacheConfig.maxTiles,
|
|
285
|
-
};
|
|
320
|
+
maxTileCacheSize: this.tileCacheConfig.maxTiles }, (0, mapInteractionOptions_1.buildInteractionOptions)(mapConfig));
|
|
286
321
|
if (mapConfig.maxZoom)
|
|
287
322
|
mapOptions.maxZoom = mapConfig.maxZoom;
|
|
288
323
|
if (mapConfig.minZoom)
|
|
@@ -292,10 +327,15 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
292
327
|
mapOptions.maxBounds = new maplibre_gl_1.LngLatBounds([boundingBox[0].lng, boundingBox[0].lat], [boundingBox[1].lng, boundingBox[1].lat]);
|
|
293
328
|
}
|
|
294
329
|
this.map = new maplibre_gl_1.Map(mapOptions);
|
|
330
|
+
// When rotation is disabled we still want pinch-to-zoom to work, so the
|
|
331
|
+
// two-finger rotation is turned off here instead of via the constructor.
|
|
332
|
+
if ((0, mapInteractionOptions_1.shouldDisableTouchRotation)(mapConfig)) {
|
|
333
|
+
(_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.disableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
334
|
+
}
|
|
295
335
|
this.map.addControl(new maplibre_gl_1.NavigationControl({
|
|
296
336
|
showCompass: mapConfig.showCompass !== undefined ? mapConfig.showCompass : true,
|
|
297
337
|
showZoom: mapConfig.showZoom !== undefined ? mapConfig.showZoom : true,
|
|
298
|
-
}), (
|
|
338
|
+
}), (_e = mapConfig.navigationPosition) !== null && _e !== void 0 ? _e : "top-right");
|
|
299
339
|
this.map.on("load", () => {
|
|
300
340
|
var _a;
|
|
301
341
|
this.whenStyleUpdates(style);
|
|
@@ -305,6 +345,12 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
305
345
|
this.onHover();
|
|
306
346
|
this.subscribeToFailedTiles();
|
|
307
347
|
});
|
|
348
|
+
// Self-healing for circles: a full style reload wipes custom sources and
|
|
349
|
+
// layers, and not every restyle path goes through whenStyleUpdates.
|
|
350
|
+
this.map.on("styledata", () => {
|
|
351
|
+
if (this.circles.length > 0)
|
|
352
|
+
this.ensureCircleLayers();
|
|
353
|
+
});
|
|
308
354
|
this.map.on("zoomend", () => {
|
|
309
355
|
var _a;
|
|
310
356
|
(_a = mapConfig.onZoomEnd) === null || _a === void 0 ? void 0 : _a.call(mapConfig, this.getZoomLevel());
|
|
@@ -423,6 +469,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
423
469
|
return Object.assign(Object.assign({}, segmentation), { token: this.token, parentPlaceId: (_a = this.parentPlaceId) !== null && _a !== void 0 ? _a : "None" });
|
|
424
470
|
}
|
|
425
471
|
destroyMap() {
|
|
472
|
+
this.circles = [];
|
|
426
473
|
this.map.remove();
|
|
427
474
|
this.unsubscribeFromFailedTiles();
|
|
428
475
|
}
|
|
@@ -506,6 +553,19 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
506
553
|
}
|
|
507
554
|
setLang(lang) {
|
|
508
555
|
this.repository.setLang(lang);
|
|
556
|
+
if (_rtl_1.rtlLanguages.includes(lang)) {
|
|
557
|
+
this.setRTLSupport();
|
|
558
|
+
}
|
|
559
|
+
// setLayersForLanguage reads this.map.getStyle()?.layers, which is empty
|
|
560
|
+
// until the style finishes loading. If setLang is called right after
|
|
561
|
+
// createMap (before the "load" event), apply it once the style is ready
|
|
562
|
+
// so the language change is not silently dropped.
|
|
563
|
+
if (this.map.isStyleLoaded()) {
|
|
564
|
+
this.setLayersForLanguage(lang);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
this.map.once("load", () => this.setLayersForLanguage(lang));
|
|
568
|
+
}
|
|
509
569
|
}
|
|
510
570
|
setParentPlace(place, updateStyle, onStyleReady) {
|
|
511
571
|
this.changeParentPlaceTo(place, updateStyle, onStyleReady);
|
|
@@ -526,6 +586,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
526
586
|
this.setBaseFilters(newStyle);
|
|
527
587
|
}
|
|
528
588
|
this.routeController.addSourcesAndLayers();
|
|
589
|
+
this.refreshCircles();
|
|
529
590
|
this.filterByFloorKey(this.currentFloor);
|
|
530
591
|
}
|
|
531
592
|
addMarker(marker) {
|
|
@@ -631,6 +692,320 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
631
692
|
throw new Error("Failed to remove all markers");
|
|
632
693
|
}
|
|
633
694
|
}
|
|
695
|
+
addCircle(circle) {
|
|
696
|
+
try {
|
|
697
|
+
// Check if a circle with the same ID already exists and replace it
|
|
698
|
+
if (circle.id) {
|
|
699
|
+
this.circles = this.circles.filter((c) => c.id !== circle.id);
|
|
700
|
+
}
|
|
701
|
+
// resolveCircleConfig validates radiusMeters and coordinates
|
|
702
|
+
const record = (0, circle_1.resolveCircleConfig)(circle);
|
|
703
|
+
this.circles.push(record);
|
|
704
|
+
this.refreshCircles();
|
|
705
|
+
this.logEvent("addCircle");
|
|
706
|
+
return record.id;
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
throw new Error(`Failed to add circle: ${error instanceof Error ? error.message : String(error)}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
updateCircle(circleConfig) {
|
|
713
|
+
try {
|
|
714
|
+
const index = this.circles.findIndex((c) => c.id === circleConfig.id);
|
|
715
|
+
if (index === -1)
|
|
716
|
+
return null;
|
|
717
|
+
// resolveCircleConfig validates radiusMeters and coordinates
|
|
718
|
+
this.circles[index] = (0, circle_1.resolveCircleConfig)(circleConfig, this.circles[index].hidden);
|
|
719
|
+
this.refreshCircles();
|
|
720
|
+
this.logEvent("updateCircle");
|
|
721
|
+
return this.circles[index].id;
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
throw new Error(`Failed to update circle: ${error instanceof Error ? error.message : String(error)}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
getCircle(circleId) {
|
|
728
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
729
|
+
return circle ? (0, circle_1.cloneCircleRecord)(circle) : undefined;
|
|
730
|
+
}
|
|
731
|
+
getCircles() {
|
|
732
|
+
return this.circles.map(circle_1.cloneCircleRecord);
|
|
733
|
+
}
|
|
734
|
+
hasCircle(circleId) {
|
|
735
|
+
return this.circles.some((c) => c.id === circleId);
|
|
736
|
+
}
|
|
737
|
+
updateCirclePosition(circleId, center, radiusMeters) {
|
|
738
|
+
try {
|
|
739
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
740
|
+
if (circle === undefined)
|
|
741
|
+
return false;
|
|
742
|
+
// Validate inputs before modifying
|
|
743
|
+
if (!center) {
|
|
744
|
+
throw new Error("Circle center is required");
|
|
745
|
+
}
|
|
746
|
+
const { lat, lng } = center;
|
|
747
|
+
if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
|
|
748
|
+
throw new Error(`Invalid latitude: ${lat}. Must be between -90 and 90.`);
|
|
749
|
+
}
|
|
750
|
+
if (!Number.isFinite(lng) || lng < -180 || lng > 180) {
|
|
751
|
+
throw new Error(`Invalid longitude: ${lng}. Must be between -180 and 180.`);
|
|
752
|
+
}
|
|
753
|
+
if (radiusMeters !== undefined) {
|
|
754
|
+
if (!Number.isFinite(radiusMeters) || radiusMeters <= 0) {
|
|
755
|
+
throw new Error(`Invalid radiusMeters: ${radiusMeters}. Must be a positive finite number.`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
circle.coordinate = { lat: center.lat, lng: center.lng };
|
|
759
|
+
if (radiusMeters !== undefined) {
|
|
760
|
+
circle.radiusMeters = radiusMeters;
|
|
761
|
+
}
|
|
762
|
+
this.refreshCircles();
|
|
763
|
+
this.logEvent("updateCirclePosition");
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
throw new Error(`Failed to update circle position: ${error instanceof Error ? error.message : String(error)}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
updateCircleStyle(circleId, style) {
|
|
771
|
+
try {
|
|
772
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
773
|
+
if (circle === undefined)
|
|
774
|
+
return false;
|
|
775
|
+
// Update only style properties, clamp opacities
|
|
776
|
+
if (style.fillColor !== undefined)
|
|
777
|
+
circle.fillColor = style.fillColor;
|
|
778
|
+
if (style.fillOpacity !== undefined)
|
|
779
|
+
circle.fillOpacity = Math.max(0, Math.min(1, style.fillOpacity));
|
|
780
|
+
if (style.strokeColor !== undefined)
|
|
781
|
+
circle.strokeColor = style.strokeColor;
|
|
782
|
+
if (style.strokeWidth !== undefined)
|
|
783
|
+
circle.strokeWidth = style.strokeWidth;
|
|
784
|
+
if (style.strokeOpacity !== undefined)
|
|
785
|
+
circle.strokeOpacity = Math.max(0, Math.min(1, style.strokeOpacity));
|
|
786
|
+
this.refreshCircles();
|
|
787
|
+
this.logEvent("updateCircleStyle");
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
throw new Error(`Failed to update circle style: ${error instanceof Error ? error.message : String(error)}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
removeCircle(circleId) {
|
|
795
|
+
try {
|
|
796
|
+
this.circles = this.circles.filter((c) => c.id !== circleId);
|
|
797
|
+
this.refreshCircles();
|
|
798
|
+
this.logEvent("removeCircle");
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
throw new Error("Failed to remove circle");
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
removeAllCircles() {
|
|
805
|
+
try {
|
|
806
|
+
this.circles = [];
|
|
807
|
+
this.refreshCircles();
|
|
808
|
+
this.logEvent("removeAllCircles");
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
throw new Error("Failed to remove all circles");
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
showCircle(circleId) {
|
|
815
|
+
try {
|
|
816
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
817
|
+
if (circle === undefined) {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
circle.hidden = false;
|
|
821
|
+
this.refreshCircles();
|
|
822
|
+
this.logEvent("showCircle");
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
hideCircle(circleId) {
|
|
830
|
+
try {
|
|
831
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
832
|
+
if (circle === undefined) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
circle.hidden = true;
|
|
836
|
+
this.refreshCircles();
|
|
837
|
+
this.logEvent("hideCircle");
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Builds the GeoJSON FeatureCollection for every currently visible circle.
|
|
846
|
+
* Visibility mirrors marker semantics: a circle with a floor is shown only
|
|
847
|
+
* while that floor is displayed, and a circle without a floor is shown only
|
|
848
|
+
* in outdoor contexts. Hidden circles are always omitted.
|
|
849
|
+
*/
|
|
850
|
+
circleFeatureCollection() {
|
|
851
|
+
var _a, _b, _c;
|
|
852
|
+
const floorId = (_a = this.currentFloor) !== null && _a !== void 0 ? _a : "";
|
|
853
|
+
const isOutdoor = !this.parentPlace ||
|
|
854
|
+
((_c = (_b = this.innerFloors.find((floor) => floor.key === floorId)) === null || _b === void 0 ? void 0 : _b.reachableFromGPS) !== null && _c !== void 0 ? _c : false);
|
|
855
|
+
const features = this.circles
|
|
856
|
+
.filter((circle) => {
|
|
857
|
+
var _a;
|
|
858
|
+
if (circle.hidden)
|
|
859
|
+
return false;
|
|
860
|
+
const circleFloor = (_a = circle.floorId) !== null && _a !== void 0 ? _a : "";
|
|
861
|
+
return circleFloor === floorId || (isOutdoor && circleFloor === "");
|
|
862
|
+
})
|
|
863
|
+
.map((circle) => ({
|
|
864
|
+
type: "Feature",
|
|
865
|
+
properties: {
|
|
866
|
+
id: circle.id,
|
|
867
|
+
fillColor: circle.fillColor,
|
|
868
|
+
fillOpacity: circle.fillOpacity,
|
|
869
|
+
strokeColor: circle.strokeColor,
|
|
870
|
+
strokeWidth: circle.strokeWidth,
|
|
871
|
+
strokeOpacity: circle.strokeOpacity,
|
|
872
|
+
renderOrder: circle.renderOrder,
|
|
873
|
+
},
|
|
874
|
+
geometry: {
|
|
875
|
+
type: "Polygon",
|
|
876
|
+
coordinates: [
|
|
877
|
+
(0, circle_1.circleRing)(circle.coordinate.lng, circle.coordinate.lat, circle.radiusMeters),
|
|
878
|
+
],
|
|
879
|
+
},
|
|
880
|
+
}));
|
|
881
|
+
return { type: "FeatureCollection", features };
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Idempotently adds the shared circle source and its fill/line layers.
|
|
885
|
+
* Layers are inserted below the first symbol layer so place labels and
|
|
886
|
+
* markers stay readable above the translucent fill. Safe to call at any
|
|
887
|
+
* time: a style that is still loading simply rejects the calls, and the
|
|
888
|
+
* styledata listener retries once the style is ready.
|
|
889
|
+
*/
|
|
890
|
+
/**
|
|
891
|
+
* Resolves the layer id to insert circle layers before, for one render
|
|
892
|
+
* order. `undefined` means "append on top".
|
|
893
|
+
*
|
|
894
|
+
* `aboveBasemap` finds the topmost non-symbol layer of the style and
|
|
895
|
+
* inserts before the first symbol layer that follows it. On indoor styles
|
|
896
|
+
* the floor-plate and building polygons are ordered after the first symbol
|
|
897
|
+
* layer, so anchoring on the topmost geometry layer — instead of the first
|
|
898
|
+
* symbol layer — guarantees circles are never occluded by basemap fills
|
|
899
|
+
* while still rendering below the labels that follow them.
|
|
900
|
+
*/
|
|
901
|
+
circleBeforeIdFor(order) {
|
|
902
|
+
var _a, _b, _c, _d;
|
|
903
|
+
if (order === "top")
|
|
904
|
+
return undefined;
|
|
905
|
+
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));
|
|
906
|
+
if (order === "belowLabels") {
|
|
907
|
+
return (_c = layers.find((layer) => layer.type === "symbol")) === null || _c === void 0 ? void 0 : _c.id;
|
|
908
|
+
}
|
|
909
|
+
// aboveBasemap
|
|
910
|
+
let lastNonSymbolIndex = -1;
|
|
911
|
+
layers.forEach((layer, index) => {
|
|
912
|
+
if (layer.type !== "symbol")
|
|
913
|
+
lastNonSymbolIndex = index;
|
|
914
|
+
});
|
|
915
|
+
return (_d = layers.slice(lastNonSymbolIndex + 1).find((layer) => layer.type === "symbol")) === null || _d === void 0 ? void 0 : _d.id;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Idempotently ensures the shared circle source and one fill/line layer
|
|
919
|
+
* pair per render order in use, at the placement that order requires.
|
|
920
|
+
* Placement is recomputed and re-asserted (via moveLayer) on every call,
|
|
921
|
+
* so circles regain their correct z-order after any style reload or
|
|
922
|
+
* floor/parent-place change. Safe to call at any time: a style that is
|
|
923
|
+
* still loading simply rejects the calls, and the styledata listener
|
|
924
|
+
* retries once the style is ready.
|
|
925
|
+
*/
|
|
926
|
+
ensureCircleLayers() {
|
|
927
|
+
if (!this.map)
|
|
928
|
+
return;
|
|
929
|
+
try {
|
|
930
|
+
if (!this.map.getSource(CIRCLE_SOURCE_ID)) {
|
|
931
|
+
this.map.addSource(CIRCLE_SOURCE_ID, {
|
|
932
|
+
type: "geojson",
|
|
933
|
+
data: this.circleFeatureCollection(),
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
const ordersInUse = new Set(this.circles.map((c) => c.renderOrder));
|
|
937
|
+
// Always keep the default bucket alive so an empty map still renders
|
|
938
|
+
// newly added circles without a layer rebuild
|
|
939
|
+
ordersInUse.add("aboveBasemap");
|
|
940
|
+
// Process buckets lowest-first (CIRCLE_RENDER_ORDERS is canonical):
|
|
941
|
+
// when two buckets resolve to the same anchor, each addLayer/moveLayer
|
|
942
|
+
// lands immediately before it, so a bucket processed later paints above
|
|
943
|
+
// the ones processed earlier. Canonical order keeps
|
|
944
|
+
// belowLabels < aboveBasemap < top regardless of circle insertion order.
|
|
945
|
+
for (const order of circle_1.CIRCLE_RENDER_ORDERS) {
|
|
946
|
+
if (!ordersInUse.has(order))
|
|
947
|
+
continue;
|
|
948
|
+
const ids = circleLayerIdsFor(order);
|
|
949
|
+
const beforeId = this.circleBeforeIdFor(order);
|
|
950
|
+
const orderFilter = ["==", ["get", "renderOrder"], order];
|
|
951
|
+
if (!this.map.getLayer(ids.fill)) {
|
|
952
|
+
const fillLayer = {
|
|
953
|
+
id: ids.fill,
|
|
954
|
+
type: "fill",
|
|
955
|
+
source: CIRCLE_SOURCE_ID,
|
|
956
|
+
filter: orderFilter,
|
|
957
|
+
paint: {
|
|
958
|
+
"fill-color": ["get", "fillColor"],
|
|
959
|
+
"fill-opacity": ["get", "fillOpacity"],
|
|
960
|
+
},
|
|
961
|
+
};
|
|
962
|
+
this.map.addLayer(fillLayer, beforeId);
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
this.map.moveLayer(ids.fill, beforeId);
|
|
966
|
+
}
|
|
967
|
+
if (!this.map.getLayer(ids.line)) {
|
|
968
|
+
const lineLayer = {
|
|
969
|
+
id: ids.line,
|
|
970
|
+
type: "line",
|
|
971
|
+
source: CIRCLE_SOURCE_ID,
|
|
972
|
+
filter: orderFilter,
|
|
973
|
+
paint: {
|
|
974
|
+
"line-color": ["get", "strokeColor"],
|
|
975
|
+
"line-width": ["get", "strokeWidth"],
|
|
976
|
+
"line-opacity": ["get", "strokeOpacity"],
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
this.map.addLayer(lineLayer, beforeId);
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
this.map.moveLayer(ids.line, beforeId);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
// Style may not be loaded yet; the styledata listener re-adds the layers
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Re-renders all circles: ensures the source and layers exist, then pushes
|
|
992
|
+
* the current FeatureCollection. Called after every mutation of the circle
|
|
993
|
+
* list, on floor changes, and when the map style reloads.
|
|
994
|
+
*/
|
|
995
|
+
refreshCircles() {
|
|
996
|
+
if (!this.map)
|
|
997
|
+
return;
|
|
998
|
+
if (this.circles.length === 0 && !this.map.getSource(CIRCLE_SOURCE_ID))
|
|
999
|
+
return;
|
|
1000
|
+
this.ensureCircleLayers();
|
|
1001
|
+
try {
|
|
1002
|
+
const source = this.map.getSource(CIRCLE_SOURCE_ID);
|
|
1003
|
+
source === null || source === void 0 ? void 0 : source.setData(this.circleFeatureCollection());
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
// Source may not exist while a new style is loading
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
634
1009
|
/**
|
|
635
1010
|
* Use it to change the current layer
|
|
636
1011
|
* @param floorKey floor id
|
|
@@ -643,6 +1018,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
643
1018
|
}
|
|
644
1019
|
this.updateFiltersTo(floorKeyString);
|
|
645
1020
|
this.updateMarkersTo(floorKeyString);
|
|
1021
|
+
this.refreshCircles();
|
|
646
1022
|
this.routeController.updateRouteLayers(floorKeyString);
|
|
647
1023
|
this.routeController.updateRouteMarkerVisibility(floorKeyString);
|
|
648
1024
|
}
|
|
@@ -748,6 +1124,49 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
748
1124
|
this.map.setMinZoom(zoomLvl);
|
|
749
1125
|
(_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
|
|
750
1126
|
}
|
|
1127
|
+
setBearing(degrees, options) {
|
|
1128
|
+
var _a;
|
|
1129
|
+
if (!Number.isFinite(degrees))
|
|
1130
|
+
return;
|
|
1131
|
+
if (options === null || options === void 0 ? void 0 : options.animate) {
|
|
1132
|
+
if (options.onComplete)
|
|
1133
|
+
void this.map.once("moveend", options.onComplete);
|
|
1134
|
+
this.map.rotateTo(degrees, { duration: 600 });
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
this.map.setBearing(degrees);
|
|
1138
|
+
(_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
|
|
1139
|
+
}
|
|
1140
|
+
setRotationEnabled(enabled) {
|
|
1141
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
1142
|
+
if (enabled) {
|
|
1143
|
+
(_b = (_a = this.map.dragRotate) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1144
|
+
(_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.enableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
1145
|
+
(_f = (_e = this.map.keyboard) === null || _e === void 0 ? void 0 : _e.enable) === null || _f === void 0 ? void 0 : _f.call(_e);
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
(_h = (_g = this.map.dragRotate) === null || _g === void 0 ? void 0 : _g.disable) === null || _h === void 0 ? void 0 : _h.call(_g);
|
|
1149
|
+
(_k = (_j = this.map.touchZoomRotate) === null || _j === void 0 ? void 0 : _j.disableRotation) === null || _k === void 0 ? void 0 : _k.call(_j);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
setPanEnabled(enabled) {
|
|
1153
|
+
var _a, _b, _c, _d;
|
|
1154
|
+
if (enabled) {
|
|
1155
|
+
(_b = (_a = this.map.dragPan) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
(_d = (_c = this.map.dragPan) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
setScrollZoomEnabled(enabled) {
|
|
1162
|
+
var _a, _b, _c, _d;
|
|
1163
|
+
if (enabled) {
|
|
1164
|
+
(_b = (_a = this.map.scrollZoom) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
(_d = (_c = this.map.scrollZoom) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
751
1170
|
isInsideBounds(point) {
|
|
752
1171
|
const mapBounds = this.map.getMaxBounds();
|
|
753
1172
|
if (mapBounds != null) {
|
|
@@ -866,7 +1285,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
866
1285
|
throw new Error("Error: Failed to add route");
|
|
867
1286
|
}
|
|
868
1287
|
}
|
|
869
|
-
updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#
|
|
1288
|
+
updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#276EF1" }) {
|
|
870
1289
|
try {
|
|
871
1290
|
const behindConfig = new routeConfiguration_1.InternalDrawRouteConfiguration({
|
|
872
1291
|
routeStyle: behindStyle,
|
|
@@ -916,13 +1335,12 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
916
1335
|
if (pointedPlace !== undefined) {
|
|
917
1336
|
new maplibre_gl_1.default.Popup()
|
|
918
1337
|
.setLngLat(pointedPlace.position)
|
|
919
|
-
.
|
|
1338
|
+
.setText(pointedPlace.title)
|
|
920
1339
|
.addTo(this.map);
|
|
921
1340
|
}
|
|
922
1341
|
}
|
|
923
1342
|
return "";
|
|
924
1343
|
}
|
|
925
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
926
1344
|
removePopOver(id) {
|
|
927
1345
|
throw Error("Not implemented");
|
|
928
1346
|
}
|
|
@@ -1045,7 +1463,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
1045
1463
|
filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
|
|
1046
1464
|
paint: {
|
|
1047
1465
|
"fill-extrusion-height": 2.5,
|
|
1048
|
-
"fill-extrusion-color":
|
|
1466
|
+
"fill-extrusion-color": circle_1.MAPVX_BRAND_COLOR,
|
|
1049
1467
|
"fill-extrusion-opacity": 0.8,
|
|
1050
1468
|
},
|
|
1051
1469
|
};
|
|
@@ -1059,7 +1477,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
1059
1477
|
"source-layer": "area",
|
|
1060
1478
|
filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
|
|
1061
1479
|
paint: {
|
|
1062
|
-
"fill-color":
|
|
1480
|
+
"fill-color": circle_1.MAPVX_BRAND_COLOR,
|
|
1063
1481
|
},
|
|
1064
1482
|
};
|
|
1065
1483
|
this.map.addLayer(layer);
|
|
@@ -1317,7 +1735,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
|
|
|
1317
1735
|
"source-layer": "area",
|
|
1318
1736
|
filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
|
|
1319
1737
|
paint: {
|
|
1320
|
-
"line-color":
|
|
1738
|
+
"line-color": circle_1.MAPVX_BRAND_COLOR,
|
|
1321
1739
|
"line-width": 4,
|
|
1322
1740
|
},
|
|
1323
1741
|
};
|