@defra/interactive-map 0.0.40-alpha → 0.0.42-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/button-definition.md +7 -8
  7. package/docs/api/control-definition.md +17 -1
  8. package/docs/api/panel-definition.md +5 -0
  9. package/docs/api/slots.md +50 -6
  10. package/docs/assets/images/slot-map.svg +61 -262
  11. package/docs/examples/add-polygons.mdx +8 -2
  12. package/docs/examples/add-symbols.mdx +8 -2
  13. package/docs/examples/draw-tools.mdx +3 -3
  14. package/docs/plugins/datasets.md +23 -15
  15. package/docs/plugins/draw.md +79 -16
  16. package/docs/plugins/map-key.md +147 -0
  17. package/docs/plugins/search.md +1 -1
  18. package/docs/plugins.md +5 -1
  19. package/govuk-prototype-kit.config.json +2 -0
  20. package/jest.setup.js +5 -0
  21. package/package.json +9 -1
  22. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  23. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  24. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  25. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  26. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  27. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  28. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  29. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  30. package/plugins/beta/use-location/src/manifest.js +1 -1
  31. package/plugins/datasets/dist/css/index.css +1 -1
  32. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  33. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  34. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  35. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  36. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  37. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  38. package/plugins/datasets/dist/umd/index.js +1 -1
  39. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  40. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  41. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  42. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  43. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  44. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  45. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  46. package/plugins/datasets/src/datasets.scss +0 -1
  47. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  48. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  49. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  50. package/plugins/datasets/src/index.test.js +36 -0
  51. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  52. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  53. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  54. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  55. package/plugins/datasets/src/manifest.js +0 -38
  56. package/plugins/datasets/src/manifest.test.js +49 -0
  57. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  58. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  59. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  60. package/plugins/datasets/src/registry/dataset.js +13 -1
  61. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  62. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  63. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  64. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  65. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  66. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  67. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  68. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  69. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  70. package/plugins/draw/dist/esm/index.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  72. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  73. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  74. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  75. package/plugins/draw/dist/umd/index.js +1 -1
  76. package/plugins/draw/src/DrawInit.jsx +10 -2
  77. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  78. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  79. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  80. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  81. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  110. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  112. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  114. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  116. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  117. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  118. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  130. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  132. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  133. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  135. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  137. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  140. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  142. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  144. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  147. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  148. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  149. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  150. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  151. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  152. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  153. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  154. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  155. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  156. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  162. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  163. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  164. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  165. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  166. package/plugins/draw/src/api/addFeature.js +16 -1
  167. package/plugins/draw/src/api/addFeature.test.js +62 -5
  168. package/plugins/draw/src/api/createNewShape.js +41 -0
  169. package/plugins/draw/src/api/createNewShape.test.js +63 -0
  170. package/plugins/draw/src/api/editFeature.js +7 -4
  171. package/plugins/draw/src/api/editFeature.test.js +16 -0
  172. package/plugins/draw/src/api/newLine.js +2 -40
  173. package/plugins/draw/src/api/newPoint.js +42 -0
  174. package/plugins/draw/src/api/newPoint.test.js +117 -0
  175. package/plugins/draw/src/api/newPolygon.js +2 -40
  176. package/plugins/draw/src/api/setStyle.js +43 -0
  177. package/plugins/draw/src/api/setStyle.test.js +75 -0
  178. package/plugins/draw/src/events.js +17 -10
  179. package/plugins/draw/src/events.test.js +14 -0
  180. package/plugins/draw/src/manifest.js +18 -6
  181. package/plugins/draw/src/manifest.test.js +32 -1
  182. package/plugins/draw/src/reducer.test.js +7 -0
  183. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  184. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  185. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  186. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  187. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  188. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  189. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  190. package/plugins/interact/dist/umd/index.js +1 -1
  191. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  192. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  193. package/plugins/interact/src/manifest.js +1 -1
  194. package/plugins/interact/src/utils/featureQueries.js +18 -0
  195. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  196. package/plugins/interact/src/utils/spatial.js +9 -3
  197. package/plugins/interact/src/utils/spatial.test.js +12 -1
  198. package/plugins/map-key/dist/css/index.css +68 -0
  199. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  200. package/plugins/map-key/dist/esm/index.js +1 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  202. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  203. package/plugins/map-key/dist/umd/index.js +2 -0
  204. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  205. package/plugins/map-key/package.json +28 -0
  206. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  208. package/plugins/map-key/src/components/Key/Key.jsx +43 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +21 -14
  210. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  212. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.jsx +26 -0
  214. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  215. package/plugins/map-key/src/components/Key/KeySvg.jsx +41 -0
  216. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  217. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +22 -0
  218. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.jsx +7 -2
  220. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +8 -2
  221. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +26 -0
  222. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.jsx +8 -3
  224. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +17 -6
  225. package/plugins/map-key/src/components/Key/MapKey.jsx +53 -0
  226. package/plugins/map-key/src/components/Key/MapKey.test.jsx +147 -0
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  228. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  229. package/plugins/map-key/src/index.js +13 -0
  230. package/plugins/map-key/src/index.test.js +31 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  232. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  233. package/plugins/map-key/src/manifest.js +30 -0
  234. package/plugins/map-key/src/manifest.test.js +63 -0
  235. package/plugins/map-key/src/mapKey.scss +4 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  237. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  238. package/plugins/map-key/src/registry/index.js +3 -0
  239. package/plugins/map-key/src/registry/index.test.js +15 -0
  240. package/plugins/menu/dist/css/index.css +80 -0
  241. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  242. package/plugins/menu/dist/esm/index.js +1 -0
  243. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  244. package/plugins/menu/dist/umd/index.js +2 -0
  245. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  246. package/plugins/menu/package.json +28 -0
  247. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  249. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  250. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  251. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  252. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  254. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  255. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  257. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  258. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  259. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  260. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  261. package/plugins/menu/src/index.js +13 -0
  262. package/plugins/menu/src/index.test.js +31 -0
  263. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  264. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  265. package/plugins/menu/src/initialise/createMenu.js +10 -0
  266. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  267. package/plugins/menu/src/manifest.js +35 -0
  268. package/plugins/menu/src/manifest.test.js +63 -0
  269. package/plugins/menu/src/menu.scss +4 -0
  270. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  271. package/plugins/menu/src/reducers/pluginState.js +35 -0
  272. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  274. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  275. package/plugins/menu/src/registry/index.js +3 -0
  276. package/plugins/menu/src/registry/index.test.js +15 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  278. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  279. package/plugins/search/dist/css/index.css +1 -1
  280. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  281. package/plugins/search/dist/esm/index.js +1 -1
  282. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  283. package/plugins/search/dist/umd/index.js +1 -1
  284. package/plugins/search/src/Search.jsx +2 -3
  285. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  286. package/plugins/search/src/index.js +0 -1
  287. package/plugins/search/src/index.test.js +2 -4
  288. package/plugins/search/src/manifest.js +1 -1
  289. package/plugins/search/src/search.scss +17 -16
  290. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  292. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  294. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  296. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  298. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  299. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  301. package/providers/maplibre/dist/umd/index.js +1 -1
  302. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  303. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  304. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  305. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  306. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  307. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  308. package/providers/maplibre/src/utils/patternImages.js +1 -1
  309. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  310. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  311. package/rollup.esm.mjs +11 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Hints/Hints.test.jsx +13 -0
  314. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
  315. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
  316. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  317. package/src/App/components/Panel/Panel.jsx +65 -8
  318. package/src/App/components/Panel/Panel.module.scss +49 -15
  319. package/src/App/components/Panel/Panel.test.jsx +114 -1
  320. package/src/App/components/Tabs/Tabs.jsx +43 -17
  321. package/src/App/components/Tabs/Tabs.module.scss +32 -5
  322. package/src/App/components/Tabs/Tabs.test.jsx +40 -10
  323. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  324. package/src/App/hooks/useFeatureItems.js +16 -7
  325. package/src/App/hooks/useFeatureItems.test.js +57 -0
  326. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  327. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  328. package/src/App/layout/Layout.jsx +9 -10
  329. package/src/App/layout/Layout.test.jsx +2 -0
  330. package/src/App/layout/layout.module.scss +31 -17
  331. package/src/App/registry/pluginRegistry.js +1 -1
  332. package/src/App/registry/pluginRegistry.test.js +12 -0
  333. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  334. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  335. package/src/App/renderer/groupByKey.js +28 -0
  336. package/src/App/renderer/groupByKey.test.js +39 -0
  337. package/src/App/renderer/groupIntoTabs.js +42 -0
  338. package/src/App/renderer/groupIntoTabs.test.js +95 -0
  339. package/src/App/renderer/mapButtons.js +97 -129
  340. package/src/App/renderer/mapButtons.test.js +67 -83
  341. package/src/App/renderer/mapControls.js +4 -1
  342. package/src/App/renderer/mapControls.test.js +32 -0
  343. package/src/App/renderer/mapPanels.js +53 -11
  344. package/src/App/renderer/mapPanels.test.js +95 -2
  345. package/src/App/renderer/orderItems.js +28 -0
  346. package/src/App/renderer/orderItems.test.js +50 -0
  347. package/src/App/renderer/slotAggregator.js +2 -18
  348. package/src/App/renderer/slotHelpers.js +3 -1
  349. package/src/App/renderer/slotHelpers.test.js +5 -0
  350. package/src/App/renderer/slots.js +3 -0
  351. package/src/App/store/AppProvider.jsx +1 -0
  352. package/src/App/store/appDispatchMiddleware.js +1 -1
  353. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  354. package/src/config/appConfig.js +2 -2
  355. package/src/config/events.js +2 -0
  356. package/src/scss/settings/_colors.scss +1 -0
  357. package/src/scss/settings/_dimensions.scss +2 -3
  358. package/src/scss/settings/_transition.scss +1 -1
  359. package/src/services/eventBus.js +44 -0
  360. package/src/services/eventBus.test.js +83 -0
  361. package/src/services/symbolRegistry.js +1 -1
  362. package/src/types.js +31 -3
  363. package/webpack.umd.mjs +2 -0
  364. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  365. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  366. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  367. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  368. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  369. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  370. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  371. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  372. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  373. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  374. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -70,6 +70,42 @@ test('the offset target hides when its vertex cannot be projected', () => {
70
70
  handler.destroy()
71
71
  })
