@defra/interactive-map 0.0.40-alpha → 0.0.42-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 (374) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/button-definition.md +7 -8
  7. package/docs/api/control-definition.md +17 -1
  8. package/docs/api/panel-definition.md +5 -0
  9. package/docs/api/slots.md +50 -6
  10. package/docs/assets/images/slot-map.svg +61 -262
  11. package/docs/examples/add-polygons.mdx +8 -2
  12. package/docs/examples/add-symbols.mdx +8 -2
  13. package/docs/examples/draw-tools.mdx +3 -3
  14. package/docs/plugins/datasets.md +23 -15
  15. package/docs/plugins/draw.md +79 -16
  16. package/docs/plugins/map-key.md +147 -0
  17. package/docs/plugins/search.md +1 -1
  18. package/docs/plugins.md +5 -1
  19. package/govuk-prototype-kit.config.json +2 -0
  20. package/jest.setup.js +5 -0
  21. package/package.json +9 -1
  22. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  23. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  24. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  25. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  26. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  27. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  28. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  29. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  30. package/plugins/beta/use-location/src/manifest.js +1 -1
  31. package/plugins/datasets/dist/css/index.css +1 -1
  32. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  33. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  34. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  35. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  36. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  37. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  38. package/plugins/datasets/dist/umd/index.js +1 -1
  39. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  40. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  41. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  42. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  43. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  44. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  45. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  46. package/plugins/datasets/src/datasets.scss +0 -1
  47. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  48. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  49. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  50. package/plugins/datasets/src/index.test.js +36 -0
  51. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  52. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  53. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  54. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  55. package/plugins/datasets/src/manifest.js +0 -38
  56. package/plugins/datasets/src/manifest.test.js +49 -0
  57. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  58. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  59. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  60. package/plugins/datasets/src/registry/dataset.js +13 -1
  61. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  62. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  63. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  64. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  65. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  66. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  67. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  68. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  69. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  70. package/plugins/draw/dist/esm/index.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  72. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  73. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  74. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  75. package/plugins/draw/dist/umd/index.js +1 -1
  76. package/plugins/draw/src/DrawInit.jsx +10 -2
  77. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  78. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  79. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  80. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  81. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  110. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  112. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  114. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  116. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  117. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  118. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  130. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  132. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  133. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  135. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  137. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  140. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  142. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  144. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  147. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  148. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  149. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  150. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  151. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  152. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  153. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  154. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  155. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  156. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  162. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  163. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  164. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  165. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  166. package/plugins/draw/src/api/addFeature.js +16 -1
  167. package/plugins/draw/src/api/addFeature.test.js +62 -5
  168. package/plugins/draw/src/api/createNewShape.js +41 -0
  169. package/plugins/draw/src/api/createNewShape.test.js +63 -0
  170. package/plugins/draw/src/api/editFeature.js +7 -4
  171. package/plugins/draw/src/api/editFeature.test.js +16 -0
  172. package/plugins/draw/src/api/newLine.js +2 -40
  173. package/plugins/draw/src/api/newPoint.js +42 -0
  174. package/plugins/draw/src/api/newPoint.test.js +117 -0
  175. package/plugins/draw/src/api/newPolygon.js +2 -40
  176. package/plugins/draw/src/api/setStyle.js +43 -0
  177. package/plugins/draw/src/api/setStyle.test.js +75 -0
  178. package/plugins/draw/src/events.js +17 -10
  179. package/plugins/draw/src/events.test.js +14 -0
  180. package/plugins/draw/src/manifest.js +18 -6
  181. package/plugins/draw/src/manifest.test.js +32 -1
  182. package/plugins/draw/src/reducer.test.js +7 -0
  183. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  184. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  185. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  186. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  187. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  188. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  189. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  190. package/plugins/interact/dist/umd/index.js +1 -1
  191. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  192. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  193. package/plugins/interact/src/manifest.js +1 -1
  194. package/plugins/interact/src/utils/featureQueries.js +18 -0
  195. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  196. package/plugins/interact/src/utils/spatial.js +9 -3
  197. package/plugins/interact/src/utils/spatial.test.js +12 -1
  198. package/plugins/map-key/dist/css/index.css +68 -0
  199. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  200. package/plugins/map-key/dist/esm/index.js +1 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  202. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  203. package/plugins/map-key/dist/umd/index.js +2 -0
  204. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  205. package/plugins/map-key/package.json +28 -0
  206. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  208. package/plugins/map-key/src/components/Key/Key.jsx +43 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +21 -14
  210. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  212. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.jsx +26 -0
  214. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  215. package/plugins/map-key/src/components/Key/KeySvg.jsx +41 -0
  216. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  217. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +22 -0
  218. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.jsx +7 -2
  220. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +8 -2
  221. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +26 -0
  222. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.jsx +8 -3
  224. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +17 -6
  225. package/plugins/map-key/src/components/Key/MapKey.jsx +53 -0
  226. package/plugins/map-key/src/components/Key/MapKey.test.jsx +147 -0
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  228. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  229. package/plugins/map-key/src/index.js +13 -0
  230. package/plugins/map-key/src/index.test.js +31 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  232. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  233. package/plugins/map-key/src/manifest.js +30 -0
  234. package/plugins/map-key/src/manifest.test.js +63 -0
  235. package/plugins/map-key/src/mapKey.scss +4 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  237. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  238. package/plugins/map-key/src/registry/index.js +3 -0
  239. package/plugins/map-key/src/registry/index.test.js +15 -0
  240. package/plugins/menu/dist/css/index.css +80 -0
  241. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  242. package/plugins/menu/dist/esm/index.js +1 -0
  243. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  244. package/plugins/menu/dist/umd/index.js +2 -0
  245. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  246. package/plugins/menu/package.json +28 -0
  247. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  249. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  250. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  251. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  252. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  254. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  255. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  257. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  258. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  259. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  260. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  261. package/plugins/menu/src/index.js +13 -0
  262. package/plugins/menu/src/index.test.js +31 -0
  263. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  264. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  265. package/plugins/menu/src/initialise/createMenu.js +10 -0
  266. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  267. package/plugins/menu/src/manifest.js +35 -0
  268. package/plugins/menu/src/manifest.test.js +63 -0
  269. package/plugins/menu/src/menu.scss +4 -0
  270. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  271. package/plugins/menu/src/reducers/pluginState.js +35 -0
  272. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  274. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  275. package/plugins/menu/src/registry/index.js +3 -0
  276. package/plugins/menu/src/registry/index.test.js +15 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  278. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  279. package/plugins/search/dist/css/index.css +1 -1
  280. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  281. package/plugins/search/dist/esm/index.js +1 -1
  282. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  283. package/plugins/search/dist/umd/index.js +1 -1
  284. package/plugins/search/src/Search.jsx +2 -3
  285. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  286. package/plugins/search/src/index.js +0 -1
  287. package/plugins/search/src/index.test.js +2 -4
  288. package/plugins/search/src/manifest.js +1 -1
  289. package/plugins/search/src/search.scss +17 -16
  290. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  292. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  294. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  296. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  298. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  299. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  301. package/providers/maplibre/dist/umd/index.js +1 -1
  302. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  303. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  304. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  305. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  306. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  307. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  308. package/providers/maplibre/src/utils/patternImages.js +1 -1
  309. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  310. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  311. package/rollup.esm.mjs +11 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Hints/Hints.test.jsx +13 -0
  314. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
  315. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
  316. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  317. package/src/App/components/Panel/Panel.jsx +65 -8
  318. package/src/App/components/Panel/Panel.module.scss +49 -15
  319. package/src/App/components/Panel/Panel.test.jsx +114 -1
  320. package/src/App/components/Tabs/Tabs.jsx +43 -17
  321. package/src/App/components/Tabs/Tabs.module.scss +32 -5
  322. package/src/App/components/Tabs/Tabs.test.jsx +40 -10
  323. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  324. package/src/App/hooks/useFeatureItems.js +16 -7
  325. package/src/App/hooks/useFeatureItems.test.js +57 -0
  326. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  327. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  328. package/src/App/layout/Layout.jsx +9 -10
  329. package/src/App/layout/Layout.test.jsx +2 -0
  330. package/src/App/layout/layout.module.scss +31 -17
  331. package/src/App/registry/pluginRegistry.js +1 -1
  332. package/src/App/registry/pluginRegistry.test.js +12 -0
  333. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  334. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  335. package/src/App/renderer/groupByKey.js +28 -0
  336. package/src/App/renderer/groupByKey.test.js +39 -0
  337. package/src/App/renderer/groupIntoTabs.js +42 -0
  338. package/src/App/renderer/groupIntoTabs.test.js +95 -0
  339. package/src/App/renderer/mapButtons.js +97 -129
  340. package/src/App/renderer/mapButtons.test.js +67 -83
  341. package/src/App/renderer/mapControls.js +4 -1
  342. package/src/App/renderer/mapControls.test.js +32 -0
  343. package/src/App/renderer/mapPanels.js +53 -11
  344. package/src/App/renderer/mapPanels.test.js +95 -2
  345. package/src/App/renderer/orderItems.js +28 -0
  346. package/src/App/renderer/orderItems.test.js +50 -0
  347. package/src/App/renderer/slotAggregator.js +2 -18
  348. package/src/App/renderer/slotHelpers.js +3 -1
  349. package/src/App/renderer/slotHelpers.test.js +5 -0
  350. package/src/App/renderer/slots.js +3 -0
  351. package/src/App/store/AppProvider.jsx +1 -0
  352. package/src/App/store/appDispatchMiddleware.js +1 -1
  353. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  354. package/src/config/appConfig.js +2 -2
  355. package/src/config/events.js +2 -0
  356. package/src/scss/settings/_colors.scss +1 -0
  357. package/src/scss/settings/_dimensions.scss +2 -3
  358. package/src/scss/settings/_transition.scss +1 -1
  359. package/src/services/eventBus.js +44 -0
  360. package/src/services/eventBus.test.js +83 -0
  361. package/src/services/symbolRegistry.js +1 -1
  362. package/src/types.js +31 -3
  363. package/webpack.umd.mjs +2 -0
  364. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  365. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  366. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  367. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  368. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  369. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  370. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  371. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  372. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  373. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  374. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -0,0 +1,18 @@
