@trackunit/react-map-adapter-mapbox 0.0.131 → 0.0.133
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/README.md +6 -0
- package/index.cjs.js +59 -0
- package/index.esm.js +59 -0
- package/package.json +3 -3
- package/src/MapboxLayerPort.d.ts +9 -0
- package/src/mapboxAdapter.d.ts +6 -0
package/README.md
CHANGED
|
@@ -51,6 +51,12 @@ Use the exported directives instead of copying host lists. Add any origins requi
|
|
|
51
51
|
|
|
52
52
|
Where Mapbox GL lacks a capability the Google Maps adapter gets natively, this adapter emulates it so consumers see identical behaviour regardless of provider. The clearest case is geodesic arcs: Google draws great-circle edges natively, while Mapbox renders Mercator straight lines, so this adapter densifies edges with `densifyGeodesicFeatures` from `@trackunit/react-map-adapter-shared` to match. Provider gaps are closed inside the adapter — never pushed up to application code.
|
|
53
53
|
|
|
54
|
+
## Heatmaps
|
|
55
|
+
|
|
56
|
+
- **Renderer:** Mapbox GL native `heatmap` layer + GeoJSON source (`MapboxLayerPort.syncHeatmap`).
|
|
57
|
+
- **Contract:** shared `HeatmapSourceConfig`; style reload re-applies heatmap sources.
|
|
58
|
+
- **ADR:** `libs/react/map/docs/adr/0029-heatmap-provider-specific-renderers.md` (monorepo).
|
|
59
|
+
|
|
54
60
|
## Dependencies
|
|
55
61
|
|
|
56
62
|
Depends on `@trackunit/react-map-adapter-shared` and Mapbox GL–related dependencies used by the renderer. It does **not** import `@trackunit/react-map` (adapters are consumed *by* the map package, not the reverse).
|
package/index.cjs.js
CHANGED
|
@@ -226,6 +226,7 @@ class MapboxLayerPort {
|
|
|
226
226
|
this.shapeSources = new Map();
|
|
227
227
|
this.routeSources = new Map();
|
|
228
228
|
this.overlaySources = new Map();
|
|
229
|
+
this.heatmapSources = new Map();
|
|
229
230
|
// Snapshot queued before the map is connected / style is ready
|
|
230
231
|
this.pendingSnapshot = null;
|
|
231
232
|
// Last non-empty snapshot the consumer pushed. Preserved across `destroy()` so the React 18
|
|
@@ -414,6 +415,9 @@ class MapboxLayerPort {
|
|
|
414
415
|
for (const id of [...this.overlaySources.keys()]) {
|
|
415
416
|
this.removeImageOverlayInternal(id);
|
|
416
417
|
}
|
|
418
|
+
for (const id of [...this.heatmapSources.keys()]) {
|
|
419
|
+
this.removeHeatmapInternal(id);
|
|
420
|
+
}
|
|
417
421
|
// Remove style.load listener
|
|
418
422
|
if (this.styleLoadHandler !== null && this.map !== null) {
|
|
419
423
|
this.map.off("style.load", this.styleLoadHandler);
|
|
@@ -944,6 +948,47 @@ class MapboxLayerPort {
|
|
|
944
948
|
}
|
|
945
949
|
this.overlaySources.delete(id);
|
|
946
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* Native Mapbox GL heatmap layer (ADR-0029).
|
|
953
|
+
*
|
|
954
|
+
* - Source: `heatmap-source-{id}`, layer: `heatmap-layer-{id}`.
|
|
955
|
+
* - Restored on style reload via `reapplySourcesAfterStyleChange`.
|
|
956
|
+
*/
|
|
957
|
+
syncHeatmap(config) {
|
|
958
|
+
if (this.map === null)
|
|
959
|
+
return;
|
|
960
|
+
const existing = this.heatmapSources.get(config.id);
|
|
961
|
+
if (existing !== undefined && esToolkit.isEqual(existing.config, config))
|
|
962
|
+
return;
|
|
963
|
+
this.removeHeatmapInternal(config.id);
|
|
964
|
+
const sourceId = `heatmap-source-${config.id}`;
|
|
965
|
+
const layerId = `heatmap-layer-${config.id}`;
|
|
966
|
+
this.map.addSource(sourceId, {
|
|
967
|
+
type: "geojson",
|
|
968
|
+
data: toMapboxGeoJson(config.features),
|
|
969
|
+
});
|
|
970
|
+
this.map.addLayer({
|
|
971
|
+
id: layerId,
|
|
972
|
+
type: "heatmap",
|
|
973
|
+
source: sourceId,
|
|
974
|
+
paint: {
|
|
975
|
+
"heatmap-weight": ["coalesce", ["get", "weight"], 1],
|
|
976
|
+
"heatmap-radius": config.style.radiusPixels,
|
|
977
|
+
"heatmap-intensity": config.style.intensity,
|
|
978
|
+
"heatmap-opacity": config.style.opacity,
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
this.heatmapSources.set(config.id, { sourceId, layerId, config });
|
|
982
|
+
}
|
|
983
|
+
removeHeatmapInternal(id) {
|
|
984
|
+
const tracked = this.heatmapSources.get(id);
|
|
985
|
+
if (tracked === undefined)
|
|
986
|
+
return;
|
|
987
|
+
if (this.map !== null) {
|
|
988
|
+
safeRemoveLayersAndSource(this.map, [tracked.layerId], tracked.sourceId);
|
|
989
|
+
}
|
|
990
|
+
this.heatmapSources.delete(id);
|
|
991
|
+
}
|
|
947
992
|
// ============================================================================
|
|
948
993
|
// Private: Layer fade-in
|
|
949
994
|
// ============================================================================
|
|
@@ -1006,6 +1051,7 @@ class MapboxLayerPort {
|
|
|
1006
1051
|
const shapeIds = new Set(snapshot.layers.filter(l => l.layerType === "shapes").map(l => l.id));
|
|
1007
1052
|
const routeIds = new Set(snapshot.layers.filter(l => l.layerType === "route").map(l => l.id));
|
|
1008
1053
|
const overlayIds = new Set(snapshot.layers.filter(l => l.layerType === "image-overlay").map(l => l.id));
|
|
1054
|
+
const heatmapIds = new Set(snapshot.layers.filter(l => l.layerType === "heatmap").map(l => l.id));
|
|
1009
1055
|
for (const layer of snapshot.layers) {
|
|
1010
1056
|
switch (layer.layerType) {
|
|
1011
1057
|
case "markers":
|
|
@@ -1020,6 +1066,9 @@ class MapboxLayerPort {
|
|
|
1020
1066
|
case "image-overlay":
|
|
1021
1067
|
this.syncImageOverlay(layer);
|
|
1022
1068
|
break;
|
|
1069
|
+
case "heatmap":
|
|
1070
|
+
this.syncHeatmap(layer);
|
|
1071
|
+
break;
|
|
1023
1072
|
default: {
|
|
1024
1073
|
const exhaustiveCheck = layer;
|
|
1025
1074
|
throw new Error(`Unknown layer type: ${String(exhaustiveCheck)}`);
|
|
@@ -1042,6 +1091,10 @@ class MapboxLayerPort {
|
|
|
1042
1091
|
if (!overlayIds.has(id))
|
|
1043
1092
|
this.removeImageOverlayInternal(id);
|
|
1044
1093
|
}
|
|
1094
|
+
for (const id of [...this.heatmapSources.keys()]) {
|
|
1095
|
+
if (!heatmapIds.has(id))
|
|
1096
|
+
this.removeHeatmapInternal(id);
|
|
1097
|
+
}
|
|
1045
1098
|
this.applyShapeSelectionState(snapshot.shapeSelection.handleId, snapshot.shapeSelection.featureId);
|
|
1046
1099
|
this.applyShapeHoverState(snapshot.shapeHover.handleId, snapshot.shapeHover.featureId);
|
|
1047
1100
|
}
|
|
@@ -2191,6 +2244,7 @@ class MapboxLayerPort {
|
|
|
2191
2244
|
const shapeEntries = Array.from(this.shapeSources.entries());
|
|
2192
2245
|
const routeEntries = Array.from(this.routeSources.entries());
|
|
2193
2246
|
const overlayEntries = Array.from(this.overlaySources.entries());
|
|
2247
|
+
const heatmapEntries = Array.from(this.heatmapSources.entries());
|
|
2194
2248
|
const markerEntriesToRestore = Array.from(this.markerSources.entries()).filter(([, tracked]) => tracked.sourceId !== undefined);
|
|
2195
2249
|
// Re-add shape sources/layers
|
|
2196
2250
|
for (const [id, { config }] of shapeEntries) {
|
|
@@ -2216,6 +2270,11 @@ class MapboxLayerPort {
|
|
|
2216
2270
|
this.overlaySources.delete(id);
|
|
2217
2271
|
this.syncImageOverlay(config);
|
|
2218
2272
|
}
|
|
2273
|
+
// Re-add heatmap sources/layers
|
|
2274
|
+
for (const [id, { config }] of heatmapEntries) {
|
|
2275
|
+
this.heatmapSources.delete(id);
|
|
2276
|
+
this.syncHeatmap(config);
|
|
2277
|
+
}
|
|
2219
2278
|
// DOM markers (mapboxgl.Marker) persist across style changes, but
|
|
2220
2279
|
// WebGL circle layers/sources are removed. Re-add marker sources
|
|
2221
2280
|
// that used circle layers (from pre-snapshot).
|
package/index.esm.js
CHANGED
|
@@ -225,6 +225,7 @@ class MapboxLayerPort {
|
|
|
225
225
|
this.shapeSources = new Map();
|
|
226
226
|
this.routeSources = new Map();
|
|
227
227
|
this.overlaySources = new Map();
|
|
228
|
+
this.heatmapSources = new Map();
|
|
228
229
|
// Snapshot queued before the map is connected / style is ready
|
|
229
230
|
this.pendingSnapshot = null;
|
|
230
231
|
// Last non-empty snapshot the consumer pushed. Preserved across `destroy()` so the React 18
|
|
@@ -413,6 +414,9 @@ class MapboxLayerPort {
|
|
|
413
414
|
for (const id of [...this.overlaySources.keys()]) {
|
|
414
415
|
this.removeImageOverlayInternal(id);
|
|
415
416
|
}
|
|
417
|
+
for (const id of [...this.heatmapSources.keys()]) {
|
|
418
|
+
this.removeHeatmapInternal(id);
|
|
419
|
+
}
|
|
416
420
|
// Remove style.load listener
|
|
417
421
|
if (this.styleLoadHandler !== null && this.map !== null) {
|
|
418
422
|
this.map.off("style.load", this.styleLoadHandler);
|
|
@@ -943,6 +947,47 @@ class MapboxLayerPort {
|
|
|
943
947
|
}
|
|
944
948
|
this.overlaySources.delete(id);
|
|
945
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Native Mapbox GL heatmap layer (ADR-0029).
|
|
952
|
+
*
|
|
953
|
+
* - Source: `heatmap-source-{id}`, layer: `heatmap-layer-{id}`.
|
|
954
|
+
* - Restored on style reload via `reapplySourcesAfterStyleChange`.
|
|
955
|
+
*/
|
|
956
|
+
syncHeatmap(config) {
|
|
957
|
+
if (this.map === null)
|
|
958
|
+
return;
|
|
959
|
+
const existing = this.heatmapSources.get(config.id);
|
|
960
|
+
if (existing !== undefined && isEqual(existing.config, config))
|
|
961
|
+
return;
|
|
962
|
+
this.removeHeatmapInternal(config.id);
|
|
963
|
+
const sourceId = `heatmap-source-${config.id}`;
|
|
964
|
+
const layerId = `heatmap-layer-${config.id}`;
|
|
965
|
+
this.map.addSource(sourceId, {
|
|
966
|
+
type: "geojson",
|
|
967
|
+
data: toMapboxGeoJson(config.features),
|
|
968
|
+
});
|
|
969
|
+
this.map.addLayer({
|
|
970
|
+
id: layerId,
|
|
971
|
+
type: "heatmap",
|
|
972
|
+
source: sourceId,
|
|
973
|
+
paint: {
|
|
974
|
+
"heatmap-weight": ["coalesce", ["get", "weight"], 1],
|
|
975
|
+
"heatmap-radius": config.style.radiusPixels,
|
|
976
|
+
"heatmap-intensity": config.style.intensity,
|
|
977
|
+
"heatmap-opacity": config.style.opacity,
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
this.heatmapSources.set(config.id, { sourceId, layerId, config });
|
|
981
|
+
}
|
|
982
|
+
removeHeatmapInternal(id) {
|
|
983
|
+
const tracked = this.heatmapSources.get(id);
|
|
984
|
+
if (tracked === undefined)
|
|
985
|
+
return;
|
|
986
|
+
if (this.map !== null) {
|
|
987
|
+
safeRemoveLayersAndSource(this.map, [tracked.layerId], tracked.sourceId);
|
|
988
|
+
}
|
|
989
|
+
this.heatmapSources.delete(id);
|
|
990
|
+
}
|
|
946
991
|
// ============================================================================
|
|
947
992
|
// Private: Layer fade-in
|
|
948
993
|
// ============================================================================
|
|
@@ -1005,6 +1050,7 @@ class MapboxLayerPort {
|
|
|
1005
1050
|
const shapeIds = new Set(snapshot.layers.filter(l => l.layerType === "shapes").map(l => l.id));
|
|
1006
1051
|
const routeIds = new Set(snapshot.layers.filter(l => l.layerType === "route").map(l => l.id));
|
|
1007
1052
|
const overlayIds = new Set(snapshot.layers.filter(l => l.layerType === "image-overlay").map(l => l.id));
|
|
1053
|
+
const heatmapIds = new Set(snapshot.layers.filter(l => l.layerType === "heatmap").map(l => l.id));
|
|
1008
1054
|
for (const layer of snapshot.layers) {
|
|
1009
1055
|
switch (layer.layerType) {
|
|
1010
1056
|
case "markers":
|
|
@@ -1019,6 +1065,9 @@ class MapboxLayerPort {
|
|
|
1019
1065
|
case "image-overlay":
|
|
1020
1066
|
this.syncImageOverlay(layer);
|
|
1021
1067
|
break;
|
|
1068
|
+
case "heatmap":
|
|
1069
|
+
this.syncHeatmap(layer);
|
|
1070
|
+
break;
|
|
1022
1071
|
default: {
|
|
1023
1072
|
const exhaustiveCheck = layer;
|
|
1024
1073
|
throw new Error(`Unknown layer type: ${String(exhaustiveCheck)}`);
|
|
@@ -1041,6 +1090,10 @@ class MapboxLayerPort {
|
|
|
1041
1090
|
if (!overlayIds.has(id))
|
|
1042
1091
|
this.removeImageOverlayInternal(id);
|
|
1043
1092
|
}
|
|
1093
|
+
for (const id of [...this.heatmapSources.keys()]) {
|
|
1094
|
+
if (!heatmapIds.has(id))
|
|
1095
|
+
this.removeHeatmapInternal(id);
|
|
1096
|
+
}
|
|
1044
1097
|
this.applyShapeSelectionState(snapshot.shapeSelection.handleId, snapshot.shapeSelection.featureId);
|
|
1045
1098
|
this.applyShapeHoverState(snapshot.shapeHover.handleId, snapshot.shapeHover.featureId);
|
|
1046
1099
|
}
|
|
@@ -2190,6 +2243,7 @@ class MapboxLayerPort {
|
|
|
2190
2243
|
const shapeEntries = Array.from(this.shapeSources.entries());
|
|
2191
2244
|
const routeEntries = Array.from(this.routeSources.entries());
|
|
2192
2245
|
const overlayEntries = Array.from(this.overlaySources.entries());
|
|
2246
|
+
const heatmapEntries = Array.from(this.heatmapSources.entries());
|
|
2193
2247
|
const markerEntriesToRestore = Array.from(this.markerSources.entries()).filter(([, tracked]) => tracked.sourceId !== undefined);
|
|
2194
2248
|
// Re-add shape sources/layers
|
|
2195
2249
|
for (const [id, { config }] of shapeEntries) {
|
|
@@ -2215,6 +2269,11 @@ class MapboxLayerPort {
|
|
|
2215
2269
|
this.overlaySources.delete(id);
|
|
2216
2270
|
this.syncImageOverlay(config);
|
|
2217
2271
|
}
|
|
2272
|
+
// Re-add heatmap sources/layers
|
|
2273
|
+
for (const [id, { config }] of heatmapEntries) {
|
|
2274
|
+
this.heatmapSources.delete(id);
|
|
2275
|
+
this.syncHeatmap(config);
|
|
2276
|
+
}
|
|
2218
2277
|
// DOM markers (mapboxgl.Marker) persist across style changes, but
|
|
2219
2278
|
// WebGL circle layers/sources are removed. Re-add marker sources
|
|
2220
2279
|
// that used circle layers (from pre-snapshot).
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map-adapter-mapbox",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.133",
|
|
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.16.
|
|
10
|
+
"@trackunit/react-map-adapter-shared": "0.0.145",
|
|
11
|
+
"@trackunit/geo-json-utils": "1.16.4",
|
|
12
12
|
"mapbox-gl": "^3.18.1",
|
|
13
13
|
"supercluster": "^8.0.1",
|
|
14
14
|
"es-toolkit": "^1.39.10",
|
package/src/MapboxLayerPort.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare class MapboxLayerPort implements LayerPort {
|
|
|
41
41
|
private readonly shapeSources;
|
|
42
42
|
private readonly routeSources;
|
|
43
43
|
private readonly overlaySources;
|
|
44
|
+
private readonly heatmapSources;
|
|
44
45
|
private pendingSnapshot;
|
|
45
46
|
private lastNonEmptySnapshot;
|
|
46
47
|
private markerPortalDescriptors;
|
|
@@ -108,6 +109,14 @@ export declare class MapboxLayerPort implements LayerPort {
|
|
|
108
109
|
private removeRouteSourceInternal;
|
|
109
110
|
private syncImageOverlay;
|
|
110
111
|
private removeImageOverlayInternal;
|
|
112
|
+
/**
|
|
113
|
+
* Native Mapbox GL heatmap layer (ADR-0029).
|
|
114
|
+
*
|
|
115
|
+
* - Source: `heatmap-source-{id}`, layer: `heatmap-layer-{id}`.
|
|
116
|
+
* - Restored on style reload via `reapplySourcesAfterStyleChange`.
|
|
117
|
+
*/
|
|
118
|
+
private syncHeatmap;
|
|
119
|
+
private removeHeatmapInternal;
|
|
111
120
|
/**
|
|
112
121
|
* Wrapper around `map.addLayer` that starts the layer at opacity 0 and
|
|
113
122
|
* animates to the target opacity via Mapbox's native paint transition system.
|
package/src/mapboxAdapter.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export declare const mapboxAdapter: (config: Readonly<Readonly<{
|
|
|
21
21
|
language?: string;
|
|
22
22
|
region?: string;
|
|
23
23
|
theme?: import("@trackunit/react-map-adapter-shared").MapTheme;
|
|
24
|
+
mapType?: import("@trackunit/react-map-adapter-shared").MapType;
|
|
25
|
+
showRoads?: boolean;
|
|
26
|
+
showTraffic?: boolean;
|
|
24
27
|
initialViewport?: import("@trackunit/react-map-adapter-shared").InitialViewport;
|
|
25
28
|
restrictBounds?: import("@trackunit/geo-json-utils").GeoJsonBbox | null;
|
|
26
29
|
}> & {
|
|
@@ -29,6 +32,9 @@ export declare const mapboxAdapter: (config: Readonly<Readonly<{
|
|
|
29
32
|
language?: string;
|
|
30
33
|
region?: string;
|
|
31
34
|
theme?: import("@trackunit/react-map-adapter-shared").MapTheme;
|
|
35
|
+
mapType?: import("@trackunit/react-map-adapter-shared").MapType;
|
|
36
|
+
showRoads?: boolean;
|
|
37
|
+
showTraffic?: boolean;
|
|
32
38
|
initialViewport?: import("@trackunit/react-map-adapter-shared").InitialViewport;
|
|
33
39
|
restrictBounds?: import("@trackunit/geo-json-utils").GeoJsonBbox | null;
|
|
34
40
|
}> & {
|