72
72
 
73
+ // The offset target is the drag handle under your finger and must track it exactly, even
74
+ // while snap pulls the vertex itself elsewhere (mirrors the ML adapter) — the vertex is what
75
+ // visibly "jumps" to the snap candidate, not the target. Once the drag actually ends, the
76
+ // target re-syncs to the vertex's own final (possibly snapped) position, collapsing the gap.
77
+ test('the offset target tracks the raw touch point while dragging, then snaps to the vertex on release', () => {
78
+ const snap = { apply: jest.fn((c) => [c[0] + 5, c[1]]), hideIndicator: jest.fn() }
79
+ const { container, handler, touch } = setup({}, { snap })
80
+ handler.updateTargetPosition()
81
+ touch('touchstart', 100, 0)
82
+ touch('touchmove', 120, 10) // raw touch → [120,10]; vertex snaps to [125,10]
83
+ const target = container.querySelector('[data-im-draw-touch-target]')
84
+ expect(target.style.left).toBe('120px') // tracks the finger…
85
+ expect(target.style.left).not.toBe('125px') // …not the snapped vertex, while still dragging
86
+
87
+ touch('touchend', 120, 10)
88
+ expect(target.style.left).toBe('125px') // re-synced to the vertex's actual final position
89
+ handler.destroy()
90
+ })
91
+
92
+ // touchcancel (the browser interrupting a gesture itself — a scroll takeover, app switch,
93
+ // etc.) must resync the target exactly like touchend does — without this listener a
94
+ // cancelled drag left the target stuck wherever the last touchmove put it.
95
+ test('touchcancel resyncs the target the same way touchend does', () => {
96
+ const snap = { apply: jest.fn((c) => [c[0] + 5, c[1]]), hideIndicator: jest.fn() }
97
+ const { container, handler, touch, grip } = setup({}, { snap })
98
+ handler.updateTargetPosition()
99
+ touch('touchstart', 100, 0)
100
+ touch('touchmove', 120, 10) // raw touch → [120,10]; vertex snaps to [125,10]
101
+ const target = container.querySelector('[data-im-draw-touch-target]')
102
+ expect(target.style.left).toBe('120px') // tracking the finger mid-drag
103
+
104
+ grip.dispatchEvent(domEvent('touchcancel', { touches: [{ clientX: 120, clientY: 10 }], changedTouches: [{ clientX: 120, clientY: 10 }] }))
105
+ expect(target.style.left).toBe('125px') // re-synced to the vertex's actual final position
106
+ handler.destroy()
107
+ })
108
+
73
109
  test('a drag applies snapping to the moved coordinate when a snap manager is active', () => {
74
110
  const snap = { apply: jest.fn((c) => [c[0] + 5, c[1]]), hideIndicator: jest.fn() }
75
111
  const { state, handler, touch } = setup({}, { snap })
@@ -77,8 +113,17 @@ test('a drag applies snapping to the moved coordinate when a snap manager is act
77
113
  touch('touchstart', 100, 0)
78
114
  touch('touchmove', 120, 10)
79
115
  expect(snap.apply).toHaveBeenCalled()
80
- expect(snap.hideIndicator).toHaveBeenCalled()
81
116
  expect(state.vertices[1]).toEqual([125, 10]) // dragged to [120,10] then snapped +5 on x
117
+
118
+ // Regression: a stray hideIndicator() right after snap.apply() on every touchmove undid its
119
+ // own show, so the indicator could never actually be seen mid-drag — it's only meant to
120
+ // clear for real once the drag actually finishes.
121
+ expect(snap.hideIndicator).not.toHaveBeenCalled()
122
+ // And it must stay showing after the drag ends too — snap.apply()'s own last call already
123
+ // left it correctly reflecting the snapped position (mirrors the ML adapter, which never
124
+ // hides its own indicator on release either).
125
+ touch('touchend', 120, 10)
126
+ expect(snap.hideIndicator).not.toHaveBeenCalled()
82
127
  handler.destroy()
83
128
  })
84
129
 
@@ -1,11 +1,9 @@
1
1
  import { OLDrawManager } from './core/OLDrawManager.js'
2
2
  import { MAP_SIZE_SCALES } from './defaults.js'
3
+ import { refreshAllPointSymbols } from './point/pointSymbolImages.js'
3
4
 
4
5
  /**
5
- * Creates the OLDrawManager, attaches it to mapProvider, and wires
6
- * app-level events (MAP_SET_SIZE for scale-aware touch targets,
7
- * MAP_SET_STYLE for dynamic color updates).
8
- *
6
+ * Creates the OLDrawManager, attaches it to mapProvider, and wires app-level size/style events.
9
7
  * @returns {{ manager: OLDrawManager, remove: () => void }}
10
8
  */
11
9
  export const createOLDraw = ({ mapProvider, events, eventBus, pluginConfig = {}, mapStyle = null }) => {
@@ -18,13 +16,22 @@ export const createOLDraw = ({ mapProvider, events, eventBus, pluginConfig = {},
18
16
 
19
17
  mapProvider.draw = manager
20
18
 
19
+ // Seed drawScale from the provider's own starting size — MAP_SET_SIZE only fires on a later runtime change, not the initial one.
20
+ mapProvider.drawScale = MAP_SIZE_SCALES[mapProvider.mapSize] ?? 1
21
+
22
+ // Nudges useHighlightSync to re-apply the highlight overlay — OL's own MAP_DATA_CHANGE is driven purely by basemap 'tileloadend', unrelated to the draw source.
23
+ const notifyPointSymbolsRefreshed = () => eventBus.emit(events.MAP_DATA_CHANGE)
24
+
21
25
  const handleSetMapSize = (size) => {
22
26
  mapProvider.drawScale = MAP_SIZE_SCALES[size] ?? 1
27
+ refreshAllPointSymbols({ manager, mapProvider }).then(notifyPointSymbolsRefreshed)
23
28
  }
24
29
  eventBus.on(events.MAP_SET_SIZE, handleSetMapSize)
25
30
 
26
31
  const handleSetMapStyle = (newMapStyle) => {
27
32
  manager.setMapStyle(newMapStyle)
33
+ // Rasterised point symbol images are style-scoped (colours resolve per map style).
34
+ refreshAllPointSymbols({ manager, mapProvider }).then(notifyPointSymbolsRefreshed)
28
35
  }
29
36
  eventBus.on(events.MAP_SET_STYLE, handleSetMapStyle)
30
37
 
@@ -1,5 +1,6 @@
1
1
  import { createOLDraw } from './olDraw.js'
2
2
  import { OLDrawManager } from './core/OLDrawManager.js'
3
+ import { refreshAllPointSymbols } from './point/pointSymbolImages.js'
3
4
  import { MAP_SIZE_SCALES } from './defaults.js'
4
5
 
5
6
  jest.mock('./core/OLDrawManager.js', () => ({
@@ -8,15 +9,16 @@ jest.mock('./core/OLDrawManager.js', () => ({
8
9
  this.remove = jest.fn()
9
10
  })
10
11
  }))
12
+ jest.mock('./point/pointSymbolImages.js', () => ({ refreshAllPointSymbols: jest.fn(() => Promise.resolve()) }))
11
13
 
12
- const events = { MAP_SET_SIZE: 'app:size', MAP_SET_STYLE: 'app:style' }
14
+ const events = { MAP_SET_SIZE: 'app:size', MAP_SET_STYLE: 'app:style', MAP_DATA_CHANGE: 'app:datachange' }
13
15
 
14
16
  const setup = (mapStyle = null) => {
15
17
  const listeners = {}
16
18
  const eventBus = {
17
19
  on: jest.fn((type, handler) => { listeners[type] = handler }),
18
20
  off: jest.fn(),
19
- emit: (type, payload) => listeners[type]?.(payload)
21
+ emit: jest.fn((type, payload) => listeners[type]?.(payload))
20
22
  }
21
23
  const mapProvider = { map: { id: 'ol-map' } }
22
24
  const olDraw = createOLDraw({ mapProvider, events, eventBus, pluginConfig: { snapRadius: 5 }, mapStyle })
@@ -35,14 +37,46 @@ test('creates the manager for the map, exposes it as mapProvider.draw and applie
35
37
  expect(setup().manager.setMapStyle).not.toHaveBeenCalled() // no initial style
36
38
  })
37
39
 
38
- test('map size changes update the draw UI scale, defaulting to 1 for unknown sizes', () => {
39
- const { eventBus, mapProvider } = setup()
40
+ // Regression: MAP_SET_SIZE only ever fires from the map-size UI control being clicked at
41
+ // runtime, so a map that loads directly at medium/large left drawScale unset (defaulting to 1,
42
+ // i.e. small) until the user happened to change size — rasterising the first symbol placed at
43
+ // the wrong resolution. mapProvider.mapSize already holds the size the map actually loaded at.
44
+ test('seeds drawScale from the provider\'s own starting map size, before any size-change event', () => {
45
+ const eventBus = { on: jest.fn(), off: jest.fn() }
46
+ const mapProvider = { map: { id: 'ol-map' }, mapSize: 'large' }
47
+ createOLDraw({ mapProvider, events, eventBus })
48
+ expect(mapProvider.drawScale).toBe(MAP_SIZE_SCALES.large)
49
+ })
50
+
51
+ test('defaults the seeded drawScale to 1 when the provider has no starting map size', () => {
52
+ const eventBus = { on: jest.fn(), off: jest.fn() }
53
+ const mapProvider = { map: { id: 'ol-map' } }
54
+ createOLDraw({ mapProvider, events, eventBus })
55
+ expect(mapProvider.drawScale).toBe(1)
56
+ })
57
+
58
+ test('map size changes update the draw UI scale, defaulting to 1 for unknown sizes, and re-resolve point symbols', async () => {
59
+ const { eventBus, mapProvider, manager } = setup()
40
60
  eventBus.emit(events.MAP_SET_SIZE, 'large')
41
61
  expect(mapProvider.drawScale).toBe(MAP_SIZE_SCALES.large)
62
+ expect(refreshAllPointSymbols).toHaveBeenCalledWith({ manager, mapProvider })
42
63
  eventBus.emit(events.MAP_SET_SIZE, 'enormous')
43
64
  expect(mapProvider.drawScale).toBe(1)
44
65
  })
45
66
 
67
+ // A selected point's highlight overlay only re-applies on MAP_DATA_CHANGE, and (unlike
68
+ // MapLibre) OL's own MAP_DATA_CHANGE is driven purely by basemap 'tileloadend' — nothing
69
+ // ties it to the draw VectorSource, so without this explicit nudge a resize/style change
70
+ // with no tiles loading would never re-trigger the highlight refresh at all, leaving an
71
+ // already-selected point's ring stuck on its old (now wrong-resolution) icon.
72
+ test('emits MAP_DATA_CHANGE once point symbols have actually finished re-resolving after a size change', async () => {
73
+ const { eventBus, manager, mapProvider } = setup()
74
+ eventBus.emit(events.MAP_SET_SIZE, 'large')
75
+ await Promise.resolve() // flush the refreshAllPointSymbols().then(...) microtask
76
+ expect(refreshAllPointSymbols).toHaveBeenCalledWith({ manager, mapProvider })
77
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_DATA_CHANGE)
78
+ })
79
+
46
80
  test('pluginConfig and mapStyle are optional, defaulting to {} and no initial style', () => {
47
81
  const eventBus = { on: jest.fn(), off: jest.fn() }
48
82
  const mapProvider = { map: { id: 'ol-map' } }
@@ -54,10 +88,18 @@ test('pluginConfig and mapStyle are optional, defaulting to {} and no initial st
54
88
  olDraw.remove()
55
89
  })
56
90
 
57
- test('map style changes are forwarded to the manager', () => {
58
- const { eventBus, manager } = setup()
91
+ test('map style changes are forwarded to the manager and re-resolve point symbols', () => {
92
+ const { eventBus, manager, mapProvider } = setup()
59
93
  eventBus.emit(events.MAP_SET_STYLE, { id: 'dark' })
60
94
  expect(manager.setMapStyle).toHaveBeenCalledWith({ id: 'dark' })
95
+ expect(refreshAllPointSymbols).toHaveBeenCalledWith({ manager, mapProvider })
96
+ })
97
+
98
+ test('emits MAP_DATA_CHANGE once point symbols have actually finished re-resolving after a style change', async () => {
99
+ const { eventBus } = setup()
100
+ eventBus.emit(events.MAP_SET_STYLE, { id: 'dark' })
101
+ await Promise.resolve()
102
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_DATA_CHANGE)
61
103
  })
62
104
 
63
105
  test('remove unsubscribes, destroys the manager and clears mapProvider.draw', () => {
@@ -0,0 +1,156 @@
1
+ import Draw from 'ol/interaction/Draw.js'
2
+ import { noModifierKeys } from 'ol/events/condition.js'
3
+ import { wireInputEvents, applyCrossHairVisibility } from '../draw/drawInput.js'
4
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
5
+ import { validatePlacement } from '../../../validation/validateGeometry.js'
6
+ import { TOLERANCES } from '../defaults.js'
7
+
8
+ const MODE = 'draw_point'
9
+
10
+ // A point has no ring/rubber-band to build a candidate from — just the coordinate about to
11
+ // be placed. Mirrors validateGeometry.js's attemptPlacement, but that helper hardcodes
12
+ // Polygon/LineString via MODE_BY_GEOMETRY, so a Point candidate is built and validated
13
+ // directly (same approach as the MapLibre adapter's drawPointMode.js).
14
+ const canPlaceVertex = (manager) => (coordinate) => {
15
+ const candidate = { type: 'Feature', geometry: { type: 'Point', coordinates: coordinate }, properties: {} }
16
+ const { valid, reason } = validatePlacement(
17
+ candidate,
18
+ { mode: MODE, vertexIndex: 0 },
19
+ { onGeometryChange: manager._geometryValidator }
20
+ )
21
+ if (!valid) {
22
+ manager.emit(ADAPTER_EVENTS.PLACEMENT_BLOCKED, { feature: candidate, reason: reason ?? null, phase: 'place', mode: MODE, vertexIndex: 0 })
23
+ }
24
+ return valid
25
+ }
26
+
27
+ // Commit a finished point to the store under the requested id and emit CREATE — mirrors
28
+ // draw/DrawMode.js's finalizeDrawnFeature (identical shape, kept separate rather than
29
+ // imported since draw/DrawMode.js isn't otherwise a shared dependency of this mode).
30
+ // Symbol resolution runs after the feature is in the store (not before), matching the
31
+ // MapLibre adapter's own ordering. The resolver itself is injected by OLDrawManager via
32
+ // changeMode's options (mirrors the ML adapter's state.resolvePointSymbol convention — see
33
+ // MaplibreDrawAdapter.js's changeMode) so this mode has no direct dependency on
34
+ // symbolRegistry/mapProvider/pointSymbolImages.js.
35
+ const finalizeDrawnFeature = (manager, resolvePointSymbol, olFeature, featureId, properties) => {
36
+ olFeature.setId(String(featureId))
37
+ olFeature.setProperties(properties)
38
+ manager.store.source.addFeature(olFeature)
39
+ manager.emit(ADAPTER_EVENTS.CREATE, manager.store.toGeoJSON(olFeature))
40
+ resolvePointSymbol?.(olFeature)
41
+ }
42
+
43
+ /**
44
+ * Touch/keyboard/"Add point" input for draw_point — the same concerns as
45
+ * draw/drawInput.js (crosshair, interface-type tracking, add-vertex-button/Enter), reused
46
+ * directly, but with a much smaller placement step: a point has no rubber band to update
47
+ * while positioning (the crosshair itself already shows where it'll land) and nothing to
48
+ * undo before a single-click commit, so those callbacks are no-ops here.
49
+ */
50
+ const buildPointInput = ({ drawInteraction, options, canPlace }) => {
51
+ const { container, addVertexButtonId, mapProvider, snap, crossHair } = options
52
+ let interfaceType = options.interfaceType ?? 'mouse'
53
+ const getInterfaceType = () => interfaceType
54
+
55
+ // appendCoordinates() only builds up LineString/Polygon rings — for a Point-type Draw
56
+ // interaction it does nothing beyond starting the sketch (confirmed by reading OL's own
57
+ // source), so finishDrawing() has to be called explicitly right after to actually commit.
58
+ const placePoint = () => {
59
+ const raw = mapProvider.getCenter()
60
+ const coord = (getInterfaceType() !== 'mouse' && snap) ? snap.apply(raw) : raw
61
+ snap?.hideIndicator()
62
+ if (!canPlace(coord)) { return }
63
+ drawInteraction.appendCoordinates([coord])
64
+ drawInteraction.finishDrawing()
65
+ }
66
+
67
+ // Keeps the snap indicator visible at the crosshair while positioning via touch/keyboard
68
+ // (mouse hover is handled by the separate OL snap interaction already attached by
69
+ // OLDrawManager) — the closest point-shaped equivalent of updateRubberbanding.
70
+ const updateSnapIndicator = () => {
71
+ if (getInterfaceType() !== 'mouse' && snap) { snap.apply(mapProvider.getCenter()) }
72
+ }
73
+
74
+ // Mirrors draw/drawInput.js's createDrawInput: an interface-type switch to touch/keyboard
75
+ // must refresh the snap indicator immediately at the crosshair, not wait for the next
76
+ // pointermove/change:center — otherwise it stays hidden until the user first pans or drags
77
+ // after entering the mode (the same gap the MapLibre adapter's onInterfaceTypeChange already
78
+ // guards against by calling onMove(state) right after _setInterface).
79
+ const applyInterfaceType = (type) => {
80
+ interfaceType = type
81
+ applyCrossHairVisibility(crossHair, type)
82
+ updateSnapIndicator()
83
+ }
84
+
85
+ const events = wireInputEvents({
86
+ container,
87
+ addVertexButtonId,
88
+ olView: drawInteraction.getMap()?.getView(),
89
+ onUndo: null,
90
+ getInterfaceType,
91
+ setInterfaceType: applyInterfaceType,
92
+ clearLastCoord: () => {},
93
+ updateRubberbanding: updateSnapIndicator,
94
+ placeVertex: placePoint
95
+ })
96
+
97
+ applyCrossHairVisibility(crossHair, interfaceType)
98
+
99
+ return {
100
+ getInterfaceType,
101
+ setInterfaceType: applyInterfaceType,
102
+ destroy () { events.destroy() }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Draw mode for placing a single point: mouse click, or crosshair + Enter/"Add point"
108
+ * button for touch/keyboard. Commits immediately on the first placement — unlike
109
+ * draw_line/draw_polygon, there is no rubber band, no multi-vertex undo, and no "Done"
110
+ * step (mirrors the MapLibre adapter's drawPointMode.js decision).
111
+ *
112
+ * OL's own Draw({type:'Point'}) interaction already finishes a real mouse click
113
+ * immediately (confirmed by reading its handleUpEvent — mode 'Point' always attempts
114
+ * finishDrawing() on pointer-up, no second click needed), so the mouse path needs no
115
+ * extra wiring beyond the placement-veto condition every mode already has. Deliberately
116
+ * NOT built via draw/DrawMode.js's createDrawMode()/createVertexPlacement() — those are
117
+ * parametrised around a ring of placed vertices + a trailing rubber-band coordinate,
118
+ * which a single-click commit doesn't have (mirrors the same decision already made for
119
+ * the MapLibre adapter, and the OL EditMode/DrawMode split noted in
120
+ * feedback_adapter_parity.md: don't force point through machinery shaped for lines/polygons
121
+ * just because they happen to share a file).
122
+ */
123
+ export const createDrawPointMode = ({ map, manager, options }) => {
124
+ const { featureId, properties = {} } = options
125
+ const canPlace = canPlaceVertex(manager)
126
+
127
+ const drawInteraction = new Draw({
128
+ type: 'Point',
129
+ // Without an explicit style, OL renders its own default sketch style (a blue circle)
130
+ // following the cursor before commit — core/styles.js's createSketchStyle() already
131
+ // suppresses this same default for the "ghost" Point sketch OL renders alongside a
132
+ // Polygon/LineString sketch (see its own comment); a real Point sketch needs the same
133
+ // treatment, since it commits on the very first click with nothing meaningful to preview.
134
+ style: () => [],
135
+ snapTolerance: TOLERANCES.snapRadius,
136
+ condition: (e) => noModifierKeys(e) && canPlace(e.coordinate)
137
+ })
138
+ map.addInteraction(drawInteraction)
139
+
140
+ drawInteraction.on('drawend', (e) => finalizeDrawnFeature(manager, options.resolvePointSymbol, e.feature, featureId, properties))
141
+ drawInteraction.on('drawabort', () => { manager.emit(ADAPTER_EVENTS.CANCEL) })
142
+
143
+ const input = buildPointInput({ drawInteraction, options, canPlace })
144
+
145
+ return {
146
+ done () {}, // no-op: commit is immediate, there's no pending sketch to finish
147
+ cancel () { drawInteraction.abortDrawing() },
148
+ undo () {}, // no-op: nothing placed yet to undo before commit
149
+ setInterfaceType (type) { input.setInterfaceType(type) },
150
+ destroy () {
151
+ manager.emit(ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE, { interfaceType: input.getInterfaceType() })
152
+ input.destroy()
153
+ map.removeInteraction(drawInteraction)
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,246 @@
1
+ import { createDrawPointMode } from './drawPointMode.js'
2
+ import { wireInputEvents, applyCrossHairVisibility } from '../draw/drawInput.js'
3
+ import { createFeatureStore } from '../core/featureStore.js'
4
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
5
+ import { createFakeMap, createFakeManager, pointFeature } from '../__helpers__/harness.js'
6
+
7
+ jest.mock('../draw/drawInput.js', () => ({
8
+ wireInputEvents: jest.fn(() => ({ destroy: jest.fn() })),
9
+ applyCrossHairVisibility: jest.fn()
10
+ }))
11
+
12
+ // Matches DrawMode.test.js's own convention: real OL interactions are used, but drawstart/
13
+ // drawend/drawabort are dispatched as plain event objects directly (rather than exercised
14
+ // through the real appendCoordinates/startDrawing_/finishDrawing internals, which need a
15
+ // fully-featured OL map — getMap().getView().getProjection() — well beyond what the
16
+ // lightweight fake map here provides or needs to).
17
+ const setup = (options = {}) => {
18
+ const map = createFakeMap()
19
+ const manager = createFakeManager()
20
+ manager.store = createFeatureStore()
21
+ const emitted = () => manager.emit.mock.calls.map(([type, payload]) => ({ type, payload }))
22
+ const mode = createDrawPointMode({
23
+ map,
24
+ manager,
25
+ options: {
26
+ featureId: 'point-1',
27
+ properties: { symbol: 'pin' },
28
+ container: null,
29
+ crossHair: null,
30
+ snap: null,
31
+ mapProvider: { getCenter: () => [5, 5] },
32
+ ...options
33
+ }
34
+ })
35
+ const interaction = map.interactions[0]
36
+ const wireArgs = wireInputEvents.mock.calls.at(-1)[0]
37
+ return { map, manager, emitted, mode, interaction, wireArgs }
38
+ }
39
+
40
+ afterEach(() => jest.clearAllMocks())
41
+
42
+ describe('createDrawPointMode', () => {
43
+ test('adds a Point-type Draw interaction to the map', () => {
44
+ const { interaction } = setup()
45
+ expect(interaction).toBeDefined()
46
+ })
47
+
48
+ test('suppresses OL\'s default sketch style — no blue cursor-following circle', () => {
49
+ const { interaction } = setup()
50
+ // OL stores the resolved style on the sketch overlay layer, not on the interaction
51
+ // itself — this is what OL actually calls per frame to render the sketch/hover point.
52
+ expect(interaction.overlay_.getStyle()()).toEqual([])
53
+ })
54
+
55
+ test('drawend commits the sketch under the requested id and fires CREATE', () => {
56
+ const { interaction, manager, emitted } = setup()
57
+ interaction.dispatchEvent({ type: 'drawend', feature: pointFeature([1, 2], 'ol-generated-id') })
58
+
59
+ const created = emitted().filter((e) => e.type === ADAPTER_EVENTS.CREATE)
60
+ expect(created).toHaveLength(1)
61
+ expect(created[0].payload.id).toBe('point-1') // re-id'd, not the OL-generated one
62
+ expect(created[0].payload.geometry.coordinates).toEqual([1, 2])
63
+ expect(created[0].payload.properties).toEqual({ symbol: 'pin' })
64
+ expect(manager.store.get('point-1')).toBeTruthy()
65
+ })
66
+
67
+ test('calls the adapter-injected resolvePointSymbol hook with the committed OL feature, after re-id', () => {
68
+ const resolvePointSymbol = jest.fn()
69
+ const { interaction } = setup({ resolvePointSymbol })
70
+ interaction.dispatchEvent({ type: 'drawend', feature: pointFeature([1, 2], 'ol-generated-id') })
71
+
72
+ expect(resolvePointSymbol).toHaveBeenCalledTimes(1)
73
+ const [olFeature] = resolvePointSymbol.mock.calls[0]
74
+ expect(olFeature.getId()).toBe('point-1')
75
+ })
76
+
77
+ test('does not throw when no resolvePointSymbol hook was supplied', () => {
78
+ const { interaction } = setup()
79
+ expect(() => interaction.dispatchEvent({ type: 'drawend', feature: pointFeature([1, 2], 'ol-generated-id') })).not.toThrow()
80
+ })
81
+
82
+ test('defaults properties to {} when none are supplied', () => {
83
+ const { interaction, manager, emitted } = setup({ properties: undefined })
84
+ interaction.dispatchEvent({ type: 'drawend', feature: pointFeature([1, 2], 'ol-generated-id') })
85
+ const created = emitted().filter((e) => e.type === ADAPTER_EVENTS.CREATE)
86
+ // OL's GeoJSON writer reports no properties as null (not {}) — the meaningful
87
+ // assertion is what setProperties() was actually called with, on the stored feature.
88
+ expect(created[0].payload.properties).toBeNull()
89
+ const { geometry, ...nonGeometryProps } = manager.store.getOL('point-1').getProperties()
90
+ expect(nonGeometryProps).toEqual({})
91
+ })
92
+
93
+ test('condition vetoes placement via onGeometryChange', () => {
94
+ const { interaction, manager, emitted } = setup()
95
+ manager._geometryValidator = () => ({ valid: false, reason: 'not allowed here' })
96
+ const condition = interaction.condition_
97
+ const allowed = condition({ coordinate: [1, 2], originalEvent: {} })
98
+ expect(allowed).toBe(false)
99
+ const blocked = emitted().filter((e) => e.type === ADAPTER_EVENTS.PLACEMENT_BLOCKED)
100
+ expect(blocked).toHaveLength(1)
101
+ expect(blocked[0].payload).toMatchObject({ reason: 'not allowed here', phase: 'place', mode: 'draw_point' })
102
+ })
103
+
104
+ test('condition allows an unvetoed placement', () => {
105
+ const { interaction } = setup()
106
+ const condition = interaction.condition_
107
+ expect(condition({ coordinate: [1, 2], originalEvent: {} })).toBe(true)
108
+ })
109
+
110
+ test('condition blocks modifier-key clicks', () => {
111
+ const { interaction } = setup()
112
+ const condition = interaction.condition_
113
+ expect(condition({ coordinate: [1, 2], originalEvent: { shiftKey: true } })).toBe(false)
114
+ })
115
+
116
+ test('drawabort emits CANCEL', () => {
117
+ const { interaction, emitted } = setup()
118
+ interaction.dispatchEvent({ type: 'drawabort' })
119
+ expect(emitted().some((e) => e.type === ADAPTER_EVENTS.CANCEL)).toBe(true)
120
+ })
121
+
122
+ describe('touch/keyboard placement (via the wired placeVertex callback)', () => {
123
+ // appendCoordinates()/finishDrawing() are OL-internal machinery already exercised by
124
+ // OL's own test suite — what this mode owns is calling them with the right (possibly
125
+ // snapped) coordinate, gated by the same placement veto the mouse path uses.
126
+ const spyOnCommit = (interaction) => ({
127
+ append: jest.spyOn(interaction, 'appendCoordinates').mockImplementation(() => {}),
128
+ finish: jest.spyOn(interaction, 'finishDrawing').mockImplementation(() => {})
129
+ })
130
+
131
+ test('places at the (possibly snapped) map centre and hides the indicator', () => {
132
+ const snap = { apply: jest.fn((c) => [c[0] + 1, c[1] + 1]), hideIndicator: jest.fn() }
133
+ const mapProvider = { getCenter: () => [5, 5] }
134
+ const { wireArgs, interaction } = setup({ snap, mapProvider, interfaceType: 'touch' })
135
+ const { append, finish } = spyOnCommit(interaction)
136
+
137
+ wireArgs.placeVertex()
138
+
139
+ expect(snap.apply).toHaveBeenCalledWith([5, 5])
140
+ expect(snap.hideIndicator).toHaveBeenCalled()
141
+ expect(append).toHaveBeenCalledWith([[6, 6]])
142
+ expect(finish).toHaveBeenCalled()
143
+ })
144
+
145
+ test('does not snap on mouse interface', () => {
146
+ const snap = { apply: jest.fn(), hideIndicator: jest.fn() }
147
+ const { wireArgs, interaction } = setup({ snap, interfaceType: 'mouse' })
148
+ const { append } = spyOnCommit(interaction)
149
+ wireArgs.placeVertex()
150
+ expect(snap.apply).not.toHaveBeenCalled()
151
+ expect(append).toHaveBeenCalledWith([[5, 5]])
152
+ })
153
+
154
+ test('a vetoed crosshair placement never commits', () => {
155
+ const { wireArgs, manager, emitted, interaction } = setup()
156
+ const { append, finish } = spyOnCommit(interaction)
157
+ manager._geometryValidator = () => false
158
+ wireArgs.placeVertex()
159
+ expect(append).not.toHaveBeenCalled()
160
+ expect(finish).not.toHaveBeenCalled()
161
+ expect(emitted().filter((e) => e.type === ADAPTER_EVENTS.PLACEMENT_BLOCKED)).toHaveLength(1)
162
+ })
163
+
164
+ test('updateRubberbanding shows the snap indicator at the crosshair for touch/keyboard, not mouse', () => {
165
+ const snap = { apply: jest.fn(), hideIndicator: jest.fn() }
166
+ const mapProvider = { getCenter: () => [7, 7] }
167
+ const { wireArgs } = setup({ snap, mapProvider, interfaceType: 'keyboard' })
168
+ wireArgs.updateRubberbanding()
169
+ expect(snap.apply).toHaveBeenCalledWith([7, 7])
170
+ })
171
+
172
+ test('updateRubberbanding does nothing on mouse interface', () => {
173
+ const snap = { apply: jest.fn(), hideIndicator: jest.fn() }
174
+ const { wireArgs } = setup({ snap, interfaceType: 'mouse' })
175
+ wireArgs.updateRubberbanding()
176
+ expect(snap.apply).not.toHaveBeenCalled()
177
+ })
178
+
179
+ // Regression: without this, switching to touch/keyboard left the indicator hidden until
180
+ // the next pointermove/change:center — mirrors draw/drawInput.js's createDrawInput and
181
+ // the MapLibre adapter's onInterfaceTypeChange (which calls onMove(state) for the same
182
+ // reason).
183
+ test('switching interface type via the wired setInterfaceType immediately refreshes the snap indicator at the crosshair', () => {
184
+ const snap = { apply: jest.fn(), hideIndicator: jest.fn() }
185
+ const mapProvider = { getCenter: () => [9, 9] }
186
+ const { wireArgs } = setup({ snap, mapProvider, interfaceType: 'mouse' })
187
+ wireArgs.setInterfaceType('touch')
188
+ expect(snap.apply).toHaveBeenCalledWith([9, 9])
189
+ })
190
+
191
+ test('switching interface type via the wired setInterfaceType does not query snap for mouse', () => {
192
+ const snap = { apply: jest.fn(), hideIndicator: jest.fn() }
193
+ const { wireArgs } = setup({ snap, interfaceType: 'touch' })
194
+ wireArgs.setInterfaceType('mouse')
195
+ expect(snap.apply).not.toHaveBeenCalled()
196
+ })
197
+
198
+ test('clearLastCoord is a harmless no-op (points have no lastPlacedCoord bookkeeping)', () => {
199
+ const { wireArgs } = setup()
200
+ expect(() => wireArgs.clearLastCoord()).not.toThrow()
201
+ })
202
+ })
203
+
204
+ test('applies crosshair visibility for the initial interface type on setup', () => {
205
+ setup({ interfaceType: 'touch' })
206
+ expect(applyCrossHairVisibility).toHaveBeenCalledWith(null, 'touch')
207
+ })
208
+
209
+ describe('mode API', () => {
210
+ test('done/undo are no-ops — a single click has nothing pending or to undo', () => {
211
+ const { mode } = setup()
212
+ expect(() => mode.done()).not.toThrow()
213
+ expect(() => mode.undo()).not.toThrow()
214
+ })
215
+
216
+ test('cancel aborts the in-progress sketch', () => {
217
+ const { mode, interaction } = setup()
218
+ const abortSpy = jest.spyOn(interaction, 'abortDrawing').mockImplementation(() => {})
219
+ mode.cancel()
220
+ expect(abortSpy).toHaveBeenCalled()
221
+ })
222
+
223
+ test('setInterfaceType delegates to applyCrossHairVisibility', () => {
224
+ const { mode } = setup()
225
+ applyCrossHairVisibility.mockClear()
226
+ mode.setInterfaceType('touch')
227
+ expect(applyCrossHairVisibility).toHaveBeenCalledWith(null, 'touch')
228
+ })
229
+
230
+ test('setInterfaceType also refreshes the snap indicator at the crosshair for touch/keyboard', () => {
231
+ const snap = { apply: jest.fn(), hideIndicator: jest.fn() }
232
+ const mapProvider = { getCenter: () => [3, 4] }
233
+ const { mode } = setup({ snap, mapProvider })
234
+ mode.setInterfaceType('keyboard')
235
+ expect(snap.apply).toHaveBeenCalledWith([3, 4])
236
+ })
237
+
238
+ test('destroy emits the final interface type, tears down input and removes the interaction', () => {
239
+ const { mode, map, interaction, emitted } = setup()
240
+ const removeSpy = jest.spyOn(map, 'removeInteraction')
241
+ mode.destroy()
242
+ expect(emitted().some((e) => e.type === ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE)).toBe(true)
243
+ expect(removeSpy).toHaveBeenCalledWith(interaction)
244
+ })
245
+ })
246
+ })