@defra/interactive-map 0.0.21-alpha → 0.0.23-alpha
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/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +39 -0
- package/docs/api/context.md +4 -4
- package/docs/api/map-style-config.md +11 -2
- package/docs/api/marker-config.md +38 -7
- package/docs/api/panel-definition.md +7 -3
- package/docs/api/symbol-config.md +10 -26
- package/docs/api/symbol-registry.md +28 -13
- package/docs/api.md +65 -4
- package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
- package/docs/assets/images/toggle-marker-label.jpg +0 -0
- package/docs/examples/index.mdx +12 -0
- package/docs/examples/toggle-marker-label.mdx +89 -0
- package/docs/plugins/datasets.md +39 -1
- package/docs/plugins/interact.md +4 -11
- package/docusaurus.config.cjs +2 -1
- package/package.json +10 -12
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -0
- package/plugins/beta/datasets/dist/css/index.css +2 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +2 -0
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +2 -0
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +2 -0
- package/plugins/beta/datasets/src/adapters/maplibre/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
- package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
- package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
- package/plugins/beta/datasets/src/index.maplibre.umd.js +23 -0
- package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +1 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +23 -55
- package/plugins/interact/src/InteractInit.test.js +58 -80
- package/plugins/interact/src/defaults.js +1 -3
- package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +65 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
- package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
- package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
- package/plugins/interact/src/hooks/useMapItemList.js +187 -0
- package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
- package/plugins/interact/src/index.js +0 -2
- package/plugins/interact/src/index.test.js +0 -2
- package/plugins/interact/src/manifest.js +37 -6
- package/plugins/interact/src/reducer.js +8 -4
- package/plugins/interact/src/reducer.test.js +18 -3
- package/plugins/interact/src/utils/buildStylesMap.js +21 -10
- package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
- package/plugins/interact/src/utils/spatial.js +0 -15
- package/plugins/interact/src/utils/spatial.test.js +1 -13
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
- package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
- package/plugins/search/src/search.scss +1 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/index.js +7 -1
- package/providers/maplibre/src/index.test.js +15 -2
- package/providers/maplibre/src/mapEvents.js +11 -5
- package/providers/maplibre/src/mapEvents.test.js +14 -3
- package/providers/maplibre/src/maplibreProvider.js +18 -11
- package/providers/maplibre/src/maplibreProvider.test.js +43 -36
- package/providers/maplibre/src/utils/highlightFeatures.js +102 -57
- package/providers/maplibre/src/utils/highlightFeatures.test.js +162 -51
- package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
- package/providers/maplibre/src/utils/patternImages.js +10 -12
- package/providers/maplibre/src/utils/patternImages.test.js +145 -167
- package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
- package/providers/maplibre/src/utils/symbolImages.js +23 -86
- package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
- package/rollup.esm.mjs +1 -1
- package/src/App/components/Actions/Actions.module.scss +1 -0
- package/src/App/components/Actions/Actions.test.jsx +17 -4
- package/src/App/components/Hints/Hints.jsx +52 -0
- package/src/App/components/Hints/Hints.module.scss +37 -0
- package/src/App/components/Hints/Hints.test.jsx +104 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
- package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
- package/src/App/components/Markers/LabelMarker.jsx +14 -0
- package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
- package/src/App/components/Markers/Markers.jsx +44 -75
- package/src/App/components/Markers/Markers.module.scss +58 -0
- package/src/App/components/Markers/Markers.test.jsx +90 -113
- package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
- package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
- package/src/App/components/Markers/SymbolMarker.jsx +31 -0
- package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
- package/src/App/components/Panel/Panel.jsx +1 -1
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
- package/src/App/components/Tabs/Tabs.jsx +68 -0
- package/src/App/components/Tabs/Tabs.module.scss +57 -0
- package/src/App/components/Tabs/Tabs.test.jsx +121 -0
- package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
- package/src/App/components/Viewport/Features.jsx +35 -0
- package/src/App/components/Viewport/Features.module.scss +31 -0
- package/src/App/components/Viewport/Features.test.jsx +124 -0
- package/src/App/components/Viewport/MapController.jsx +1 -1
- package/src/App/components/Viewport/MapStatus.jsx +0 -4
- package/src/App/components/Viewport/Viewport.jsx +35 -29
- package/src/App/components/Viewport/Viewport.module.scss +7 -21
- package/src/App/components/Viewport/Viewport.test.jsx +108 -65
- package/src/App/controls/keyboardActions.js +9 -3
- package/src/App/controls/keyboardActions.test.js +122 -87
- package/src/App/controls/keyboardShortcuts.js +1 -14
- package/src/App/hooks/useFeatureFocus.js +199 -0
- package/src/App/hooks/useFeatureFocus.test.js +635 -0
- package/src/App/hooks/useFeatureItems.js +30 -0
- package/src/App/hooks/useFeatureItems.test.js +97 -0
- package/src/App/hooks/useFocusVisible.js +4 -7
- package/src/App/hooks/useFocusVisible.test.js +6 -6
- package/src/App/hooks/useKeyboardHint.js +9 -34
- package/src/App/hooks/useKeyboardHint.test.js +34 -84
- package/src/App/hooks/useKeyboardShortcuts.js +8 -5
- package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
- package/src/App/hooks/useLayoutMeasurements.js +10 -4
- package/src/App/hooks/useLayoutMeasurements.test.js +30 -0
- package/src/App/hooks/useMapURLSync.js +3 -3
- package/src/App/hooks/useMapURLSync.test.js +20 -3
- package/src/App/hooks/useMarkerCursor.js +33 -0
- package/src/App/hooks/useMarkerCursor.test.js +87 -0
- package/src/App/hooks/useMarkersAPI.js +32 -9
- package/src/App/hooks/useMarkersAPI.test.js +162 -94
- package/src/App/layout/Layout.jsx +2 -0
- package/src/App/layout/Layout.test.jsx +3 -0
- package/src/App/registry/keyboardShortcutRegistry.js +3 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
- package/src/App/renderer/mapButtons.js +5 -1
- package/src/App/renderer/mapButtons.test.js +37 -1
- package/src/App/store/ServiceProvider.jsx +4 -1
- package/src/App/store/ServiceProvider.test.jsx +11 -0
- package/src/App/store/appActionsMap.js +5 -1
- package/src/App/store/appActionsMap.test.js +13 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +13 -1
- package/src/InteractiveMap/InteractiveMap.test.js +6 -4
- package/src/InteractiveMap/behaviourController.js +1 -2
- package/src/InteractiveMap/buttonManager.js +1 -3
- package/src/InteractiveMap/buttonManager.test.js +16 -8
- package/src/InteractiveMap/domStateManager.js +2 -3
- package/src/InteractiveMap/historyManager.js +4 -3
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +3 -2
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- package/src/config/mergeConfig.js +8 -2
- package/src/config/mergeConfig.test.js +19 -0
- package/src/config/symbolConfig.js +23 -16
- package/src/scss/main.scss +3 -0
- package/src/scss/tools/_border-focus.scss +0 -1
- package/src/services/eventBus.js +15 -0
- package/src/services/eventBus.test.js +11 -0
- package/src/services/hints.js +47 -0
- package/src/services/hints.test.js +110 -0
- package/src/services/patternRegistry.js +69 -1
- package/src/services/patternRegistry.test.js +112 -1
- package/src/services/reverseGeocode.js +4 -0
- package/src/services/reverseGeocode.test.js +11 -1
- package/src/services/symbolRegistry.js +152 -17
- package/src/services/symbolRegistry.test.js +141 -31
- package/src/types.js +93 -16
- package/src/utils/constrainKeyboardFocus.js +5 -5
- package/src/utils/detectInterfaceType.js +27 -5
- package/src/utils/detectInterfaceType.test.js +29 -3
- package/src/utils/mapStateSync.js +2 -2
- package/src/utils/mapStateSync.test.js +18 -0
- package/src/utils/patternUtils.js +4 -67
- package/src/utils/patternUtils.test.js +1 -123
- package/src/utils/symbolUtils.js +11 -13
- package/src/utils/symbolUtils.test.js +18 -26
- package/webpack.dev.mjs +4 -0
- package/webpack.umd.mjs +9 -6
- package/plugins/beta/datasets/adapters/maplibre/dist/esm/im-datasets-ml-adapter.js +0 -1
- package/plugins/beta/datasets/adapters/maplibre/dist/umd/im-datasets-ml-adapter.js +0 -2
- package/plugins/beta/datasets/adapters/maplibre/dist/umd/index.js +0 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +0 -2
- package/plugins/beta/datasets/dist/umd/index.js +0 -2
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
- /package/plugins/beta/datasets/{adapters/maplibre/dist → dist/adapters/maplibre}/esm/index.js +0 -0
- /package/plugins/beta/datasets/{adapters/maplibre/dist/umd/im-datasets-ml-adapter.js.LICENSE.txt → dist/umd/maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/dist/umd/{im-datasets-plugin.js.LICENSE.txt → maplibre/im-datasets-plugin.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/dist/umd/{index.js.LICENSE.txt → maplibre/index.js.LICENSE.txt} +0 -0
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
2
2
|
import { symbolDefaults, pin, circle, square, graphics } from '../config/symbolConfig.js'
|
|
3
|
-
import {
|
|
3
|
+
import { getSymbolStyleColors, getSymbolViewBox } from '../utils/symbolUtils.js'
|
|
4
|
+
import { THEME_COLORS } from '../config/mapTheme.js'
|
|
5
|
+
import { rasteriseToImageData } from '../../providers/maplibre/src/utils/rasteriseToImageData.js'
|
|
4
6
|
|
|
5
7
|
const symbols = new Map()
|
|
8
|
+
// Module-level cache: imageId → ImageData. Avoids re-rasterising identical symbols.
|
|
9
|
+
const imageDataCache = new Map()
|
|
6
10
|
let _constructorDefaults = {}
|
|
7
11
|
|
|
12
|
+
const HASH_BASE = 36
|
|
13
|
+
|
|
14
|
+
const hashString = (str) => {
|
|
15
|
+
let hash = 0
|
|
16
|
+
for (const ch of str) {
|
|
17
|
+
hash = Math.trunc(((hash << 5) - hash) + ch.codePointAt(0))
|
|
18
|
+
}
|
|
19
|
+
return Math.abs(hash).toString(HASH_BASE)
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
// Keys that are structural — not token values for SVG substitution
|
|
9
23
|
const STRUCTURAL = new Set(['id', 'svg', 'viewBox', 'anchor', 'symbol', 'symbolSvgContent'])
|
|
10
24
|
|
|
11
|
-
//
|
|
12
|
-
// selectedColor is a map style concern — always injected from mapStyle, never from cascade.
|
|
13
|
-
const REGISTRY_EXCLUDED = new Set([...STRUCTURAL, 'selectedWidth'])
|
|
25
|
+
// selectedColor and activeColor are map style concerns — always injected from mapStyle, never from cascade.
|
|
14
26
|
|
|
15
27
|
function resolveValues (symbolDef, markerValues, mapStyle) {
|
|
16
28
|
const mapStyleId = mapStyle?.id
|
|
17
29
|
const symbolTokens = Object.fromEntries(
|
|
18
|
-
Object.entries(symbolDef || {}).filter(([k]) => !
|
|
30
|
+
Object.entries(symbolDef || {}).filter(([k]) => !STRUCTURAL.has(k))
|
|
19
31
|
)
|
|
20
32
|
const constructorTokens = Object.fromEntries(
|
|
21
33
|
Object.entries(_constructorDefaults).filter(([k]) => !STRUCTURAL.has(k))
|
|
@@ -24,10 +36,11 @@ function resolveValues (symbolDef, markerValues, mapStyle) {
|
|
|
24
36
|
Object.entries(markerValues).filter(([, v]) => v != null)
|
|
25
37
|
)
|
|
26
38
|
const merged = { ...symbolDefaults, ...constructorTokens, ...symbolTokens, ...defined }
|
|
27
|
-
// haloColor and
|
|
28
|
-
const scheme =
|
|
39
|
+
// haloColor, selectedColor and activeColor are map style concerns — always injected from mapStyle, never from the cascade
|
|
40
|
+
const scheme = THEME_COLORS[mapStyle?.mapColorScheme] ?? THEME_COLORS.light
|
|
29
41
|
merged.haloColor = mapStyle?.haloColor ?? scheme.haloColor
|
|
30
42
|
merged.selectedColor = mapStyle?.selectedColor ?? scheme.selectedColor
|
|
43
|
+
merged.activeColor = mapStyle?.activeColor ?? scheme.activeColor
|
|
31
44
|
if (typeof merged.graphic === 'string' && graphics[merged.graphic]) {
|
|
32
45
|
merged.graphic = graphics[merged.graphic]
|
|
33
46
|
}
|
|
@@ -77,37 +90,159 @@ export const symbolRegistry = {
|
|
|
77
90
|
},
|
|
78
91
|
|
|
79
92
|
/**
|
|
80
|
-
*
|
|
81
|
-
|
|
93
|
+
* Clears all registered symbols (including built-ins). Mainly for testing purposes.
|
|
94
|
+
*/
|
|
95
|
+
clear () {
|
|
96
|
+
symbols.clear()
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clears all registered symbols (including built-ins). Mainly for testing purposes.
|
|
101
|
+
*/
|
|
102
|
+
initialise () {
|
|
103
|
+
this.register(pin)
|
|
104
|
+
this.register(circle)
|
|
105
|
+
this.register(square)
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a symbol's SVG string for normal (unselected, inactive) rendering.
|
|
110
|
+
* Both selectedColor and activeColor are set to 'none' — all rings hidden.
|
|
82
111
|
*
|
|
83
112
|
* @param {Object} symbolDef - Symbol definition
|
|
84
113
|
* @param {Object} styleColors - Token overrides
|
|
85
|
-
* @param {Object} mapStyle - Current map style config (provides selectedColor, haloColor)
|
|
114
|
+
* @param {Object} mapStyle - Current map style config (provides selectedColor, activeColor, haloColor)
|
|
86
115
|
* @returns {string} Resolved SVG string
|
|
87
116
|
*/
|
|
88
117
|
resolve (symbolDef, styleColors, mapStyle) {
|
|
89
118
|
const colors = resolveValues(symbolDef, styleColors || {}, mapStyle)
|
|
90
119
|
if (!symbolDef) { return '' }
|
|
91
|
-
colors.selectedColor = ''
|
|
120
|
+
colors.selectedColor = 'none'
|
|
121
|
+
colors.activeColor = 'none'
|
|
92
122
|
return resolveLayer(symbolDef.svg, colors)
|
|
93
123
|
},
|
|
94
124
|
|
|
95
125
|
/**
|
|
96
|
-
* Resolve a symbol's SVG string for
|
|
97
|
-
* selectedColor
|
|
126
|
+
* Resolve a symbol's SVG string for active (keyboard cursor) rendering.
|
|
127
|
+
* Both selectedColor (committed ring) and activeColor (focus ring) are shown simultaneously,
|
|
128
|
+
* so an item that is active always displays both rings regardless of selection state.
|
|
98
129
|
*
|
|
99
130
|
* @param {Object} symbolDef - Symbol definition
|
|
100
131
|
* @param {Object} styleColors - Token overrides
|
|
101
|
-
* @param {Object} mapStyle - Current map style config (provides selectedColor, haloColor)
|
|
132
|
+
* @param {Object} mapStyle - Current map style config (provides selectedColor, activeColor, haloColor)
|
|
133
|
+
* @returns {string} Resolved SVG string
|
|
134
|
+
*/
|
|
135
|
+
resolveActive (symbolDef, styleColors, mapStyle) {
|
|
136
|
+
const colors = resolveValues(symbolDef, styleColors || {}, mapStyle)
|
|
137
|
+
if (!symbolDef) { return '' }
|
|
138
|
+
return resolveLayer(symbolDef.svg, colors)
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a symbol's SVG string for committed-selection rendering.
|
|
143
|
+
* selectedColor (committed ring) is shown; activeColor is set to 'none'.
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} symbolDef - Symbol definition
|
|
146
|
+
* @param {Object} styleColors - Token overrides
|
|
147
|
+
* @param {Object} mapStyle - Current map style config (provides selectedColor, activeColor, haloColor)
|
|
102
148
|
* @returns {string} Resolved SVG string
|
|
103
149
|
*/
|
|
104
150
|
resolveSelected (symbolDef, styleColors, mapStyle) {
|
|
105
151
|
const colors = resolveValues(symbolDef, styleColors || {}, mapStyle)
|
|
106
152
|
if (!symbolDef) { return '' }
|
|
153
|
+
colors.activeColor = 'none'
|
|
107
154
|
return resolveLayer(symbolDef.svg, colors)
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// ─── Image IDs ────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns a deterministic image ID for a symbol in normal or selected state.
|
|
161
|
+
* Based on the hash of the fully resolved SVG content and the pixel ratio.
|
|
162
|
+
*
|
|
163
|
+
* @param {Object} style
|
|
164
|
+
* @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor)
|
|
165
|
+
* @param {boolean} [selected=false]
|
|
166
|
+
* @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor
|
|
167
|
+
* @returns {string|null}
|
|
168
|
+
*/
|
|
169
|
+
getSymbolImageId (style, mapStyle, active = false, pixelRatio = 2) {
|
|
170
|
+
const symbolDef = this.getSymbolDef(style)
|
|
171
|
+
if (!symbolDef) {
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
const styleColors = getSymbolStyleColors(style)
|
|
175
|
+
const resolved = active
|
|
176
|
+
? this.resolveActive(symbolDef, styleColors, mapStyle)
|
|
177
|
+
: this.resolve(symbolDef, styleColors, mapStyle)
|
|
178
|
+
return `symbol-${active ? 'act-' : ''}${hashString(resolved)}-${pixelRatio}x`
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolves the symbolDef for a style's symbol config.
|
|
183
|
+
*
|
|
184
|
+
* style.symbol is a string symbol ID (e.g. 'pin').
|
|
185
|
+
* style.symbolSvgContent is inline SVG content for a custom symbol.
|
|
186
|
+
*
|
|
187
|
+
* @param {Object} style
|
|
188
|
+
* @returns {Object|undefined}
|
|
189
|
+
*/
|
|
190
|
+
getSymbolDef (style) {
|
|
191
|
+
if (style.symbolSvgContent) {
|
|
192
|
+
return { svg: style.symbolSvgContent }
|
|
193
|
+
}
|
|
194
|
+
if (style.symbol) {
|
|
195
|
+
return this.get(style.symbol)
|
|
196
|
+
}
|
|
197
|
+
return undefined
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Rasterise one variant of a symbol to ImageData for use as a MapLibre image.
|
|
202
|
+
* Results are cached by imageId so identical symbols are only rendered once.
|
|
203
|
+
*
|
|
204
|
+
* @param {Object} style - Dataset or marker config with symbol properties
|
|
205
|
+
* @param {Object} mapStyle - Current map style config
|
|
206
|
+
* @param {'normal'|'active'|'selected'} variant
|
|
207
|
+
* - `'normal'` — no rings (default display)
|
|
208
|
+
* - `'active'` — both rings (keyboard cursor, yellow + black)
|
|
209
|
+
* - `'selected'` — selected ring only (black)
|
|
210
|
+
* @param {number} pixelRatio - Device pixel ratio × map size scale factor
|
|
211
|
+
* @returns {Promise<{imageId: string, imageData: ImageData}|null>}
|
|
212
|
+
*/
|
|
213
|
+
async rasteriseSymbolImage (style, mapStyle, variant, pixelRatio) {
|
|
214
|
+
const symbolDef = this.getSymbolDef(style)
|
|
215
|
+
if (!symbolDef) {
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
const styleColors = getSymbolStyleColors(style)
|
|
219
|
+
let resolvedContent, prefix
|
|
220
|
+
if (variant === 'active') {
|
|
221
|
+
resolvedContent = this.resolveActive(symbolDef, styleColors, mapStyle)
|
|
222
|
+
prefix = 'act-'
|
|
223
|
+
} else if (variant === 'selected') {
|
|
224
|
+
resolvedContent = this.resolveSelected(symbolDef, styleColors, mapStyle)
|
|
225
|
+
prefix = 'sel-'
|
|
226
|
+
} else {
|
|
227
|
+
resolvedContent = this.resolve(symbolDef, styleColors, mapStyle)
|
|
228
|
+
prefix = ''
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const imageId = `symbol-${prefix}${hashString(resolvedContent)}-${pixelRatio}x`
|
|
232
|
+
|
|
233
|
+
let imageData = imageDataCache.get(imageId)
|
|
234
|
+
if (!imageData) {
|
|
235
|
+
const viewBox = getSymbolViewBox(style, symbolDef)
|
|
236
|
+
const [,, width, height] = viewBox.split(' ').map(Number)
|
|
237
|
+
// Render at pixelRatio× to keep icons crisp at the current device DPI and map size.
|
|
238
|
+
// MapLibre receives the matching pixelRatio so the image displays at its original logical size.
|
|
239
|
+
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="${width * pixelRatio}" height="${height * pixelRatio}" viewBox="${viewBox}">${resolvedContent}</svg>`
|
|
240
|
+
imageData = await rasteriseToImageData(svgString, width * pixelRatio, height * pixelRatio)
|
|
241
|
+
imageDataCache.set(imageId, imageData)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { imageId, imageData }
|
|
108
245
|
}
|
|
109
246
|
}
|
|
110
247
|
|
|
111
|
-
symbolRegistry.
|
|
112
|
-
symbolRegistry.register(circle)
|
|
113
|
-
symbolRegistry.register(square)
|
|
248
|
+
symbolRegistry.initialise()
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { symbolRegistry } from './symbolRegistry.js'
|
|
2
|
-
import { symbolDefaults } from '../config/symbolConfig.js'
|
|
3
|
-
import {
|
|
2
|
+
import { symbolDefaults, pin } from '../config/symbolConfig.js'
|
|
3
|
+
import { THEME_COLORS } from '../config/mapTheme.js'
|
|
4
4
|
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
5
5
|
|
|
6
6
|
const STYLE_ID = 'test'
|
|
7
7
|
const mapStyle = { id: STYLE_ID }
|
|
8
|
+
const COLOR_OVERRIDE = '#ff0000'
|
|
9
|
+
const FILL_OVERRIDE = `fill="${COLOR_OVERRIDE}"`
|
|
8
10
|
|
|
9
11
|
beforeEach(() => {
|
|
12
|
+
symbolRegistry.clear()
|
|
13
|
+
symbolRegistry.initialise()
|
|
10
14
|
symbolRegistry.setDefaults({})
|
|
11
15
|
})
|
|
12
16
|
|
|
@@ -58,20 +62,20 @@ describe('symbolRegistry — setDefaults / getDefaults', () => {
|
|
|
58
62
|
})
|
|
59
63
|
|
|
60
64
|
it('constructor defaults override hardcoded defaults', () => {
|
|
61
|
-
symbolRegistry.setDefaults({ backgroundColor:
|
|
65
|
+
symbolRegistry.setDefaults({ backgroundColor: COLOR_OVERRIDE, symbol: 'circle' })
|
|
62
66
|
const defaults = symbolRegistry.getDefaults()
|
|
63
|
-
expect(defaults.backgroundColor).toBe(
|
|
67
|
+
expect(defaults.backgroundColor).toBe(COLOR_OVERRIDE)
|
|
64
68
|
expect(defaults.symbol).toBe('circle')
|
|
65
69
|
})
|
|
66
70
|
|
|
67
71
|
it('constructor defaults do not affect unset properties', () => {
|
|
68
|
-
symbolRegistry.setDefaults({ backgroundColor:
|
|
72
|
+
symbolRegistry.setDefaults({ backgroundColor: COLOR_OVERRIDE })
|
|
69
73
|
const defaults = symbolRegistry.getDefaults()
|
|
70
74
|
expect(defaults.foregroundColor).toBe(symbolDefaults.foregroundColor)
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
it('setDefaults with null or undefined resets to hardcoded defaults', () => {
|
|
74
|
-
symbolRegistry.setDefaults({ backgroundColor:
|
|
78
|
+
symbolRegistry.setDefaults({ backgroundColor: COLOR_OVERRIDE })
|
|
75
79
|
symbolRegistry.setDefaults(null)
|
|
76
80
|
expect(symbolRegistry.getDefaults().backgroundColor).toBe(symbolDefaults.backgroundColor)
|
|
77
81
|
})
|
|
@@ -81,24 +85,23 @@ describe('symbolRegistry — resolve', () => {
|
|
|
81
85
|
const BACKGROUND_SVG = '<path fill="{{backgroundColor}}"/>'
|
|
82
86
|
const symbolDef = {
|
|
83
87
|
id: 'test',
|
|
84
|
-
svg: '<path fill="{{backgroundColor}}" stroke="{{haloColor}}"
|
|
88
|
+
svg: '<path fill="{{backgroundColor}}" stroke="{{haloColor}}"/><path fill="{{foregroundColor}}" stroke="{{selectedColor}}"/><path stroke="{{activeColor}}"/>'
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
it('injects default token values when no overrides given', () => {
|
|
88
92
|
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
89
93
|
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.backgroundColor, STYLE_ID)}"`)
|
|
90
94
|
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.foregroundColor, STYLE_ID)}"`)
|
|
91
|
-
expect(resolved).toContain(`stroke-width="${symbolDefaults.haloWidth}"`)
|
|
92
95
|
})
|
|
93
96
|
|
|
94
|
-
it('always produces empty selectedColor
|
|
97
|
+
it('always produces empty selectedColor and activeColor tokens — rings are hidden', () => {
|
|
95
98
|
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
96
|
-
expect(resolved).toContain('stroke=""')
|
|
99
|
+
expect(resolved).toContain('stroke="none"')
|
|
97
100
|
})
|
|
98
101
|
|
|
99
102
|
it('uses light scheme haloColor when mapStyle has no haloColor', () => {
|
|
100
103
|
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
101
|
-
expect(resolved).toContain(`stroke="${
|
|
104
|
+
expect(resolved).toContain(`stroke="${THEME_COLORS.light.haloColor}"`)
|
|
102
105
|
})
|
|
103
106
|
|
|
104
107
|
it('uses mapStyle.haloColor when provided', () => {
|
|
@@ -107,8 +110,8 @@ describe('symbolRegistry — resolve', () => {
|
|
|
107
110
|
})
|
|
108
111
|
|
|
109
112
|
it('overrides default backgroundColor with a plain string', () => {
|
|
110
|
-
const resolved = symbolRegistry.resolve(symbolDef, { backgroundColor:
|
|
111
|
-
expect(resolved).toContain(
|
|
113
|
+
const resolved = symbolRegistry.resolve(symbolDef, { backgroundColor: COLOR_OVERRIDE }, mapStyle)
|
|
114
|
+
expect(resolved).toContain(FILL_OVERRIDE)
|
|
112
115
|
})
|
|
113
116
|
|
|
114
117
|
it('overrides default with a style-keyed color', () => {
|
|
@@ -163,48 +166,70 @@ describe('symbolRegistry — resolve', () => {
|
|
|
163
166
|
})
|
|
164
167
|
})
|
|
165
168
|
|
|
166
|
-
describe('symbolRegistry —
|
|
169
|
+
describe('symbolRegistry — resolveActive (keyboard cursor state)', () => {
|
|
167
170
|
const symbolDef = {
|
|
168
|
-
id: 'test-
|
|
169
|
-
svg: '<path
|
|
171
|
+
id: 'test-active',
|
|
172
|
+
svg: '<path fill="{{selectedColor}}" stroke="{{activeColor}}" stroke-width="6"/><path fill="{{backgroundColor}}"/>'
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
it('
|
|
173
|
-
const resolved = symbolRegistry.
|
|
174
|
-
expect(resolved).toContain(`
|
|
175
|
+
it('renders selectedColor from scheme when mapStyle has no selectedColor', () => {
|
|
176
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, {}, mapStyle)
|
|
177
|
+
expect(resolved).toContain(`fill="${THEME_COLORS.light.selectedColor}"`)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('renders activeColor from scheme when mapStyle has no activeColor', () => {
|
|
181
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, {}, mapStyle)
|
|
182
|
+
expect(resolved).toContain(`stroke="${THEME_COLORS.light.activeColor}"`)
|
|
175
183
|
})
|
|
176
184
|
|
|
177
185
|
it('uses mapStyle.selectedColor when provided', () => {
|
|
178
|
-
const resolved = symbolRegistry.
|
|
179
|
-
expect(resolved).toContain(
|
|
186
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, {}, { id: STYLE_ID, selectedColor: COLOR_OVERRIDE })
|
|
187
|
+
expect(resolved).toContain(FILL_OVERRIDE)
|
|
180
188
|
})
|
|
181
189
|
|
|
182
|
-
it('uses
|
|
183
|
-
const resolved = symbolRegistry.
|
|
184
|
-
expect(resolved).toContain(
|
|
190
|
+
it('uses mapStyle.activeColor when provided', () => {
|
|
191
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, {}, { id: STYLE_ID, activeColor: '#00ff00' })
|
|
192
|
+
expect(resolved).toContain('stroke="#00ff00"')
|
|
185
193
|
})
|
|
186
194
|
|
|
187
195
|
it('handles null styleColors — uses cascade defaults', () => {
|
|
188
|
-
const resolved = symbolRegistry.
|
|
189
|
-
expect(resolved).toContain(`
|
|
196
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, null, mapStyle)
|
|
197
|
+
expect(resolved).toContain(`fill="${THEME_COLORS.light.selectedColor}"`)
|
|
190
198
|
})
|
|
191
199
|
|
|
192
200
|
it('returns empty string for null symbolDef', () => {
|
|
193
|
-
expect(symbolRegistry.
|
|
201
|
+
expect(symbolRegistry.resolveActive(null, {}, mapStyle)).toBe('')
|
|
194
202
|
})
|
|
195
203
|
|
|
196
204
|
it('symbol-level selectedColor is ignored — mapStyle wins', () => {
|
|
197
205
|
const defWithSelected = { ...symbolDef, selectedColor: '#00ff00' }
|
|
198
|
-
const resolved = symbolRegistry.
|
|
199
|
-
expect(resolved).toContain(
|
|
206
|
+
const resolved = symbolRegistry.resolveActive(defWithSelected, {}, { id: STYLE_ID, selectedColor: COLOR_OVERRIDE })
|
|
207
|
+
expect(resolved).toContain(FILL_OVERRIDE)
|
|
200
208
|
})
|
|
201
209
|
|
|
202
210
|
it('still resolves other tokens correctly', () => {
|
|
203
|
-
const resolved = symbolRegistry.
|
|
211
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, { backgroundColor: '#d4351c' }, mapStyle)
|
|
204
212
|
expect(resolved).toContain('fill="#d4351c"')
|
|
205
213
|
})
|
|
206
214
|
})
|
|
207
215
|
|
|
216
|
+
describe('symbolRegistry — resolveSelected (committed selection state)', () => {
|
|
217
|
+
const symbolDef = {
|
|
218
|
+
id: 'test-selected',
|
|
219
|
+
svg: '<path fill="{{selectedColor}}"/>'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
it('returns empty string for null symbolDef', () => {
|
|
223
|
+
expect(symbolRegistry.resolveSelected(null, {}, mapStyle)).toBe('')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('handles null styleColors — uses cascade defaults', () => {
|
|
227
|
+
const resolved = symbolRegistry.resolveSelected(symbolDef, null, mapStyle)
|
|
228
|
+
expect(typeof resolved).toBe('string')
|
|
229
|
+
expect(resolved.length).toBeGreaterThan(0)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
208
233
|
describe('symbolRegistry — graphic token', () => {
|
|
209
234
|
const graphicDef = {
|
|
210
235
|
id: 'test-graphic',
|
|
@@ -257,6 +282,91 @@ describe('symbolRegistry — graphic token', () => {
|
|
|
257
282
|
const pin = symbolRegistry.get('pin')
|
|
258
283
|
const resolved = symbolRegistry.resolve(pin, {}, mapStyle)
|
|
259
284
|
expect(resolved).toContain(`d="${pin.graphic}"`)
|
|
260
|
-
expect(resolved).toContain('translate(
|
|
285
|
+
expect(resolved).toContain('translate(22, 19) scale(0.8) translate(-8, -8)')
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// ─── getSymbolDef ─────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
describe('getSymbolDef', () => {
|
|
292
|
+
it('returns undefined when dataset has no symbol', () => {
|
|
293
|
+
expect(symbolRegistry.getSymbolDef({})).toBeUndefined()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('looks up string symbol id in the registry', () => {
|
|
297
|
+
expect(symbolRegistry.getSymbolDef({ symbol: 'pin' })).toBe(pin)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('returns undefined for an unregistered string symbol', () => {
|
|
301
|
+
expect(symbolRegistry.getSymbolDef({ symbol: 'missing' })).toBeUndefined()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('returns inline def from symbolSvgContent with svg key', () => {
|
|
305
|
+
const dataset = { symbolSvgContent: '<circle/>', symbolViewBox: '0 0 10 10' }
|
|
306
|
+
const result = symbolRegistry.getSymbolDef(dataset)
|
|
307
|
+
expect(result.svg).toBe('<circle/>')
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('symbolSvgContent takes precedence over symbol id', () => {
|
|
311
|
+
const result = symbolRegistry.getSymbolDef({ symbol: 'pin', symbolSvgContent: '<circle/>' })
|
|
312
|
+
expect(result.svg).toBe('<circle/>')
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// ─── getSymbolImageId ─────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
describe('getSymbolImageId', () => {
|
|
319
|
+
it('returns null when dataset has no symbol', () => {
|
|
320
|
+
expect(symbolRegistry.getSymbolImageId({}, mapStyle)).toBeNull()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('returns null for an unregistered symbol id', () => {
|
|
324
|
+
expect(symbolRegistry.getSymbolImageId({ symbol: 'does-not-exist' }, mapStyle)).toBeNull()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('returns a string prefixed symbol- for normal state', () => {
|
|
328
|
+
const id = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle)
|
|
329
|
+
expect(typeof id).toBe('string')
|
|
330
|
+
expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('returns a string prefixed symbol-act- for active state', () => {
|
|
334
|
+
const id = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, true)
|
|
335
|
+
expect(typeof id).toBe('string')
|
|
336
|
+
expect(id).toMatch(/^symbol-act-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('normal and active ids differ for the same dataset', () => {
|
|
340
|
+
const normalId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, false)
|
|
341
|
+
const activeId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, true)
|
|
342
|
+
expect(normalId).not.toBe(activeId)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('same dataset and style always produces the same id', () => {
|
|
346
|
+
const id1 = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle)
|
|
347
|
+
const id2 = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle)
|
|
348
|
+
expect(id1).toBe(id2)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('different symbols produce different ids', () => {
|
|
352
|
+
const pinId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle)
|
|
353
|
+
const circleId = symbolRegistry.getSymbolImageId({ symbol: 'circle' }, mapStyle)
|
|
354
|
+
expect(pinId).not.toBe(circleId)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('different backgrounds produce different ids', () => {
|
|
358
|
+
const redId = symbolRegistry.getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#ff0000' }, mapStyle)
|
|
359
|
+
const blueId = symbolRegistry.getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#0000ff' }, mapStyle)
|
|
360
|
+
expect(redId).not.toBe(blueId)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('resolves inline symbolSvgContent', () => {
|
|
364
|
+
const dataset = {
|
|
365
|
+
symbolSvgContent: '<circle cx="19" cy="19" r="12" fill="{{backgroundColor}}"/>',
|
|
366
|
+
symbolViewBox: '0 0 38 38',
|
|
367
|
+
symbolAnchor: [0.5, 0.5]
|
|
368
|
+
}
|
|
369
|
+
const id = symbolRegistry.getSymbolImageId(dataset, mapStyle)
|
|
370
|
+
expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
261
371
|
})
|
|
262
372
|
})
|