@defra/interactive-map 0.0.39-alpha → 0.0.41-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (387) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/control-definition.md +9 -1
  7. package/docs/api/slots.md +26 -5
  8. package/docs/api.md +4 -4
  9. package/docs/assets/images/slot-map.svg +61 -262
  10. package/docs/examples/draw-tools.mdx +3 -3
  11. package/docs/plugins/draw.md +80 -17
  12. package/docs/plugins/search.md +1 -1
  13. package/govuk-prototype-kit.config.json +2 -0
  14. package/jest.setup.js +5 -0
  15. package/package.json +9 -1
  16. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  17. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  18. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  19. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  20. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  21. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  22. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  23. package/plugins/datasets/dist/css/index.css +1 -1
  24. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  25. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  26. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  27. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  28. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  29. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  30. package/plugins/datasets/dist/umd/index.js +1 -1
  31. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +2 -3
  32. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  33. package/plugins/datasets/src/adapters/esri/registry/esriDataset.js +1 -1
  34. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  35. package/plugins/datasets/src/adapters/loadLayerAdapter.js +1 -1
  36. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  37. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +1 -3
  38. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +17 -3
  39. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  40. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  41. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  42. package/plugins/datasets/src/datasets.scss +0 -1
  43. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  44. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  45. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  46. package/plugins/datasets/src/index.test.js +36 -0
  47. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  48. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  49. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -5
  50. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  51. package/plugins/datasets/src/manifest.js +0 -38
  52. package/plugins/datasets/src/manifest.test.js +49 -0
  53. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  54. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  55. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  56. package/plugins/datasets/src/registry/dataset.js +15 -4
  57. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  58. package/plugins/datasets/src/registry/datasetRegistry.js +12 -9
  59. package/plugins/datasets/src/registry/datasetRegistry.test.js +17 -5
  60. package/plugins/datasets/src/registry/globalDataset.js +0 -3
  61. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  62. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  63. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  64. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  65. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  66. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  67. package/plugins/draw/dist/esm/index.js +1 -1
  68. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  69. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  70. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  72. package/plugins/draw/dist/umd/index.js +1 -1
  73. package/plugins/draw/src/DrawInit.jsx +12 -4
  74. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  75. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +92 -12
  76. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  77. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  78. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  79. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  81. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +1 -1
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +1 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +5 -28
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -54
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +11 -4
  110. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  112. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  114. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  116. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  117. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  118. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  130. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  132. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  133. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  135. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  137. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +1 -0
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +27 -1
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +24 -4
  140. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +47 -3
  141. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +1 -1
  142. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +32 -1
  143. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  144. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +34 -1
  145. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +12 -5
  148. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +26 -3
  149. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  150. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  151. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  152. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  153. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  154. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  155. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  156. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  162. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  163. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  164. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  165. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  166. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  167. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  168. package/plugins/draw/src/api/addFeature.js +16 -1
  169. package/plugins/draw/src/api/addFeature.test.js +62 -5
  170. package/plugins/draw/src/api/editFeature.js +7 -4
  171. package/plugins/draw/src/api/editFeature.test.js +16 -0
  172. package/plugins/draw/src/api/newPoint.js +42 -0
  173. package/plugins/draw/src/api/newPoint.test.js +117 -0
  174. package/plugins/draw/src/api/setStyle.js +43 -0
  175. package/plugins/draw/src/api/setStyle.test.js +75 -0
  176. package/plugins/draw/src/events.js +21 -14
  177. package/plugins/draw/src/events.test.js +15 -1
  178. package/plugins/draw/src/manifest.js +18 -6
  179. package/plugins/draw/src/manifest.test.js +32 -1
  180. package/plugins/draw/src/reducer.test.js +7 -0
  181. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  182. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  183. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  184. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  185. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  186. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  187. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  188. package/plugins/interact/dist/umd/index.js +1 -1
  189. package/plugins/interact/src/hooks/useCrossHairVisibility.js +12 -4
  190. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +88 -1
  191. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  192. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  193. package/plugins/interact/src/utils/featureQueries.js +18 -0
  194. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  195. package/plugins/interact/src/utils/spatial.js +9 -3
  196. package/plugins/interact/src/utils/spatial.test.js +12 -1
  197. package/plugins/map-key/dist/css/index.css +62 -0
  198. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  199. package/plugins/map-key/dist/esm/index.js +1 -0
  200. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  202. package/plugins/map-key/dist/umd/index.js +2 -0
  203. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  204. package/plugins/map-key/package.json +28 -0
  205. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  206. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  207. package/plugins/map-key/src/components/Key/Key.jsx +41 -0
  208. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
  209. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  210. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  212. package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  214. package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
  215. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  216. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
  217. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  218. package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
  220. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
  221. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  222. package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
  224. package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
  225. package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
  226. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  228. package/plugins/map-key/src/index.js +13 -0
  229. package/plugins/map-key/src/index.test.js +31 -0
  230. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  232. package/plugins/map-key/src/manifest.js +30 -0
  233. package/plugins/map-key/src/manifest.test.js +63 -0
  234. package/plugins/map-key/src/mapKey.scss +4 -0
  235. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  237. package/plugins/map-key/src/registry/index.js +3 -0
  238. package/plugins/map-key/src/registry/index.test.js +15 -0
  239. package/plugins/menu/dist/css/index.css +80 -0
  240. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  241. package/plugins/menu/dist/esm/index.js +1 -0
  242. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  243. package/plugins/menu/dist/umd/index.js +2 -0
  244. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  245. package/plugins/menu/package.json +28 -0
  246. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  247. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  249. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  250. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  251. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  252. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  254. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  255. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  257. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  258. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  259. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  260. package/plugins/menu/src/index.js +13 -0
  261. package/plugins/menu/src/index.test.js +31 -0
  262. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  263. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  264. package/plugins/menu/src/initialise/createMenu.js +10 -0
  265. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  266. package/plugins/menu/src/manifest.js +35 -0
  267. package/plugins/menu/src/manifest.test.js +63 -0
  268. package/plugins/menu/src/menu.scss +4 -0
  269. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  270. package/plugins/menu/src/reducers/pluginState.js +35 -0
  271. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  272. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  274. package/plugins/menu/src/registry/index.js +3 -0
  275. package/plugins/menu/src/registry/index.test.js +15 -0
  276. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  278. package/plugins/search/dist/css/index.css +1 -1
  279. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  280. package/plugins/search/dist/esm/index.js +1 -1
  281. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  282. package/plugins/search/dist/umd/index.js +1 -1
  283. package/plugins/search/src/Search.jsx +7 -5
  284. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  285. package/plugins/search/src/index.js +0 -1
  286. package/plugins/search/src/index.test.js +2 -4
  287. package/plugins/search/src/manifest.js +1 -1
  288. package/plugins/search/src/search.scss +17 -16
  289. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  290. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  292. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  294. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  296. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  298. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  299. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/index.js +1 -1
  301. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  302. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  303. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  304. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  305. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  306. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  307. package/providers/maplibre/src/utils/patternImages.js +1 -1
  308. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  309. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  310. package/rollup.esm.mjs +11 -1
  311. package/sonar-project.properties +7 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Attributions/Attributions.module.scss +10 -0
  314. package/src/App/components/Hints/Hints.jsx +7 -2
  315. package/src/App/components/Hints/Hints.test.jsx +13 -0
  316. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +2 -3
  317. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +3 -6
  318. package/src/App/components/{MoveControl/MoveControl.jsx → MoveControls/MoveControls.jsx} +10 -10
  319. package/src/App/components/{MoveControl/MoveControl.module.scss → MoveControls/MoveControls.module.scss} +60 -29
  320. package/src/App/components/{MoveControl/MoveControl.test.jsx → MoveControls/MoveControls.test.jsx} +48 -48
  321. package/src/App/components/Panel/Panel.jsx +18 -3
  322. package/src/App/components/Panel/Panel.module.scss +18 -8
  323. package/src/App/components/Panel/Panel.test.jsx +34 -0
  324. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  325. package/src/App/components/Viewport/Viewport.jsx +9 -1
  326. package/src/App/components/Viewport/Viewport.test.jsx +11 -0
  327. package/src/App/hooks/useFeatureItems.js +16 -7
  328. package/src/App/hooks/useFeatureItems.test.js +57 -0
  329. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  330. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  331. package/src/App/initialiseApp.js +11 -8
  332. package/src/App/initialiseApp.test.js +15 -3
  333. package/src/App/layout/Layout.jsx +12 -11
  334. package/src/App/layout/Layout.test.jsx +2 -0
  335. package/src/App/layout/layout.module.scss +31 -17
  336. package/src/App/registry/keyboardShortcutRegistry.js +25 -13
  337. package/src/App/registry/keyboardShortcutRegistry.test.js +49 -32
  338. package/src/App/registry/pluginRegistry.js +2 -3
  339. package/src/App/registry/pluginRegistry.test.js +16 -3
  340. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  341. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  342. package/src/App/renderer/mapButtons.js +28 -12
  343. package/src/App/renderer/mapButtons.test.js +17 -4
  344. package/src/App/renderer/mapControls.js +3 -1
  345. package/src/App/renderer/mapControls.test.js +16 -0
  346. package/src/App/renderer/mapPanels.js +44 -9
  347. package/src/App/renderer/mapPanels.test.js +51 -2
  348. package/src/App/renderer/orderItems.js +28 -0
  349. package/src/App/renderer/orderItems.test.js +50 -0
  350. package/src/App/renderer/slotAggregator.js +2 -18
  351. package/src/App/renderer/slotHelpers.js +3 -1
  352. package/src/App/renderer/slotHelpers.test.js +5 -0
  353. package/src/App/renderer/slots.js +3 -0
  354. package/src/App/store/AppProvider.jsx +4 -2
  355. package/src/App/store/appDispatchMiddleware.js +1 -1
  356. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  357. package/src/InteractiveMap/InteractiveMap.js +8 -2
  358. package/src/InteractiveMap/InteractiveMap.test.js +20 -3
  359. package/src/config/appConfig.js +17 -15
  360. package/src/config/appConfig.test.js +22 -22
  361. package/src/config/defaults.js +3 -1
  362. package/src/config/events.js +2 -0
  363. package/src/config/mergeConfig.js +7 -0
  364. package/src/config/mergeConfig.test.js +20 -0
  365. package/src/scss/main.scss +1 -1
  366. package/src/scss/settings/_colors.scss +1 -0
  367. package/src/scss/settings/_dimensions.scss +2 -3
  368. package/src/scss/settings/_transition.scss +1 -1
  369. package/src/services/eventBus.js +44 -0
  370. package/src/services/eventBus.test.js +83 -0
  371. package/src/services/symbolRegistry.js +1 -1
  372. package/src/test-utils.js +5 -0
  373. package/src/types.js +25 -7
  374. package/webpack.umd.mjs +2 -0
  375. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  376. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  377. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  378. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  379. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  380. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  381. package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
  382. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  383. package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
  384. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  385. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  386. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  387. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -0,0 +1,117 @@
