@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/es/map/map.js
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import maplibregl, { LngLatBounds, Map, NavigationControl, setRTLTextPlugin, } from "maplibre-gl";
|
|
2
|
+
import { Semaphore } from "../utils/semaphore";
|
|
3
|
+
/** Shared GeoJSON source holding every circle drawn through the circle API. */
|
|
4
|
+
const CIRCLE_SOURCE_ID = "mapvx-circles";
|
|
5
|
+
/** Fill layer rendering the translucent interior of the circles. */
|
|
6
|
+
const CIRCLE_FILL_LAYER_ID = "mapvx-circles-fill";
|
|
7
|
+
/** Line layer rendering the circle outlines. */
|
|
8
|
+
const CIRCLE_LINE_LAYER_ID = "mapvx-circles-line";
|
|
9
|
+
/**
|
|
10
|
+
* Layer-id suffix per circle render order. The default placement keeps the
|
|
11
|
+
* unsuffixed ids so existing consumers referencing them keep working.
|
|
12
|
+
*/
|
|
13
|
+
const CIRCLE_LAYER_SUFFIXES = {
|
|
14
|
+
aboveBasemap: "",
|
|
15
|
+
belowLabels: "-below-labels",
|
|
16
|
+
top: "-top",
|
|
17
|
+
};
|
|
18
|
+
/** Fill/line layer ids for one circle render-order bucket. */
|
|
19
|
+
function circleLayerIdsFor(order) {
|
|
20
|
+
const suffix = CIRCLE_LAYER_SUFFIXES[order];
|
|
21
|
+
return { fill: CIRCLE_FILL_LAYER_ID + suffix, line: CIRCLE_LINE_LAYER_ID + suffix };
|
|
22
|
+
}
|
|
23
|
+
/** True for the layer ids owned by the circle API, regardless of bucket. */
|
|
24
|
+
function isCircleLayerId(id) {
|
|
25
|
+
return id.startsWith(CIRCLE_FILL_LAYER_ID) || id.startsWith(CIRCLE_LINE_LAYER_ID);
|
|
26
|
+
}
|
|
2
27
|
// Flag to track if cached-tile protocol has been registered
|
|
3
28
|
let cachedTileProtocolRegistered = false;
|
|
4
29
|
/**
|
|
@@ -19,21 +44,36 @@ function deepClone(obj) {
|
|
|
19
44
|
/**
|
|
20
45
|
* Register a custom protocol for cached tiles that routes requests through the main thread.
|
|
21
46
|
* This allows the service worker to intercept and cache tile requests.
|
|
47
|
+
*
|
|
48
|
+
* Wraps fetches in a {@link Semaphore} so tile CDN / WAFs are not hit with
|
|
49
|
+
* unbounded parallel requests (same host as the service worker cache). The
|
|
50
|
+
* semaphore is created once with the limit from the first map's config; later
|
|
51
|
+
* calls are no-ops because the protocol can only be registered once globally
|
|
52
|
+
* on MapLibre.
|
|
53
|
+
*
|
|
54
|
+
* @param maxConcurrentFetches - Maximum number of in-flight tile fetches.
|
|
22
55
|
*/
|
|
23
|
-
function registerCachedTileProtocol() {
|
|
56
|
+
function registerCachedTileProtocol(maxConcurrentFetches) {
|
|
24
57
|
if (cachedTileProtocolRegistered)
|
|
25
58
|
return;
|
|
59
|
+
const semaphore = new Semaphore(maxConcurrentFetches);
|
|
26
60
|
maplibregl.addProtocol("cached-tile", (params, abortController) => {
|
|
27
|
-
// Convert cached-tile:// URL back to https://
|
|
28
61
|
const url = params.url.replace("cached-tile://", "https://");
|
|
29
|
-
|
|
62
|
+
// Pass the abort signal to acquire() so a request cancelled while still
|
|
63
|
+
// queued (e.g. during rapid zoom) drops out of the FIFO queue instead of
|
|
64
|
+
// waiting for a slot just to bail. release() runs only inside this chain,
|
|
65
|
+
// i.e. only after a slot was actually granted.
|
|
66
|
+
return semaphore.acquire(abortController.signal).then(() => fetch(url, { signal: abortController.signal })
|
|
30
67
|
.then((response) => {
|
|
31
68
|
if (!response.ok) {
|
|
32
69
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
33
70
|
}
|
|
34
71
|
return response.arrayBuffer();
|
|
35
72
|
})
|
|
36
|
-
.then((data) => ({ data }))
|
|
73
|
+
.then((data) => ({ data }))
|
|
74
|
+
.finally(() => {
|
|
75
|
+
semaphore.release();
|
|
76
|
+
}));
|
|
37
77
|
});
|
|
38
78
|
cachedTileProtocolRegistered = true;
|
|
39
79
|
}
|
|
@@ -92,13 +132,15 @@ import { RouteController } from "../controllers/routeController";
|
|
|
92
132
|
import { rtlLanguages } from "../domain/models/_rtl";
|
|
93
133
|
import { InternalAnimationConfig, InternalAnimationDrawingConfig, } from "../domain/models/animation";
|
|
94
134
|
import { Loggeable } from "../domain/models/loggeable";
|
|
95
|
-
import { DEFAULT_TILE_CACHE_CONFIG, } from "../domain/models/mapConfig";
|
|
135
|
+
import { DEFAULT_TILE_CACHE_CONFIG, MAPLIBRE_MAX_TILE_CACHE_HARD_CAP, } from "../domain/models/mapConfig";
|
|
136
|
+
import { CIRCLE_RENDER_ORDERS, circleRing, cloneCircleRecord, MAPVX_BRAND_COLOR, resolveCircleConfig, } from "../domain/models/circle";
|
|
96
137
|
import { MarkerAttribute } from "../domain/models/marker";
|
|
97
138
|
import { MVXRoute } from "../domain/models/route";
|
|
98
139
|
import { InternalDrawRouteConfiguration, InternalGetRouteConfiguration, } from "../domain/models/routeConfiguration";
|
|
99
140
|
import { Repository } from "../repository/repository";
|
|
100
141
|
import { getBoundingBox } from "../utils/utils";
|
|
101
142
|
import { extractStepCoordinates } from "../utils/route-utils";
|
|
143
|
+
import { buildInteractionOptions, shouldDisableTouchRotation } from "./mapInteractionOptions";
|
|
102
144
|
/**
|
|
103
145
|
* Class to interact with the map.
|
|
104
146
|
* @category Map
|
|
@@ -122,6 +164,7 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
122
164
|
this.currentFloor = "";
|
|
123
165
|
this.baseFilters = {};
|
|
124
166
|
this.markers = [];
|
|
167
|
+
this.circles = [];
|
|
125
168
|
this.enableHover = false;
|
|
126
169
|
this.hoveredId = "unselected";
|
|
127
170
|
this.failedTiles = new Set();
|
|
@@ -137,8 +180,11 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
137
180
|
this.watchPositionID = undefined;
|
|
138
181
|
this.onFloorChange = mapConfig.onFloorChange;
|
|
139
182
|
this.onParentPlaceChange = mapConfig.onParentPlaceChange;
|
|
140
|
-
|
|
141
|
-
|
|
183
|
+
this.tileCacheConfig = (() => {
|
|
184
|
+
const merged = Object.assign(Object.assign({}, DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
|
|
185
|
+
merged.maxTiles = Math.min(merged.maxTiles, MAPLIBRE_MAX_TILE_CACHE_HARD_CAP);
|
|
186
|
+
return merged;
|
|
187
|
+
})();
|
|
142
188
|
if (mapConfig.parentPlaceId != null) {
|
|
143
189
|
this.initialPlaceDetailSetUp(mapConfig.parentPlaceId, mapConfig.authToken);
|
|
144
190
|
}
|
|
@@ -235,28 +281,17 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
235
281
|
.catch(console.error);
|
|
236
282
|
}
|
|
237
283
|
onMapStyleLoaded(mapConfig, container, style) {
|
|
238
|
-
var _a, _b, _c;
|
|
284
|
+
var _a, _b, _c, _d, _e;
|
|
239
285
|
// Determine if service worker caching should be enabled
|
|
240
286
|
const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
|
|
241
|
-
// Register cached-tile protocol only if service worker caching is enabled
|
|
242
287
|
if (useServiceWorkerCaching) {
|
|
243
|
-
registerCachedTileProtocol();
|
|
288
|
+
registerCachedTileProtocol(this.tileCacheConfig.maxConcurrentTileFetches);
|
|
244
289
|
}
|
|
245
290
|
// Transform tile URLs only if service worker caching is enabled
|
|
246
291
|
const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
|
|
247
|
-
const mapOptions = {
|
|
248
|
-
container,
|
|
249
|
-
style: finalStyle,
|
|
250
|
-
center: mapConfig.center,
|
|
251
|
-
zoom: mapConfig.zoom,
|
|
252
|
-
pitch: (_a = mapConfig.pitch) !== null && _a !== void 0 ? _a : 0,
|
|
253
|
-
attributionControl: false,
|
|
254
|
-
maplibreLogo: false,
|
|
255
|
-
bearingSnap: (_b = mapConfig.bearingSnap) !== null && _b !== void 0 ? _b : 0,
|
|
256
|
-
cancelPendingTileRequestsWhileZooming: false,
|
|
292
|
+
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,
|
|
257
293
|
// Use configured maxTiles for MapLibre's memory cache
|
|
258
|
-
maxTileCacheSize: this.tileCacheConfig.maxTiles,
|
|
259
|
-
};
|
|
294
|
+
maxTileCacheSize: this.tileCacheConfig.maxTiles }, buildInteractionOptions(mapConfig));
|
|
260
295
|
if (mapConfig.maxZoom)
|
|
261
296
|
mapOptions.maxZoom = mapConfig.maxZoom;
|
|
262
297
|
if (mapConfig.minZoom)
|
|
@@ -266,10 +301,15 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
266
301
|
mapOptions.maxBounds = new LngLatBounds([boundingBox[0].lng, boundingBox[0].lat], [boundingBox[1].lng, boundingBox[1].lat]);
|
|
267
302
|
}
|
|
268
303
|
this.map = new Map(mapOptions);
|
|
304
|
+
// When rotation is disabled we still want pinch-to-zoom to work, so the
|
|
305
|
+
// two-finger rotation is turned off here instead of via the constructor.
|
|
306
|
+
if (shouldDisableTouchRotation(mapConfig)) {
|
|
307
|
+
(_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.disableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
308
|
+
}
|
|
269
309
|
this.map.addControl(new NavigationControl({
|
|
270
310
|
showCompass: mapConfig.showCompass !== undefined ? mapConfig.showCompass : true,
|
|
271
311
|
showZoom: mapConfig.showZoom !== undefined ? mapConfig.showZoom : true,
|
|
272
|
-
}), (
|
|
312
|
+
}), (_e = mapConfig.navigationPosition) !== null && _e !== void 0 ? _e : "top-right");
|
|
273
313
|
this.map.on("load", () => {
|
|
274
314
|
var _a;
|
|
275
315
|
this.whenStyleUpdates(style);
|
|
@@ -279,6 +319,12 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
279
319
|
this.onHover();
|
|
280
320
|
this.subscribeToFailedTiles();
|
|
281
321
|
});
|
|
322
|
+
// Self-healing for circles: a full style reload wipes custom sources and
|
|
323
|
+
// layers, and not every restyle path goes through whenStyleUpdates.
|
|
324
|
+
this.map.on("styledata", () => {
|
|
325
|
+
if (this.circles.length > 0)
|
|
326
|
+
this.ensureCircleLayers();
|
|
327
|
+
});
|
|
282
328
|
this.map.on("zoomend", () => {
|
|
283
329
|
var _a;
|
|
284
330
|
(_a = mapConfig.onZoomEnd) === null || _a === void 0 ? void 0 : _a.call(mapConfig, this.getZoomLevel());
|
|
@@ -397,6 +443,7 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
397
443
|
return Object.assign(Object.assign({}, segmentation), { token: this.token, parentPlaceId: (_a = this.parentPlaceId) !== null && _a !== void 0 ? _a : "None" });
|
|
398
444
|
}
|
|
399
445
|
destroyMap() {
|
|
446
|
+
this.circles = [];
|
|
400
447
|
this.map.remove();
|
|
401
448
|
this.unsubscribeFromFailedTiles();
|
|
402
449
|
}
|
|
@@ -480,6 +527,19 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
480
527
|
}
|
|
481
528
|
setLang(lang) {
|
|
482
529
|
this.repository.setLang(lang);
|
|
530
|
+
if (rtlLanguages.includes(lang)) {
|
|
531
|
+
this.setRTLSupport();
|
|
532
|
+
}
|
|
533
|
+
// setLayersForLanguage reads this.map.getStyle()?.layers, which is empty
|
|
534
|
+
// until the style finishes loading. If setLang is called right after
|
|
535
|
+
// createMap (before the "load" event), apply it once the style is ready
|
|
536
|
+
// so the language change is not silently dropped.
|
|
537
|
+
if (this.map.isStyleLoaded()) {
|
|
538
|
+
this.setLayersForLanguage(lang);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
this.map.once("load", () => this.setLayersForLanguage(lang));
|
|
542
|
+
}
|
|
483
543
|
}
|
|
484
544
|
setParentPlace(place, updateStyle, onStyleReady) {
|
|
485
545
|
this.changeParentPlaceTo(place, updateStyle, onStyleReady);
|
|
@@ -500,6 +560,7 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
500
560
|
this.setBaseFilters(newStyle);
|
|
501
561
|
}
|
|
502
562
|
this.routeController.addSourcesAndLayers();
|
|
563
|
+
this.refreshCircles();
|
|
503
564
|
this.filterByFloorKey(this.currentFloor);
|
|
504
565
|
}
|
|
505
566
|
addMarker(marker) {
|
|
@@ -605,6 +666,320 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
605
666
|
throw new Error("Failed to remove all markers");
|
|
606
667
|
}
|
|
607
668
|
}
|
|
669
|
+
addCircle(circle) {
|
|
670
|
+
try {
|
|
671
|
+
// Check if a circle with the same ID already exists and replace it
|
|
672
|
+
if (circle.id) {
|
|
673
|
+
this.circles = this.circles.filter((c) => c.id !== circle.id);
|
|
674
|
+
}
|
|
675
|
+
// resolveCircleConfig validates radiusMeters and coordinates
|
|
676
|
+
const record = resolveCircleConfig(circle);
|
|
677
|
+
this.circles.push(record);
|
|
678
|
+
this.refreshCircles();
|
|
679
|
+
this.logEvent("addCircle");
|
|
680
|
+
return record.id;
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
throw new Error(`Failed to add circle: ${error instanceof Error ? error.message : String(error)}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
updateCircle(circleConfig) {
|
|
687
|
+
try {
|
|
688
|
+
const index = this.circles.findIndex((c) => c.id === circleConfig.id);
|
|
689
|
+
if (index === -1)
|
|
690
|
+
return null;
|
|
691
|
+
// resolveCircleConfig validates radiusMeters and coordinates
|
|
692
|
+
this.circles[index] = resolveCircleConfig(circleConfig, this.circles[index].hidden);
|
|
693
|
+
this.refreshCircles();
|
|
694
|
+
this.logEvent("updateCircle");
|
|
695
|
+
return this.circles[index].id;
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
throw new Error(`Failed to update circle: ${error instanceof Error ? error.message : String(error)}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
getCircle(circleId) {
|
|
702
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
703
|
+
return circle ? cloneCircleRecord(circle) : undefined;
|
|
704
|
+
}
|
|
705
|
+
getCircles() {
|
|
706
|
+
return this.circles.map(cloneCircleRecord);
|
|
707
|
+
}
|
|
708
|
+
hasCircle(circleId) {
|
|
709
|
+
return this.circles.some((c) => c.id === circleId);
|
|
710
|
+
}
|
|
711
|
+
updateCirclePosition(circleId, center, radiusMeters) {
|
|
712
|
+
try {
|
|
713
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
714
|
+
if (circle === undefined)
|
|
715
|
+
return false;
|
|
716
|
+
// Validate inputs before modifying
|
|
717
|
+
if (!center) {
|
|
718
|
+
throw new Error("Circle center is required");
|
|
719
|
+
}
|
|
720
|
+
const { lat, lng } = center;
|
|
721
|
+
if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
|
|
722
|
+
throw new Error(`Invalid latitude: ${lat}. Must be between -90 and 90.`);
|
|
723
|
+
}
|
|
724
|
+
if (!Number.isFinite(lng) || lng < -180 || lng > 180) {
|
|
725
|
+
throw new Error(`Invalid longitude: ${lng}. Must be between -180 and 180.`);
|
|
726
|
+
}
|
|
727
|
+
if (radiusMeters !== undefined) {
|
|
728
|
+
if (!Number.isFinite(radiusMeters) || radiusMeters <= 0) {
|
|
729
|
+
throw new Error(`Invalid radiusMeters: ${radiusMeters}. Must be a positive finite number.`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
circle.coordinate = { lat: center.lat, lng: center.lng };
|
|
733
|
+
if (radiusMeters !== undefined) {
|
|
734
|
+
circle.radiusMeters = radiusMeters;
|
|
735
|
+
}
|
|
736
|
+
this.refreshCircles();
|
|
737
|
+
this.logEvent("updateCirclePosition");
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
throw new Error(`Failed to update circle position: ${error instanceof Error ? error.message : String(error)}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
updateCircleStyle(circleId, style) {
|
|
745
|
+
try {
|
|
746
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
747
|
+
if (circle === undefined)
|
|
748
|
+
return false;
|
|
749
|
+
// Update only style properties, clamp opacities
|
|
750
|
+
if (style.fillColor !== undefined)
|
|
751
|
+
circle.fillColor = style.fillColor;
|
|
752
|
+
if (style.fillOpacity !== undefined)
|
|
753
|
+
circle.fillOpacity = Math.max(0, Math.min(1, style.fillOpacity));
|
|
754
|
+
if (style.strokeColor !== undefined)
|
|
755
|
+
circle.strokeColor = style.strokeColor;
|
|
756
|
+
if (style.strokeWidth !== undefined)
|
|
757
|
+
circle.strokeWidth = style.strokeWidth;
|
|
758
|
+
if (style.strokeOpacity !== undefined)
|
|
759
|
+
circle.strokeOpacity = Math.max(0, Math.min(1, style.strokeOpacity));
|
|
760
|
+
this.refreshCircles();
|
|
761
|
+
this.logEvent("updateCircleStyle");
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
throw new Error(`Failed to update circle style: ${error instanceof Error ? error.message : String(error)}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
removeCircle(circleId) {
|
|
769
|
+
try {
|
|
770
|
+
this.circles = this.circles.filter((c) => c.id !== circleId);
|
|
771
|
+
this.refreshCircles();
|
|
772
|
+
this.logEvent("removeCircle");
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
throw new Error("Failed to remove circle");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
removeAllCircles() {
|
|
779
|
+
try {
|
|
780
|
+
this.circles = [];
|
|
781
|
+
this.refreshCircles();
|
|
782
|
+
this.logEvent("removeAllCircles");
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
throw new Error("Failed to remove all circles");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
showCircle(circleId) {
|
|
789
|
+
try {
|
|
790
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
791
|
+
if (circle === undefined) {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
circle.hidden = false;
|
|
795
|
+
this.refreshCircles();
|
|
796
|
+
this.logEvent("showCircle");
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
hideCircle(circleId) {
|
|
804
|
+
try {
|
|
805
|
+
const circle = this.circles.find((c) => c.id === circleId);
|
|
806
|
+
if (circle === undefined) {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
circle.hidden = true;
|
|
810
|
+
this.refreshCircles();
|
|
811
|
+
this.logEvent("hideCircle");
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Builds the GeoJSON FeatureCollection for every currently visible circle.
|
|
820
|
+
* Visibility mirrors marker semantics: a circle with a floor is shown only
|
|
821
|
+
* while that floor is displayed, and a circle without a floor is shown only
|
|
822
|
+
* in outdoor contexts. Hidden circles are always omitted.
|
|
823
|
+
*/
|
|
824
|
+
circleFeatureCollection() {
|
|
825
|
+
var _a, _b, _c;
|
|
826
|
+
const floorId = (_a = this.currentFloor) !== null && _a !== void 0 ? _a : "";
|
|
827
|
+
const isOutdoor = !this.parentPlace ||
|
|
828
|
+
((_c = (_b = this.innerFloors.find((floor) => floor.key === floorId)) === null || _b === void 0 ? void 0 : _b.reachableFromGPS) !== null && _c !== void 0 ? _c : false);
|
|
829
|
+
const features = this.circles
|
|
830
|
+
.filter((circle) => {
|
|
831
|
+
var _a;
|
|
832
|
+
if (circle.hidden)
|
|
833
|
+
return false;
|
|
834
|
+
const circleFloor = (_a = circle.floorId) !== null && _a !== void 0 ? _a : "";
|
|
835
|
+
return circleFloor === floorId || (isOutdoor && circleFloor === "");
|
|
836
|
+
})
|
|
837
|
+
.map((circle) => ({
|
|
838
|
+
type: "Feature",
|
|
839
|
+
properties: {
|
|
840
|
+
id: circle.id,
|
|
841
|
+
fillColor: circle.fillColor,
|
|
842
|
+
fillOpacity: circle.fillOpacity,
|
|
843
|
+
strokeColor: circle.strokeColor,
|
|
844
|
+
strokeWidth: circle.strokeWidth,
|
|
845
|
+
strokeOpacity: circle.strokeOpacity,
|
|
846
|
+
renderOrder: circle.renderOrder,
|
|
847
|
+
},
|
|
848
|
+
geometry: {
|
|
849
|
+
type: "Polygon",
|
|
850
|
+
coordinates: [
|
|
851
|
+
circleRing(circle.coordinate.lng, circle.coordinate.lat, circle.radiusMeters),
|
|
852
|
+
],
|
|
853
|
+
},
|
|
854
|
+
}));
|
|
855
|
+
return { type: "FeatureCollection", features };
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Idempotently adds the shared circle source and its fill/line layers.
|
|
859
|
+
* Layers are inserted below the first symbol layer so place labels and
|
|
860
|
+
* markers stay readable above the translucent fill. Safe to call at any
|
|
861
|
+
* time: a style that is still loading simply rejects the calls, and the
|
|
862
|
+
* styledata listener retries once the style is ready.
|
|
863
|
+
*/
|
|
864
|
+
/**
|
|
865
|
+
* Resolves the layer id to insert circle layers before, for one render
|
|
866
|
+
* order. `undefined` means "append on top".
|
|
867
|
+
*
|
|
868
|
+
* `aboveBasemap` finds the topmost non-symbol layer of the style and
|
|
869
|
+
* inserts before the first symbol layer that follows it. On indoor styles
|
|
870
|
+
* the floor-plate and building polygons are ordered after the first symbol
|
|
871
|
+
* layer, so anchoring on the topmost geometry layer — instead of the first
|
|
872
|
+
* symbol layer — guarantees circles are never occluded by basemap fills
|
|
873
|
+
* while still rendering below the labels that follow them.
|
|
874
|
+
*/
|
|
875
|
+
circleBeforeIdFor(order) {
|
|
876
|
+
var _a, _b, _c, _d;
|
|
877
|
+
if (order === "top")
|
|
878
|
+
return undefined;
|
|
879
|
+
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));
|
|
880
|
+
if (order === "belowLabels") {
|
|
881
|
+
return (_c = layers.find((layer) => layer.type === "symbol")) === null || _c === void 0 ? void 0 : _c.id;
|
|
882
|
+
}
|
|
883
|
+
// aboveBasemap
|
|
884
|
+
let lastNonSymbolIndex = -1;
|
|
885
|
+
layers.forEach((layer, index) => {
|
|
886
|
+
if (layer.type !== "symbol")
|
|
887
|
+
lastNonSymbolIndex = index;
|
|
888
|
+
});
|
|
889
|
+
return (_d = layers.slice(lastNonSymbolIndex + 1).find((layer) => layer.type === "symbol")) === null || _d === void 0 ? void 0 : _d.id;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Idempotently ensures the shared circle source and one fill/line layer
|
|
893
|
+
* pair per render order in use, at the placement that order requires.
|
|
894
|
+
* Placement is recomputed and re-asserted (via moveLayer) on every call,
|
|
895
|
+
* so circles regain their correct z-order after any style reload or
|
|
896
|
+
* floor/parent-place change. Safe to call at any time: a style that is
|
|
897
|
+
* still loading simply rejects the calls, and the styledata listener
|
|
898
|
+
* retries once the style is ready.
|
|
899
|
+
*/
|
|
900
|
+
ensureCircleLayers() {
|
|
901
|
+
if (!this.map)
|
|
902
|
+
return;
|
|
903
|
+
try {
|
|
904
|
+
if (!this.map.getSource(CIRCLE_SOURCE_ID)) {
|
|
905
|
+
this.map.addSource(CIRCLE_SOURCE_ID, {
|
|
906
|
+
type: "geojson",
|
|
907
|
+
data: this.circleFeatureCollection(),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
const ordersInUse = new Set(this.circles.map((c) => c.renderOrder));
|
|
911
|
+
// Always keep the default bucket alive so an empty map still renders
|
|
912
|
+
// newly added circles without a layer rebuild
|
|
913
|
+
ordersInUse.add("aboveBasemap");
|
|
914
|
+
// Process buckets lowest-first (CIRCLE_RENDER_ORDERS is canonical):
|
|
915
|
+
// when two buckets resolve to the same anchor, each addLayer/moveLayer
|
|
916
|
+
// lands immediately before it, so a bucket processed later paints above
|
|
917
|
+
// the ones processed earlier. Canonical order keeps
|
|
918
|
+
// belowLabels < aboveBasemap < top regardless of circle insertion order.
|
|
919
|
+
for (const order of CIRCLE_RENDER_ORDERS) {
|
|
920
|
+
if (!ordersInUse.has(order))
|
|
921
|
+
continue;
|
|
922
|
+
const ids = circleLayerIdsFor(order);
|
|
923
|
+
const beforeId = this.circleBeforeIdFor(order);
|
|
924
|
+
const orderFilter = ["==", ["get", "renderOrder"], order];
|
|
925
|
+
if (!this.map.getLayer(ids.fill)) {
|
|
926
|
+
const fillLayer = {
|
|
927
|
+
id: ids.fill,
|
|
928
|
+
type: "fill",
|
|
929
|
+
source: CIRCLE_SOURCE_ID,
|
|
930
|
+
filter: orderFilter,
|
|
931
|
+
paint: {
|
|
932
|
+
"fill-color": ["get", "fillColor"],
|
|
933
|
+
"fill-opacity": ["get", "fillOpacity"],
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
this.map.addLayer(fillLayer, beforeId);
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
this.map.moveLayer(ids.fill, beforeId);
|
|
940
|
+
}
|
|
941
|
+
if (!this.map.getLayer(ids.line)) {
|
|
942
|
+
const lineLayer = {
|
|
943
|
+
id: ids.line,
|
|
944
|
+
type: "line",
|
|
945
|
+
source: CIRCLE_SOURCE_ID,
|
|
946
|
+
filter: orderFilter,
|
|
947
|
+
paint: {
|
|
948
|
+
"line-color": ["get", "strokeColor"],
|
|
949
|
+
"line-width": ["get", "strokeWidth"],
|
|
950
|
+
"line-opacity": ["get", "strokeOpacity"],
|
|
951
|
+
},
|
|
952
|
+
};
|
|
953
|
+
this.map.addLayer(lineLayer, beforeId);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
this.map.moveLayer(ids.line, beforeId);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
catch (error) {
|
|
961
|
+
// Style may not be loaded yet; the styledata listener re-adds the layers
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Re-renders all circles: ensures the source and layers exist, then pushes
|
|
966
|
+
* the current FeatureCollection. Called after every mutation of the circle
|
|
967
|
+
* list, on floor changes, and when the map style reloads.
|
|
968
|
+
*/
|
|
969
|
+
refreshCircles() {
|
|
970
|
+
if (!this.map)
|
|
971
|
+
return;
|
|
972
|
+
if (this.circles.length === 0 && !this.map.getSource(CIRCLE_SOURCE_ID))
|
|
973
|
+
return;
|
|
974
|
+
this.ensureCircleLayers();
|
|
975
|
+
try {
|
|
976
|
+
const source = this.map.getSource(CIRCLE_SOURCE_ID);
|
|
977
|
+
source === null || source === void 0 ? void 0 : source.setData(this.circleFeatureCollection());
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
// Source may not exist while a new style is loading
|
|
981
|
+
}
|
|
982
|
+
}
|
|
608
983
|
/**
|
|
609
984
|
* Use it to change the current layer
|
|
610
985
|
* @param floorKey floor id
|
|
@@ -617,6 +992,7 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
617
992
|
}
|
|
618
993
|
this.updateFiltersTo(floorKeyString);
|
|
619
994
|
this.updateMarkersTo(floorKeyString);
|
|
995
|
+
this.refreshCircles();
|
|
620
996
|
this.routeController.updateRouteLayers(floorKeyString);
|
|
621
997
|
this.routeController.updateRouteMarkerVisibility(floorKeyString);
|
|
622
998
|
}
|
|
@@ -722,6 +1098,49 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
722
1098
|
this.map.setMinZoom(zoomLvl);
|
|
723
1099
|
(_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
|
|
724
1100
|
}
|
|
1101
|
+
setBearing(degrees, options) {
|
|
1102
|
+
var _a;
|
|
1103
|
+
if (!Number.isFinite(degrees))
|
|
1104
|
+
return;
|
|
1105
|
+
if (options === null || options === void 0 ? void 0 : options.animate) {
|
|
1106
|
+
if (options.onComplete)
|
|
1107
|
+
void this.map.once("moveend", options.onComplete);
|
|
1108
|
+
this.map.rotateTo(degrees, { duration: 600 });
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
this.map.setBearing(degrees);
|
|
1112
|
+
(_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
|
|
1113
|
+
}
|
|
1114
|
+
setRotationEnabled(enabled) {
|
|
1115
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
1116
|
+
if (enabled) {
|
|
1117
|
+
(_b = (_a = this.map.dragRotate) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1118
|
+
(_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.enableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
1119
|
+
(_f = (_e = this.map.keyboard) === null || _e === void 0 ? void 0 : _e.enable) === null || _f === void 0 ? void 0 : _f.call(_e);
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
(_h = (_g = this.map.dragRotate) === null || _g === void 0 ? void 0 : _g.disable) === null || _h === void 0 ? void 0 : _h.call(_g);
|
|
1123
|
+
(_k = (_j = this.map.touchZoomRotate) === null || _j === void 0 ? void 0 : _j.disableRotation) === null || _k === void 0 ? void 0 : _k.call(_j);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
setPanEnabled(enabled) {
|
|
1127
|
+
var _a, _b, _c, _d;
|
|
1128
|
+
if (enabled) {
|
|
1129
|
+
(_b = (_a = this.map.dragPan) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
(_d = (_c = this.map.dragPan) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
setScrollZoomEnabled(enabled) {
|
|
1136
|
+
var _a, _b, _c, _d;
|
|
1137
|
+
if (enabled) {
|
|
1138
|
+
(_b = (_a = this.map.scrollZoom) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
(_d = (_c = this.map.scrollZoom) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
725
1144
|
isInsideBounds(point) {
|
|
726
1145
|
const mapBounds = this.map.getMaxBounds();
|
|
727
1146
|
if (mapBounds != null) {
|
|
@@ -840,7 +1259,7 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
840
1259
|
throw new Error("Error: Failed to add route");
|
|
841
1260
|
}
|
|
842
1261
|
}
|
|
843
|
-
updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#
|
|
1262
|
+
updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#276EF1" }) {
|
|
844
1263
|
try {
|
|
845
1264
|
const behindConfig = new InternalDrawRouteConfiguration({
|
|
846
1265
|
routeStyle: behindStyle,
|
|
@@ -890,13 +1309,12 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
890
1309
|
if (pointedPlace !== undefined) {
|
|
891
1310
|
new maplibregl.Popup()
|
|
892
1311
|
.setLngLat(pointedPlace.position)
|
|
893
|
-
.
|
|
1312
|
+
.setText(pointedPlace.title)
|
|
894
1313
|
.addTo(this.map);
|
|
895
1314
|
}
|
|
896
1315
|
}
|
|
897
1316
|
return "";
|
|
898
1317
|
}
|
|
899
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
900
1318
|
removePopOver(id) {
|
|
901
1319
|
throw Error("Not implemented");
|
|
902
1320
|
}
|
|
@@ -1019,7 +1437,7 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
1019
1437
|
filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
|
|
1020
1438
|
paint: {
|
|
1021
1439
|
"fill-extrusion-height": 2.5,
|
|
1022
|
-
"fill-extrusion-color":
|
|
1440
|
+
"fill-extrusion-color": MAPVX_BRAND_COLOR,
|
|
1023
1441
|
"fill-extrusion-opacity": 0.8,
|
|
1024
1442
|
},
|
|
1025
1443
|
};
|
|
@@ -1033,7 +1451,7 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
1033
1451
|
"source-layer": "area",
|
|
1034
1452
|
filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
|
|
1035
1453
|
paint: {
|
|
1036
|
-
"fill-color":
|
|
1454
|
+
"fill-color": MAPVX_BRAND_COLOR,
|
|
1037
1455
|
},
|
|
1038
1456
|
};
|
|
1039
1457
|
this.map.addLayer(layer);
|
|
@@ -1291,7 +1709,7 @@ export class InternalMapVXMap extends Loggeable {
|
|
|
1291
1709
|
"source-layer": "area",
|
|
1292
1710
|
filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
|
|
1293
1711
|
paint: {
|
|
1294
|
-
"line-color":
|
|
1712
|
+
"line-color": MAPVX_BRAND_COLOR,
|
|
1295
1713
|
"line-width": 4,
|
|
1296
1714
|
},
|
|
1297
1715
|
};
|