@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,34 @@
1
+ /**
2
+ * Coordinate mutation for edit_point. A Point has one coordinate, always selected — no
3
+ * ring/segment bookkeeping the way edit/undoOps.js and edit/vertexOps.js need for a
4
+ * Polygon/LineString. movePoint's (olFeature, _index, coord) signature matches
5
+ * edit/vertexOps.js's moveVertex exactly (the unused index is only there so nudge.js/
6
+ * touchHandler.js can call either interchangeably via the injected moveCoord parameter).
7
+ */
8
+
9
+ /** Signature-compatible with edit/vertexOps.js's moveVertex — _index is unused, a Point has none. */
10
+ export const movePoint = (olFeature, _index, coord) => {
11
+ olFeature.getGeometry().setCoordinates([...coord])
12
+ }
13
+
14
+ // Op type stays 'move_vertex' (not 'move_point') — deriveModifyOp (edit/modifyInteraction.js,
15
+ // reused unchanged) always produces that type, and keeping it lets applyPointUndo below mirror
16
+ // edit/undoOps.js's applyUndo dispatch pattern with the shared leaf handlers' onVertexMoved
17
+ // callbacks (which push {type: 'move_vertex', ...}) working completely unmodified.
18
+ export const undoMovePoint = (olFeature, op) => {
19
+ const { previousCoord } = op
20
+ olFeature.getGeometry().setCoordinates([...previousCoord])
21
+ return 0
22
+ }
23
+
24
+ /**
25
+ * Dispatch the correct undo handler based on operation type — mirrors edit/undoOps.js's
26
+ * applyUndo, but with the single op type a Point ever produces.
27
+ * @returns {number} vertex index to re-select after undo (always 0), or -1 for none
28
+ */
29
+ export const applyPointUndo = (olFeature, op) => {
30
+ if (op.type === 'move_vertex') {
31
+ return undoMovePoint(olFeature, op)
32
+ }
33
+ return -1
34
+ }
@@ -0,0 +1,32 @@
1
+ import { movePoint, undoMovePoint, applyPointUndo } from './pointOps.js'
2
+ import { pointFeature } from '../__helpers__/harness.js'
3
+
4
+ describe('movePoint', () => {
5
+ test('writes the coordinate directly, ignoring the unused index', () => {
6
+ const feature = pointFeature([0, 0])
7
+ movePoint(feature, 0, [5, 5])
8
+ expect(feature.getGeometry().getCoordinates()).toEqual([5, 5])
9
+ })
10
+ })
11
+
12
+ describe('undoMovePoint', () => {
13
+ test('restores the previous coordinate and returns vertex index 0', () => {
14
+ const feature = pointFeature([5, 5])
15
+ expect(undoMovePoint(feature, { previousCoord: [1, 2] })).toBe(0)
16
+ expect(feature.getGeometry().getCoordinates()).toEqual([1, 2])
17
+ })
18
+ })
19
+
20
+ describe('applyPointUndo', () => {
21
+ test('dispatches move_vertex to undoMovePoint', () => {
22
+ const feature = pointFeature([5, 5])
23
+ expect(applyPointUndo(feature, { type: 'move_vertex', previousCoord: [3, 4] })).toBe(0)
24
+ expect(feature.getGeometry().getCoordinates()).toEqual([3, 4])
25
+ })
26
+
27
+ test('returns -1 for an unknown operation type', () => {
28
+ const feature = pointFeature([5, 5])
29
+ expect(applyPointUndo(feature, { type: 'unknown' })).toBe(-1)
30
+ expect(feature.getGeometry().getCoordinates()).toEqual([5, 5])
31
+ })
32
+ })
@@ -0,0 +1,82 @@
1
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
2
+
3
+ // Deferred commit-level geometrychange emitter (feature + change phase + vertex index)
4
+ // consumed by the validation layer — copied from edit/selectionState.js's own (identical
5
+ // shape, no ring dependency to strip).
6
+ const createGeometryValidationEmitter = (manager, store, olFeature) => (phase, vertexIndex) => {
7
+ if (!phase) { return }
8
+ setTimeout(() => {
9
+ manager.emit(ADAPTER_EVENTS.GEOMETRY_CHANGE, { feature: store.toGeoJSON(olFeature), phase, vertexIndex })
10
+ }, 0)
11
+ }
12
+
13
+ /**
14
+ * Mutable edit_point state, matching edit/selectionState.js's public surface
15
+ * ({state, getState, setState, syncGeom, emitGeometryValidation, setHooks, destroy}) closely
16
+ * enough that edit/modifyInteraction.js, edit/touchHandler.js and edit/keyboardHandler.js all
17
+ * run over it unmodified (beyond the moveCoord injection) — but a point has one coordinate,
18
+ * always selected, and no ring/midpoint list to navigate, so this is a standalone
19
+ * implementation rather than a wrapper around the ring-shaped one. No handle layer to keep in
20
+ * step either — the point's own symbol icon is the only visual, styled by the draw layer.
21
+ *
22
+ * @param {{ manager, store, olFeature, interfaceType }} options
23
+ * @returns {{ state, getState, setState, syncGeom, emitGeometryValidation, setHooks, destroy }}
24
+ */
25
+ export const createPointSelectionState = ({ manager, store, olFeature, interfaceType }) => {
26
+ const state = {
27
+ olFeature,
28
+ selectedVertexIndex: 0,
29
+ selectedVertexType: 'vertex',
30
+ vertices: [],
31
+ midpoints: [],
32
+ interfaceType: interfaceType ?? 'mouse'
33
+ }
34
+
35
+ const hooks = { onUpdate: null }
36
+ const setHooks = ({ onUpdate }) => { hooks.onUpdate = onUpdate }
37
+
38
+ const refreshCoord = () => {
39
+ state.vertices = [olFeature.getGeometry().getCoordinates()]
40
+ }
41
+
42
+ // The point is always selected — a selectedVertexIndex/Type write (e.g. Escape or
43
+ // Alt+Arrow in the reused edit/keyboardHandler.js, which try to deselect/navigate) is
44
+ // silently dropped rather than stranding the point with no selection. Every other update
45
+ // (interfaceType, vertices) still applies normally.
46
+ const setState = (updates) => {
47
+ const { selectedVertexIndex, selectedVertexType, ...rest } = updates
48
+ Object.assign(state, rest)
49
+ if (updates.vertices !== undefined) {
50
+ hooks.onUpdate?.()
51
+ }
52
+ }
53
+
54
+ // Never emits VERTEX_CHANGE — that event unconditionally releases
55
+ // mapProvider.activeMoveTarget (events.js's onVertexChange). The D-pad claim is made once
56
+ // below (VERTEX_SELECTION) and held for the whole edit session.
57
+ const syncGeom = () => {
58
+ refreshCoord()
59
+ manager.emit(ADAPTER_EVENTS.UPDATE, store.toGeoJSON(olFeature))
60
+ }
61
+
62
+ const emitGeometryValidation = createGeometryValidationEmitter(manager, store, olFeature)
63
+
64
+ // Keeps the cached coordinate in sync on every geometry change (e.g. during a Modify drag)
65
+ const onGeometryChange = () => refreshCoord()
66
+ olFeature.getGeometry().on('change', onGeometryChange)
67
+
68
+ refreshCoord()
69
+ manager.emit(ADAPTER_EVENTS.VERTEX_SELECTION, { index: 0, numVertices: 1 })
70
+
71
+ return {
72
+ state,
73
+ getState: () => state,
74
+ setState,
75
+ syncGeom,
76
+ emitGeometryValidation,
77
+ setHooks,
78
+ destroy () {
79
+ olFeature.getGeometry().un('change', onGeometryChange)
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,107 @@
1
+ import { createPointSelectionState } from './pointSelectionState.js'
2
+ import { createFakeManager, pointFeature } from '../__helpers__/harness.js'
3
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
4
+
5
+ const setup = (interfaceType) => {
6
+ const manager = createFakeManager()
7
+ const olFeature = pointFeature([5, 5])
8
+ const store = { toGeoJSON: jest.fn(() => ({ id: 'f1' })) }
9
+ const selection = createPointSelectionState({ manager, store, olFeature, interfaceType })
10
+ return { manager, olFeature, store, selection }
11
+ }
12
+
13
+ test('getState returns the live state object', () => {
14
+ const { selection } = setup()
15
+ expect(selection.getState()).toBe(selection.state)
16
+ })
17
+
18
+ test('defaults to mouse interface when none is given, and is always selected at index 0', () => {
19
+ const { selection } = setup()
20
+ expect(selection.state.interfaceType).toBe('mouse')
21
+ expect(selection.state.selectedVertexIndex).toBe(0)
22
+ expect(selection.state.selectedVertexType).toBe('vertex')
23
+ expect(setup('touch').selection.state.interfaceType).toBe('touch')
24
+ })
25
+
26
+ test('claims the D-pad exactly once at construction, never VERTEX_CHANGE', () => {
27
+ const { manager } = setup()
28
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, { index: 0, numVertices: 1 })
29
+ expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_CHANGE, expect.anything())
30
+ })
31
+
32
+ test('populates the initial coordinate at construction', () => {
33
+ const { selection } = setup()
34
+ expect(selection.state.vertices).toEqual([[5, 5]])
35
+ })
36
+
37
+ test('a selectedVertexIndex/Type write is silently dropped — the point stays always-selected', () => {
38
+ const { selection, manager } = setup()
39
+ manager.emit.mockClear()
40
+ selection.setState({ selectedVertexIndex: -1, selectedVertexType: null })
41
+ expect(selection.state.selectedVertexIndex).toBe(0)
42
+ expect(selection.state.selectedVertexType).toBe('vertex')
43
+ expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.anything())
44
+ })
45
+
46
+ test('other setState updates apply normally, without firing onUpdate unless vertices changes', () => {
47
+ const { selection } = setup()
48
+ const onUpdate = jest.fn()
49
+ selection.setHooks({ onUpdate })
50
+ selection.setState({ interfaceType: 'keyboard' })
51
+ expect(selection.state.interfaceType).toBe('keyboard')
52
+ expect(onUpdate).not.toHaveBeenCalled()
53
+ })
54
+
55
+ test('a vertices update fires the onUpdate hook', () => {
56
+ const { selection } = setup()
57
+ const onUpdate = jest.fn()
58
+ selection.setHooks({ onUpdate })
59
+ selection.setState({ vertices: [[9, 9]] })
60
+ expect(onUpdate).toHaveBeenCalled()
61
+ })
62
+
63
+ test('syncGeom derives the coordinate from the geometry and emits UPDATE only (never VERTEX_CHANGE)', () => {
64
+ const { selection, manager, store, olFeature } = setup()
65
+ manager.emit.mockClear()
66
+ olFeature.getGeometry().setCoordinates([7, 8])
67
+ selection.syncGeom()
68
+ expect(selection.state.vertices).toEqual([[7, 8]])
69
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.UPDATE, store.toGeoJSON())
70
+ expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_CHANGE, expect.anything())
71
+ })
72
+
73
+ test('emitGeometryValidation emits a deferred commit-level geometrychange with the change phase', () => {
74
+ jest.useFakeTimers()
75
+ const { selection, manager, store } = setup()
76
+ manager.emit.mockClear()
77
+
78
+ selection.emitGeometryValidation('commit-move', 0)
79
+ expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, expect.anything())
80
+
81
+ jest.runAllTimers()
82
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, {
83
+ feature: store.toGeoJSON(),
84
+ phase: 'commit-move',
85
+ vertexIndex: 0
86
+ })
87
+ jest.useRealTimers()
88
+ })
89
+
90
+ test('emitGeometryValidation is a no-op without a change phase', () => {
91
+ jest.useFakeTimers()
92
+ const { selection, manager } = setup()
93
+ manager.emit.mockClear()
94
+ selection.emitGeometryValidation(undefined, 0)
95
+ jest.runAllTimers()
96
+ expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, expect.anything())
97
+ jest.useRealTimers()
98
+ })
99
+
100
+ test('geometry changes refresh the cached coordinate until destroy unbinds the listener', () => {
101
+ const { selection, olFeature } = setup()
102
+ olFeature.getGeometry().setCoordinates([20, 20])
103
+ expect(selection.state.vertices).toEqual([[20, 20]])
104
+ selection.destroy()
105
+ olFeature.getGeometry().setCoordinates([30, 30])
106
+ expect(selection.state.vertices).toEqual([[20, 20]])
107
+ })
@@ -0,0 +1,83 @@
1
+ import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js'
2
+ import { getOrCreateSymbolImage } from '../../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
3
+
4
+ /**
5
+ * Resolves a drawn point's symbol-config icon and writes the resolved image id back onto the
6
+ * OL feature, so core/styles.js's createFeatureStyle() can render it via an ol/style/Icon.
7
+ * Also resolves and caches the 'active'/'selected' variants as symbolActiveImageId/
8
+ * symbolSelectedImageId, read directly by highlightFeatures.js's selection overlay.
9
+ */
10
+
11
+ const VARIANTS = ['normal', 'active', 'selected']
12
+ const PROPERTY_FOR_VARIANT = { normal: 'symbolImageId', active: 'symbolActiveImageId', selected: 'symbolSelectedImageId' }
13
+
14
+ export const hasSymbolStyle = (properties) => !!(properties?.symbol || properties?.symbolSvgContent)
15
+
16
+ // Combines the browser's native devicePixelRatio with this app's own map-size scale factor
17
+ // (mapProvider.drawScale), matching MapController.jsx's construction-time formula.
18
+ export const getPixelRatio = (mapProvider) => (globalThis.devicePixelRatio || 1) * (mapProvider?.drawScale ?? 1)
19
+
20
+ /**
21
+ * Resolve one point feature's symbol image and write it back onto the feature.
22
+ * A no-op for points with no symbol config (nothing to render as an icon).
23
+ *
24
+ * @param {Object} params
25
+ * @param {Object} params.manager - OLDrawManager (needs store.source, mapStyle)
26
+ * @param {Object} params.mapProvider
27
+ * @param {import('ol/Feature.js').default} params.olFeature
28
+ * @returns {Promise<void>}
29
+ */
30
+ export const resolvePointSymbol = async ({ manager, mapProvider, olFeature }) => {
31
+ const properties = olFeature.getProperties()
32
+ if (!hasSymbolStyle(properties)) {
33
+ return
34
+ }
35
+
36
+ const pixelRatio = getPixelRatio(mapProvider)
37
+
38
+ try {
39
+ const results = await Promise.all(
40
+ VARIANTS.map((variant) => symbolRegistry.rasteriseSymbolImage(properties, manager.mapStyle, variant, pixelRatio))
41
+ )
42
+ // All three variants resolve from the same symbol config, so either all succeed or (an
43
+ // unresolvable symbol id) all fail together — gating on 'normal' alone is enough.
44
+ if (!results[0]) {
45
+ return
46
+ }
47
+
48
+ // The feature may have been deleted/cancelled while rasterising was in flight.
49
+ if (!manager.store.source.hasFeature(olFeature)) {
50
+ return
51
+ }
52
+ VARIANTS.forEach((variant, i) => {
53
+ const result = results[i]
54
+ if (!result) { return }
55
+ getOrCreateSymbolImage(result.imageId, result.imageData)
56
+ olFeature.set(PROPERTY_FOR_VARIANT[variant], result.imageId)
57
+ })
58
+ // ol/style/Icon draws its source canvas at native pixel size — core/styles.js reads this
59
+ // back to apply `scale: 1 / symbolPixelRatio`, correcting for the canvas being rasterised
60
+ // at pixelRatio device pixels for crispness.
61
+ olFeature.set('symbolPixelRatio', pixelRatio)
62
+ } catch (err) {
63
+ // A silent failure here means a point simply never gets/keeps an icon, with nothing in
64
+ // the UI to explain why — surface it instead of letting the rejection vanish.
65
+ console.error('[draw] failed to resolve point symbol', olFeature.getId(), err) // NOSONAR
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Re-resolve every existing drawn point's symbol — called on map style/size change, since
71
+ * the rasterised image (colours, and pixel-ratio-scaled dimensions) depends on both.
72
+ *
73
+ * @param {Object} params
74
+ * @param {Object} params.manager
75
+ * @param {Object} params.mapProvider
76
+ * @returns {Promise<void>}
77
+ */
78
+ export const refreshAllPointSymbols = ({ manager, mapProvider }) => {
79
+ const points = manager.store.source.getFeatures().filter(
80
+ (f) => f.getGeometry()?.getType() === 'Point' && hasSymbolStyle(f.getProperties())
81
+ )
82
+ return Promise.all(points.map((olFeature) => resolvePointSymbol({ manager, mapProvider, olFeature })))
83
+ }
@@ -0,0 +1,214 @@
1
+ import { hasSymbolStyle, getPixelRatio, resolvePointSymbol, refreshAllPointSymbols } from './pointSymbolImages.js'
2
+ import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js'
3
+ import { getCachedSymbolImage, clearSymbolImageCache } from '../../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
4
+
5
+ const mapStyle = { id: 'outdoor', mapColorScheme: 'light' }
6
+
7
+ const createMapProvider = ({ drawScale = 1 } = {}) => ({ drawScale })
8
+
9
+ // A minimal ol.Feature stand-in — resolvePointSymbol only ever calls
10
+ // getProperties()/set() on it, and manager.store.source.hasFeature() to check liveness.
11
+ const createOlFeature = (properties) => {
12
+ const props = { ...properties }
13
+ return {
14
+ getProperties: () => props,
15
+ set: jest.fn((key, value) => { props[key] = value })
16
+ }
17
+ }
18
+
19
+ const createManager = ({ features = [] } = {}) => ({
20
+ mapStyle,
21
+ store: {
22
+ source: {
23
+ hasFeature: jest.fn((f) => features.includes(f)),
24
+ getFeatures: jest.fn(() => features)
25
+ }
26
+ }
27
+ })
28
+
29
+ // symbolRegistry.rasteriseSymbolImage() draws an SVG through a real `new Image()`/onload
30
+ // round-trip that jsdom never resolves — every other test file touching it (e.g. the
31
+ // MapLibre adapter's pointSymbolImages.test.js) instead lets a mocked provider call
32
+ // (addSymbolsToMap) stand in for it. This adapter calls rasteriseSymbolImage directly (it
33
+ // *is* the last-mile pipeline, with no provider-level call to mock instead), so it's stubbed
34
+ // here with a fake resolved ImageData keyed to the properties/pixelRatio, matching
35
+ // getSymbolImageId's own id shape closely enough for assertions to key off it.
36
+ const stubRasterise = () => jest.spyOn(symbolRegistry, 'rasteriseSymbolImage')
37
+ .mockImplementation(async (style, ms, variant, pixelRatio) => ({
38
+ // variant folded into the id (mirroring the real act-/sel- prefixing) so normal/active/
39
+ // selected resolve to distinct ids, the way real symbol configs do.
40
+ imageId: `${variant}-${symbolRegistry.getSymbolImageId(style, ms, false, pixelRatio)}`,
41
+ imageData: { width: 10, height: 10 }
42
+ }))
43
+
44
+ beforeEach(() => {
45
+ clearSymbolImageCache()
46
+ HTMLCanvasElement.prototype.getContext = jest.fn(function () {
47
+ this._ctx ??= { putImageData: jest.fn() }
48
+ return this._ctx
49
+ })
50
+ stubRasterise()
51
+ })
52
+
53
+ afterEach(() => jest.restoreAllMocks())
54
+
55
+ describe('hasSymbolStyle', () => {
56
+ it('is true when symbol is set', () => {
57
+ expect(hasSymbolStyle({ symbol: 'pin' })).toBe(true)
58
+ })
59
+
60
+ it('is true when symbolSvgContent is set', () => {
61
+ expect(hasSymbolStyle({ symbolSvgContent: '<path/>' })).toBe(true)
62
+ })
63
+
64
+ it('is false with neither, or no properties at all', () => {
65
+ expect(hasSymbolStyle({})).toBe(false)
66
+ expect(hasSymbolStyle(undefined)).toBe(false)
67
+ })
68
+ })
69
+
70
+ describe('getPixelRatio', () => {
71
+ it('combines devicePixelRatio and mapProvider.drawScale', () => {
72
+ const restore = globalThis.devicePixelRatio
73
+ globalThis.devicePixelRatio = 2
74
+ expect(getPixelRatio(createMapProvider({ drawScale: 1.5 }))).toBe(3)
75
+ globalThis.devicePixelRatio = restore
76
+ })
77
+
78
+ it('falls back to 1 for either factor when unset', () => {
79
+ const restore = globalThis.devicePixelRatio
80
+ delete globalThis.devicePixelRatio
81
+ expect(getPixelRatio(createMapProvider({ drawScale: undefined }))).toBe(1)
82
+ expect(getPixelRatio(undefined)).toBe(1)
83
+ globalThis.devicePixelRatio = restore
84
+ })
85
+ })
86
+
87
+ describe('resolvePointSymbol', () => {
88
+ it('does nothing for a feature with no symbol config', async () => {
89
+ const olFeature = createOlFeature({})
90
+ const manager = createManager()
91
+ await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
92
+ expect(olFeature.set).not.toHaveBeenCalled()
93
+ })
94
+
95
+ it('rasterises, caches the canvas and writes symbolImageId/symbolPixelRatio back onto the feature', async () => {
96
+ const properties = { symbol: 'pin' }
97
+ const olFeature = createOlFeature(properties)
98
+ const manager = createManager({ features: [olFeature] })
99
+ const mapProvider = createMapProvider({ drawScale: 2 })
100
+
101
+ await resolvePointSymbol({ manager, mapProvider, olFeature })
102
+
103
+ const pixelRatio = getPixelRatio(mapProvider)
104
+ const baseId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, pixelRatio)
105
+ expect(olFeature.set).toHaveBeenCalledWith('symbolImageId', `normal-${baseId}`)
106
+ // symbolPixelRatio is what core/styles.js later uses to scale the (pixelRatio×-oversized)
107
+ // cached canvas back down to its intended CSS display size.
108
+ expect(olFeature.set).toHaveBeenCalledWith('symbolPixelRatio', pixelRatio)
109
+ expect(getCachedSymbolImage(`normal-${baseId}`)).toBeInstanceOf(HTMLCanvasElement)
110
+ })
111
+
112
+ it('also rasterises, caches and writes the active/selected variants — one image id each, keyed to the same symbol config', async () => {
113
+ const properties = { symbol: 'pin' }
114
+ const olFeature = createOlFeature(properties)
115
+ const manager = createManager({ features: [olFeature] })
116
+ const mapProvider = createMapProvider()
117
+
118
+ await resolvePointSymbol({ manager, mapProvider, olFeature })
119
+
120
+ const pixelRatio = getPixelRatio(mapProvider)
121
+ const baseId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, pixelRatio)
122
+ expect(olFeature.set).toHaveBeenCalledWith('symbolActiveImageId', `active-${baseId}`)
123
+ expect(olFeature.set).toHaveBeenCalledWith('symbolSelectedImageId', `selected-${baseId}`)
124
+ expect(getCachedSymbolImage(`active-${baseId}`)).toBeInstanceOf(HTMLCanvasElement)
125
+ expect(getCachedSymbolImage(`selected-${baseId}`)).toBeInstanceOf(HTMLCanvasElement)
126
+ // objectContaining, not the exact `properties` object — resolvePointSymbol's own
127
+ // property-write calls mutate that same (by-reference) object in place afterwards, and
128
+ // jest's mock.calls records the reference, not a snapshot at call time.
129
+ const symbolMatch = expect.objectContaining({ symbol: 'pin' })
130
+ expect(symbolRegistry.rasteriseSymbolImage).toHaveBeenCalledWith(symbolMatch, mapStyle, 'normal', pixelRatio)
131
+ expect(symbolRegistry.rasteriseSymbolImage).toHaveBeenCalledWith(symbolMatch, mapStyle, 'active', pixelRatio)
132
+ expect(symbolRegistry.rasteriseSymbolImage).toHaveBeenCalledWith(symbolMatch, mapStyle, 'selected', pixelRatio)
133
+ })
134
+
135
+ it('skips just the active/selected properties if only those variants resolve null (normal still succeeds)', async () => {
136
+ const properties = { symbol: 'pin' }
137
+ const olFeature = createOlFeature(properties)
138
+ const manager = createManager({ features: [olFeature] })
139
+ symbolRegistry.rasteriseSymbolImage
140
+ .mockImplementationOnce(async (style, ms, variant, pixelRatio) => // normal — real stub behaviour
141
+ ({ imageId: `${variant}-${symbolRegistry.getSymbolImageId(style, ms, false, pixelRatio)}`, imageData: { width: 10, height: 10 } }))
142
+ .mockResolvedValueOnce(null) // active
143
+ .mockResolvedValueOnce(null) // selected
144
+
145
+ await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
146
+
147
+ expect(olFeature.set).toHaveBeenCalledWith('symbolImageId', expect.any(String))
148
+ expect(olFeature.set).not.toHaveBeenCalledWith('symbolActiveImageId', expect.anything())
149
+ expect(olFeature.set).not.toHaveBeenCalledWith('symbolSelectedImageId', expect.anything())
150
+ })
151
+
152
+ it('does nothing when rasterisation resolves null (e.g. an unresolvable symbol id)', async () => {
153
+ const properties = { symbol: 'not-a-real-symbol' }
154
+ const olFeature = createOlFeature(properties)
155
+ const manager = createManager({ features: [olFeature] })
156
+ symbolRegistry.rasteriseSymbolImage.mockResolvedValueOnce(null)
157
+
158
+ await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
159
+
160
+ expect(olFeature.set).not.toHaveBeenCalled()
161
+ })
162
+
163
+ it('does not write back if the feature was removed from the source while rasterising was in flight', async () => {
164
+ const properties = { symbol: 'pin' }
165
+ const olFeature = createOlFeature(properties)
166
+ const manager = createManager({ features: [] }) // not present in the source
167
+
168
+ await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
169
+
170
+ expect(olFeature.set).not.toHaveBeenCalled()
171
+ })
172
+
173
+ it('surfaces (rather than swallows) a rasterisation failure', async () => {
174
+ const properties = { symbol: 'pin' }
175
+ const olFeature = createOlFeature(properties)
176
+ olFeature.getId = () => 'p1'
177
+ const manager = createManager({ features: [olFeature] })
178
+ const error = new Error('rasterise failed')
179
+ symbolRegistry.rasteriseSymbolImage.mockRejectedValueOnce(error)
180
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
181
+
182
+ await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
183
+
184
+ expect(olFeature.set).not.toHaveBeenCalled()
185
+ expect(consoleError).toHaveBeenCalledWith('[draw] failed to resolve point symbol', 'p1', error)
186
+ })
187
+ })
188
+
189
+ describe('refreshAllPointSymbols', () => {
190
+ const feature = (getType, properties) => {
191
+ const f = createOlFeature(properties)
192
+ f.getGeometry = () => ({ getType: () => getType })
193
+ return f
194
+ }
195
+
196
+ it('re-resolves every point feature with a symbol config, ignoring non-point and unstyled features', async () => {
197
+ const pointWithSymbol = feature('Point', { symbol: 'pin' })
198
+ const pointWithoutSymbol = feature('Point', {})
199
+ const polygon = feature('Polygon', { symbol: 'pin' })
200
+ const manager = createManager({ features: [pointWithSymbol, pointWithoutSymbol, polygon] })
201
+ manager.store.source.hasFeature = jest.fn(() => true)
202
+
203
+ await refreshAllPointSymbols({ manager, mapProvider: createMapProvider() })
204
+
205
+ expect(pointWithSymbol.set).toHaveBeenCalledWith('symbolImageId', expect.any(String))
206
+ expect(pointWithoutSymbol.set).not.toHaveBeenCalled()
207
+ expect(polygon.set).not.toHaveBeenCalled()
208
+ })
209
+
210
+ it('does nothing when there are no drawn points', async () => {
211
+ const manager = createManager({ features: [] })
212
+ await expect(refreshAllPointSymbols({ manager, mapProvider: createMapProvider() })).resolves.toEqual([])
213
+ })
214
+ })
@@ -37,7 +37,9 @@ export const createSnapIndicator = (map, colors) => {
37
37
  const layer = new VectorLayer({
38
38
  source,
39
39
  style: (f) => styles[f.get('snapType')] ?? null,
40
- zIndex: 200,
40
+ // Below the draw layer (zIndex 100) and its vertex/midpoint handles (101-103), matching
41
+ // the ML adapter — the indicator sits behind the symbols/vertices it's snapping to.
42
+ zIndex: 99,
41
43
  updateWhileAnimating: true,
42
44
  updateWhileInteracting: true
43
45
  })
@@ -3,9 +3,9 @@
3
3
  * mapBrowserEvent.coordinate to the nearest snap candidate before any draw
4
4
  * or modify interaction sees it.
5
5
  *
6
- * Coordinate snapping applies to all pointer events (pointermove, pointerdown,
7
- * pointerup, singleclick) so that both rubberbanding and vertex placement are snapped.
8
- * The visual indicator is only updated on pointermove.
6
+ * The visual indicator updates on both pointermove (hover preview) and pointerdrag (an active
7
+ * OL Modify drag), staying visible through to release nothing hides it on release; it only
8
+ * clears on a fresh query, a mode change, or snapping being switched off.
9
9
  *
10
10
  * Must be added to the map AFTER the Draw/Modify interaction so it is processed
11
11
  * first (OL iterates interactions in reverse-add order).
@@ -15,6 +15,7 @@
15
15
  import Interaction from 'ol/interaction/Interaction.js'
16
16
 
17
17
  const SNAP_EVENTS = new Set(['pointermove', 'pointerdrag', 'pointerdown', 'pointerup', 'singleclick', 'click'])
18
+ const INDICATOR_EVENTS = new Set(['pointermove', 'pointerdrag'])
18
19
 
19
20
  const processSnapEvent = (mapBrowserEvent, engine, indicator, snapRadius, isIndicatorActive) => {
20
21
  const { type } = mapBrowserEvent
@@ -33,13 +34,9 @@ const processSnapEvent = (mapBrowserEvent, engine, indicator, snapRadius, isIndi
33
34
  mapBrowserEvent.coordinate = result.coord.slice()
34
35
  }
35
36
 
36
- // Only update indicator during free mouse movement hide during drag, no-op for clicks
37
- if (type === 'pointermove' && isIndicatorActive()) {
37
+ // Hover preview and active drag both keep the indicator live; no update for click/down/up.
38
+ if (INDICATOR_EVENTS.has(type) && isIndicatorActive()) {
38
39
  result ? indicator.show(result.coord, result.type) : indicator.hide()
39
- } else if (type === 'pointerdrag') {
40
- indicator.hide()
41
- } else {
42
- // no indicator update for click/down/up events
43
40
  }
44
41
  }
45
42
 
@@ -35,7 +35,7 @@ test('rewrites the event coordinate to a copy of the snap candidate for all poin
35
35
  }
36
36
  })