1
+ import { newPoint } from './newPoint.js'
2
+
3
+ const makeContext = (overrides = {}) => {
4
+ const dispatch = jest.fn()
5
+ const eventBus = { emit: jest.fn() }
6
+ const draw = { setSnapLayers: jest.fn(), changeMode: jest.fn(), isSnapEnabled: jest.fn(() => false) }
7
+ const context = {
8
+ appState: { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'mouse' },
9
+ appConfig: { id: 'app' },
10
+ pluginConfig: { snapLayers: ['pc-layer'] },
11
+ pluginState: { dispatch },
12
+ mapState: { crossHair: true },
13
+ mapProvider: { draw },
14
+ services: { eventBus },
15
+ ...overrides
16
+ }
17
+ return { context, dispatch, eventBus, draw }
18
+ }
19
+
20
+ beforeEach(() => jest.clearAllMocks())
21
+
22
+ describe('newPoint', () => {
23
+ test('does nothing when there is no draw instance', () => {
24
+ const { context, eventBus } = makeContext({ mapProvider: { draw: null } })
25
+ newPoint(context, 'f1')
26
+ expect(eventBus.emit).not.toHaveBeenCalled()
27
+ })
28
+
29
+ test('starts point placement, splitting symbol-config keys out of properties', () => {
30
+ const { context, dispatch, eventBus, draw } = makeContext()
31
+
32
+ newPoint(context, 'f1', {
33
+ symbol: 'pin',
34
+ symbolBackgroundColor: { outdoor: '#1d70b8' },
35
+ properties: { name: 'x' },
36
+ extra: 'opt'
37
+ })
38
+
39
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:started', { mode: 'draw_point' })
40
+
41
+ expect(draw.changeMode).toHaveBeenCalledWith('draw_point', expect.objectContaining({
42
+ container: 'viewport',
43
+ addVertexButtonId: 'app-draw-add-point',
44
+ interfaceType: 'mouse',
45
+ crossHair: true,
46
+ featureId: 'f1',
47
+ extra: 'opt',
48
+ properties: { name: 'x', symbol: 'pin', symbolBackgroundColor: { outdoor: '#1d70b8' } }
49
+ }))
50
+
51
+ const opts = draw.changeMode.mock.calls[0][1]
52
+ expect(opts.getSnapEnabled()).toBe(false)
53
+ expect(draw.isSnapEnabled).toHaveBeenCalled()
54
+ // Symbol keys are pulled off modeOptions entirely, not just copied into properties.
55
+ expect(opts).not.toHaveProperty('symbol')
56
+ expect(opts).not.toHaveProperty('symbolBackgroundColor')
57
+
58
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: 'draw_point' })
59
+ })
60
+
61
+ test('carries every recognised symbol-config key through to properties', () => {
62
+ const { context, draw } = makeContext()
63
+ const symbolStyle = {
64
+ symbol: 'pin',
65
+ symbolSvgContent: '<path/>',
66
+ symbolBackgroundColor: '#fff',
67
+ symbolForegroundColor: '#000',
68
+ symbolHaloWidth: 2,
69
+ symbolGraphic: 'g',
70
+ symbolViewBox: '0 0 10 10',
71
+ symbolAnchor: [0.5, 0.9]
72
+ }
73
+ newPoint(context, 'f1', symbolStyle)
74
+ expect(draw.changeMode.mock.calls[0][1].properties).toEqual(symbolStyle)
75
+ })
76
+
77
+ test('omits symbol-config keys entirely when none are supplied', () => {
78
+ const { context, draw } = makeContext()
79
+ newPoint(context, 'f1')
80
+ expect(draw.changeMode.mock.calls[0][1].properties).toEqual({})
81
+ })
82
+
83
+ test('prefers explicit option snapLayers and flags them', () => {
84
+ const { context, draw, dispatch } = makeContext()
85
+ newPoint(context, 'f1', { snapLayers: ['opt'] })
86
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(['opt'])
87
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: true })
88
+ })
89
+
90
+ test('falls back to the plugin config snapLayers', () => {
91
+ const { context, draw } = makeContext()
92
+ newPoint(context, 'f1')
93
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(['pc-layer'])
94
+ })
95
+
96
+ test('falls back to null when neither option nor config is set', () => {
97
+ const { context, draw, dispatch } = makeContext({ pluginConfig: {} })
98
+ newPoint(context, 'f1')
99
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(null)
100
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: false })
101
+ })
102
+
103
+ test('stores a per-call onGeometryChange validator without leaking it into mode options', () => {
104
+ const { context, draw } = makeContext()
105
+ const onGeometryChange = jest.fn()
106
+ newPoint(context, 'f1', { onGeometryChange })
107
+ expect(draw._geometryValidator).toBe(onGeometryChange)
108
+ expect(draw.changeMode.mock.calls[0][1]).not.toHaveProperty('onGeometryChange')
109
+ })
110
+
111
+ test('falls back to the plugin-level onGeometryChange validator', () => {
112
+ const pluginOnGeometryChange = jest.fn()
113
+ const { context, draw } = makeContext({ pluginConfig: { onGeometryChange: pluginOnGeometryChange } })
114
+ newPoint(context, 'f1')
115
+ expect(draw._geometryValidator).toBe(pluginOnGeometryChange)
116
+ })
117
+ })
@@ -0,0 +1,43 @@
1
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
2
+ import { logger } from '../../../../src/services/logger.js'
3
+
4
+ const STYLE_KEYS = new Set(['stroke', 'fill', 'strokeWidth'])
5
+
6
+ // A partial patch — unlike addFeature.js building a whole new feature from scratch,
7
+ // flattenStyleProperties would otherwise write stroke/fill/strokeWidth as explicit
8
+ // `undefined` for any of the three the caller didn't mention, blanking out whatever that
9
+ // feature already had.
10
+ const definedOnly = (properties) => {
11
+ const result = {}
12
+ for (const [key, value] of Object.entries(properties)) {
13
+ if (value !== undefined) {
14
+ result[key] = value
15
+ }
16
+ }
17
+ return result
18
+ }
19
+
20
+ export const setStyle = ({ mapProvider, services }, featureId, styleChanges = {}) => {
21
+ const { draw } = mapProvider
22
+ const { eventBus } = services
23
+
24
+ if (!draw) {
25
+ return false
26
+ }
27
+
28
+ if (!draw.get(featureId)) {
29
+ logger.warn('setStyle: no feature found for id', featureId)
30
+ return false
31
+ }
32
+
33
+ const styleProps = {}
34
+ const symbolProps = {}
35
+ for (const [key, value] of Object.entries(styleChanges)) {
36
+ (STYLE_KEYS.has(key) ? styleProps : symbolProps)[key] = value
37
+ }
38
+ const properties = definedOnly({ ...flattenStyleProperties(styleProps), ...symbolProps })
39
+
40
+ draw.setStyle(featureId, properties)
41
+ eventBus.emit('draw:stylechange', { featureId, properties })
42
+ return true
43
+ }
@@ -0,0 +1,75 @@
1
+ import { setStyle } from './setStyle.js'
2
+ import { logger } from '../../../../src/services/logger.js'
3
+
4
+ jest.mock('../../../../src/services/logger.js', () => ({
5
+ logger: { warn: jest.fn(), error: jest.fn() }
6
+ }))
7
+
8
+ const setup = (draw) => {
9
+ const eventBus = { emit: jest.fn() }
10
+ return { context: { mapProvider: { draw }, services: { eventBus } }, eventBus }
11
+ }
12
+
13
+ beforeEach(() => jest.clearAllMocks())
14
+
15
+ describe('setStyle', () => {
16
+ test('does nothing when there is no draw instance', () => {
17
+ const { context, eventBus } = setup(undefined)
18
+ expect(setStyle(context, 'a', { stroke: 'red' })).toBe(false)
19
+ expect(eventBus.emit).not.toHaveBeenCalled()
20
+ })
21
+
22
+ test('warns and does nothing for an id with no existing feature', () => {
23
+ const draw = { get: jest.fn(() => null), setStyle: jest.fn() }
24
+ const { context, eventBus } = setup(draw)
25
+ expect(setStyle(context, 'missing', { stroke: 'red' })).toBe(false)
26
+ expect(draw.setStyle).not.toHaveBeenCalled()
27
+ expect(eventBus.emit).not.toHaveBeenCalled()
28
+ expect(logger.warn).toHaveBeenCalledWith('setStyle: no feature found for id', 'missing')
29
+ })
30
+
31
+ test('flattens stroke/fill/strokeWidth and forwards symbol keys as-is', () => {
32
+ const draw = { get: jest.fn(() => ({ id: 'a' })), setStyle: jest.fn() }
33
+ const { context, eventBus } = setup(draw)
34
+
35
+ const result = setStyle(context, 'a', {
36
+ stroke: { outdoor: '#1d70b8', dark: '#4c9ed9' },
37
+ symbol: 'square',
38
+ symbolBackgroundColor: { outdoor: '#ca3535', dark: '#ffffff' }
39
+ })
40
+
41
+ const expected = {
42
+ stroke: '#1d70b8',
43
+ strokeOutdoor: '#1d70b8',
44
+ strokeDark: '#4c9ed9',
45
+ symbol: 'square',
46
+ symbolBackgroundColor: { outdoor: '#ca3535', dark: '#ffffff' }
47
+ }
48
+ expect(draw.setStyle).toHaveBeenCalledWith('a', expected)
49
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:stylechange', { featureId: 'a', properties: expected })
50
+ expect(result).toBe(true)
51
+ })
52
+
53
+ // A partial patch — omitting fill/strokeWidth must leave whatever the feature already has
54
+ // alone, not blank them out to undefined.
55
+ test('only includes the style keys actually supplied, dropping the unset ones', () => {
56
+ const draw = { get: jest.fn(() => ({ id: 'a' })), setStyle: jest.fn() }
57
+ const { context } = setup(draw)
58
+ setStyle(context, 'a', { stroke: '#e6c700' })
59
+ expect(draw.setStyle).toHaveBeenCalledWith('a', { stroke: '#e6c700' })
60
+ })
61
+
62
+ test('drops keys explicitly passed as undefined, from both style and symbol props', () => {
63
+ const draw = { get: jest.fn(() => ({ id: 'a' })), setStyle: jest.fn() }
64
+ const { context } = setup(draw)
65
+ setStyle(context, 'a', { stroke: 'red', fill: undefined, symbol: 'pin', symbolBackgroundColor: undefined })
66
+ expect(draw.setStyle).toHaveBeenCalledWith('a', { stroke: 'red', symbol: 'pin' })
67
+ })
68
+
69
+ test('defaults styleChanges to {} and still resolves cleanly', () => {
70
+ const draw = { get: jest.fn(() => ({ id: 'a' })), setStyle: jest.fn() }
71
+ const { context } = setup(draw)
72
+ setStyle(context, 'a')
73
+ expect(draw.setStyle).toHaveBeenCalledWith('a', {})
74
+ })
75
+ })
@@ -9,10 +9,17 @@ import { ADAPTER_EVENTS } from './adapterEvents.js'
9
9
  import { validateGeometry } from './validation/validateGeometry.js'
