@defra/interactive-map 0.0.40-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 (351) 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/assets/images/slot-map.svg +61 -262
  9. package/docs/examples/draw-tools.mdx +3 -3
  10. package/docs/plugins/draw.md +79 -16
  11. package/docs/plugins/search.md +1 -1
  12. package/govuk-prototype-kit.config.json +2 -0
  13. package/jest.setup.js +5 -0
  14. package/package.json +9 -1
  15. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  18. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  19. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  20. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  21. package/plugins/datasets/dist/css/index.css +1 -1
  22. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  23. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  24. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  25. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  26. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  27. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  28. package/plugins/datasets/dist/umd/index.js +1 -1
  29. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  30. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  31. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  32. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  33. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  34. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  35. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  36. package/plugins/datasets/src/datasets.scss +0 -1
  37. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  38. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  39. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  40. package/plugins/datasets/src/index.test.js +36 -0
  41. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  42. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  43. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  44. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  45. package/plugins/datasets/src/manifest.js +0 -38
  46. package/plugins/datasets/src/manifest.test.js +49 -0
  47. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  48. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  49. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  50. package/plugins/datasets/src/registry/dataset.js +13 -1
  51. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  52. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  53. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  54. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  55. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  56. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  57. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  58. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  59. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  60. package/plugins/draw/dist/esm/index.js +1 -1
  61. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  62. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  63. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  64. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  65. package/plugins/draw/dist/umd/index.js +1 -1
  66. package/plugins/draw/src/DrawInit.jsx +10 -2
  67. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  68. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  69. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  70. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  71. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  72. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  74. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  75. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  91. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  92. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  93. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  94. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  95. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  96. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  97. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  98. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  99. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  101. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  102. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  103. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  104. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  105. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  106. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  107. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  108. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  109. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  110. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  111. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  112. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  113. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  114. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  115. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  116. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  117. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  118. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  121. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  122. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  123. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  124. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  125. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  126. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  127. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  128. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  129. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  130. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  132. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  134. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  136. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  137. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  139. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  140. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  141. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  142. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  143. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  144. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  145. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  146. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  147. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  148. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  149. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  150. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  151. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  153. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  154. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  155. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  156. package/plugins/draw/src/api/addFeature.js +16 -1
  157. package/plugins/draw/src/api/addFeature.test.js +62 -5
  158. package/plugins/draw/src/api/editFeature.js +7 -4
  159. package/plugins/draw/src/api/editFeature.test.js +16 -0
  160. package/plugins/draw/src/api/newPoint.js +42 -0
  161. package/plugins/draw/src/api/newPoint.test.js +117 -0
  162. package/plugins/draw/src/api/setStyle.js +43 -0
  163. package/plugins/draw/src/api/setStyle.test.js +75 -0
  164. package/plugins/draw/src/events.js +17 -10
  165. package/plugins/draw/src/events.test.js +14 -0
  166. package/plugins/draw/src/manifest.js +18 -6
  167. package/plugins/draw/src/manifest.test.js +32 -1
  168. package/plugins/draw/src/reducer.test.js +7 -0
  169. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  170. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  171. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  172. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  173. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  174. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  175. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  176. package/plugins/interact/dist/umd/index.js +1 -1
  177. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  178. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  179. package/plugins/interact/src/utils/featureQueries.js +18 -0
  180. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  181. package/plugins/interact/src/utils/spatial.js +9 -3
  182. package/plugins/interact/src/utils/spatial.test.js +12 -1
  183. package/plugins/map-key/dist/css/index.css +62 -0
  184. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  185. package/plugins/map-key/dist/esm/index.js +1 -0
  186. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  187. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  188. package/plugins/map-key/dist/umd/index.js +2 -0
  189. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  190. package/plugins/map-key/package.json +28 -0
  191. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  192. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  193. package/plugins/map-key/src/components/Key/Key.jsx +41 -0
  194. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
  195. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  196. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  197. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  198. package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
  199. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  200. package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
  201. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  202. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
  203. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  204. package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
  205. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
  206. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  208. package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
  210. package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
  211. package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
  212. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  213. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  214. package/plugins/map-key/src/index.js +13 -0
  215. package/plugins/map-key/src/index.test.js +31 -0
  216. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  217. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  218. package/plugins/map-key/src/manifest.js +30 -0
  219. package/plugins/map-key/src/manifest.test.js +63 -0
  220. package/plugins/map-key/src/mapKey.scss +4 -0
  221. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  222. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  223. package/plugins/map-key/src/registry/index.js +3 -0
  224. package/plugins/map-key/src/registry/index.test.js +15 -0
  225. package/plugins/menu/dist/css/index.css +80 -0
  226. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  227. package/plugins/menu/dist/esm/index.js +1 -0
  228. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  229. package/plugins/menu/dist/umd/index.js +2 -0
  230. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  231. package/plugins/menu/package.json +28 -0
  232. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  233. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  234. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  235. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  236. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  237. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  238. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  239. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  240. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  241. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  242. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  243. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  244. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  245. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  246. package/plugins/menu/src/index.js +13 -0
  247. package/plugins/menu/src/index.test.js +31 -0
  248. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  249. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  250. package/plugins/menu/src/initialise/createMenu.js +10 -0
  251. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  252. package/plugins/menu/src/manifest.js +35 -0
  253. package/plugins/menu/src/manifest.test.js +63 -0
  254. package/plugins/menu/src/menu.scss +4 -0
  255. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  256. package/plugins/menu/src/reducers/pluginState.js +35 -0
  257. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  258. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  259. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  260. package/plugins/menu/src/registry/index.js +3 -0
  261. package/plugins/menu/src/registry/index.test.js +15 -0
  262. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  263. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  264. package/plugins/search/dist/css/index.css +1 -1
  265. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  266. package/plugins/search/dist/esm/index.js +1 -1
  267. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  268. package/plugins/search/dist/umd/index.js +1 -1
  269. package/plugins/search/src/Search.jsx +2 -3
  270. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  271. package/plugins/search/src/index.js +0 -1
  272. package/plugins/search/src/index.test.js +2 -4
  273. package/plugins/search/src/manifest.js +1 -1
  274. package/plugins/search/src/search.scss +17 -16
  275. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  276. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  277. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  278. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  279. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  280. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  281. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  282. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  283. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  284. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  285. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  286. package/providers/maplibre/dist/umd/index.js +1 -1
  287. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  288. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  289. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  290. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  291. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  292. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  293. package/providers/maplibre/src/utils/patternImages.js +1 -1
  294. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  295. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  296. package/rollup.esm.mjs +11 -1
  297. package/src/App/components/Actions/Actions.test.jsx +47 -0
  298. package/src/App/components/Hints/Hints.test.jsx +13 -0
  299. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  300. package/src/App/components/Panel/Panel.jsx +18 -3
  301. package/src/App/components/Panel/Panel.module.scss +18 -8
  302. package/src/App/components/Panel/Panel.test.jsx +34 -0
  303. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  304. package/src/App/hooks/useFeatureItems.js +16 -7
  305. package/src/App/hooks/useFeatureItems.test.js +57 -0
  306. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  307. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  308. package/src/App/layout/Layout.jsx +9 -10
  309. package/src/App/layout/Layout.test.jsx +2 -0
  310. package/src/App/layout/layout.module.scss +31 -17
  311. package/src/App/registry/pluginRegistry.js +1 -1
  312. package/src/App/registry/pluginRegistry.test.js +12 -0
  313. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  314. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  315. package/src/App/renderer/mapButtons.js +28 -12
  316. package/src/App/renderer/mapButtons.test.js +17 -4
  317. package/src/App/renderer/mapControls.js +3 -1
  318. package/src/App/renderer/mapControls.test.js +16 -0
  319. package/src/App/renderer/mapPanels.js +44 -9
  320. package/src/App/renderer/mapPanels.test.js +51 -2
  321. package/src/App/renderer/orderItems.js +28 -0
  322. package/src/App/renderer/orderItems.test.js +50 -0
  323. package/src/App/renderer/slotAggregator.js +2 -18
  324. package/src/App/renderer/slotHelpers.js +3 -1
  325. package/src/App/renderer/slotHelpers.test.js +5 -0
  326. package/src/App/renderer/slots.js +3 -0
  327. package/src/App/store/AppProvider.jsx +1 -0
  328. package/src/App/store/appDispatchMiddleware.js +1 -1
  329. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  330. package/src/config/events.js +2 -0
  331. package/src/scss/settings/_colors.scss +1 -0
  332. package/src/scss/settings/_dimensions.scss +2 -3
  333. package/src/scss/settings/_transition.scss +1 -1
  334. package/src/services/eventBus.js +44 -0
  335. package/src/services/eventBus.test.js +83 -0
  336. package/src/services/symbolRegistry.js +1 -1
  337. package/src/types.js +11 -1
  338. package/webpack.umd.mjs +2 -0
  339. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  340. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  341. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  342. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  343. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  344. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  345. package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
  346. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  347. package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
  348. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  349. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  350. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  351. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -1,4 +1,13 @@
