@defra/interactive-map 0.0.39-alpha → 0.0.41-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 (387) 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/control-definition.md +9 -1
  7. package/docs/api/slots.md +26 -5
  8. package/docs/api.md +4 -4
  9. package/docs/assets/images/slot-map.svg +61 -262
  10. package/docs/examples/draw-tools.mdx +3 -3
  11. package/docs/plugins/draw.md +80 -17
  12. package/docs/plugins/search.md +1 -1
  13. package/govuk-prototype-kit.config.json +2 -0
  14. package/jest.setup.js +5 -0
  15. package/package.json +9 -1
  16. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  17. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  18. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  19. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  20. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  21. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  22. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  23. package/plugins/datasets/dist/css/index.css +1 -1
  24. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  25. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  26. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  27. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  28. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  29. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  30. package/plugins/datasets/dist/umd/index.js +1 -1
  31. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +2 -3
  32. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  33. package/plugins/datasets/src/adapters/esri/registry/esriDataset.js +1 -1
  34. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  35. package/plugins/datasets/src/adapters/loadLayerAdapter.js +1 -1
  36. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  37. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +1 -3
  38. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +17 -3
  39. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  40. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  41. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  42. package/plugins/datasets/src/datasets.scss +0 -1
  43. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  44. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  45. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  46. package/plugins/datasets/src/index.test.js +36 -0
  47. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  48. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  49. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -5
  50. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  51. package/plugins/datasets/src/manifest.js +0 -38
  52. package/plugins/datasets/src/manifest.test.js +49 -0
  53. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  54. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  55. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  56. package/plugins/datasets/src/registry/dataset.js +15 -4
  57. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  58. package/plugins/datasets/src/registry/datasetRegistry.js +12 -9
  59. package/plugins/datasets/src/registry/datasetRegistry.test.js +17 -5
  60. package/plugins/datasets/src/registry/globalDataset.js +0 -3
  61. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  62. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  63. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  64. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  65. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  66. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  67. package/plugins/draw/dist/esm/index.js +1 -1
  68. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  69. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  70. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  72. package/plugins/draw/dist/umd/index.js +1 -1
  73. package/plugins/draw/src/DrawInit.jsx +12 -4
  74. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  75. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +92 -12
  76. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  77. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  78. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  79. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  81. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +1 -1
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +1 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +5 -28
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -54
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +11 -4
  110. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  112. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  114. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  116. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  117. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  118. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  130. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  132. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  133. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  135. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  137. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +1 -0
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +27 -1
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +24 -4
  140. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +47 -3
  141. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +1 -1
  142. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +32 -1
  143. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  144. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +34 -1
  145. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +12 -5
  148. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +26 -3
  149. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  150. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  151. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  152. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  153. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  154. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  155. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  156. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  162. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  163. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  164. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  165. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  166. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  167. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  168. package/plugins/draw/src/api/addFeature.js +16 -1
  169. package/plugins/draw/src/api/addFeature.test.js +62 -5
  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/newPoint.js +42 -0
  173. package/plugins/draw/src/api/newPoint.test.js +117 -0
  174. package/plugins/draw/src/api/setStyle.js +43 -0
  175. package/plugins/draw/src/api/setStyle.test.js +75 -0
  176. package/plugins/draw/src/events.js +21 -14
  177. package/plugins/draw/src/events.test.js +15 -1
  178. package/plugins/draw/src/manifest.js +18 -6
  179. package/plugins/draw/src/manifest.test.js +32 -1
  180. package/plugins/draw/src/reducer.test.js +7 -0
  181. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  182. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  183. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  184. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  185. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  186. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  187. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  188. package/plugins/interact/dist/umd/index.js +1 -1
  189. package/plugins/interact/src/hooks/useCrossHairVisibility.js +12 -4
  190. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +88 -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/utils/featureQueries.js +18 -0
  194. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  195. package/plugins/interact/src/utils/spatial.js +9 -3
  196. package/plugins/interact/src/utils/spatial.test.js +12 -1
  197. package/plugins/map-key/dist/css/index.css +62 -0
  198. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  199. package/plugins/map-key/dist/esm/index.js +1 -0
  200. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  202. package/plugins/map-key/dist/umd/index.js +2 -0
  203. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  204. package/plugins/map-key/package.json +28 -0
  205. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  206. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  207. package/plugins/map-key/src/components/Key/Key.jsx +41 -0
  208. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
  209. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  210. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  212. package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  214. package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
  215. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  216. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
  217. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  218. package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
  220. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
  221. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  222. package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
  224. package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
  225. package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
  226. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  228. package/plugins/map-key/src/index.js +13 -0
  229. package/plugins/map-key/src/index.test.js +31 -0
  230. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  232. package/plugins/map-key/src/manifest.js +30 -0
  233. package/plugins/map-key/src/manifest.test.js +63 -0
  234. package/plugins/map-key/src/mapKey.scss +4 -0
  235. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  237. package/plugins/map-key/src/registry/index.js +3 -0
  238. package/plugins/map-key/src/registry/index.test.js +15 -0
  239. package/plugins/menu/dist/css/index.css +80 -0
  240. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  241. package/plugins/menu/dist/esm/index.js +1 -0
  242. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  243. package/plugins/menu/dist/umd/index.js +2 -0
  244. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  245. package/plugins/menu/package.json +28 -0
  246. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  247. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  249. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  250. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  251. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  252. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  254. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  255. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  257. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  258. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  259. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  260. package/plugins/menu/src/index.js +13 -0
  261. package/plugins/menu/src/index.test.js +31 -0
  262. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  263. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  264. package/plugins/menu/src/initialise/createMenu.js +10 -0
  265. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  266. package/plugins/menu/src/manifest.js +35 -0
  267. package/plugins/menu/src/manifest.test.js +63 -0
  268. package/plugins/menu/src/menu.scss +4 -0
  269. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  270. package/plugins/menu/src/reducers/pluginState.js +35 -0
  271. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  272. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  274. package/plugins/menu/src/registry/index.js +3 -0
  275. package/plugins/menu/src/registry/index.test.js +15 -0
  276. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  278. package/plugins/search/dist/css/index.css +1 -1
  279. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  280. package/plugins/search/dist/esm/index.js +1 -1
  281. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  282. package/plugins/search/dist/umd/index.js +1 -1
  283. package/plugins/search/src/Search.jsx +7 -5
  284. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  285. package/plugins/search/src/index.js +0 -1
  286. package/plugins/search/src/index.test.js +2 -4
  287. package/plugins/search/src/manifest.js +1 -1
  288. package/plugins/search/src/search.scss +17 -16
  289. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  290. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  292. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  294. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  296. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  298. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  299. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/index.js +1 -1
  301. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  302. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  303. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  304. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  305. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  306. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  307. package/providers/maplibre/src/utils/patternImages.js +1 -1
  308. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  309. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  310. package/rollup.esm.mjs +11 -1
  311. package/sonar-project.properties +7 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Attributions/Attributions.module.scss +10 -0
  314. package/src/App/components/Hints/Hints.jsx +7 -2
  315. package/src/App/components/Hints/Hints.test.jsx +13 -0
  316. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +2 -3
  317. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +3 -6
  318. package/src/App/components/{MoveControl/MoveControl.jsx → MoveControls/MoveControls.jsx} +10 -10
  319. package/src/App/components/{MoveControl/MoveControl.module.scss → MoveControls/MoveControls.module.scss} +60 -29
  320. package/src/App/components/{MoveControl/MoveControl.test.jsx → MoveControls/MoveControls.test.jsx} +48 -48
  321. package/src/App/components/Panel/Panel.jsx +18 -3
  322. package/src/App/components/Panel/Panel.module.scss +18 -8
  323. package/src/App/components/Panel/Panel.test.jsx +34 -0
  324. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  325. package/src/App/components/Viewport/Viewport.jsx +9 -1
  326. package/src/App/components/Viewport/Viewport.test.jsx +11 -0
  327. package/src/App/hooks/useFeatureItems.js +16 -7
  328. package/src/App/hooks/useFeatureItems.test.js +57 -0
  329. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  330. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  331. package/src/App/initialiseApp.js +11 -8
  332. package/src/App/initialiseApp.test.js +15 -3
  333. package/src/App/layout/Layout.jsx +12 -11
  334. package/src/App/layout/Layout.test.jsx +2 -0
  335. package/src/App/layout/layout.module.scss +31 -17
  336. package/src/App/registry/keyboardShortcutRegistry.js +25 -13
  337. package/src/App/registry/keyboardShortcutRegistry.test.js +49 -32
  338. package/src/App/registry/pluginRegistry.js +2 -3
  339. package/src/App/registry/pluginRegistry.test.js +16 -3
  340. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  341. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  342. package/src/App/renderer/mapButtons.js +28 -12
  343. package/src/App/renderer/mapButtons.test.js +17 -4
  344. package/src/App/renderer/mapControls.js +3 -1
  345. package/src/App/renderer/mapControls.test.js +16 -0
  346. package/src/App/renderer/mapPanels.js +44 -9
  347. package/src/App/renderer/mapPanels.test.js +51 -2
  348. package/src/App/renderer/orderItems.js +28 -0
  349. package/src/App/renderer/orderItems.test.js +50 -0
  350. package/src/App/renderer/slotAggregator.js +2 -18
  351. package/src/App/renderer/slotHelpers.js +3 -1
  352. package/src/App/renderer/slotHelpers.test.js +5 -0
  353. package/src/App/renderer/slots.js +3 -0
  354. package/src/App/store/AppProvider.jsx +4 -2
  355. package/src/App/store/appDispatchMiddleware.js +1 -1
  356. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  357. package/src/InteractiveMap/InteractiveMap.js +8 -2
  358. package/src/InteractiveMap/InteractiveMap.test.js +20 -3
  359. package/src/config/appConfig.js +17 -15
  360. package/src/config/appConfig.test.js +22 -22
  361. package/src/config/defaults.js +3 -1
  362. package/src/config/events.js +2 -0
  363. package/src/config/mergeConfig.js +7 -0
  364. package/src/config/mergeConfig.test.js +20 -0
  365. package/src/scss/main.scss +1 -1
  366. package/src/scss/settings/_colors.scss +1 -0
  367. package/src/scss/settings/_dimensions.scss +2 -3
  368. package/src/scss/settings/_transition.scss +1 -1
  369. package/src/services/eventBus.js +44 -0
  370. package/src/services/eventBus.test.js +83 -0
  371. package/src/services/symbolRegistry.js +1 -1
  372. package/src/test-utils.js +5 -0
  373. package/src/types.js +25 -7
  374. package/webpack.umd.mjs +2 -0
  375. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  376. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  377. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  378. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  379. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  380. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  381. package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
  382. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  383. package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
  384. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  385. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  386. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  387. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -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 })