10
10
  import { MIN_VERTICES_REASONS } from './validation/rules.js'
11
11
  import { MAP_SIZE_SCALES } from './defaults.js'
12
+ import { stripInternalProperties } from './utils/stripInternalProperties.js'
12
13
 
13
14
  const EDIT_VERTEX_MODE = 'edit_vertex'
14
15
  const GEOMETRY_INVALID_EVENT = 'draw:geometryinvalid'
15
16
 
17
+ // edit_point behaves like edit_vertex for the two checks below (restore-on-cancel, live
18
+ // invalid-stroke gating) — a relocate session, just with one coordinate instead of a ring.
19
+ // enterEditVertexMode (below) stays edit_vertex-only — it's only ever reached for a shape
20
+ // that finished invalid, and Point geometry has no such failure mode (see rules.js).
21
+ const EDIT_MODES = new Set([EDIT_VERTEX_MODE, 'edit_point'])
22
+
16
23
  // A shape that simply hasn't reached its minimum vertex count yet isn't a mistake —
17
24
  // it's the normal, expected state of "still being drawn" — so it's excluded below
18
25
  // even though it's a genuine SOFT_RULES failure like any other.
@@ -29,8 +36,8 @@ const createGeometryInvalidEmitter = (eventBus, hints) => (payload) => {
29
36
  if (payload.reason && !isIncompleteShape(payload.reason)) { hints.show(payload.reason) }
30
37
  }