1
+ import { siblingDrawLayerId } from './drawLayerBuckets.js'
2
+
3
+ test('returns the .hot sibling for a .cold layer id', () => {
4
+ expect(siblingDrawLayerId('point-symbol.cold')).toBe('point-symbol.hot')
5
+ })
6
+
7
+ test('returns the .cold sibling for a .hot layer id', () => {
8
+ expect(siblingDrawLayerId('point-symbol.hot')).toBe('point-symbol.cold')
9
+ })
10
+
11
+ test('returns null for a layer id with no cold/hot suffix', () => {
12
+ expect(siblingDrawLayerId('field-parcels')).toBeNull()
13
+ })
14
+
15
+ test('does not false-positive on a layer id that merely contains "cold" or "hot" mid-string', () => {
16
+ expect(siblingDrawLayerId('coldstore-layer')).toBeNull()
17
+ expect(siblingDrawLayerId('hotfix-layer')).toBeNull()
18
+ })
@@ -1,3 +1,5 @@
1
+ import { siblingDrawLayerId } from './drawLayerBuckets.js'
2
+
1
3
  const ICON_IMAGE = 'icon-image'
2
4
 
3
5
  const ACTIVE_PREFIX = 'active-highlight'
@@ -10,44 +12,55 @@ const usesSelectedStyle = (prefix) => prefix === SELECTED_PREFIX || prefix === A
10
12
  const getActiveImageId = (map, imageId) => map._activeSymbolImageMap?.[imageId] ?? null
