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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/button-definition.md +7 -8
  7. package/docs/api/control-definition.md +17 -1
  8. package/docs/api/panel-definition.md +5 -0
  9. package/docs/api/slots.md +50 -6
  10. package/docs/assets/images/slot-map.svg +61 -262
  11. package/docs/examples/add-polygons.mdx +8 -2
  12. package/docs/examples/add-symbols.mdx +8 -2
  13. package/docs/examples/draw-tools.mdx +3 -3
  14. package/docs/plugins/datasets.md +23 -15
  15. package/docs/plugins/draw.md +79 -16
  16. package/docs/plugins/map-key.md +147 -0
  17. package/docs/plugins/search.md +1 -1
  18. package/docs/plugins.md +5 -1
  19. package/govuk-prototype-kit.config.json +2 -0
  20. package/jest.setup.js +5 -0
  21. package/package.json +9 -1
  22. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  23. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  24. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  25. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  26. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  27. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  28. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  29. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  30. package/plugins/beta/use-location/src/manifest.js +1 -1
  31. package/plugins/datasets/dist/css/index.css +1 -1
  32. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  33. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  34. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  35. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  36. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  37. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  38. package/plugins/datasets/dist/umd/index.js +1 -1
  39. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  40. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  41. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  42. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  43. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  44. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  45. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  46. package/plugins/datasets/src/datasets.scss +0 -1
  47. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  48. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  49. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  50. package/plugins/datasets/src/index.test.js +36 -0
  51. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  52. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  53. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  54. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  55. package/plugins/datasets/src/manifest.js +0 -38
  56. package/plugins/datasets/src/manifest.test.js +49 -0
  57. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  58. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  59. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  60. package/plugins/datasets/src/registry/dataset.js +13 -1
  61. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  62. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  63. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  64. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  65. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  66. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  67. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  68. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  69. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  70. package/plugins/draw/dist/esm/index.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  72. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  73. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  74. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  75. package/plugins/draw/dist/umd/index.js +1 -1
  76. package/plugins/draw/src/DrawInit.jsx +10 -2
  77. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  78. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  79. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  80. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  81. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  110. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  112. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  114. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  116. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  117. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  118. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  130. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  132. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  133. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  135. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  137. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  140. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  142. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  144. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  147. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  148. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  149. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  150. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  151. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  152. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  153. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  154. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  155. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  156. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  162. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  163. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  164. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  165. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  166. package/plugins/draw/src/api/addFeature.js +16 -1
  167. package/plugins/draw/src/api/addFeature.test.js +62 -5
  168. package/plugins/draw/src/api/createNewShape.js +41 -0
  169. package/plugins/draw/src/api/createNewShape.test.js +63 -0
  170. package/plugins/draw/src/api/editFeature.js +7 -4
  171. package/plugins/draw/src/api/editFeature.test.js +16 -0
  172. package/plugins/draw/src/api/newLine.js +2 -40
  173. package/plugins/draw/src/api/newPoint.js +42 -0
  174. package/plugins/draw/src/api/newPoint.test.js +117 -0
  175. package/plugins/draw/src/api/newPolygon.js +2 -40
  176. package/plugins/draw/src/api/setStyle.js +43 -0
  177. package/plugins/draw/src/api/setStyle.test.js +75 -0
  178. package/plugins/draw/src/events.js +17 -10
  179. package/plugins/draw/src/events.test.js +14 -0
  180. package/plugins/draw/src/manifest.js +18 -6
  181. package/plugins/draw/src/manifest.test.js +32 -1
  182. package/plugins/draw/src/reducer.test.js +7 -0
  183. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  184. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  185. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  186. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  187. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  188. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  189. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  190. package/plugins/interact/dist/umd/index.js +1 -1
  191. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  192. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  193. package/plugins/interact/src/manifest.js +1 -1
  194. package/plugins/interact/src/utils/featureQueries.js +18 -0
  195. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  196. package/plugins/interact/src/utils/spatial.js +9 -3
  197. package/plugins/interact/src/utils/spatial.test.js +12 -1
  198. package/plugins/map-key/dist/css/index.css +68 -0
  199. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  200. package/plugins/map-key/dist/esm/index.js +1 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  202. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  203. package/plugins/map-key/dist/umd/index.js +2 -0
  204. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  205. package/plugins/map-key/package.json +28 -0
  206. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  208. package/plugins/map-key/src/components/Key/Key.jsx +43 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +21 -14
  210. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  212. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.jsx +26 -0
  214. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  215. package/plugins/map-key/src/components/Key/KeySvg.jsx +41 -0
  216. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  217. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +22 -0
  218. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.jsx +7 -2
  220. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +8 -2
  221. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +26 -0
  222. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.jsx +8 -3
  224. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +17 -6
  225. package/plugins/map-key/src/components/Key/MapKey.jsx +53 -0
  226. package/plugins/map-key/src/components/Key/MapKey.test.jsx +147 -0
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  228. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  229. package/plugins/map-key/src/index.js +13 -0
  230. package/plugins/map-key/src/index.test.js +31 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  232. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  233. package/plugins/map-key/src/manifest.js +30 -0
  234. package/plugins/map-key/src/manifest.test.js +63 -0
  235. package/plugins/map-key/src/mapKey.scss +4 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  237. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  238. package/plugins/map-key/src/registry/index.js +3 -0
  239. package/plugins/map-key/src/registry/index.test.js +15 -0
  240. package/plugins/menu/dist/css/index.css +80 -0
  241. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  242. package/plugins/menu/dist/esm/index.js +1 -0
  243. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  244. package/plugins/menu/dist/umd/index.js +2 -0
  245. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  246. package/plugins/menu/package.json +28 -0
  247. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  249. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  250. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  251. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  252. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  254. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  255. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  257. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  258. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  259. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  260. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  261. package/plugins/menu/src/index.js +13 -0
  262. package/plugins/menu/src/index.test.js +31 -0
  263. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  264. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  265. package/plugins/menu/src/initialise/createMenu.js +10 -0
  266. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  267. package/plugins/menu/src/manifest.js +35 -0
  268. package/plugins/menu/src/manifest.test.js +63 -0
  269. package/plugins/menu/src/menu.scss +4 -0
  270. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  271. package/plugins/menu/src/reducers/pluginState.js +35 -0
  272. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  274. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  275. package/plugins/menu/src/registry/index.js +3 -0
  276. package/plugins/menu/src/registry/index.test.js +15 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  278. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  279. package/plugins/search/dist/css/index.css +1 -1
  280. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  281. package/plugins/search/dist/esm/index.js +1 -1
  282. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  283. package/plugins/search/dist/umd/index.js +1 -1
  284. package/plugins/search/src/Search.jsx +2 -3
  285. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  286. package/plugins/search/src/index.js +0 -1
  287. package/plugins/search/src/index.test.js +2 -4
  288. package/plugins/search/src/manifest.js +1 -1
  289. package/plugins/search/src/search.scss +17 -16
  290. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  292. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  294. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  296. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  298. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  299. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  301. package/providers/maplibre/dist/umd/index.js +1 -1
  302. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  303. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  304. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  305. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  306. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  307. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  308. package/providers/maplibre/src/utils/patternImages.js +1 -1
  309. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  310. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  311. package/rollup.esm.mjs +11 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Hints/Hints.test.jsx +13 -0
  314. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
  315. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
  316. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  317. package/src/App/components/Panel/Panel.jsx +65 -8
  318. package/src/App/components/Panel/Panel.module.scss +49 -15
  319. package/src/App/components/Panel/Panel.test.jsx +114 -1
  320. package/src/App/components/Tabs/Tabs.jsx +43 -17
  321. package/src/App/components/Tabs/Tabs.module.scss +32 -5
  322. package/src/App/components/Tabs/Tabs.test.jsx +40 -10
  323. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  324. package/src/App/hooks/useFeatureItems.js +16 -7
  325. package/src/App/hooks/useFeatureItems.test.js +57 -0
  326. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  327. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  328. package/src/App/layout/Layout.jsx +9 -10
  329. package/src/App/layout/Layout.test.jsx +2 -0
  330. package/src/App/layout/layout.module.scss +31 -17
  331. package/src/App/registry/pluginRegistry.js +1 -1
  332. package/src/App/registry/pluginRegistry.test.js +12 -0
  333. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  334. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  335. package/src/App/renderer/groupByKey.js +28 -0
  336. package/src/App/renderer/groupByKey.test.js +39 -0
  337. package/src/App/renderer/groupIntoTabs.js +42 -0
  338. package/src/App/renderer/groupIntoTabs.test.js +95 -0
  339. package/src/App/renderer/mapButtons.js +97 -129
  340. package/src/App/renderer/mapButtons.test.js +67 -83
  341. package/src/App/renderer/mapControls.js +4 -1
  342. package/src/App/renderer/mapControls.test.js +32 -0
  343. package/src/App/renderer/mapPanels.js +53 -11
  344. package/src/App/renderer/mapPanels.test.js +95 -2
  345. package/src/App/renderer/orderItems.js +28 -0
  346. package/src/App/renderer/orderItems.test.js +50 -0
  347. package/src/App/renderer/slotAggregator.js +2 -18
  348. package/src/App/renderer/slotHelpers.js +3 -1
  349. package/src/App/renderer/slotHelpers.test.js +5 -0
  350. package/src/App/renderer/slots.js +3 -0
  351. package/src/App/store/AppProvider.jsx +1 -0
  352. package/src/App/store/appDispatchMiddleware.js +1 -1
  353. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  354. package/src/config/appConfig.js +2 -2
  355. package/src/config/events.js +2 -0
  356. package/src/scss/settings/_colors.scss +1 -0
  357. package/src/scss/settings/_dimensions.scss +2 -3
  358. package/src/scss/settings/_transition.scss +1 -1
  359. package/src/services/eventBus.js +44 -0
  360. package/src/services/eventBus.test.js +83 -0
  361. package/src/services/symbolRegistry.js +1 -1
  362. package/src/types.js +31 -3
  363. package/webpack.umd.mjs +2 -0
  364. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  365. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  366. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  367. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  368. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  369. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  370. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  371. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  372. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  373. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  374. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -0,0 +1,267 @@
