@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,19 +1,23 @@
|
|
|
1
1
|
import { updateHighlightedFeatures } from './highlightFeatures.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
this.coords = []
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
class LngLatBounds {
|
|
4
|
+
constructor () { this.coords = [] }
|
|
5
|
+
extend (c) { this.coords.push(c) }
|
|
6
|
+
getWest () { return Math.min(...this.coords.map(c => c[0])) }
|
|
7
|
+
getSouth () { return Math.min(...this.coords.map(c => c[1])) }
|
|
8
|
+
getEast () { return Math.max(...this.coords.map(c => c[0])) }
|
|
9
|
+
getNorth () { return Math.max(...this.coords.map(c => c[1])) }
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const SYMBOL_IMAGE = 'symbol-abc123'
|
|
12
13
|
const EMPTY_FILTER = ['==', 'id', '']
|
|
13
|
-
const
|
|
14
|
+
const LINE_COLOR = 'line-color'
|
|
15
|
+
const STALE_SYMBOL_LAYER = 'active-highlight-stale-symbol'
|
|
16
|
+
const SEL_STALE_SYMBOL_LAYER = 'selected-highlight-stale-symbol'
|
|
14
17
|
|
|
15
18
|
const makeMap = (overrides = {}) => ({
|
|
16
|
-
|
|
19
|
+
_activehighlightSources: new Set(),
|
|
20
|
+
_selectedhighlightSources: new Set(),
|
|
17
21
|
getLayer: jest.fn(),
|
|
18
22
|
addLayer: jest.fn(),
|
|
19
23
|
moveLayer: jest.fn(),
|
|
@@ -25,7 +29,9 @@ const makeMap = (overrides = {}) => ({
|
|
|
25
29
|
...overrides
|
|
26
30
|
})
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
// ─── Active (cursor) features — active-highlight-* layers ────────────────────
|
|
33
|
+
|
|
34
|
+
describe('Highlighting Utils — active (cursor) fill and line', () => {
|
|
29
35
|
let map
|
|
30
36
|
|
|
31
37
|
const ALL_BRANCHES_FEATURES = [
|
|
@@ -38,7 +44,7 @@ describe('Highlighting Utils — fill and line', () => {
|
|
|
38
44
|
const ALL_BRANCHES_STYLES = { l1: { stroke: 'red', fill: 'blue' }, l2: { stroke: 'green' } }
|
|
39
45
|
|
|
40
46
|
beforeEach(() => {
|
|
41
|
-
map = makeMap({
|
|
47
|
+
map = makeMap({ _activehighlightSources: new Set(['stale']) })
|
|
42
48
|
})
|
|
43
49
|
|
|
44
50
|
test('All branches', () => {
|
|
@@ -48,69 +54,140 @@ describe('Highlighting Utils — fill and line', () => {
|
|
|
48
54
|
if (id.includes('stale')) { return {} }
|
|
49
55
|
if (id === 'l1') { return { source: 's1', type: 'fill' } }
|
|
50
56
|
if (id === 'l2') { return { source: 's2', type: 'line' } }
|
|
51
|
-
if (id === 'highlight-s2-fill') { return {} }
|
|
57
|
+
if (id === 'active-highlight-s2-fill') { return {} }
|
|
52
58
|
return null
|
|
53
59
|
})
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
const coordMid = 5
|
|
57
|
-
map.queryRenderedFeatures.mockReturnValue([
|
|
58
|
-
{ id: 1, geometry: { coordinates: [coordMax, coordMax] } },
|
|
59
|
-
{ id: 2, properties: { customId: 4 }, geometry: { coordinates: [[0, 0], [coordMid, coordMid]] } }
|
|
60
|
-
])
|
|
61
|
-
|
|
62
|
-
const bounds = updateHighlightedFeatures({ LngLatBounds: lngLatBounds, map, selectedFeatures: ALL_BRANCHES_FEATURES, stylesMap: ALL_BRANCHES_STYLES })
|
|
61
|
+
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: ALL_BRANCHES_FEATURES, stylesMap: ALL_BRANCHES_STYLES })
|
|
63
62
|
|
|
64
|
-
expect(map.setFilter).toHaveBeenCalledWith('highlight-stale-fill', EMPTY_FILTER)
|
|
63
|
+
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-stale-fill', EMPTY_FILTER)
|
|
65
64
|
expect(map.setFilter).toHaveBeenCalledWith(STALE_SYMBOL_LAYER, EMPTY_FILTER)
|
|
66
|
-
expect(map.setFilter).toHaveBeenCalledWith('highlight-s2-fill', EMPTY_FILTER)
|
|
67
|
-
expect(map.setFilter).toHaveBeenCalledWith('highlight-s2-line', expect.arrayContaining([['get', 'customId']]))
|
|
68
|
-
expect(bounds).toEqual([0, 0, 10, 10])
|
|
65
|
+
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-s2-fill', EMPTY_FILTER)
|
|
66
|
+
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-s2-line', expect.arrayContaining([['get', 'customId']]))
|
|
69
67
|
})
|
|
70
68
|
|
|
71
|
-
test('null
|
|
72
|
-
map.
|
|
69
|
+
test('null _activehighlightSources falls back to empty set; line geom skips absent fill layer', () => {
|
|
70
|
+
map._activehighlightSources = null
|
|
73
71
|
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
74
|
-
updateHighlightedFeatures({ LngLatBounds
|
|
75
|
-
expect(map.setFilter).not.toHaveBeenCalledWith('highlight-s1-fill', expect.anything())
|
|
72
|
+
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap: { l1: { stroke: 'red' } } })
|
|
73
|
+
expect(map.setFilter).not.toHaveBeenCalledWith('active-highlight-s1-fill', expect.anything())
|
|
76
74
|
})
|
|
77
75
|
|
|
78
76
|
test('persistent source skips cleanup; missing stale layers skip setFilter', () => {
|
|
79
|
-
map.
|
|
77
|
+
map._activehighlightSources = new Set(['stale', 's1'])
|
|
80
78
|
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
81
|
-
updateHighlightedFeatures({ LngLatBounds
|
|
79
|
+
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap: { l1: { stroke: 'red' } } })
|
|
82
80
|
expect(map.setFilter).not.toHaveBeenCalledWith(expect.stringContaining('stale'), expect.anything())
|
|
83
81
|
})
|
|
82
|
+
|
|
83
|
+
test('active features get selected-style overlay line on top', () => {
|
|
84
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
85
|
+
const stylesMap = { l1: { stroke: 'yellow', selectionStroke: 'black', strokeWidth: 3, activeStrokeWidth: 9 } }
|
|
86
|
+
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap })
|
|
87
|
+
|
|
88
|
+
const activeColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-s1-line' && c[1] === LINE_COLOR)
|
|
89
|
+
expect(activeColorCall?.[2]).toBe('yellow')
|
|
90
|
+
|
|
91
|
+
const overlayColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-inner-s1-line' && c[1] === LINE_COLOR)
|
|
92
|
+
expect(overlayColorCall?.[2]).toBe('black')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('overlay cleanup when active features cleared', () => {
|
|
96
|
+
map._activehighlightinnerSources = new Set(['stale'])
|
|
97
|
+
map.getLayer.mockImplementation(id => id === 'active-highlight-inner-stale-line' ? {} : null) // NOSONAR
|
|
98
|
+
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [], stylesMap: {} })
|
|
99
|
+
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-inner-stale-line', EMPTY_FILTER)
|
|
100
|
+
})
|
|
84
101
|
})
|
|
85
102
|
|
|
103
|
+
// ─── Selected features — selected-highlight-* layers ─────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('Highlighting Utils — selected fill and line', () => {
|
|
106
|
+
let map
|
|
107
|
+
|
|
108
|
+
const SELECTED_FEATURES = [
|
|
109
|
+
{ featureId: 1, layerId: 'l1', geometry: { type: 'Polygon' } }
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const STYLES = { l1: { stroke: 'red', selectionStroke: '#ffdd00', fill: 'blue', strokeWidth: 3 } }
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
map = makeMap({ _selectedhighlightSources: new Set(['stale']) })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('creates selected-highlight-* layers with selectionStroke color', () => {
|
|
119
|
+
map.getLayer.mockImplementation(id => { // NOSONAR
|
|
120
|
+
if (id.includes('stale')) { return {} }
|
|
121
|
+
if (id === 'l1') { return { source: 's1', type: 'fill' } }
|
|
122
|
+
return null
|
|
123
|
+
})
|
|
124
|
+
updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures: SELECTED_FEATURES, stylesMap: STYLES })
|
|
125
|
+
expect(map.setFilter).toHaveBeenCalledWith(SEL_STALE_SYMBOL_LAYER, EMPTY_FILTER)
|
|
126
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ id: 'selected-highlight-s1-line' }))
|
|
127
|
+
const linePaintCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'selected-highlight-s1-line' && c[1] === LINE_COLOR)
|
|
128
|
+
expect(linePaintCall).toBeTruthy()
|
|
129
|
+
expect(linePaintCall[2]).toBe('#ffdd00')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('bounds matched using idProperty when set', () => {
|
|
133
|
+
const features = [{ featureId: 'parcel-1', layerId: 'l1', idProperty: 'parcelId', geometry: { type: 'Polygon' } }]
|
|
134
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'fill' } : null) // NOSONAR
|
|
135
|
+
map.queryRenderedFeatures.mockReturnValue([
|
|
136
|
+
{ properties: { parcelId: 'parcel-1' }, geometry: { coordinates: [[1, 2]] } }
|
|
137
|
+
])
|
|
138
|
+
const bounds = updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures: features, stylesMap: STYLES })
|
|
139
|
+
expect(bounds).not.toBeNull()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('bounds are calculated from selected features only', () => {
|
|
143
|
+
const features = [
|
|
144
|
+
{ featureId: 1, layerId: 'l1', geometry: { type: 'Polygon' } },
|
|
145
|
+
{ featureId: 2, layerId: 'l1', geometry: { type: 'Polygon' } }
|
|
146
|
+
]
|
|
147
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
148
|
+
const coordMax = 10
|
|
149
|
+
const coordMid = 5
|
|
150
|
+
map.queryRenderedFeatures.mockReturnValue([
|
|
151
|
+
{ id: 1, geometry: { coordinates: [coordMax, coordMax] } },
|
|
152
|
+
{ id: 2, geometry: { coordinates: [[0, 0], [coordMid, coordMid]] } }
|
|
153
|
+
])
|
|
154
|
+
const bounds = updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures: features, stylesMap: STYLES })
|
|
155
|
+
expect(bounds).toEqual([0, 0, 10, 10])
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// ─── Layer management ─────────────────────────────────────────────────────────
|
|
160
|
+
|
|
86
161
|
describe('Highlighting Utils — layer management', () => {
|
|
87
162
|
let map
|
|
88
163
|
|
|
89
164
|
beforeEach(() => {
|
|
90
|
-
map = makeMap({
|
|
165
|
+
map = makeMap({ _activehighlightSources: new Set(['stale']) })
|
|
91
166
|
})
|
|
92
167
|
|
|
93
|
-
test('reuses existing highlight layer; new
|
|
168
|
+
test('reuses existing active-highlight layer; new layers spread sourceLayer', () => {
|
|
94
169
|
map.getLayer.mockImplementation(id => { // NOSONAR
|
|
95
170
|
if (id === 'l1') { return { source: 's1', type: 'line' } }
|
|
96
171
|
if (id === 'l2') { return { source: 's2', type: 'line', sourceLayer: 'tiles' } }
|
|
97
|
-
if (id === 'highlight-s1-line') { return {} }
|
|
172
|
+
if (id === 'active-highlight-s1-line') { return {} }
|
|
98
173
|
return null
|
|
99
174
|
})
|
|
100
|
-
updateHighlightedFeatures({ LngLatBounds
|
|
101
|
-
|
|
175
|
+
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }, { featureId: 2, layerId: 'l2' }], stylesMap: { l1: { stroke: 'blue' }, l2: { stroke: 'green' } } })
|
|
176
|
+
// active-highlight-s1-line reused; active-highlight-s2-line, active-highlight-inner-s1-line, active-highlight-inner-s2-line are new
|
|
177
|
+
expect(map.addLayer).toHaveBeenCalledTimes(3)
|
|
102
178
|
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ 'source-layer': 'tiles' }))
|
|
103
179
|
})
|
|
104
180
|
|
|
105
181
|
test('returns null when no rendered features match', () => {
|
|
106
|
-
expect(updateHighlightedFeatures({ LngLatBounds
|
|
182
|
+
expect(updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures: [], stylesMap: {} })).toBeNull()
|
|
107
183
|
})
|
|
108
184
|
})
|
|
109
185
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
186
|
+
// ─── Symbol layers ────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
describe('Highlighting Utils — symbol layers (active cursor)', () => {
|
|
189
|
+
const ACTIVE_IMAGE = 'symbol-act-abc123'
|
|
190
|
+
const HIGHLIGHT_LAYER = 'active-highlight-s1-symbol'
|
|
114
191
|
const ICON_IMAGE = 'icon-image'
|
|
115
192
|
const ICON_ANCHOR = 'icon-anchor'
|
|
116
193
|
const POINT_FEATURE = { featureId: 1, layerId: 'l1', geometry: { type: 'Point' } }
|
|
@@ -119,22 +196,22 @@ describe('Highlighting Utils — symbol layers', () => {
|
|
|
119
196
|
|
|
120
197
|
beforeEach(() => {
|
|
121
198
|
map = makeMap()
|
|
122
|
-
map.
|
|
199
|
+
map._activeSymbolImageMap = { [SYMBOL_IMAGE]: ACTIVE_IMAGE }
|
|
123
200
|
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'symbol' } : null) // NOSONAR
|
|
124
201
|
map.getLayoutProperty.mockReturnValue(SYMBOL_IMAGE)
|
|
125
202
|
})
|
|
126
203
|
|
|
127
|
-
const run = (
|
|
128
|
-
updateHighlightedFeatures({ LngLatBounds
|
|
204
|
+
const run = (activeFeatures = [POINT_FEATURE]) =>
|
|
205
|
+
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures, stylesMap: { l1: {} } })
|
|
129
206
|
|
|
130
|
-
test('creates symbol highlight layer with
|
|
207
|
+
test('creates symbol highlight layer with cursor image variant', () => {
|
|
131
208
|
run()
|
|
132
209
|
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
133
210
|
id: HIGHLIGHT_LAYER,
|
|
134
211
|
type: 'symbol',
|
|
135
|
-
layout: expect.objectContaining({ [ICON_IMAGE]:
|
|
212
|
+
layout: expect.objectContaining({ [ICON_IMAGE]: ACTIVE_IMAGE })
|
|
136
213
|
}))
|
|
137
|
-
expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, ICON_IMAGE,
|
|
214
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, ICON_IMAGE, ACTIVE_IMAGE)
|
|
138
215
|
})
|
|
139
216
|
|
|
140
217
|
test('reads icon-anchor from original layer', () => {
|
|
@@ -171,20 +248,54 @@ describe('Highlighting Utils — symbol layers', () => {
|
|
|
171
248
|
})
|
|
172
249
|
run()
|
|
173
250
|
expect(map.addLayer).not.toHaveBeenCalled()
|
|
174
|
-
expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, ICON_IMAGE,
|
|
251
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, ICON_IMAGE, ACTIVE_IMAGE)
|
|
175
252
|
})
|
|
176
253
|
|
|
177
|
-
test('skips highlight when icon-image has no entry in
|
|
178
|
-
map.getLayoutProperty.mockReturnValue(
|
|
179
|
-
map.
|
|
254
|
+
test('skips highlight when icon-image has no entry in _activeSymbolImageMap', () => {
|
|
255
|
+
map.getLayoutProperty.mockReturnValue(SYMBOL_IMAGE)
|
|
256
|
+
map._activeSymbolImageMap = {} // no mapping registered
|
|
180
257
|
run()
|
|
181
258
|
expect(map.addLayer).not.toHaveBeenCalled()
|
|
182
259
|
})
|
|
183
260
|
|
|
184
261
|
test('cleans up stale symbol highlight layer', () => {
|
|
185
|
-
map.
|
|
262
|
+
map._activehighlightSources = new Set(['stale'])
|
|
186
263
|
map.getLayer.mockImplementation(id => id === STALE_SYMBOL_LAYER ? { type: 'symbol' } : null) // NOSONAR
|
|
187
264
|
run([])
|
|
188
265
|
expect(map.setFilter).toHaveBeenCalledWith(STALE_SYMBOL_LAYER, EMPTY_FILTER)
|
|
189
266
|
})
|
|
190
267
|
})
|
|
268
|
+
|
|
269
|
+
describe('Highlighting Utils — symbol layers (committed selection)', () => {
|
|
270
|
+
const SELECTED_IMAGE = 'symbol-sel-abc123'
|
|
271
|
+
const SEL_HIGHLIGHT_LAYER = 'selected-highlight-s1-symbol'
|
|
272
|
+
const ICON_IMAGE = 'icon-image'
|
|
273
|
+
const POINT_FEATURE = { featureId: 1, layerId: 'l1', geometry: { type: 'Point' } }
|
|
274
|
+
|
|
275
|
+
let map
|
|
276
|
+
|
|
277
|
+
beforeEach(() => {
|
|
278
|
+
map = makeMap()
|
|
279
|
+
map._selectedSymbolImageMap = { [SYMBOL_IMAGE]: SELECTED_IMAGE }
|
|
280
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'symbol' } : null) // NOSONAR
|
|
281
|
+
map.getLayoutProperty.mockReturnValue(SYMBOL_IMAGE)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const run = (selectedFeatures = [POINT_FEATURE]) =>
|
|
285
|
+
updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures, stylesMap: { l1: {} } })
|
|
286
|
+
|
|
287
|
+
test('creates selected-highlight symbol layer with selected image variant', () => {
|
|
288
|
+
run()
|
|
289
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
290
|
+
id: SEL_HIGHLIGHT_LAYER,
|
|
291
|
+
type: 'symbol',
|
|
292
|
+
layout: expect.objectContaining({ [ICON_IMAGE]: SELECTED_IMAGE })
|
|
293
|
+
}))
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('skips highlight when icon-image has no entry in _selectedSymbolImageMap', () => {
|
|
297
|
+
map._selectedSymbolImageMap = {}
|
|
298
|
+
run()
|
|
299
|
+
expect(map.addLayer).not.toHaveBeenCalled()
|
|
300
|
+
})
|
|
301
|
+
})
|
|
@@ -5,6 +5,16 @@ export function cleanCanvas (map) {
|
|
|
5
5
|
canvas.setAttribute('tabindex', -1) // If removed altogether Chrome can add a focus-visible style
|
|
6
6
|
canvas.removeAttribute('aria-label')
|
|
7
7
|
canvas.style.display = 'block'
|
|
8
|
+
// The map container is aria-hidden, but MapLibre's canvas inside it can still
|
|
9
|
+
// receive focus, triggering: "Blocked aria-hidden on an element because its
|
|
10
|
+
// descendant retained focus." Immediately blurring and returning focus to
|
|
11
|
+
// relatedTarget prevents the canvas from holding focus inside an aria-hidden subtree.
|
|
12
|
+
canvas.addEventListener('focus', (event) => { // NOSONAR
|
|
13
|
+
canvas.blur()
|
|
14
|
+
if (event.relatedTarget) {
|
|
15
|
+
event.relatedTarget.focus()
|
|
16
|
+
}
|
|
17
|
+
})
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
// Fix touch preventDefault console error
|
|
@@ -16,6 +16,37 @@ describe('cleanCanvas', () => {
|
|
|
16
16
|
expect(canvas.hasAttribute('aria-label')).toBe(false)
|
|
17
17
|
expect(canvas.style.display).toBe('block')
|
|
18
18
|
})
|
|
19
|
+
|
|
20
|
+
it('redirects focus back to the previously focused element when canvas receives focus', () => {
|
|
21
|
+
const canvas = document.createElement('canvas')
|
|
22
|
+
const map = { getCanvas: () => canvas }
|
|
23
|
+
const prev = document.createElement('button')
|
|
24
|
+
prev.focus = jest.fn()
|
|
25
|
+
document.body.appendChild(canvas)
|
|
26
|
+
|
|
27
|
+
cleanCanvas(map)
|
|
28
|
+
|
|
29
|
+
const event = new FocusEvent('focus', { relatedTarget: prev })
|
|
30
|
+
const blurSpy = jest.spyOn(canvas, 'blur')
|
|
31
|
+
canvas.dispatchEvent(event)
|
|
32
|
+
|
|
33
|
+
expect(blurSpy).toHaveBeenCalled()
|
|
34
|
+
expect(prev.focus).toHaveBeenCalled()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('blurs canvas without restoring focus when there is no previous element', () => {
|
|
38
|
+
const canvas = document.createElement('canvas')
|
|
39
|
+
const map = { getCanvas: () => canvas }
|
|
40
|
+
document.body.appendChild(canvas)
|
|
41
|
+
|
|
42
|
+
cleanCanvas(map)
|
|
43
|
+
|
|
44
|
+
const event = new FocusEvent('focus', { relatedTarget: null })
|
|
45
|
+
const blurSpy = jest.spyOn(canvas, 'blur')
|
|
46
|
+
canvas.dispatchEvent(event)
|
|
47
|
+
|
|
48
|
+
expect(blurSpy).toHaveBeenCalled()
|
|
49
|
+
})
|
|
19
50
|
})
|
|
20
51
|
|
|
21
52
|
describe('applyPreventDefaultFix', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { injectColors, getEffectivePixelRatio } from '../../../../src/utils/patternUtils.js'
|
|
2
2
|
import { getValueForStyle } from '../../../../src/utils/getValueForStyle.js'
|
|
3
3
|
import { rasteriseToImageData } from './rasteriseToImageData.js'
|
|
4
4
|
|
|
@@ -16,12 +16,12 @@ const imageDataCache = new Map()
|
|
|
16
16
|
* @returns {Promise<{ imageId: string, imageData: ImageData }|null>}
|
|
17
17
|
*/
|
|
18
18
|
const rasterisePattern = async (dataset, mapStyleId, patternRegistry, pixelRatio) => {
|
|
19
|
-
const innerContent = getPatternInnerContent(dataset
|
|
19
|
+
const innerContent = patternRegistry.getPatternInnerContent(dataset)
|
|
20
20
|
if (!innerContent) {
|
|
21
21
|
return null
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const imageId = getPatternImageId(dataset, mapStyleId,
|
|
24
|
+
const imageId = patternRegistry.getPatternImageId(dataset, mapStyleId, pixelRatio)
|
|
25
25
|
if (!imageId) {
|
|
26
26
|
return null
|
|
27
27
|
}
|
|
@@ -32,9 +32,7 @@ const rasterisePattern = async (dataset, mapStyleId, patternRegistry, pixelRatio
|
|
|
32
32
|
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
33
33
|
const colored = injectColors(innerContent, fg, bg)
|
|
34
34
|
const bgRect = `<rect width="16" height="16" fill="${bg}"/>`
|
|
35
|
-
|
|
36
|
-
// SVG detail renders crisply. Logical tile size = physicalSize / effectiveRatio = 8px always.
|
|
37
|
-
const effectiveRatio = Math.max(PATTERN_MIN_PIXEL_RATIO, pixelRatio)
|
|
35
|
+
const effectiveRatio = getEffectivePixelRatio(pixelRatio)
|
|
38
36
|
const physicalSize = Math.round(8 * effectiveRatio)
|
|
39
37
|
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="${physicalSize}" height="${physicalSize}" viewBox="0 0 16 16">${bgRect}${colored}</svg>`
|
|
40
38
|
imageData = await rasteriseToImageData(svgString, physicalSize, physicalSize)
|
|
@@ -51,20 +49,20 @@ const rasterisePattern = async (dataset, mapStyleId, patternRegistry, pixelRatio
|
|
|
51
49
|
* (see `getPatternConfigs` in the datasets plugin adapter).
|
|
52
50
|
*
|
|
53
51
|
* @param {Object} map - MapLibre map instance
|
|
54
|
-
* @param {Object[]}
|
|
52
|
+
* @param {Object[]} styleArray - Flat list of datasets/merged-sublayers with a pattern config
|
|
55
53
|
* @param {string} mapStyleId
|
|
56
54
|
* @param {Object} patternRegistry
|
|
57
55
|
* @param {number} pixelRatio
|
|
58
56
|
* @returns {Promise<void>}
|
|
59
57
|
*/
|
|
60
|
-
export const
|
|
61
|
-
if (!
|
|
58
|
+
export const addPatternsToMap = async (map, styleArray, mapStyleId, patternRegistry, pixelRatio) => {
|
|
59
|
+
if (!styleArray.length) {
|
|
62
60
|
return
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
const effectiveRatio =
|
|
66
|
-
await Promise.all(
|
|
67
|
-
const imageId = getPatternImageId(config, mapStyleId,
|
|
63
|
+
const effectiveRatio = getEffectivePixelRatio(pixelRatio)
|
|
64
|
+
await Promise.all(styleArray.map(async (config) => {
|
|
65
|
+
const imageId = patternRegistry.getPatternImageId(config, mapStyleId, pixelRatio)
|
|
68
66
|
if (!imageId || map.hasImage(imageId)) {
|
|
69
67
|
return
|
|
70
68
|
}
|