@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
@@ -77,4 +77,15 @@ describe('createModifyInteraction', () => {
77
77
  modify.destroy()
78
78
  expect(map.removeInteraction).toHaveBeenCalledWith(interaction)
79
79
  })
80
+
81
+ // point/editPointMode.js injects its own condition (built around map.forEachFeatureAtPixel)
82
+ // in place of buildModifyCondition's fixed-radius hit-test, since a symbol icon can render
83
+ // far bigger than PIXEL_TOLERANCE covers.
84
+ test('an injected condition overrides buildModifyCondition\'s own default', () => {
85
+ const map = createFakeMap()
86
+ const state = { interfaceType: 'mouse', vertices: [[0, 0]], midpoints: [] }
87
+ const customCondition = jest.fn(() => true)
88
+ createModifyInteraction({ map, olFeature: polygonFeature(RING), getState: () => state, onModifyEnd: jest.fn(), condition: customCondition })
89
+ expect(map.interactions[0].condition_).toBe(customCondition)
90
+ })
80
91
  })
@@ -30,9 +30,12 @@ export const resolveSnappedCoord = (snap, map, current, nudgedCoord, snappedCoor
30
30
  * `keyMove` tracks the coordinate at the start of a nudge sequence so a single
31
31
  * move_vertex undo op can be pushed on keyup (see keyboardHandler).
32
32
  *
33
+ * @param {(olFeature, index: number, coord: number[]) => void} [options.moveCoord] - coordinate
34
+ * writer, defaulting to moveVertex; point/editPointMode.js injects point/pointOps.js's
35
+ * movePoint instead, since a Point has no ring index to address.
33
36
  * @returns {{ nudge: (e: KeyboardEvent) => void, keyMove: { start, index }, nudgeByDelta: (dx: number, dy: number, isLargeStep: boolean) => void }}
34
37
  */
35
- export const wireNudge = ({ map, snap, getState, setState, onInserted, onVertexMoved }) => {
38
+ export const wireNudge = ({ map, snap, getState, setState, onInserted, onVertexMoved, moveCoord = moveVertex }) => {
36
39
  const keyMove = { start: null, index: null }
37
40
 
38
41
  // Shared core: moves the selected vertex by an already pixel-scaled delta,
@@ -46,9 +49,13 @@ export const wireNudge = ({ map, snap, getState, setState, onInserted, onVertexM
46
49
  const current = vertices[selectedVertexIndex]
47
50
  const nudgedCoord = nudgeCoord(map, current, dx, dy)
48
51
  const snappedCoord = snap ? snap.apply(nudgedCoord) : nudgedCoord
49
- snap?.hideIndicator()
50
52
  const newCoord = resolveSnappedCoord(snap, map, current, nudgedCoord, snappedCoord, dx, dy)
51
- moveVertex(olFeature, selectedVertexIndex, newCoord)
53
+ // resolveSnappedCoord can override snappedCoord with an escape jump clear of the snap
54
+ // radius — when it does, the indicator snap.apply() just showed is now stale, so correct it.
55
+ if (snap && newCoord !== snappedCoord) {
56
+ snap.hideIndicator()
57
+ }
58
+ moveCoord(olFeature, selectedVertexIndex, newCoord)
52
59
  setState({ vertices: vertices.map((c, i) => i === selectedVertexIndex ? newCoord : c) })
53
60
  return { previousCoord: current, vertexIndex: selectedVertexIndex }
54
61
  }
@@ -99,12 +106,12 @@ export const wireNudge = ({ map, snap, getState, setState, onInserted, onVertexM
99
106
  moveSelectedVertexBy(dx, dy)
100
107
  }
101
108
 
102
- // Explicit-delta counterpart to nudge(e) — the entry point for MoveControl's
109
+ // Explicit-delta counterpart to nudge(e) — the entry point for MoveControls'
103
110
  // D-pad (see mapProvider.activeMoveTarget in events.js). Unlike nudge(e)'s
104
111
  // held-key sequencing (undo pushed on keyup via keyMove, so repeated keydowns
105
112
  // while held batch into one undo step), each call here is one complete,
106
113
  // undoable action — a button click has no "held" state to batch. Midpoints
107
- // aren't handled here: MoveControl only claims the D-pad once a real vertex is
114
+ // aren't handled here: MoveControls only claims the D-pad once a real vertex is
108
115
  // selected (see buildVertexMoveTarget in events.js).
109
116
  const nudgeByDelta = (dx, dy, isLargeStep) => {
110
117
  const step = isLargeStep ? KEYBOARD.stepAmount : KEYBOARD.nudgeAmount
@@ -53,7 +53,7 @@ describe('nudging a vertex', () => {
53
53
  })
54
54
  })
55
55
 
56
- describe('nudgeByDelta (MoveControl D-pad)', () => {
56
+ describe('nudgeByDelta (MoveControls D-pad)', () => {
57
57
  test('moves the selected vertex by an explicit direction, scaled by isLargeStep, and reports the move for undo', () => {
58
58
  const { state, nudgeByDelta, olFeature, onVertexMoved } = setup()
59
59
  Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
@@ -79,7 +79,7 @@ describe('nudgeByDelta (MoveControl D-pad)', () => {
79
79
  const { state, nudgeByDelta, onVertexMoved } = setup()
80
80
  nudgeByDelta(1, 0, true) // nothing selected
81
81
  Object.assign(state, { selectedVertexIndex: 4, selectedVertexType: 'midpoint' })
82
- nudgeByDelta(1, 0, true) // midpoint selected — MoveControl only claims a real vertex
82
+ nudgeByDelta(1, 0, true) // midpoint selected — MoveControls only claims a real vertex
83
83
  state.olFeature = null
84
84
  Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
85
85
  nudgeByDelta(1, 0, true)
@@ -134,7 +134,17 @@ describe('snapping while nudging', () => {
134
134
  Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
135
135
  nudge({ key: 'ArrowRight' })
136
136
  expect(ring(olFeature)[1]).toEqual([16, 0])
137
- expect(snap.hideIndicator).toHaveBeenCalled()
137
+ })
138
+
139
+ // Regression: a stray hideIndicator() right after snap.apply() undid its own show on every
140
+ // single nudge, so the indicator could never actually be seen. keyboardHandler.js's own keyup
141
+ // no longer hides it either — see its own comment — so once shown here, it stays shown.
142
+ test('does not hide the indicator mid-sequence — snap.apply() already shows/hides it', () => {
143
+ const snap = snapReturning((c) => [c[0] + 1, c[1]])
144
+ const { state, nudge } = setup({ snap })
145
+ Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
146
+ nudge({ key: 'ArrowRight' })
147
+ expect(snap.hideIndicator).not.toHaveBeenCalled()
138
148
  })
139
149
 
140
150
  test('escapes the snap radius when snapping blocks the nudge', () => {
@@ -144,6 +154,19 @@ describe('snapping while nudging', () => {
144
154
  nudge({ key: 'ArrowRight' })
145
155
  expect(ring(olFeature)[1]).toEqual([23, 0]) // snapRadius + 1 past the original position
146
156
  })
157
+
158
+ // Regression: nudging away from a target you're sitting on escapes the snap radius (above),
159
+ // but snap.apply() had already shown the indicator at the target it's about to flee — left
160
+ // uncorrected, the indicator stayed pinned there for one extra press even though the vertex
161
+ // had already jumped clear, only catching up on the next press once querying from the
162
+ // escaped position genuinely found nothing.
163
+ test('hides the indicator when a nudge escapes the snap radius', () => {
164
+ const snap = snapReturning(() => [10, 0]) // snap holds the vertex in place
165
+ const { state, nudge } = setup({ snap })
166
+ Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
167
+ nudge({ key: 'ArrowRight' })
168
+ expect(snap.hideIndicator).toHaveBeenCalled()
169
+ })
147
170
  })
148
171
 
149
172
  describe('resolveSnappedCoord', () => {
@@ -30,21 +30,22 @@ const handleTouchStart = (e, { map, targetEl, cssToOl, getState, drag }) => {
30
30
  e.preventDefault()
31
31
  }
32
32
 
33
- const handleTouchMove = (e, { map, targetEl, olToCSS, getState, setState, snap, drag }) => {
33
+ const handleTouchMove = (e, { map, targetEl, olToCSS, getState, setState, snap, drag, moveCoord }) => {
34
34
  if (!isOnTouchTarget(e.target) || drag.dragStartIndex == null) {
35
35
  return
36
36
  }
37
37
  e.preventDefault()
38
38
  const tOl = map.getEventPixel({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY })
39
39
  const rawCoord = pixelToCoord(map, { x: tOl[0] - drag.vertexTouchDelta.x, y: tOl[1] - drag.vertexTouchDelta.y })
40
+ // snap.apply() already shows/hides the indicator based on whether a candidate was found.
40
41
  const newCoord = snap ? snap.apply(rawCoord) : rawCoord
41
- snap?.hideIndicator()
42
42
  const { olFeature, vertices } = getState()
43
43
  if (!olFeature) {
44
44
  return
45
45
  }
46
- moveVertex(olFeature, drag.dragStartIndex, newCoord)
46
+ moveCoord(olFeature, drag.dragStartIndex, newCoord)
47
47
  setState({ vertices: vertices.map((c, i) => i === drag.dragStartIndex ? newCoord : c) })
48
+ // Tracks the raw touch point, not newCoord — the vertex is what jumps to the snap candidate.
48
49
  showTouchTarget(targetEl, olToCSS({ x: tOl[0] - drag.targetTouchDelta.x, y: tOl[1] - drag.targetTouchDelta.y }))
49
50
  }
50
51
 
@@ -63,7 +64,7 @@ const handleTap = (e, { map, getState, onTap, drag }) => {
63
64
  }
64
65
 
65
66
  const handleTouchEnd = (e, ctx) => {
66
- const { getState, onVertexMoved, snap, drag } = ctx
67
+ const { getState, onVertexMoved, drag, updateTargetPosition } = ctx
67
68
  if (drag.dragStartIndex == null) {
68
69
  handleTap(e, ctx)
69
70
  drag.tapStart = null
@@ -74,8 +75,9 @@ const handleTouchEnd = (e, ctx) => {
74
75
  if (vertices[drag.dragStartIndex] && drag.dragStartCoord) {
75
76
  onVertexMoved({ vertexIndex: drag.dragStartIndex, previousCoord: drag.dragStartCoord })
76
77
  }
77
- snap?.hideIndicator()
78
+ // Not hiding the indicator here — it should stay showing after the drag ends.
78
79
  drag.dragStartCoord = null; drag.dragStartIndex = null; drag.vertexTouchDelta = null; drag.targetTouchDelta = null
80
+ updateTargetPosition() // re-sync the target to the vertex's final position
79
81
  e.preventDefault()
80
82
  }
81
83
 
@@ -97,6 +99,8 @@ const wireTouchEvents = (deps) => {
97
99
  container.addEventListener('touchstart', onTouchstart, { passive: false })
98
100
  container.addEventListener('touchmove', onTouchmove, { passive: false })
99
101
  container.addEventListener('touchend', onTouchend, { passive: false })
102
+ // touchcancel fires instead of touchend when the browser itself interrupts the gesture.
103
+ container.addEventListener('touchcancel', onTouchend, { passive: false })
100
104
 
101
105
  return {
102
106
  isDragging: () => drag.dragStartIndex != null,
@@ -104,6 +108,7 @@ const wireTouchEvents = (deps) => {
104
108
  container.removeEventListener('touchstart', onTouchstart)
105
109
  container.removeEventListener('touchmove', onTouchmove)
106
110
  container.removeEventListener('touchend', onTouchend)
111
+ container.removeEventListener('touchcancel', onTouchend)
107
112
  }
108
113
  }
109
114
  }
@@ -114,15 +119,16 @@ const wireTouchEvents = (deps) => {
114
119
  * without finger occlusion. Tap on a vertex or midpoint selects it via onTap.
115
120
  *
116
121
  * @param {{ map, container, getState, setState, onVertexMoved, onTap, colors }} options
122
+ * @param {(olFeature, index: number, coord: number[]) => void} [options.moveCoord] - coordinate
123
+ * writer, defaulting to moveVertex; point/editPointMode.js injects point/pointOps.js's
124
+ * movePoint instead, since a Point has no ring index to address.
117
125
  * @returns {{ updateTargetPosition, updateColors, hide, destroy }}
118
126
  */
119
- export const createTouchHandler = ({ map, container, getState, setState, onVertexMoved, onTap, colors, snap }) => {
127
+ export const createTouchHandler = ({ map, container, getState, setState, onVertexMoved, onTap, colors, snap, moveCoord = moveVertex }) => {
120
128
  const targetEl = createTouchTarget(container)
121
129
  applyTouchTargetColors(targetEl, colors)
122
130
 
123
- // OL pixel space is relative to ol-viewport at its pre-scale CSS size.
124
- // Container CSS space is larger when a CSS transform scales up ol-viewport
125
- // (e.g. scale(1.5) at medium map size makes OL pixels 1.5× smaller than CSS pixels).
131
+ // Converts between OL pixel space and CSS space, which differ when a CSS transform scales up ol-viewport.
126
132
  const cssTx = { scale: 1, ox: 0, oy: 0 }
127
133
  const olToCSS = (p) => ({ x: p.x * cssTx.scale + cssTx.ox, y: p.y * cssTx.scale + cssTx.oy })
128
134
  const cssToOl = (p) => ({ x: (p.x - cssTx.ox) / cssTx.scale, y: (p.y - cssTx.oy) / cssTx.scale })
@@ -140,8 +146,6 @@ export const createTouchHandler = ({ map, container, getState, setState, onVerte
140
146
  })
141
147
  }
142
148
 
143
- const touchEvents = wireTouchEvents({ container, map, targetEl, olToCSS, cssToOl, getState, setState, onVertexMoved, onTap, snap })
144
-
145
149
  const updateTargetPosition = () => {
146
150
  const { selectedVertexIndex, vertices, interfaceType } = getState()
147
151
  if (selectedVertexIndex < 0 || !vertices[selectedVertexIndex] || interfaceType !== 'touch') {
@@ -156,8 +160,9 @@ export const createTouchHandler = ({ map, container, getState, setState, onVerte
156
160
  showTouchTarget(targetEl, olToCSS(px))
157
161
  }
158
162
 
159
- // Reposition on every render keeps target anchored during pinch-zoom and pan.
160
- // Skipped during drag since touchmove handles position directly.
163
+ const touchEvents = wireTouchEvents({ container, map, targetEl, olToCSS, cssToOl, getState, setState, onVertexMoved, onTap, snap, moveCoord, updateTargetPosition })
164
+
165
+ // Reposition on every render (skipped mid-drag, which handles position directly) to stay anchored during pinch-zoom and pan.
161
166
  const onPostrender = () => {
162
167
  const { selectedVertexIndex, interfaceType } = getState()
163
168
  if (selectedVertexIndex >= 0 && !touchEvents.isDragging() && interfaceType === 'touch') {
@@ -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
+ }