1
+ import { createPointSelectionState } from './pointSelectionState.js'
2
+ import { deriveModifyOp } from '../edit/modifyInteraction.js'
3
+ import { createPointDragInteraction } from './pointDragInteraction.js'
4
+ import { PIXEL_TOLERANCE } from '../edit/vertexHitTest.js'
5
+ import { createTouchHandler } from '../edit/touchHandler.js'
6
+ import { createKeyboardHandler } from '../edit/keyboardHandler.js'
7
+ import { movePoint, applyPointUndo } from './pointOps.js'
8
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
9
+ import { STYLES_CHANGED_EVENT } from '../core/internalEvents.js'
10
+ import { createLiveStroke } from '../../../validation/liveStroke.js'
11
+
12
+ const TOUCH_INTERFACE = 'touch'
13
+ // Shared phase name with edit_vertex's own move_vertex op — a moved point re-validates the
14
+ // same way (validation/rules.js has no Point-specific rule, so this only ever gates a
15
+ // consumer's own onGeometryChange callback).
16
+ const MOVE_VERTEX = 'commit-move'
17
+
18
+ // A symbol icon renders far bigger than buildModifyCondition's fixed-radius hit-test (tuned
19
+ // for a plain vertex dot), so this hit-tests the icon's own rendered pixels instead. No touch
20
+ // check — pointDragInteraction.js's handleDownEvent already gates on that before calling this.
21
+ const buildPointModifyCondition = ({ map, olFeature }) => (mapBrowserEvent) => {
22
+ const pixel = map.getEventPixel(mapBrowserEvent.originalEvent)
23
+ return !!map.forEachFeatureAtPixel(pixel, (feature) => feature === olFeature, { hitTolerance: PIXEL_TOLERANCE })
24
+ }
25
+
26
+ /**
27
+ * Live invalid-stroke wiring for edit_point — unlike edit/EditMode.js's wireLiveStroke, this
28
+ * must NOT call olFeature.setStyle(...): a Stroke/Fill style renders nothing for a Point (see
29
+ * core/styles.js's own placeholder-style comment) and would make the icon vanish. There is
30
+ * also no dashed-stroke visual for a point to begin with (validation/rules.js has no
31
+ * Point-specific rule — see the module doc above), so onChange only relays the verdict to
32
+ * gate the Done button, never touching the feature's style.
33
+ */
34
+ const wirePointLiveValidity = ({ manager, olFeature }) => {
35
+ const liveStroke = createLiveStroke({
36
+ onChange: (invalid, reason) => {
37
+ manager.emit(ADAPTER_EVENTS.VALIDITY_CHANGE, { valid: !invalid, reason })
38
+ }
39
+ })
40
+ const updateLiveValidity = () => {
41
+ const coordinates = olFeature.getGeometry().getCoordinates()
42
+ liveStroke.update({
43
+ feature: { type: 'Feature', geometry: { type: 'Point', coordinates } },
44
+ context: { mode: 'edit_point' },
45
+ numVertices: 1,
46
+ onGeometryChange: manager._geometryValidator
47
+ })
48
+ }
49
+ olFeature.getGeometry().on('change', updateLiveValidity)
50
+ return {
51
+ liveStroke,
52
+ destroy () {
53
+ olFeature.getGeometry().un('change', updateLiveValidity)
54
+ liveStroke.destroy()
55
+ }
56
+ }
57
+ }
58
+
59
+ const wireTouchHandler = ({ map, container, manager, snap, undoStack, selection }) => {
60
+ const { getState, setState, syncGeom, emitGeometryValidation } = selection
61
+
62
+ const touchHandler = createTouchHandler({
63
+ map,
64
+ container,
65
+ getState,
66
+ setState,
67
+ colors: manager.colors,
68
+ snap,
69
+ moveCoord: movePoint,
70
+ onVertexMoved ({ vertexIndex, previousCoord }) {
71
+ undoStack.push({ type: 'move_vertex', vertexIndex, previousCoord })
72
+ syncGeom()
73
+ emitGeometryValidation(MOVE_VERTEX, vertexIndex)
74
+ },
75
+ // A stray tap never relocates the point — only the offset-target drag and the D-pad do
76
+ // (mirrors the ML adapter's editPointMode/touchHandlers.js onTap no-op).
77
+ onTap () {}
78
+ })
79
+
80
+ selection.setHooks({
81
+ onUpdate () {
82
+ if (getState().interfaceType === TOUCH_INTERFACE) {
83
+ touchHandler.updateTargetPosition()
84
+ }
85
+ }
86
+ })
87
+
88
+ return touchHandler
89
+ }
90
+
91
+ // Undo the last move — shared by the keyboard's Cmd/Ctrl+Z shortcut and the mode API's own
92
+ // undo() (Undo button/menu action), so both apply the exact same op-popping logic.
93
+ const createUndoAction = ({ olFeature, undoStack, selection }) => () => {
94
+ const op = undoStack.pop()
95
+ if (!op) {
96
+ return
97
+ }
98
+ applyPointUndo(olFeature, op)
99
+ selection.syncGeom()
100
+ // An undo commits the inverse change, so it must re-validate like any other commit —
101
+ // otherwise the Done gate could go stale. Always vertexIndex 0 — a point has only one.
102
+ selection.emitGeometryValidation(MOVE_VERTEX, 0)
103
+ }
104
+
105
+ const wireKeyboardHandler = ({ map, container, snap, undoStack, selection, touchHandler, doUndo }) => {
106
+ const { getState, setState, syncGeom, emitGeometryValidation } = selection
107
+
108
+ return createKeyboardHandler({
109
+ map,
110
+ getState,
111
+ setState,
112
+ snap,
113
+ moveCoord: movePoint,
114
+ onVertexMoved ({ vertexIndex, previousCoord }) {
115
+ undoStack.push({ type: 'move_vertex', vertexIndex, previousCoord })
116
+ syncGeom()
117
+ emitGeometryValidation(MOVE_VERTEX, vertexIndex)
118
+ },
119
+ // No midpoint concept for a point, so this is never actually reached — required by
120
+ // createKeyboardHandler's interface all the same. Not reachable through this module's
121
+ // public surface either, so there's no meaningful way to exercise it from a test.
122
+ /* istanbul ignore next */
123
+ onInserted () {},
124
+ // No delete-vertex action for a point — deleteVertex() below is a documented no-op.
125
+ onDeleted () {},
126
+ onUndo: doUndo,
127
+ onKeyboardActive () {
128
+ if (getState().interfaceType === 'keyboard') {
129
+ return
130
+ }
131
+ getState().interfaceType = 'keyboard'
132
+ touchHandler.hide()
133
+ container.focus({ preventScroll: true })
134
+ }
135
+ })
136
+ }
137
+
138
+ // Style hot-swap on map style change + touch-target reposition on map resize — mirrors
139
+ // edit/EditMode.js's wireMapSync, minus the vertex/midpoint handle layers a point has none of.
140
+ const wireMapSync = ({ map, manager, selection, touchHandler, live }) => {
141
+ const { state } = selection
142
+
143
+ const onStylesChanged = () => {
144
+ live.liveStroke.refresh()
145
+ touchHandler.updateColors(manager.colors)
146
+ }
147
+ manager.on(STYLES_CHANGED_EVENT, onStylesChanged)
148
+
149
+ const onMapSizeChange = () => {
150
+ if (state.interfaceType !== TOUCH_INTERFACE) {
151
+ return
152
+ }
153
+ map.once('postrender', () => touchHandler.updateTargetPosition())
154
+ }
155
+ map.on('change:size', onMapSizeChange)
156
+
157
+ return {
158
+ destroy () {
159
+ manager.off(STYLES_CHANGED_EVENT, onStylesChanged)
160
+ map.un('change:size', onMapSizeChange)
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * @returns {{ setInterfaceType, done, setInvalid, cancel, undo, deleteVertex, nudgeSelectedVertex, destroy } | null}
167
+ */
168
+ export const createEditPointMode = ({ map, manager, options }) => {
169
+ const { featureId, container, interfaceType, snap } = options
170
+ const { store, undoStack } = manager
171
+
172
+ const olFeature = store.getOL(featureId)
173
+ if (!olFeature) {
174
+ return null
175
+ }
176
+
177
+ // edit_vertex's first interaction is always a click that selects a vertex, which as a side
178
+ // effect moves focus onto the map — satisfying keyboardHandler.js's isInteractiveElementFocused
179
+ // guard. A point is already selected from the moment this mode starts, so a keyboard-only
180
+ // session can go straight to arrow keys with no click in between; claim focus explicitly so
181
+ // that guard reads correctly from the first keypress (mirrors the ML adapter's own fix).
182
+ container?.focus({ preventScroll: true })
183
+
184
+ const originalFeatureStyle = olFeature.getStyle()
185
+ // Keeps showing the same "selected" (black ring) icon variant the interact plugin's own
186
+ // highlight uses, for the whole edit session — a style FUNCTION, not a one-off Style
187
+ // instance, so it re-reads manager.styles fresh on every render and survives a style
188
+ // hot-swap without needing to be re-applied. See core/styles.js's selectedPointStyleFor.
189
+ olFeature.setStyle(() => manager.styles.selectedPointStyleFor(olFeature))
190
+
191
+ const selection = createPointSelectionState({ manager, store, olFeature, interfaceType })
192
+ const { getState, setState } = selection
193
+
194
+ const pointDrag = createPointDragInteraction({
195
+ map,
196
+ olFeature,
197
+ getState,
198
+ condition: buildPointModifyCondition({ map, olFeature }),
199
+ snap,
200
+ onModifyEnd (prevCoords) {
201
+ selection.syncGeom()
202
+ const op = prevCoords && deriveModifyOp(prevCoords, getState().vertices)
203
+ if (!op) {
204
+ return
205
+ }
206
+ undoStack.push(op)
207
+ selection.emitGeometryValidation(MOVE_VERTEX, op.vertexIndex)
208
+ }
209
+ })
210
+
211
+ const doUndo = createUndoAction({ olFeature, undoStack, selection })
212
+ const live = wirePointLiveValidity({ manager, olFeature })
213
+ const touchHandler = wireTouchHandler({ map, container, manager, snap, undoStack, selection })
214
+ const keyboardHandler = wireKeyboardHandler({ map, container, snap, undoStack, selection, touchHandler, doUndo })
215
+ const mapSync = wireMapSync({ map, manager, selection, touchHandler, live })
216
+
217
+ return {
218
+ setInterfaceType (type) {
219
+ const state = getState()
220
+ if (type === state.interfaceType) {
221
+ return
222
+ }
223
+ setState({ interfaceType: type })
224
+ if (type === TOUCH_INTERFACE) {
225
+ touchHandler.updateTargetPosition()
226
+ } else {
227
+ touchHandler.hide()
228
+ }
229
+ },
230
+
231
+ done () {
232
+ manager.emit(ADAPTER_EVENTS.EDIT_FINISH, store.toGeoJSON(olFeature))
233
+ },
234
+
235
+ // Committed-verdict write (events.js): routed through the live-stroke controller so its
236
+ // cached state stays in sync — see wirePointLiveValidity's own comment on why this never
237
+ // touches the feature's style.
238
+ setInvalid (invalid) {
239
+ live.liveStroke.set(invalid)
240
+ },
241
+
242
+ // Nothing to restore here: the pre-edit feature is kept as tempFeature in the reducer and
243
+ // events.js re-adds it on cancel.
244
+ cancel () {},
245
+
246
+ undo: doUndo,
247
+
248
+ // Intentionally a no-op — deleting a Point's one coordinate isn't a "delete vertex"
249
+ // action the way it is for a ring; whole-feature deletion is deleteFeature()'s job.
250
+ // Matches MaplibreDrawAdapter.deleteVertex's existing no-op precedent.
251
+ deleteVertex () {},
252
+
253
+ // MoveControls' D-pad, routed here via mapProvider.activeMoveTarget (events.js) — claimed
254
+ // once at construction (pointSelectionState's VERTEX_SELECTION) and held for the session.
255
+ nudgeSelectedVertex: keyboardHandler.nudgeByDelta,
256
+
257
+ destroy () {
258
+ live.destroy()
259
+ olFeature.setStyle(originalFeatureStyle)
260
+ selection.destroy()
261
+ mapSync.destroy()
262
+ pointDrag.destroy()
263
+ touchHandler.destroy()
264
+ keyboardHandler.destroy()
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,327 @@
1
+ import { createEditPointMode } from './editPointMode.js'
2
+ import { createFeatureStore } from '../core/featureStore.js'
3
+ import { STYLES_CHANGED_EVENT } from '../core/internalEvents.js'
4
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
5
+ import { createFakeMap, createFakeManager, createContainer, domEvent } from '../__helpers__/harness.js'
6
+
7
+ const setup = (coordinates = [5, 5], snap = null) => {
8
+ const map = createFakeMap()
9
+ const manager = createFakeManager()
10
+ manager.store = createFeatureStore()
11
+ manager.store.add({ type: 'Feature', id: 'f1', properties: {}, geometry: { type: 'Point', coordinates } })
12
+ const container = createContainer()
13
+ map.getViewport().appendChild(container)
14
+ const mode = createEditPointMode({
15
+ map,
16
+ manager,
17
+ options: { featureId: 'f1', container, interfaceType: 'mouse', snap }
18
+ })
19
+ liveModes.push(mode)
20
+ const olFeature = manager.store.getOL('f1')
21
+ const tapAt = (x, y) => {
22
+ container.dispatchEvent(domEvent('touchstart', { touches: [{ clientX: x, clientY: y }] }))
23
+ container.dispatchEvent(domEvent('touchend', { changedTouches: [{ clientX: x, clientY: y }] }))
24
+ }
25
+ return { map, manager, container, mode, olFeature, coord: () => olFeature.getGeometry().getCoordinates(), tapAt }
26
+ }
27
+
28
+ const liveModes = []
29
+ afterEach(() => {
30
+ liveModes.splice(0).forEach((m) => m?.destroy())
31
+ document.body.innerHTML = ''
32
+ })
33
+
34
+ const key = (type, props) => window.dispatchEvent(new KeyboardEvent(type, { cancelable: true, ...props }))
35
+
36
+ test('returns null for an unknown feature id', () => {
37
+ const { map, manager } = setup()
38
+ expect(createEditPointMode({ map, manager, options: { featureId: 'nope', container: createContainer() } })).toBeNull()
39
+ })
40
+
41
+ test('entering edit mode claims the D-pad exactly once, never emitting VERTEX_CHANGE', () => {
42
+ const { manager } = setup()
43
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, { index: 0, numVertices: 1 })
44
+ expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_CHANGE, expect.anything())
45
+ })
46
+
47
+ test('sets the feature style once at construction, to the selected-icon override — nothing else re-touches it', () => {
48
+ const { olFeature, container, manager } = setup()
49
+ expect(olFeature.getStyle()).toEqual(expect.any(Function))
50
+ olFeature.getStyle()()
51
+ expect(manager.styles.selectedPointStyleFor).toHaveBeenCalledWith(olFeature)
52
+
53
+ const setStyleSpy = jest.spyOn(olFeature, 'setStyle')
54
+ olFeature.getGeometry().setCoordinates([9, 9])
55
+ key('keydown', { key: 'ArrowRight' })
56
+ key('keyup', { key: 'ArrowRight' })
57
+ container.dispatchEvent(domEvent('touchstart', { touches: [{ clientX: 9, clientY: 9 }] }))
58
+ expect(setStyleSpy).not.toHaveBeenCalled()
59
+ })
60
+
61
+ test('destroy restores the feature\'s pre-edit style', () => {
62
+ const { mode, olFeature } = setup()
63
+ const selectedStyleFn = olFeature.getStyle()
64
+ mode.destroy()
65
+ expect(olFeature.getStyle()).toBeNull() // unset before entering edit_point → falls back to the layer style
66
+ expect(olFeature.getStyle()).not.toBe(selectedStyleFn)
67
+ })
68
+
69
+ test('validity flips (user callback only — a Point has no built-in HARD_RULES violation) gate the Done button', () => {
70
+ jest.useFakeTimers()
71
+ const { manager, olFeature } = setup()
72
+ manager._geometryValidator = jest.fn(() => ({ valid: false, reason: 'outside region' }))
73
+ olFeature.getGeometry().setCoordinates([9, 9])
74
+ jest.runAllTimers()
75
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VALIDITY_CHANGE,
76
+ expect.objectContaining({ valid: false, reason: 'outside region' }))
77
+ jest.useRealTimers()
78
+ })
79
+
80
+ test('done() emits the edited feature; cancel() is a no-op', () => {
81
+ const { manager, mode } = setup()
82
+ mode.done()
83
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.EDIT_FINISH, expect.objectContaining({ id: 'f1' }))
84
+ expect(mode.cancel()).toBeUndefined()
85
+ })
86
+
87
+ test('setInvalid routes through the live-stroke controller without touching the feature style', () => {
88
+ const { mode, olFeature } = setup()
89
+ const setStyleSpy = jest.spyOn(olFeature, 'setStyle')
90
+ mode.setInvalid(true)
91
+ mode.setInvalid(false)
92
+ expect(setStyleSpy).not.toHaveBeenCalled()
93
+ })
94
+
95
+ test('deleteVertex is a documented no-op — whole-feature deletion is deleteFeature\'s job', () => {
96
+ const { mode, coord } = setup()
97
+ const before = coord()
98
+ expect(() => mode.deleteVertex()).not.toThrow()
99
+ expect(coord()).toEqual(before)
100
+ })
101
+
102
+ test('a mouse drag pushes a move_vertex op derived from the before/after coordinate', () => {
103
+ const { map, manager, olFeature } = setup()
104
+ const interaction = map.interactions[0]
105
+ map.forEachFeatureAtPixel = jest.fn((pixel, cb) => cb(olFeature))
106
+ const down = { originalEvent: {}, pixel: [5, 5] }
107
+ expect(interaction.handleDownEvent(down)).toBe(true)
108
+ interaction.handleDragEvent({ pixel: [9, 9] })
109
+ interaction.handleUpEvent()
110
+ expect(manager.undoStack.pop()).toEqual({ type: 'move_vertex', vertexIndex: 0, previousCoord: [5, 5] })
111
+
112
+ expect(interaction.handleDownEvent(down)).toBe(true)
113
+ interaction.handleUpEvent() // no drag in between — no coordinate change, nothing pushed
114
+ expect(manager.undoStack.length).toBe(0)
115
+ })
116
+
117
+ // OL only calls handleDragEvent/handleUpEvent between a handleDownEvent that returned true and
118
+ // the matching release, but both guard against being invoked outside that sequence regardless.
119
+ test('handleDragEvent and handleUpEvent are no-ops without a preceding successful handleDownEvent', () => {
120
+ const { map, manager, olFeature, coord } = setup()
121
+ const interaction = map.interactions[0]
122
+
123
+ interaction.handleDragEvent({ pixel: [9, 9] }) // no down event — no grab offset captured yet
124
+ expect(coord()).toEqual([5, 5])
125
+
126
+ expect(() => interaction.handleUpEvent()).not.toThrow()
127
+ expect(manager.undoStack.length).toBe(0) // no start coordinate — nothing to derive an op from
128
+ expect(olFeature.getGeometry().getCoordinates()).toEqual([5, 5])
129
+ })
130
+
131
+ // The mouse click can land anywhere on the icon, not just on top of its anchor coordinate —
132
+ // dragging must move the point by the same delta as the pointer, not snap the anchor under it
133
+ // (unlike OL's own Modify, which always sets the dragged coordinate to the pointer position).
134
+ test('a mouse drag preserves the click\'s offset from the anchor instead of snapping to the pointer', () => {
135
+ const { map, olFeature } = setup([5, 5])
136
+ const interaction = map.interactions[0]
137
+ map.forEachFeatureAtPixel = jest.fn((pixel, cb) => cb(olFeature))
138
+ // Click 3 map-units up-left of the anchor (e.g. near the top of a pin icon).
139
+ interaction.handleDownEvent({ originalEvent: {}, pixel: [2, 2] })
140
+ interaction.handleDragEvent({ pixel: [2, 2] }) // no pointer movement yet
141
+ expect(olFeature.getGeometry().getCoordinates()).toEqual([5, 5]) // anchor did NOT jump to [2, 2]
142
+
143
+ interaction.handleDragEvent({ pixel: [12, 2] }) // pointer moves 10 units right
144
+ expect(olFeature.getGeometry().getCoordinates()).toEqual([15, 5]) // anchor moves the same 10 units
145
+ })
146
+
147
+ // snap/snapInteraction.js's OL Interaction rewrites mapBrowserEvent.coordinate to the nearest
148
+ // candidate near the raw POINTER position — right for Modify, wrong here, since it would snap
149
+ // to whatever's near the cursor rather than near the icon's own anchor. This asserts snap.apply
150
+ // gets called with the offset-corrected anchor candidate, not the raw event coordinate.
151
+ test('a mouse drag snaps the icon\'s own offset-corrected position, not the raw pointer', () => {
152
+ const snap = { apply: jest.fn((coord) => [coord[0] + 100, coord[1] + 100]), hideIndicator: jest.fn() }
153
+ const { map, olFeature } = setup([5, 5], snap)
154
+ const interaction = map.interactions[0]
155
+ map.forEachFeatureAtPixel = jest.fn((pixel, cb) => cb(olFeature))
156
+
157
+ // Click 3 map-units up-left of the anchor, then drag 10 units right.
158
+ interaction.handleDownEvent({ originalEvent: {}, pixel: [2, 2] })
159
+ // mapBrowserEvent.coordinate deliberately left stale/wrong here — a real snapInteraction may
160
+ // have already rewritten it to something near the cursor before this interaction runs, and
161
+ // the fix must ignore it in favour of .pixel.
162
+ interaction.handleDragEvent({ pixel: [12, 2], coordinate: [999, 999] })
163
+
164
+ expect(snap.apply).toHaveBeenCalledWith([15, 5]) // the icon's own candidate position, not [999, 999]
165
+ expect(olFeature.getGeometry().getCoordinates()).toEqual([115, 105]) // snap.apply's result is applied
166
+
167
+ // The indicator must stay showing after the drag ends — snap.apply()'s own last call already
168
+ // left it correctly reflecting the snapped position (matches the ML adapter, which never
169
+ // hides its own indicator on release either).
170
+ interaction.handleUpEvent()
171
+ expect(snap.hideIndicator).not.toHaveBeenCalled()
172
+ })
173
+
174
+ // A symbol icon renders far bigger than PIXEL_TOLERANCE (tuned for a plain ~6px vertex dot)
175
+ // covers, so the mouse hit-test uses OL's own forEachFeatureAtPixel against the icon's real
176
+ // rendered pixels instead of a fixed-radius distance check.
177
+ describe('mouse hit-testing respects the icon\'s own rendered pixels, not a fixed radius', () => {
178
+ test('engages when forEachFeatureAtPixel hits this exact feature', () => {
179
+ const { map, olFeature } = setup()
180
+ map.forEachFeatureAtPixel = jest.fn((pixel, cb) => cb(olFeature))
181
+ const interaction = map.interactions[0]
182
+ expect(interaction.handleDownEvent({ originalEvent: { clientX: 5, clientY: 5 }, pixel: [5, 5] })).toBe(true)
183
+ expect(map.forEachFeatureAtPixel).toHaveBeenCalledWith([5, 5], expect.any(Function), { hitTolerance: expect.any(Number) })
184
+ })
185
+
186
+ test('does not engage when forEachFeatureAtPixel finds nothing', () => {
187
+ const { map } = setup()
188
+ map.forEachFeatureAtPixel = jest.fn(() => undefined)
189
+ const interaction = map.interactions[0]
190
+ expect(interaction.handleDownEvent({ originalEvent: { clientX: 5, clientY: 5 }, pixel: [5, 5] })).toBe(false)
191
+ })
192
+
193
+ test('does not engage for a different feature under the pixel', () => {
194
+ const { map } = setup()
195
+ const otherFeature = {}
196
+ map.forEachFeatureAtPixel = jest.fn((pixel, cb) => cb(otherFeature))
197
+ const interaction = map.interactions[0]
198
+ expect(interaction.handleDownEvent({ originalEvent: { clientX: 5, clientY: 5 }, pixel: [5, 5] })).toBe(false)
199
+ })
200
+
201
+ test('bails for touch without even querying', () => {
202
+ const { map, mode } = setup()
203
+ mode.setInterfaceType('touch')
204
+ map.forEachFeatureAtPixel = jest.fn()
205
+ const interaction = map.interactions[0]
206
+ expect(interaction.handleDownEvent({ originalEvent: { clientX: 5, clientY: 5 }, pixel: [5, 5] })).toBe(false)
207
+ expect(map.forEachFeatureAtPixel).not.toHaveBeenCalled()
208
+ })
209
+ })
210
+
211
+ test('nudgeSelectedVertex (MoveControls D-pad) moves the point and is undoable', () => {
212
+ const { manager, mode, coord } = setup()
213
+ mode.nudgeSelectedVertex(1, 0, true)
214
+ expect(coord()).not.toEqual([5, 5])
215
+ expect(manager.undoStack.length).toBe(1)
216
+
217
+ mode.undo()
218
+ expect(coord()).toEqual([5, 5])
219
+ mode.undo() // empty stack — no-op
220
+ })
221
+
222
+ test('undo re-validates with the commit-move phase', () => {
223
+ jest.useFakeTimers()
224
+ const { manager, mode } = setup()
225
+ mode.nudgeSelectedVertex(1, 0, true)
226
+ jest.runAllTimers()
227
+ manager.emit.mockClear()
228
+ mode.undo()
229
+ jest.runAllTimers()
230
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, expect.objectContaining({
231
+ phase: 'commit-move',
232
+ vertexIndex: 0,
233
+ feature: expect.any(Object)
234
+ }))
235
+ jest.useRealTimers()
236
+ })
237
+
238
+ test('keyboard: arrows nudge the point and commit one undo op on keyup', () => {
239
+ const { manager, coord } = setup()
240
+ key('keydown', { key: 'ArrowRight' })
241
+ key('keyup', { key: 'ArrowRight' })
242
+ expect(coord()).not.toEqual([5, 5])
243
+ expect(manager.undoStack.pop()).toEqual({ type: 'move_vertex', vertexIndex: 0, previousCoord: [5, 5] })
244
+ })
245
+
246
+ test('keyboard: Delete is a no-op — there is no delete-vertex action for a point', () => {
247
+ const { coord } = setup()
248
+ key('keyup', { key: 'Delete' })
249
+ expect(coord()).toEqual([5, 5])
250
+ })
251
+
252
+ test('keyboard: Cmd/Ctrl+Z undoes via the same op-popping logic as mode.undo()', () => {
253
+ const { manager, coord } = setup()
254
+ key('keydown', { key: 'ArrowRight' })
255
+ key('keyup', { key: 'ArrowRight' })
256
+ expect(manager.undoStack.length).toBe(1)
257
+ key('keydown', { key: 'z', metaKey: true })
258
+ expect(coord()).toEqual([5, 5])
259
+ expect(manager.undoStack.length).toBe(0)
260
+ })
261
+
262
+ test('keyboard: Escape and Space are inert — the point cannot be deselected, and there is nothing else to select', () => {
263
+ const { manager, coord } = setup()
264
+ manager.emit.mockClear()
265
+ key('keydown', { key: 'Escape' })
266
+ key('keydown', { key: ' ' })
267
+ expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.anything())
268
+ key('keydown', { key: 'ArrowRight' })
269
+ key('keyup', { key: 'ArrowRight' })
270
+ expect(coord()).not.toEqual([5, 5]) // arrows still work after Escape/Space
271
+ })
272
+
273
+ test('touch: dragging the offset target moves the point and records one move op', () => {
274
+ const { container, manager, mode, tapAt, coord } = setup()
275
+ tapAt(5, 5)
276
+ mode.setInterfaceType('touch')
277
+ const grip = container.querySelector('[data-im-draw-touch-target] circle')
278
+ grip.dispatchEvent(domEvent('touchstart', { touches: [{ clientX: 5, clientY: 5 }] }))
279
+ grip.dispatchEvent(domEvent('touchmove', { touches: [{ clientX: 20, clientY: 15 }] }))
280
+ grip.dispatchEvent(domEvent('touchend', { changedTouches: [{ clientX: 20, clientY: 15 }] }))
281
+ expect(coord()).toEqual([20, 15])
282
+ expect(manager.undoStack.pop()).toEqual({ type: 'move_vertex', vertexIndex: 0, previousCoord: [5, 5] })
283
+ })
284
+
285
+ test('touch: a tap never relocates the point — only the offset-target drag and the D-pad do', () => {
286
+ const { manager, tapAt, coord } = setup()
287
+ manager.undoStack.clear()
288
+ tapAt(5, 5)
289
+ expect(coord()).toEqual([5, 5])
290
+ expect(manager.undoStack.length).toBe(0)
291
+ })
292
+
293
+ test('setInterfaceType shows/hides the touch target, and is a no-op for the same type', () => {
294
+ const { container, mode } = setup()
295
+ const target = container.querySelector('[data-im-draw-touch-target]')
296
+ mode.setInterfaceType('touch')
297
+ expect(target.style.display).toBe('block')
298
+ mode.setInterfaceType('touch') // same type — no change
299
+ mode.setInterfaceType('mouse')
300
+ expect(target.style.display).toBe('none')
301
+ })
302
+
303
+ test('style changes refresh the touch target colours, without touching the feature style', () => {
304
+ const { manager, olFeature } = setup()
305
+ const setStyleSpy = jest.spyOn(olFeature, 'setStyle')
306
+ const newStyles = { ...manager.styles }
307
+ manager.styles = newStyles
308
+ manager.emit(STYLES_CHANGED_EVENT, newStyles)
309
+ expect(setStyleSpy).not.toHaveBeenCalled()
310
+ })
311
+
312
+ test('map resize repositions the touch target after the next render — touch only', () => {
313
+ const { map, mode } = setup()
314
+ map.emit('change:size') // mouse interface — ignored
315
+ mode.setInterfaceType('touch')
316
+ map.emit('change:size')
317
+ map.emit('postrender') // once-listener fires; must not throw
318
+ expect(map.once).toHaveBeenCalledWith('postrender', expect.any(Function))
319
+ })
320
+
321
+ test('destroy removes the point drag interaction and listeners', () => {
322
+ const { map, mode } = setup()
323
+ const interactionsBefore = map.interactions.length
324
+ mode.destroy()
325
+ expect(map.removeInteraction).toHaveBeenCalled()
326
+ expect(map.interactions.length).toBeLessThan(interactionsBefore)
327
+ })
@@ -0,0 +1,71 @@
1
+ import PointerInteraction from 'ol/interaction/Pointer.js'
2
+
3
+ /**
4
+ * Mouse/pointer drag for edit_point — a hand-rolled `ol/interaction/Pointer` rather than
5
+ * edit/modifyInteraction.js's OL `Modify` wrapper. `Modify` snaps the dragged coordinate
6
+ * straight to the pointer, which is fine for a small vertex dot but jumps a symbol icon under
7
+ * the cursor the moment you click anywhere off its anchor — this instead holds a fixed
8
+ * click-to-anchor offset for the whole drag, so the icon moves exactly as far as the pointer
9
+ * does. Snap is applied to that offset-corrected candidate (read from .pixel, since
10
+ * snap/snapInteraction.js may already have rewritten .coordinate to something near the raw
11
+ * pointer) rather than the raw pointer itself, for the same reason.
12
+ *
13
+ * @param {{ map, olFeature, getState, condition, snap, onModifyEnd }} options
14
+ * @param {(mapBrowserEvent) => boolean} options.condition - gates drag start, e.g.
15
+ * buildPointModifyCondition's forEachFeatureAtPixel icon hit-test.
16
+ * @param {{ apply } | null} [options.snap] - snap/snapManager.js's controller, or
17
+ * null/undefined when no snap layers are configured.
18
+ * @param {(prevCoords: number[][]) => void} options.onModifyEnd - called once at drag end with
19
+ * the single-coordinate vertices array as it stood at drag start — same shape
20
+ * edit/modifyInteraction.js's onModifyEnd gets, so callers can share its deriveModifyOp.
21
+ * @returns {{ destroy: () => void }}
22
+ */
23
+ export const createPointDragInteraction = ({ map, olFeature, getState, condition, snap, onModifyEnd }) => {
24
+ let grabOffset = null // [dx, dy] map-coordinate offset (anchor − pointer), held for the drag
25
+ let startCoord = null
26
+
27
+ // .pixel is never rewritten by snap/snapInteraction.js (unlike .coordinate), so it's the
28
+ // reliable way to get the true pointer position.
29
+ const pointerCoord = (mapBrowserEvent) => map.getCoordinateFromPixel(mapBrowserEvent.pixel)
30
+
31
+ const handleDownEvent = (mapBrowserEvent) => {
32
+ if (getState().interfaceType === 'touch' || !condition(mapBrowserEvent)) {
33
+ return false
34
+ }
35
+ const coord = olFeature.getGeometry().getCoordinates()
36
+ const pointer = pointerCoord(mapBrowserEvent)
37
+ grabOffset = [coord[0] - pointer[0], coord[1] - pointer[1]]
38
+ startCoord = [...coord]
39
+ return true
40
+ }
41
+
42
+ const handleDragEvent = (mapBrowserEvent) => {
43
+ if (!grabOffset) {
44
+ return
45
+ }
46
+ const pointer = pointerCoord(mapBrowserEvent)
47
+ const rawCoord = [pointer[0] + grabOffset[0], pointer[1] + grabOffset[1]]
48
+ const newCoord = snap ? snap.apply(rawCoord) : rawCoord
49
+ olFeature.getGeometry().setCoordinates(newCoord)
50
+ }
51
+
52
+ const handleUpEvent = () => {
53
+ grabOffset = null
54
+ // Not hiding the snap indicator here — handleDragEvent's own snap.apply() already left it
55
+ // showing correctly, and it should stay that way after the drag ends.
56
+ if (startCoord) {
57
+ onModifyEnd([startCoord])
58
+ }
59
+ startCoord = null
60
+ return false // ends the drag sequence — never chains into another
61
+ }
62
+
63
+ const interaction = new PointerInteraction({ handleDownEvent, handleDragEvent, handleUpEvent })
64
+ map.addInteraction(interaction)
65
+
66
+ return {
67
+ destroy () {
68
+ map.removeInteraction(interaction)
69
+ }
70
+ }
71
+ }