@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
@@ -2,9 +2,13 @@ import Style from 'ol/style/Style.js'
2
2
  import Fill from 'ol/style/Fill.js'
3
3
  import Stroke from 'ol/style/Stroke.js'
4
4
  import CircleStyle from 'ol/style/Circle.js'
5
+ import Icon from 'ol/style/Icon.js'
5
6
  import MultiPoint from 'ol/geom/MultiPoint.js'
6
7
  import { SIZES } from '../defaults.js'
7
8
  import { getPlacedSketchCoords } from '../utils/sketchHelpers.js'
9
+ import { getCachedSymbolImage } from '../../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
10
+ import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js'
11
+ import { getSymbolAnchor } from '../../../../../../src/utils/symbolUtils.js'
8
12
 
9
13
  const HALO_RADIUS_OFFSET = 3
10
14
 
@@ -74,12 +78,74 @@ const createSketchLineStyles = (colors) => ({
74
78
  })
75
79
  })
76
80
 
81
+ // Placeholder for a committed point feature that has no symbol config, or whose symbol
82
+ // image hasn't rasterised/cached yet (point/pointSymbolImages.js resolves it asynchronously
83
+ // after the feature is added to the store — this is what renders in the gap, and what
84
+ // permanently renders for a point with no symbol properties at all). Stroke/Fill styles
85
+ // have no effect on a Point geometry, so without this branch a committed point renders
86
+ // invisibly.
87
+ const createPointPlaceholderStyle = (colors) => new Style({
88
+ image: new CircleStyle({
89
+ radius: SIZES.vertexRadius + HALO_RADIUS_OFFSET,
90
+ fill: new Fill({ color: colors.shapeFill }),
91
+ stroke: new Stroke({ color: colors.shapeStroke, width: 2 })
92
+ })
93
+ })
94
+
95
+ // ol/style/Icon defaults (anchorOrigin: 'top-left', anchorXUnits/anchorYUnits: 'fraction')
96
+ // already match symbolAnchor's own [x,y]-fraction-from-top-left convention, so unlike the
97
+ // MapLibre adapter (whose icon-anchor only has 9 discrete positions) no offset compensation
98
+ // is needed here — the raw anchor is usable as-is. Icon style instances are cached per
99
+ // imageId+anchor+pixelRatio so a style function running every render frame doesn't rebuild
100
+ // one each time.
101
+ const createPointStyles = (colors) => {
102
+ const pointStyle = createPointPlaceholderStyle(colors)
103
+ const iconStyleCache = new Map()
104
+
105
+ // imageIdProp lets edit_point's own per-feature style override (point/editPointMode.js)
106
+ // ask for the precomputed "selected" (black ring) variant instead of the normal one — the
107
+ // same variant the interact plugin's own highlight already uses, via the same
108
+ // symbolSelectedImageId point/pointSymbolImages.js resolves for every point.
109
+ const getPointIconStyle = (properties, imageIdProp = 'symbolImageId') => {
110
+ const imageId = properties[imageIdProp]
111
+ const canvas = imageId && getCachedSymbolImage(imageId)
112
+ if (!canvas) {
113
+ return null
114
+ }
115
+ const symbolDef = symbolRegistry.getSymbolDef(properties)
116
+ const [anchorX, anchorY] = getSymbolAnchor(properties, symbolDef)
117
+ // The cached canvas was rasterised at viewBox × pixelRatio device pixels for crispness
118
+ // (point/pointSymbolImages.js) — unlike MapLibre's map.addImage(id, data, {pixelRatio}),
119
+ // which uses the registered pixelRatio to auto-derive the displayed CSS size, ol/style/
120
+ // Icon draws its source at native pixel size by default, so `scale` has to cancel that
121
+ // back out here or the icon renders pixelRatio× too big.
122
+ const pixelRatio = properties.symbolPixelRatio || 1
123
+ const cacheKey = `${imageId}|${anchorX}|${anchorY}|${pixelRatio}`
124
+ let iconStyle = iconStyleCache.get(cacheKey)
125
+ if (!iconStyle) {
126
+ iconStyle = new Style({ image: new Icon({ img: canvas, anchor: [anchorX, anchorY], scale: 1 / pixelRatio }) })
127
+ iconStyleCache.set(cacheKey, iconStyle)
128
+ }
129
+ return iconStyle
130
+ }
131
+
132
+ return {
133
+ pointStyleFor: (feature) => getPointIconStyle(feature.getProperties()) ?? pointStyle,
134
+ // Falls back through selected → normal → placeholder, so edit_point still shows
135
+ // something sensible before the selected variant has resolved.
136
+ selectedPointStyleFor: (feature) => {
137
+ const properties = feature.getProperties()
138
+ return getPointIconStyle(properties, 'symbolSelectedImageId') ?? getPointIconStyle(properties) ?? pointStyle
139
+ }
140
+ }
141
+ }
142
+
77
143
  /**
78
144
  * Create all draw-ol style instances for the given resolved color set.
79
145
  *
80
146
  * @param {object} colors - Output of resolveColors()
81
147
  * @returns {{ vertexStyle, selectedVertexStyle, midpointStyle, selectedMidpointStyle,
82
- * editFeatureStyle, createSketchStyle, createFeatureStyle }}
148
+ * editFeatureStyle, selectedPointStyleFor, createSketchStyle, createFeatureStyle }}
83
149
  */