1
1
  import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
2
+ import { stripInternalProperties } from '../utils/stripInternalProperties.js'
3
+ import { logger } from '../../../../src/services/logger.js'
4
+
5
+ const SUPPORTED_GEOMETRY_TYPES = new Set(['Point', 'LineString', 'Polygon'])
6
+
7
+ // type: 'Feature' is defaulted below (both adapters mishandle it missing), but a missing or
8
+ // unsupported geometry is a genuine caller mistake, so that's rejected instead.
9
+ const isValidFeature = (feature) =>
10
+ SUPPORTED_GEOMETRY_TYPES.has(feature.geometry?.type) && feature.geometry?.coordinates !== undefined
2
11
 
3
12
  export const addFeature = ({ mapProvider, services }, feature) => {
4
13
  const { draw } = mapProvider
@@ -10,6 +19,7 @@ export const addFeature = ({ mapProvider, services }, feature) => {
10
19
 
11
20
  const { stroke, fill, strokeWidth, properties, ...featureRest } = feature
12
21
  const flatFeature = {
22
+ type: 'Feature',
13
23
  ...featureRest,
14
24
  properties: {
15
25
  ...properties,
@@ -17,6 +27,11 @@ export const addFeature = ({ mapProvider, services }, feature) => {
17
27
  }
18
28
  }
19
29
 
30
+ if (!isValidFeature(flatFeature)) {
31
+ logger.warn('addFeature: ignoring invalid GeoJSON feature', flatFeature)
32
+ return
33
+ }
34
+
20
35
  draw.add(flatFeature)
21
- eventBus.emit('draw:add', flatFeature)
36
+ eventBus.emit('draw:add', stripInternalProperties(flatFeature))
22
37
  }
@@ -1,9 +1,13 @@
1
1
  import { addFeature } from './addFeature.js'
2
2
  import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
3
+ import { logger } from '../../../../src/services/logger.js'
3
4
 
4
5
  jest.mock('../utils/flattenStyleProperties.js', () => ({
5
6
  flattenStyleProperties: jest.fn(() => ({ _flat: true }))
6
7
  }))
8
+ jest.mock('../../../../src/services/logger.js', () => ({
9
+ logger: { warn: jest.fn(), error: jest.fn() }
10
+ }))
7
11
 
8
12
  const setup = (draw) => {
9
13
  const eventBus = { emit: jest.fn() }
@@ -19,15 +23,27 @@ describe('addFeature', () => {
19
23
  expect(eventBus.emit).not.toHaveBeenCalled()
20
24
  })
21
25
 
22
- test('flattens style properties and adds the feature', () => {
26
+ test('flattens style properties, defaults type to Feature, and adds the feature', () => {
23
27
  const draw = { add: jest.fn() }
24
28
  const { context, eventBus } = setup(draw)
25
- const feature = { id: 'a', geometry: {}, stroke: 'red', fill: 'blue', strokeWidth: 2, properties: { name: 'x' } }
29
+ const feature = {
30
+ id: 'a',
31
+ geometry: { type: 'Point', coordinates: [0, 0] },
32
+ stroke: 'red',
33
+ fill: 'blue',
34
+ strokeWidth: 2,
35
+ properties: { name: 'x' }
36
+ }
26
37
 
27
38
  addFeature(context, feature)
28
39
 
29
40
  expect(flattenStyleProperties).toHaveBeenCalledWith({ stroke: 'red', fill: 'blue', strokeWidth: 2 })
30
- const expected = { id: 'a', geometry: {}, properties: { name: 'x', _flat: true } }
41
+ const expected = {
42
+ type: 'Feature',
43
+ id: 'a',
44
+ geometry: { type: 'Point', coordinates: [0, 0] },
45
+ properties: { name: 'x', _flat: true }
46
+ }
31
47
  expect(draw.add).toHaveBeenCalledWith(expected)
32
48
  expect(eventBus.emit).toHaveBeenCalledWith('draw:add', expected)
33
49
  })
@@ -35,7 +51,48 @@ describe('addFeature', () => {
35
51
  test('handles a feature without existing properties', () => {
36
52
  const draw = { add: jest.fn() }
37
53
  const { context } = setup(draw)
38
- addFeature(context, { id: 'b', geometry: {} })
39
- expect(draw.add).toHaveBeenCalledWith({ id: 'b', geometry: {}, properties: { _flat: true } })
54
+ addFeature(context, { id: 'b', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] } })
55
+ expect(draw.add).toHaveBeenCalledWith({
56
+ type: 'Feature',
57
+ id: 'b',
58
+ geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] },
59
+ properties: { _flat: true }
60
+ })
61
+ })
62
+
63
+ test('does not override an explicitly supplied type', () => {
64
+ const draw = { add: jest.fn() }
65
+ const { context } = setup(draw)
66
+ addFeature(context, { id: 'c', type: 'Feature', geometry: { type: 'Polygon', coordinates: [[]] } })
67
+ expect(draw.add).toHaveBeenCalledWith(expect.objectContaining({ type: 'Feature' }))
68
+ })
69
+
70
+ // A missing/unsupported geometry is a genuine caller mistake — warned and dropped rather
71
+ // than handed to an adapter that would throw or silently misread it.
72
+ describe('invalid geometry', () => {
73
+ test('rejects a feature with no geometry at all', () => {
74
+ const draw = { add: jest.fn() }
75
+ const { context, eventBus } = setup(draw)
76
+ addFeature(context, { id: 'd' })
77
+ expect(draw.add).not.toHaveBeenCalled()
78
+ expect(eventBus.emit).not.toHaveBeenCalled()
79
+ expect(logger.warn).toHaveBeenCalledWith('addFeature: ignoring invalid GeoJSON feature', expect.objectContaining({ id: 'd' }))
80
+ })
81
+
82
+ test('rejects an unsupported geometry type', () => {
83
+ const draw = { add: jest.fn() }
84
+ const { context } = setup(draw)
85
+ addFeature(context, { id: 'e', geometry: { type: 'MultiPoint', coordinates: [[0, 0]] } })
86
+ expect(draw.add).not.toHaveBeenCalled()
87
+ expect(logger.warn).toHaveBeenCalled()
88
+ })
89
+
90
+ test('rejects a geometry with no coordinates', () => {
91
+ const draw = { add: jest.fn() }
92
+ const { context } = setup(draw)
93
+ addFeature(context, { id: 'f', geometry: { type: 'Point' } })
94
+ expect(draw.add).not.toHaveBeenCalled()
95
+ expect(logger.warn).toHaveBeenCalled()
96
+ })
40
97
  })
41
98
  })
@@ -18,14 +18,17 @@ export const editFeature = ({ appState, appConfig, mapState, pluginConfig, plugi
18
18
  // Per-call callback overrides the plugin-level one; events.js reads this on every commit.
19
19
  draw._geometryValidator = options.onGeometryChange ?? pluginConfig.onGeometryChange
20
20
 
21
- const editModeMap = { LineString: 'edit_line', Polygon: 'edit_polygon' }
21
+ // A Point has no ring of vertices to edit — edit_point relocates the single coordinate
22
+ // instead of edit_vertex's ring-of-vertices machinery.
23
+ const editModeMap = { LineString: 'edit_line', Polygon: 'edit_polygon', Point: 'edit_point' }
24
+ const mode = existingFeature.geometry.type === 'Point' ? 'edit_point' : 'edit_vertex'
22
25
  eventBus.emit('draw:editstart', { mode: editModeMap[existingFeature.geometry.type] })
23
26
 
24
27
  const snapLayers = options.snapLayers === undefined ? (pluginConfig.snapLayers ?? null) : options.snapLayers
25
28
  draw.setSnapLayers(snapLayers)
26
29
  dispatch({ type: 'SET_HAS_SNAP_LAYERS', payload: snapLayers?.length > 0 })
27
30
 
28
- draw.changeMode('edit_vertex', {
31
+ draw.changeMode(mode, {
29
32
  container: appState.layoutRefs.viewportRef.current,
30
33
  deleteVertexButtonId: `${appConfig.id}-draw-delete-point`,
31
34
  undoButtonId: `${appConfig.id}-draw-undo`,
@@ -42,11 +45,11 @@ export const editFeature = ({ appState, appConfig, mapState, pluginConfig, plugi
42
45
  payload: { feature, tempFeature: feature }
43
46
  })
44
47
 
45
- dispatch({ type: 'SET_MODE', payload: 'edit_vertex' })
48
+ dispatch({ type: 'SET_MODE', payload: mode })
46
49
 
47
50
  // Seed the Done-button gate and the stroke from the feature's starting validity
48
51
  // so an already invalid feature opens dashed and cannot be "finished" until fixed.
49
- const { valid } = validateGeometry(feature, { phase: 'edit-start', mode: 'edit_vertex' }, { onGeometryChange: draw._geometryValidator })
52
+ const { valid } = validateGeometry(feature, { phase: 'edit-start', mode }, { onGeometryChange: draw._geometryValidator })
50
53
  dispatch({ type: 'SET_GEOMETRY_VALID', payload: valid })
51
54
  draw.setInvalid?.(!valid)
52
55
 
@@ -74,6 +74,22 @@ describe('editFeature', () => {
74
74
  expect(eventBus.emit).toHaveBeenCalledWith('draw:editstart', { mode: 'edit_line' })
75
75
  })
76
76
 
77
+ // A Point has no ring of vertices — editing one goes through edit_point (relocate the
78
+ // single coordinate), not edit_vertex, across every mode-name site this function touches.
79
+ test('uses edit_point mode for a point feature', () => {
80
+ const { context, dispatch, eventBus, draw } = makeContext()
81
+ draw.get.mockReturnValue({ id: 'f1', geometry: { type: 'Point', coordinates: [0, 0] } })
82
+
83
+ editFeature(context, 'f1')
84
+
85
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:editstart', { mode: 'edit_point' })
86
+ expect(draw.changeMode).toHaveBeenCalledWith('edit_point', expect.objectContaining({ featureId: 'f1' }))
87
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: 'edit_point' })
88
+ // Point geometry is always valid (validation/rules.js has no Point branch) — the gate
89
+ // opens immediately, unlike a self-intersecting/zero-area polygon.
90
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: true })
91
+ })
92
+
77
93
  test('stores a per-call onGeometryChange validator, overriding the plugin-level one', () => {
78
94
  const pluginOnGeometryChange = jest.fn()
79
95
  const onGeometryChange = jest.fn()
@@ -0,0 +1,42 @@
1
+ import { SYMBOL_KEYS } from '../utils/symbolKeys.js'
2
+
3
+ export const newPoint = ({ appState, appConfig, pluginConfig, pluginState, mapState, mapProvider, services }, featureId, options = {}) => {
4
+ const { dispatch } = pluginState
5
+ const { draw } = mapProvider
6
+ const { eventBus } = services
7
+
8
+ if (!draw) {
9
+ return
10
+ }
11
+
12
+ eventBus.emit('draw:started', { mode: 'draw_point' })
13
+
14
+ const snapLayers = options.snapLayers === undefined ? (pluginConfig.snapLayers ?? null) : options.snapLayers
15
+ draw.setSnapLayers(snapLayers)
16
+ dispatch({ type: 'SET_HAS_SNAP_LAYERS', payload: snapLayers?.length > 0 })
17
+
18
+ const { properties: customProperties, onGeometryChange, ...modeOptions } = options
19
+ // Per-call callback overrides the plugin-level one; events.js reads this on every commit.
20
+ draw._geometryValidator = onGeometryChange ?? pluginConfig.onGeometryChange
21
+
22
+ const symbolStyle = {}
23
+ SYMBOL_KEYS.forEach((key) => {
24
+ if (modeOptions[key] !== undefined) {
25
+ symbolStyle[key] = modeOptions[key]
26
+ delete modeOptions[key]
27
+ }
28
+ })
29
+
30
+ draw.changeMode('draw_point', {
31
+ container: appState.layoutRefs.viewportRef.current,
32
+ addVertexButtonId: `${appConfig.id}-draw-add-point`,
33
+ interfaceType: appState.interfaceType,
34
+ crossHair: mapState.crossHair,
35
+ getSnapEnabled: () => draw.isSnapEnabled(),
36
+ featureId,
37
+ ...modeOptions,
38
+ properties: { ...customProperties, ...symbolStyle }
39
+ })
40
+
41
+ dispatch({ type: 'SET_MODE', payload: 'draw_point' })
42
+ }
@@ -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.
@@ -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
@@ -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')
@@ -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