@@ -1,4 +1,13 @@
1
1
  import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
2
+ import { stripInternalProperties } from '../utils/stripInternalProperties.js'
3
+ import { logger } from '../../../../src/services/logger.js'
4
+
5
+ const SUPPORTED_GEOMETRY_TYPES = new Set(['Point', 'LineString', 'Polygon'])
6
+
7
+ // type: 'Feature' is defaulted below (both adapters mishandle it missing), but a missing or
8
+ // unsupported geometry is a genuine caller mistake, so that's rejected instead.
9
+ const isValidFeature = (feature) =>
10
+ SUPPORTED_GEOMETRY_TYPES.has(feature.geometry?.type) && feature.geometry?.coordinates !== undefined
2
11
 
3
12
  export const addFeature = ({ mapProvider, services }, feature) => {
4
13
  const { draw } = mapProvider
@@ -10,6 +19,7 @@ export const addFeature = ({ mapProvider, services }, feature) => {
10
19
 
11
20
  const { stroke, fill, strokeWidth, properties, ...featureRest } = feature
12
21
  const flatFeature = {
22
+ type: 'Feature',
13
23
  ...featureRest,
14
24
  properties: {
15
25
  ...properties,
@@ -17,6 +27,11 @@ export const addFeature = ({ mapProvider, services }, feature) => {
17
27
  }
18
28
  }
19
29
 
30
+ if (!isValidFeature(flatFeature)) {
31
+ logger.warn('addFeature: ignoring invalid GeoJSON feature', flatFeature)
32
+ return
33
+ }
34
+
20
35
  draw.add(flatFeature)
21
- eventBus.emit('draw:add', flatFeature)
36
+ eventBus.emit('draw:add', stripInternalProperties(flatFeature))
22
37
  }
@@ -1,9 +1,13 @@
1
1
  import { addFeature } from './addFeature.js'
2
2
  import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
3
+ import { logger } from '../../../../src/services/logger.js'
3
4
 
4
5
  jest.mock('../utils/flattenStyleProperties.js', () => ({
5
6
  flattenStyleProperties: jest.fn(() => ({ _flat: true }))
6
7
  }))
8
+ jest.mock('../../../../src/services/logger.js', () => ({
9
+ logger: { warn: jest.fn(), error: jest.fn() }
10
+ }))
7
11
 
8
12
  const setup = (draw) => {
9
13
  const eventBus = { emit: jest.fn() }
@@ -19,15 +23,27 @@ describe('addFeature', () => {
19
23
  expect(eventBus.emit).not.toHaveBeenCalled()
20
24
  })
21
25
 
22
- test('flattens style properties and adds the feature', () => {
26
+ test('flattens style properties, defaults type to Feature, and adds the feature', () => {
23
27
  const draw = { add: jest.fn() }
24
28
  const { context, eventBus } = setup(draw)
25
- const feature = { id: 'a', geometry: {}, stroke: 'red', fill: 'blue', strokeWidth: 2, properties: { name: 'x' } }
29
+ const feature = {
30
+ id: 'a',
31
+ geometry: { type: 'Point', coordinates: [0, 0] },
32
+ stroke: 'red',
33
+ fill: 'blue',
34
+ strokeWidth: 2,
35
+ properties: { name: 'x' }
36
+ }
26
37
 
27
38
  addFeature(context, feature)
28
39
 
29
40
  expect(flattenStyleProperties).toHaveBeenCalledWith({ stroke: 'red', fill: 'blue', strokeWidth: 2 })
30
- const expected = { id: 'a', geometry: {}, properties: { name: 'x', _flat: true } }
41
+ const expected = {
42
+ type: 'Feature',
43
+ id: 'a',
44
+ geometry: { type: 'Point', coordinates: [0, 0] },
45
+ properties: { name: 'x', _flat: true }
46
+ }
31
47
  expect(draw.add).toHaveBeenCalledWith(expected)
32
48
  expect(eventBus.emit).toHaveBeenCalledWith('draw:add', expected)
33
49
  })
@@ -35,7 +51,48 @@ describe('addFeature', () => {
35
51
  test('handles a feature without existing properties', () => {
36
52
  const draw = { add: jest.fn() }
37
53
  const { context } = setup(draw)
38
- addFeature(context, { id: 'b', geometry: {} })
39
- expect(draw.add).toHaveBeenCalledWith({ id: 'b', geometry: {}, properties: { _flat: true } })
54
+ addFeature(context, { id: 'b', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] } })
55
+ expect(draw.add).toHaveBeenCalledWith({
56
+ type: 'Feature',
57
+ id: 'b',
58
+ geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] },
59
+ properties: { _flat: true }
60
+ })
61
+ })
62
+
63
+ test('does not override an explicitly supplied type', () => {
64
+ const draw = { add: jest.fn() }
65
+ const { context } = setup(draw)
66
+ addFeature(context, { id: 'c', type: 'Feature', geometry: { type: 'Polygon', coordinates: [[]] } })
67
+ expect(draw.add).toHaveBeenCalledWith(expect.objectContaining({ type: 'Feature' }))
68
+ })
69
+
70
+ // A missing/unsupported geometry is a genuine caller mistake — warned and dropped rather
71
+ // than handed to an adapter that would throw or silently misread it.
72
+ describe('invalid geometry', () => {
73
+ test('rejects a feature with no geometry at all', () => {
74
+ const draw = { add: jest.fn() }
75
+ const { context, eventBus } = setup(draw)
76
+ addFeature(context, { id: 'd' })
77
+ expect(draw.add).not.toHaveBeenCalled()
78
+ expect(eventBus.emit).not.toHaveBeenCalled()
79
+ expect(logger.warn).toHaveBeenCalledWith('addFeature: ignoring invalid GeoJSON feature', expect.objectContaining({ id: 'd' }))
80
+ })
81
+
82
+ test('rejects an unsupported geometry type', () => {
83
+ const draw = { add: jest.fn() }
84
+ const { context } = setup(draw)
85
+ addFeature(context, { id: 'e', geometry: { type: 'MultiPoint', coordinates: [[0, 0]] } })
86
+ expect(draw.add).not.toHaveBeenCalled()
87
+ expect(logger.warn).toHaveBeenCalled()
88
+ })
89
+
90
+ test('rejects a geometry with no coordinates', () => {
91
+ const draw = { add: jest.fn() }
92
+ const { context } = setup(draw)
93
+ addFeature(context, { id: 'f', geometry: { type: 'Point' } })
94
+ expect(draw.add).not.toHaveBeenCalled()
95
+ expect(logger.warn).toHaveBeenCalled()
96
+ })
40
97
  })
41
98
  })
@@ -18,14 +18,17 @@ export const editFeature = ({ appState, appConfig, mapState, pluginConfig, plugi
18
18
  // Per-call callback overrides the plugin-level one; events.js reads this on every commit.
19
19
  draw._geometryValidator = options.onGeometryChange ?? pluginConfig.onGeometryChange
20
20
 
21
- const editModeMap = { LineString: 'edit_line', Polygon: 'edit_polygon' }
21
+ // A Point has no ring of vertices to edit — edit_point relocates the single coordinate
22
+ // instead of edit_vertex's ring-of-vertices machinery.
23
+ const editModeMap = { LineString: 'edit_line', Polygon: 'edit_polygon', Point: 'edit_point' }
24
+ const mode = existingFeature.geometry.type === 'Point' ? 'edit_point' : 'edit_vertex'
22
25
  eventBus.emit('draw:editstart', { mode: editModeMap[existingFeature.geometry.type] })
23
26
 
24
27
  const snapLayers = options.snapLayers === undefined ? (pluginConfig.snapLayers ?? null) : options.snapLayers
25
28
  draw.setSnapLayers(snapLayers)
26
29
  dispatch({ type: 'SET_HAS_SNAP_LAYERS', payload: snapLayers?.length > 0 })
27
30
 
28
- draw.changeMode('edit_vertex', {
31
+ draw.changeMode(mode, {
29
32
  container: appState.layoutRefs.viewportRef.current,
30
33
  deleteVertexButtonId: `${appConfig.id}-draw-delete-point`,
31
34
  undoButtonId: `${appConfig.id}-draw-undo`,
@@ -42,11 +45,11 @@ export const editFeature = ({ appState, appConfig, mapState, pluginConfig, plugi
42
45
  payload: { feature, tempFeature: feature }
43
46
  })
44
47
 
45
- dispatch({ type: 'SET_MODE', payload: 'edit_vertex' })
48
+ dispatch({ type: 'SET_MODE', payload: mode })
46
49
 
47
50
  // Seed the Done-button gate and the stroke from the feature's starting validity
48
51
  // so an already invalid feature opens dashed and cannot be "finished" until fixed.
49
- const { valid } = validateGeometry(feature, { phase: 'edit-start', mode: 'edit_vertex' }, { onGeometryChange: draw._geometryValidator })
52
+ const { valid } = validateGeometry(feature, { phase: 'edit-start', mode }, { onGeometryChange: draw._geometryValidator })
50
53
  dispatch({ type: 'SET_GEOMETRY_VALID', payload: valid })
51
54
  draw.setInvalid?.(!valid)
52
55
 
@@ -74,6 +74,22 @@ describe('editFeature', () => {
74
74
  expect(eventBus.emit).toHaveBeenCalledWith('draw:editstart', { mode: 'edit_line' })
75
75
  })
76
76
 
77
+ // A Point has no ring of vertices — editing one goes through edit_point (relocate the
78
+ // single coordinate), not edit_vertex, across every mode-name site this function touches.
79
+ test('uses edit_point mode for a point feature', () => {
80
+ const { context, dispatch, eventBus, draw } = makeContext()
81
+ draw.get.mockReturnValue({ id: 'f1', geometry: { type: 'Point', coordinates: [0, 0] } })
82
+
83
+ editFeature(context, 'f1')
84
+
85
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:editstart', { mode: 'edit_point' })
86
+ expect(draw.changeMode).toHaveBeenCalledWith('edit_point', expect.objectContaining({ featureId: 'f1' }))
87
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: 'edit_point' })
88
+ // Point geometry is always valid (validation/rules.js has no Point branch) — the gate
89
+ // opens immediately, unlike a self-intersecting/zero-area polygon.
90
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: true })
91
+ })
92
+
77
93
  test('stores a per-call onGeometryChange validator, overriding the plugin-level one', () => {
78
94
  const pluginOnGeometryChange = jest.fn()
79
95
  const onGeometryChange = jest.fn()
@@ -0,0 +1,42 @@
1
+ import { SYMBOL_KEYS } from '../utils/symbolKeys.js'
2
+
3
+ export const newPoint = ({ appState, appConfig, pluginConfig, pluginState, mapState, mapProvider, services }, featureId, options = {}) => {
4
+ const { dispatch } = pluginState
5
+ const { draw } = mapProvider
6
+ const { eventBus } = services
7
+
8
+ if (!draw) {
9
+ return
10
+ }
11
+
12
+ eventBus.emit('draw:started', { mode: 'draw_point' })
13
+
14
+ const snapLayers = options.snapLayers === undefined ? (pluginConfig.snapLayers ?? null) : options.snapLayers
15
+ draw.setSnapLayers(snapLayers)
16
+ dispatch({ type: 'SET_HAS_SNAP_LAYERS', payload: snapLayers?.length > 0 })
17
+
18
+ const { properties: customProperties, onGeometryChange, ...modeOptions } = options
19
+ // Per-call callback overrides the plugin-level one; events.js reads this on every commit.
20
+ draw._geometryValidator = onGeometryChange ?? pluginConfig.onGeometryChange
21
+
22
+ const symbolStyle = {}
23
+ SYMBOL_KEYS.forEach((key) => {
24
+ if (modeOptions[key] !== undefined) {
25
+ symbolStyle[key] = modeOptions[key]
26
+ delete modeOptions[key]
27
+ }
28
+ })
29
+
30
+ draw.changeMode('draw_point', {
31
+ container: appState.layoutRefs.viewportRef.current,
32
+ addVertexButtonId: `${appConfig.id}-draw-add-point`,
33
+ interfaceType: appState.interfaceType,
34
+ crossHair: mapState.crossHair,
35
+ getSnapEnabled: () => draw.isSnapEnabled(),
36
+ featureId,
37
+ ...modeOptions,
38
+ properties: { ...customProperties, ...symbolStyle }
39
+ })
40
+
41
+ dispatch({ type: 'SET_MODE', payload: 'draw_point' })
42
+ }