@trackunit/react-map-adapter-mapbox 0.0.149 → 0.0.152
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/index.cjs.js +99 -24
- package/index.esm.js +99 -24
- package/package.json +6 -4
- package/src/MapboxAdapterInstance.d.ts +28 -0
- package/src/constants.d.ts +7 -0
- package/src/draw/createMapboxDrawEngineLoader.d.ts +10 -0
package/index.cjs.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var reactMapAdapterShared = require('@trackunit/react-map-adapter-shared');
|
|
4
4
|
var geoJsonUtils = require('@trackunit/geo-json-utils');
|
|
5
|
+
var reactMapDraw = require('@trackunit/react-map-draw');
|
|
5
6
|
var esToolkit = require('es-toolkit');
|
|
6
7
|
var mapboxgl = require('mapbox-gl');
|
|
7
8
|
var Supercluster = require('supercluster');
|
|
@@ -27,6 +28,27 @@ const MAPBOX_MAP_TYPE_STYLES = {
|
|
|
27
28
|
dark: "mapbox://styles/mapbox/satellite-streets-v12",
|
|
28
29
|
},
|
|
29
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Style every Mapbox map is created with. The renderer passes it to `mapboxgl.Map`
|
|
33
|
+
* and the adapter instance records it as the applied style, so the two agree on
|
|
34
|
+
* what is on the map before the first `applyStyle`.
|
|
35
|
+
*/
|
|
36
|
+
const getInitialStyleUrl = (theme) => MAPBOX_MAP_TYPE_STYLES.roadmap[theme];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds the Mapbox GL bridge for the shared drawing engine (ADR 0030).
|
|
40
|
+
*
|
|
41
|
+
* One loader per loaded style: `setStyle` (theme, map type, Roads) removes every
|
|
42
|
+
* custom source and layer, so `MapboxAdapterInstance` disconnects the controller
|
|
43
|
+
* before a style change and reconnects it with a fresh loader on `style.load`.
|
|
44
|
+
* Terra Draw and its Mapbox adapter are only downloaded when the loader runs.
|
|
45
|
+
*/
|
|
46
|
+
const createMapboxDrawEngineLoader = (map) => async () => {
|
|
47
|
+
const { TerraDrawMapboxGLAdapter } = await import('terra-draw-mapbox-gl-adapter');
|
|
48
|
+
return reactMapDraw.loadTerraDrawEngine({
|
|
49
|
+
adapter: new TerraDrawMapboxGLAdapter({ map, coordinatePrecision: reactMapDraw.DRAW_COORDINATE_PRECISION }),
|
|
50
|
+
});
|
|
51
|
+
};
|
|
30
52
|
|
|
31
53
|
// ============================================================================
|
|
32
54
|
// Helpers
|
|
@@ -2440,15 +2462,29 @@ class MapboxAdapterInstance {
|
|
|
2440
2462
|
initializationFailed: reactMapAdapterShared.INITIAL_MAP_STATE.initializationFailed,
|
|
2441
2463
|
appearance: reactMapAdapterShared.INITIAL_MAP_STATE.appearance,
|
|
2442
2464
|
tileSize: MAPBOX_TILE_SIZE,
|
|
2465
|
+
drawing: reactMapAdapterShared.INITIAL_MAP_STATE.drawing,
|
|
2443
2466
|
};
|
|
2444
2467
|
this.cameraListeners = new Set();
|
|
2445
2468
|
this.statusListeners = new Set();
|
|
2446
2469
|
this.eventListeners = new Map();
|
|
2447
2470
|
this.mapboxEventCleanups = [];
|
|
2448
2471
|
this.isDestroyedFlag = false;
|
|
2472
|
+
/**
|
|
2473
|
+
* The map whose style has loaded at least once. `map.isStyleLoaded()` is false
|
|
2474
|
+
* inside `style.load` and transiently while sources load, so the drawing bridge
|
|
2475
|
+
* keeps its own readiness (same reason as `MapboxLayerPort.styleReady`).
|
|
2476
|
+
*/
|
|
2477
|
+
this.styleLoadedMap = null;
|
|
2478
|
+
/** Style URL currently applied to the map, so a same-URL change is skipped instead of reloaded. */
|
|
2479
|
+
this.appliedStyleUrl = null;
|
|
2449
2480
|
this.appearance = { ...reactMapAdapterShared.DEFAULT_MAP_APPEARANCE, theme: config.theme ?? "light" };
|
|
2450
2481
|
this.layers = new MapboxLayerPort();
|
|
2451
2482
|
this.layers.setTheme(this.appearance.theme);
|
|
2483
|
+
// Engine load failures are not surfaced here (same as the deck.gl runtime): the
|
|
2484
|
+
// controller keeps mode and value and loads again on the next connect, setMode
|
|
2485
|
+
// or setValue that needs an engine.
|
|
2486
|
+
this.draw = new reactMapDraw.DrawController();
|
|
2487
|
+
this.draw.subscribe(() => this.syncDrawingStatus());
|
|
2452
2488
|
this.state = { ...reactMapAdapterShared.computeInitialState(this.appearance, config.initialViewport), tileSize: MAPBOX_TILE_SIZE };
|
|
2453
2489
|
this.cachedCameraState = {
|
|
2454
2490
|
center: this.state.center,
|
|
@@ -2461,6 +2497,7 @@ class MapboxAdapterInstance {
|
|
|
2461
2497
|
initializationFailed: this.state.initializationFailed,
|
|
2462
2498
|
appearance: this.state.appearance,
|
|
2463
2499
|
tileSize: this.state.tileSize,
|
|
2500
|
+
drawing: this.state.drawing,
|
|
2464
2501
|
};
|
|
2465
2502
|
}
|
|
2466
2503
|
// ============================================================================
|
|
@@ -2483,8 +2520,18 @@ class MapboxAdapterInstance {
|
|
|
2483
2520
|
this.setupEventListeners();
|
|
2484
2521
|
// Connect the layer port so it can render on this map
|
|
2485
2522
|
this.layers.connect(map);
|
|
2523
|
+
// Drawing lives in style-bound sources and layers, so the engine is (re)built on
|
|
2524
|
+
// every style load and torn down before setStyle (ADR 0030). Mode and value survive
|
|
2525
|
+
// in the controller; the engine is rebuilt from the committed value.
|
|
2526
|
+
const connectDraw = () => this.connectDraw(map);
|
|
2527
|
+
map.on("style.load", connectDraw);
|
|
2528
|
+
this.mapboxEventCleanups.push(() => map.off("style.load", connectDraw));
|
|
2529
|
+
if (this.styleLoadedMap === map || map.isStyleLoaded()) {
|
|
2530
|
+
connectDraw();
|
|
2531
|
+
}
|
|
2532
|
+
this.appliedStyleUrl = getInitialStyleUrl(this.appearance.theme);
|
|
2486
2533
|
// Set isReady and notify status listeners before syncing camera state
|
|
2487
|
-
this.state = { ...this.state, isReady: true, initializationFailed: false };
|
|
2534
|
+
this.state = { ...this.state, isReady: true, initializationFailed: false, drawing: this.draw.getMode() !== "idle" };
|
|
2488
2535
|
this.updateStatusCache();
|
|
2489
2536
|
this.notifyStatusListeners();
|
|
2490
2537
|
// Sync initial map state
|
|
@@ -2603,12 +2650,7 @@ class MapboxAdapterInstance {
|
|
|
2603
2650
|
const currentMap = this.map;
|
|
2604
2651
|
if (currentMap === null)
|
|
2605
2652
|
return;
|
|
2606
|
-
|
|
2607
|
-
const styleUrl = MAPBOX_MAP_TYPE_STYLES[effectiveType][this.appearance.theme];
|
|
2608
|
-
return new Promise(resolve => {
|
|
2609
|
-
currentMap.setStyle(styleUrl);
|
|
2610
|
-
currentMap.once("idle", () => resolve());
|
|
2611
|
-
});
|
|
2653
|
+
return this.applyStyle(currentMap);
|
|
2612
2654
|
}
|
|
2613
2655
|
async setTheme(theme) {
|
|
2614
2656
|
if (this.appearance.theme === theme)
|
|
@@ -2621,12 +2663,7 @@ class MapboxAdapterInstance {
|
|
|
2621
2663
|
const currentMap = this.map;
|
|
2622
2664
|
if (currentMap === null)
|
|
2623
2665
|
return;
|
|
2624
|
-
|
|
2625
|
-
const styleUrl = MAPBOX_MAP_TYPE_STYLES[effectiveType][this.appearance.theme];
|
|
2626
|
-
return new Promise(resolve => {
|
|
2627
|
-
currentMap.setStyle(styleUrl);
|
|
2628
|
-
currentMap.once("idle", () => resolve());
|
|
2629
|
-
});
|
|
2666
|
+
return this.applyStyle(currentMap);
|
|
2630
2667
|
}
|
|
2631
2668
|
async setShowRoads(showRoads) {
|
|
2632
2669
|
if (this.appearance.showRoads === showRoads)
|
|
@@ -2640,12 +2677,7 @@ class MapboxAdapterInstance {
|
|
|
2640
2677
|
if (currentMap === null)
|
|
2641
2678
|
return;
|
|
2642
2679
|
if (this.appearance.mapType === "satellite" || this.appearance.mapType === "hybrid") {
|
|
2643
|
-
|
|
2644
|
-
const styleUrl = MAPBOX_MAP_TYPE_STYLES[effectiveType][this.appearance.theme];
|
|
2645
|
-
return new Promise(resolve => {
|
|
2646
|
-
currentMap.setStyle(styleUrl);
|
|
2647
|
-
currentMap.once("idle", () => resolve());
|
|
2648
|
-
});
|
|
2680
|
+
return this.applyStyle(currentMap);
|
|
2649
2681
|
}
|
|
2650
2682
|
}
|
|
2651
2683
|
on(event, handler) {
|
|
@@ -2679,7 +2711,8 @@ class MapboxAdapterInstance {
|
|
|
2679
2711
|
}
|
|
2680
2712
|
destroy() {
|
|
2681
2713
|
this.isDestroyedFlag = true;
|
|
2682
|
-
// Clean up layer port before removing event listeners
|
|
2714
|
+
// Clean up drawing and layer port before removing event listeners
|
|
2715
|
+
this.draw.disconnect();
|
|
2683
2716
|
this.layers.destroy();
|
|
2684
2717
|
// Remove all Mapbox event listeners
|
|
2685
2718
|
this.mapboxEventCleanups.forEach(cleanup => cleanup());
|
|
@@ -2807,6 +2840,39 @@ class MapboxAdapterInstance {
|
|
|
2807
2840
|
this.updateCameraCache();
|
|
2808
2841
|
this.notifyCameraListeners();
|
|
2809
2842
|
}
|
|
2843
|
+
connectDraw(map) {
|
|
2844
|
+
this.styleLoadedMap = map;
|
|
2845
|
+
this.draw.connect(createMapboxDrawEngineLoader(map));
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Applies the style for the current appearance.
|
|
2849
|
+
*
|
|
2850
|
+
* Satellite light and dark share one URL, and satellite with roads is the hybrid
|
|
2851
|
+
* URL, so a change can resolve to the style already on the map. Mapbox diffs such
|
|
2852
|
+
* a request as a no-op and never fires `style.load`; skipping it keeps the layers
|
|
2853
|
+
* and the drawing in place. For a real change the drawing is torn down first and
|
|
2854
|
+
* rebound on `style.load`, with `idle` as the safety net should the load event not
|
|
2855
|
+
* fire.
|
|
2856
|
+
*/
|
|
2857
|
+
applyStyle(currentMap) {
|
|
2858
|
+
const styleUrl = MAPBOX_MAP_TYPE_STYLES[this.effectiveMapType()][this.appearance.theme];
|
|
2859
|
+
if (styleUrl === this.appliedStyleUrl)
|
|
2860
|
+
return Promise.resolve();
|
|
2861
|
+
this.appliedStyleUrl = styleUrl;
|
|
2862
|
+
return new Promise(resolve => {
|
|
2863
|
+
this.draw.disconnect();
|
|
2864
|
+
currentMap.setStyle(styleUrl);
|
|
2865
|
+
currentMap.once("idle", () => {
|
|
2866
|
+
// A late idle after destroy() or after a map swap must not attach Terra Draw
|
|
2867
|
+
// to a torn-down adapter or a dead map.
|
|
2868
|
+
const stillCurrent = this.map === currentMap && !this.isDestroyedFlag;
|
|
2869
|
+
if (stillCurrent && !this.draw.isConnected()) {
|
|
2870
|
+
this.connectDraw(currentMap);
|
|
2871
|
+
}
|
|
2872
|
+
resolve();
|
|
2873
|
+
});
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2810
2876
|
/**
|
|
2811
2877
|
* Resolves the effective Mapbox style key: when the user asks for "satellite"
|
|
2812
2878
|
* with roads enabled, we use "hybrid" (satellite + road labels).
|
|
@@ -2822,8 +2888,17 @@ class MapboxAdapterInstance {
|
|
|
2822
2888
|
this.cachedCameraState = { center, zoom, bounds, isIdle };
|
|
2823
2889
|
}
|
|
2824
2890
|
updateStatusCache() {
|
|
2825
|
-
const { isReady, initializationFailed, appearance, tileSize } = this.state;
|
|
2826
|
-
this.cachedStatus = { isReady, initializationFailed, appearance, tileSize };
|
|
2891
|
+
const { isReady, initializationFailed, appearance, tileSize, drawing } = this.state;
|
|
2892
|
+
this.cachedStatus = { isReady, initializationFailed, appearance, tileSize, drawing };
|
|
2893
|
+
}
|
|
2894
|
+
/** Mirrors the draw controller's mode into `MapStatus.drawing` (ADR 0030). */
|
|
2895
|
+
syncDrawingStatus() {
|
|
2896
|
+
const drawing = this.draw.getMode() !== "idle";
|
|
2897
|
+
if (this.state.drawing === drawing)
|
|
2898
|
+
return;
|
|
2899
|
+
this.state = { ...this.state, drawing };
|
|
2900
|
+
this.updateStatusCache();
|
|
2901
|
+
this.notifyStatusListeners();
|
|
2827
2902
|
}
|
|
2828
2903
|
notifyCameraListeners() {
|
|
2829
2904
|
this.cameraListeners.forEach(listener => listener());
|
|
@@ -2912,7 +2987,7 @@ const MapboxMapInternal = ({ adapterInstance, children, className, style, theme,
|
|
|
2912
2987
|
const mapOptions = initialViewport?.type === "bounds"
|
|
2913
2988
|
? {
|
|
2914
2989
|
container: el,
|
|
2915
|
-
style:
|
|
2990
|
+
style: getInitialStyleUrl(theme),
|
|
2916
2991
|
bounds: bboxToLngLatBounds(initialViewport.bounds),
|
|
2917
2992
|
fitBoundsOptions: initialViewport.padding !== undefined || initialViewport.maxZoom !== undefined
|
|
2918
2993
|
? {
|
|
@@ -2925,7 +3000,7 @@ const MapboxMapInternal = ({ adapterInstance, children, className, style, theme,
|
|
|
2925
3000
|
}
|
|
2926
3001
|
: {
|
|
2927
3002
|
container: el,
|
|
2928
|
-
style:
|
|
3003
|
+
style: getInitialStyleUrl(theme),
|
|
2929
3004
|
center: initialViewport?.type === "center"
|
|
2930
3005
|
? { lng: initialViewport.center[0], lat: initialViewport.center[1] }
|
|
2931
3006
|
: { lng: reactMapAdapterShared.DEFAULT_CENTER.lng, lat: reactMapAdapterShared.DEFAULT_CENTER.lat },
|
package/index.esm.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canPatchAdaptiveMarker, canPatchAdaptiveViewport, canPatchMarkerInPlace, getAdaptiveDomFeatureIds, buildAdaptiveSymbolStyleFn, extractPointCoordinates, resolveCircleSymbolDefaults, resolveSymbolDescriptor, mergeAntimeridianFeatures, densifyGeodesicFeatures, LAYER_FADE_DURATION_MS, patchPortalDescriptors, buildAdaptiveDomRenderFn, extractSourceData, collectFeatureIdSet, removeGoneIndexedMarkers, createSymbolDotElement, createDefaultClusterElement, createClusterPinElement, fadeInElement, buildAdaptiveDomEntries, attachSafeAreaHoverListeners, resolveSelectedStyle, resolveHoveredStyle, MAP_CURSORS, geometryTypeToShapeType, INITIAL_MAP_STATE, DEFAULT_MAP_APPEARANCE, computeInitialState, cameraStateEquals, isEventOfType, getEffectiveRestrictBounds, DEFAULT_ZOOM, DEFAULT_CENTER, defineAdapter } from '@trackunit/react-map-adapter-shared';
|
|
2
2
|
export { mapboxMapCspDirectives } from '@trackunit/react-map-adapter-shared';
|
|
3
3
|
import { validateBbox, validatePosition } from '@trackunit/geo-json-utils';
|
|
4
|
+
import { loadTerraDrawEngine, DRAW_COORDINATE_PRECISION, DrawController } from '@trackunit/react-map-draw';
|
|
4
5
|
import { isEqual } from 'es-toolkit';
|
|
5
6
|
import mapboxgl from 'mapbox-gl';
|
|
6
7
|
import Supercluster from 'supercluster';
|
|
@@ -26,6 +27,27 @@ const MAPBOX_MAP_TYPE_STYLES = {
|
|
|
26
27
|
dark: "mapbox://styles/mapbox/satellite-streets-v12",
|
|
27
28
|
},
|
|
28
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Style every Mapbox map is created with. The renderer passes it to `mapboxgl.Map`
|
|
32
|
+
* and the adapter instance records it as the applied style, so the two agree on
|
|
33
|
+
* what is on the map before the first `applyStyle`.
|
|
34
|
+
*/
|
|
35
|
+
const getInitialStyleUrl = (theme) => MAPBOX_MAP_TYPE_STYLES.roadmap[theme];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the Mapbox GL bridge for the shared drawing engine (ADR 0030).
|
|
39
|
+
*
|
|
40
|
+
* One loader per loaded style: `setStyle` (theme, map type, Roads) removes every
|
|
41
|
+
* custom source and layer, so `MapboxAdapterInstance` disconnects the controller
|
|
42
|
+
* before a style change and reconnects it with a fresh loader on `style.load`.
|
|
43
|
+
* Terra Draw and its Mapbox adapter are only downloaded when the loader runs.
|
|
44
|
+
*/
|
|
45
|
+
const createMapboxDrawEngineLoader = (map) => async () => {
|
|
46
|
+
const { TerraDrawMapboxGLAdapter } = await import('terra-draw-mapbox-gl-adapter');
|
|
47
|
+
return loadTerraDrawEngine({
|
|
48
|
+
adapter: new TerraDrawMapboxGLAdapter({ map, coordinatePrecision: DRAW_COORDINATE_PRECISION }),
|
|
49
|
+
});
|
|
50
|
+
};
|
|
29
51
|
|
|
30
52
|
// ============================================================================
|
|
31
53
|
// Helpers
|
|
@@ -2439,15 +2461,29 @@ class MapboxAdapterInstance {
|
|
|
2439
2461
|
initializationFailed: INITIAL_MAP_STATE.initializationFailed,
|
|
2440
2462
|
appearance: INITIAL_MAP_STATE.appearance,
|
|
2441
2463
|
tileSize: MAPBOX_TILE_SIZE,
|
|
2464
|
+
drawing: INITIAL_MAP_STATE.drawing,
|
|
2442
2465
|
};
|
|
2443
2466
|
this.cameraListeners = new Set();
|
|
2444
2467
|
this.statusListeners = new Set();
|
|
2445
2468
|
this.eventListeners = new Map();
|
|
2446
2469
|
this.mapboxEventCleanups = [];
|
|
2447
2470
|
this.isDestroyedFlag = false;
|
|
2471
|
+
/**
|
|
2472
|
+
* The map whose style has loaded at least once. `map.isStyleLoaded()` is false
|
|
2473
|
+
* inside `style.load` and transiently while sources load, so the drawing bridge
|
|
2474
|
+
* keeps its own readiness (same reason as `MapboxLayerPort.styleReady`).
|
|
2475
|
+
*/
|
|
2476
|
+
this.styleLoadedMap = null;
|
|
2477
|
+
/** Style URL currently applied to the map, so a same-URL change is skipped instead of reloaded. */
|
|
2478
|
+
this.appliedStyleUrl = null;
|
|
2448
2479
|
this.appearance = { ...DEFAULT_MAP_APPEARANCE, theme: config.theme ?? "light" };
|
|
2449
2480
|
this.layers = new MapboxLayerPort();
|
|
2450
2481
|
this.layers.setTheme(this.appearance.theme);
|
|
2482
|
+
// Engine load failures are not surfaced here (same as the deck.gl runtime): the
|
|
2483
|
+
// controller keeps mode and value and loads again on the next connect, setMode
|
|
2484
|
+
// or setValue that needs an engine.
|
|
2485
|
+
this.draw = new DrawController();
|
|
2486
|
+
this.draw.subscribe(() => this.syncDrawingStatus());
|
|
2451
2487
|
this.state = { ...computeInitialState(this.appearance, config.initialViewport), tileSize: MAPBOX_TILE_SIZE };
|
|
2452
2488
|
this.cachedCameraState = {
|
|
2453
2489
|
center: this.state.center,
|
|
@@ -2460,6 +2496,7 @@ class MapboxAdapterInstance {
|
|
|
2460
2496
|
initializationFailed: this.state.initializationFailed,
|
|
2461
2497
|
appearance: this.state.appearance,
|
|
2462
2498
|
tileSize: this.state.tileSize,
|
|
2499
|
+
drawing: this.state.drawing,
|
|
2463
2500
|
};
|
|
2464
2501
|
}
|
|
2465
2502
|
// ============================================================================
|
|
@@ -2482,8 +2519,18 @@ class MapboxAdapterInstance {
|
|
|
2482
2519
|
this.setupEventListeners();
|
|
2483
2520
|
// Connect the layer port so it can render on this map
|
|
2484
2521
|
this.layers.connect(map);
|
|
2522
|
+
// Drawing lives in style-bound sources and layers, so the engine is (re)built on
|
|
2523
|
+
// every style load and torn down before setStyle (ADR 0030). Mode and value survive
|
|
2524
|
+
// in the controller; the engine is rebuilt from the committed value.
|
|
2525
|
+
const connectDraw = () => this.connectDraw(map);
|
|
2526
|
+
map.on("style.load", connectDraw);
|
|
2527
|
+
this.mapboxEventCleanups.push(() => map.off("style.load", connectDraw));
|
|
2528
|
+
if (this.styleLoadedMap === map || map.isStyleLoaded()) {
|
|
2529
|
+
connectDraw();
|
|
2530
|
+
}
|
|
2531
|
+
this.appliedStyleUrl = getInitialStyleUrl(this.appearance.theme);
|
|
2485
2532
|
// Set isReady and notify status listeners before syncing camera state
|
|
2486
|
-
this.state = { ...this.state, isReady: true, initializationFailed: false };
|
|
2533
|
+
this.state = { ...this.state, isReady: true, initializationFailed: false, drawing: this.draw.getMode() !== "idle" };
|
|
2487
2534
|
this.updateStatusCache();
|
|
2488
2535
|
this.notifyStatusListeners();
|
|
2489
2536
|
// Sync initial map state
|
|
@@ -2602,12 +2649,7 @@ class MapboxAdapterInstance {
|
|
|
2602
2649
|
const currentMap = this.map;
|
|
2603
2650
|
if (currentMap === null)
|
|
2604
2651
|
return;
|
|
2605
|
-
|
|
2606
|
-
const styleUrl = MAPBOX_MAP_TYPE_STYLES[effectiveType][this.appearance.theme];
|
|
2607
|
-
return new Promise(resolve => {
|
|
2608
|
-
currentMap.setStyle(styleUrl);
|
|
2609
|
-
currentMap.once("idle", () => resolve());
|
|
2610
|
-
});
|
|
2652
|
+
return this.applyStyle(currentMap);
|
|
2611
2653
|
}
|
|
2612
2654
|
async setTheme(theme) {
|
|
2613
2655
|
if (this.appearance.theme === theme)
|
|
@@ -2620,12 +2662,7 @@ class MapboxAdapterInstance {
|
|
|
2620
2662
|
const currentMap = this.map;
|
|
2621
2663
|
if (currentMap === null)
|
|
2622
2664
|
return;
|
|
2623
|
-
|
|
2624
|
-
const styleUrl = MAPBOX_MAP_TYPE_STYLES[effectiveType][this.appearance.theme];
|
|
2625
|
-
return new Promise(resolve => {
|
|
2626
|
-
currentMap.setStyle(styleUrl);
|
|
2627
|
-
currentMap.once("idle", () => resolve());
|
|
2628
|
-
});
|
|
2665
|
+
return this.applyStyle(currentMap);
|
|
2629
2666
|
}
|
|
2630
2667
|
async setShowRoads(showRoads) {
|
|
2631
2668
|
if (this.appearance.showRoads === showRoads)
|
|
@@ -2639,12 +2676,7 @@ class MapboxAdapterInstance {
|
|
|
2639
2676
|
if (currentMap === null)
|
|
2640
2677
|
return;
|
|
2641
2678
|
if (this.appearance.mapType === "satellite" || this.appearance.mapType === "hybrid") {
|
|
2642
|
-
|
|
2643
|
-
const styleUrl = MAPBOX_MAP_TYPE_STYLES[effectiveType][this.appearance.theme];
|
|
2644
|
-
return new Promise(resolve => {
|
|
2645
|
-
currentMap.setStyle(styleUrl);
|
|
2646
|
-
currentMap.once("idle", () => resolve());
|
|
2647
|
-
});
|
|
2679
|
+
return this.applyStyle(currentMap);
|
|
2648
2680
|
}
|
|
2649
2681
|
}
|
|
2650
2682
|
on(event, handler) {
|
|
@@ -2678,7 +2710,8 @@ class MapboxAdapterInstance {
|
|
|
2678
2710
|
}
|
|
2679
2711
|
destroy() {
|
|
2680
2712
|
this.isDestroyedFlag = true;
|
|
2681
|
-
// Clean up layer port before removing event listeners
|
|
2713
|
+
// Clean up drawing and layer port before removing event listeners
|
|
2714
|
+
this.draw.disconnect();
|
|
2682
2715
|
this.layers.destroy();
|
|
2683
2716
|
// Remove all Mapbox event listeners
|
|
2684
2717
|
this.mapboxEventCleanups.forEach(cleanup => cleanup());
|
|
@@ -2806,6 +2839,39 @@ class MapboxAdapterInstance {
|
|
|
2806
2839
|
this.updateCameraCache();
|
|
2807
2840
|
this.notifyCameraListeners();
|
|
2808
2841
|
}
|
|
2842
|
+
connectDraw(map) {
|
|
2843
|
+
this.styleLoadedMap = map;
|
|
2844
|
+
this.draw.connect(createMapboxDrawEngineLoader(map));
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Applies the style for the current appearance.
|
|
2848
|
+
*
|
|
2849
|
+
* Satellite light and dark share one URL, and satellite with roads is the hybrid
|
|
2850
|
+
* URL, so a change can resolve to the style already on the map. Mapbox diffs such
|
|
2851
|
+
* a request as a no-op and never fires `style.load`; skipping it keeps the layers
|
|
2852
|
+
* and the drawing in place. For a real change the drawing is torn down first and
|
|
2853
|
+
* rebound on `style.load`, with `idle` as the safety net should the load event not
|
|
2854
|
+
* fire.
|
|
2855
|
+
*/
|
|
2856
|
+
applyStyle(currentMap) {
|
|
2857
|
+
const styleUrl = MAPBOX_MAP_TYPE_STYLES[this.effectiveMapType()][this.appearance.theme];
|
|
2858
|
+
if (styleUrl === this.appliedStyleUrl)
|
|
2859
|
+
return Promise.resolve();
|
|
2860
|
+
this.appliedStyleUrl = styleUrl;
|
|
2861
|
+
return new Promise(resolve => {
|
|
2862
|
+
this.draw.disconnect();
|
|
2863
|
+
currentMap.setStyle(styleUrl);
|
|
2864
|
+
currentMap.once("idle", () => {
|
|
2865
|
+
// A late idle after destroy() or after a map swap must not attach Terra Draw
|
|
2866
|
+
// to a torn-down adapter or a dead map.
|
|
2867
|
+
const stillCurrent = this.map === currentMap && !this.isDestroyedFlag;
|
|
2868
|
+
if (stillCurrent && !this.draw.isConnected()) {
|
|
2869
|
+
this.connectDraw(currentMap);
|
|
2870
|
+
}
|
|
2871
|
+
resolve();
|
|
2872
|
+
});
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2809
2875
|
/**
|
|
2810
2876
|
* Resolves the effective Mapbox style key: when the user asks for "satellite"
|
|
2811
2877
|
* with roads enabled, we use "hybrid" (satellite + road labels).
|
|
@@ -2821,8 +2887,17 @@ class MapboxAdapterInstance {
|
|
|
2821
2887
|
this.cachedCameraState = { center, zoom, bounds, isIdle };
|
|
2822
2888
|
}
|
|
2823
2889
|
updateStatusCache() {
|
|
2824
|
-
const { isReady, initializationFailed, appearance, tileSize } = this.state;
|
|
2825
|
-
this.cachedStatus = { isReady, initializationFailed, appearance, tileSize };
|
|
2890
|
+
const { isReady, initializationFailed, appearance, tileSize, drawing } = this.state;
|
|
2891
|
+
this.cachedStatus = { isReady, initializationFailed, appearance, tileSize, drawing };
|
|
2892
|
+
}
|
|
2893
|
+
/** Mirrors the draw controller's mode into `MapStatus.drawing` (ADR 0030). */
|
|
2894
|
+
syncDrawingStatus() {
|
|
2895
|
+
const drawing = this.draw.getMode() !== "idle";
|
|
2896
|
+
if (this.state.drawing === drawing)
|
|
2897
|
+
return;
|
|
2898
|
+
this.state = { ...this.state, drawing };
|
|
2899
|
+
this.updateStatusCache();
|
|
2900
|
+
this.notifyStatusListeners();
|
|
2826
2901
|
}
|
|
2827
2902
|
notifyCameraListeners() {
|
|
2828
2903
|
this.cameraListeners.forEach(listener => listener());
|
|
@@ -2911,7 +2986,7 @@ const MapboxMapInternal = ({ adapterInstance, children, className, style, theme,
|
|
|
2911
2986
|
const mapOptions = initialViewport?.type === "bounds"
|
|
2912
2987
|
? {
|
|
2913
2988
|
container: el,
|
|
2914
|
-
style:
|
|
2989
|
+
style: getInitialStyleUrl(theme),
|
|
2915
2990
|
bounds: bboxToLngLatBounds(initialViewport.bounds),
|
|
2916
2991
|
fitBoundsOptions: initialViewport.padding !== undefined || initialViewport.maxZoom !== undefined
|
|
2917
2992
|
? {
|
|
@@ -2924,7 +2999,7 @@ const MapboxMapInternal = ({ adapterInstance, children, className, style, theme,
|
|
|
2924
2999
|
}
|
|
2925
3000
|
: {
|
|
2926
3001
|
container: el,
|
|
2927
|
-
style:
|
|
3002
|
+
style: getInitialStyleUrl(theme),
|
|
2928
3003
|
center: initialViewport?.type === "center"
|
|
2929
3004
|
? { lng: initialViewport.center[0], lat: initialViewport.center[1] }
|
|
2930
3005
|
: { lng: DEFAULT_CENTER.lng, lat: DEFAULT_CENTER.lat },
|
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map-adapter-mapbox",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.152",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.x"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@trackunit/react-map-adapter-shared": "0.0.
|
|
11
|
-
"@trackunit/geo-json-utils": "1.
|
|
10
|
+
"@trackunit/react-map-adapter-shared": "0.0.164",
|
|
11
|
+
"@trackunit/geo-json-utils": "1.19.0",
|
|
12
12
|
"mapbox-gl": "^3.18.1",
|
|
13
13
|
"supercluster": "^8.0.1",
|
|
14
14
|
"es-toolkit": "^1.52.0",
|
|
15
15
|
"react-device-detect": "^2.2.3",
|
|
16
|
-
"zod": "^3.25.76"
|
|
16
|
+
"zod": "^3.25.76",
|
|
17
|
+
"@trackunit/react-map-draw": "0.0.2",
|
|
18
|
+
"terra-draw-mapbox-gl-adapter": "^1.4.0"
|
|
17
19
|
},
|
|
18
20
|
"peerDependencies": {
|
|
19
21
|
"react": "^19.0.0"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AdapterInstance, type BaseAdapterConfig, type CameraState, type FitBoundsOptions, type GeoJsonBbox, type GeoJsonPosition, type MapEvent, type MapEventHandler, type MapStatus, type MapTheme, type MapType } from "@trackunit/react-map-adapter-shared";
|
|
2
|
+
import { DrawController } from "@trackunit/react-map-draw";
|
|
2
3
|
import { MapboxLayerPort } from "./MapboxLayerPort";
|
|
3
4
|
/**
|
|
4
5
|
* Configuration for Mapbox adapter
|
|
@@ -15,6 +16,11 @@ export declare class MapboxAdapterInstance implements AdapterInstance<MapboxConf
|
|
|
15
16
|
private readonly config;
|
|
16
17
|
/** Layer rendering port -- manages markers, shapes, routes, and overlays on the map */
|
|
17
18
|
readonly layers: MapboxLayerPort;
|
|
19
|
+
/**
|
|
20
|
+
* Drawing capability (ADR 0030). The controller keeps mode and value across style
|
|
21
|
+
* reloads; `connect()` rebinds it on every `style.load`.
|
|
22
|
+
*/
|
|
23
|
+
readonly draw: DrawController;
|
|
18
24
|
private map;
|
|
19
25
|
private state;
|
|
20
26
|
private cachedCameraState;
|
|
@@ -24,6 +30,14 @@ export declare class MapboxAdapterInstance implements AdapterInstance<MapboxConf
|
|
|
24
30
|
private readonly eventListeners;
|
|
25
31
|
private mapboxEventCleanups;
|
|
26
32
|
private isDestroyedFlag;
|
|
33
|
+
/**
|
|
34
|
+
* The map whose style has loaded at least once. `map.isStyleLoaded()` is false
|
|
35
|
+
* inside `style.load` and transiently while sources load, so the drawing bridge
|
|
36
|
+
* keeps its own readiness (same reason as `MapboxLayerPort.styleReady`).
|
|
37
|
+
*/
|
|
38
|
+
private styleLoadedMap;
|
|
39
|
+
/** Style URL currently applied to the map, so a same-URL change is skipped instead of reloaded. */
|
|
40
|
+
private appliedStyleUrl;
|
|
27
41
|
/** Unified appearance state — theme, mapType, and showRoads in one place */
|
|
28
42
|
private appearance;
|
|
29
43
|
constructor(config: MapboxConfig);
|
|
@@ -61,6 +75,18 @@ export declare class MapboxAdapterInstance implements AdapterInstance<MapboxConf
|
|
|
61
75
|
* Used by movestart (false) and the idle path already calls updateState (true).
|
|
62
76
|
*/
|
|
63
77
|
private setIdleState;
|
|
78
|
+
private connectDraw;
|
|
79
|
+
/**
|
|
80
|
+
* Applies the style for the current appearance.
|
|
81
|
+
*
|
|
82
|
+
* Satellite light and dark share one URL, and satellite with roads is the hybrid
|
|
83
|
+
* URL, so a change can resolve to the style already on the map. Mapbox diffs such
|
|
84
|
+
* a request as a no-op and never fires `style.load`; skipping it keeps the layers
|
|
85
|
+
* and the drawing in place. For a real change the drawing is torn down first and
|
|
86
|
+
* rebound on `style.load`, with `idle` as the safety net should the load event not
|
|
87
|
+
* fire.
|
|
88
|
+
*/
|
|
89
|
+
private applyStyle;
|
|
64
90
|
/**
|
|
65
91
|
* Resolves the effective Mapbox style key: when the user asks for "satellite"
|
|
66
92
|
* with roads enabled, we use "hybrid" (satellite + road labels).
|
|
@@ -68,6 +94,8 @@ export declare class MapboxAdapterInstance implements AdapterInstance<MapboxConf
|
|
|
68
94
|
private effectiveMapType;
|
|
69
95
|
private updateCameraCache;
|
|
70
96
|
private updateStatusCache;
|
|
97
|
+
/** Mirrors the draw controller's mode into `MapStatus.drawing` (ADR 0030). */
|
|
98
|
+
private syncDrawingStatus;
|
|
71
99
|
private notifyCameraListeners;
|
|
72
100
|
private notifyStatusListeners;
|
|
73
101
|
private emitEvent;
|
package/src/constants.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MapTheme } from "@trackunit/react-map-adapter-shared";
|
|
1
2
|
/**
|
|
2
3
|
* Mapbox style URLs for different map types
|
|
3
4
|
*/
|
|
@@ -15,3 +16,9 @@ export declare const MAPBOX_MAP_TYPE_STYLES: {
|
|
|
15
16
|
readonly dark: "mapbox://styles/mapbox/satellite-streets-v12";
|
|
16
17
|
};
|
|
17
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Style every Mapbox map is created with. The renderer passes it to `mapboxgl.Map`
|
|
21
|
+
* and the adapter instance records it as the applied style, so the two agree on
|
|
22
|
+
* what is on the map before the first `applyStyle`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const getInitialStyleUrl: (theme: MapTheme) => string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type DrawEngineLoader } from "@trackunit/react-map-draw";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the Mapbox GL bridge for the shared drawing engine (ADR 0030).
|
|
4
|
+
*
|
|
5
|
+
* One loader per loaded style: `setStyle` (theme, map type, Roads) removes every
|
|
6
|
+
* custom source and layer, so `MapboxAdapterInstance` disconnects the controller
|
|
7
|
+
* before a style change and reconnects it with a fresh loader on `style.load`.
|
|
8
|
+
* Terra Draw and its Mapbox adapter are only downloaded when the loader runs.
|
|
9
|
+
*/
|
|
10
|
+
export declare const createMapboxDrawEngineLoader: (map: mapboxgl.Map) => DrawEngineLoader;
|