@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,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addPatternsToMap } from './patternImages.js'
|
|
2
|
+
import { patternRegistry } from '../../../../src/services/patternRegistry.js'
|
|
2
3
|
|
|
3
4
|
const OUTDOOR = 'outdoor'
|
|
4
5
|
|
|
@@ -30,182 +31,159 @@ const makeMap = (existingIds = []) => ({
|
|
|
30
31
|
addImage: jest.fn()
|
|
31
32
|
})
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
describe('registerPatterns — registration', () => {
|
|
40
|
-
it('returns early and does not touch map for empty configs', async () => {
|
|
41
|
-
const map = makeMap()
|
|
42
|
-
const registry = makePatternRegistry()
|
|
43
|
-
await registerPatterns(map, [], OUTDOOR, registry)
|
|
44
|
-
expect(map.hasImage).not.toHaveBeenCalled()
|
|
45
|
-
expect(map.addImage).not.toHaveBeenCalled()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('calls addImage for an inline fillPatternSvgContent config', async () => {
|
|
49
|
-
const map = makeMap()
|
|
50
|
-
const registry = makePatternRegistry()
|
|
51
|
-
const config = { fillPatternSvgContent: SVG_CONTENT }
|
|
52
|
-
await registerPatterns(map, [config], OUTDOOR, registry)
|
|
53
|
-
expect(map.addImage).toHaveBeenCalledTimes(1)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('skips addImage when image is already registered', async () => {
|
|
57
|
-
const registry = makePatternRegistry()
|
|
58
|
-
const config = { fillPattern: 'stripes' }
|
|
59
|
-
const pixelRatio = 2
|
|
60
|
-
const { getPatternImageId } = await import('../../../../src/utils/patternUtils.js')
|
|
61
|
-
const existingId = getPatternImageId(config, OUTDOOR, registry, pixelRatio)
|
|
62
|
-
const map = makeMap([existingId])
|
|
63
|
-
await registerPatterns(map, [config], OUTDOOR, registry, pixelRatio)
|
|
64
|
-
expect(map.addImage).not.toHaveBeenCalled()
|
|
34
|
+
// ─── addPatternsToMap ─────────────────────────────────────────────────────────
|
|
35
|
+
describe('addPatternsToMap', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
// Clear the registry and re-register a known pattern to ensure consistent test conditions
|
|
38
|
+
patternRegistry.clear()
|
|
39
|
+
patternRegistry.register('stripes', SVG_CONTENT)
|
|
65
40
|
})
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
41
|
+
describe('addPatternsToMap — registration', () => {
|
|
42
|
+
it('returns early and does not touch map for empty configs', async () => {
|
|
43
|
+
const map = makeMap()
|
|
44
|
+
await addPatternsToMap(map, [], OUTDOOR, patternRegistry)
|
|
45
|
+
expect(map.hasImage).not.toHaveBeenCalled()
|
|
46
|
+
expect(map.addImage).not.toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('calls addImage for an inline fillPatternSvgContent config', async () => {
|
|
50
|
+
const map = makeMap()
|
|
51
|
+
const config = { fillPatternSvgContent: SVG_CONTENT }
|
|
52
|
+
await addPatternsToMap(map, [config], OUTDOOR, patternRegistry)
|
|
53
|
+
expect(map.addImage).toHaveBeenCalledTimes(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('skips addImage when image is already registered', async () => {
|
|
57
|
+
const style = { fillPattern: 'stripes' }
|
|
58
|
+
const pixelRatio = 1
|
|
59
|
+
const map = makeMap(['pattern-mpxwil-2x'])
|
|
60
|
+
await addPatternsToMap(map, [style], OUTDOOR, patternRegistry, pixelRatio)
|
|
61
|
+
expect(map.addImage).not.toHaveBeenCalled()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('skips config when pattern has no inner content', async () => {
|
|
65
|
+
const map = makeMap()
|
|
66
|
+
patternRegistry.clear()
|
|
67
|
+
await addPatternsToMap(map, [{ fillPattern: 'unknown' }], OUTDOOR, patternRegistry)
|
|
68
|
+
expect(map.addImage).not.toHaveBeenCalled()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('skips config when neither fillPattern nor fillPatternSvgContent is set', async () => {
|
|
72
|
+
const map = makeMap()
|
|
73
|
+
await addPatternsToMap(map, [{ fillColor: '#ff0000' }], OUTDOOR, patternRegistry)
|
|
74
|
+
expect(map.addImage).not.toHaveBeenCalled()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('processes multiple configs in parallel', async () => {
|
|
78
|
+
const map = makeMap()
|
|
79
|
+
patternRegistry.register('dots', SVG_CONTENT)
|
|
80
|
+
await addPatternsToMap(map, [{ fillPattern: 'stripes' }, { fillPattern: 'dots' }], OUTDOOR, patternRegistry)
|
|
81
|
+
expect(map.addImage).toHaveBeenCalledTimes(2)
|
|
82
|
+
})
|
|
72
83
|
})
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
describe('addPatternsToMap — pixel ratio', () => {
|
|
86
|
+
it('encodes effectiveRatio in the image ID and passes it to addImage', async () => {
|
|
87
|
+
const map = makeMap()
|
|
88
|
+
const config = { fillPattern: 'stripes' }
|
|
89
|
+
await addPatternsToMap(map, [config], OUTDOOR, patternRegistry, 2)
|
|
90
|
+
expect(map.addImage).toHaveBeenCalledTimes(1)
|
|
91
|
+
expect(map.addImage).toHaveBeenCalledWith(
|
|
92
|
+
expect.stringMatching(/^pattern-[a-z0-9]+-4x$/),
|
|
93
|
+
expect.any(Object),
|
|
94
|
+
{ pixelRatio: 4 }
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('floors effectiveRatio at 2 so low-DPI patterns stay crisp', async () => {
|
|
99
|
+
const map = makeMap()
|
|
100
|
+
const config = { fillPattern: 'stripes' }
|
|
101
|
+
await addPatternsToMap(map, [config], OUTDOOR, patternRegistry, 1)
|
|
102
|
+
expect(map.addImage).toHaveBeenCalledWith(
|
|
103
|
+
expect.stringMatching(/-2x$/),
|
|
104
|
+
expect.any(Object),
|
|
105
|
+
{ pixelRatio: 2 }
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('produces different image IDs for ratios above the floor', async () => {
|
|
110
|
+
const map1 = makeMap()
|
|
111
|
+
const map2 = makeMap()
|
|
112
|
+
const config = { fillPattern: 'stripes' }
|
|
113
|
+
const hiDpi = 3
|
|
114
|
+
await addPatternsToMap(map1, [config], OUTDOOR, patternRegistry, 2)
|
|
115
|
+
await addPatternsToMap(map2, [config], OUTDOOR, patternRegistry, hiDpi)
|
|
116
|
+
const [id2x] = map1.addImage.mock.calls[0]
|
|
117
|
+
const [id3x] = map2.addImage.mock.calls[0]
|
|
118
|
+
expect(id2x).not.toBe(id3x)
|
|
119
|
+
expect(id2x).toMatch(/-4x$/)
|
|
120
|
+
expect(id3x).toMatch(/-6x$/)
|
|
121
|
+
})
|
|
79
122
|
})
|
|
80
123
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
124
|
+
describe('addPatternsToMap — color resolution and caching', () => {
|
|
125
|
+
it('applies foreground and background colors when resolving the SVG', async () => {
|
|
126
|
+
const map = makeMap()
|
|
127
|
+
const getContextSpy = HTMLCanvasElement.prototype.getContext
|
|
128
|
+
await addPatternsToMap(
|
|
129
|
+
map,
|
|
130
|
+
[{ fillPattern: 'stripes', fillPatternForegroundColor: '#aabbcc', fillPatternBackgroundColor: '#112233' }],
|
|
131
|
+
OUTDOOR,
|
|
132
|
+
patternRegistry
|
|
133
|
+
)
|
|
134
|
+
expect(map.addImage).toHaveBeenCalledTimes(1)
|
|
135
|
+
expect(getContextSpy).toHaveBeenCalled()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('resolves style-keyed foreground color for the given mapStyleId', async () => {
|
|
139
|
+
const map = makeMap()
|
|
140
|
+
const config = {
|
|
141
|
+
fillPattern: 'stripes',
|
|
142
|
+
fillPatternForegroundColor: { outdoor: '#aabbcc', dark: '#112233' }
|
|
143
|
+
}
|
|
144
|
+
await addPatternsToMap(map, [config], OUTDOOR, patternRegistry)
|
|
145
|
+
const map2 = makeMap()
|
|
146
|
+
await addPatternsToMap(map2, [config], 'dark', patternRegistry)
|
|
147
|
+
const [idOutdoor] = map.addImage.mock.calls[0]
|
|
148
|
+
const [idDark] = map2.addImage.mock.calls[0]
|
|
149
|
+
expect(idOutdoor).not.toBe(idDark)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('uses cached ImageData on second call with identical config', async () => {
|
|
153
|
+
const map = makeMap()
|
|
154
|
+
const config = { fillPattern: 'stripes', fillPatternForegroundColor: '#unique2' }
|
|
155
|
+
const pixelRatio = 2
|
|
156
|
+
await addPatternsToMap(map, [config], OUTDOOR, patternRegistry, pixelRatio)
|
|
157
|
+
const imageId = patternRegistry.getPatternImageId(config, OUTDOOR, pixelRatio)
|
|
158
|
+
const map2 = makeMap([imageId])
|
|
159
|
+
await addPatternsToMap(map2, [config], OUTDOOR, patternRegistry, pixelRatio)
|
|
160
|
+
expect(map2.addImage).not.toHaveBeenCalled()
|
|
161
|
+
})
|
|
92
162
|
})
|
|
93
|
-
})
|
|
94
163
|
|
|
95
|
-
describe('
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const config = { fillPattern: 'stripes' }
|
|
100
|
-
await registerPatterns(map, [config], OUTDOOR, registry, 2)
|
|
101
|
-
expect(map.addImage).toHaveBeenCalledTimes(1)
|
|
102
|
-
expect(map.addImage).toHaveBeenCalledWith(
|
|
103
|
-
expect.stringMatching(/^pattern-[a-z0-9]+-2x$/),
|
|
104
|
-
expect.any(Object),
|
|
105
|
-
{ pixelRatio: 2 }
|
|
106
|
-
)
|
|
107
|
-
})
|
|
164
|
+
describe('addPatternsToMap — null results', () => {
|
|
165
|
+
it('does not call addImage when innerContent becomes unavailable inside rasterisePattern', async () => {
|
|
166
|
+
const getPatternInnerContent = jest.spyOn(patternRegistry, 'getPatternInnerContent').mockReturnValue(undefined)
|
|
167
|
+
const getPatternImageId = jest.spyOn(patternRegistry, 'getPatternImageId').mockReturnValue('test-id')
|
|
108
168
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const config = { fillPattern: 'stripes' }
|
|
113
|
-
await registerPatterns(map, [config], OUTDOOR, registry, 1)
|
|
114
|
-
expect(map.addImage).toHaveBeenCalledWith(
|
|
115
|
-
expect.stringMatching(/-2x$/),
|
|
116
|
-
expect.any(Object),
|
|
117
|
-
{ pixelRatio: 2 }
|
|
118
|
-
)
|
|
119
|
-
})
|
|
169
|
+
const map = makeMap()
|
|
170
|
+
await addPatternsToMap(map, [{ fillPattern: 'stripes' }], OUTDOOR, patternRegistry)
|
|
171
|
+
expect(map.addImage).not.toHaveBeenCalled()
|
|
120
172
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const registry = makePatternRegistry()
|
|
125
|
-
const config = { fillPattern: 'stripes' }
|
|
126
|
-
const hiDpi = 3
|
|
127
|
-
await registerPatterns(map1, [config], OUTDOOR, registry, 2)
|
|
128
|
-
await registerPatterns(map2, [config], OUTDOOR, registry, hiDpi)
|
|
129
|
-
const [id2x] = map1.addImage.mock.calls[0]
|
|
130
|
-
const [id3x] = map2.addImage.mock.calls[0]
|
|
131
|
-
expect(id2x).not.toBe(id3x)
|
|
132
|
-
expect(id2x).toMatch(/-2x$/)
|
|
133
|
-
expect(id3x).toMatch(/-3x$/)
|
|
134
|
-
})
|
|
135
|
-
})
|
|
173
|
+
getPatternImageId.mockRestore()
|
|
174
|
+
getPatternInnerContent.mockRestore()
|
|
175
|
+
})
|
|
136
176
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const getContextSpy = HTMLCanvasElement.prototype.getContext
|
|
142
|
-
await registerPatterns(
|
|
143
|
-
map,
|
|
144
|
-
[{ fillPattern: 'stripes', fillPatternForegroundColor: '#aabbcc', fillPatternBackgroundColor: '#112233' }],
|
|
145
|
-
OUTDOOR,
|
|
146
|
-
registry
|
|
147
|
-
)
|
|
148
|
-
expect(map.addImage).toHaveBeenCalledTimes(1)
|
|
149
|
-
expect(getContextSpy).toHaveBeenCalled()
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('resolves style-keyed foreground color for the given mapStyleId', async () => {
|
|
153
|
-
const map = makeMap()
|
|
154
|
-
const registry = makePatternRegistry()
|
|
155
|
-
const config = {
|
|
156
|
-
fillPattern: 'stripes',
|
|
157
|
-
fillPatternForegroundColor: { outdoor: '#aabbcc', dark: '#112233' }
|
|
158
|
-
}
|
|
159
|
-
await registerPatterns(map, [config], OUTDOOR, registry)
|
|
160
|
-
const map2 = makeMap()
|
|
161
|
-
await registerPatterns(map2, [config], 'dark', registry)
|
|
162
|
-
const [idOutdoor] = map.addImage.mock.calls[0]
|
|
163
|
-
const [idDark] = map2.addImage.mock.calls[0]
|
|
164
|
-
expect(idOutdoor).not.toBe(idDark)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('uses cached ImageData on second call with identical config', async () => {
|
|
168
|
-
const map = makeMap()
|
|
169
|
-
const registry = makePatternRegistry()
|
|
170
|
-
const config = { fillPattern: 'stripes', fillPatternForegroundColor: '#unique2' }
|
|
171
|
-
const pixelRatio = 2
|
|
172
|
-
await registerPatterns(map, [config], OUTDOOR, registry, pixelRatio)
|
|
173
|
-
const { getPatternImageId } = await import('../../../../src/utils/patternUtils.js')
|
|
174
|
-
const imageId = getPatternImageId(config, OUTDOOR, registry, pixelRatio)
|
|
175
|
-
const map2 = makeMap([imageId])
|
|
176
|
-
await registerPatterns(map2, [config], OUTDOOR, registry, pixelRatio)
|
|
177
|
-
expect(map2.addImage).not.toHaveBeenCalled()
|
|
178
|
-
})
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
describe('registerPatterns — null results', () => {
|
|
182
|
-
it('does not call addImage when innerContent becomes unavailable inside rasterisePattern', async () => {
|
|
183
|
-
// registry.get returns content on the first call (for getPatternImageId in registerPatterns)
|
|
184
|
-
// but undefined on the second call (for getPatternInnerContent inside rasterisePattern)
|
|
185
|
-
const registry = {
|
|
186
|
-
get: jest.fn()
|
|
187
|
-
.mockReturnValueOnce({ svgContent: SVG_CONTENT })
|
|
188
|
-
.mockReturnValueOnce(undefined)
|
|
189
|
-
}
|
|
190
|
-
const map = makeMap()
|
|
191
|
-
await registerPatterns(map, [{ fillPattern: 'stripes' }], OUTDOOR, registry)
|
|
192
|
-
expect(map.addImage).not.toHaveBeenCalled()
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
it('does not call addImage when imageId becomes unavailable inside rasterisePattern', async () => {
|
|
196
|
-
// Three consecutive calls to registry.get:
|
|
197
|
-
// 1. getPatternImageId in registerPatterns → returns content → imageId is truthy
|
|
198
|
-
// 2. getPatternInnerContent directly in rasterisePattern → returns content → passes innerContent guard
|
|
199
|
-
// 3. getPatternInnerContent inside getPatternImageId in rasterisePattern → returns undefined
|
|
200
|
-
// → getPatternImageId returns null → hits the imageId null guard
|
|
201
|
-
const registry = {
|
|
202
|
-
get: jest.fn()
|
|
203
|
-
.mockReturnValueOnce({ svgContent: SVG_CONTENT })
|
|
204
|
-
.mockReturnValueOnce({ svgContent: SVG_CONTENT })
|
|
177
|
+
it('does not call addImage when imageId becomes unavailable inside rasterisePattern', async () => {
|
|
178
|
+
const getPatternInnerContent = jest.spyOn(patternRegistry, 'getPatternInnerContent').mockReturnValue('CONTENT')
|
|
179
|
+
const getPatternImageId = jest.spyOn(patternRegistry, 'getPatternImageId')
|
|
180
|
+
.mockReturnValueOnce('test-id')
|
|
205
181
|
.mockReturnValueOnce(undefined)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
182
|
+
const map = makeMap()
|
|
183
|
+
await addPatternsToMap(map, [{ fillPattern: 'stripes' }], OUTDOOR, patternRegistry)
|
|
184
|
+
expect(map.addImage).not.toHaveBeenCalled()
|
|
185
|
+
getPatternImageId.mockRestore()
|
|
186
|
+
getPatternInnerContent.mockRestore()
|
|
187
|
+
})
|
|
210
188
|
})
|
|
211
189
|
})
|
|
@@ -10,8 +10,7 @@ const SVG_ERROR_PREVIEW_LENGTH = 80
|
|
|
10
10
|
*/
|
|
11
11
|
export const rasteriseToImageData = (svgString, width, height) =>
|
|
12
12
|
new Promise((resolve, reject) => {
|
|
13
|
-
const
|
|
14
|
-
const url = URL.createObjectURL(blob)
|
|
13
|
+
const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`
|
|
15
14
|
const img = new Image(width, height)
|
|
16
15
|
img.onload = () => {
|
|
17
16
|
const canvas = document.createElement('canvas')
|
|
@@ -19,11 +18,9 @@ export const rasteriseToImageData = (svgString, width, height) =>
|
|
|
19
18
|
canvas.height = height
|
|
20
19
|
const ctx = canvas.getContext('2d')
|
|
21
20
|
ctx.drawImage(img, 0, 0, width, height)
|
|
22
|
-
URL.revokeObjectURL(url)
|
|
23
21
|
resolve(ctx.getImageData(0, 0, width, height))
|
|
24
22
|
}
|
|
25
23
|
img.onerror = () => {
|
|
26
|
-
URL.revokeObjectURL(url)
|
|
27
24
|
reject(new Error(`Failed to rasterise SVG: ${svgString.slice(0, SVG_ERROR_PREVIEW_LENGTH)}`))
|
|
28
25
|
}
|
|
29
26
|
img.src = url
|
|
@@ -9,20 +9,25 @@ const ERROR_PREVIEW_LENGTH = 80
|
|
|
9
9
|
// Length chosen to be well over ERROR_PREVIEW_LENGTH so truncation is exercised
|
|
10
10
|
const LONG_CONTENT_LENGTH = 200
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
globalThis.URL.createObjectURL = jest.fn(() => 'blob:mock')
|
|
14
|
-
globalThis.URL.revokeObjectURL = jest.fn()
|
|
12
|
+
let imageInstances = []
|
|
15
13
|
|
|
14
|
+
beforeAll(() => {
|
|
16
15
|
HTMLCanvasElement.prototype.getContext = jest.fn(() => ({
|
|
17
16
|
drawImage: jest.fn(),
|
|
18
17
|
getImageData: jest.fn((_x, _y, w, h) => ({ width: w, height: h }))
|
|
19
18
|
}))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
imageInstances = []
|
|
23
|
+
jest.clearAllMocks()
|
|
20
24
|
|
|
21
25
|
globalThis.Image = class {
|
|
22
26
|
constructor (w, h) {
|
|
23
27
|
this.width = w
|
|
24
28
|
this.height = h
|
|
25
29
|
this._src = ''
|
|
30
|
+
imageInstances.push(this)
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
get src () { return this._src }
|
|
@@ -30,40 +35,34 @@ beforeAll(() => {
|
|
|
30
35
|
}
|
|
31
36
|
})
|
|
32
37
|
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
jest.clearAllMocks()
|
|
35
|
-
globalThis.URL.createObjectURL.mockReturnValue('blob:mock')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
38
|
describe('rasteriseToImageData', () => {
|
|
39
|
-
it('resolves with ImageData at the requested dimensions
|
|
39
|
+
it('resolves with ImageData at the requested dimensions and draws via canvas', async () => {
|
|
40
40
|
const getContext = HTMLCanvasElement.prototype.getContext
|
|
41
41
|
const result = await rasteriseToImageData(SVG, WIDTH, HEIGHT)
|
|
42
42
|
expect(result).toMatchObject({ width: WIDTH, height: HEIGHT })
|
|
43
|
-
expect(globalThis.URL.createObjectURL).toHaveBeenCalledWith(expect.any(Blob))
|
|
44
|
-
expect(globalThis.URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock')
|
|
45
43
|
const { drawImage, getImageData } = getContext.mock.results[0].value
|
|
46
44
|
expect(drawImage).toHaveBeenCalledWith(expect.any(Object), 0, 0, WIDTH, HEIGHT)
|
|
47
45
|
expect(getImageData).toHaveBeenCalledWith(0, 0, WIDTH, HEIGHT)
|
|
48
46
|
})
|
|
49
47
|
|
|
50
|
-
it('
|
|
51
|
-
|
|
48
|
+
it('sets img.src to a data URI', async () => {
|
|
49
|
+
await rasteriseToImageData(SVG, WIDTH, HEIGHT)
|
|
50
|
+
const src = imageInstances[0]._src
|
|
51
|
+
expect(src).toMatch(/^data:image\/svg\+xml;charset=utf-8,/)
|
|
52
|
+
expect(src).toContain(encodeURIComponent(SVG))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('rejects with a truncated SVG preview on error', async () => {
|
|
52
56
|
globalThis.Image = class {
|
|
53
57
|
constructor (w, h) { this.width = w; this.height = h; this._src = '' }
|
|
54
58
|
get src () { return this._src }
|
|
55
59
|
set src (val) { this._src = val; this.onerror?.() }
|
|
56
60
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
expect(preview).toBe(longSvg.slice(0, ERROR_PREVIEW_LENGTH))
|
|
64
|
-
expect(globalThis.URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock')
|
|
65
|
-
} finally {
|
|
66
|
-
globalThis.Image = originalImage
|
|
67
|
-
}
|
|
61
|
+
const longSvg = `<svg>${'x'.repeat(LONG_CONTENT_LENGTH)}</svg>`
|
|
62
|
+
const error = await rasteriseToImageData(longSvg, WIDTH, HEIGHT).catch(e => e)
|
|
63
|
+
expect(error.message).toMatch('Failed to rasterise SVG')
|
|
64
|
+
const preview = error.message.replace('Failed to rasterise SVG: ', '')
|
|
65
|
+
expect(preview).toHaveLength(ERROR_PREVIEW_LENGTH)
|
|
66
|
+
expect(preview).toBe(longSvg.slice(0, ERROR_PREVIEW_LENGTH))
|
|
68
67
|
})
|
|
69
68
|
})
|
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
import { getSymbolDef, getSymbolStyleColors, getSymbolViewBox } from '../../../../src/utils/symbolUtils.js'
|
|
2
|
-
import { rasteriseToImageData } from './rasteriseToImageData.js'
|
|
3
|
-
|
|
4
1
|
const ANCHOR_LOW = 0.25
|
|
5
2
|
const ANCHOR_HIGH = 0.75
|
|
6
|
-
const HASH_BASE = 36
|
|
7
|
-
|
|
8
|
-
const hashString = (str) => {
|
|
9
|
-
let hash = 0
|
|
10
|
-
for (const ch of str) {
|
|
11
|
-
hash = Math.trunc(((hash << 5) - hash) + ch.codePointAt(0))
|
|
12
|
-
}
|
|
13
|
-
return Math.abs(hash).toString(HASH_BASE)
|
|
14
|
-
}
|
|
15
3
|
|
|
16
4
|
// ─── MapLibre-specific anchor conversion ──────────────────────────────────────
|
|
17
5
|
|
|
@@ -48,99 +36,48 @@ export const anchorToMaplibre = ([ax, ay]) => {
|
|
|
48
36
|
return (y + (x && y ? '-' : '') + x) || 'center'
|
|
49
37
|
}
|
|
50
38
|
|
|
51
|
-
// ─── Image IDs ────────────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Returns a deterministic image ID for a symbol in normal or selected state.
|
|
55
|
-
* Based on the hash of the fully resolved SVG content and the pixel ratio.
|
|
56
|
-
*
|
|
57
|
-
* @param {Object} dataset
|
|
58
|
-
* @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor)
|
|
59
|
-
* @param {Object} symbolRegistry
|
|
60
|
-
* @param {boolean} [selected=false]
|
|
61
|
-
* @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor
|
|
62
|
-
* @returns {string|null}
|
|
63
|
-
*/
|
|
64
|
-
export const getSymbolImageId = (dataset, mapStyle, symbolRegistry, selected = false, pixelRatio = 2) => {
|
|
65
|
-
const symbolDef = getSymbolDef(dataset, symbolRegistry)
|
|
66
|
-
if (!symbolDef) {
|
|
67
|
-
return null
|
|
68
|
-
}
|
|
69
|
-
const styleColors = getSymbolStyleColors(dataset)
|
|
70
|
-
const resolved = selected
|
|
71
|
-
? symbolRegistry.resolveSelected(symbolDef, styleColors, mapStyle)
|
|
72
|
-
: symbolRegistry.resolve(symbolDef, styleColors, mapStyle)
|
|
73
|
-
return `symbol-${selected ? 'sel-' : ''}${hashString(resolved)}-${pixelRatio}x`
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ─── Rasterisation ────────────────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
// Module-level cache: imageId → ImageData. Avoids re-rasterising identical symbols.
|
|
79
|
-
const imageDataCache = new Map()
|
|
80
|
-
|
|
81
|
-
const rasteriseSymbolImage = async (dataset, mapStyle, symbolRegistry, selected, pixelRatio) => {
|
|
82
|
-
const symbolDef = getSymbolDef(dataset, symbolRegistry)
|
|
83
|
-
if (!symbolDef) {
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
const styleColors = getSymbolStyleColors(dataset)
|
|
87
|
-
const resolvedContent = selected
|
|
88
|
-
? symbolRegistry.resolveSelected(symbolDef, styleColors, mapStyle)
|
|
89
|
-
: symbolRegistry.resolve(symbolDef, styleColors, mapStyle)
|
|
90
|
-
|
|
91
|
-
const imageId = `symbol-${selected ? 'sel-' : ''}${hashString(resolvedContent)}-${pixelRatio}x`
|
|
92
|
-
|
|
93
|
-
let imageData = imageDataCache.get(imageId)
|
|
94
|
-
if (!imageData) {
|
|
95
|
-
const viewBox = getSymbolViewBox(dataset, symbolDef)
|
|
96
|
-
const [,, width, height] = viewBox.split(' ').map(Number)
|
|
97
|
-
// Render at pixelRatio× to keep icons crisp at the current device DPI and map size.
|
|
98
|
-
// MapLibre receives the matching pixelRatio so the image displays at its original logical size.
|
|
99
|
-
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="${width * pixelRatio}" height="${height * pixelRatio}" viewBox="${viewBox}">${resolvedContent}</svg>`
|
|
100
|
-
imageData = await rasteriseToImageData(svgString, width * pixelRatio, height * pixelRatio)
|
|
101
|
-
imageDataCache.set(imageId, imageData)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { imageId, imageData }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
39
|
/**
|
|
108
|
-
* Register normal
|
|
40
|
+
* Register normal, active (both rings) and selected (black ring) symbol images.
|
|
109
41
|
* Skips images that are already registered (safe to call on style change).
|
|
110
|
-
* Updates `map.
|
|
42
|
+
* Updates `map._activeSymbolImageMap` (normal→active) and `map._selectedSymbolImageMap` (normal→selected).
|
|
111
43
|
*
|
|
112
44
|
* Callers are responsible for resolving sublayers before calling this function
|
|
113
45
|
* (see `getSymbolConfigs` in the datasets plugin adapter).
|
|
114
46
|
*
|
|
115
47
|
* @param {Object} map - MapLibre map instance
|
|
116
|
-
* @param {Object[]}
|
|
48
|
+
* @param {Object[]} styleArray - Flat list of datasets/merged-sublayers that have a symbol config
|
|
117
49
|
* @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor)
|
|
118
50
|
* @param {Object} symbolRegistry
|
|
119
51
|
* @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor (computed by caller)
|
|
120
52
|
* @returns {Promise<void>}
|
|
121
53
|
*/
|
|
122
|
-
export const
|
|
123
|
-
if (!
|
|
54
|
+
export const addSymbolsToMap = async (map, styleArray, mapStyle, symbolRegistry, pixelRatio = 2) => {
|
|
55
|
+
if (!styleArray.length) {
|
|
124
56
|
return
|
|
125
57
|
}
|
|
126
58
|
|
|
127
|
-
|
|
128
|
-
map.
|
|
59
|
+
map._activeSymbolImageMap = {}
|
|
60
|
+
map._selectedSymbolImageMap = {}
|
|
129
61
|
|
|
130
|
-
await Promise.all(
|
|
131
|
-
const normalId = getSymbolImageId(config, mapStyle,
|
|
132
|
-
const
|
|
133
|
-
if (normalId &&
|
|
134
|
-
map.
|
|
62
|
+
await Promise.all(styleArray.flatMap(config => {
|
|
63
|
+
const normalId = symbolRegistry.getSymbolImageId(config, mapStyle, false, pixelRatio)
|
|
64
|
+
const activeId = symbolRegistry.getSymbolImageId(config, mapStyle, true, pixelRatio)
|
|
65
|
+
if (normalId && activeId) {
|
|
66
|
+
map._activeSymbolImageMap[normalId] = activeId
|
|
135
67
|
}
|
|
136
|
-
return [
|
|
137
|
-
const imageId =
|
|
138
|
-
if (!imageId || map.hasImage(imageId)) {
|
|
68
|
+
return ['normal', 'active', 'selected'].map(async (variant) => {
|
|
69
|
+
const imageId = variant === 'active' ? activeId : normalId
|
|
70
|
+
if (variant !== 'selected' && (!imageId || map.hasImage(imageId))) {
|
|
139
71
|
return
|
|
140
72
|
}
|
|
141
|
-
const result = await rasteriseSymbolImage(config, mapStyle,
|
|
142
|
-
if (result
|
|
143
|
-
|
|
73
|
+
const result = await symbolRegistry.rasteriseSymbolImage(config, mapStyle, variant, pixelRatio)
|
|
74
|
+
if (result) {
|
|
75
|
+
if (variant === 'selected' && normalId) {
|
|
76
|
+
map._selectedSymbolImageMap[normalId] = result.imageId
|
|
77
|
+
}
|
|
78
|
+
if (!map.hasImage(result.imageId)) {
|
|
79
|
+
map.addImage(result.imageId, result.imageData, { pixelRatio })
|
|
80
|
+
}
|
|
144
81
|
}
|
|
145
82
|
})
|
|
146
83
|
}))
|