84
150
  export const createStyles = (colors) => {
85
151
  const { vertexImage, vertexStyle, selectedVertexStyle } = createVertexStyles(colors)
@@ -130,7 +196,12 @@ export const createStyles = (colors) => {
130
196
  return type === geometryType ? [lineStyle, sketchVertexStyle] : [lineStyle]
131
197
  }
132
198
 
199
+ const { pointStyleFor, selectedPointStyleFor } = createPointStyles(colors)
200
+
133
201
  const createFeatureStyle = () => (feature) => {
202
+ if (feature.getGeometry().getType() === 'Point') {
203
+ return [pointStyleFor(feature)]
204
+ }
134
205
  const p = feature.getProperties()
135
206
  const id = colors.mapStyleId
136
207
  const stroke = (id && p[`stroke${capitalize(id)}`]) || p.stroke || colors.shapeStroke
@@ -149,6 +220,7 @@ export const createStyles = (colors) => {
149
220
  selectedMidpointStyle,
150
221
  editFeatureStyle,
151
222
  editFeatureStyleInvalid,
223
+ selectedPointStyleFor,
152
224
  createSketchStyle,
153
225
  createFeatureStyle
154
226
  }
@@ -1,9 +1,11 @@
1
1
  import Point from 'ol/geom/Point.js'
2
2
  import LineString from 'ol/geom/LineString.js'
3
3
  import Feature from 'ol/Feature.js'
4
+ import Icon from 'ol/style/Icon.js'
4
5
  import { createStyles } from './styles.js'
5
6
  import { SIZES } from '../defaults.js'
6
- import { polygonFeature, lineFeature } from '../__helpers__/harness.js'
7
+ import { polygonFeature, lineFeature, pointFeature } from '../__helpers__/harness.js'
8
+ import { getOrCreateSymbolImage, clearSymbolImageCache } from '../../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
7
9
 
8
10
  const colors = {
9
11
  editStroke: '#e5',
@@ -117,7 +119,11 @@ describe('sketch styles while drawing', () => {
117
119
  })
118
120
 
119
121
  describe('createFeatureStyle (inactive shapes)', () => {
120
- const styleFor = (properties) => styles.createFeatureStyle()({ getProperties: () => properties })[0]
122
+ const styleFor = (properties) => {
123
+ const feature = polygonFeature([[0, 0], [1, 0], [1, 1], [0, 0]])
124
+ feature.setProperties(properties)
125
+ return styles.createFeatureStyle()(feature)[0]
126
+ }
121
127
 
122
128
  test('falls back to the configured shape colours', () => {
123
129
  const style = styleFor({})
@@ -137,7 +143,120 @@ describe('createFeatureStyle (inactive shapes)', () => {
137
143
 
138
144
  test('without a map style id only the generic properties apply', () => {
139
145
  const plain = createStyles({ ...colors, mapStyleId: null })
140
- const style = plain.createFeatureStyle()({ getProperties: () => ({ strokeRoad: '#r1' }) })[0]
146
+ const feature = polygonFeature([[0, 0], [1, 0], [1, 1], [0, 0]])
147
+ feature.setProperties({ strokeRoad: '#r1' })
148
+ const style = plain.createFeatureStyle()(feature)[0]
141
149
  expect(style.getStroke().getColor()).toBe(colors.shapeStroke)
142
150
  })
151
+
152
+ test('a committed Point feature gets the placeholder circle style, not Stroke/Fill', () => {
153
+ const style = styles.createFeatureStyle()(pointFeature([5, 5]))[0]
154
+ expect(style.getImage()).toBeTruthy()
155
+ expect(style.getStroke()).toBeFalsy()
156
+ expect(style.getFill()).toBeFalsy()
157
+ })
158
+
159
+ test('a Point feature with a resolved+cached symbolImageId renders a real Icon, not the placeholder', () => {
160
+ HTMLCanvasElement.prototype.getContext = jest.fn(function () {
161
+ this._ctx ??= { putImageData: jest.fn() }
162
+ return this._ctx
163
+ })
164
+ clearSymbolImageCache()
165
+ const canvas = getOrCreateSymbolImage('symbol-pin-1x', { width: 10, height: 10 })
166
+
167
+ const feature = pointFeature([5, 5])
168
+ feature.setProperties({ symbol: 'pin', symbolImageId: 'symbol-pin-1x' })
169
+
170
+ const style = styles.createFeatureStyle()(feature)[0]
171
+ expect(style.getImage()).toBeInstanceOf(Icon)
172
+ expect(style.getImage().getImage(1)).toBe(canvas)
173
+ })
174
+
175
+ test('reuses the same Icon Style instance across renders for the same imageId/anchor/pixelRatio', () => {
176
+ HTMLCanvasElement.prototype.getContext = jest.fn(function () {
177
+ this._ctx ??= { putImageData: jest.fn() }
178
+ return this._ctx
179
+ })
180
+ clearSymbolImageCache()
181
+ getOrCreateSymbolImage('symbol-pin-cache', { width: 10, height: 10 })
182
+
183
+ const feature = pointFeature([5, 5])
184
+ feature.setProperties({ symbol: 'pin', symbolImageId: 'symbol-pin-cache' })
185
+
186
+ const styleFn = styles.createFeatureStyle()
187
+ const first = styleFn(feature)[0]
188
+ const second = styleFn(feature)[0] // a style function runs every render frame
189
+ expect(second).toBe(first)
190
+ })
191
+
192
+ test('scales the Icon down by the rasterisation pixelRatio, so a high-DPI raster still displays at its intended CSS size', () => {
193
+ HTMLCanvasElement.prototype.getContext = jest.fn(function () {
194
+ this._ctx ??= { putImageData: jest.fn() }
195
+ return this._ctx
196
+ })
197
+ clearSymbolImageCache()
198
+ getOrCreateSymbolImage('symbol-pin-4x', { width: 176, height: 176 }) // 44 viewBox × pixelRatio 4
199
+
200
+ const feature = pointFeature([5, 5])
201
+ feature.setProperties({ symbol: 'pin', symbolImageId: 'symbol-pin-4x', symbolPixelRatio: 4 })
202
+
203
+ const style = styles.createFeatureStyle()(feature)[0]
204
+ expect(style.getImage().getScale()).toBe(0.25)
205
+ })
206
+
207
+ test('defaults to scale 1 when symbolPixelRatio is missing', () => {
208
+ HTMLCanvasElement.prototype.getContext = jest.fn(function () {
209
+ this._ctx ??= { putImageData: jest.fn() }
210
+ return this._ctx
211
+ })
212
+ clearSymbolImageCache()
213
+ getOrCreateSymbolImage('symbol-pin-no-ratio', { width: 44, height: 44 })
214
+
215
+ const feature = pointFeature([5, 5])
216
+ feature.setProperties({ symbol: 'pin', symbolImageId: 'symbol-pin-no-ratio' })
217
+
218
+ const style = styles.createFeatureStyle()(feature)[0]
219
+ expect(style.getImage().getScale()).toBe(1)
220
+ })
221
+
222
+ // edit_point's per-feature style override (point/editPointMode.js) uses this to keep
223
+ // showing the same "selected" (black ring) look the interact plugin's own highlight
224
+ // already uses for the whole edit session — see selectedPointStyleFor's own comment.
225
+ test('selectedPointStyleFor renders the selected variant, falling back to normal then the placeholder', () => {
226
+ HTMLCanvasElement.prototype.getContext = jest.fn(function () {
227
+ this._ctx ??= { putImageData: jest.fn() }
228
+ return this._ctx
229
+ })
230
+ clearSymbolImageCache()
231
+ const selectedCanvas = getOrCreateSymbolImage('symbol-pin-selected', { width: 10, height: 10 })
232
+
233
+ const selected = pointFeature([5, 5])
234
+ selected.setProperties({ symbol: 'pin', symbolImageId: 'symbol-pin-normal', symbolSelectedImageId: 'symbol-pin-selected' })
235
+ const selectedStyle = styles.selectedPointStyleFor(selected)
236
+ expect(selectedStyle.getImage()).toBeInstanceOf(Icon)
237
+ expect(selectedStyle.getImage().getImage(1)).toBe(selectedCanvas)
238
+
239
+ // No selected variant resolved yet → falls back to the normal icon
240
+ const normalCanvas = getOrCreateSymbolImage('symbol-pin-normal-only', { width: 10, height: 10 })
241
+ const normalOnly = pointFeature([5, 5])
242
+ normalOnly.setProperties({ symbol: 'pin', symbolImageId: 'symbol-pin-normal-only' })
243
+ const fallbackStyle = styles.selectedPointStyleFor(normalOnly)
244
+ expect(fallbackStyle.getImage().getImage(1)).toBe(normalCanvas)
245
+
246
+ // Neither resolved → the placeholder circle
247
+ const placeholderStyle = styles.selectedPointStyleFor(pointFeature([5, 5]))
248
+ expect(placeholderStyle.getStroke()).toBeFalsy()
249
+ expect(placeholderStyle.getImage()).not.toBeInstanceOf(Icon)
250
+ })
251
+
252
+ test('a Point feature whose symbolImageId has not been rasterised/cached yet falls back to the placeholder', () => {
253
+ clearSymbolImageCache()
254
+ const feature = pointFeature([5, 5])
255
+ feature.setProperties({ symbol: 'pin', symbolImageId: 'symbol-not-cached' })
256
+
257
+ const style = styles.createFeatureStyle()(feature)[0]
258
+ expect(style.getImage()).not.toBeInstanceOf(Icon)
259
+ expect(style.getStroke()).toBeFalsy()
260
+ expect(style.getFill()).toBeFalsy()
261
+ })
143
262
  })
@@ -172,6 +172,7 @@ const buildDrawInput = ({ drawInteraction, options, geometryType, getSketch, upd
172
172
  addVertexButtonId: options.addVertexButtonId,
173
173
  mapProvider: options.mapProvider,
174
174
  snap: options.snap,
175
+ crossHair: options.crossHair,
175
176
  onUndo: () => { drawInteraction.removeLastPoint(); updateVertexCount(); emitUndoValidation() },
176
177
  canFinish: () => canFinish(geometryType, getSketch()),
177
178
  canPlace: canPlaceVertex
@@ -4,6 +4,8 @@ import { createFeatureStore } from '../core/featureStore.js'
4
4
  import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
5
5
  import { STYLES_CHANGED_EVENT } from '../core/internalEvents.js'
6
6
  import { createFakeMap, createFakeManager, polygonFeature, lineFeature } from '../__helpers__/harness.js'
7
+ import Feature from 'ol/Feature.js'
8
+ import Polygon from 'ol/geom/Polygon.js'
7
9
 
8
10
  jest.mock('./drawInput.js', () => ({
9
11
  createDrawInput: jest.fn(() => ({
@@ -174,6 +176,16 @@ describe('drawing lifecycle', () => {
174
176
  expect(manager.styles.createSketchStyle).not.toHaveBeenCalledWith('Polygon', true)
175
177
  })
176
178
 
179
+ test('a Polygon sketch with no ring yet (freshly started, before the first coordinate) never throws or goes dashed', () => {
180
+ const { manager, interaction } = setup('Polygon')
181
+ const sketch = new Feature(new Polygon([]))
182
+ sketch.setId('shape-1')
183
+ interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
184
+ // coords[0] is undefined with zero rings — displayedSketch's `?? []` fallback covers it.
185
+ expect(() => sketch.getGeometry().setCoordinates([])).not.toThrow()
186
+ expect(manager.styles.createSketchStyle).not.toHaveBeenCalledWith('Polygon', true)
187
+ })
188
+
177
189
  test('2 placed vertices + cursor never go dashed (below the 4-point threshold)', () => {
178
190
  const { manager, interaction } = setup('Polygon')
179
191
  const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
@@ -187,6 +199,20 @@ describe('drawing lifecycle', () => {
187
199
  expect(typeof inputOptions.canPlace).toBe('function')
188
200
  })
189
201
 
202
+ test('drawInput receives the crossHair passed to the mode, so it can sync visibility itself', () => {
203
+ const map = createFakeMap()
204
+ const manager = createFakeManager()
205
+ manager.store = createFeatureStore()
206
+ const crossHair = { fixAtCenter: jest.fn(), hide: jest.fn() }
207
+ createDrawMode({
208
+ map,
209
+ manager,
210
+ options: { geometryType: 'Polygon', featureId: 'shape-1', properties: {}, container: null, snap: null, crossHair }
211
+ })
212
+ const inputOptions = createDrawInput.mock.calls.at(-1)[0].options
213
+ expect(inputOptions.crossHair).toBe(crossHair)
214
+ })
215
+
190
216
  test('a placement-vetoing path disables Add point; a legal one re-enables it', () => {
191
217
  const { emitted, interaction } = setup('Polygon')
192
218
  const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
@@ -249,7 +275,7 @@ describe('drawing lifecycle', () => {
249
275
  expect(changed).toHaveBeenCalled()
250
276
  })
251
277
 
252
- test('setInterfaceType forwards to the draw input (e.g. DrawInit syncing MoveControl-driven interface changes mid-session)', () => {
278
+ test('setInterfaceType forwards to the draw input (e.g. DrawInit syncing MoveControls-driven interface changes mid-session)', () => {
253
279
  const { mode, input } = setup('Polygon')
254
280
  mode.setInterfaceType('touch')
255
281
  expect(input.setInterfaceType).toHaveBeenCalledWith('touch')
@@ -2,7 +2,23 @@ import { createVertexPlacement } from './vertexPlacement.js'
2
2
 
3
3
  const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])
4
4
 
5
- const wireInputEvents = ({
5
+ // Mirrors DrawInit.jsx's fixAtCenter()/hide() gate, but reasserted on every interface-type transition this module detects — matching MapLibre's own onSetup/onInterfaceTypeChange redundancy.
6
+ // Exported alongside wireInputEvents for the same reason — point/drawPointMode.js reuses it directly.
7
+ export const applyCrossHairVisibility = (crossHair, type) => {
8
+ if (!crossHair) { return }
9
+ if (['touch', 'keyboard'].includes(type)) {
10
+ crossHair.fixAtCenter()
11
+ } else {
12
+ crossHair.hide()
13
+ }
14
+ }
15
+
16
+ // Exported so point/drawPointMode.js can reuse this DOM-wiring layer directly — it's
17
+ // already generic (crosshair/interface-type tracking, add-vertex-button/Enter/undo
18
+ // listeners), parametrised entirely by the callbacks passed in, with no ring-shaped
19
+ // assumptions baked in. draw_point supplies point-shaped versions of those callbacks
20
+ // (no rubber band, no undo) rather than this file growing a second copy of the wiring.
21
+ export const wireInputEvents = ({
6
22
  container, addVertexButtonId, olView, onUndo,
7
23
  getInterfaceType, setInterfaceType, clearLastCoord,
8
24
  updateRubberbanding, placeVertex
@@ -83,7 +99,7 @@ const wireInputEvents = ({
83
99
  * @returns {{ getInterfaceType: () => string, destroy: () => void }}
84
100
  */
85
101
  export const createDrawInput = ({ drawInteraction, options }) => {
86
- const { container, addVertexButtonId, mapProvider, snap, onUndo, canFinish, canPlace } = options
102
+ const { container, addVertexButtonId, mapProvider, snap, onUndo, canFinish, canPlace, crossHair } = options
87
103
  let interfaceType = options.interfaceType ?? 'mouse'
88
104
  const getInterfaceType = () => interfaceType
89
105
 
@@ -105,12 +121,15 @@ export const createDrawInput = ({ drawInteraction, options }) => {
105
121
  olView,
106
122
  onUndo,
107
123
  getInterfaceType,
108
- setInterfaceType: (t) => { interfaceType = t },
124
+ setInterfaceType: (t) => { interfaceType = t; applyCrossHairVisibility(crossHair, t) },
109
125
  clearLastCoord: placement.clearLastCoord,
110
126
  updateRubberbanding: placement.updateRubberbanding,
111
127
  placeVertex: placement.placeVertex
112
128
  })
113
129
 
130
+ // Sync on creation too (mirrors MapLibre's own onSetup), so the crosshair reflects the mode's starting interface type immediately.
131
+ applyCrossHairVisibility(crossHair, interfaceType)
132
+
114
133
  // change:center fires once when a keyboard pan animation starts; postrender tracks each frame.
115
134
  const onMapRender = () => {
116
135
  if (interfaceType !== 'mouse' && olView?.getAnimating()) {
@@ -122,11 +141,12 @@ export const createDrawInput = ({ drawInteraction, options }) => {
122
141
  return {
123
142
  getInterfaceType,
124
143
  // Called when the global interface type changes without any pointer/touch/key
125
- // event landing on the map container (e.g. panning via MoveControl after
144
+ // event landing on the map container (e.g. panning via MoveControls after
126
145
  // switching to touch) — refresh the rubber band immediately rather than
127
146
  // waiting for the next incidental change:center/postrender event.
128
147
  setInterfaceType (type) {
129
148
  interfaceType = type
149
+ applyCrossHairVisibility(crossHair, type)
130
150
  if (type !== 'mouse') {
131
151
  placement.updateRubberbanding()
132
152
  }
@@ -10,7 +10,7 @@ jest.mock('./vertexPlacement.js', () => ({
10
10
  }))
11
11
  }))
12
12
 
13
- const setup = (interfaceType = 'mouse') => {
13
+ const setup = (interfaceType = 'mouse', crossHair) => {
14
14
  const map = createFakeMap()
15
15
  const container = createContainer()
16
16
  const button = document.createElement('button')
@@ -19,13 +19,15 @@ const setup = (interfaceType = 'mouse') => {
19
19
  const onUndo = jest.fn()
20
20
  const input = createDrawInput({
21
21
  drawInteraction: { getMap: () => map },
22
- options: { container, addVertexButtonId: 'add-pt', interfaceType, mapProvider: {}, snap: null, onUndo }
22
+ options: { container, addVertexButtonId: 'add-pt', interfaceType, mapProvider: {}, snap: null, onUndo, crossHair }
23
23
  })
24
24
  const placement = createVertexPlacement.mock.results.at(-1).value
25
25
  liveInputs.push(input)
26
26
  return { map, view: map.getView(), container, button, input, placement, onUndo }
27
27
  }
28
28
 
29
+ const fakeCrossHair = () => ({ fixAtCenter: jest.fn(), hide: jest.fn() })
30
+
29
31
  const liveInputs = []
30
32
  afterEach(() => {
31
33
  liveInputs.splice(0).forEach((i) => i.destroy())
@@ -109,7 +111,7 @@ test('pointer moves and map pans update the rubber band except for the mouse int
109
111
  expect(mouse.placement.updateRubberbanding).not.toHaveBeenCalled()
110
112
  })
111
113
 
112
- test('setInterfaceType updates the interface and refreshes the rubber band immediately for non-mouse types, e.g. switching to touch and panning via MoveControl mid-session', () => {
114
+ test('setInterfaceType updates the interface and refreshes the rubber band immediately for non-mouse types, e.g. switching to touch and panning via MoveControls mid-session', () => {
113
115
  const { input, placement } = setup('mouse')
114
116
  input.setInterfaceType('touch')
115
117
  expect(input.getInterfaceType()).toBe('touch')
@@ -130,6 +132,48 @@ test('keyboard pan animations keep the rubber band anchored via postrender', ()
130
132
  expect(placement.updateRubberbanding).toHaveBeenCalledTimes(1)
131
133
  })
132
134
 
135
+ test('syncs the crosshair on creation to match the starting interface type', () => {
136
+ const touchCrossHair = fakeCrossHair()
137
+ setup('touch', touchCrossHair)
138
+ expect(touchCrossHair.fixAtCenter).toHaveBeenCalled()
139
+ expect(touchCrossHair.hide).not.toHaveBeenCalled()
140
+
141
+ const mouseCrossHair = fakeCrossHair()
142
+ setup('mouse', mouseCrossHair)
143
+ expect(mouseCrossHair.hide).toHaveBeenCalled()
144
+ expect(mouseCrossHair.fixAtCenter).not.toHaveBeenCalled()
145
+ })
146
+
147
+ test('shows the crosshair when a key press switches to keyboard, hides it when a mouse pointerdown switches back', () => {
148
+ const crossHair = fakeCrossHair()
149
+ const { container } = setup('mouse', crossHair)
150
+ expect(crossHair.hide).toHaveBeenCalledTimes(1) // initial sync at creation, starting interface is 'mouse'
151
+
152
+ container.focus()
153
+ key({ key: 'ArrowUp' })
154
+ expect(crossHair.fixAtCenter).toHaveBeenCalledTimes(1)
155
+
156
+ container.dispatchEvent(Object.assign(new Event('pointerdown'), { pointerType: 'mouse' }))
157
+ expect(crossHair.hide).toHaveBeenCalledTimes(2)
158
+ })
159
+
160
+ test('setInterfaceType syncs the crosshair the same way as a detected input change', () => {
161
+ const crossHair = fakeCrossHair()
162
+ const { input } = setup('mouse', crossHair)
163
+ expect(crossHair.hide).toHaveBeenCalledTimes(1) // initial sync at creation, starting interface is 'mouse'
164
+
165
+ input.setInterfaceType('touch')
166
+ expect(crossHair.fixAtCenter).toHaveBeenCalledTimes(1)
167
+
168
+ input.setInterfaceType('mouse')
169
+ expect(crossHair.hide).toHaveBeenCalledTimes(2)
170
+ })
171
+
172
+ test('tolerates a missing crosshair (e.g. edit_vertex mode, which does not supply one)', () => {
173
+ const { input } = setup('mouse')
174
+ expect(() => input.setInterfaceType('keyboard')).not.toThrow()
175
+ })
176
+
133
177
  test('destroy removes all listeners', () => {
134
178
  const { container, button, input, placement } = setup()
135
179
  input.destroy()
@@ -291,7 +291,7 @@ const buildModeApi = ({ manager, store, olFeature, originalFeatureStyle, selecti
291
291
 
292
292
  undo: actions.doUndo,
293
293
  deleteVertex: actions.doDeleteVertex,
294
- // MoveControl's D-pad, routed here via mapProvider.activeMoveTarget (see
294
+ // MoveControls' D-pad, routed here via mapProvider.activeMoveTarget (see
295
295
  // events.js) once a vertex is selected.
296
296
  nudgeSelectedVertex: parts.keyboardHandler.nudgeByDelta,
297
297
 
@@ -71,6 +71,13 @@ test('a mid-drag crossing turns the stroke dashed live, and back when it clears'
71
71
  expect(currentStyle(olFeature)).toBe(manager.styles.editFeatureStyle)
72
72
  })
73
73
 
74
+ test('a Polygon with no ring yet (transiently empty mid-drag) never throws computing numVertices', () => {
75
+ const { olFeature } = setup()
76
+ // OL's own Polygon geometry supports zero rings natively — coordinates[0] is undefined,
77
+ // exercising updateLiveValidity's `?? 1` fallback rather than a real drag scenario.
78
+ expect(() => olFeature.getGeometry().setCoordinates([])).not.toThrow()
79
+ })
80
+
74
81
  test('a validity flip repaints the map without touching the feature — Modify tracks its own drag via feature #change, which setStyle() would trip', () => {
75
82
  const { map, olFeature } = setup()
76
83
  const renderCallsBefore = map.render.mock.calls.length
@@ -88,6 +95,30 @@ test('validity flips while editing also gate the Done button', () => {
88
95
  expect.objectContaining({ valid: true }))
89
96
  })
90
97
 
98
+ test('a LineString edit computes numVertices directly from coordinates.length (no ring -1 adjustment)', () => {
99
+ const map = createFakeMap()
100
+ const manager = createFakeManager()
101
+ manager.store = createFeatureStore()
102
+ manager.store.add({ type: 'Feature', id: 'l1', properties: {}, geometry: { type: 'LineString', coordinates: [[0, 0], [100, 0], [100, 100]] } })
103
+ const container = createContainer()
104
+ map.getViewport().appendChild(container)
105
+ const mode = createEditMode({
106
+ map,
107
+ manager,
108
+ options: { featureId: 'l1', container, interfaceType: 'mouse', deleteVertexButtonId: 'del-v', snap: null }
109
+ })
110
+ liveModes.push(mode)
111
+ const olFeature = manager.store.getOL('l1')
112
+
113
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_CHANGE, { numVertices: 3 })
114
+
115
+ // LIVE_RULES (self-intersect/area) only ever apply to Polygon (see rules.js's getPolygon
116
+ // guard) — a self-crossing LineString still drives updateLiveValidity, but never flips
117
+ // the stroke invalid, unlike the Polygon case above.
118
+ olFeature.getGeometry().setCoordinates([[0, 0], [100, 100], [100, 0], [0, 100]])
119
+ expect(currentStyle(olFeature)).toBe(manager.styles.editFeatureStyle)
120
+ })
121
+
91
122
  test('the user callback runs throttled during an edit drag', () => {
92
123
  jest.useFakeTimers()
93
124
  const { manager, olFeature } = setup()
@@ -123,7 +154,7 @@ test('select via pointer, delete the vertex, then undo restores it', () => {
123
154
  mode.undo() // empty stack — no-op
124
155
  })
125
156
 
126
- test('nudgeSelectedVertex (MoveControl D-pad) moves the selected vertex and is undoable', () => {
157
+ test('nudgeSelectedVertex (MoveControls D-pad) moves the selected vertex and is undoable', () => {
127
158
  const { container, manager, mode, ring } = setup()
128
159
  container.dispatchEvent(domEvent('pointerdown', { pointerType: 'mouse', clientX: 100, clientY: 0 }))
129
160
  mode.nudgeSelectedVertex(1, 0, true)
@@ -104,12 +104,13 @@ const buildKeydownHandler = ({ map, getState, setState, nudge, keyMove, onUndo,
104
104
  }
105
105
  }
106
106
 
107
- const buildKeyupHandler = ({ snap, keyMove, onVertexMoved, onDeleted, isFocused }) => (e) => {
107
+ const buildKeyupHandler = ({ keyMove, onVertexMoved, onDeleted, isFocused }) => (e) => {
108
108
  if (isFocused()) {
109
109
  return
110
110
  }
111
111
  if (ARROW_KEYS.has(e.key) && keyMove.start && keyMove.index != null) {
112
- snap?.hideIndicator()
112
+ // Not hiding the snap indicator here — nudge.js's own snap.apply() already left it showing
113
+ // correctly, and it should stay that way after the key is released.
113
114
  onVertexMoved({ vertexIndex: keyMove.index, previousCoord: keyMove.start })
114
115
  keyMove.start = null
115
116
  keyMove.index = null
@@ -133,16 +134,19 @@ const buildKeyupHandler = ({ snap, keyMove, onVertexMoved, onDeleted, isFocused
133
134
  * convert it. Only pressing a plain/Shift arrow converts it.
134
135
  *
135
136
  * @param {{ map, getState, setState, snap, onVertexMoved, onInserted, onDeleted, onUndo, onKeyboardActive }} options
137
+ * @param {(olFeature, index: number, coord: number[]) => void} [options.moveCoord] - coordinate
138
+ * writer forwarded to wireNudge, defaulting there to moveVertex; point/editPointMode.js
139
+ * injects point/pointOps.js's movePoint instead.
136
140
  * @returns {{ nudgeByDelta: (dx: number, dy: number, isLargeStep: boolean) => void, destroy: () => void }}
137
141
  */
138
142
  export const createKeyboardHandler = (options) => {
139
- const { map, snap, getState, setState, onVertexMoved, onInserted, onDeleted, onUndo, onKeyboardActive } = options
140
- const { nudge, keyMove, nudgeByDelta } = wireNudge({ map, snap, getState, setState, onInserted, onVertexMoved })
143
+ const { map, snap, getState, setState, onVertexMoved, onInserted, onDeleted, onUndo, onKeyboardActive, moveCoord } = options
144
+ const { nudge, keyMove, nudgeByDelta } = wireNudge({ map, snap, getState, setState, onInserted, onVertexMoved, moveCoord })
141
145
  const appViewport = map.getViewport().closest('[role="application"]') ?? map.getViewport()
142
146
  const isFocused = () => isInteractiveElementFocused(appViewport)
143
147
 
144
148
  const onKeydown = buildKeydownHandler({ map, getState, setState, nudge, keyMove, onUndo, onKeyboardActive, isFocused })
145
- const onKeyup = buildKeyupHandler({ snap, keyMove, onVertexMoved, onDeleted, isFocused })
149
+ const onKeyup = buildKeyupHandler({ keyMove, onVertexMoved, onDeleted, isFocused })
146
150
 
147
151
  globalThis.addEventListener('keydown', onKeydown, { capture: true })
148
152
  globalThis.addEventListener('keyup', onKeyup, { capture: true })
@@ -33,7 +33,7 @@ afterEach(() => {
33
33
 
34
34
  const key = (type, props) => window.dispatchEvent(new KeyboardEvent(type, { cancelable: true, ...props }))
35
35
 
36
- test('nudgeByDelta (exposed for MoveControl) moves the selected vertex and reports it via onVertexMoved', () => {
36
+ test('nudgeByDelta (exposed for MoveControls) moves the selected vertex and reports it via onVertexMoved', () => {
37
37
  const { state, handler, onVertexMoved } = setup()
38
38
  Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
39
39
  handler.nudgeByDelta(1, 0, true)
@@ -115,6 +115,39 @@ test('a plain arrow nudges the selected vertex, and keyup commits a single move
115
115
  expect(onVertexMoved).toHaveBeenCalledTimes(1)
116
116
  })
117
117
 
118
+ // Regression: keyup used to force-hide the snap indicator unconditionally, so it vanished the
119
+ // instant the key was released even when the vertex landed exactly on a snap target — making it
120
+ // impossible to tell whether the nudge had actually snapped. nudge.js's own snap.apply() already
121
+ // leaves the indicator correctly reflecting reality; keyup must leave it alone (mirrors the ML
122
+ // adapter, which never hides its own indicator on keyup either).
123
+ test('keyup does not hide the snap indicator', () => {
124
+ const snap = { apply: jest.fn((c) => c), hideIndicator: jest.fn(), snapRadius: 12 }
125
+ const map = createFakeMap({ center: [98, 98] })
126
+ const state = {
127
+ olFeature: polygonFeature(RING),
128
+ selectedVertexIndex: 1,
129
+ selectedVertexType: 'vertex',
130
+ vertices: [[0, 0], [100, 0], [100, 100]],
131
+ midpoints: [[50, 0], [100, 50], [50, 50]]
132
+ }
133
+ const setState = jest.fn((updates) => Object.assign(state, updates))
134
+ const handler = createKeyboardHandler({
135
+ map,
136
+ getState: () => state,
137
+ setState,
138
+ snap,
139
+ onVertexMoved: jest.fn(),
140
+ onInserted: jest.fn(),
141
+ onDeleted: jest.fn(),
142
+ onUndo: jest.fn(),
143
+ onKeyboardActive: jest.fn()
144
+ })
145
+ liveHandlers.push(handler)
146
+ key('keydown', { key: 'ArrowRight' })
147
+ key('keyup', { key: 'ArrowRight' })
148
+ expect(snap.hideIndicator).not.toHaveBeenCalled()
149
+ })
150
+
118
151
  test('arrows without a selection do nothing', () => {
119
152
  const { setState } = setup()
120
153
  key('keydown', { key: 'ArrowRight' })
@@ -44,16 +44,18 @@ export const buildModifyCondition = ({ map, getState }) => (mapBrowserEvent) =>
44
44
  * each completed drag via `onModifyEnd(prevCoords)` with the vertices
45
45
  * snapshotted at drag start.
46
46
  *
47
+ * @param {(mapBrowserEvent) => boolean} [condition] - overrides buildModifyCondition's
48
+ * fixed-radius vertex hit-test.
47
49
  * @returns {{ destroy: () => void }}
48
50
  */
49
- export const createModifyInteraction = ({ map, olFeature, getState, onModifyEnd }) => {
50
- const condition = buildModifyCondition({ map, getState })
51
+ export const createModifyInteraction = ({ map, olFeature, getState, onModifyEnd, condition }) => {
52
+ const modifyCondition = condition ?? buildModifyCondition({ map, getState })
51
53
 
52
54
  const modifyInteraction = new Modify({
53
55
  features: new Collection([olFeature]),
54
56
  style: () => [], // vertex circles rendered by vertexLayer instead
55
57
  pixelTolerance: PIXEL_TOLERANCE,
56
- condition
58
+ condition: modifyCondition
57
59
  })
58
60
  map.addInteraction(modifyInteraction)
59
61