@defra/interactive-map 0.0.16-alpha → 0.0.18-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/assets/images/slot-map.svg +264 -0
- 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/context.md +53 -7
- package/docs/api/map-style-config.md +41 -2
- package/docs/api/marker-config.md +53 -11
- package/docs/api/slots.md +16 -15
- package/docs/api/symbol-config.md +160 -0
- package/docs/api/symbol-registry.md +115 -0
- package/docs/api.md +25 -22
- package/docs/getting-started.md +4 -1
- package/docs/plugins/datasets.md +657 -0
- package/docs/plugins/interact.md +68 -43
- package/docs/plugins/search.md +15 -3
- package/docs/plugins.md +1 -1
- package/package.json +2 -2
- package/plugins/beta/datasets/dist/css/index.css +103 -15
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/esm/index.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/DatasetsInit.jsx +29 -9
- package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +159 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +75 -0
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +440 -0
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
- package/plugins/beta/datasets/src/api/addDataset.js +2 -8
- package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
- package/plugins/beta/datasets/src/api/getStyle.js +13 -0
- package/plugins/beta/datasets/src/api/removeDataset.js +2 -44
- package/plugins/beta/datasets/src/api/setData.js +10 -0
- package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
- package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
- package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
- package/plugins/beta/datasets/src/api/setStyle.js +22 -0
- package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
- package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
- package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
- package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
- package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
- package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
- package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
- package/plugins/beta/datasets/src/datasets.js +39 -56
- package/plugins/beta/datasets/src/defaults.js +44 -8
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +34 -25
- package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
- package/plugins/beta/datasets/src/index.js +2 -1
- package/plugins/beta/datasets/src/manifest.js +25 -17
- package/plugins/beta/datasets/src/panels/Key.jsx +51 -51
- package/plugins/beta/datasets/src/panels/Key.module.scss +59 -9
- package/plugins/beta/datasets/src/panels/Layers.jsx +132 -29
- package/plugins/beta/datasets/src/panels/Layers.module.scss +56 -8
- package/plugins/beta/datasets/src/reducer.js +134 -9
- package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
- package/plugins/beta/datasets/src/utils/bbox.js +7 -5
- package/plugins/beta/datasets/src/utils/filters.js +5 -2
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +86 -0
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
- package/plugins/beta/draw-ml/dist/css/index.css +21 -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/draw-ml/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
- package/plugins/beta/draw-ml/src/draw.scss +0 -7
- package/plugins/beta/draw-ml/src/manifest.js +16 -16
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/src/Frame.jsx +5 -5
- 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/map-styles/src/MapStyles.jsx +5 -4
- package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
- package/plugins/beta/map-styles/src/manifest.js +1 -1
- package/plugins/beta/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/src/index.test.js +3 -3
- package/plugins/beta/scale-bar/src/manifest.js +3 -3
- package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
- package/plugins/interact/dist/css/index.css +1 -1
- 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 +14 -5
- package/plugins/interact/src/InteractInit.test.js +26 -6
- package/plugins/interact/src/api/enable.test.js +7 -7
- package/plugins/interact/src/defaults.js +4 -6
- package/plugins/interact/src/events.js +9 -6
- package/plugins/interact/src/events.test.js +28 -4
- package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
- package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
- package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
- package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
- package/plugins/interact/src/interact.scss +0 -7
- package/plugins/interact/src/manifest.js +14 -18
- package/plugins/interact/src/manifest.test.js +3 -1
- package/plugins/interact/src/reducer.js +23 -4
- package/plugins/interact/src/reducer.test.js +60 -11
- package/plugins/interact/src/utils/buildStylesMap.js +17 -4
- package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
- package/plugins/interact/src/utils/featureQueries.js +11 -6
- package/plugins/interact/src/utils/featureQueries.test.js +8 -1
- 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/Search.jsx +3 -1
- package/plugins/search/src/components/Form/Form.module.scss +2 -1
- package/plugins/search/src/events/fetchSuggestions.js +6 -4
- package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
- package/plugins/search/src/events/formHandlers.js +3 -3
- package/plugins/search/src/events/formHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -2
- package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
- package/plugins/search/src/utils/updateMap.js +3 -3
- package/plugins/search/src/utils/updateMap.test.js +3 -3
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +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/appEvents.js +7 -0
- package/providers/maplibre/src/appEvents.test.js +18 -4
- package/providers/maplibre/src/maplibreProvider.js +52 -0
- package/providers/maplibre/src/maplibreProvider.test.js +105 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +37 -7
- package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -95
- package/providers/maplibre/src/utils/hoverCursor.js +61 -0
- package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
- package/providers/maplibre/src/utils/patternImages.js +70 -0
- package/providers/maplibre/src/utils/patternImages.test.js +180 -0
- package/providers/maplibre/src/utils/queryFeatures.js +38 -16
- package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
- package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
- package/providers/maplibre/src/utils/symbolImages.js +147 -0
- package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
- package/src/App/components/Actions/Actions.jsx +2 -2
- package/src/App/components/Actions/Actions.module.scss +0 -7
- package/src/App/components/Actions/Actions.test.jsx +1 -1
- package/src/App/components/Icon/Icon.jsx +3 -2
- package/src/App/components/Icon/Icon.module.scss +4 -0
- package/src/App/components/Icon/Icon.test.jsx +43 -4
- package/src/App/components/MapButton/MapButton.jsx +42 -17
- package/src/App/components/MapButton/MapButton.module.scss +4 -13
- package/src/App/components/MapButton/MapButton.test.jsx +27 -3
- package/src/App/components/Markers/Markers.jsx +122 -27
- package/src/App/components/Markers/Markers.module.scss +0 -10
- package/src/App/components/Markers/Markers.test.jsx +246 -0
- package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
- package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
- package/src/App/hooks/useButtonStateEvaluator.js +12 -2
- package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
- package/src/App/hooks/useInterfaceAPI.js +6 -0
- package/src/App/hooks/useInterfaceAPI.test.js +156 -0
- package/src/App/hooks/useLayoutMeasurements.js +84 -18
- package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
- package/src/App/hooks/useMarkersAPI.js +2 -5
- package/src/App/hooks/useMarkersAPI.test.js +4 -4
- package/src/App/layout/Layout.jsx +14 -9
- package/src/App/layout/Layout.test.jsx +6 -4
- package/src/App/layout/layout.module.scss +67 -29
- package/src/App/registry/pluginRegistry.js +1 -1
- package/src/App/renderer/HtmlElementHost.jsx +2 -1
- package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
- package/src/App/renderer/mapButtons.js +1 -1
- package/src/App/renderer/mapPanels.test.js +2 -2
- package/src/App/renderer/slotHelpers.js +2 -2
- package/src/App/renderer/slotHelpers.test.js +5 -5
- package/src/App/renderer/slots.js +9 -5
- package/src/App/store/AppProvider.jsx +3 -1
- package/src/App/store/AppProvider.test.jsx +1 -1
- package/src/App/store/ServiceProvider.jsx +8 -4
- package/src/App/store/appActionsMap.js +16 -0
- package/src/App/store/appActionsMap.test.js +27 -0
- package/src/App/store/appDispatchMiddleware.js +1 -1
- package/src/App/store/appDispatchMiddleware.test.js +2 -2
- package/src/App/store/appReducer.js +2 -0
- package/src/App/store/mapActionsMap.js +4 -6
- package/src/App/store/mapActionsMap.test.js +3 -2
- package/src/App/store/mapReducer.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +4 -0
- package/src/config/appConfig.js +5 -8
- package/src/config/appConfig.test.js +1 -2
- package/src/config/defaults.js +0 -2
- package/src/config/events.js +28 -0
- package/src/config/mapTheme.js +56 -0
- package/src/config/patternConfig.js +16 -0
- package/src/config/symbolConfig.js +80 -0
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_colors.scss +0 -9
- package/src/scss/settings/_dimensions.scss +0 -1
- package/src/services/patternRegistry.js +40 -0
- package/src/services/patternRegistry.test.js +48 -0
- package/src/services/symbolRegistry.js +113 -0
- package/src/services/symbolRegistry.test.js +262 -0
- package/src/types.js +93 -11
- package/src/utils/getSafeZoneInset.js +9 -7
- package/src/utils/getSafeZoneInset.test.js +10 -10
- package/src/utils/patternUtils.js +94 -0
- package/src/utils/patternUtils.test.js +160 -0
- package/src/utils/symbolUtils.js +85 -0
- package/src/utils/symbolUtils.test.js +156 -0
- package/webpack.dev.mjs +1 -1
- package/docs/api/slot-map.svg +0 -1
- package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
- package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
- package/plugins/beta/datasets/src/api/showDataset.js +0 -14
- package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
- package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
- package/plugins/beta/datasets/src/mapLayers.js +0 -164
- /package/src/{utils → services}/logger.js +0 -0
- /package/src/{utils → services}/logger.test.js +0 -0
|
@@ -4,6 +4,8 @@ import { attachAppEvents } from './appEvents.js'
|
|
|
4
4
|
import { createMapLabelNavigator } from './utils/labels.js'
|
|
5
5
|
import { updateHighlightedFeatures } from './utils/highlightFeatures.js'
|
|
6
6
|
import { queryFeatures } from './utils/queryFeatures.js'
|
|
7
|
+
import { registerSymbols } from './utils/symbolImages.js'
|
|
8
|
+
import { registerPatterns } from './utils/patternImages.js'
|
|
7
9
|
import { getAreaDimensions, getCardinalMove, getResolution, getPaddedBounds, isGeometryObscured } from './utils/spatial.js'
|
|
8
10
|
|
|
9
11
|
jest.mock('./defaults.js', () => ({
|
|
@@ -33,6 +35,8 @@ jest.mock('./utils/labels.js', () => ({
|
|
|
33
35
|
}))
|
|
34
36
|
jest.mock('./utils/highlightFeatures.js', () => ({ updateHighlightedFeatures: jest.fn(() => []) }))
|
|
35
37
|
jest.mock('./utils/queryFeatures.js', () => ({ queryFeatures: jest.fn(() => []) }))
|
|
38
|
+
jest.mock('./utils/symbolImages.js', () => ({ registerSymbols: jest.fn(() => Promise.resolve()) }))
|
|
39
|
+
jest.mock('./utils/patternImages.js', () => ({ registerPatterns: jest.fn(() => Promise.resolve()) }))
|
|
36
40
|
|
|
37
41
|
describe('MapLibreProvider', () => {
|
|
38
42
|
let map, eventBus, maplibreModule, loadCallback
|
|
@@ -53,7 +57,11 @@ describe('MapLibreProvider', () => {
|
|
|
53
57
|
unproject: jest.fn(() => ({ lng: 1, lat: 2 })),
|
|
54
58
|
getCenter: jest.fn(() => ({ lng: 1.2345678, lat: 2.3456789 })),
|
|
55
59
|
getZoom: jest.fn(() => 10),
|
|
56
|
-
getBounds: jest.fn(() => ({ toArray: jest.fn(() => [[0, 0], [1, 1]]) }))
|
|
60
|
+
getBounds: jest.fn(() => ({ toArray: jest.fn(() => [[0, 0], [1, 1]]) })),
|
|
61
|
+
getPixelRatio: jest.fn(() => 1),
|
|
62
|
+
getCanvas: jest.fn(() => ({ style: {} })),
|
|
63
|
+
getLayer: jest.fn(() => true),
|
|
64
|
+
queryRenderedFeatures: jest.fn(() => [])
|
|
57
65
|
}
|
|
58
66
|
eventBus = { emit: jest.fn() }
|
|
59
67
|
maplibreModule = { Map: jest.fn(() => map), LngLatBounds: jest.fn() }
|
|
@@ -220,6 +228,102 @@ describe('MapLibreProvider', () => {
|
|
|
220
228
|
})
|
|
221
229
|
})
|
|
222
230
|
|
|
231
|
+
test('registerSymbols delegates to utility with map instance', async () => {
|
|
232
|
+
const p = makeProvider()
|
|
233
|
+
await doInitMap(p)
|
|
234
|
+
const configs = [{ symbol: 'pin' }]
|
|
235
|
+
const mapStyle = { id: 'test', selectedColor: '#0b0c0c' }
|
|
236
|
+
const registry = {}
|
|
237
|
+
await p.registerSymbols(configs, mapStyle, registry)
|
|
238
|
+
expect(registerSymbols).toHaveBeenCalledWith(map, configs, mapStyle, registry, expect.any(Number))
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('registerSymbols computes pixelRatio from getPixelRatio and mapSize scale factor', async () => {
|
|
242
|
+
const p = makeProvider()
|
|
243
|
+
await doInitMap(p)
|
|
244
|
+
map.getPixelRatio.mockReturnValue(2)
|
|
245
|
+
p.mapSize = 'medium' // scaleFactor['medium'] = 1.5
|
|
246
|
+
const registry = {}
|
|
247
|
+
await p.registerSymbols([], { id: 'test' }, registry)
|
|
248
|
+
expect(registerSymbols).toHaveBeenCalledWith(map, [], { id: 'test' }, registry, 3) // 2 * 1.5
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('registerSymbols falls back to pixelRatio 1 when getPixelRatio returns 0', async () => {
|
|
252
|
+
const p = makeProvider()
|
|
253
|
+
await doInitMap(p)
|
|
254
|
+
map.getPixelRatio.mockReturnValue(0)
|
|
255
|
+
p.mapSize = 'small' // scaleFactor['small'] = 1
|
|
256
|
+
const registry = {}
|
|
257
|
+
await p.registerSymbols([], { id: 'test' }, registry)
|
|
258
|
+
expect(registerSymbols).toHaveBeenCalledWith(map, [], { id: 'test' }, registry, 1) // (0 || 1) * 1
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('registerPatterns delegates to utility with map instance', async () => {
|
|
262
|
+
const p = makeProvider()
|
|
263
|
+
await doInitMap(p)
|
|
264
|
+
const configs = [{ fillPattern: 'dot' }]
|
|
265
|
+
const registry = {}
|
|
266
|
+
await p.registerPatterns(configs, 'test', registry)
|
|
267
|
+
expect(registerPatterns).toHaveBeenCalledWith(map, configs, 'test', registry)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('setHoverCursor', () => {
|
|
271
|
+
test('registers mousemove handler on the map when layerIds provided', async () => {
|
|
272
|
+
const p = makeProvider()
|
|
273
|
+
await doInitMap(p)
|
|
274
|
+
p.setHoverCursor(['layer-a'])
|
|
275
|
+
expect(map.on).toHaveBeenCalledWith('mousemove', expect.any(Function))
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('sets cursor to pointer when queryRenderedFeatures returns a hit', async () => {
|
|
279
|
+
const canvas = { style: {} }
|
|
280
|
+
map.getCanvas.mockReturnValue(canvas)
|
|
281
|
+
map.queryRenderedFeatures.mockReturnValue([{ id: 'f1' }])
|
|
282
|
+
|
|
283
|
+
const p = makeProvider()
|
|
284
|
+
await doInitMap(p)
|
|
285
|
+
p.setHoverCursor(['layer-a'])
|
|
286
|
+
|
|
287
|
+
const moveHandler = map.on.mock.calls.find(([e]) => e === 'mousemove')?.[1]
|
|
288
|
+
moveHandler({ point: { x: 10, y: 20 } })
|
|
289
|
+
|
|
290
|
+
expect(canvas.style.cursor).toBe('pointer')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('clears cursor when queryRenderedFeatures returns no hit', async () => {
|
|
294
|
+
const canvas = { style: { cursor: 'pointer' } }
|
|
295
|
+
map.getCanvas.mockReturnValue(canvas)
|
|
296
|
+
map.queryRenderedFeatures.mockReturnValue([])
|
|
297
|
+
|
|
298
|
+
const p = makeProvider()
|
|
299
|
+
await doInitMap(p)
|
|
300
|
+
p.setHoverCursor(['layer-a'])
|
|
301
|
+
|
|
302
|
+
const moveHandler = map.on.mock.calls.find(([e]) => e === 'mousemove')?.[1]
|
|
303
|
+
moveHandler({ point: { x: 10, y: 20 } })
|
|
304
|
+
|
|
305
|
+
expect(canvas.style.cursor).toBe('')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('removes mousemove handler and clears cursor when called with empty array', async () => {
|
|
309
|
+
const canvas = { style: { cursor: 'pointer' } }
|
|
310
|
+
map.getCanvas.mockReturnValue(canvas)
|
|
311
|
+
|
|
312
|
+
const p = makeProvider()
|
|
313
|
+
await doInitMap(p)
|
|
314
|
+
p.setHoverCursor(['layer-a'])
|
|
315
|
+
p.setHoverCursor([])
|
|
316
|
+
|
|
317
|
+
expect(map.off).toHaveBeenCalledWith('mousemove', expect.any(Function))
|
|
318
|
+
expect(canvas.style.cursor).toBe('')
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('does nothing when map is not initialised', () => {
|
|
322
|
+
const p = makeProvider()
|
|
323
|
+
expect(() => p.setHoverCursor(['layer-a'])).not.toThrow()
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
223
327
|
test('label methods return null without labelNavigator; delegate when set', async () => {
|
|
224
328
|
const p = makeProvider()
|
|
225
329
|
await doInitMap(p)
|
|
@@ -36,7 +36,7 @@ const cleanupStaleSources = (map, previousSources, currentSources) => {
|
|
|
36
36
|
previousSources.forEach(src => {
|
|
37
37
|
if (!currentSources.has(src)) {
|
|
38
38
|
const base = `highlight-${src}`
|
|
39
|
-
const layers = [`${base}-fill`, `${base}-line`]
|
|
39
|
+
const layers = [`${base}-fill`, `${base}-line`, `${base}-symbol`]
|
|
40
40
|
layers.forEach(id => {
|
|
41
41
|
if (map.getLayer(id)) {
|
|
42
42
|
map.setFilter(id, ['==', 'id', ''])
|
|
@@ -60,6 +60,26 @@ const applyHighlightLayer = (map, id, type, sourceId, srcLayer, paint, filter) =
|
|
|
60
60
|
map.setPaintProperty(id, prop, value)
|
|
61
61
|
})
|
|
62
62
|
map.setFilter(id, filter)
|
|
63
|
+
map.moveLayer(id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const applySymbolHighlightLayer = (map, id, sourceId, srcLayer, originalLayerId, imageId, filter) => {
|
|
67
|
+
if (!map.getLayer(id)) {
|
|
68
|
+
map.addLayer({
|
|
69
|
+
id,
|
|
70
|
+
type: 'symbol',
|
|
71
|
+
source: sourceId,
|
|
72
|
+
...(srcLayer && { 'source-layer': srcLayer }),
|
|
73
|
+
layout: {
|
|
74
|
+
'icon-image': imageId,
|
|
75
|
+
'icon-anchor': map.getLayoutProperty(originalLayerId, 'icon-anchor') ?? 'center',
|
|
76
|
+
'icon-allow-overlap': true
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
map.setLayoutProperty(id, 'icon-image', imageId)
|
|
81
|
+
map.setFilter(id, filter)
|
|
82
|
+
map.moveLayer(id)
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
const calculateBounds = (LngLatBounds, renderedFeatures) => {
|
|
@@ -77,9 +97,11 @@ const calculateBounds = (LngLatBounds, renderedFeatures) => {
|
|
|
77
97
|
return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]
|
|
78
98
|
}
|
|
79
99
|
|
|
100
|
+
const getSelectedImageId = (map, imageId) => map._symbolImageMap?.[imageId] ?? null
|
|
101
|
+
|
|
80
102
|
/**
|
|
81
103
|
* Update highlighted features using pure filters.
|
|
82
|
-
* Supports fill
|
|
104
|
+
* Supports fill, line and symbol geometry, multi-source, cleanup, and bounds.
|
|
83
105
|
*/
|
|
84
106
|
export function updateHighlightedFeatures ({ LngLatBounds, map, selectedFeatures, stylesMap }) {
|
|
85
107
|
if (!map) {
|
|
@@ -105,21 +127,21 @@ export function updateHighlightedFeatures ({ LngLatBounds, map, selectedFeatures
|
|
|
105
127
|
const geom = hasFillGeometry ? 'fill' : baseLayer.type
|
|
106
128
|
const base = `highlight-${sourceId}`
|
|
107
129
|
|
|
108
|
-
const { stroke, strokeWidth, fill } = stylesMap[layerId]
|
|
109
|
-
// Use ['id'] for feature.id, ['get', idProperty] for properties
|
|
110
130
|
const idExpression = idProperty ? ['get', idProperty] : ['id']
|
|
111
131
|
const filter = ['in', idExpression, ['literal', [...ids]]]
|
|
112
|
-
const fillFilter = ['in', idExpression, ['literal', [...fillIds]]]
|
|
113
|
-
|
|
114
|
-
const linePaint = { 'line-color': stroke, 'line-width': strokeWidth }
|
|
115
132
|
|
|
116
133
|
if (geom === 'fill') {
|
|
134
|
+
const { stroke, strokeWidth, fill } = stylesMap[layerId]
|
|
135
|
+
const fillFilter = ['in', idExpression, ['literal', [...fillIds]]]
|
|
136
|
+
const linePaint = { 'line-color': stroke, 'line-width': strokeWidth }
|
|
117
137
|
// Only apply fill highlight to polygon features, not to any co-selected line features
|
|
118
138
|
applyHighlightLayer(map, `${base}-fill`, 'fill', sourceId, srcLayer, { 'fill-color': fill }, fillFilter)
|
|
119
139
|
applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, linePaint, filter)
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
if (geom === 'line') {
|
|
143
|
+
const { stroke, strokeWidth } = stylesMap[layerId]
|
|
144
|
+
const linePaint = { 'line-color': stroke, 'line-width': strokeWidth }
|
|
123
145
|
// Clear any fill highlight from a previous polygon selection on the same source
|
|
124
146
|
if (map.getLayer(`${base}-fill`)) {
|
|
125
147
|
map.setFilter(`${base}-fill`, ['==', 'id', ''])
|
|
@@ -127,6 +149,14 @@ export function updateHighlightedFeatures ({ LngLatBounds, map, selectedFeatures
|
|
|
127
149
|
applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, linePaint, filter)
|
|
128
150
|
}
|
|
129
151
|
|
|
152
|
+
if (geom === 'symbol') {
|
|
153
|
+
const imageId = map.getLayoutProperty(layerId, 'icon-image')
|
|
154
|
+
const selectedImageId = getSelectedImageId(map, imageId)
|
|
155
|
+
if (selectedImageId) {
|
|
156
|
+
applySymbolHighlightLayer(map, `${base}-symbol`, sourceId, srcLayer, layerId, selectedImageId, filter)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
130
160
|
// Bounds only from rendered tiles
|
|
131
161
|
renderedFeatures.push(
|
|
132
162
|
...map
|
|
@@ -1,132 +1,190 @@
|
|
|
1
1
|
import { updateHighlightedFeatures } from './highlightFeatures.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function lngLatBounds () {
|
|
4
|
+
this.coords = []
|
|
5
|
+
this.extend = (c) => this.coords.push(c)
|
|
6
|
+
this.getWest = () => Math.min(...this.coords.map(c => c[0]))
|
|
7
|
+
this.getSouth = () => Math.min(...this.coords.map(c => c[1]))
|
|
8
|
+
this.getEast = () => Math.max(...this.coords.map(c => c[0]))
|
|
9
|
+
this.getNorth = () => Math.max(...this.coords.map(c => c[1]))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const EMPTY_FILTER = ['==', 'id', '']
|
|
13
|
+
const STALE_SYMBOL_LAYER = 'highlight-stale-symbol'
|
|
14
|
+
|
|
15
|
+
const makeMap = (overrides = {}) => ({
|
|
16
|
+
_highlightedSources: new Set(),
|
|
17
|
+
getLayer: jest.fn(),
|
|
18
|
+
addLayer: jest.fn(),
|
|
19
|
+
moveLayer: jest.fn(),
|
|
20
|
+
setFilter: jest.fn(),
|
|
21
|
+
setPaintProperty: jest.fn(),
|
|
22
|
+
setLayoutProperty: jest.fn(),
|
|
23
|
+
getLayoutProperty: jest.fn(),
|
|
24
|
+
queryRenderedFeatures: jest.fn().mockReturnValue([]),
|
|
25
|
+
...overrides
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('Highlighting Utils — fill and line', () => {
|
|
4
29
|
let map
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
30
|
+
|
|
31
|
+
const ALL_BRANCHES_FEATURES = [
|
|
32
|
+
{ featureId: 1, layerId: 'l1', geometry: { type: 'Polygon' } },
|
|
33
|
+
{ featureId: 2, layerId: 'l1', geometry: { type: 'MultiPolygon' } },
|
|
34
|
+
{ featureId: 3, layerId: 'invalid' },
|
|
35
|
+
{ featureId: 4, layerId: 'l2', idProperty: 'customId', geometry: { type: 'Point' } }
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const ALL_BRANCHES_STYLES = { l1: { stroke: 'red', fill: 'blue' }, l2: { stroke: 'green' } }
|
|
13
39
|
|
|
14
40
|
beforeEach(() => {
|
|
15
|
-
map = {
|
|
16
|
-
_highlightedSources: new Set(['stale']),
|
|
17
|
-
getLayer: jest.fn(),
|
|
18
|
-
addLayer: jest.fn(),
|
|
19
|
-
setFilter: jest.fn(),
|
|
20
|
-
setPaintProperty: jest.fn(),
|
|
21
|
-
queryRenderedFeatures: jest.fn()
|
|
22
|
-
}
|
|
41
|
+
map = makeMap({ _highlightedSources: new Set(['stale']) })
|
|
23
42
|
})
|
|
24
43
|
|
|
25
44
|
test('All branches', () => {
|
|
26
|
-
// Coverage for Line 93: Null map check
|
|
27
45
|
expect(updateHighlightedFeatures({ map: null })).toBeNull()
|
|
28
46
|
|
|
29
|
-
map.getLayer.mockImplementation((id) => {
|
|
30
|
-
if (id.includes('stale')) return
|
|
31
|
-
if (id === 'l1') return { source: 's1', type: 'fill' }
|
|
32
|
-
if (id === 'l2') return { source: 's2', type: 'line' }
|
|
33
|
-
if (id === 'highlight-s2-fill') return
|
|
34
|
-
return null
|
|
47
|
+
map.getLayer.mockImplementation((id) => { // NOSONAR
|
|
48
|
+
if (id.includes('stale')) { return {} }
|
|
49
|
+
if (id === 'l1') { return { source: 's1', type: 'fill' } }
|
|
50
|
+
if (id === 'l2') { return { source: 's2', type: 'line' } }
|
|
51
|
+
if (id === 'highlight-s2-fill') { return {} }
|
|
52
|
+
return null
|
|
35
53
|
})
|
|
36
54
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
{ featureId: 1, layerId: 'l1', geometry: { type: 'Polygon' } },
|
|
40
|
-
{ featureId: 2, layerId: 'l1', geometry: { type: 'MultiPolygon' } },
|
|
41
|
-
// Coverage for Line 13: Invalid layer
|
|
42
|
-
{ featureId: 3, layerId: 'invalid' },
|
|
43
|
-
// Coverage for Line 116: idProperty exists
|
|
44
|
-
{ featureId: 4, layerId: 'l2', idProperty: 'customId', geometry: { type: 'Point' } }
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
const stylesMap = {
|
|
48
|
-
l1: { stroke: 'red', fill: 'blue' },
|
|
49
|
-
l2: { stroke: 'green' }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Coverage for Lines 78-80: Recursive coordinate handling (numbers vs arrays)
|
|
55
|
+
const coordMax = 10
|
|
56
|
+
const coordMid = 5
|
|
53
57
|
map.queryRenderedFeatures.mockReturnValue([
|
|
54
|
-
{ id: 1, geometry: { coordinates: [
|
|
55
|
-
{ id: 2, properties: { customId: 4 }, geometry: { coordinates: [[0, 0], [
|
|
58
|
+
{ id: 1, geometry: { coordinates: [coordMax, coordMax] } },
|
|
59
|
+
{ id: 2, properties: { customId: 4 }, geometry: { coordinates: [[0, 0], [coordMid, coordMid]] } }
|
|
56
60
|
])
|
|
57
61
|
|
|
58
|
-
const bounds = updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures, stylesMap })
|
|
59
|
-
|
|
60
|
-
// Line 13 verify: map.getLayer returned null and function returned early
|
|
61
|
-
// Line 49-50 verify: Stale sources filtered out
|
|
62
|
-
expect(map.setFilter).toHaveBeenCalledWith('highlight-stale-fill', ['==', 'id', ''])
|
|
62
|
+
const bounds = updateHighlightedFeatures({ LngLatBounds: lngLatBounds, map, selectedFeatures: ALL_BRANCHES_FEATURES, stylesMap: ALL_BRANCHES_STYLES })
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
expect(map.setFilter).toHaveBeenCalledWith(
|
|
66
|
-
|
|
67
|
-
// Line 116 verify: Using ['get', idProperty]
|
|
64
|
+
expect(map.setFilter).toHaveBeenCalledWith('highlight-stale-fill', EMPTY_FILTER)
|
|
65
|
+
expect(map.setFilter).toHaveBeenCalledWith(STALE_SYMBOL_LAYER, EMPTY_FILTER)
|
|
66
|
+
expect(map.setFilter).toHaveBeenCalledWith('highlight-s2-fill', EMPTY_FILTER)
|
|
68
67
|
expect(map.setFilter).toHaveBeenCalledWith('highlight-s2-line', expect.arrayContaining([['get', 'customId']]))
|
|
69
|
-
|
|
70
|
-
// Line 80-82 verify: Recursive LngLatBounds logic
|
|
71
68
|
expect(bounds).toEqual([0, 0, 10, 10])
|
|
72
69
|
})
|
|
73
70
|
|
|
74
|
-
test('
|
|
75
|
-
|
|
76
|
-
map.
|
|
77
|
-
|
|
78
|
-
map.queryRenderedFeatures.mockReturnValue([])
|
|
79
|
-
updateHighlightedFeatures({
|
|
80
|
-
LngLatBounds,
|
|
81
|
-
map,
|
|
82
|
-
selectedFeatures: [{ featureId: 1, layerId: 'l1' }],
|
|
83
|
-
stylesMap: { l1: { stroke: 'red' } }
|
|
84
|
-
})
|
|
71
|
+
test('null _highlightedSources falls back to empty set; line geom skips absent fill layer', () => {
|
|
72
|
+
map._highlightedSources = null
|
|
73
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
74
|
+
updateHighlightedFeatures({ LngLatBounds: lngLatBounds, map, selectedFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap: { l1: { stroke: 'red' } } })
|
|
85
75
|
expect(map.setFilter).not.toHaveBeenCalledWith('highlight-s1-fill', expect.anything())
|
|
86
76
|
})
|
|
87
77
|
|
|
88
78
|
test('persistent source skips cleanup; missing stale layers skip setFilter', () => {
|
|
89
|
-
// line 37 false: src IS in currentSources; line 41 false: getLayer returns null for stale layers
|
|
90
79
|
map._highlightedSources = new Set(['stale', 's1'])
|
|
91
|
-
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null)
|
|
92
|
-
map
|
|
93
|
-
updateHighlightedFeatures({
|
|
94
|
-
LngLatBounds,
|
|
95
|
-
map,
|
|
96
|
-
selectedFeatures: [{ featureId: 1, layerId: 'l1' }],
|
|
97
|
-
stylesMap: { l1: { stroke: 'red' } }
|
|
98
|
-
})
|
|
80
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
81
|
+
updateHighlightedFeatures({ LngLatBounds: lngLatBounds, map, selectedFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap: { l1: { stroke: 'red' } } })
|
|
99
82
|
expect(map.setFilter).not.toHaveBeenCalledWith(expect.stringContaining('stale'), expect.anything())
|
|
100
83
|
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('Highlighting Utils — layer management', () => {
|
|
87
|
+
let map
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
map = makeMap({ _highlightedSources: new Set(['stale']) })
|
|
91
|
+
})
|
|
101
92
|
|
|
102
93
|
test('reuses existing highlight layer; new layer spreads sourceLayer', () => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (id === '
|
|
107
|
-
if (id === 'l2') return { source: 's2', type: 'line', sourceLayer: 'tiles' }
|
|
108
|
-
if (id === 'highlight-s1-line') return true
|
|
94
|
+
map.getLayer.mockImplementation(id => { // NOSONAR
|
|
95
|
+
if (id === 'l1') { return { source: 's1', type: 'line' } }
|
|
96
|
+
if (id === 'l2') { return { source: 's2', type: 'line', sourceLayer: 'tiles' } }
|
|
97
|
+
if (id === 'highlight-s1-line') { return {} }
|
|
109
98
|
return null
|
|
110
99
|
})
|
|
111
|
-
map
|
|
112
|
-
updateHighlightedFeatures({
|
|
113
|
-
LngLatBounds,
|
|
114
|
-
map,
|
|
115
|
-
selectedFeatures: [
|
|
116
|
-
{ featureId: 1, layerId: 'l1' },
|
|
117
|
-
{ featureId: 2, layerId: 'l2' }
|
|
118
|
-
],
|
|
119
|
-
stylesMap: { l1: { stroke: 'blue' }, l2: { stroke: 'green' } }
|
|
120
|
-
})
|
|
100
|
+
updateHighlightedFeatures({ LngLatBounds: lngLatBounds, map, selectedFeatures: [{ featureId: 1, layerId: 'l1' }, { featureId: 2, layerId: 'l2' }], stylesMap: { l1: { stroke: 'blue' }, l2: { stroke: 'green' } } })
|
|
121
101
|
expect(map.addLayer).toHaveBeenCalledTimes(1)
|
|
122
102
|
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ 'source-layer': 'tiles' }))
|
|
123
103
|
})
|
|
124
104
|
|
|
125
|
-
test('
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
105
|
+
test('returns null when no rendered features match', () => {
|
|
106
|
+
expect(updateHighlightedFeatures({ LngLatBounds: lngLatBounds, map, selectedFeatures: [], stylesMap: {} })).toBeNull()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('Highlighting Utils — symbol layers', () => {
|
|
111
|
+
const SYMBOL_IMAGE = 'symbol-abc123'
|
|
112
|
+
const SELECTED_IMAGE = 'symbol-sel-abc123'
|
|
113
|
+
const HIGHLIGHT_LAYER = 'highlight-s1-symbol'
|
|
114
|
+
const ICON_IMAGE = 'icon-image'
|
|
115
|
+
const ICON_ANCHOR = 'icon-anchor'
|
|
116
|
+
const POINT_FEATURE = { featureId: 1, layerId: 'l1', geometry: { type: 'Point' } }
|
|
117
|
+
|
|
118
|
+
let map
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
map = makeMap()
|
|
122
|
+
map._symbolImageMap = { [SYMBOL_IMAGE]: SELECTED_IMAGE }
|
|
123
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'symbol' } : null) // NOSONAR
|
|
124
|
+
map.getLayoutProperty.mockReturnValue(SYMBOL_IMAGE)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const run = (selectedFeatures = [POINT_FEATURE]) =>
|
|
128
|
+
updateHighlightedFeatures({ LngLatBounds: lngLatBounds, map, selectedFeatures, stylesMap: { l1: {} } })
|
|
129
|
+
|
|
130
|
+
test('creates symbol highlight layer with selected image variant', () => {
|
|
131
|
+
run()
|
|
132
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
133
|
+
id: HIGHLIGHT_LAYER,
|
|
134
|
+
type: 'symbol',
|
|
135
|
+
layout: expect.objectContaining({ [ICON_IMAGE]: SELECTED_IMAGE })
|
|
136
|
+
}))
|
|
137
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, ICON_IMAGE, SELECTED_IMAGE)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('reads icon-anchor from original layer', () => {
|
|
141
|
+
map.getLayoutProperty.mockImplementation((_id, prop) => { // NOSONAR
|
|
142
|
+
if (prop === ICON_IMAGE) { return SYMBOL_IMAGE }
|
|
143
|
+
if (prop === ICON_ANCHOR) { return 'bottom' }
|
|
144
|
+
return null
|
|
145
|
+
})
|
|
146
|
+
run()
|
|
147
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
148
|
+
layout: expect.objectContaining({ [ICON_ANCHOR]: 'bottom' })
|
|
149
|
+
}))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('falls back to center anchor when icon-anchor is not set on original layer', () => {
|
|
153
|
+
map.getLayoutProperty.mockImplementation((_id, prop) => prop === ICON_IMAGE ? SYMBOL_IMAGE : null) // NOSONAR
|
|
154
|
+
run()
|
|
155
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
156
|
+
layout: expect.objectContaining({ [ICON_ANCHOR]: 'center' })
|
|
157
|
+
}))
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('spreads source-layer into symbol highlight layer for vector tile source', () => {
|
|
161
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'symbol', sourceLayer: 'points' } : null) // NOSONAR
|
|
162
|
+
run()
|
|
163
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ 'source-layer': 'points' }))
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('reuses existing symbol highlight layer without re-adding', () => {
|
|
167
|
+
map.getLayer.mockImplementation(id => { // NOSONAR
|
|
168
|
+
if (id === 'l1') { return { source: 's1', type: 'symbol' } }
|
|
169
|
+
if (id === HIGHLIGHT_LAYER) { return { source: 's1', type: 'symbol' } }
|
|
170
|
+
return null
|
|
171
|
+
})
|
|
172
|
+
run()
|
|
173
|
+
expect(map.addLayer).not.toHaveBeenCalled()
|
|
174
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, ICON_IMAGE, SELECTED_IMAGE)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('skips highlight when icon-image has no entry in _symbolImageMap', () => {
|
|
178
|
+
map.getLayoutProperty.mockReturnValue('symbol-abc123')
|
|
179
|
+
map._symbolImageMap = {} // no mapping registered
|
|
180
|
+
run()
|
|
181
|
+
expect(map.addLayer).not.toHaveBeenCalled()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('cleans up stale symbol highlight layer', () => {
|
|
185
|
+
map._highlightedSources = new Set(['stale'])
|
|
186
|
+
map.getLayer.mockImplementation(id => id === STALE_SYMBOL_LAYER ? { type: 'symbol' } : null) // NOSONAR
|
|
187
|
+
run([])
|
|
188
|
+
expect(map.setFilter).toHaveBeenCalledWith(STALE_SYMBOL_LAYER, EMPTY_FILTER)
|
|
131
189
|
})
|
|
132
190
|
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attaches a mousemove listener that changes the map cursor to a pointer when
|
|
3
|
+
* hovering over any of the specified layers.
|
|
4
|
+
*
|
|
5
|
+
* Line layers use a 10px tolerance bbox. Stroke layers that have a companion
|
|
6
|
+
* fill layer are skipped — the fill handles hover. Fill and symbol layers use
|
|
7
|
+
* exact point hit-testing.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} map - MapLibre map instance
|
|
10
|
+
* @param {string[]} layerIds - Layer IDs to watch
|
|
11
|
+
* @param {Function|null} prevHandler - Previous mousemove handler to remove
|
|
12
|
+
* @returns {Function|null} The new handler, or null if layerIds is empty
|
|
13
|
+
*/
|
|
14
|
+
const splitLayers = (map, layerIds) => {
|
|
15
|
+
const lineLayers = []
|
|
16
|
+
const otherLayers = []
|
|
17
|
+
for (const id of layerIds) {
|
|
18
|
+
const type = map.getLayer(id).type
|
|
19
|
+
if (type === 'line') {
|
|
20
|
+
const fillId = id.endsWith('-stroke') ? id.slice(0, -7) : null // NOSONAR
|
|
21
|
+
const hasFillCompanion = fillId !== null && layerIds.includes(fillId)
|
|
22
|
+
if (!hasFillCompanion) {
|
|
23
|
+
lineLayers.push(id)
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
otherLayers.push(id)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { lineLayers, otherLayers }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const setupHoverCursor = (map, layerIds, prevHandler) => {
|
|
33
|
+
const canvas = map.getCanvas()
|
|
34
|
+
|
|
35
|
+
if (prevHandler) {
|
|
36
|
+
map.off('mousemove', prevHandler)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!layerIds?.length) {
|
|
40
|
+
canvas.style.cursor = ''
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handler = (e) => {
|
|
45
|
+
const existingLayers = layerIds.filter(id => map.getLayer(id))
|
|
46
|
+
if (existingLayers.length === 0) {
|
|
47
|
+
canvas.style.cursor = ''
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { lineLayers, otherLayers } = splitLayers(map, existingLayers)
|
|
52
|
+
const { x, y } = e.point
|
|
53
|
+
const bbox = [[x - 10, y - 10], [x + 10, y + 10]]
|
|
54
|
+
const lineHit = lineLayers.length > 0 && map.queryRenderedFeatures(bbox, { layers: lineLayers }).length > 0
|
|
55
|
+
const otherHit = otherLayers.length > 0 && map.queryRenderedFeatures(e.point, { layers: otherLayers }).length > 0
|
|
56
|
+
canvas.style.cursor = (lineHit || otherHit) ? 'pointer' : ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
map.on('mousemove', handler)
|
|
60
|
+
return handler
|
|
61
|
+
}
|