@defra/interactive-map 0.0.22-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/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 +24 -2
- 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/interact.md +4 -11
- package/docusaurus.config.cjs +2 -1
- package/package.json +8 -8
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -1
- 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 +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/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/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 +1 -1
- package/providers/maplibre/src/index.test.js +9 -14
- 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/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/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/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 +12 -0
- package/src/InteractiveMap/InteractiveMap.test.js +2 -0
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +1 -1
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- 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 +71 -14
- 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/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/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BUILT_IN_PATTERNS } from '../config/patternConfig.js'
|
|
2
|
-
|
|
2
|
+
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
3
|
+
import { KEY_BORDER_PATH, getEffectivePixelRatio, injectColors, hashString } from '../utils/patternUtils.js'
|
|
3
4
|
const patterns = new Map()
|
|
4
5
|
|
|
5
6
|
export const patternRegistry = {
|
|
@@ -31,6 +32,73 @@ export const patternRegistry = {
|
|
|
31
32
|
*/
|
|
32
33
|
list () {
|
|
33
34
|
return [...patterns.values()]
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clears all registered patterns (including built-ins). Mainly for testing purposes.
|
|
39
|
+
*/
|
|
40
|
+
clear () {
|
|
41
|
+
patterns.clear()
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the raw (un-coloured) inner SVG content for a style's pattern.
|
|
46
|
+
* Precedence: inline fillPatternSvgContent → named fillPattern from registry.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} style
|
|
49
|
+
* @returns {string|null}
|
|
50
|
+
*/
|
|
51
|
+
getPatternInnerContent (style) {
|
|
52
|
+
if (style.fillPatternSvgContent) {
|
|
53
|
+
return style.fillPatternSvgContent
|
|
54
|
+
}
|
|
55
|
+
if (style.fillPattern) {
|
|
56
|
+
return this.get(style.fillPattern)?.svgContent ?? null
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns colour-injected SVG path content for use in Key panel pattern symbols.
|
|
63
|
+
* Returns { border, content } where border is the rounded-rect outline and content
|
|
64
|
+
* is the pattern fill. Returns null if the style has no pattern.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} style
|
|
67
|
+
* @param {string} mapStyleId
|
|
68
|
+
* @returns {{ border: string, content: string }|null}
|
|
69
|
+
*/
|
|
70
|
+
getKeyPatternPaths (style, mapStyleId) {
|
|
71
|
+
const innerContent = this.getPatternInnerContent(style)
|
|
72
|
+
if (!innerContent) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
const fg = getValueForStyle(style.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
76
|
+
const bg = getValueForStyle(style.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
77
|
+
const borderStroke = getValueForStyle(style.stroke, mapStyleId) || fg
|
|
78
|
+
const keyPatternPaths = {
|
|
79
|
+
border: injectColors(KEY_BORDER_PATH, borderStroke, bg),
|
|
80
|
+
content: injectColors(innerContent, fg, bg)
|
|
81
|
+
}
|
|
82
|
+
return keyPatternPaths
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns a deterministic image ID for a pattern + resolved colour + pixel ratio combination.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} dataset
|
|
89
|
+
* @param {string} mapStyleId
|
|
90
|
+
* @param {number} [pixelRatio=1]
|
|
91
|
+
* @returns {string|null}
|
|
92
|
+
*/
|
|
93
|
+
getPatternImageId (dataset, mapStyleId, pixelRatio = 1) {
|
|
94
|
+
const innerContent = this.getPatternInnerContent(dataset)
|
|
95
|
+
if (!innerContent) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
99
|
+
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
100
|
+
const effectiveRatio = getEffectivePixelRatio(pixelRatio)
|
|
101
|
+
return `pattern-${hashString(innerContent + fg + bg)}-${effectiveRatio}x`
|
|
34
102
|
}
|
|
35
103
|
}
|
|
36
104
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { patternRegistry } from './patternRegistry.js'
|
|
2
|
-
|
|
2
|
+
import { KEY_BORDER_PATH } from '../utils/patternUtils.js'
|
|
3
3
|
describe('patternRegistry', () => {
|
|
4
4
|
describe('built-in patterns', () => {
|
|
5
5
|
test.each([
|
|
@@ -45,4 +45,115 @@ describe('patternRegistry', () => {
|
|
|
45
45
|
expect(all.every(p => p.id && p.svgContent)).toBe(true)
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
describe('getPatternInnerContent', () => {
|
|
50
|
+
test('returns fillPatternSvgContent when set (inline SVG takes precedence)', () => {
|
|
51
|
+
const style = { fillPatternSvgContent: '<path d="custom"/>', fillPattern: 'dot' }
|
|
52
|
+
expect(patternRegistry.getPatternInnerContent(style)).toBe('<path d="custom"/>')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('returns svgContent from registry for a named fillPattern', () => {
|
|
56
|
+
const style = { fillPattern: 'dot' }
|
|
57
|
+
patternRegistry.register('dot', '<path d="M4 4" fill="{{foregroundColor}}"/>')
|
|
58
|
+
expect(patternRegistry.getPatternInnerContent(style)).toBe('<path d="M4 4" fill="{{foregroundColor}}"/>')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('returns null for an unregistered fillPattern name', () => {
|
|
62
|
+
const style = { fillPattern: 'unknown-pattern' }
|
|
63
|
+
expect(patternRegistry.getPatternInnerContent(style)).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('returns null when no pattern is configured', () => {
|
|
67
|
+
expect(patternRegistry.getPatternInnerContent({})).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('getKeyPatternPaths', () => {
|
|
72
|
+
test('returns border and content strings with colours injected', () => {
|
|
73
|
+
const dataset = {
|
|
74
|
+
fillPattern: 'dot',
|
|
75
|
+
fillPatternForegroundColor: 'red',
|
|
76
|
+
fillPatternBackgroundColor: 'white',
|
|
77
|
+
stroke: 'black'
|
|
78
|
+
}
|
|
79
|
+
const result = patternRegistry.getKeyPatternPaths(dataset, 'style-a')
|
|
80
|
+
expect(result).not.toBeNull()
|
|
81
|
+
expect(result.border).toContain('black') // stroke colour
|
|
82
|
+
expect(result.border).toContain('white') // background colour
|
|
83
|
+
expect(result.content).toContain('red') // foreground colour
|
|
84
|
+
expect(result.border).not.toContain('{{foregroundColor}}')
|
|
85
|
+
expect(result.content).not.toContain('{{foregroundColor}}')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('returns null when no pattern content is found', () => {
|
|
89
|
+
expect(patternRegistry.getKeyPatternPaths({ fillPattern: 'unknown' }, 'style-a')).toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('falls back to "black" fg and "transparent" bg when colour properties are absent', () => {
|
|
93
|
+
const result = patternRegistry.getKeyPatternPaths({ fillPattern: 'dot' }, 'style-a')
|
|
94
|
+
expect(result).not.toBeNull()
|
|
95
|
+
expect(result.content).toContain('black')
|
|
96
|
+
expect(result.border).toContain('transparent')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('border stroke falls back to foreground colour when stroke is absent', () => {
|
|
100
|
+
const result = patternRegistry.getKeyPatternPaths(
|
|
101
|
+
{ fillPattern: 'dot', fillPatternForegroundColor: 'green' },
|
|
102
|
+
'style-a'
|
|
103
|
+
)
|
|
104
|
+
expect(result).not.toBeNull()
|
|
105
|
+
// borderStroke falls back to fg ('green'), so the border uses green for both stroke and background
|
|
106
|
+
expect(result.border).toContain('green')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('KEY_BORDER_PATH contains foregroundColor and backgroundColor tokens', () => {
|
|
110
|
+
expect(KEY_BORDER_PATH).toContain('{{foregroundColor}}')
|
|
111
|
+
expect(KEY_BORDER_PATH).toContain('{{backgroundColor}}')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('patternRegistry.getPatternImageId', () => {
|
|
116
|
+
test('returns a deterministic string id', () => {
|
|
117
|
+
const dataset = { fillPattern: 'dot', fillPatternForegroundColor: 'red', fillPatternBackgroundColor: 'blue' }
|
|
118
|
+
const id = patternRegistry.getPatternImageId(dataset, 'style-a')
|
|
119
|
+
expect(typeof id).toBe('string')
|
|
120
|
+
expect(id).toMatch(/^pattern-/)
|
|
121
|
+
expect(id).toBe(patternRegistry.getPatternImageId(dataset, 'style-a'))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('returns null when no pattern content is found', () => {
|
|
125
|
+
expect(patternRegistry.getPatternImageId({ fillPattern: 'unknown' }, 'style-a')).toBeNull()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('produces different ids for different colours', () => {
|
|
129
|
+
const base = { fillPattern: 'dot' }
|
|
130
|
+
const idA = patternRegistry.getPatternImageId({ ...base, fillPatternForegroundColor: 'red' }, 'style-a')
|
|
131
|
+
const idB = patternRegistry.getPatternImageId({ ...base, fillPatternForegroundColor: 'blue' }, 'style-a')
|
|
132
|
+
expect(idA).not.toBe(idB)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('floors effective ratio at 2 * so low-DPI ids match 2x', () => {
|
|
136
|
+
const dataset = { fillPattern: 'dot' }
|
|
137
|
+
const id1x = patternRegistry.getPatternImageId(dataset, 'style-a', 1)
|
|
138
|
+
expect(id1x).toMatch(/-2x$/)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('produces different ids for pixelRatios above the floor', () => {
|
|
142
|
+
const dataset = { fillPattern: 'dot' }
|
|
143
|
+
const id2x = patternRegistry.getPatternImageId(dataset, 'style-a', 2)
|
|
144
|
+
const id3x = patternRegistry.getPatternImageId(dataset, 'style-a', 3)
|
|
145
|
+
expect(id2x).not.toBe(id3x)
|
|
146
|
+
expect(id2x).toMatch(/-4x$/)
|
|
147
|
+
expect(id3x).toMatch(/-6x$/)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('falls back to "black" foreground and "transparent" background when colours are absent', () => {
|
|
151
|
+
const id = patternRegistry.getPatternImageId({ fillPattern: 'dot' }, 'style-a')
|
|
152
|
+
const idExplicit = patternRegistry.getPatternImageId(
|
|
153
|
+
{ fillPattern: 'dot', fillPatternForegroundColor: 'black', fillPatternBackgroundColor: 'transparent' },
|
|
154
|
+
'style-a'
|
|
155
|
+
)
|
|
156
|
+
expect(id).toBe(idExplicit)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
48
159
|
})
|
|
@@ -7,6 +7,10 @@ export function createReverseGeocode ({ url, transformRequest, load }, crs) {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function hasReverseGeocode () {
|
|
11
|
+
return reverseGeocodeFn !== null
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
export function reverseGeocode (zoom, coord) {
|
|
11
15
|
if (!reverseGeocodeFn) {
|
|
12
16
|
throw new Error('ReverseGeocode not initialised')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// reverseGeocode.test.js
|
|
2
|
-
import { createReverseGeocode, reverseGeocode } from './reverseGeocode'
|
|
2
|
+
import { createReverseGeocode, reverseGeocode, hasReverseGeocode } from './reverseGeocode'
|
|
3
3
|
|
|
4
4
|
describe('reverseGeocode module', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -11,6 +11,10 @@ describe('reverseGeocode module', () => {
|
|
|
11
11
|
expect(() => reverseGeocode(10, [0, 0])).toThrow('ReverseGeocode not initialised')
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
+
it('hasReverseGeocode returns false before initialisation', () => {
|
|
15
|
+
expect(hasReverseGeocode()).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
14
18
|
it('sets reverseGeocodeFn via createReverseGeocode and calls providerFn correctly', async () => {
|
|
15
19
|
const mockProviderFn = jest.fn().mockResolvedValue('result')
|
|
16
20
|
|
|
@@ -44,6 +48,12 @@ describe('reverseGeocode module', () => {
|
|
|
44
48
|
expect(result).toBe('result')
|
|
45
49
|
})
|
|
46
50
|
|
|
51
|
+
it('hasReverseGeocode returns true after initialisation', () => {
|
|
52
|
+
const config = { url: 'x', transformRequest: jest.fn(), load: jest.fn().mockResolvedValue(jest.fn()) }
|
|
53
|
+
createReverseGeocode(config, 'EPSG:4326')
|
|
54
|
+
expect(hasReverseGeocode()).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
47
57
|
it('supports multiple calls after initialisation', async () => {
|
|
48
58
|
const mockProviderFn = jest.fn().mockResolvedValue('ok')
|
|
49
59
|
|
|
@@ -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()
|