@defra/interactive-map 0.0.22-alpha → 0.0.24-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.
Files changed (242) hide show
  1. package/assets/images/light-raster-thumb.jpg +0 -0
  2. package/assets/images/outdoor-raster-thumb.jpg +0 -0
  3. package/assets/images/road-raster-thumb.jpg +0 -0
  4. package/dist/css/index.css +1 -1
  5. package/dist/esm/im-core.js +1 -1
  6. package/dist/esm/im-shell.js +1 -1
  7. package/dist/umd/im-core.js +1 -1
  8. package/dist/umd/index.js +1 -1
  9. package/docs/api/button-definition.md +39 -0
  10. package/docs/api/context.md +4 -4
  11. package/docs/api/map-style-config.md +11 -2
  12. package/docs/api/marker-config.md +38 -7
  13. package/docs/api/panel-definition.md +7 -3
  14. package/docs/api/symbol-config.md +10 -26
  15. package/docs/api/symbol-registry.md +28 -13
  16. package/docs/api.md +24 -2
  17. package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
  18. package/docs/assets/images/toggle-marker-label.jpg +0 -0
  19. package/docs/examples/index.mdx +12 -0
  20. package/docs/examples/toggle-marker-label.mdx +89 -0
  21. package/docs/plugins/interact.md +4 -11
  22. package/docs/plugins/search.md +70 -0
  23. package/docusaurus.config.cjs +2 -1
  24. package/package.json +23 -9
  25. package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -1
  26. package/plugins/beta/datasets/dist/css/index.css +2 -2
  27. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  28. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +1 -1
  29. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +1 -1
  30. package/plugins/beta/datasets/dist/umd/maplibre/index.js +1 -1
  31. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
  32. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
  33. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
  34. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
  35. package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
  36. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
  37. package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
  38. package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
  39. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  40. package/plugins/beta/draw-ml/dist/css/index.css +1 -21
  41. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  42. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  43. package/plugins/beta/draw-ml/src/api/editFeature.js +9 -4
  44. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +11 -3
  45. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  46. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  47. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  48. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  49. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  50. package/plugins/beta/use-location/src/manifest.js +1 -0
  51. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  52. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  53. package/plugins/interact/dist/umd/index.js +1 -1
  54. package/plugins/interact/src/InteractInit.jsx +23 -55
  55. package/plugins/interact/src/InteractInit.test.js +58 -80
  56. package/plugins/interact/src/defaults.js +1 -3
  57. package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
  58. package/plugins/interact/src/hooks/useAttachEvents.test.js +68 -0
  59. package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
  60. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
  61. package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
  62. package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
  63. package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
  64. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
  65. package/plugins/interact/src/hooks/useMapItemList.js +187 -0
  66. package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
  67. package/plugins/interact/src/index.js +0 -2
  68. package/plugins/interact/src/index.test.js +0 -2
  69. package/plugins/interact/src/manifest.js +37 -6
  70. package/plugins/interact/src/reducer.js +8 -4
  71. package/plugins/interact/src/reducer.test.js +18 -3
  72. package/plugins/interact/src/utils/buildStylesMap.js +21 -10
  73. package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
  74. package/plugins/interact/src/utils/spatial.js +0 -15
  75. package/plugins/interact/src/utils/spatial.test.js +1 -13
  76. package/plugins/search/dist/css/index.css +1 -1
  77. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  78. package/plugins/search/dist/esm/index.js +1 -1
  79. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  80. package/plugins/search/dist/umd/index.js +1 -1
  81. package/plugins/search/src/Search.jsx +2 -1
  82. package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
  83. package/plugins/search/src/components/Form/Form.jsx +11 -7
  84. package/plugins/search/src/components/Form/Form.test.jsx +23 -1
  85. package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
  86. package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
  87. package/plugins/search/src/datasets.js +2 -2
  88. package/plugins/search/src/datasets.test.js +1 -1
  89. package/plugins/search/src/defaults.js +7 -1
  90. package/plugins/search/src/events/fetchSuggestions.js +13 -10
  91. package/plugins/search/src/events/fetchSuggestions.test.js +14 -1
  92. package/plugins/search/src/events/formHandlers.js +2 -4
  93. package/plugins/search/src/events/formHandlers.test.js +3 -3
  94. package/plugins/search/src/events/inputHandlers.js +1 -1
  95. package/plugins/search/src/events/inputHandlers.test.js +1 -1
  96. package/plugins/search/src/events/suggestionHandlers.js +2 -1
  97. package/plugins/search/src/events/suggestionHandlers.test.js +7 -1
  98. package/plugins/search/src/index.js +3 -7
  99. package/plugins/search/src/reducer.js +10 -5
  100. package/plugins/search/src/reducer.test.js +2 -1
  101. package/plugins/search/src/search.scss +1 -0
  102. package/plugins/search/src/utils/parseOsNamesResults.js +3 -3
  103. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  104. package/providers/beta/esri/src/esriProvider.js +4 -0
  105. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -0
  106. package/providers/beta/openlayers/dist/esm/index.js +1 -0
  107. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +2 -0
  108. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js.LICENSE.txt +1 -0
  109. package/providers/beta/openlayers/dist/umd/index.js +2 -0
  110. package/providers/beta/openlayers/dist/umd/index.js.LICENSE.txt +1 -0
  111. package/providers/beta/openlayers/src/appEvents.js +50 -0
  112. package/providers/beta/openlayers/src/appEvents.test.js +140 -0
  113. package/providers/beta/openlayers/src/defaults.js +36 -0
  114. package/providers/beta/openlayers/src/index.js +21 -0
  115. package/providers/beta/openlayers/src/mapEvents.js +132 -0
  116. package/providers/beta/openlayers/src/mapEvents.test.js +197 -0
  117. package/providers/beta/openlayers/src/openlayersProvider.js +234 -0
  118. package/providers/beta/openlayers/src/openlayersProvider.test.js +331 -0
  119. package/providers/beta/openlayers/src/utils/spatial.js +142 -0
  120. package/providers/beta/openlayers/src/utils/spatial.test.js +117 -0
  121. package/providers/beta/openlayers/src/utils/zoom.js +26 -0
  122. package/providers/beta/openlayers/src/utils/zoom.test.js +23 -0
  123. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  124. package/providers/maplibre/dist/esm/index.js +1 -1
  125. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  126. package/providers/maplibre/dist/umd/index.js +1 -1
  127. package/providers/maplibre/src/index.js +1 -1
  128. package/providers/maplibre/src/index.test.js +9 -14
  129. package/providers/maplibre/src/mapEvents.js +11 -5
  130. package/providers/maplibre/src/mapEvents.test.js +14 -3
  131. package/providers/maplibre/src/maplibreProvider.js +18 -11
  132. package/providers/maplibre/src/maplibreProvider.test.js +43 -36
  133. package/providers/maplibre/src/utils/highlightFeatures.js +112 -57
  134. package/providers/maplibre/src/utils/highlightFeatures.test.js +179 -51
  135. package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
  136. package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
  137. package/providers/maplibre/src/utils/patternImages.js +22 -19
  138. package/providers/maplibre/src/utils/patternImages.test.js +146 -167
  139. package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
  140. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
  141. package/providers/maplibre/src/utils/symbolImages.js +23 -86
  142. package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
  143. package/rollup.esm.mjs +14 -3
  144. package/src/App/components/Actions/Actions.module.scss +1 -0
  145. package/src/App/components/Actions/Actions.test.jsx +17 -4
  146. package/src/App/components/Hints/Hints.jsx +52 -0
  147. package/src/App/components/Hints/Hints.module.scss +37 -0
  148. package/src/App/components/Hints/Hints.test.jsx +104 -0
  149. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
  150. package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
  151. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
  152. package/src/App/components/Markers/LabelMarker.jsx +14 -0
  153. package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
  154. package/src/App/components/Markers/Markers.jsx +44 -75
  155. package/src/App/components/Markers/Markers.module.scss +58 -0
  156. package/src/App/components/Markers/Markers.test.jsx +90 -113
  157. package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
  158. package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
  159. package/src/App/components/Markers/SymbolMarker.jsx +31 -0
  160. package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
  161. package/src/App/components/Panel/Panel.jsx +1 -1
  162. package/src/App/components/Panel/Panel.module.scss +1 -0
  163. package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
  164. package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
  165. package/src/App/components/Tabs/Tabs.jsx +68 -0
  166. package/src/App/components/Tabs/Tabs.module.scss +57 -0
  167. package/src/App/components/Tabs/Tabs.test.jsx +121 -0
  168. package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
  169. package/src/App/components/Viewport/Features.jsx +35 -0
  170. package/src/App/components/Viewport/Features.module.scss +31 -0
  171. package/src/App/components/Viewport/Features.test.jsx +124 -0
  172. package/src/App/components/Viewport/MapStatus.jsx +0 -4
  173. package/src/App/components/Viewport/Viewport.jsx +35 -29
  174. package/src/App/components/Viewport/Viewport.module.scss +7 -21
  175. package/src/App/components/Viewport/Viewport.test.jsx +108 -65
  176. package/src/App/controls/keyboardActions.js +9 -3
  177. package/src/App/controls/keyboardActions.test.js +122 -87
  178. package/src/App/controls/keyboardShortcuts.js +1 -14
  179. package/src/App/hooks/useFeatureFocus.js +199 -0
  180. package/src/App/hooks/useFeatureFocus.test.js +635 -0
  181. package/src/App/hooks/useFeatureItems.js +30 -0
  182. package/src/App/hooks/useFeatureItems.test.js +98 -0
  183. package/src/App/hooks/useFocusVisible.js +4 -7
  184. package/src/App/hooks/useFocusVisible.test.js +6 -6
  185. package/src/App/hooks/useKeyboardHint.js +9 -34
  186. package/src/App/hooks/useKeyboardHint.test.js +34 -84
  187. package/src/App/hooks/useKeyboardShortcuts.js +8 -5
  188. package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
  189. package/src/App/hooks/useLayoutMeasurements.js +10 -4
  190. package/src/App/hooks/useLayoutMeasurements.test.js +30 -1
  191. package/src/App/hooks/useMarkerCursor.js +33 -0
  192. package/src/App/hooks/useMarkerCursor.test.js +87 -0
  193. package/src/App/hooks/useMarkersAPI.js +32 -9
  194. package/src/App/hooks/useMarkersAPI.test.js +163 -94
  195. package/src/App/hooks/useModalPanelBehaviour.test.js +2 -1
  196. package/src/App/layout/Layout.jsx +2 -0
  197. package/src/App/layout/Layout.test.jsx +3 -0
  198. package/src/App/registry/keyboardShortcutRegistry.js +3 -2
  199. package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
  200. package/src/App/renderer/mapButtons.js +5 -1
  201. package/src/App/renderer/mapButtons.test.js +37 -1
  202. package/src/App/store/AppProvider.test.jsx +5 -0
  203. package/src/App/store/MapProvider.test.jsx +8 -1
  204. package/src/App/store/ServiceProvider.jsx +4 -1
  205. package/src/App/store/ServiceProvider.test.jsx +11 -0
  206. package/src/App/store/appActionsMap.js +5 -1
  207. package/src/App/store/appActionsMap.test.js +13 -0
  208. package/src/App/store/appReducer.js +1 -0
  209. package/src/App/store/mapActionsMap.js +2 -1
  210. package/src/InteractiveMap/InteractiveMap.js +12 -0
  211. package/src/InteractiveMap/InteractiveMap.test.js +2 -0
  212. package/src/config/appConfig.js +3 -1
  213. package/src/config/appConfig.test.js +4 -0
  214. package/src/config/defaults.js +1 -1
  215. package/src/config/events.js +8 -0
  216. package/src/config/mapTheme.js +8 -4
  217. package/src/config/symbolConfig.js +23 -16
  218. package/src/scss/main.scss +3 -0
  219. package/src/scss/tools/_border-focus.scss +0 -1
  220. package/src/services/announcer.test.js +5 -7
  221. package/src/services/eventBus.js +15 -0
  222. package/src/services/eventBus.test.js +11 -0
  223. package/src/services/hints.js +47 -0
  224. package/src/services/hints.test.js +110 -0
  225. package/src/services/patternRegistry.js +69 -1
  226. package/src/services/patternRegistry.test.js +112 -1
  227. package/src/services/reverseGeocode.js +4 -0
  228. package/src/services/reverseGeocode.test.js +11 -1
  229. package/src/services/symbolRegistry.js +152 -17
  230. package/src/services/symbolRegistry.test.js +141 -31
  231. package/src/types.js +71 -14
  232. package/src/utils/constrainKeyboardFocus.js +5 -5
  233. package/src/utils/detectInterfaceType.js +27 -5
  234. package/src/utils/detectInterfaceType.test.js +29 -3
  235. package/src/utils/patternUtils.js +4 -67
  236. package/src/utils/patternUtils.test.js +1 -123
  237. package/src/utils/symbolUtils.js +11 -13
  238. package/src/utils/symbolUtils.test.js +18 -26
  239. package/webpack.dev.mjs +6 -1
  240. package/webpack.umd.mjs +1 -0
  241. package/plugins/interact/dist/css/index.css +0 -1
  242. package/plugins/interact/src/interact.scss +0 -24
