@trackunit/react-map-adapter-mapbox 0.0.131 → 0.0.132

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 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.131",
3
+ "version": "0.0.132",
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.143",
11
- "@trackunit/geo-json-utils": "1.16.2",
10
+ "@trackunit/react-map-adapter-shared": "0.0.144",
11
+ "@trackunit/geo-json-utils": "1.16.3",
12
12
  "mapbox-gl": "^3.18.1",
13
13
  "supercluster": "^8.0.1",
14
14
  "es-toolkit": "^1.39.10",
@@ -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.
@@ -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
  }> & {