37
37
 
38
- test('the indicator follows free mouse movement only', () => {
38
+ test('the indicator follows both free mouse movement and an active drag', () => {
39
39
  const { engine, indicator, fire } = setup()
40
40
  fire('pointermove')
41
41
  expect(indicator.show).toHaveBeenCalledWith([9, 9], 'vertex')
@@ -44,17 +44,28 @@ test('the indicator follows free mouse movement only', () => {
44
44
  fire('pointermove')
45
45
  expect(indicator.hide).toHaveBeenCalledTimes(1)
46
46
 
47
+ // A drag (e.g. edit_vertex's mouse-drag path, edit/modifyInteraction.js) keeps it live too —
48
+ // it must stay visible for as long as the dragged vertex sits on a candidate, not just on a
49
+ // free hover. See snapInteraction.js's own comment for why: this used to force-hide it on
50
+ // every pointerdrag, so the indicator could never actually be seen mid-drag.
47
51
  engine.query.mockReturnValue({ type: 'edge', coord: [5, 5] })
48
52
  fire('pointerdrag')
49
- expect(indicator.hide).toHaveBeenCalledTimes(2) // hidden during drags
53
+ expect(indicator.show).toHaveBeenCalledWith([5, 5], 'edge')
54
+
55
+ engine.query.mockReturnValue(null)
56
+ fire('pointerdrag')
57
+ expect(indicator.hide).toHaveBeenCalledTimes(2)
58
+
50
59
  fire('singleclick')
51
- expect(indicator.show).toHaveBeenCalledTimes(1) // clicks never touch the indicator
60
+ expect(indicator.show).toHaveBeenCalledTimes(2) // clicks never touch the indicator
61
+ expect(indicator.hide).toHaveBeenCalledTimes(2)
52
62
  })
53
63
 
54
- test('the indicator stays untouched during pointermove when the indicator is gated off', () => {
64
+ test('the indicator stays untouched during pointermove or a drag when the indicator is gated off', () => {
55
65
  const { indicator, fire } = setup({ indicatorActive: false })
56
66
  const { event } = fire('pointermove')
57
67
  expect(event.coordinate).toEqual([9, 9]) // snapping still applies
68
+ fire('pointerdrag')
58
69
  expect(indicator.show).not.toHaveBeenCalled()
59
70
  expect(indicator.hide).not.toHaveBeenCalled()
60
71
  })
@@ -16,7 +16,7 @@ export const pixelToCoord = (map, pixel) => {
16
16
  }
17
17
 
18
18
  /** Pixel distance between two { x, y } points */
19
- export const pixelDist = (a, b) => Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
19
+ export const pixelDist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y)
20
20
 
21
21
  /** OL pixel array [x, y] → { x, y } */
22
22
  export const arrayToPixel = ([x, y]) => ({ x, y })