@trackunit/react-map 0.2.180-alpha-7fd06f36b49.0 → 0.2.183
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 +180 -26
- package/index.esm.js +178 -27
- package/package.json +11 -10
- package/src/appearance/appearanceMigrations.d.ts +9 -0
- package/src/appearance/appearanceTypes.d.ts +7 -1
- package/src/appearance/usePersistedAppearanceSeed.d.ts +51 -0
- package/src/controls/useDefaultControls.d.ts +13 -0
- package/src/core/__testHelpers.d.ts +2 -2
- package/src/index.d.ts +4 -1
- package/src/layers/types.d.ts +8 -3
- package/src/layers/useHeatmap/heatmapPocFixture.d.ts +7 -0
- package/src/layers/useHeatmap/useHeatmap.d.ts +31 -0
package/index.cjs.js
CHANGED
|
@@ -13,6 +13,7 @@ var esToolkit = require('es-toolkit');
|
|
|
13
13
|
var reactCoreHooks = require('@trackunit/react-core-hooks');
|
|
14
14
|
var reactFormComponents = require('@trackunit/react-form-components');
|
|
15
15
|
var zod = require('zod');
|
|
16
|
+
var sharedUtils = require('@trackunit/shared-utils');
|
|
16
17
|
var reactMinimalPieChart = require('react-minimal-pie-chart');
|
|
17
18
|
var uiDesignTokens = require('@trackunit/ui-design-tokens');
|
|
18
19
|
var geoJsonUtils = require('@trackunit/geo-json-utils');
|
|
@@ -2792,22 +2793,20 @@ const AppearancePreview = ({ adapterConfig, mapType, label, selected, onSelect,
|
|
|
2792
2793
|
* @internal
|
|
2793
2794
|
*/
|
|
2794
2795
|
const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme, currentMapType, onChange, }) => {
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
return (jsxRuntime.jsx(AppearancePreview, { adapterConfig: derivedConfig, "data-testid": `map-style-preview-${option.key}`, label: option.label, mapType: option.mapType, onSelect: () => onChange(option.theme, option.mapType), selected: option.theme === currentTheme && option.mapType === currentMapType }, option.key));
|
|
2810
|
-
}) }));
|
|
2796
|
+
const derivedOptions = react.useMemo(() => options.map(option => ({
|
|
2797
|
+
option,
|
|
2798
|
+
// Pin the full appearance per option: the main map's config may carry a
|
|
2799
|
+
// persisted seed (theme, mapType, roads, traffic — see
|
|
2800
|
+
// usePersistedAppearanceSeed), and a preview depicts exactly one
|
|
2801
|
+
// theme/mapType combination with no overlays.
|
|
2802
|
+
config: adapterConfig.derive({
|
|
2803
|
+
theme: option.theme,
|
|
2804
|
+
mapType: option.mapType,
|
|
2805
|
+
showRoads: false,
|
|
2806
|
+
showTraffic: false,
|
|
2807
|
+
}),
|
|
2808
|
+
})), [adapterConfig, options]);
|
|
2809
|
+
return (jsxRuntime.jsx("div", { "aria-label": ariaLabel, className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: derivedOptions.map(({ option, config }) => (jsxRuntime.jsx(AppearancePreview, { adapterConfig: config, "data-testid": `map-style-preview-${option.key}`, label: option.label, mapType: option.mapType, onSelect: () => onChange(option.theme, option.mapType), selected: option.theme === currentTheme && option.mapType === currentMapType }, option.key))) }));
|
|
2811
2810
|
};
|
|
2812
2811
|
|
|
2813
2812
|
// ============================================================================
|
|
@@ -2815,10 +2814,16 @@ const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme
|
|
|
2815
2814
|
// ============================================================================
|
|
2816
2815
|
/**
|
|
2817
2816
|
* Increment this number when the persisted appearance shape changes, and add a
|
|
2818
|
-
* matching migration step in
|
|
2817
|
+
* matching migration step in appearanceMigrations.ts so stored settings are
|
|
2819
2818
|
* upgraded instead of reset. (v2: added showTraffic.)
|
|
2820
2819
|
*/
|
|
2821
2820
|
const APPEARANCE_SETTINGS_VERSION = 2;
|
|
2821
|
+
/**
|
|
2822
|
+
* Default localStorage key for the persisted map appearance settings.
|
|
2823
|
+
* Shared by `useMapAppearanceControls` (owner) and `usePersistedAppearanceSeed`
|
|
2824
|
+
* (read-only) so both resolve the same stored preference by default.
|
|
2825
|
+
*/
|
|
2826
|
+
const DEFAULT_APPEARANCE_PERSISTENCE_KEY = "map-appearance-settings";
|
|
2822
2827
|
// ============================================================================
|
|
2823
2828
|
// Persisted Appearance Schema (extends core mapAppearanceSchema with version)
|
|
2824
2829
|
// ============================================================================
|
|
@@ -2853,8 +2858,6 @@ const appearanceReducer = (state, action) => {
|
|
|
2853
2858
|
}
|
|
2854
2859
|
};
|
|
2855
2860
|
|
|
2856
|
-
const DEFAULT_THEMES = ["light", "dark"];
|
|
2857
|
-
const DEFAULT_MAP_TYPES = ["roadmap", "satellite"];
|
|
2858
2861
|
/**
|
|
2859
2862
|
* The v1 persisted shape, frozen as a literal. Deliberately NOT derived from
|
|
2860
2863
|
* the live mapAppearanceSchema: deriving would make every future appearance
|
|
@@ -2890,6 +2893,20 @@ const appearanceMigrationV2 = {
|
|
|
2890
2893
|
: DEFAULT_PERSISTED_APPEARANCE;
|
|
2891
2894
|
},
|
|
2892
2895
|
};
|
|
2896
|
+
/**
|
|
2897
|
+
* Migration pipeline for the persisted map appearance settings.
|
|
2898
|
+
*
|
|
2899
|
+
* Shared by `useMapAppearanceControls` (the owning reader/writer) and
|
|
2900
|
+
* `usePersistedAppearanceSeed` (a read-only consumer), so both interpret
|
|
2901
|
+
* stored data — including pre-envelope and v1 shapes — identically.
|
|
2902
|
+
*/
|
|
2903
|
+
const APPEARANCE_MIGRATION = {
|
|
2904
|
+
version: APPEARANCE_SETTINGS_VERSION,
|
|
2905
|
+
steps: [appearanceMigrationV1, appearanceMigrationV2],
|
|
2906
|
+
};
|
|
2907
|
+
|
|
2908
|
+
const DEFAULT_THEMES = ["light", "dark"];
|
|
2909
|
+
const DEFAULT_MAP_TYPES = ["roadmap", "satellite"];
|
|
2893
2910
|
const themeToTranslationKey = (theme) => {
|
|
2894
2911
|
switch (theme) {
|
|
2895
2912
|
case "light":
|
|
@@ -2978,7 +2995,7 @@ const buildAppearanceOptions = (themes, mapTypes, t) => {
|
|
|
2978
2995
|
* <Controls api={api} controls={controlsConfig} />
|
|
2979
2996
|
* ```
|
|
2980
2997
|
*/
|
|
2981
|
-
const useMapAppearanceControls = ({ api, options: appearanceOptions, persistenceKey =
|
|
2998
|
+
const useMapAppearanceControls = ({ api, options: appearanceOptions, persistenceKey = DEFAULT_APPEARANCE_PERSISTENCE_KEY, }) => {
|
|
2982
2999
|
const [t] = useTranslation();
|
|
2983
3000
|
const themes = appearanceOptions?.themes ?? DEFAULT_THEMES;
|
|
2984
3001
|
const mapTypes = appearanceOptions?.mapTypes ?? DEFAULT_MAP_TYPES;
|
|
@@ -2993,7 +3010,7 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
2993
3010
|
const [localState, dispatch] = reactComponents.useLocalStorageReducer({
|
|
2994
3011
|
key: persistenceKey,
|
|
2995
3012
|
defaultState: DEFAULT_PERSISTED_APPEARANCE,
|
|
2996
|
-
migration:
|
|
3013
|
+
migration: APPEARANCE_MIGRATION,
|
|
2997
3014
|
reducer: appearanceReducer,
|
|
2998
3015
|
schema: persistedAppearanceSchema,
|
|
2999
3016
|
});
|
|
@@ -3241,7 +3258,11 @@ const useDefaultControls = (api, options) => {
|
|
|
3241
3258
|
"data-testid": "map-my-location-button",
|
|
3242
3259
|
}, [api.actions, getPosition, options?.navigation?.myLocation?.enabled, t]);
|
|
3243
3260
|
const navigationControls = react.useMemo(() => defineControlStack([myLocationControl, zoomControl, fullscreenControl]), [fullscreenControl, myLocationControl, zoomControl]);
|
|
3244
|
-
const mapAppearanceControls = useMapAppearanceControls({
|
|
3261
|
+
const mapAppearanceControls = useMapAppearanceControls({
|
|
3262
|
+
api,
|
|
3263
|
+
options: options?.settings?.mapStyle?.options,
|
|
3264
|
+
persistenceKey: options?.settings?.mapStyle?.persistenceKey,
|
|
3265
|
+
});
|
|
3245
3266
|
const mapStyleControl = react.useMemo(() => options?.settings?.mapStyle?.enabled === false ? undefined : mapAppearanceControls[0], [mapAppearanceControls, options?.settings?.mapStyle?.enabled]);
|
|
3246
3267
|
const settingsControls = react.useMemo(() => defineControlStack([mapStyleControl]), [mapStyleControl]);
|
|
3247
3268
|
return react.useMemo(() => ({
|
|
@@ -3362,6 +3383,57 @@ const useControls = (base, areas) => {
|
|
|
3362
3383
|
}, [base, areas]);
|
|
3363
3384
|
};
|
|
3364
3385
|
|
|
3386
|
+
/**
|
|
3387
|
+
* Reads the persisted map appearance once (on mount) so it can seed the adapter
|
|
3388
|
+
* factory, letting the FIRST map be constructed with the user's stored theme,
|
|
3389
|
+
* map type, and traffic preference instead of being corrected after mount.
|
|
3390
|
+
*
|
|
3391
|
+
* Without the seed, a stored dark theme or traffic-on preference only reaches
|
|
3392
|
+
* the adapter once the map is ready, forcing an immediate map recreation on
|
|
3393
|
+
* every page load: a visible blink, a wasted provider map construction (each
|
|
3394
|
+
* one is a billed Dynamic Maps load on Google), and overlays built on a
|
|
3395
|
+
* throwaway instance (ADR 0029).
|
|
3396
|
+
*
|
|
3397
|
+
* Read-only: `useMapAppearanceControls` keeps owning the persistence and pushes
|
|
3398
|
+
* all later changes through the adapter's appearance actions. The seed is
|
|
3399
|
+
* deliberately not reactive — it only matters at construction time.
|
|
3400
|
+
*
|
|
3401
|
+
* Storage being unavailable or corrupt is never an error: the seed then falls
|
|
3402
|
+
* back to the defaults, which is exactly the pre-seeding behavior.
|
|
3403
|
+
*
|
|
3404
|
+
* @example
|
|
3405
|
+
* ```tsx
|
|
3406
|
+
* const appearanceSeed = usePersistedAppearanceSeed();
|
|
3407
|
+
* const [Map, api] = useMap(googleMapsAdapter({ apiKey, ...appearanceSeed }));
|
|
3408
|
+
* ```
|
|
3409
|
+
*/
|
|
3410
|
+
const usePersistedAppearanceSeed = (params) => {
|
|
3411
|
+
const [seed] = react.useState(() => {
|
|
3412
|
+
let persisted = DEFAULT_PERSISTED_APPEARANCE;
|
|
3413
|
+
try {
|
|
3414
|
+
persisted = sharedUtils.readLocalStorageValue({
|
|
3415
|
+
key: params?.persistenceKey ?? DEFAULT_APPEARANCE_PERSISTENCE_KEY,
|
|
3416
|
+
defaultState: DEFAULT_PERSISTED_APPEARANCE,
|
|
3417
|
+
schema: persistedAppearanceSchema,
|
|
3418
|
+
migration: APPEARANCE_MIGRATION,
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
catch {
|
|
3422
|
+
// Accessing localStorage can throw (privacy mode, blocked site data,
|
|
3423
|
+
// non-browser rendering) — fall through to the defaults.
|
|
3424
|
+
}
|
|
3425
|
+
return {
|
|
3426
|
+
theme: persisted.theme,
|
|
3427
|
+
mapType: persisted.mapType,
|
|
3428
|
+
showRoads: persisted.showRoads,
|
|
3429
|
+
// `traffic: false` opts the whole surface out of traffic (see
|
|
3430
|
+
// useMapAppearanceControls), so it must override a stored preference here too
|
|
3431
|
+
showTraffic: params?.options?.traffic === false ? false : persisted.showTraffic,
|
|
3432
|
+
};
|
|
3433
|
+
});
|
|
3434
|
+
return seed;
|
|
3435
|
+
};
|
|
3436
|
+
|
|
3365
3437
|
const MARKER_SPRING_CONFIG = {
|
|
3366
3438
|
stiffness: 380,
|
|
3367
3439
|
damping: 30,
|
|
@@ -5173,7 +5245,7 @@ const useLayerReady = (loading) => {
|
|
|
5173
5245
|
// ============================================================================
|
|
5174
5246
|
// Constants
|
|
5175
5247
|
// ============================================================================
|
|
5176
|
-
const EMPTY_CONTROLS$
|
|
5248
|
+
const EMPTY_CONTROLS$4 = {};
|
|
5177
5249
|
// ============================================================================
|
|
5178
5250
|
// Hook
|
|
5179
5251
|
// ============================================================================
|
|
@@ -5213,7 +5285,7 @@ const useImageOverlay = (options) => {
|
|
|
5213
5285
|
ready: layerReady.ready,
|
|
5214
5286
|
getBounds,
|
|
5215
5287
|
counts,
|
|
5216
|
-
controls: controls ?? EMPTY_CONTROLS$
|
|
5288
|
+
controls: controls ?? EMPTY_CONTROLS$4,
|
|
5217
5289
|
fitParticipation,
|
|
5218
5290
|
layerType: "image-overlay",
|
|
5219
5291
|
url,
|
|
@@ -5494,7 +5566,7 @@ const createLazyGetter = (compute) => {
|
|
|
5494
5566
|
// ============================================================================
|
|
5495
5567
|
// Constants
|
|
5496
5568
|
// ============================================================================
|
|
5497
|
-
const EMPTY_CONTROLS$
|
|
5569
|
+
const EMPTY_CONTROLS$3 = {};
|
|
5498
5570
|
const EMPTY_FEATURES$1 = { type: "FeatureCollection", features: [] };
|
|
5499
5571
|
// ============================================================================
|
|
5500
5572
|
// Internal helpers
|
|
@@ -5582,7 +5654,7 @@ const useRoute = (options) => {
|
|
|
5582
5654
|
ready: layerReady.ready,
|
|
5583
5655
|
getBounds,
|
|
5584
5656
|
counts,
|
|
5585
|
-
controls: controls ?? EMPTY_CONTROLS$
|
|
5657
|
+
controls: controls ?? EMPTY_CONTROLS$3,
|
|
5586
5658
|
fitParticipation,
|
|
5587
5659
|
layerType: "route",
|
|
5588
5660
|
features,
|
|
@@ -5708,6 +5780,78 @@ const useFitToContent = (api, handles, options) => {
|
|
|
5708
5780
|
return react.useMemo(() => ({ hasFitted, isWaiting, fitNow }), [hasFitted, isWaiting, fitNow]);
|
|
5709
5781
|
};
|
|
5710
5782
|
|
|
5783
|
+
const EMPTY_CONTROLS$2 = {};
|
|
5784
|
+
/**
|
|
5785
|
+
* Weighted point heatmap layer handle (provider-agnostic).
|
|
5786
|
+
*
|
|
5787
|
+
* - Normalizes `data` → GeoJSON `Point` features + `properties.weight`.
|
|
5788
|
+
* - Synced as `HeatmapSourceConfig` in {@link LayerSnapshot}.
|
|
5789
|
+
* - Renderers: ADR-0029 (`libs/react/map/docs/adr/0029-heatmap-provider-specific-renderers.md`).
|
|
5790
|
+
*/
|
|
5791
|
+
const useHeatmap = (options) => {
|
|
5792
|
+
const { id, name, data, getPosition, getWeight, style, controls, loading, fitParticipation = "all" } = options;
|
|
5793
|
+
const layerReady = useLayerReady(loading);
|
|
5794
|
+
const normalized = react.useMemo(() => {
|
|
5795
|
+
const features = [];
|
|
5796
|
+
const positions = [];
|
|
5797
|
+
for (const item of data) {
|
|
5798
|
+
const position = getPosition(item);
|
|
5799
|
+
if (position === null)
|
|
5800
|
+
continue;
|
|
5801
|
+
const parsedPosition = geoJsonUtils.geoJsonPositionSchema.safeParse(position);
|
|
5802
|
+
if (!parsedPosition.success)
|
|
5803
|
+
continue;
|
|
5804
|
+
const weight = getWeight?.(item) ?? 1;
|
|
5805
|
+
if (!Number.isFinite(weight) || weight <= 0)
|
|
5806
|
+
continue;
|
|
5807
|
+
positions.push(parsedPosition.data);
|
|
5808
|
+
features.push({
|
|
5809
|
+
type: "Feature",
|
|
5810
|
+
geometry: { type: "Point", coordinates: parsedPosition.data },
|
|
5811
|
+
properties: { weight },
|
|
5812
|
+
});
|
|
5813
|
+
}
|
|
5814
|
+
return {
|
|
5815
|
+
features: { type: "FeatureCollection", features },
|
|
5816
|
+
positions,
|
|
5817
|
+
};
|
|
5818
|
+
}, [data, getPosition, getWeight]);
|
|
5819
|
+
const resolvedStyle = react.useMemo(() => ({
|
|
5820
|
+
radiusPixels: style?.radiusPixels ?? 30,
|
|
5821
|
+
intensity: style?.intensity ?? 1,
|
|
5822
|
+
opacity: style?.opacity ?? 1,
|
|
5823
|
+
}), [style?.radiusPixels, style?.intensity, style?.opacity]);
|
|
5824
|
+
const getBounds = react.useMemo(() => createLazyGetter(() => computeBoundsFromPositions(normalized.positions)), [normalized.positions]);
|
|
5825
|
+
const counts = react.useMemo(() => ({ total: data.length, points: normalized.features.features.length }), [data.length, normalized.features.features.length]);
|
|
5826
|
+
return react.useMemo(() => ({
|
|
5827
|
+
id,
|
|
5828
|
+
name,
|
|
5829
|
+
loadsData: layerReady.loadsData,
|
|
5830
|
+
loading: layerReady.isLoading,
|
|
5831
|
+
ready: layerReady.ready,
|
|
5832
|
+
getBounds,
|
|
5833
|
+
counts,
|
|
5834
|
+
controls: controls ?? EMPTY_CONTROLS$2,
|
|
5835
|
+
fitParticipation,
|
|
5836
|
+
layerType: "heatmap",
|
|
5837
|
+
features: normalized.features,
|
|
5838
|
+
style: resolvedStyle,
|
|
5839
|
+
pointCount: normalized.features.features.length,
|
|
5840
|
+
totalCount: data.length,
|
|
5841
|
+
}), [
|
|
5842
|
+
id,
|
|
5843
|
+
name,
|
|
5844
|
+
layerReady,
|
|
5845
|
+
getBounds,
|
|
5846
|
+
counts,
|
|
5847
|
+
controls,
|
|
5848
|
+
fitParticipation,
|
|
5849
|
+
normalized.features,
|
|
5850
|
+
resolvedStyle,
|
|
5851
|
+
data.length,
|
|
5852
|
+
]);
|
|
5853
|
+
};
|
|
5854
|
+
|
|
5711
5855
|
/**
|
|
5712
5856
|
* Do two label footprints overlap? Each is a box of its own size centred on its point, so the boxes
|
|
5713
5857
|
* overlap when the centres are closer than the sum of their half-extents in each axis. Shared by the
|
|
@@ -8002,6 +8146,13 @@ function toMapLayer(handle) {
|
|
|
8002
8146
|
imageBounds: handle.imageBounds,
|
|
8003
8147
|
opacity: handle.opacity,
|
|
8004
8148
|
};
|
|
8149
|
+
case "heatmap":
|
|
8150
|
+
return {
|
|
8151
|
+
layerType: "heatmap",
|
|
8152
|
+
id: handle.id,
|
|
8153
|
+
features: handle.features,
|
|
8154
|
+
style: handle.style,
|
|
8155
|
+
};
|
|
8005
8156
|
default: {
|
|
8006
8157
|
const exhaustiveCheck = handle;
|
|
8007
8158
|
throw new Error(`${String(exhaustiveCheck)} is not known`);
|
|
@@ -12884,6 +13035,7 @@ Object.defineProperty(exports, "mapTypeSchema", {
|
|
|
12884
13035
|
exports.ClusterMarker = ClusterMarker;
|
|
12885
13036
|
exports.ClusterStick = ClusterStick;
|
|
12886
13037
|
exports.Controls = Controls;
|
|
13038
|
+
exports.DEFAULT_APPEARANCE_PERSISTENCE_KEY = DEFAULT_APPEARANCE_PERSISTENCE_KEY;
|
|
12887
13039
|
exports.DEFAULT_MARKER_SIZE_BREAKPOINTS = DEFAULT_MARKER_SIZE_BREAKPOINTS;
|
|
12888
13040
|
exports.DefaultControls = DefaultControls;
|
|
12889
13041
|
exports.Layers = Layers;
|
|
@@ -12915,6 +13067,7 @@ exports.useEntitiesNearCursor = useEntitiesNearCursor;
|
|
|
12915
13067
|
exports.useExpandedIds = useExpandedIds;
|
|
12916
13068
|
exports.useFitFeatureBounds = useFitFeatureBounds;
|
|
12917
13069
|
exports.useFitToContent = useFitToContent;
|
|
13070
|
+
exports.useHeatmap = useHeatmap;
|
|
12918
13071
|
exports.useImageOverlay = useImageOverlay;
|
|
12919
13072
|
exports.useLabelPlacement = useLabelPlacement;
|
|
12920
13073
|
exports.useLayers = useLayers;
|
|
@@ -12928,6 +13081,7 @@ exports.useMarkerStateResolvers = useMarkerStateResolvers;
|
|
|
12928
13081
|
exports.useMarkers = useMarkers;
|
|
12929
13082
|
exports.usePanel = usePanel;
|
|
12930
13083
|
exports.usePanelPreload = usePanelPreload;
|
|
13084
|
+
exports.usePersistedAppearanceSeed = usePersistedAppearanceSeed;
|
|
12931
13085
|
exports.usePreviewMap = usePreviewMap;
|
|
12932
13086
|
exports.useRoute = useRoute;
|
|
12933
13087
|
exports.useShapeLabelHelpers = useShapeLabelHelpers;
|
package/index.esm.js
CHANGED
|
@@ -12,6 +12,7 @@ import { isEqual } from 'es-toolkit';
|
|
|
12
12
|
import { useGeolocation, useErrorHandlerOrNull } from '@trackunit/react-core-hooks';
|
|
13
13
|
import { Checkbox, ToggleSwitch, RadioGroup as RadioGroup$1, RadioItem, FormGroup, Search as Search$1, SelectField, BaseSelect } from '@trackunit/react-form-components';
|
|
14
14
|
import { z } from 'zod';
|
|
15
|
+
import { readLocalStorageValue } from '@trackunit/shared-utils';
|
|
15
16
|
import { PieChart } from 'react-minimal-pie-chart';
|
|
16
17
|
import { tailwindPalette } from '@trackunit/ui-design-tokens';
|
|
17
18
|
import { lngLatToMercatorPxWS, validateBboxWithFallback, extractPositionsFromGeometry, geoJsonPositionSchema, validateFeatureCollection, EMPTY_FEATURE_COLLECTION, splitAntimeridianCrossingFeatures, validateBbox, projectPolygonalToWebMercator, geoJsonPolygonDifference, unprojectPolygonalFromWebMercator, geoJsonPolygonSchema, geoJsonMultiPolygonSchema, distanceToGeoJsonPolygonBoundary, getGeoJsonPolygonIntersection, isFullyContainedInGeometry, isGeoJsonPointInPolygon, isBboxInsideFeatureCollection, extractEdges, computeGeometryCentroid, mercatorPxToLngLatWS, isPositionInsideRing, lngLatToWebMercatorPx, edgePixelLength, isFullyContainedInGeoJsonGeometry } from '@trackunit/geo-json-utils';
|
|
@@ -2791,22 +2792,20 @@ const AppearancePreview = ({ adapterConfig, mapType, label, selected, onSelect,
|
|
|
2791
2792
|
* @internal
|
|
2792
2793
|
*/
|
|
2793
2794
|
const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme, currentMapType, onChange, }) => {
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
return (jsx(AppearancePreview, { adapterConfig: derivedConfig, "data-testid": `map-style-preview-${option.key}`, label: option.label, mapType: option.mapType, onSelect: () => onChange(option.theme, option.mapType), selected: option.theme === currentTheme && option.mapType === currentMapType }, option.key));
|
|
2809
|
-
}) }));
|
|
2795
|
+
const derivedOptions = useMemo(() => options.map(option => ({
|
|
2796
|
+
option,
|
|
2797
|
+
// Pin the full appearance per option: the main map's config may carry a
|
|
2798
|
+
// persisted seed (theme, mapType, roads, traffic — see
|
|
2799
|
+
// usePersistedAppearanceSeed), and a preview depicts exactly one
|
|
2800
|
+
// theme/mapType combination with no overlays.
|
|
2801
|
+
config: adapterConfig.derive({
|
|
2802
|
+
theme: option.theme,
|
|
2803
|
+
mapType: option.mapType,
|
|
2804
|
+
showRoads: false,
|
|
2805
|
+
showTraffic: false,
|
|
2806
|
+
}),
|
|
2807
|
+
})), [adapterConfig, options]);
|
|
2808
|
+
return (jsx("div", { "aria-label": ariaLabel, className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: derivedOptions.map(({ option, config }) => (jsx(AppearancePreview, { adapterConfig: config, "data-testid": `map-style-preview-${option.key}`, label: option.label, mapType: option.mapType, onSelect: () => onChange(option.theme, option.mapType), selected: option.theme === currentTheme && option.mapType === currentMapType }, option.key))) }));
|
|
2810
2809
|
};
|
|
2811
2810
|
|
|
2812
2811
|
// ============================================================================
|
|
@@ -2814,10 +2813,16 @@ const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme
|
|
|
2814
2813
|
// ============================================================================
|
|
2815
2814
|
/**
|
|
2816
2815
|
* Increment this number when the persisted appearance shape changes, and add a
|
|
2817
|
-
* matching migration step in
|
|
2816
|
+
* matching migration step in appearanceMigrations.ts so stored settings are
|
|
2818
2817
|
* upgraded instead of reset. (v2: added showTraffic.)
|
|
2819
2818
|
*/
|
|
2820
2819
|
const APPEARANCE_SETTINGS_VERSION = 2;
|
|
2820
|
+
/**
|
|
2821
|
+
* Default localStorage key for the persisted map appearance settings.
|
|
2822
|
+
* Shared by `useMapAppearanceControls` (owner) and `usePersistedAppearanceSeed`
|
|
2823
|
+
* (read-only) so both resolve the same stored preference by default.
|
|
2824
|
+
*/
|
|
2825
|
+
const DEFAULT_APPEARANCE_PERSISTENCE_KEY = "map-appearance-settings";
|
|
2821
2826
|
// ============================================================================
|
|
2822
2827
|
// Persisted Appearance Schema (extends core mapAppearanceSchema with version)
|
|
2823
2828
|
// ============================================================================
|
|
@@ -2852,8 +2857,6 @@ const appearanceReducer = (state, action) => {
|
|
|
2852
2857
|
}
|
|
2853
2858
|
};
|
|
2854
2859
|
|
|
2855
|
-
const DEFAULT_THEMES = ["light", "dark"];
|
|
2856
|
-
const DEFAULT_MAP_TYPES = ["roadmap", "satellite"];
|
|
2857
2860
|
/**
|
|
2858
2861
|
* The v1 persisted shape, frozen as a literal. Deliberately NOT derived from
|
|
2859
2862
|
* the live mapAppearanceSchema: deriving would make every future appearance
|
|
@@ -2889,6 +2892,20 @@ const appearanceMigrationV2 = {
|
|
|
2889
2892
|
: DEFAULT_PERSISTED_APPEARANCE;
|
|
2890
2893
|
},
|
|
2891
2894
|
};
|
|
2895
|
+
/**
|
|
2896
|
+
* Migration pipeline for the persisted map appearance settings.
|
|
2897
|
+
*
|
|
2898
|
+
* Shared by `useMapAppearanceControls` (the owning reader/writer) and
|
|
2899
|
+
* `usePersistedAppearanceSeed` (a read-only consumer), so both interpret
|
|
2900
|
+
* stored data — including pre-envelope and v1 shapes — identically.
|
|
2901
|
+
*/
|
|
2902
|
+
const APPEARANCE_MIGRATION = {
|
|
2903
|
+
version: APPEARANCE_SETTINGS_VERSION,
|
|
2904
|
+
steps: [appearanceMigrationV1, appearanceMigrationV2],
|
|
2905
|
+
};
|
|
2906
|
+
|
|
2907
|
+
const DEFAULT_THEMES = ["light", "dark"];
|
|
2908
|
+
const DEFAULT_MAP_TYPES = ["roadmap", "satellite"];
|
|
2892
2909
|
const themeToTranslationKey = (theme) => {
|
|
2893
2910
|
switch (theme) {
|
|
2894
2911
|
case "light":
|
|
@@ -2977,7 +2994,7 @@ const buildAppearanceOptions = (themes, mapTypes, t) => {
|
|
|
2977
2994
|
* <Controls api={api} controls={controlsConfig} />
|
|
2978
2995
|
* ```
|
|
2979
2996
|
*/
|
|
2980
|
-
const useMapAppearanceControls = ({ api, options: appearanceOptions, persistenceKey =
|
|
2997
|
+
const useMapAppearanceControls = ({ api, options: appearanceOptions, persistenceKey = DEFAULT_APPEARANCE_PERSISTENCE_KEY, }) => {
|
|
2981
2998
|
const [t] = useTranslation();
|
|
2982
2999
|
const themes = appearanceOptions?.themes ?? DEFAULT_THEMES;
|
|
2983
3000
|
const mapTypes = appearanceOptions?.mapTypes ?? DEFAULT_MAP_TYPES;
|
|
@@ -2992,7 +3009,7 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
2992
3009
|
const [localState, dispatch] = useLocalStorageReducer({
|
|
2993
3010
|
key: persistenceKey,
|
|
2994
3011
|
defaultState: DEFAULT_PERSISTED_APPEARANCE,
|
|
2995
|
-
migration:
|
|
3012
|
+
migration: APPEARANCE_MIGRATION,
|
|
2996
3013
|
reducer: appearanceReducer,
|
|
2997
3014
|
schema: persistedAppearanceSchema,
|
|
2998
3015
|
});
|
|
@@ -3240,7 +3257,11 @@ const useDefaultControls = (api, options) => {
|
|
|
3240
3257
|
"data-testid": "map-my-location-button",
|
|
3241
3258
|
}, [api.actions, getPosition, options?.navigation?.myLocation?.enabled, t]);
|
|
3242
3259
|
const navigationControls = useMemo(() => defineControlStack([myLocationControl, zoomControl, fullscreenControl]), [fullscreenControl, myLocationControl, zoomControl]);
|
|
3243
|
-
const mapAppearanceControls = useMapAppearanceControls({
|
|
3260
|
+
const mapAppearanceControls = useMapAppearanceControls({
|
|
3261
|
+
api,
|
|
3262
|
+
options: options?.settings?.mapStyle?.options,
|
|
3263
|
+
persistenceKey: options?.settings?.mapStyle?.persistenceKey,
|
|
3264
|
+
});
|
|
3244
3265
|
const mapStyleControl = useMemo(() => options?.settings?.mapStyle?.enabled === false ? undefined : mapAppearanceControls[0], [mapAppearanceControls, options?.settings?.mapStyle?.enabled]);
|
|
3245
3266
|
const settingsControls = useMemo(() => defineControlStack([mapStyleControl]), [mapStyleControl]);
|
|
3246
3267
|
return useMemo(() => ({
|
|
@@ -3361,6 +3382,57 @@ const useControls = (base, areas) => {
|
|
|
3361
3382
|
}, [base, areas]);
|
|
3362
3383
|
};
|
|
3363
3384
|
|
|
3385
|
+
/**
|
|
3386
|
+
* Reads the persisted map appearance once (on mount) so it can seed the adapter
|
|
3387
|
+
* factory, letting the FIRST map be constructed with the user's stored theme,
|
|
3388
|
+
* map type, and traffic preference instead of being corrected after mount.
|
|
3389
|
+
*
|
|
3390
|
+
* Without the seed, a stored dark theme or traffic-on preference only reaches
|
|
3391
|
+
* the adapter once the map is ready, forcing an immediate map recreation on
|
|
3392
|
+
* every page load: a visible blink, a wasted provider map construction (each
|
|
3393
|
+
* one is a billed Dynamic Maps load on Google), and overlays built on a
|
|
3394
|
+
* throwaway instance (ADR 0029).
|
|
3395
|
+
*
|
|
3396
|
+
* Read-only: `useMapAppearanceControls` keeps owning the persistence and pushes
|
|
3397
|
+
* all later changes through the adapter's appearance actions. The seed is
|
|
3398
|
+
* deliberately not reactive — it only matters at construction time.
|
|
3399
|
+
*
|
|
3400
|
+
* Storage being unavailable or corrupt is never an error: the seed then falls
|
|
3401
|
+
* back to the defaults, which is exactly the pre-seeding behavior.
|
|
3402
|
+
*
|
|
3403
|
+
* @example
|
|
3404
|
+
* ```tsx
|
|
3405
|
+
* const appearanceSeed = usePersistedAppearanceSeed();
|
|
3406
|
+
* const [Map, api] = useMap(googleMapsAdapter({ apiKey, ...appearanceSeed }));
|
|
3407
|
+
* ```
|
|
3408
|
+
*/
|
|
3409
|
+
const usePersistedAppearanceSeed = (params) => {
|
|
3410
|
+
const [seed] = useState(() => {
|
|
3411
|
+
let persisted = DEFAULT_PERSISTED_APPEARANCE;
|
|
3412
|
+
try {
|
|
3413
|
+
persisted = readLocalStorageValue({
|
|
3414
|
+
key: params?.persistenceKey ?? DEFAULT_APPEARANCE_PERSISTENCE_KEY,
|
|
3415
|
+
defaultState: DEFAULT_PERSISTED_APPEARANCE,
|
|
3416
|
+
schema: persistedAppearanceSchema,
|
|
3417
|
+
migration: APPEARANCE_MIGRATION,
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
catch {
|
|
3421
|
+
// Accessing localStorage can throw (privacy mode, blocked site data,
|
|
3422
|
+
// non-browser rendering) — fall through to the defaults.
|
|
3423
|
+
}
|
|
3424
|
+
return {
|
|
3425
|
+
theme: persisted.theme,
|
|
3426
|
+
mapType: persisted.mapType,
|
|
3427
|
+
showRoads: persisted.showRoads,
|
|
3428
|
+
// `traffic: false` opts the whole surface out of traffic (see
|
|
3429
|
+
// useMapAppearanceControls), so it must override a stored preference here too
|
|
3430
|
+
showTraffic: params?.options?.traffic === false ? false : persisted.showTraffic,
|
|
3431
|
+
};
|
|
3432
|
+
});
|
|
3433
|
+
return seed;
|
|
3434
|
+
};
|
|
3435
|
+
|
|
3364
3436
|
const MARKER_SPRING_CONFIG = {
|
|
3365
3437
|
stiffness: 380,
|
|
3366
3438
|
damping: 30,
|
|
@@ -5172,7 +5244,7 @@ const useLayerReady = (loading) => {
|
|
|
5172
5244
|
// ============================================================================
|
|
5173
5245
|
// Constants
|
|
5174
5246
|
// ============================================================================
|
|
5175
|
-
const EMPTY_CONTROLS$
|
|
5247
|
+
const EMPTY_CONTROLS$4 = {};
|
|
5176
5248
|
// ============================================================================
|
|
5177
5249
|
// Hook
|
|
5178
5250
|
// ============================================================================
|
|
@@ -5212,7 +5284,7 @@ const useImageOverlay = (options) => {
|
|
|
5212
5284
|
ready: layerReady.ready,
|
|
5213
5285
|
getBounds,
|
|
5214
5286
|
counts,
|
|
5215
|
-
controls: controls ?? EMPTY_CONTROLS$
|
|
5287
|
+
controls: controls ?? EMPTY_CONTROLS$4,
|
|
5216
5288
|
fitParticipation,
|
|
5217
5289
|
layerType: "image-overlay",
|
|
5218
5290
|
url,
|
|
@@ -5493,7 +5565,7 @@ const createLazyGetter = (compute) => {
|
|
|
5493
5565
|
// ============================================================================
|
|
5494
5566
|
// Constants
|
|
5495
5567
|
// ============================================================================
|
|
5496
|
-
const EMPTY_CONTROLS$
|
|
5568
|
+
const EMPTY_CONTROLS$3 = {};
|
|
5497
5569
|
const EMPTY_FEATURES$1 = { type: "FeatureCollection", features: [] };
|
|
5498
5570
|
// ============================================================================
|
|
5499
5571
|
// Internal helpers
|
|
@@ -5581,7 +5653,7 @@ const useRoute = (options) => {
|
|
|
5581
5653
|
ready: layerReady.ready,
|
|
5582
5654
|
getBounds,
|
|
5583
5655
|
counts,
|
|
5584
|
-
controls: controls ?? EMPTY_CONTROLS$
|
|
5656
|
+
controls: controls ?? EMPTY_CONTROLS$3,
|
|
5585
5657
|
fitParticipation,
|
|
5586
5658
|
layerType: "route",
|
|
5587
5659
|
features,
|
|
@@ -5707,6 +5779,78 @@ const useFitToContent = (api, handles, options) => {
|
|
|
5707
5779
|
return useMemo(() => ({ hasFitted, isWaiting, fitNow }), [hasFitted, isWaiting, fitNow]);
|
|
5708
5780
|
};
|
|
5709
5781
|
|
|
5782
|
+
const EMPTY_CONTROLS$2 = {};
|
|
5783
|
+
/**
|
|
5784
|
+
* Weighted point heatmap layer handle (provider-agnostic).
|
|
5785
|
+
*
|
|
5786
|
+
* - Normalizes `data` → GeoJSON `Point` features + `properties.weight`.
|
|
5787
|
+
* - Synced as `HeatmapSourceConfig` in {@link LayerSnapshot}.
|
|
5788
|
+
* - Renderers: ADR-0029 (`libs/react/map/docs/adr/0029-heatmap-provider-specific-renderers.md`).
|
|
5789
|
+
*/
|
|
5790
|
+
const useHeatmap = (options) => {
|
|
5791
|
+
const { id, name, data, getPosition, getWeight, style, controls, loading, fitParticipation = "all" } = options;
|
|
5792
|
+
const layerReady = useLayerReady(loading);
|
|
5793
|
+
const normalized = useMemo(() => {
|
|
5794
|
+
const features = [];
|
|
5795
|
+
const positions = [];
|
|
5796
|
+
for (const item of data) {
|
|
5797
|
+
const position = getPosition(item);
|
|
5798
|
+
if (position === null)
|
|
5799
|
+
continue;
|
|
5800
|
+
const parsedPosition = geoJsonPositionSchema.safeParse(position);
|
|
5801
|
+
if (!parsedPosition.success)
|
|
5802
|
+
continue;
|
|
5803
|
+
const weight = getWeight?.(item) ?? 1;
|
|
5804
|
+
if (!Number.isFinite(weight) || weight <= 0)
|
|
5805
|
+
continue;
|
|
5806
|
+
positions.push(parsedPosition.data);
|
|
5807
|
+
features.push({
|
|
5808
|
+
type: "Feature",
|
|
5809
|
+
geometry: { type: "Point", coordinates: parsedPosition.data },
|
|
5810
|
+
properties: { weight },
|
|
5811
|
+
});
|
|
5812
|
+
}
|
|
5813
|
+
return {
|
|
5814
|
+
features: { type: "FeatureCollection", features },
|
|
5815
|
+
positions,
|
|
5816
|
+
};
|
|
5817
|
+
}, [data, getPosition, getWeight]);
|
|
5818
|
+
const resolvedStyle = useMemo(() => ({
|
|
5819
|
+
radiusPixels: style?.radiusPixels ?? 30,
|
|
5820
|
+
intensity: style?.intensity ?? 1,
|
|
5821
|
+
opacity: style?.opacity ?? 1,
|
|
5822
|
+
}), [style?.radiusPixels, style?.intensity, style?.opacity]);
|
|
5823
|
+
const getBounds = useMemo(() => createLazyGetter(() => computeBoundsFromPositions(normalized.positions)), [normalized.positions]);
|
|
5824
|
+
const counts = useMemo(() => ({ total: data.length, points: normalized.features.features.length }), [data.length, normalized.features.features.length]);
|
|
5825
|
+
return useMemo(() => ({
|
|
5826
|
+
id,
|
|
5827
|
+
name,
|
|
5828
|
+
loadsData: layerReady.loadsData,
|
|
5829
|
+
loading: layerReady.isLoading,
|
|
5830
|
+
ready: layerReady.ready,
|
|
5831
|
+
getBounds,
|
|
5832
|
+
counts,
|
|
5833
|
+
controls: controls ?? EMPTY_CONTROLS$2,
|
|
5834
|
+
fitParticipation,
|
|
5835
|
+
layerType: "heatmap",
|
|
5836
|
+
features: normalized.features,
|
|
5837
|
+
style: resolvedStyle,
|
|
5838
|
+
pointCount: normalized.features.features.length,
|
|
5839
|
+
totalCount: data.length,
|
|
5840
|
+
}), [
|
|
5841
|
+
id,
|
|
5842
|
+
name,
|
|
5843
|
+
layerReady,
|
|
5844
|
+
getBounds,
|
|
5845
|
+
counts,
|
|
5846
|
+
controls,
|
|
5847
|
+
fitParticipation,
|
|
5848
|
+
normalized.features,
|
|
5849
|
+
resolvedStyle,
|
|
5850
|
+
data.length,
|
|
5851
|
+
]);
|
|
5852
|
+
};
|
|
5853
|
+
|
|
5710
5854
|
/**
|
|
5711
5855
|
* Do two label footprints overlap? Each is a box of its own size centred on its point, so the boxes
|
|
5712
5856
|
* overlap when the centres are closer than the sum of their half-extents in each axis. Shared by the
|
|
@@ -8001,6 +8145,13 @@ function toMapLayer(handle) {
|
|
|
8001
8145
|
imageBounds: handle.imageBounds,
|
|
8002
8146
|
opacity: handle.opacity,
|
|
8003
8147
|
};
|
|
8148
|
+
case "heatmap":
|
|
8149
|
+
return {
|
|
8150
|
+
layerType: "heatmap",
|
|
8151
|
+
id: handle.id,
|
|
8152
|
+
features: handle.features,
|
|
8153
|
+
style: handle.style,
|
|
8154
|
+
};
|
|
8004
8155
|
default: {
|
|
8005
8156
|
const exhaustiveCheck = handle;
|
|
8006
8157
|
throw new Error(`${String(exhaustiveCheck)} is not known`);
|
|
@@ -12828,4 +12979,4 @@ const mockMapApi = (overrides) => {
|
|
|
12828
12979
|
*/
|
|
12829
12980
|
setupLibraryTranslations();
|
|
12830
12981
|
|
|
12831
|
-
export { ClusterMarker, ClusterStick, Controls, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, PanelIconButton, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useEntitiesNearCursor, useExpandedIds, useFitFeatureBounds, useFitToContent, useImageOverlay, useLabelPlacement, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
|
|
12982
|
+
export { ClusterMarker, ClusterStick, Controls, DEFAULT_APPEARANCE_PERSISTENCE_KEY, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, PanelIconButton, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useEntitiesNearCursor, useExpandedIds, useFitFeatureBounds, useFitToContent, useHeatmap, useImageOverlay, useLabelPlacement, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePersistedAppearanceSeed, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
|
package/package.json
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.183",
|
|
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-components": "3.4.
|
|
11
|
-
"@trackunit/css-class-variance-utilities": "2.1.
|
|
12
|
-
"@trackunit/react-form-components": "2.8.
|
|
13
|
-
"@trackunit/react-core-hooks": "1.23.
|
|
14
|
-
"@trackunit/geo-json-utils": "1.16.
|
|
15
|
-
"@trackunit/i18n-library-translation": "2.6.
|
|
10
|
+
"@trackunit/react-components": "3.4.4",
|
|
11
|
+
"@trackunit/css-class-variance-utilities": "2.1.3",
|
|
12
|
+
"@trackunit/react-form-components": "2.8.4",
|
|
13
|
+
"@trackunit/react-core-hooks": "1.23.3",
|
|
14
|
+
"@trackunit/geo-json-utils": "1.16.3",
|
|
15
|
+
"@trackunit/i18n-library-translation": "2.6.3",
|
|
16
16
|
"react-minimal-pie-chart": "^8.4.0",
|
|
17
|
-
"@trackunit/react-map-adapter-shared": "0.0.
|
|
18
|
-
"@trackunit/react-map-color-utils": "0.0.
|
|
19
|
-
"@trackunit/
|
|
17
|
+
"@trackunit/react-map-adapter-shared": "0.0.144",
|
|
18
|
+
"@trackunit/react-map-color-utils": "0.0.119",
|
|
19
|
+
"@trackunit/shared-utils": "1.17.3",
|
|
20
|
+
"@trackunit/ui-design-tokens": "1.16.3",
|
|
20
21
|
"@floating-ui/react": "^0.26.25",
|
|
21
22
|
"es-toolkit": "^1.39.10",
|
|
22
23
|
"tailwind-merge": "^2.0.0",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MigrationConfig } from "@trackunit/react-components";
|
|
2
|
+
/**
|
|
3
|
+
* Migration pipeline for the persisted map appearance settings.
|
|
4
|
+
*
|
|
5
|
+
* Shared by `useMapAppearanceControls` (the owning reader/writer) and
|
|
6
|
+
* `usePersistedAppearanceSeed` (a read-only consumer), so both interpret
|
|
7
|
+
* stored data — including pre-envelope and v1 shapes — identically.
|
|
8
|
+
*/
|
|
9
|
+
export declare const APPEARANCE_MIGRATION: MigrationConfig;
|
|
@@ -3,10 +3,16 @@ import { z } from "zod";
|
|
|
3
3
|
import { type MapActions, type MapStatus, type MapTheme, type MapType } from "../core/types";
|
|
4
4
|
/**
|
|
5
5
|
* Increment this number when the persisted appearance shape changes, and add a
|
|
6
|
-
* matching migration step in
|
|
6
|
+
* matching migration step in appearanceMigrations.ts so stored settings are
|
|
7
7
|
* upgraded instead of reset. (v2: added showTraffic.)
|
|
8
8
|
*/
|
|
9
9
|
export declare const APPEARANCE_SETTINGS_VERSION = 2;
|
|
10
|
+
/**
|
|
11
|
+
* Default localStorage key for the persisted map appearance settings.
|
|
12
|
+
* Shared by `useMapAppearanceControls` (owner) and `usePersistedAppearanceSeed`
|
|
13
|
+
* (read-only) so both resolve the same stored preference by default.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_APPEARANCE_PERSISTENCE_KEY = "map-appearance-settings";
|
|
10
16
|
export declare const persistedAppearanceSchema: z.ZodObject<{
|
|
11
17
|
theme: z.ZodEnum<["light", "dark"]>;
|
|
12
18
|
mapType: z.ZodEnum<["roadmap", "satellite", "hybrid"]>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MapAppearance } from "@trackunit/react-map-adapter-shared";
|
|
2
|
+
import { type AppearanceOptions } from "./appearanceTypes";
|
|
3
|
+
/**
|
|
4
|
+
* The persisted appearance as a seed for adapter construction: every field
|
|
5
|
+
* resolved (`showTraffic` is optional in the core `MapAppearance` for external
|
|
6
|
+
* compatibility, but a seed always knows its value). The shape spreads directly
|
|
7
|
+
* into an adapter factory's `BaseAdapterConfig` fields.
|
|
8
|
+
*/
|
|
9
|
+
export type MapAppearanceSeed = Readonly<Required<MapAppearance>>;
|
|
10
|
+
/** Parameters for the usePersistedAppearanceSeed hook */
|
|
11
|
+
export type UsePersistedAppearanceSeedParams = Readonly<{
|
|
12
|
+
/**
|
|
13
|
+
* localStorage persistence key. Must match the `persistenceKey` passed to
|
|
14
|
+
* `useMapAppearanceControls` on the same surface. Default: "map-appearance-settings"
|
|
15
|
+
*/
|
|
16
|
+
persistenceKey?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The surface's appearance options (the same object given to
|
|
19
|
+
* `useMapAppearanceControls` works). Only `traffic` affects the seed: a
|
|
20
|
+
* surface with `traffic: false` must not seed a traffic-on preference stored
|
|
21
|
+
* by another surface sharing the persistence key. `themes`/`mapTypes` never
|
|
22
|
+
* clamp the seed, matching the controls' push path, which does not clamp
|
|
23
|
+
* stored values either — hence the narrowed Pick.
|
|
24
|
+
*/
|
|
25
|
+
options?: Pick<AppearanceOptions, "traffic">;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Reads the persisted map appearance once (on mount) so it can seed the adapter
|
|
29
|
+
* factory, letting the FIRST map be constructed with the user's stored theme,
|
|
30
|
+
* map type, and traffic preference instead of being corrected after mount.
|
|
31
|
+
*
|
|
32
|
+
* Without the seed, a stored dark theme or traffic-on preference only reaches
|
|
33
|
+
* the adapter once the map is ready, forcing an immediate map recreation on
|
|
34
|
+
* every page load: a visible blink, a wasted provider map construction (each
|
|
35
|
+
* one is a billed Dynamic Maps load on Google), and overlays built on a
|
|
36
|
+
* throwaway instance (ADR 0029).
|
|
37
|
+
*
|
|
38
|
+
* Read-only: `useMapAppearanceControls` keeps owning the persistence and pushes
|
|
39
|
+
* all later changes through the adapter's appearance actions. The seed is
|
|
40
|
+
* deliberately not reactive — it only matters at construction time.
|
|
41
|
+
*
|
|
42
|
+
* Storage being unavailable or corrupt is never an error: the seed then falls
|
|
43
|
+
* back to the defaults, which is exactly the pre-seeding behavior.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const appearanceSeed = usePersistedAppearanceSeed();
|
|
48
|
+
* const [Map, api] = useMap(googleMapsAdapter({ apiKey, ...appearanceSeed }));
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare const usePersistedAppearanceSeed: (params?: UsePersistedAppearanceSeedParams) => MapAppearanceSeed;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AdapterConfig } from "@trackunit/react-map-adapter-shared";
|
|
2
|
+
import type { AppearanceOptions } from "../appearance/appearanceTypes";
|
|
2
3
|
import type { ContainerRefHolder, MapActions, MapStatus } from "../core/types";
|
|
3
4
|
import type { BuiltInControlsConfig } from "./types";
|
|
4
5
|
/**
|
|
@@ -21,6 +22,18 @@ export type UseDefaultControlsOptions = Readonly<{
|
|
|
21
22
|
settings?: Readonly<{
|
|
22
23
|
mapStyle?: Readonly<{
|
|
23
24
|
enabled?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Appearance options forwarded to `useMapAppearanceControls`. A surface
|
|
27
|
+
* that also uses `usePersistedAppearanceSeed` must pass the same options
|
|
28
|
+
* to both, so opt-outs (e.g. `traffic: false`) apply to the seed too.
|
|
29
|
+
*/
|
|
30
|
+
options?: AppearanceOptions;
|
|
31
|
+
/**
|
|
32
|
+
* localStorage key forwarded to `useMapAppearanceControls`. A surface
|
|
33
|
+
* that also uses `usePersistedAppearanceSeed` must pass the same key to
|
|
34
|
+
* both. Default: "map-appearance-settings"
|
|
35
|
+
*/
|
|
36
|
+
persistenceKey?: string;
|
|
24
37
|
}>;
|
|
25
38
|
}>;
|
|
26
39
|
}>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { AdapterConfig, AdapterInstance, AdapterRendererProps, CameraState, MapStatus, SafeAreaInsets } from "@trackunit/react-map-adapter-shared";
|
|
1
|
+
import type { AdapterConfig, AdapterInstance, AdapterRendererProps, BaseAdapterConfig, CameraState, MapStatus, SafeAreaInsets } from "@trackunit/react-map-adapter-shared";
|
|
2
2
|
import type { ReactElement } from "react";
|
|
3
3
|
import type { MapApi, MapState } from "./types";
|
|
4
|
-
export type MockConfig = {
|
|
4
|
+
export type MockConfig = BaseAdapterConfig & {
|
|
5
5
|
readonly apiKey: string;
|
|
6
6
|
};
|
|
7
7
|
export declare const INITIAL_MOCK_MAP_STATE: MapState;
|
package/src/index.d.ts
CHANGED
|
@@ -17,12 +17,14 @@ export type { UseControlsAreas } from "./controls/types";
|
|
|
17
17
|
export { useControls } from "./controls/useControls";
|
|
18
18
|
export { useDefaultControls, type UseDefaultControlsOptions } from "./controls/useDefaultControls";
|
|
19
19
|
export type { AppearanceAction, AppearanceOption, AppearanceOptions, MapAppearanceApi, PersistedAppearance, UseMapAppearanceControlsParams, } from "./appearance/appearanceTypes";
|
|
20
|
+
export { DEFAULT_APPEARANCE_PERSISTENCE_KEY } from "./appearance/appearanceTypes";
|
|
20
21
|
export { useMapAppearanceControls } from "./appearance/useMapAppearanceControls";
|
|
22
|
+
export { usePersistedAppearanceSeed, type MapAppearanceSeed, type UsePersistedAppearanceSeedParams, } from "./appearance/usePersistedAppearanceSeed";
|
|
21
23
|
export type { ControlMiddleware, MiddlewareContext } from "./controls/renderingRules";
|
|
22
24
|
export type { CategoryMetadata } from "./controls/collapseControls";
|
|
23
25
|
export type { ClusterEntity, Entity, MapInteractionState, MarkerEntity, RouteEntity, ShapeEntity, ShapeType, } from "@trackunit/react-map-adapter-shared";
|
|
24
26
|
export { geometryTypeToShapeType, INITIAL_INTERACTION_STATE } from "@trackunit/react-map-adapter-shared";
|
|
25
|
-
export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterInfo, ClusterRenderState, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, FitParticipation, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, ImageOverlayHandle, LayerHandle, LayerMeta, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, MarkerLayerHandle, RenderConfig, RenderMedium, ResolutionContext, RouteLayerHandle, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeLayerHandle, ShapeOverlapConfig, ShapeStyle, ShapeStyleOverrides, ShapesUnderCursorHit, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "./layers/types";
|
|
27
|
+
export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterInfo, ClusterRenderState, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, FitParticipation, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, HeatmapLayerHandle, ImageOverlayHandle, LayerHandle, LayerMeta, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, MarkerLayerHandle, RenderConfig, RenderMedium, ResolutionContext, RouteLayerHandle, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeLayerHandle, ShapeOverlapConfig, ShapeStyle, ShapeStyleOverrides, ShapesUnderCursorHit, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "./layers/types";
|
|
26
28
|
export { computeMarkerDomPortalZIndex } from "./layers/types";
|
|
27
29
|
export { ANCHOR_SELECTOR, HIT_SURFACE_SELECTOR } from "@trackunit/react-map-adapter-shared";
|
|
28
30
|
export { ClusterMarker, type ClusterMarkerProps, type ClusterMarkerStickInput, type ClusterSegment, } from "./clusters/ClusterMarker";
|
|
@@ -53,6 +55,7 @@ export { useImageOverlay, type UseImageOverlayOptions, type UseImageOverlayRetur
|
|
|
53
55
|
export { buildExpandedIds, useExpandedIds, type MapFocus, type MapFocusTier, type MapFocusTierDisplay, } from "./layers/mapFocus";
|
|
54
56
|
export { useRoute, type UseRouteOptions, type UseRouteReturn } from "./layers/routes/useRoute";
|
|
55
57
|
export { useFitToContent, type FitToContentOptions, type FitToContentResult } from "./layers/useFitToContent";
|
|
58
|
+
export { useHeatmap, type HeatmapStyle, type UseHeatmapOptions, type UseHeatmapReturn, } from "./layers/useHeatmap/useHeatmap";
|
|
56
59
|
export { useLabelPlacement, type UseLabelPlacementParams } from "./layers/useLabelPlacement";
|
|
57
60
|
export { useLayers, type UseLayersReturn } from "./layers/useLayers";
|
|
58
61
|
export { DEFAULT_MARKER_SIZE_BREAKPOINTS, MARKER_DISC_BORDER_WIDTH_PX, type MarkerSizeBreakpoint, type PickMarkerSizeOptions, type PickRenderMediumOptions, type PickSymbolDiameterOptions, } from "./layers/useMarkers/adaptiveHelpers";
|
package/src/layers/types.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { GeoJsonBbox, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, GeoJsonPosition } from "@trackunit/geo-json-utils";
|
|
2
|
-
import type { AdaptiveMarkerResolution, ClusterConfig, ClusterRenderConfig, RenderConfig, RouteStyle, ShapeInteractiveMode, ShapeStyle } from "@trackunit/react-map-adapter-shared";
|
|
2
|
+
import type { AdaptiveMarkerResolution, ClusterConfig, ClusterRenderConfig, HeatmapRenderStyle, RenderConfig, RouteStyle, ShapeInteractiveMode, ShapeStyle } from "@trackunit/react-map-adapter-shared";
|
|
3
3
|
import type { CategoryKey, ControlConfig } from "../controls/types";
|
|
4
4
|
import type { ShapeDecoration } from "./useShapes/shapeDecorations";
|
|
5
5
|
import type { ShapesUnderCursorHit } from "./useShapes/shapeFillTiling";
|
|
6
6
|
import type { ShapeLabelResolutionContext } from "./useShapes/shapeLabelResolution";
|
|
7
7
|
import type { ResolveShapeStackOrder } from "./useShapes/shapeStackOrder";
|
|
8
8
|
export type { ShapesUnderCursorHit };
|
|
9
|
-
export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterDomRenderConfig, ClusterInfo, ClusterRenderConfig, ClusterRenderState, ClusterSymbolRenderConfig, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, PixelOffset, RenderConfig, RenderMedium, ResolutionContext, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeStyle, ShapeStyleOverrides, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "@trackunit/react-map-adapter-shared";
|
|
9
|
+
export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterDomRenderConfig, ClusterInfo, ClusterRenderConfig, ClusterRenderState, ClusterSymbolRenderConfig, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, HeatmapRenderStyle, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, PixelOffset, RenderConfig, RenderMedium, ResolutionContext, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeStyle, ShapeStyleOverrides, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "@trackunit/react-map-adapter-shared";
|
|
10
10
|
export { computeMarkerDomPortalZIndex } from "@trackunit/react-map-adapter-shared";
|
|
11
11
|
/**
|
|
12
12
|
* Controls whether a layer's bounds are included in fit-to-content operations.
|
|
@@ -187,8 +187,13 @@ export interface ImageOverlayHandle extends LayerMeta {
|
|
|
187
187
|
/** Opacity from 0 to 1 */
|
|
188
188
|
readonly opacity: number;
|
|
189
189
|
}
|
|
190
|
+
export interface HeatmapLayerHandle extends LayerMeta {
|
|
191
|
+
readonly layerType: "heatmap";
|
|
192
|
+
readonly features: GeoJsonFeatureCollection;
|
|
193
|
+
readonly style: HeatmapRenderStyle;
|
|
194
|
+
}
|
|
190
195
|
/**
|
|
191
196
|
* Discriminated union of all layer handle types.
|
|
192
197
|
* Discriminated on `layerType`.
|
|
193
198
|
*/
|
|
194
|
-
export type LayerHandle = MarkerLayerHandle | ShapeLayerHandle | RouteLayerHandle | ImageOverlayHandle;
|
|
199
|
+
export type LayerHandle = MarkerLayerHandle | ShapeLayerHandle | RouteLayerHandle | ImageOverlayHandle | HeatmapLayerHandle;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GeoJsonBbox, GeoJsonPosition } from "@trackunit/geo-json-utils";
|
|
2
|
+
export type HeatmapPocPoint = Readonly<{
|
|
3
|
+
position: GeoJsonPosition;
|
|
4
|
+
weight: number;
|
|
5
|
+
}>;
|
|
6
|
+
export declare const HEATMAP_POC_BOUNDS: GeoJsonBbox;
|
|
7
|
+
export declare const HEATMAP_POC_POINTS: ReadonlyArray<HeatmapPocPoint>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type GeoJsonPosition } from "@trackunit/geo-json-utils";
|
|
2
|
+
import type { CategoryKey, ControlConfig } from "../../controls/types";
|
|
3
|
+
import type { FitParticipation, HeatmapLayerHandle } from "../types";
|
|
4
|
+
export type HeatmapStyle = Readonly<{
|
|
5
|
+
radiusPixels?: number;
|
|
6
|
+
intensity?: number;
|
|
7
|
+
opacity?: number;
|
|
8
|
+
}>;
|
|
9
|
+
export type UseHeatmapOptions<TItem> = Readonly<{
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
data: ReadonlyArray<TItem>;
|
|
13
|
+
getPosition: (item: TItem) => GeoJsonPosition | null;
|
|
14
|
+
getWeight?: (item: TItem) => number;
|
|
15
|
+
style?: HeatmapStyle;
|
|
16
|
+
controls?: Partial<Record<CategoryKey, ReadonlyArray<ControlConfig>>>;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
fitParticipation?: FitParticipation;
|
|
19
|
+
}>;
|
|
20
|
+
export interface UseHeatmapReturn extends HeatmapLayerHandle {
|
|
21
|
+
readonly pointCount: number;
|
|
22
|
+
readonly totalCount: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Weighted point heatmap layer handle (provider-agnostic).
|
|
26
|
+
*
|
|
27
|
+
* - Normalizes `data` → GeoJSON `Point` features + `properties.weight`.
|
|
28
|
+
* - Synced as `HeatmapSourceConfig` in {@link LayerSnapshot}.
|
|
29
|
+
* - Renderers: ADR-0029 (`libs/react/map/docs/adr/0029-heatmap-provider-specific-renderers.md`).
|
|
30
|
+
*/
|
|
31
|
+
export declare const useHeatmap: <TItem>(options: UseHeatmapOptions<TItem>) => UseHeatmapReturn;
|