@trackunit/react-map-adapter-google 0.0.126 → 0.0.128
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 +98 -9
- package/index.esm.js +100 -11
- package/package.json +4 -4
- package/src/GoogleMapsAdapterInstance.d.ts +28 -1
package/index.cjs.js
CHANGED
|
@@ -1652,8 +1652,16 @@ class GoogleMapsLayerPort {
|
|
|
1652
1652
|
const canvas = this.canvas;
|
|
1653
1653
|
if (canvas === null)
|
|
1654
1654
|
return;
|
|
1655
|
+
// Read viewport state from the map this overlay is CURRENTLY attached to.
|
|
1656
|
+
// reconnectAllSources() re-parents the overlay onto recreated map instances
|
|
1657
|
+
// (theme or renderingType changes); the closed-over `map` parameter keeps
|
|
1658
|
+
// pointing at the old, detached instance, whose bounds no longer move —
|
|
1659
|
+
// which froze canvas dots on screen until a data refresh rebuilt the overlay.
|
|
1660
|
+
const attachedMap = this.getMap();
|
|
1661
|
+
if (attachedMap === null || !("getBounds" in attachedMap))
|
|
1662
|
+
return;
|
|
1655
1663
|
const projection = this.getProjection();
|
|
1656
|
-
const bounds =
|
|
1664
|
+
const bounds = attachedMap.getBounds();
|
|
1657
1665
|
const pad = CANVAS_VIEWPORT_PADDING;
|
|
1658
1666
|
const dpr = window.devicePixelRatio || 1;
|
|
1659
1667
|
let originX;
|
|
@@ -2570,6 +2578,17 @@ class GoogleMapsAdapterInstance {
|
|
|
2570
2578
|
/** Throttled pointermove channel shared by map mousemove and shape overlay mousemove. */
|
|
2571
2579
|
this.pointerMoveRaf = null;
|
|
2572
2580
|
this.lastPointerPosition = null;
|
|
2581
|
+
/**
|
|
2582
|
+
* Traffic overlay, created lazily in syncTrafficLayer() because the google
|
|
2583
|
+
* global only exists once the Maps JS API has loaded.
|
|
2584
|
+
*/
|
|
2585
|
+
this.trafficLayer = null;
|
|
2586
|
+
/**
|
|
2587
|
+
* Rendering type of the currently connected map, as declared by the renderer
|
|
2588
|
+
* at connect() time. The traffic layer only ever attaches to RASTER maps
|
|
2589
|
+
* (ADR 0028: attaching it to a vector map silently degrades the renderer).
|
|
2590
|
+
*/
|
|
2591
|
+
this.connectedRenderingType = "VECTOR";
|
|
2573
2592
|
this.appearance = { ...reactMapAdapterShared.DEFAULT_MAP_APPEARANCE, theme: config.theme ?? "light" };
|
|
2574
2593
|
this.layers = new GoogleMapsLayerPort();
|
|
2575
2594
|
this.layers.setTheme(this.appearance.theme);
|
|
@@ -2598,7 +2617,7 @@ class GoogleMapsAdapterInstance {
|
|
|
2598
2617
|
* On reconnection the previous center, zoom, and map type are restored so
|
|
2599
2618
|
* the user doesn't see the viewport jump.
|
|
2600
2619
|
*/
|
|
2601
|
-
connect(map) {
|
|
2620
|
+
connect(map, renderingType = "VECTOR") {
|
|
2602
2621
|
// React 18 StrictMode (dev) runs effect cleanups between the first and second mount, so
|
|
2603
2622
|
// `useMap`'s destroy cleanup fires before the real connect. Allow `connect` to revive a
|
|
2604
2623
|
// destroyed adapter so the dev cycle self-heals; production unmounts never re-mount.
|
|
@@ -2616,7 +2635,16 @@ class GoogleMapsAdapterInstance {
|
|
|
2616
2635
|
this.removeGoogleEventListeners();
|
|
2617
2636
|
}
|
|
2618
2637
|
this.map = map;
|
|
2638
|
+
this.connectedRenderingType = renderingType;
|
|
2639
|
+
// Re-derive the status appearance from the tracked appearance: destroy() resets
|
|
2640
|
+
// `state` to defaults while `this.appearance` keeps the real values, so a
|
|
2641
|
+
// StrictMode destroy → revive would otherwise report stale defaults, and the
|
|
2642
|
+
// appearance controls' sync-back would overwrite persisted preferences with them.
|
|
2643
|
+
this.state = { ...this.state, appearance: this.appearance };
|
|
2644
|
+
this.updateStatusCache();
|
|
2619
2645
|
this.layers.connect(map);
|
|
2646
|
+
// Theme changes recreate the google.maps.Map, so traffic must re-attach on every (re)connect
|
|
2647
|
+
this.syncTrafficLayer();
|
|
2620
2648
|
this.setupEventListeners();
|
|
2621
2649
|
this.layers.setPointerMoveReporter(position => {
|
|
2622
2650
|
this.reportPointerMove(position);
|
|
@@ -2666,6 +2694,16 @@ class GoogleMapsAdapterInstance {
|
|
|
2666
2694
|
getTheme() {
|
|
2667
2695
|
return this.appearance.theme;
|
|
2668
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Returns the current traffic preference.
|
|
2699
|
+
* Used by GoogleMapsRenderer (via useSyncExternalStore) to pick the map's
|
|
2700
|
+
* renderingType: TrafficLayer silently forces a vector map into raster
|
|
2701
|
+
* fallback and orphans OverlayViews, so the renderer instead recreates the
|
|
2702
|
+
* map with an explicit RASTER renderingType while traffic is on.
|
|
2703
|
+
*/
|
|
2704
|
+
getShowTraffic() {
|
|
2705
|
+
return this.appearance.showTraffic === true;
|
|
2706
|
+
}
|
|
2669
2707
|
async setCenter(center) {
|
|
2670
2708
|
const validCenter = geoJsonUtils.validatePosition(center, "[setCenter]");
|
|
2671
2709
|
if (validCenter === null)
|
|
@@ -2785,6 +2823,15 @@ class GoogleMapsAdapterInstance {
|
|
|
2785
2823
|
currentMap.setMapTypeId(this.mapTypeToGoogleMapTypeId(this.appearance.mapType, showRoads));
|
|
2786
2824
|
}
|
|
2787
2825
|
}
|
|
2826
|
+
async setShowTraffic(showTraffic) {
|
|
2827
|
+
if (this.appearance.showTraffic === showTraffic)
|
|
2828
|
+
return;
|
|
2829
|
+
this.appearance = { ...this.appearance, showTraffic };
|
|
2830
|
+
this.state = { ...this.state, appearance: this.appearance };
|
|
2831
|
+
this.updateStatusCache();
|
|
2832
|
+
this.notifyStatusListeners();
|
|
2833
|
+
this.syncTrafficLayer();
|
|
2834
|
+
}
|
|
2788
2835
|
on(event, handler) {
|
|
2789
2836
|
let listeners = this.eventListeners.get(event);
|
|
2790
2837
|
if (listeners === undefined) {
|
|
@@ -2820,6 +2867,8 @@ class GoogleMapsAdapterInstance {
|
|
|
2820
2867
|
}
|
|
2821
2868
|
this.isDestroyedFlag = true;
|
|
2822
2869
|
this.removeGoogleEventListeners();
|
|
2870
|
+
this.trafficLayer?.setMap(null);
|
|
2871
|
+
this.trafficLayer = null;
|
|
2823
2872
|
this.layers.setPointerMoveReporter(null);
|
|
2824
2873
|
this.layers.destroy();
|
|
2825
2874
|
// Clear our listeners
|
|
@@ -2835,6 +2884,30 @@ class GoogleMapsAdapterInstance {
|
|
|
2835
2884
|
// ============================================================================
|
|
2836
2885
|
// Private methods
|
|
2837
2886
|
// ============================================================================
|
|
2887
|
+
/**
|
|
2888
|
+
* Attach or detach the traffic overlay so it matches the current preference
|
|
2889
|
+
* and zoom. Runs on setShowTraffic, on (re)connect (theme changes recreate
|
|
2890
|
+
* the google.maps.Map), and on zoom changes. Google natively suppresses
|
|
2891
|
+
* traffic on plain satellite, so no mapType check is needed here.
|
|
2892
|
+
*/
|
|
2893
|
+
syncTrafficLayer() {
|
|
2894
|
+
const currentMap = this.map;
|
|
2895
|
+
if (currentMap === null)
|
|
2896
|
+
return;
|
|
2897
|
+
const zoom = currentMap.getZoom() ?? this.state.zoom;
|
|
2898
|
+
// TrafficLayer must never touch a vector map — it silently degrades the renderer
|
|
2899
|
+
// (ADR 0028). When the preference turns on, the attached map is briefly still the
|
|
2900
|
+
// vector instance the renderer is about to replace; skip it, and the connect()
|
|
2901
|
+
// that follows the raster recreation attaches on the right map.
|
|
2902
|
+
const isRasterMap = this.connectedRenderingType === "RASTER";
|
|
2903
|
+
const targetMap = isRasterMap && this.appearance.showTraffic === true && zoom >= reactMapAdapterShared.TRAFFIC_MIN_ZOOM ? currentMap : null;
|
|
2904
|
+
if (this.trafficLayer === null && targetMap === null)
|
|
2905
|
+
return;
|
|
2906
|
+
this.trafficLayer ?? (this.trafficLayer = new google.maps.TrafficLayer());
|
|
2907
|
+
if (this.trafficLayer.getMap() !== targetMap) {
|
|
2908
|
+
this.trafficLayer.setMap(targetMap);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2838
2911
|
/** Detach all Google Maps native event listeners without clearing our own subscribers */
|
|
2839
2912
|
removeGoogleEventListeners() {
|
|
2840
2913
|
this.googleEventListeners.forEach(listener => {
|
|
@@ -2882,6 +2955,8 @@ class GoogleMapsAdapterInstance {
|
|
|
2882
2955
|
// Wheel / control zoom also fires bounds_changed but not dragstart
|
|
2883
2956
|
this.googleEventListeners.push(currentMap.addListener("zoom_changed", () => {
|
|
2884
2957
|
markCameraActive();
|
|
2958
|
+
// Traffic hides below TRAFFIC_MIN_ZOOM; cheap no-op unless the threshold is crossed
|
|
2959
|
+
this.syncTrafficLayer();
|
|
2885
2960
|
}));
|
|
2886
2961
|
// Bounds changed (during move) — keep isIdle false until the idle event
|
|
2887
2962
|
this.googleEventListeners.push(currentMap.addListener("bounds_changed", () => {
|
|
@@ -2987,7 +3062,7 @@ const createGoogleMapsInstance = (config) => {
|
|
|
2987
3062
|
* Internal component that renders the actual Google Map
|
|
2988
3063
|
* Must be inside APIProvider
|
|
2989
3064
|
*/
|
|
2990
|
-
const GoogleMapInternal = ({ adapterInstance, children, className, style, theme, mapInstanceId, initialViewport, restrictBounds, }) => {
|
|
3065
|
+
const GoogleMapInternal = ({ adapterInstance, children, className, style, theme, renderingType, mapInstanceId, initialViewport, restrictBounds, }) => {
|
|
2991
3066
|
const map = reactGoogleMaps.useMap(mapInstanceId);
|
|
2992
3067
|
const loadingStatus = reactGoogleMaps.useApiLoadingStatus();
|
|
2993
3068
|
const restrictionBbox = react.useMemo(() => reactMapAdapterShared.getEffectiveRestrictBounds(restrictBounds), [restrictBounds]);
|
|
@@ -3015,9 +3090,11 @@ const GoogleMapInternal = ({ adapterInstance, children, className, style, theme,
|
|
|
3015
3090
|
return;
|
|
3016
3091
|
}
|
|
3017
3092
|
if (map !== null && adapterInstance instanceof GoogleMapsAdapterInstance) {
|
|
3018
|
-
|
|
3093
|
+
// Declare the rendering type so the adapter only ever attaches the traffic
|
|
3094
|
+
// layer to RASTER maps (ADR 0028)
|
|
3095
|
+
adapterInstance.connect(map, renderingType === reactGoogleMaps.RenderingType.RASTER ? "RASTER" : "VECTOR");
|
|
3019
3096
|
}
|
|
3020
|
-
}, [loadingStatus, map, adapterInstance]);
|
|
3097
|
+
}, [loadingStatus, map, adapterInstance, renderingType]);
|
|
3021
3098
|
react.useEffect(() => {
|
|
3022
3099
|
if (loadingStatus !== reactGoogleMaps.APILoadingStatus.FAILED) {
|
|
3023
3100
|
return;
|
|
@@ -3038,7 +3115,9 @@ const GoogleMapInternal = ({ adapterInstance, children, className, style, theme,
|
|
|
3038
3115
|
},
|
|
3039
3116
|
}
|
|
3040
3117
|
: {};
|
|
3041
|
-
return (jsxRuntime.jsx(reactGoogleMaps.Map, { className: className, clickableIcons: false, disableDefaultUI: true, draggableCursor: reactMapAdapterShared.MAP_CURSORS.default, gestureHandling: reactDeviceDetect.isDesktop ? "greedy" : "cooperative", id: mapInstanceId,
|
|
3118
|
+
return (jsxRuntime.jsx(reactGoogleMaps.Map, { className: className, clickableIcons: false, disableDefaultUI: true, draggableCursor: reactMapAdapterShared.MAP_CURSORS.default, gestureHandling: reactDeviceDetect.isDesktop ? "greedy" : "cooperative", id: mapInstanceId,
|
|
3119
|
+
// Fractional zoom is a vector-renderer feature; raster snaps to integer zooms
|
|
3120
|
+
isFractionalZoomEnabled: renderingType === reactGoogleMaps.RenderingType.VECTOR, mapId: GOOGLE_MAP_IDS[theme], renderingType: renderingType, style: { width: "100%", height: "100%", ...style }, ...restrictionProps, ...mapProps, children: children }));
|
|
3042
3121
|
};
|
|
3043
3122
|
/**
|
|
3044
3123
|
* Google Maps Renderer component
|
|
@@ -3065,9 +3144,19 @@ const GoogleMapsRenderer = (props) => {
|
|
|
3065
3144
|
// Subscribe to the adapter's current theme reactively.
|
|
3066
3145
|
// When setTheme() updates currentTheme and notifies listeners, this
|
|
3067
3146
|
// causes a re-render so the <GoogleMap> receives the updated mapId.
|
|
3068
|
-
const
|
|
3147
|
+
const subscribeToStatus = react.useCallback((onStoreChange) => adapterInstance.subscribeStatus(onStoreChange), [adapterInstance]);
|
|
3069
3148
|
const getThemeSnapshot = react.useCallback(() => adapterInstance.getTheme(), [adapterInstance]);
|
|
3070
|
-
const currentTheme = react.useSyncExternalStore(
|
|
3149
|
+
const currentTheme = react.useSyncExternalStore(subscribeToStatus, getThemeSnapshot, getThemeSnapshot);
|
|
3150
|
+
// Subscribe to the traffic preference the same way. TrafficLayer is not
|
|
3151
|
+
// supported by the vector renderer: attaching it silently drops the map into
|
|
3152
|
+
// raster fallback and orphans mounted OverlayViews (frozen markers). Instead
|
|
3153
|
+
// of that implicit half-state, the map is recreated with an explicit
|
|
3154
|
+
// renderingType — RASTER while traffic is on, VECTOR when it is off. vis.gl
|
|
3155
|
+
// treats renderingType like mapId: a change creates a fresh google.maps.Map
|
|
3156
|
+
// (camera preserved), and connect() re-attaches layers and traffic.
|
|
3157
|
+
const getShowTrafficSnapshot = react.useCallback(() => adapterInstance.getShowTraffic(), [adapterInstance]);
|
|
3158
|
+
const showTraffic = react.useSyncExternalStore(subscribeToStatus, getShowTrafficSnapshot, getShowTrafficSnapshot);
|
|
3159
|
+
const renderingType = showTraffic ? reactGoogleMaps.RenderingType.RASTER : reactGoogleMaps.RenderingType.VECTOR;
|
|
3071
3160
|
// "marker" is required for AdvancedMarkerElement used by the layer port.
|
|
3072
3161
|
// using satisfies to avoid type errors from the library while maintaining type safety
|
|
3073
3162
|
const libraries = react.useMemo(() => ["marker"], []);
|
|
@@ -3080,7 +3169,7 @@ const GoogleMapsRenderer = (props) => {
|
|
|
3080
3169
|
// initializations that cause zombie google.maps.Map instances.
|
|
3081
3170
|
const existingContext = react.useContext(reactGoogleMaps.APIProviderContext);
|
|
3082
3171
|
const hasExternalProvider = existingContext !== null;
|
|
3083
|
-
const internalContent = (jsxRuntime.jsx(GoogleMapInternal, { ...props, initialViewport: initialViewport, mapInstanceId: mapInstanceId, restrictBounds: restrictBounds, theme: currentTheme }));
|
|
3172
|
+
const internalContent = (jsxRuntime.jsx(GoogleMapInternal, { ...props, initialViewport: initialViewport, mapInstanceId: mapInstanceId, renderingType: renderingType, restrictBounds: restrictBounds, theme: currentTheme }));
|
|
3084
3173
|
if (hasExternalProvider) {
|
|
3085
3174
|
return internalContent;
|
|
3086
3175
|
}
|
package/index.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { canPatchAdaptiveMarker, canPatchAdaptiveViewport, canPatchMarkerInPlace, extractPointCoordinates, mergeAntimeridianFeatures, extractPolygonPaths, extractLineCoordinates, patchPortalDescriptors, collectFeatureIdSet, removeGoneIndexedMarkers, getAdaptiveDomFeatureIds, extractSourceData, buildAdaptiveDomRenderFn, resolveCircleSymbolDefaults, resolveSymbolDescriptor, buildAdaptiveSymbolStyleFn, createSymbolDotElement, anchorFromBottomCenter, createDefaultClusterElement, createClusterPinElement, fadeInElement, buildAdaptiveDomEntries, MAP_CURSORS, resolveSelectedStyle, resolveHoveredStyle, attachSafeAreaHoverListeners, INITIAL_MAP_STATE, DEFAULT_MAP_APPEARANCE, computeInitialState, getEffectiveRestrictBounds, cameraStateEquals, isEventOfType, DEFAULT_ZOOM, DEFAULT_CENTER, defineAdapter } from '@trackunit/react-map-adapter-shared';
|
|
1
|
+
import { canPatchAdaptiveMarker, canPatchAdaptiveViewport, canPatchMarkerInPlace, extractPointCoordinates, mergeAntimeridianFeatures, extractPolygonPaths, extractLineCoordinates, patchPortalDescriptors, collectFeatureIdSet, removeGoneIndexedMarkers, getAdaptiveDomFeatureIds, extractSourceData, buildAdaptiveDomRenderFn, resolveCircleSymbolDefaults, resolveSymbolDescriptor, buildAdaptiveSymbolStyleFn, createSymbolDotElement, anchorFromBottomCenter, createDefaultClusterElement, createClusterPinElement, fadeInElement, buildAdaptiveDomEntries, MAP_CURSORS, resolveSelectedStyle, resolveHoveredStyle, attachSafeAreaHoverListeners, INITIAL_MAP_STATE, DEFAULT_MAP_APPEARANCE, computeInitialState, getEffectiveRestrictBounds, TRAFFIC_MIN_ZOOM, cameraStateEquals, isEventOfType, DEFAULT_ZOOM, DEFAULT_CENTER, defineAdapter } from '@trackunit/react-map-adapter-shared';
|
|
2
2
|
export { googleMapCspDirectives } from '@trackunit/react-map-adapter-shared';
|
|
3
3
|
import { jsx } from 'react/jsx-runtime';
|
|
4
|
-
import { APIProvider, APIProviderContext, useMap, useApiLoadingStatus, APILoadingStatus, Map as Map$1 } from '@vis.gl/react-google-maps';
|
|
4
|
+
import { APIProvider, RenderingType, APIProviderContext, useMap, useApiLoadingStatus, APILoadingStatus, Map as Map$1 } from '@vis.gl/react-google-maps';
|
|
5
5
|
import { useMemo, useCallback, useSyncExternalStore, useId, useContext, useEffect } from 'react';
|
|
6
6
|
import { validateBbox, validatePosition } from '@trackunit/geo-json-utils';
|
|
7
7
|
import { MarkerClusterer, SuperClusterAlgorithm } from '@googlemaps/markerclusterer';
|
|
@@ -1651,8 +1651,16 @@ class GoogleMapsLayerPort {
|
|
|
1651
1651
|
const canvas = this.canvas;
|
|
1652
1652
|
if (canvas === null)
|
|
1653
1653
|
return;
|
|
1654
|
+
// Read viewport state from the map this overlay is CURRENTLY attached to.
|
|
1655
|
+
// reconnectAllSources() re-parents the overlay onto recreated map instances
|
|
1656
|
+
// (theme or renderingType changes); the closed-over `map` parameter keeps
|
|
1657
|
+
// pointing at the old, detached instance, whose bounds no longer move —
|
|
1658
|
+
// which froze canvas dots on screen until a data refresh rebuilt the overlay.
|
|
1659
|
+
const attachedMap = this.getMap();
|
|
1660
|
+
if (attachedMap === null || !("getBounds" in attachedMap))
|
|
1661
|
+
return;
|
|
1654
1662
|
const projection = this.getProjection();
|
|
1655
|
-
const bounds =
|
|
1663
|
+
const bounds = attachedMap.getBounds();
|
|
1656
1664
|
const pad = CANVAS_VIEWPORT_PADDING;
|
|
1657
1665
|
const dpr = window.devicePixelRatio || 1;
|
|
1658
1666
|
let originX;
|
|
@@ -2569,6 +2577,17 @@ class GoogleMapsAdapterInstance {
|
|
|
2569
2577
|
/** Throttled pointermove channel shared by map mousemove and shape overlay mousemove. */
|
|
2570
2578
|
this.pointerMoveRaf = null;
|
|
2571
2579
|
this.lastPointerPosition = null;
|
|
2580
|
+
/**
|
|
2581
|
+
* Traffic overlay, created lazily in syncTrafficLayer() because the google
|
|
2582
|
+
* global only exists once the Maps JS API has loaded.
|
|
2583
|
+
*/
|
|
2584
|
+
this.trafficLayer = null;
|
|
2585
|
+
/**
|
|
2586
|
+
* Rendering type of the currently connected map, as declared by the renderer
|
|
2587
|
+
* at connect() time. The traffic layer only ever attaches to RASTER maps
|
|
2588
|
+
* (ADR 0028: attaching it to a vector map silently degrades the renderer).
|
|
2589
|
+
*/
|
|
2590
|
+
this.connectedRenderingType = "VECTOR";
|
|
2572
2591
|
this.appearance = { ...DEFAULT_MAP_APPEARANCE, theme: config.theme ?? "light" };
|
|
2573
2592
|
this.layers = new GoogleMapsLayerPort();
|
|
2574
2593
|
this.layers.setTheme(this.appearance.theme);
|
|
@@ -2597,7 +2616,7 @@ class GoogleMapsAdapterInstance {
|
|
|
2597
2616
|
* On reconnection the previous center, zoom, and map type are restored so
|
|
2598
2617
|
* the user doesn't see the viewport jump.
|
|
2599
2618
|
*/
|
|
2600
|
-
connect(map) {
|
|
2619
|
+
connect(map, renderingType = "VECTOR") {
|
|
2601
2620
|
// React 18 StrictMode (dev) runs effect cleanups between the first and second mount, so
|
|
2602
2621
|
// `useMap`'s destroy cleanup fires before the real connect. Allow `connect` to revive a
|
|
2603
2622
|
// destroyed adapter so the dev cycle self-heals; production unmounts never re-mount.
|
|
@@ -2615,7 +2634,16 @@ class GoogleMapsAdapterInstance {
|
|
|
2615
2634
|
this.removeGoogleEventListeners();
|
|
2616
2635
|
}
|
|
2617
2636
|
this.map = map;
|
|
2637
|
+
this.connectedRenderingType = renderingType;
|
|
2638
|
+
// Re-derive the status appearance from the tracked appearance: destroy() resets
|
|
2639
|
+
// `state` to defaults while `this.appearance` keeps the real values, so a
|
|
2640
|
+
// StrictMode destroy → revive would otherwise report stale defaults, and the
|
|
2641
|
+
// appearance controls' sync-back would overwrite persisted preferences with them.
|
|
2642
|
+
this.state = { ...this.state, appearance: this.appearance };
|
|
2643
|
+
this.updateStatusCache();
|
|
2618
2644
|
this.layers.connect(map);
|
|
2645
|
+
// Theme changes recreate the google.maps.Map, so traffic must re-attach on every (re)connect
|
|
2646
|
+
this.syncTrafficLayer();
|
|
2619
2647
|
this.setupEventListeners();
|
|
2620
2648
|
this.layers.setPointerMoveReporter(position => {
|
|
2621
2649
|
this.reportPointerMove(position);
|
|
@@ -2665,6 +2693,16 @@ class GoogleMapsAdapterInstance {
|
|
|
2665
2693
|
getTheme() {
|
|
2666
2694
|
return this.appearance.theme;
|
|
2667
2695
|
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Returns the current traffic preference.
|
|
2698
|
+
* Used by GoogleMapsRenderer (via useSyncExternalStore) to pick the map's
|
|
2699
|
+
* renderingType: TrafficLayer silently forces a vector map into raster
|
|
2700
|
+
* fallback and orphans OverlayViews, so the renderer instead recreates the
|
|
2701
|
+
* map with an explicit RASTER renderingType while traffic is on.
|
|
2702
|
+
*/
|
|
2703
|
+
getShowTraffic() {
|
|
2704
|
+
return this.appearance.showTraffic === true;
|
|
2705
|
+
}
|
|
2668
2706
|
async setCenter(center) {
|
|
2669
2707
|
const validCenter = validatePosition(center, "[setCenter]");
|
|
2670
2708
|
if (validCenter === null)
|
|
@@ -2784,6 +2822,15 @@ class GoogleMapsAdapterInstance {
|
|
|
2784
2822
|
currentMap.setMapTypeId(this.mapTypeToGoogleMapTypeId(this.appearance.mapType, showRoads));
|
|
2785
2823
|
}
|
|
2786
2824
|
}
|
|
2825
|
+
async setShowTraffic(showTraffic) {
|
|
2826
|
+
if (this.appearance.showTraffic === showTraffic)
|
|
2827
|
+
return;
|
|
2828
|
+
this.appearance = { ...this.appearance, showTraffic };
|
|
2829
|
+
this.state = { ...this.state, appearance: this.appearance };
|
|
2830
|
+
this.updateStatusCache();
|
|
2831
|
+
this.notifyStatusListeners();
|
|
2832
|
+
this.syncTrafficLayer();
|
|
2833
|
+
}
|
|
2787
2834
|
on(event, handler) {
|
|
2788
2835
|
let listeners = this.eventListeners.get(event);
|
|
2789
2836
|
if (listeners === undefined) {
|
|
@@ -2819,6 +2866,8 @@ class GoogleMapsAdapterInstance {
|
|
|
2819
2866
|
}
|
|
2820
2867
|
this.isDestroyedFlag = true;
|
|
2821
2868
|
this.removeGoogleEventListeners();
|
|
2869
|
+
this.trafficLayer?.setMap(null);
|
|
2870
|
+
this.trafficLayer = null;
|
|
2822
2871
|
this.layers.setPointerMoveReporter(null);
|
|
2823
2872
|
this.layers.destroy();
|
|
2824
2873
|
// Clear our listeners
|
|
@@ -2834,6 +2883,30 @@ class GoogleMapsAdapterInstance {
|
|
|
2834
2883
|
// ============================================================================
|
|
2835
2884
|
// Private methods
|
|
2836
2885
|
// ============================================================================
|
|
2886
|
+
/**
|
|
2887
|
+
* Attach or detach the traffic overlay so it matches the current preference
|
|
2888
|
+
* and zoom. Runs on setShowTraffic, on (re)connect (theme changes recreate
|
|
2889
|
+
* the google.maps.Map), and on zoom changes. Google natively suppresses
|
|
2890
|
+
* traffic on plain satellite, so no mapType check is needed here.
|
|
2891
|
+
*/
|
|
2892
|
+
syncTrafficLayer() {
|
|
2893
|
+
const currentMap = this.map;
|
|
2894
|
+
if (currentMap === null)
|
|
2895
|
+
return;
|
|
2896
|
+
const zoom = currentMap.getZoom() ?? this.state.zoom;
|
|
2897
|
+
// TrafficLayer must never touch a vector map — it silently degrades the renderer
|
|
2898
|
+
// (ADR 0028). When the preference turns on, the attached map is briefly still the
|
|
2899
|
+
// vector instance the renderer is about to replace; skip it, and the connect()
|
|
2900
|
+
// that follows the raster recreation attaches on the right map.
|
|
2901
|
+
const isRasterMap = this.connectedRenderingType === "RASTER";
|
|
2902
|
+
const targetMap = isRasterMap && this.appearance.showTraffic === true && zoom >= TRAFFIC_MIN_ZOOM ? currentMap : null;
|
|
2903
|
+
if (this.trafficLayer === null && targetMap === null)
|
|
2904
|
+
return;
|
|
2905
|
+
this.trafficLayer ?? (this.trafficLayer = new google.maps.TrafficLayer());
|
|
2906
|
+
if (this.trafficLayer.getMap() !== targetMap) {
|
|
2907
|
+
this.trafficLayer.setMap(targetMap);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2837
2910
|
/** Detach all Google Maps native event listeners without clearing our own subscribers */
|
|
2838
2911
|
removeGoogleEventListeners() {
|
|
2839
2912
|
this.googleEventListeners.forEach(listener => {
|
|
@@ -2881,6 +2954,8 @@ class GoogleMapsAdapterInstance {
|
|
|
2881
2954
|
// Wheel / control zoom also fires bounds_changed but not dragstart
|
|
2882
2955
|
this.googleEventListeners.push(currentMap.addListener("zoom_changed", () => {
|
|
2883
2956
|
markCameraActive();
|
|
2957
|
+
// Traffic hides below TRAFFIC_MIN_ZOOM; cheap no-op unless the threshold is crossed
|
|
2958
|
+
this.syncTrafficLayer();
|
|
2884
2959
|
}));
|
|
2885
2960
|
// Bounds changed (during move) — keep isIdle false until the idle event
|
|
2886
2961
|
this.googleEventListeners.push(currentMap.addListener("bounds_changed", () => {
|
|
@@ -2986,7 +3061,7 @@ const createGoogleMapsInstance = (config) => {
|
|
|
2986
3061
|
* Internal component that renders the actual Google Map
|
|
2987
3062
|
* Must be inside APIProvider
|
|
2988
3063
|
*/
|
|
2989
|
-
const GoogleMapInternal = ({ adapterInstance, children, className, style, theme, mapInstanceId, initialViewport, restrictBounds, }) => {
|
|
3064
|
+
const GoogleMapInternal = ({ adapterInstance, children, className, style, theme, renderingType, mapInstanceId, initialViewport, restrictBounds, }) => {
|
|
2990
3065
|
const map = useMap(mapInstanceId);
|
|
2991
3066
|
const loadingStatus = useApiLoadingStatus();
|
|
2992
3067
|
const restrictionBbox = useMemo(() => getEffectiveRestrictBounds(restrictBounds), [restrictBounds]);
|
|
@@ -3014,9 +3089,11 @@ const GoogleMapInternal = ({ adapterInstance, children, className, style, theme,
|
|
|
3014
3089
|
return;
|
|
3015
3090
|
}
|
|
3016
3091
|
if (map !== null && adapterInstance instanceof GoogleMapsAdapterInstance) {
|
|
3017
|
-
|
|
3092
|
+
// Declare the rendering type so the adapter only ever attaches the traffic
|
|
3093
|
+
// layer to RASTER maps (ADR 0028)
|
|
3094
|
+
adapterInstance.connect(map, renderingType === RenderingType.RASTER ? "RASTER" : "VECTOR");
|
|
3018
3095
|
}
|
|
3019
|
-
}, [loadingStatus, map, adapterInstance]);
|
|
3096
|
+
}, [loadingStatus, map, adapterInstance, renderingType]);
|
|
3020
3097
|
useEffect(() => {
|
|
3021
3098
|
if (loadingStatus !== APILoadingStatus.FAILED) {
|
|
3022
3099
|
return;
|
|
@@ -3037,7 +3114,9 @@ const GoogleMapInternal = ({ adapterInstance, children, className, style, theme,
|
|
|
3037
3114
|
},
|
|
3038
3115
|
}
|
|
3039
3116
|
: {};
|
|
3040
|
-
return (jsx(Map$1, { className: className, clickableIcons: false, disableDefaultUI: true, draggableCursor: MAP_CURSORS.default, gestureHandling: isDesktop ? "greedy" : "cooperative", id: mapInstanceId,
|
|
3117
|
+
return (jsx(Map$1, { className: className, clickableIcons: false, disableDefaultUI: true, draggableCursor: MAP_CURSORS.default, gestureHandling: isDesktop ? "greedy" : "cooperative", id: mapInstanceId,
|
|
3118
|
+
// Fractional zoom is a vector-renderer feature; raster snaps to integer zooms
|
|
3119
|
+
isFractionalZoomEnabled: renderingType === RenderingType.VECTOR, mapId: GOOGLE_MAP_IDS[theme], renderingType: renderingType, style: { width: "100%", height: "100%", ...style }, ...restrictionProps, ...mapProps, children: children }));
|
|
3041
3120
|
};
|
|
3042
3121
|
/**
|
|
3043
3122
|
* Google Maps Renderer component
|
|
@@ -3064,9 +3143,19 @@ const GoogleMapsRenderer = (props) => {
|
|
|
3064
3143
|
// Subscribe to the adapter's current theme reactively.
|
|
3065
3144
|
// When setTheme() updates currentTheme and notifies listeners, this
|
|
3066
3145
|
// causes a re-render so the <GoogleMap> receives the updated mapId.
|
|
3067
|
-
const
|
|
3146
|
+
const subscribeToStatus = useCallback((onStoreChange) => adapterInstance.subscribeStatus(onStoreChange), [adapterInstance]);
|
|
3068
3147
|
const getThemeSnapshot = useCallback(() => adapterInstance.getTheme(), [adapterInstance]);
|
|
3069
|
-
const currentTheme = useSyncExternalStore(
|
|
3148
|
+
const currentTheme = useSyncExternalStore(subscribeToStatus, getThemeSnapshot, getThemeSnapshot);
|
|
3149
|
+
// Subscribe to the traffic preference the same way. TrafficLayer is not
|
|
3150
|
+
// supported by the vector renderer: attaching it silently drops the map into
|
|
3151
|
+
// raster fallback and orphans mounted OverlayViews (frozen markers). Instead
|
|
3152
|
+
// of that implicit half-state, the map is recreated with an explicit
|
|
3153
|
+
// renderingType — RASTER while traffic is on, VECTOR when it is off. vis.gl
|
|
3154
|
+
// treats renderingType like mapId: a change creates a fresh google.maps.Map
|
|
3155
|
+
// (camera preserved), and connect() re-attaches layers and traffic.
|
|
3156
|
+
const getShowTrafficSnapshot = useCallback(() => adapterInstance.getShowTraffic(), [adapterInstance]);
|
|
3157
|
+
const showTraffic = useSyncExternalStore(subscribeToStatus, getShowTrafficSnapshot, getShowTrafficSnapshot);
|
|
3158
|
+
const renderingType = showTraffic ? RenderingType.RASTER : RenderingType.VECTOR;
|
|
3070
3159
|
// "marker" is required for AdvancedMarkerElement used by the layer port.
|
|
3071
3160
|
// using satisfies to avoid type errors from the library while maintaining type safety
|
|
3072
3161
|
const libraries = useMemo(() => ["marker"], []);
|
|
@@ -3079,7 +3168,7 @@ const GoogleMapsRenderer = (props) => {
|
|
|
3079
3168
|
// initializations that cause zombie google.maps.Map instances.
|
|
3080
3169
|
const existingContext = useContext(APIProviderContext);
|
|
3081
3170
|
const hasExternalProvider = existingContext !== null;
|
|
3082
|
-
const internalContent = (jsx(GoogleMapInternal, { ...props, initialViewport: initialViewport, mapInstanceId: mapInstanceId, restrictBounds: restrictBounds, theme: currentTheme }));
|
|
3171
|
+
const internalContent = (jsx(GoogleMapInternal, { ...props, initialViewport: initialViewport, mapInstanceId: mapInstanceId, renderingType: renderingType, restrictBounds: restrictBounds, theme: currentTheme }));
|
|
3083
3172
|
if (hasExternalProvider) {
|
|
3084
3173
|
return internalContent;
|
|
3085
3174
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map-adapter-google",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.128",
|
|
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/react-map-color-utils": "0.0.
|
|
12
|
-
"@trackunit/geo-json-utils": "1.15.
|
|
10
|
+
"@trackunit/react-map-adapter-shared": "0.0.139",
|
|
11
|
+
"@trackunit/react-map-color-utils": "0.0.114",
|
|
12
|
+
"@trackunit/geo-json-utils": "1.15.54",
|
|
13
13
|
"@googlemaps/markerclusterer": "^2.6.2",
|
|
14
14
|
"@vis.gl/react-google-maps": "^1.7.1",
|
|
15
15
|
"es-toolkit": "^1.39.10",
|
|
@@ -35,6 +35,17 @@ export declare class GoogleMapsAdapterInstance implements AdapterInstance {
|
|
|
35
35
|
private lastPointerPosition;
|
|
36
36
|
/** Unified appearance state — theme, mapType, and showRoads in one place */
|
|
37
37
|
private appearance;
|
|
38
|
+
/**
|
|
39
|
+
* Traffic overlay, created lazily in syncTrafficLayer() because the google
|
|
40
|
+
* global only exists once the Maps JS API has loaded.
|
|
41
|
+
*/
|
|
42
|
+
private trafficLayer;
|
|
43
|
+
/**
|
|
44
|
+
* Rendering type of the currently connected map, as declared by the renderer
|
|
45
|
+
* at connect() time. The traffic layer only ever attaches to RASTER maps
|
|
46
|
+
* (ADR 0028: attaching it to a vector map silently degrades the renderer).
|
|
47
|
+
*/
|
|
48
|
+
private connectedRenderingType;
|
|
38
49
|
constructor(config: GoogleMapsConfig);
|
|
39
50
|
/**
|
|
40
51
|
* Connect to a Google Maps instance - called by GoogleMapsRenderer.
|
|
@@ -44,7 +55,7 @@ export declare class GoogleMapsAdapterInstance implements AdapterInstance {
|
|
|
44
55
|
* On reconnection the previous center, zoom, and map type are restored so
|
|
45
56
|
* the user doesn't see the viewport jump.
|
|
46
57
|
*/
|
|
47
|
-
connect(map: google.maps.Map): void;
|
|
58
|
+
connect(map: google.maps.Map, renderingType?: "RASTER" | "VECTOR"): void;
|
|
48
59
|
getConfig(): GoogleMapsConfig;
|
|
49
60
|
getCameraState(): CameraState;
|
|
50
61
|
getStatus(): MapStatus;
|
|
@@ -56,6 +67,14 @@ export declare class GoogleMapsAdapterInstance implements AdapterInstance {
|
|
|
56
67
|
* update the mapId prop on the <GoogleMap> component.
|
|
57
68
|
*/
|
|
58
69
|
getTheme(): MapTheme;
|
|
70
|
+
/**
|
|
71
|
+
* Returns the current traffic preference.
|
|
72
|
+
* Used by GoogleMapsRenderer (via useSyncExternalStore) to pick the map's
|
|
73
|
+
* renderingType: TrafficLayer silently forces a vector map into raster
|
|
74
|
+
* fallback and orphans OverlayViews, so the renderer instead recreates the
|
|
75
|
+
* map with an explicit RASTER renderingType while traffic is on.
|
|
76
|
+
*/
|
|
77
|
+
getShowTraffic(): boolean;
|
|
59
78
|
setCenter(center: GeoJsonPosition): Promise<void>;
|
|
60
79
|
setZoom(zoomLevel: number): Promise<void>;
|
|
61
80
|
zoomBy(delta: number): Promise<void>;
|
|
@@ -65,9 +84,17 @@ export declare class GoogleMapsAdapterInstance implements AdapterInstance {
|
|
|
65
84
|
setMapType(type: MapType): Promise<void>;
|
|
66
85
|
setTheme(theme: MapTheme): Promise<void>;
|
|
67
86
|
setShowRoads(showRoads: boolean): Promise<void>;
|
|
87
|
+
setShowTraffic(showTraffic: boolean): Promise<void>;
|
|
68
88
|
on<TEventType extends MapEvent["type"]>(event: TEventType, handler: MapEventHandler<TEventType>): () => void;
|
|
69
89
|
notifyInitializationFailed(): void;
|
|
70
90
|
destroy(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Attach or detach the traffic overlay so it matches the current preference
|
|
93
|
+
* and zoom. Runs on setShowTraffic, on (re)connect (theme changes recreate
|
|
94
|
+
* the google.maps.Map), and on zoom changes. Google natively suppresses
|
|
95
|
+
* traffic on plain satellite, so no mapType check is needed here.
|
|
96
|
+
*/
|
|
97
|
+
private syncTrafficLayer;
|
|
71
98
|
/** Detach all Google Maps native event listeners without clearing our own subscribers */
|
|
72
99
|
private removeGoogleEventListeners;
|
|
73
100
|
/** Emit a throttled pointermove event (map canvas + shape overlays). */
|