@@ -4,9 +4,10 @@ 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
+ import { addSymbolsToMap } from './utils/symbolImages.js'
8
+ import { addPatternsToMap } from './utils/patternImages.js'
9
9
  import { getAreaDimensions, getCardinalMove, getResolution, getPaddedBounds, isGeometryObscured } from './utils/spatial.js'
10
+ import { symbolRegistry } from '../../../src/services/symbolRegistry.js'
10
11
 
11
12
  jest.mock('./defaults.js', () => ({
12
13
  DEFAULTS: { animationDuration: 400, coordinatePrecision: 7 },
@@ -35,8 +36,8 @@ jest.mock('./utils/labels.js', () => ({
35
36
  }))
36
37
  jest.mock('./utils/highlightFeatures.js', () => ({ updateHighlightedFeatures: jest.fn(() => []) }))
37
38
  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()) }))
39
+ jest.mock('./utils/symbolImages.js', () => ({ addSymbolsToMap: jest.fn(() => Promise.resolve()) }))
40
+ jest.mock('./utils/patternImages.js', () => ({ addPatternsToMap: jest.fn(() => Promise.resolve()) }))
40
41
 
41
42
  describe('MapLibreProvider', () => {
42
43
  let map, eventBus, maplibreModule, loadCallback
@@ -244,69 +245,75 @@ describe('MapLibreProvider', () => {
244
245
  await doInitMap(p)
245
246
  p.getFeaturesAtPoint({ x: 10, y: 20 }, { radius: 5 })
246
247
  expect(queryFeatures).toHaveBeenCalledWith(map, { x: 10, y: 20 }, { radius: 5 })
247
- p.updateHighlightedFeatures(['feat'], { style: 1 })
248
+ p.updateHighlightedFeatures(['feat'], null, { style: 1 })
248
249
  expect(updateHighlightedFeatures).toHaveBeenCalledWith({
249
- LngLatBounds: maplibreModule.LngLatBounds, map, selectedFeatures: ['feat'], stylesMap: { style: 1 }
250
+ LngLatBounds: maplibreModule.LngLatBounds, map, selectedFeatures: ['feat'], activeFeatures: null, stylesMap: { style: 1 }
250
251
  })
251
252
  })
252
253
 
253
- test('registerSymbols delegates to utility with map instance', async () => {
254
+ test('getVisibleFeatures returns empty array when no layers are present on the map', async () => {
255
+ const p = makeProvider()
256
+ await doInitMap(p)
257
+ map.getLayer.mockReturnValue(null)
258
+ expect(p.getVisibleFeatures(['l1', 'l2'])).toEqual([])
259
+ expect(map.queryRenderedFeatures).not.toHaveBeenCalled()
260
+ })
261
+
262
+ test('getVisibleFeatures queries only layers present on the map', async () => {
263
+ const p = makeProvider()
264
+ await doInitMap(p)
265
+ map.getLayer.mockImplementation(id => id === 'l1' ? {} : null) // NOSONAR
266
+ const features = [{ id: 'f1' }]
267
+ map.queryRenderedFeatures.mockReturnValue(features)
268
+ expect(p.getVisibleFeatures(['l1', 'l2'])).toEqual(features)
269
+ expect(map.queryRenderedFeatures).toHaveBeenCalledWith(undefined, { layers: ['l1'] })
270
+ })
271
+
272
+ test('addSymbolsToMap delegates to utility with map instance', async () => {
254
273
  const p = makeProvider()
255
274
  await doInitMap(p)
256
275
  const configs = [{ symbol: 'pin' }]
257
276
  const mapStyle = { id: 'test', selectedColor: '#0b0c0c' }
258
- const registry = {}
259
- await p.registerSymbols(configs, mapStyle, registry)
260
- expect(registerSymbols).toHaveBeenCalledWith(map, configs, mapStyle, registry, expect.any(Number))
277
+ await p.addSymbolsToMap(configs, mapStyle, symbolRegistry)
278
+ expect(addSymbolsToMap).toHaveBeenCalledWith(map, configs, mapStyle, symbolRegistry, expect.any(Number))
261
279
  })
262
280
 
263
- test('registerSymbols computes pixelRatio from getPixelRatio and mapSize scale factor', async () => {
281
+ test('addSymbolsToMap computes pixelRatio from getPixelRatio and mapSize scale factor', async () => {
264
282
  const p = makeProvider()
265
283
  await doInitMap(p)
266
284
  map.getPixelRatio.mockReturnValue(2)
267
- p.mapSize = 'medium' // scaleFactor['medium'] = 1.5
268
- const registry = {}
269
- await p.registerSymbols([], { id: 'test' }, registry)
270
- expect(registerSymbols).toHaveBeenCalledWith(map, [], { id: 'test' }, registry, 3) // 2 * 1.5
285
+ p.mapSize = 'medium'
286
+ await p.addSymbolsToMap([], { id: 'test' }, symbolRegistry)
287
+ expect(addSymbolsToMap).toHaveBeenCalledWith(map, [], { id: 'test' }, symbolRegistry, 2)
271
288
  })
272
289
 
273
- test('registerSymbols falls back to pixelRatio 1 when getPixelRatio returns 0', async () => {
290
+ test('addSymbolsToMap falls back to pixelRatio 1 when getPixelRatio returns 0', async () => {
274
291
  const p = makeProvider()
275
292
  await doInitMap(p)
276
293
  map.getPixelRatio.mockReturnValue(0)
277
294
  p.mapSize = 'small' // scaleFactor['small'] = 1
278
- const registry = {}
279
- await p.registerSymbols([], { id: 'test' }, registry)
280
- expect(registerSymbols).toHaveBeenCalledWith(map, [], { id: 'test' }, registry, 1) // (0 || 1) * 1
295
+ await p.addSymbolsToMap([], { id: 'test' }, symbolRegistry)
296
+ expect(addSymbolsToMap).toHaveBeenCalledWith(map, [], { id: 'test' }, symbolRegistry, 1)
281
297
  })
282
298
 
283
- test('registerPatterns delegates to utility with map instance and pixelRatio', async () => {
299
+ test('addPatternsToMap falls back to pixelRatio 1 when getPixelRatio returns 0', async () => {
284
300
  const p = makeProvider()
285
301
  await doInitMap(p)
302
+ map.getPixelRatio.mockReturnValue(0)
286
303
  const configs = [{ fillPattern: 'dot' }]
287
304
  const registry = {}
288
- await p.registerPatterns(configs, 'test', registry)
289
- expect(registerPatterns).toHaveBeenCalledWith(map, configs, 'test', registry, 1) // getPixelRatio()=1, mapSize unset → 1*1
305
+ await p.addPatternsToMap(configs, 'test', registry)
306
+ expect(addPatternsToMap).toHaveBeenCalledWith(map, configs, 'test', registry, 1)
290
307
  })
291
308
 
292
- test('registerPatterns computes pixelRatio from getPixelRatio and mapSize scale factor', async () => {
309
+ test('addPatternsToMap is called with pixelRatio from getPixelRatio', async () => {
293
310
  const p = makeProvider()
294
311
  await doInitMap(p)
295
312
  map.getPixelRatio.mockReturnValue(2)
296
- p.mapSize = 'medium' // scaleFactor['medium'] = 1.5
297
- const registry = {}
298
- await p.registerPatterns([], 'test', registry)
299
- expect(registerPatterns).toHaveBeenCalledWith(map, [], 'test', registry, 3) // 2 * 1.5
300
- })
301
-
302
- test('registerPatterns falls back to pixelRatio 1 when getPixelRatio returns 0', async () => {
303
- const p = makeProvider()
304
- await doInitMap(p)
305
- map.getPixelRatio.mockReturnValue(0)
306
- p.mapSize = 'small' // scaleFactor['small'] = 1
313
+ p.mapSize = 'medium'
307
314
  const registry = {}
308
- await p.registerPatterns([], 'test', registry)
309
- expect(registerPatterns).toHaveBeenCalledWith(map, [], 'test', registry, 1) // (0 || 1) * 1
315
+ await p.addPatternsToMap([], 'test', registry)
316
+ expect(addPatternsToMap).toHaveBeenCalledWith(map, [], 'test', registry, 2)
310
317
  })
311
318
 
312
319
  describe('setHoverCursor', () => {
@@ -1,3 +1,15 @@
1
+ const ICON_IMAGE = 'icon-image'
2
+
3
+ const ACTIVE_PREFIX = 'active-highlight'
4
+ const ACTIVE_INNER_PREFIX = 'active-highlight-inner'
5
+ const SELECTED_PREFIX = 'selected-highlight'
6
+
7
+ // Inner and selected both use the selected colour/width (black, thin)
8
+ const usesSelectedStyle = (prefix) => prefix === SELECTED_PREFIX || prefix === ACTIVE_INNER_PREFIX
9
+
10
+ const getActiveImageId = (map, imageId) => map._activeSymbolImageMap?.[imageId] ?? null
11
+ const getSelectedImageId = (map, imageId) => map._selectedSymbolImageMap?.[imageId] ?? null
12
+
1
13
  const groupFeaturesBySource = (map, selectedFeatures) => {
2
14
  const featuresBySource = {}
3
15
 
@@ -20,7 +32,7 @@ const groupFeaturesBySource = (map, selectedFeatures) => {
20
32
  }
21
33
  }
22
34
 
23
- // Track whether any selected feature on this source is a polygon
35
+ // Track whether any feature on this source is a polygon
24
36
  if (geometry && (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon')) {
25
37
  featuresBySource[sourceId].hasFillGeometry = true
26
38
  featuresBySource[sourceId].fillIds.add(featureId)
@@ -32,12 +44,11 @@ const groupFeaturesBySource = (map, selectedFeatures) => {
32
44
  return featuresBySource
33
45
  }
34
46
 
35
- const cleanupStaleSources = (map, previousSources, currentSources) => {
47
+ const cleanupStaleSources = (map, previousSources, currentSources, prefix) => {
36
48
  previousSources.forEach(src => {
37
49
  if (!currentSources.has(src)) {
38
- const base = `highlight-${src}`
39
- const layers = [`${base}-fill`, `${base}-line`, `${base}-symbol`]
40
- layers.forEach(id => {
50
+ const base = `${prefix}-${src}`
51
+ ;[`${base}-fill`, `${base}-line`, `${base}-symbol`].forEach(id => {
41
52
  if (map.getLayer(id)) {
42
53
  map.setFilter(id, ['==', 'id', ''])
43
54
  }
@@ -46,6 +57,12 @@ const cleanupStaleSources = (map, previousSources, currentSources) => {
46
57
  })
47
58
  }
48
59
 
60
+ const clearPrefixSources = (map, prefix) => {
61
+ const key = `_${prefix.replaceAll('-', '')}Sources`
62
+ cleanupStaleSources(map, map[key] || new Set(), new Set(), prefix)
63
+ map[key] = new Set()
64
+ }
65
+
49
66
  const applyHighlightLayer = (map, id, type, sourceId, srcLayer, paint, filter) => {
50
67
  if (!map.getLayer(id)) {
51
68
  map.addLayer({
@@ -71,13 +88,13 @@ const applySymbolHighlightLayer = (map, id, sourceId, srcLayer, originalLayerId,
71
88
  source: sourceId,
72
89
  ...(srcLayer && { 'source-layer': srcLayer }),
73
90
  layout: {
74
- 'icon-image': imageId,
91
+ [ICON_IMAGE]: imageId,
75
92
  'icon-anchor': map.getLayoutProperty(originalLayerId, 'icon-anchor') ?? 'center',
76
93
  'icon-allow-overlap': true
77
94
  }
78
95
  })
79
96
  }
80
- map.setLayoutProperty(id, 'icon-image', imageId)
97
+ map.setLayoutProperty(id, ICON_IMAGE, imageId)
81
98
  map.setFilter(id, filter)
82
99
  map.moveLayer(id)
83
100
  }
@@ -97,67 +114,105 @@ const calculateBounds = (LngLatBounds, renderedFeatures) => {
97
114
  return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]
98
115
  }
99
116
 
100
- const getSelectedImageId = (map, imageId) => map._symbolImageMap?.[imageId] ?? null
117
+ const applySymbolGeomHighlight = (map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId) => {
118
+ const imageId = map.getLayoutProperty(layerId, ICON_IMAGE)
119
+ const symbolImageId = getSymbolImageId(map, imageId)
120
+ if (symbolImageId) {
121
+ applySymbolHighlightLayer(map, `${base}-symbol`, sourceId, srcLayer, layerId, symbolImageId, filter)
122
+ }
123
+ }
124
+
125
+ const applyFillGeomHighlight = (map, base, sourceId, srcLayer, { isSelected, idExpression, fillIds, fill, lineColor, lineWidth, filter }) => {
126
+ if (isSelected) {
127
+ const fillFilter = ['in', idExpression, ['literal', [...fillIds]]]
128
+ // Only apply fill highlight to polygon features, not to any co-selected line features
129
+ applyHighlightLayer(map, `${base}-fill`, 'fill', sourceId, srcLayer, { 'fill-color': fill }, fillFilter)
130
+ }
131
+ applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, { 'line-color': lineColor, 'line-width': lineWidth }, filter)
132
+ }
133
+
134
+ const applySourceHighlight = (map, sourceId, featuresBySource, stylesMap, prefix, getSymbolImageId) => {
135
+ const { ids, fillIds, idProperty, layerId, hasFillGeometry } = featuresBySource[sourceId]
136
+ const baseLayer = map.getLayer(layerId)
137
+ const srcLayer = baseLayer.sourceLayer
138
+ const geom = hasFillGeometry ? 'fill' : baseLayer.type
139
+ const base = `${prefix}-${sourceId}`
140
+ const style = stylesMap[layerId]
141
+
142
+ if (!style) {
143
+ return
144
+ }
145
+
146
+ const { stroke, selectionStroke, strokeWidth, activeStrokeWidth, fill } = style
147
+ const isSelected = prefix === SELECTED_PREFIX
148
+ const selectedStyle = usesSelectedStyle(prefix)
149
+ const lineColor = selectedStyle ? selectionStroke : stroke
150
+ const lineWidth = selectedStyle ? strokeWidth : activeStrokeWidth
151
+ const idExpression = idProperty ? ['get', idProperty] : ['id']
152
+ const filter = ['in', idExpression, ['literal', [...ids]]]
153
+
154
+ if (geom === 'fill') {
155
+ applyFillGeomHighlight(map, base, sourceId, srcLayer, { isSelected, idExpression, fillIds, fill, lineColor, lineWidth, filter })
156
+ }
157
+
158
+ if (geom === 'line') {
159
+ if (map.getLayer(`${base}-fill`)) {
160
+ // Clear any fill highlight from a previous polygon on the same source
161
+ map.setFilter(`${base}-fill`, ['==', 'id', ''])
162
+ }
163
+ applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, { 'line-color': lineColor, 'line-width': lineWidth }, filter)
164
+ }
165
+
166
+ if (geom === 'symbol') {
167
+ applySymbolGeomHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
168
+ }
169
+ }
170
+
171
+ const applyFeatureHighlights = (map, features, stylesMap, prefix, getSymbolImageId) => {
172
+ const featuresBySource = groupFeaturesBySource(map, features)
173
+ const currentSources = new Set(Object.keys(featuresBySource))
174
+ const storageKey = `_${prefix.replaceAll('-', '')}Sources`
175
+ const previousSources = map[storageKey] || new Set()
176
+
177
+ cleanupStaleSources(map, previousSources, currentSources, prefix)
178
+ map[storageKey] = currentSources
179
+ currentSources.forEach(sourceId => applySourceHighlight(map, sourceId, featuresBySource, stylesMap, prefix, getSymbolImageId))
180
+
181
+ return featuresBySource
182
+ }
101
183
 
102
184
  /**
103
185
  * Update highlighted features using pure filters.
186
+ * activeFeatures (keyboard cursor) render with the active ring (yellow) plus a selected ring inner (black).
187
+ * selectedFeatures render with the selected ring (black) only.
104
188
  * Supports fill, line and symbol geometry, multi-source, cleanup, and bounds.
105
189
  */
106
- export function updateHighlightedFeatures ({ LngLatBounds, map, selectedFeatures, stylesMap }) {
190
+ export function updateHighlightedFeatures ({ LngLatBounds, map, selectedFeatures, activeFeatures, stylesMap }) {
107
191
  if (!map) {
108
192
  return null
109
193
  }
110
194
 
111
- const featuresBySource = groupFeaturesBySource(map, selectedFeatures)
112
- const renderedFeatures = []
113
-
114
- const currentSources = new Set(Object.keys(featuresBySource))
115
- const previousSources = map._highlightedSources || new Set()
116
-
117
- cleanupStaleSources(map, previousSources, currentSources)
118
- map._highlightedSources = currentSources
119
-
120
- // Apply highlights for current sources
121
- currentSources.forEach(sourceId => {
122
- const { ids, fillIds, idProperty, layerId, hasFillGeometry } = featuresBySource[sourceId]
123
- const baseLayer = map.getLayer(layerId)
124
- const srcLayer = baseLayer.sourceLayer
125
-
126
- // Use the actual feature geometry to determine highlight type
127
- const geom = hasFillGeometry ? 'fill' : baseLayer.type
128
- const base = `highlight-${sourceId}`
129
-
130
- const idExpression = idProperty ? ['get', idProperty] : ['id']
131
- const filter = ['in', idExpression, ['literal', [...ids]]]
132
-
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 }
137
- // Only apply fill highlight to polygon features, not to any co-selected line features
138
- applyHighlightLayer(map, `${base}-fill`, 'fill', sourceId, srcLayer, { 'fill-color': fill }, fillFilter)
139
- applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, linePaint, filter)
140
- }
141
-
142
- if (geom === 'line') {
143
- const { stroke, strokeWidth } = stylesMap[layerId]
144
- const linePaint = { 'line-color': stroke, 'line-width': strokeWidth }
145
- // Clear any fill highlight from a previous polygon selection on the same source
146
- if (map.getLayer(`${base}-fill`)) {
147
- map.setFilter(`${base}-fill`, ['==', 'id', ''])
148
- }
149
- applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, linePaint, filter)
150
- }
195
+ // Active cursor features — rendered first so selected layers appear on top
196
+ if (activeFeatures?.length) {
197
+ applyFeatureHighlights(map, activeFeatures, stylesMap, ACTIVE_PREFIX, getActiveImageId)
198
+ // Black selected stroke on top of yellow active (mirrors resolveActive for symbols)
199
+ applyFeatureHighlights(map, activeFeatures, stylesMap, ACTIVE_INNER_PREFIX, getSelectedImageId)
200
+ } else {
201
+ clearPrefixSources(map, ACTIVE_PREFIX)
202
+ clearPrefixSources(map, ACTIVE_INNER_PREFIX)
203
+ }
151
204
 
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
- }
205
+ // Selection features
206
+ let featuresBySource = {}
207
+ if (selectedFeatures?.length) {
208
+ featuresBySource = applyFeatureHighlights(map, selectedFeatures, stylesMap, SELECTED_PREFIX, getSelectedImageId)
209
+ } else {
210
+ clearPrefixSources(map, SELECTED_PREFIX)
211
+ }
159
212
 
160
- // Bounds only from rendered tiles
213
+ // Bounds only from selected features
214
+ const renderedFeatures = []
215
+ Object.entries(featuresBySource).forEach(([, { ids, idProperty, layerId }]) => {
161
216
  renderedFeatures.push(
162
217
  ...map
163
218
  .queryRenderedFeatures({ layers: [layerId] })