11
13
  const getSelectedImageId = (map, imageId) => map._selectedSymbolImageMap?.[imageId] ?? null
12
14
 
13
- const groupFeaturesBySource = (map, selectedFeatures) => {
14
- const featuresBySource = {}
15
-
16
- selectedFeatures?.forEach(({ featureId, layerId, idProperty, geometry }) => {
17
- const layer = map.getLayer(layerId)
15
+ // Grouped by layerId, not sourceId: mapbox-gl-draw stores every geometry type in the same
16
+ // cold/hot sources, so a point (symbol layer) and a polygon (fill layer) selected together
17
+ // share a source — grouping by source would collapse them into one group that can only
18
+ // resolve one geometry-type path, silently dropping the other's highlight.
19
+ //
20
+ // A selection's recorded layerId can also go stale if the feature later moves to its cold/hot
21
+ // sibling bucket — see drawLayerBuckets.js.
22
+ const addFeatureUnderLayer = (map, featuresByLayer, layerId, { featureId, idProperty, geometry }) => {
23
+ const layer = map.getLayer(layerId)
24
+ if (!layer) {
25
+ return
26
+ }
18
27
 
19
- if (!layer) {
20
- return
28
+ if (!featuresByLayer[layerId]) {
29
+ featuresByLayer[layerId] = {
30
+ ids: new Set(),
31
+ fillIds: new Set(),
32
+ idProperty,
33
+ sourceId: layer.source,
34
+ hasFillGeometry: false
21
35
  }
36
+ }
22
37
 
23
- const sourceId = layer.source
38
+ if (geometry && (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon')) {
39
+ featuresByLayer[layerId].hasFillGeometry = true
40
+ featuresByLayer[layerId].fillIds.add(featureId)
41
+ }
24
42
 
25
- if (!featuresBySource[sourceId]) {
26
- featuresBySource[sourceId] = {
27
- ids: new Set(),
28
- fillIds: new Set(),
29
- idProperty,
30
- layerId,
31
- hasFillGeometry: false
32
- }
33
- }
43
+ featuresByLayer[layerId].ids.add(featureId)
44
+ }
34
45
 
35
- // Track whether any feature on this source is a polygon
36
- if (geometry && (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon')) {
37
- featuresBySource[sourceId].hasFillGeometry = true
38
- featuresBySource[sourceId].fillIds.add(featureId)
39
- }
46
+ const groupFeaturesByLayer = (map, selectedFeatures) => {
47
+ const featuresByLayer = {}
40
48
 
41
- featuresBySource[sourceId].ids.add(featureId)
49
+ selectedFeatures?.forEach((feature) => {
50
+ addFeatureUnderLayer(map, featuresByLayer, feature.layerId, feature)
51
+ const sibling = siblingDrawLayerId(feature.layerId)
52
+ if (sibling) {
53
+ addFeatureUnderLayer(map, featuresByLayer, sibling, feature)
54
+ }
42
55
  })
43
56
 
44
- return featuresBySource
57
+ return featuresByLayer
45
58
  }
46
59
 
47
- const cleanupStaleSources = (map, previousSources, currentSources, prefix) => {
48
- previousSources.forEach(src => {
49
- if (!currentSources.has(src)) {
50
- const base = `${prefix}-${src}`
60
+ const cleanupStaleLayers = (map, previousLayerIds, currentLayerIds, prefix) => {
61
+ previousLayerIds.forEach(layerId => {
62
+ if (!currentLayerIds.has(layerId)) {
63
+ const base = `${prefix}-${layerId}`
51
64
  ;[`${base}-fill`, `${base}-line`, `${base}-symbol`].forEach(id => {
52
65
  if (map.getLayer(id)) {
53
66
  map.setFilter(id, ['==', 'id', ''])
@@ -57,9 +70,9 @@ const cleanupStaleSources = (map, previousSources, currentSources, prefix) => {
57
70
  })
58
71
  }
59
72
 
60
- const clearPrefixSources = (map, prefix) => {
73
+ const clearPrefixLayers = (map, prefix) => {
61
74
  const key = `_${prefix.replaceAll('-', '')}Sources`
62
- cleanupStaleSources(map, map[key] || new Set(), new Set(), prefix)
75
+ cleanupStaleLayers(map, map[key] || new Set(), new Set(), prefix)
63
76
  map[key] = new Set()
64
77
  }
65
78
 
@@ -82,6 +95,9 @@ const applyHighlightLayer = (map, id, type, sourceId, srcLayer, paint, filter) =
82
95
  }
83
96
 
84
97
  const applySymbolHighlightLayer = (map, id, sourceId, srcLayer, originalLayerId, imageId, filter) => {
98
+ // icon-offset only exists on layers needing anchor precision correction (draw's point-symbol
99
+ // layer) — read off the original layer, omitted when absent.
100
+ const iconOffset = map.getLayoutProperty(originalLayerId, 'icon-offset')
85
101
  if (!map.getLayer(id)) {
86
102
  map.addLayer({
87
103
  id,
@@ -91,11 +107,15 @@ const applySymbolHighlightLayer = (map, id, sourceId, srcLayer, originalLayerId,
91
107
  layout: {
92
108
  [ICON_IMAGE]: imageId,
93
109
  'icon-anchor': map.getLayoutProperty(originalLayerId, 'icon-anchor') ?? 'center',
110
+ ...(iconOffset !== undefined && { 'icon-offset': iconOffset }),
94
111
  'icon-allow-overlap': true
95
112
  }
96
113
  })
97
114
  }
98
115
  map.setLayoutProperty(id, ICON_IMAGE, imageId)
116
+ if (iconOffset !== undefined) {
117
+ map.setLayoutProperty(id, 'icon-offset', iconOffset)
118
+ }
99
119
  map.setFilter(id, filter)
100
120
  map.moveLayer(id)
101
121
  }
@@ -115,8 +135,16 @@ const calculateBounds = (LngLatBounds, renderedFeatures) => {
115
135
  return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]
116
136
  }
117
137
 
138
+ // Draw's point-symbol layer has a per-feature data-driven icon-image, so there's no single
139
+ // shared image id to reverse-map — pointSymbolImages.js precomputes each point's own active/
140
+ // selected variant as a property, and the highlight layer just reads that directly.
118
141
  const applySymbolGeomHighlight = (map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId) => {
119
142
  const imageId = map.getLayoutProperty(layerId, ICON_IMAGE)
143
+ if (Array.isArray(imageId)) {
144
+ const property = getSymbolImageId === getActiveImageId ? 'user_symbolActiveImageId' : 'user_symbolSelectedImageId'
145
+ applySymbolHighlightLayer(map, `${base}-symbol`, sourceId, srcLayer, layerId, ['get', property], filter)
146
+ return
147
+ }
120
148
  const symbolImageId = getSymbolImageId(map, imageId)
121
149
  if (symbolImageId) {
122
150
  applySymbolHighlightLayer(map, `${base}-symbol`, sourceId, srcLayer, layerId, symbolImageId, filter)
@@ -161,26 +189,23 @@ const applyLineHighlight = (map, base, sourceId, srcLayer, lineColor, lineWidth,
161
189
  applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, { 'line-color': lineColor, 'line-width': lineWidth }, filter)
162
190
  }
163
191
 
164
- const applySymbolHighlight = (map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId) => {
165
- applySymbolGeomHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
166
- }
167
-
168
- // Orchestrates highlight layers for a single source. prefix drives which visual style is used:
192
+ // Orchestrates highlight layers for a single layer. prefix drives which visual style is used:
169
193
  // active-highlight → yellow active ring (stroke only)
170
194
  // active-highlight-inner → black thin ring drawn on top of active
171
195
  // selected-highlight → black thin ring + fill for polygon features
172
- const applySourceHighlight = (map, sourceId, featuresBySource, stylesMap, prefix, getSymbolImageId) => {
173
- const { ids, fillIds, idProperty, layerId, hasFillGeometry } = featuresBySource[sourceId]
196
+ const applyLayerHighlight = (map, layerId, featuresByLayer, stylesMap, prefix, getSymbolImageId) => {
197
+ const { ids, fillIds, idProperty, sourceId, hasFillGeometry } = featuresByLayer[layerId]
174
198
  const baseLayer = map.getLayer(layerId)
199
+ // Only the recorded layerId is registered in stylesMap — falls back to its cold/hot sibling
200
+ // (same drawn layer, identical styling) rather than requiring both to be registered.
201
+ const style = stylesMap[layerId] ?? stylesMap[siblingDrawLayerId(layerId)]
175
202
 
176
- if (!baseLayer || !stylesMap[layerId]) {
203
+ if (!baseLayer || !style) {
177
204
  return
178
205
  }
179
-
180
- const style = stylesMap[layerId]
181
206
  const srcLayer = baseLayer.sourceLayer
182
207
  const geom = hasFillGeometry ? 'fill' : baseLayer.type
183
- const base = `${prefix}-${sourceId}`
208
+ const base = `${prefix}-${layerId}`
184
209
  const { fill } = style
185
210
  const isSelected = prefix === SELECTED_PREFIX
186
211
  const selectedStyle = usesSelectedStyle(prefix)
@@ -202,7 +227,7 @@ const applySourceHighlight = (map, sourceId, featuresBySource, stylesMap, prefix
202
227
  applyLineHighlight(map, base, sourceId, srcLayer, lineColor, lineWidth, filter)
203
228
  break
204
229
  case 'symbol':
205
- applySymbolHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
230
+ applySymbolGeomHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
206
231
  break
207
232
  default:
208
233
  break
@@ -210,16 +235,16 @@ const applySourceHighlight = (map, sourceId, featuresBySource, stylesMap, prefix
210
235
  }
211
236
 
212
237
  const applyFeatureHighlights = (map, features, stylesMap, prefix, getSymbolImageId) => {
213
- const featuresBySource = groupFeaturesBySource(map, features)
214
- const currentSources = new Set(Object.keys(featuresBySource))
238
+ const featuresByLayer = groupFeaturesByLayer(map, features)
239
+ const currentLayerIds = new Set(Object.keys(featuresByLayer))
215
240
  const storageKey = `_${prefix.replaceAll('-', '')}Sources`
216
- const previousSources = map[storageKey] || new Set()
241
+ const previousLayerIds = map[storageKey] || new Set()
217
242
 
218
- cleanupStaleSources(map, previousSources, currentSources, prefix)
219
- map[storageKey] = currentSources
220
- currentSources.forEach(sourceId => applySourceHighlight(map, sourceId, featuresBySource, stylesMap, prefix, getSymbolImageId))
243
+ cleanupStaleLayers(map, previousLayerIds, currentLayerIds, prefix)
244
+ map[storageKey] = currentLayerIds
245
+ currentLayerIds.forEach(layerId => applyLayerHighlight(map, layerId, featuresByLayer, stylesMap, prefix, getSymbolImageId))
221
246
 
222
- return featuresBySource
247
+ return featuresByLayer
223
248
  }
224
249
 
225
250
  /**
@@ -247,21 +272,21 @@ export function updateHighlightedFeatures ({ LngLatBounds, map, selectedFeatures
247
272
  // Black selected stroke on top of yellow active (mirrors resolveActive for symbols)
248
273
  applyFeatureHighlights(map, activeFeatures, stylesMap, ACTIVE_INNER_PREFIX, getSelectedImageId)
249
274
  } else {
250
- clearPrefixSources(map, ACTIVE_PREFIX)
251
- clearPrefixSources(map, ACTIVE_INNER_PREFIX)
275
+ clearPrefixLayers(map, ACTIVE_PREFIX)
276
+ clearPrefixLayers(map, ACTIVE_INNER_PREFIX)
252
277
  }
253
278
 
254
279
  // Selection features
255
- let featuresBySource = {}
280
+ let featuresByLayer = {}
256
281
  if (selectedFeatures?.length) {
257
- featuresBySource = applyFeatureHighlights(map, selectedFeatures, stylesMap, SELECTED_PREFIX, getSelectedImageId)
282
+ featuresByLayer = applyFeatureHighlights(map, selectedFeatures, stylesMap, SELECTED_PREFIX, getSelectedImageId)
258
283
  } else {
259
- clearPrefixSources(map, SELECTED_PREFIX)
284
+ clearPrefixLayers(map, SELECTED_PREFIX)
260
285
  }
261
286
 
262
287
  // Bounds only from selected features
263
288
  const renderedFeatures = []
264
- Object.entries(featuresBySource).forEach(([, { ids, idProperty, layerId }]) => {
289
+ Object.entries(featuresByLayer).forEach(([layerId, { ids, idProperty }]) => {
265
290
  renderedFeatures.push(
266
291
  ...map
267
292
  .queryRenderedFeatures({ layers: [layerId] })
@@ -54,7 +54,7 @@ describe('Highlighting Utils — active (cursor) fill and line', () => {
54
54
  if (id.includes('stale')) { return {} }
55
55
  if (id === 'l1') { return { source: 's1', type: 'fill' } }
56
56
  if (id === 'l2') { return { source: 's2', type: 'line' } }
57
- if (id === 'active-highlight-s2-fill') { return {} }
57
+ if (id === 'active-highlight-l2-fill') { return {} }
58
58
  return null
59
59
  })
60
60
 
@@ -62,19 +62,19 @@ describe('Highlighting Utils — active (cursor) fill and line', () => {
62
62
 
63
63
  expect(map.setFilter).toHaveBeenCalledWith('active-highlight-stale-fill', EMPTY_FILTER)
64
64
  expect(map.setFilter).toHaveBeenCalledWith(STALE_SYMBOL_LAYER, EMPTY_FILTER)
65
- expect(map.setFilter).toHaveBeenCalledWith('active-highlight-s2-fill', EMPTY_FILTER)
66
- expect(map.setFilter).toHaveBeenCalledWith('active-highlight-s2-line', expect.arrayContaining([['get', 'customId']]))
65
+ expect(map.setFilter).toHaveBeenCalledWith('active-highlight-l2-fill', EMPTY_FILTER)
66
+ expect(map.setFilter).toHaveBeenCalledWith('active-highlight-l2-line', expect.arrayContaining([['get', 'customId']]))
67
67
  })
68
68
 
69
69
  test('null _activehighlightSources falls back to empty set; line geom skips absent fill layer', () => {
70
70
  map._activehighlightSources = null
71
71
  map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
72
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())
73
+ expect(map.setFilter).not.toHaveBeenCalledWith('active-highlight-l1-fill', expect.anything())
74
74
  })
75
75
 
76
- test('persistent source skips cleanup; missing stale layers skip setFilter', () => {
77
- map._activehighlightSources = new Set(['stale', 's1'])
76
+ test('persistent layer skips cleanup; missing stale layers skip setFilter', () => {
77
+ map._activehighlightSources = new Set(['stale', 'l1'])
78
78
  map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
79
79
  updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap: { l1: { stroke: 'red' } } })
80
80
  expect(map.setFilter).not.toHaveBeenCalledWith(expect.stringContaining('stale'), expect.anything())
@@ -85,10 +85,10 @@ describe('Highlighting Utils — active (cursor) fill and line', () => {
85
85
  const stylesMap = { l1: { stroke: 'yellow', selectionStroke: 'black', strokeWidth: 3, activeStrokeWidth: 9 } }
86
86
  updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap })
87
87
 
88
- const activeColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-s1-line' && c[1] === LINE_COLOR)
88
+ const activeColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-l1-line' && c[1] === LINE_COLOR)
89
89
  expect(activeColorCall?.[2]).toBe('yellow')
90
90
 
91
- const overlayColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-inner-s1-line' && c[1] === LINE_COLOR)
91
+ const overlayColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-inner-l1-line' && c[1] === LINE_COLOR)
92
92
  expect(overlayColorCall?.[2]).toBe('black')
93
93
  })
94
94
 
@@ -123,8 +123,8 @@ describe('Highlighting Utils — selected fill and line', () => {
123
123
  })
124
124
  updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures: SELECTED_FEATURES, stylesMap: STYLES })
125
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)
126
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ id: 'selected-highlight-l1-line' }))
127
+ const linePaintCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'selected-highlight-l1-line' && c[1] === LINE_COLOR)
128
128
  expect(linePaintCall).toBeTruthy()
129
129
  expect(linePaintCall[2]).toBe('#ffdd00')
130
130
  })
@@ -169,11 +169,11 @@ describe('Highlighting Utils — layer management', () => {
169
169
  map.getLayer.mockImplementation(id => { // NOSONAR
170
170
  if (id === 'l1') { return { source: 's1', type: 'line' } }
171
171
  if (id === 'l2') { return { source: 's2', type: 'line', sourceLayer: 'tiles' } }
172
- if (id === 'active-highlight-s1-line') { return {} }
172
+ if (id === 'active-highlight-l1-line') { return {} }
173
173
  return null
174
174
  })
175
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
176
+ // active-highlight-l1-line reused; active-highlight-l2-line, active-highlight-inner-l1-line, active-highlight-inner-l2-line are new
177
177
  expect(map.addLayer).toHaveBeenCalledTimes(3)
178
178
  expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ 'source-layer': 'tiles' }))
179
179
  })
@@ -187,7 +187,7 @@ describe('Highlighting Utils — layer management', () => {
187
187
 
188
188
  describe('Highlighting Utils — symbol layers (active cursor)', () => {
189
189
  const ACTIVE_IMAGE = 'symbol-act-abc123'
190
- const HIGHLIGHT_LAYER = 'active-highlight-s1-symbol'
190
+ const HIGHLIGHT_LAYER = 'active-highlight-l1-symbol'
191
191
  const ICON_IMAGE = 'icon-image'
192
192
  const ICON_ANCHOR = 'icon-anchor'
193
193
  const POINT_FEATURE = { featureId: 1, layerId: 'l1', geometry: { type: 'Point' } }
@@ -258,6 +258,41 @@ describe('Highlighting Utils — symbol layers (active cursor)', () => {
258
258
  expect(map.addLayer).not.toHaveBeenCalled()
259
259
  })
260
260
 
261
+ test('copies icon-offset from the original layer when present (draw points need it, datasets do not)', () => {
262
+ map.getLayoutProperty.mockImplementation((_id, prop) => { // NOSONAR
263
+ if (prop === ICON_IMAGE) { return SYMBOL_IMAGE }
264
+ if (prop === 'icon-offset') { return [0, 4.4] }
265
+ return null
266
+ })
267
+ run()
268
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
269
+ layout: expect.objectContaining({ 'icon-offset': [0, 4.4] })
270
+ }))
271
+ expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, 'icon-offset', [0, 4.4])
272
+ })
273
+
274
+ test('omits icon-offset entirely when the original layer has none', () => {
275
+ map.getLayoutProperty.mockImplementation((_id, prop) => (prop === ICON_IMAGE ? SYMBOL_IMAGE : undefined)) // NOSONAR
276
+ run()
277
+ const call = map.addLayer.mock.calls[0][0]
278
+ expect(call.layout).not.toHaveProperty('icon-offset')
279
+ expect(map.setLayoutProperty).not.toHaveBeenCalledWith(HIGHLIGHT_LAYER, 'icon-offset', expect.anything())
280
+ })
281
+
282
+ // Draw's point-symbol layer has a per-feature data-driven icon-image (each point can carry
283
+ // a different symbol config) — the highlight layer must reference the SAME per-feature
284
+ // expression against the precomputed symbolActiveImageId/symbolSelectedImageId property
285
+ // (pointSymbolImages.js) rather than reverse-mapping a single shared id.
286
+ test('references the per-feature symbolActiveImageId property when icon-image is a data-driven expression', () => {
287
+ map.getLayoutProperty.mockImplementation((_id, prop) => // NOSONAR
288
+ prop === ICON_IMAGE ? ['get', 'user_symbolImageId'] : null)
289
+ run()
290
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
291
+ layout: expect.objectContaining({ [ICON_IMAGE]: ['get', 'user_symbolActiveImageId'] })
292
+ }))
293
+ expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, ICON_IMAGE, ['get', 'user_symbolActiveImageId'])
294
+ })
295
+
261
296
  test('cleans up stale symbol highlight layer', () => {
262
297
  map._activehighlightSources = new Set(['stale'])
263
298
  map.getLayer.mockImplementation(id => id === STALE_SYMBOL_LAYER ? { type: 'symbol' } : null) // NOSONAR
@@ -268,7 +303,7 @@ describe('Highlighting Utils — symbol layers (active cursor)', () => {
268
303
 
269
304
  describe('Highlighting Utils — symbol layers (committed selection)', () => {
270
305
  const SELECTED_IMAGE = 'symbol-sel-abc123'
271
- const SEL_HIGHLIGHT_LAYER = 'selected-highlight-s1-symbol'
306
+ const SEL_HIGHLIGHT_LAYER = 'selected-highlight-l1-symbol'
272
307
  const ICON_IMAGE = 'icon-image'
273
308
  const POINT_FEATURE = { featureId: 1, layerId: 'l1', geometry: { type: 'Point' } }
274
309
 
@@ -298,6 +333,15 @@ describe('Highlighting Utils — symbol layers (committed selection)', () => {
298
333
  run()
299
334
  expect(map.addLayer).not.toHaveBeenCalled()
300
335
  })
336
+
337
+ test('references the per-feature symbolSelectedImageId property when icon-image is a data-driven expression', () => {
338
+ map.getLayoutProperty.mockImplementation((_id, prop) => // NOSONAR
339
+ prop === ICON_IMAGE ? ['get', 'user_symbolImageId'] : null)
340
+ run()
341
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
342
+ layout: expect.objectContaining({ [ICON_IMAGE]: ['get', 'user_symbolSelectedImageId'] })
343
+ }))
344
+ })
301
345
  })
302
346
 
303
347
  // ─── fill-extrusion layers (3D buildings) ────────────────────────────────────
@@ -305,7 +349,7 @@ describe('Highlighting Utils — symbol layers (committed selection)', () => {
305
349
  describe('Highlighting Utils — fill-extrusion layers', () => {
306
350
  const LAYER_ID = 'buildings 3D'
307
351
  const STYLES = { [LAYER_ID]: { stroke: '#aaa', selectionStroke: '#0073cc', strokeWidth: 3, activeStrokeWidth: 6 } }
308
- const LINE_LAYER_ID = 'selected-highlight-composite-line'
352
+ const LINE_LAYER_ID = 'selected-highlight-buildings 3D-line'
309
353
 
310
354
  let map
311
355
 
@@ -409,12 +453,12 @@ describe('Highlighting Utils — missing style entry', () => {
409
453
  const map = makeMap()
410
454
  let callCount = 0
411
455
  map.getLayer.mockImplementation((id) => {
412
- // First call from groupFeaturesBySource - return a valid layer
456
+ // First call from groupFeaturesByLayer - return a valid layer
413
457
  if (id === 'l1' && callCount === 0) {
414
458
  callCount++
415
459
  return { source: 's1', type: 'line' }
416
460
  }
417
- // Subsequent calls from applySourceHighlight - return null to trigger early return
461
+ // Subsequent calls from applyLayerHighlight - return null to trigger early return
418
462
  return null
419
463
  })
420
464
 
@@ -448,3 +492,98 @@ describe('Highlighting Utils — missing style entry', () => {
448
492
  expect(map.addLayer).not.toHaveBeenCalled()
449
493
  })
450
494
  })
495
+
496
+ // mapbox-gl-draw duplicates every one of its own layers into a `.cold`/`.hot` sibling pair,
497
+ // each backed by its own source — a feature moves out of cold and into hot the instant any of
498
+ // its own properties change, only cooling back down on a later render with nothing changed.
499
+ // A selection payload's layerId (e.g. 'point-symbol.cold') is captured once, at selection time
500
+ // — if the feature later moves to the sibling bucket, the recorded layer still exists (so a
501
+ // naive "does the layer exist" check looks fine) but its source no longer holds the feature.
502
+ describe('Highlighting Utils — mapbox-gl-draw cold/hot sibling layers', () => {
503
+ test('highlights a selected feature that has moved from its recorded .cold layer into the sibling .hot bucket', () => {
504
+ const map = makeMap()
505
+ map.getLayer.mockImplementation(id => { // NOSONAR
506
+ if (id === 'point-symbol.cold') { return { source: 'mapbox-gl-draw-cold', type: 'symbol' } }
507
+ if (id === 'point-symbol.hot') { return { source: 'mapbox-gl-draw-hot', type: 'symbol' } }
508
+ return null
509
+ })
510
+ map.getLayoutProperty.mockImplementation((id, prop) => // NOSONAR
511
+ prop === 'icon-image' ? SYMBOL_IMAGE : undefined)
512
+ map._selectedSymbolImageMap = { [SYMBOL_IMAGE]: 'symbol-sel-xyz' }
513
+
514
+ // The feature's own current geometry lives on the hot source (it moved there when its
515
+ // properties were last refreshed) — only stylesMap['point-symbol.cold'] is registered,
516
+ // matching how the demo only registers the .cold layerId for selectability.
517
+ updateHighlightedFeatures({
518
+ LngLatBounds,
519
+ map,
520
+ selectedFeatures: [{ featureId: 'p1', layerId: 'point-symbol.cold', geometry: { type: 'Point' } }],
521
+ stylesMap: { 'point-symbol.cold': { stroke: 'red', selectionStroke: 'black', strokeWidth: 2 } }
522
+ })
523
+
524
+ // A highlight layer gets created/filtered for BOTH the recorded source and its sibling —
525
+ // whichever one the feature actually lives in right now, this finds it.
526
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ source: 'mapbox-gl-draw-cold' }))
527
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ source: 'mapbox-gl-draw-hot' }))
528
+ })
529
+
530
+ test('does not touch a non-draw layer id that happens to not end in .cold/.hot', () => {
531
+ const map = makeMap()
532
+ map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
533
+
534
+ updateHighlightedFeatures({
535
+ LngLatBounds,
536
+ map,
537
+ selectedFeatures: [{ featureId: 1, layerId: 'l1' }],
538
+ stylesMap: { l1: { stroke: 'red', selectionStroke: 'black', strokeWidth: 2 } }
539
+ })
540
+
541
+ expect(map.getLayer).not.toHaveBeenCalledWith('l1.hot')
542
+ expect(map.getLayer).not.toHaveBeenCalledWith('l1.cold')
543
+ })
544
+ })
545
+
546
+ // mapbox-gl-draw stores every geometry type (point, polygon, line) in the same cold/hot
547
+ // sources — a point and a polygon selected together share a source, even though they're
548
+ // rendered by different layers (point-symbol vs fill-inactive). Grouping by source alone would
549
+ // collapse both into one group that can only resolve one geometry-type path, silently dropping
550
+ // whichever feature's highlight didn't win.
551
+ describe('Highlighting Utils — multi-select across geometry types sharing a source', () => {
552
+ test('a selected point and a selected polygon sharing the same mapbox-gl-draw source both get their own highlight', () => {
553
+ const map = makeMap()
554
+ map.getLayer.mockImplementation(id => { // NOSONAR
555
+ if (id === 'point-symbol.cold') { return { source: 'mapbox-gl-draw-cold', type: 'symbol' } }
556
+ if (id === 'fill-inactive.cold') { return { source: 'mapbox-gl-draw-cold', type: 'fill' } }
557
+ return null
558
+ })
559
+ map.getLayoutProperty.mockImplementation((id, prop) => // NOSONAR
560
+ prop === 'icon-image' ? SYMBOL_IMAGE : undefined)
561
+ map._selectedSymbolImageMap = { [SYMBOL_IMAGE]: 'symbol-sel-xyz' }
562
+
563
+ updateHighlightedFeatures({
564
+ LngLatBounds,
565
+ map,
566
+ selectedFeatures: [
567
+ { featureId: 'pt1', layerId: 'point-symbol.cold', geometry: { type: 'Point' } },
568
+ { featureId: 'poly1', layerId: 'fill-inactive.cold', geometry: { type: 'Polygon' } }
569
+ ],
570
+ stylesMap: {
571
+ 'point-symbol.cold': { stroke: 'red', selectionStroke: 'black', strokeWidth: 2 },
572
+ 'fill-inactive.cold': { stroke: 'red', selectionStroke: 'black', fill: 'blue', strokeWidth: 2 }
573
+ }
574
+ })
575
+
576
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
577
+ id: 'selected-highlight-point-symbol.cold-symbol',
578
+ type: 'symbol'
579
+ }))
580
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
581
+ id: 'selected-highlight-fill-inactive.cold-fill',
582
+ type: 'fill'
583
+ }))
584
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
585
+ id: 'selected-highlight-fill-inactive.cold-line',
586
+ type: 'line'
587
+ }))
588
+ })
589
+ })
@@ -1,16 +1,19 @@
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
- */
1
+ import { siblingDrawLayerId } from './drawLayerBuckets.js'
2
+
3
+ // layerIds is a config-derived list, built once and never refreshed as features move between
4
+ // mapbox-gl-draw's cold/hot buckets (see drawLayerBuckets.js) — adding each sibling keeps a
5
+ // query finding a feature that has since moved to the other bucket.
6
+ const expandDrawLayerSiblings = (layerIds) => {
7
+ const expanded = new Set(layerIds)
8
+ layerIds.forEach(id => {
9
+ const sibling = siblingDrawLayerId(id)
10
+ if (sibling) {
11
+ expanded.add(sibling)
12
+ }
13
+ })
14
+ return [...expanded]
15
+ }
16
+
14
17
  const splitLayers = (map, layerIds) => {
15
18
  const lineLayers = []
16
19
  const otherLayers = []
@@ -29,6 +32,19 @@ const splitLayers = (map, layerIds) => {
29
32
  return { lineLayers, otherLayers }
30
33
  }
31
34
 
35
+ /**
36
+ * Attaches a mousemove listener that changes the map cursor to a pointer when
37
+ * hovering over any of the specified layers.
38
+ *
39
+ * Line layers use a 10px tolerance bbox. Stroke layers that have a companion
40
+ * fill layer are skipped — the fill handles hover. Fill and symbol layers use
41
+ * exact point hit-testing.
42
+ *
43
+ * @param {Object} map - MapLibre map instance
44
+ * @param {string[]} layerIds - Layer IDs to watch
45
+ * @param {Function|null} prevHandler - Previous mousemove handler to remove
46
+ * @returns {Function|null} The new handler, or null if layerIds is empty
47
+ */
32
48
  export const setupHoverCursor = (map, layerIds, prevHandler) => {
33
49
  const canvas = map.getCanvas()
34
50
 
@@ -42,7 +58,7 @@ export const setupHoverCursor = (map, layerIds, prevHandler) => {
42
58
  }
43
59
 
44
60
  const handler = (e) => {
45
- const existingLayers = layerIds.filter(id => map.getLayer(id))
61
+ const existingLayers = expandDrawLayerSiblings(layerIds).filter(id => map.getLayer(id))
46
62
  if (existingLayers.length === 0) {
47
63
  canvas.style.cursor = ''
48
64
  return
@@ -127,4 +127,39 @@ describe('setupHoverCursor', () => {
127
127
  { layers: ['hedge-stroke'] }
128
128
  )
129
129
  })
130
+
131
+ /* ------------------------------------------------------------------ */
132
+ /* mapbox-gl-draw cold/hot sibling layers */
133
+ /* ------------------------------------------------------------------ */
134
+
135
+ // mapbox-gl-draw moves a feature out of its `.cold` layer/source into a `.hot` sibling the
136
+ // instant any of its own properties change — only registering the config's recorded
137
+ // '.cold' id would otherwise mean the cursor stops recognising a feature the moment it's
138
+ // moved, only recovering once something else happens to settle it back.
139
+ it('queries a .cold layerId\'s .hot sibling too, and detects a hit that only the sibling has', () => {
140
+ const canvas = { style: { cursor: '' } }
141
+ const map = {
142
+ ...makeMap({ 'point-symbol.cold': 'symbol', 'point-symbol.hot': 'symbol' }, []),
143
+ getCanvas: () => canvas,
144
+ queryRenderedFeatures: jest.fn((_point, { layers }) =>
145
+ layers.includes('point-symbol.hot') ? [{ id: 'p1' }] : [])
146
+ }
147
+ const handler = setupHoverCursor(map, ['point-symbol.cold'], null)
148
+ move(handler)
149
+ expect(map.queryRenderedFeatures).toHaveBeenCalledWith(
150
+ expect.objectContaining({ x: 10, y: 10 }),
151
+ { layers: expect.arrayContaining(['point-symbol.cold', 'point-symbol.hot']) }
152
+ )
153
+ expect(canvas.style.cursor).toBe('pointer')
154
+ })
155
+
156
+ it('does not add a sibling for a layer id with no cold/hot suffix', () => {
157
+ const map = makeMap({ 'field-parcels': 'fill' }, [])
158
+ const handler = setupHoverCursor(map, ['field-parcels'], null)
159
+ move(handler)
160
+ expect(map.queryRenderedFeatures).toHaveBeenCalledWith(
161
+ expect.objectContaining({ x: 10, y: 10 }),
162
+ { layers: ['field-parcels'] }
163
+ )
164
+ })
130
165
  })
@@ -1,6 +1,6 @@
1
1
  import { injectColors, getEffectivePixelRatio } from '../../../../src/utils/patternUtils.js'
2
2
  import { getValueForStyle } from '../../../../src/utils/getValueForStyle.js'
3
- import { rasteriseToImageData } from './rasteriseToImageData.js'
3
+ import { rasteriseToImageData } from '../../../../src/utils/rasteriseToImageData.js'
4
4
 
5
5
  // Module-level cache: imageId → ImageData. Avoids re-rasterising identical patterns.
6
6
  const imageDataCache = new Map()