31
38
 
32
- // Claims MoveControl's D-pad for the currently selected vertex — see the generic
33
- // mapProvider.activeMoveTarget contract (MoveControl.jsx). Any plugin could use
39
+ // Claims MoveControls' D-pad for the currently selected vertex — see the generic
40
+ // mapProvider.activeMoveTarget contract (MoveControls.jsx). Any plugin could use
34
41
  // this slot; the draw plugin is just its first consumer.
35
42
  const buildVertexMoveTarget = (draw) => ({
36
43
  move: (dx, dy, isLargeStep) => draw.nudgeSelectedVertex(dx, dy, isLargeStep),
@@ -38,7 +45,7 @@ const buildVertexMoveTarget = (draw) => ({
38
45
  })
39
46
 
40
47
  // Vertex-selection handlers: sync pluginState.selectedVertexIndex, and claim/release
41
- // MoveControl's D-pad via buildVertexMoveTarget above.
48
+ // MoveControls' D-pad via buildVertexMoveTarget above.
42
49
  const createVertexSelectionHandlers = ({ draw, pluginState, mapProvider, eventBus }) => ({
43
50
  onVertexSelection: (e) => {
44
51
  pluginState.dispatch({ type: 'SET_SELECTED_VERTEX_INDEX', payload: e })
@@ -86,10 +93,10 @@ function createHandlers ({ appState, appConfig, mapState, pluginState, mapProvid
86
93
  handleDone: () => { draw.done() },
87
94
  handleCancel: () => {
88
95
  const mode = draw.getMode()
89
- if (mode === EDIT_VERTEX_MODE && tempFeature?.id) { draw.add(feature) }
96
+ if (EDIT_MODES.has(mode) && tempFeature?.id) { draw.add(feature) }
90
97
  pendingCreateId = null
91
98
  draw.cancel(); resetState()
92
- eventBus.emit('draw:cancelled', feature)
99
+ eventBus.emit('draw:cancelled', stripInternalProperties(feature))
93
100
  },
94
101
  handleUndo: () => draw.undo(),
95
102
  handleDeleteVertex: () => draw.deleteVertex(),
@@ -104,11 +111,11 @@ function createHandlers ({ appState, appConfig, mapState, pluginState, mapProvid
104
111
  const { valid, reason } = validateGeometry(f, { phase: 'create', mode: draw.getMode() }, { onGeometryChange: draw._geometryValidator })
105
112
  if (!valid) {
106
113
  pendingCreateId = f.id
107
- emitGeometryInvalid({ feature: f, reason, phase: 'create', mode: EDIT_VERTEX_MODE })
114
+ emitGeometryInvalid({ feature: stripInternalProperties(f), reason, phase: 'create', mode: EDIT_VERTEX_MODE })
108
115
  setTimeout(() => enterEditVertexMode({ draw, appState, appConfig, mapState, dispatch }, f.id), 0)
109
116
  return
110
117
  }
111
- resetState(); setTimeout(() => draw.changeMode('disabled'), 0); eventBus.emit('draw:created', f)
118
+ resetState(); setTimeout(() => draw.changeMode('disabled'), 0); eventBus.emit('draw:created', stripInternalProperties(f))
112
119
  },
113
120
  onEditFinish: (f) => {
114
121
  resetState()
@@ -116,15 +123,15 @@ function createHandlers ({ appState, appConfig, mapState, pluginState, mapProvid
116
123
  // A shape that was drawn-then-fixed reports as a creation, not an edit.
117
124
  if (pendingCreateId && f.id === pendingCreateId) {
118
125
  pendingCreateId = null
119
- eventBus.emit('draw:created', f)
126
+ eventBus.emit('draw:created', stripInternalProperties(f))
120
127
  } else {
121
- eventBus.emit('draw:edited', f)
128
+ eventBus.emit('draw:edited', stripInternalProperties(f))
122
129
  }
123
130
  },
124
131
  onCancel: () => {},
125
132
  ...createVertexSelectionHandlers({ draw, pluginState, mapProvider, eventBus }),
126
133
  onUndoChange: (l) => { pluginState.dispatch({ type: 'SET_UNDO_STACK_LENGTH', payload: l }) },
127
- onUpdate: (f) => { eventBus.emit('draw:updated', f) },
134
+ onUpdate: (f) => { eventBus.emit('draw:updated', stripInternalProperties(f)) },
128
135
  onGeometryChange: (e) => {
129
136
  // Only commit-level changes (commit-add/move/insert/delete) carry a `phase`.
130
137
  // Preview events (e.g. split's live preview) have none and are ignored.
@@ -141,17 +148,17 @@ function createHandlers ({ appState, appConfig, mapState, pluginState, mapProvid
141
148
  // The invalid stroke is committed-validity-driven in edit mode only; in draw
142
149
  // mode the adapters drive it live from the displayed geometry (placed
143
150
  // vertices + cursor) on every rubber-band move.
144
- if (mode === EDIT_VERTEX_MODE) {
151
+ if (EDIT_MODES.has(mode)) {
145
152
  draw.setInvalid?.(!valid)
146
153
  }
147
154
  if (!valid) {
148
- emitGeometryInvalid({ reason, ...context, feature: e.feature })
155
+ emitGeometryInvalid({ reason, ...context, feature: stripInternalProperties(e.feature) })
149
156
  }
150
157
  },
151
158
  // A vertex placement was rejected (hard rule or user callback veto). Surface it
152
159
  // on the public bus with phase 'place', and as a hint toast, so the reason is visible.
153
160
  onPlacementBlocked: (e) => {
154
- emitGeometryInvalid(e)
161
+ emitGeometryInvalid({ ...e, feature: stripInternalProperties(e.feature) })
155
162
  },
156
163
  // Live (mid-drag) validity flip while editing — the displayed shape is exactly
157
164
  // what Done finishes there, so it gates the Done button in real time. Emitted
@@ -226,7 +233,7 @@ export function attachEvents ({ appState, appConfig, mapState, pluginState, mapP
226
233
  const resetState = () => {
227
234
  pluginState.dispatch({ type: 'SET_MODE', payload: null })
228
235
  pluginState.dispatch({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null } })
229
- // Release MoveControl's D-pad back to panning the map whenever a draw/edit
236
+ // Release MoveControls' D-pad back to panning the map whenever a draw/edit
230
237
  // session ends — a stale claim here would silently hijack it for good.
231
238
  mapProvider.activeMoveTarget = null
232
239
  }
@@ -88,6 +88,13 @@ describe('button handlers', () => {
88
88
  expect(mapProvider.activeMoveTarget).toBeNull()
89
89
  })
90
90
 
91
+ test('cancel re-adds the feature when cancelling a point edit too', () => {
92
+ const { buttonConfig, draw } = setup()
93
+ draw.getMode.mockReturnValue('edit_point')
94
+ buttonConfig.drawCancel.onClick()
95
+ expect(draw.add).toHaveBeenCalledWith({ id: 'F' })
96
+ })
97
+
91
98
  test('cancel does not re-add outside a vertex edit', () => {
92
99
  const { buttonConfig, draw } = setup()
93
100
  draw.getMode.mockReturnValue('draw_polygon')
@@ -172,7 +179,7 @@ describe('draw event handlers', () => {
172
179
  expect(() => drawHandler(draw, 'cancel')()).not.toThrow()
173
180
  })
174
181
 
175
- test('vertexselection dispatches, emits, and claims mapProvider.activeMoveTarget for MoveControl', () => {
182
+ test('vertexselection dispatches, emits, and claims mapProvider.activeMoveTarget for MoveControls', () => {
176
183
  const { draw, dispatch, eventBus, mapProvider } = setup()
177
184
  drawHandler(draw, 'vertexselection')({ index: 2 })
178
185
  expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_VERTEX_INDEX', payload: { index: 2 } })
@@ -308,6 +315,13 @@ describe('geometrychange validation', () => {
308
315
  expect(draw.setInvalid).toHaveBeenCalledWith(true)
309
316
  })
310
317
 
318
+ test('drives the invalid stroke from committed validity in a point edit too', () => {
319
+ const { draw } = setup()
320
+ draw.getMode.mockReturnValue('edit_point')
321
+ drawHandler(draw, 'geometrychange')({ feature: squareFeature, phase: 'commit-move', vertexIndex: 0 })
322
+ expect(draw.setInvalid).toHaveBeenCalledWith(false)
323
+ })
324
+
311
325
  test('a live validity flip (edit drag) drives the Done gate', () => {
312
326
  const { draw, dispatch } = setup()
313
327
  drawHandler(draw, 'validitychange')({ valid: false, reason: 'Shape must not intersect itself' })
@@ -2,8 +2,10 @@ import { initialState, actions } from './reducer.js'
2
2
  import { DrawInit } from './DrawInit.jsx'
3
3
  import { newPolygon } from './api/newPolygon.js'
4
4
  import { newLine } from './api/newLine.js'
5
+ import { newPoint } from './api/newPoint.js'
5
6
  import { editFeature } from './api/editFeature.js'
6
7
  import { addFeature } from './api/addFeature.js'
8
+ import { setStyle } from './api/setStyle.js'
7
9
  import { deleteFeature } from './api/deleteFeature.js'
8
10
  import { split } from './api/split.js'
9
11
  import { merge } from './api/merge.js'
@@ -11,6 +13,11 @@ import { isMac } from '../../../src/utils/isMac.js'
11
13
 
12
14
  const DRAW_ACTIONS_SLOT = 'top-middle'
13
15
 
16
+ // edit_point behaves like edit_vertex for Done/Menu/Undo, but stays out of drawDeletePoint and mergeShapes (neither applies to a single coordinate).
17
+ const EDIT_MODES = new Set(['edit_vertex', 'edit_point'])
18
+ // .has(), not spread — [...aSet] can silently misbehave under a loose-mode Babel build (see OLDrawManager.js's emit()).
19
+ const isEditMode = (mode) => EDIT_MODES.has(mode)
20
+
14
21
  // Show the platform-appropriate undo modifier (⌘ on macOS, Ctrl elsewhere).
15
22
  const undoCommand = isMac() ? '<kbd>Command</kbd> + <kbd>Z</kbd>' : '<kbd>Ctrl</kbd> + <kbd>Z</kbd>'
16
23
 
@@ -43,7 +50,7 @@ export const manifest = {
43
50
  variant: 'primary',
44
51
  exclusiveSlot: true,
45
52
  hiddenWhen: ({ appState, pluginState }) =>
46
- !['draw_polygon', 'draw_line'].includes(pluginState.mode) || appState.interfaceType !== 'touch',
53
+ !['draw_polygon', 'draw_line', 'draw_point'].includes(pluginState.mode) || appState.interfaceType !== 'touch',
47
54
  // Disabled while placing at the crosshair would be vetoed (validatePlacement) —
48
55
  // driven live so the button never looks active when a tap would do nothing.
49
56
  enableWhen: ({ pluginState }) => pluginState.canAddPoint,
@@ -54,27 +61,30 @@ export const manifest = {
54
61
  label: 'Done',
55
62
  variant: 'primary',
56
63
  exclusiveSlot: true,
57
- hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
64
+ hiddenWhen: ({ pluginState }) => !(['draw_polygon', 'draw_line'].includes(pluginState.mode) || isEditMode(pluginState.mode)),
58
65
  enableWhen: ({ pluginState }) => {
59
66
  const { mode, geometryValid } = pluginState
60
67
  // Min-vertices, area and self-intersection are all enforced by the validation
61
68
  // rules via geometryValid, so the gate is simply "is the geometry valid now".
62
- return ['draw_polygon', 'draw_line', 'edit_vertex'].includes(mode) && geometryValid
69
+ return (['draw_polygon', 'draw_line'].includes(mode) || isEditMode(mode)) && geometryValid
63
70
  },
64
71
  ...createButtonSlots(true)
65
72
  },
66
73
  {
67
74
  id: 'drawMenu',
68
- label: ({ pluginState }) => pluginState.mode === 'edit_vertex' ? 'Edit actions' : 'Draw actions',
75
+ label: ({ pluginState }) => isEditMode(pluginState.mode) ? 'Edit actions' : 'Draw actions',
69
76
  iconId: 'menu',
70
77
  exclusiveSlot: true,
71
- hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
78
+ // draw_point belongs here too it has no Undo (nothing to undo before a single-click
79
+ // commit, gated separately below) and no delete-vertex, but it DOES support snapping,
80
+ // and the Snap toggle is a menuItem living inside this same button.
81
+ hiddenWhen: ({ pluginState }) => !(['draw_polygon', 'draw_line', 'draw_point'].includes(pluginState.mode) || isEditMode(pluginState.mode)),
72
82
  menuItems: [
73
83
  {
74
84
  id: 'drawUndo',
75
85
  label: 'Undo',
76
86
  iconId: 'undo',
77
- hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
87
+ hiddenWhen: ({ pluginState }) => !(['draw_polygon', 'draw_line'].includes(pluginState.mode) || isEditMode(pluginState.mode)),
78
88
  enableWhen: ({ pluginState }) => {
79
89
  if (['draw_polygon', 'draw_line'].includes(pluginState.mode)) {
80
90
  return pluginState.numVertices > 0
@@ -161,8 +171,10 @@ export const manifest = {
161
171
  api: {
162
172
  newPolygon,
163
173
  newLine,
174
+ newPoint,
164
175
  editFeature,
165
176
  addFeature,
177
+ setStyle,
166
178
  deleteFeature,
167
179
  split,
168
180
  merge
@@ -9,7 +9,7 @@ describe('manifest structure', () => {
9
9
  expect(manifest.reducer).toHaveProperty('actions')
10
10
  expect(manifest.InitComponent).toBeDefined()
11
11
  expect(Object.keys(manifest.api)).toEqual(expect.arrayContaining([
12
- 'newPolygon', 'newLine', 'editFeature', 'addFeature', 'deleteFeature', 'split', 'merge'
12
+ 'newPolygon', 'newLine', 'newPoint', 'editFeature', 'addFeature', 'setStyle', 'deleteFeature', 'split', 'merge'
13
13
  ]))
14
14
  })
15
15
  })
@@ -44,12 +44,14 @@ describe('drawDone', () => {
44
44
  test('is hidden outside draw/edit modes', () => {
45
45
  expect(btn().hiddenWhen({ pluginState: { mode: null } })).toBe(true)
46
46
  expect(btn().hiddenWhen({ pluginState: { mode: 'draw_polygon' } })).toBe(false)
47
+ expect(btn().hiddenWhen({ pluginState: { mode: 'edit_point' } })).toBe(false)
47
48
  })
48
49
 
49
50
  test('enables in a draw/edit mode only when the geometry is valid', () => {
50
51
  expect(btn().enableWhen({ pluginState: { mode: 'draw_polygon', geometryValid: true } })).toBe(true)
51
52
  expect(btn().enableWhen({ pluginState: { mode: 'draw_line', geometryValid: true } })).toBe(true)
52
53
  expect(btn().enableWhen({ pluginState: { mode: 'edit_vertex', geometryValid: true } })).toBe(true)
54
+ expect(btn().enableWhen({ pluginState: { mode: 'edit_point', geometryValid: true } })).toBe(true)
53
55
  expect(btn().enableWhen({ pluginState: { mode: 'disabled', geometryValid: true } })).toBe(false)
54
56
  })
55
57
 
@@ -57,6 +59,7 @@ describe('drawDone', () => {
57
59
  expect(btn().enableWhen({ pluginState: { mode: 'draw_polygon', geometryValid: false } })).toBe(false)
58
60
  expect(btn().enableWhen({ pluginState: { mode: 'draw_line', geometryValid: false } })).toBe(false)
59
61
  expect(btn().enableWhen({ pluginState: { mode: 'edit_vertex', geometryValid: false } })).toBe(false)
62
+ expect(btn().enableWhen({ pluginState: { mode: 'edit_point', geometryValid: false } })).toBe(false)
60
63
  })
61
64
  })
62
65
 
@@ -66,12 +69,24 @@ describe('drawMenu', () => {
66
69
  expect(findButton('drawMenu').hiddenWhen({ pluginState: { mode: 'edit_vertex' } })).toBe(false)
67
70
  })
68
71
 
72
+ // draw_point has no Undo and no delete-vertex, but it does support snapping, and Snap is
73
+ // a menuItem inside this same button — so the button itself must stay visible for it even
74
+ // though drawUndo/drawDeletePoint (below) correctly keep excluding it.
75
+ test('is visible during draw_point, for the Snap toggle, even though it has no undo/delete', () => {
76
+ expect(findButton('drawMenu').hiddenWhen({ pluginState: { mode: 'draw_point' } })).toBe(false)
77
+ })
78
+
69
79
  test('labels "Edit actions" in edit mode, "Draw actions" otherwise', () => {
70
80
  expect(findButton('drawMenu').label({ pluginState: { mode: 'edit_vertex' } })).toBe('Edit actions')
81
+ expect(findButton('drawMenu').label({ pluginState: { mode: 'edit_point' } })).toBe('Edit actions')
71
82
  expect(findButton('drawMenu').label({ pluginState: { mode: 'draw_polygon' } })).toBe('Draw actions')
72
83
  expect(findButton('drawMenu').label({ pluginState: { mode: 'draw_line' } })).toBe('Draw actions')
73
84
  })
74
85
 
86
+ test('is visible during edit_point', () => {
87
+ expect(findButton('drawMenu').hiddenWhen({ pluginState: { mode: 'edit_point' } })).toBe(false)
88
+ })
89
+
75
90
  describe('drawUndo', () => {
76
91
  const item = () => findMenuItem('drawMenu', 'drawUndo')
77
92
 
@@ -80,11 +95,21 @@ describe('drawMenu', () => {
80
95
  expect(item().hiddenWhen({ pluginState: { mode: 'draw_line' } })).toBe(false)
81
96
  })
82
97
 
98
+ test('is visible in edit_point', () => {
99
+ expect(item().hiddenWhen({ pluginState: { mode: 'edit_point' } })).toBe(false)
100
+ })
101
+
102
+ test('stays hidden for draw_point — nothing to undo before a single-click commit', () => {
103
+ expect(item().hiddenWhen({ pluginState: { mode: 'draw_point' } })).toBe(true)
104
+ })
105
+
83
106
  test('enables from vertex count while drawing and from the undo stack while editing', () => {
84
107
  expect(item().enableWhen({ pluginState: { mode: 'draw_polygon', numVertices: 1 } })).toBe(true)
85
108
  expect(item().enableWhen({ pluginState: { mode: 'draw_polygon', numVertices: 0 } })).toBe(false)
86
109
  expect(item().enableWhen({ pluginState: { mode: 'edit_vertex', undoStackLength: 2 } })).toBe(true)
87
110
  expect(item().enableWhen({ pluginState: { mode: 'edit_vertex', undoStackLength: 0 } })).toBe(false)
111
+ expect(item().enableWhen({ pluginState: { mode: 'edit_point', undoStackLength: 1 } })).toBe(true)
112
+ expect(item().enableWhen({ pluginState: { mode: 'edit_point', undoStackLength: 0 } })).toBe(false)
88
113
  })
89
114
  })
90
115
 
@@ -111,6 +136,12 @@ describe('drawMenu', () => {
111
136
  expect(item().hiddenWhen({ pluginState: { mode: 'edit_vertex' } })).toBe(false)
112
137
  })
113
138
 
139
+ // Deliberately excluded from edit_point — deleting a point's one coordinate is deleting
140
+ // the whole feature (deleteFeature's job), not a per-vertex action.
141
+ test('stays hidden for edit_point', () => {
142
+ expect(item().hiddenWhen({ pluginState: { mode: 'edit_point' } })).toBe(true)
143
+ })
144
+
114
145
  test('enables only with a selection and enough vertices (polygon vs line)', () => {
115
146
  expect(item().enableWhen({ pluginState: { selectedVertexIndex: -1 } })).toBe(false)
116
147
  expect(item().enableWhen({ pluginState: { selectedVertexIndex: 0, feature: { geometry: { type: 'Polygon' } }, numVertices: 4 } })).toBe(true)
@@ -77,6 +77,13 @@ describe('SET_UNDO_STACK_LENGTH', () => {
77
77
  })
78
78
  })
79
79
 
80
+ describe('SET_GEOMETRY_VALID', () => {
81
+ test('coerces the payload to a boolean', () => {
82
+ expect(actions.SET_GEOMETRY_VALID(initialState, false).geometryValid).toBe(false)
83
+ expect(actions.SET_GEOMETRY_VALID(initialState, 1).geometryValid).toBe(true)
84
+ })
85
+ })
86
+
80
87
  describe('SET_CAN_ADD_POINT', () => {
81
88
  test('coerces the payload to a boolean', () => {
82
89
  expect(actions.SET_CAN_ADD_POINT(initialState, false).canAddPoint).toBe(false)
@@ -0,0 +1,19 @@
1
+ import { SYMBOL_KEYS } from './symbolKeys.js'
2
+
3
+ // Drops adapter-derived bookkeeping from a feature before it reaches a public event:
4
+ // symbol* keys not in SYMBOL_KEYS (e.g. symbolImageId, symbolPixelRatio), and `sortKey`
5
+ // (drives fill/line/symbol-sort-key render order — see MaplibreDrawAdapter.js — never
6
+ // caller-supplied or meaningful application data).
7
+ export const stripInternalProperties = (feature) => {
8
+ if (!feature?.properties) {
9
+ return feature
10
+ }
11
+ const properties = {}
12
+ for (const [key, value] of Object.entries(feature.properties)) {
13
+ const isInternal = key === 'sortKey' || (key.startsWith('symbol') && !SYMBOL_KEYS.has(key))
14
+ if (!isInternal) {
15
+ properties[key] = value
16
+ }
17
+ }
18
+ return { ...feature, properties }
19
+ }
@@ -0,0 +1,50 @@
1
+ import { stripInternalProperties } from './stripInternalProperties.js'
2
+
3
+ describe('stripInternalProperties', () => {
4
+ test('drops adapter-derived symbol* keys not in SYMBOL_KEYS', () => {
5
+ const feature = {
6
+ id: 'a',
7
+ properties: {
8
+ symbol: 'pin',
9
+ symbolBackgroundColor: '#ca3535',
10
+ symbolImageId: 'symbol-abc-2x',
11
+ symbolIconAnchor: 'bottom',
12
+ symbolIconOffset: [0, -10],
13
+ symbolPixelRatio: 2,
14
+ symbolActiveImageId: 'symbol-act-abc-2x',
15
+ symbolSelectedImageId: 'symbol-sel-abc-2x'
16
+ }
17
+ }
18
+
19
+ expect(stripInternalProperties(feature)).toEqual({
20
+ id: 'a',
21
+ properties: { symbol: 'pin', symbolBackgroundColor: '#ca3535' }
22
+ })
23
+ })
24
+
25
+ test('drops the internal sortKey property', () => {
26
+ const feature = { id: 'a', properties: { label: 'My point', sortKey: 3 } }
27
+ expect(stripInternalProperties(feature)).toEqual({ id: 'a', properties: { label: 'My point' } })
28
+ })
29
+
30
+ test('leaves non-symbol-prefixed properties untouched', () => {
31
+ const feature = { id: 'a', properties: { label: 'My point', stroke: '#1d70b8' } }
32
+ expect(stripInternalProperties(feature)).toEqual(feature)
33
+ })
34
+
35
+ test('does not mutate the original feature', () => {
36
+ const feature = { id: 'a', properties: { symbol: 'pin', symbolImageId: 'x' } }
37
+ stripInternalProperties(feature)
38
+ expect(feature.properties.symbolImageId).toBe('x')
39
+ })
40
+
41
+ test('returns the feature unchanged when it has no properties', () => {
42
+ const feature = { id: 'a', geometry: { type: 'Point', coordinates: [0, 0] } }
43
+ expect(stripInternalProperties(feature)).toBe(feature)
44
+ })
45
+
46
+ test('returns null/undefined as-is', () => {
47
+ expect(stripInternalProperties(null)).toBeNull()
48
+ expect(stripInternalProperties(undefined)).toBeUndefined()
49
+ })
50
+ })
@@ -0,0 +1,5 @@
1
+ // Public symbol-config keys a caller can set on a Point (see docs/api/symbol-config.md).
2
+ export const SYMBOL_KEYS = new Set([
3
+ 'symbol', 'symbolSvgContent', 'symbolBackgroundColor', 'symbolForegroundColor',
4
+ 'symbolHaloWidth', 'symbolGraphic', 'symbolViewBox', 'symbolAnchor'
5
+ ])