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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/button-definition.md +7 -8
  7. package/docs/api/control-definition.md +17 -1
  8. package/docs/api/panel-definition.md +5 -0
  9. package/docs/api/slots.md +50 -6
  10. package/docs/assets/images/slot-map.svg +61 -262
  11. package/docs/examples/add-polygons.mdx +8 -2
  12. package/docs/examples/add-symbols.mdx +8 -2
  13. package/docs/examples/draw-tools.mdx +3 -3
  14. package/docs/plugins/datasets.md +23 -15
  15. package/docs/plugins/draw.md +79 -16
  16. package/docs/plugins/map-key.md +147 -0
  17. package/docs/plugins/search.md +1 -1
  18. package/docs/plugins.md +5 -1
  19. package/govuk-prototype-kit.config.json +2 -0
  20. package/jest.setup.js +5 -0
  21. package/package.json +9 -1
  22. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  23. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  24. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  25. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  26. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  27. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  28. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  29. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  30. package/plugins/beta/use-location/src/manifest.js +1 -1
  31. package/plugins/datasets/dist/css/index.css +1 -1
  32. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  33. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  34. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  35. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  36. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  37. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  38. package/plugins/datasets/dist/umd/index.js +1 -1
  39. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  40. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  41. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  42. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  43. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  44. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  45. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  46. package/plugins/datasets/src/datasets.scss +0 -1
  47. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  48. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  49. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  50. package/plugins/datasets/src/index.test.js +36 -0
  51. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  52. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  53. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  54. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  55. package/plugins/datasets/src/manifest.js +0 -38
  56. package/plugins/datasets/src/manifest.test.js +49 -0
  57. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  58. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  59. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  60. package/plugins/datasets/src/registry/dataset.js +13 -1
  61. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  62. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  63. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  64. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  65. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  66. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  67. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  68. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  69. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  70. package/plugins/draw/dist/esm/index.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  72. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  73. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  74. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  75. package/plugins/draw/dist/umd/index.js +1 -1
  76. package/plugins/draw/src/DrawInit.jsx +10 -2
  77. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  78. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  79. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  80. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  81. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  110. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  112. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  114. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  116. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  117. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  118. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  130. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  132. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  133. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  135. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  137. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  140. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  142. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  144. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  147. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  148. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  149. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  150. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  151. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  152. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  153. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  154. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  155. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  156. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  162. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  163. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  164. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  165. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  166. package/plugins/draw/src/api/addFeature.js +16 -1
  167. package/plugins/draw/src/api/addFeature.test.js +62 -5
  168. package/plugins/draw/src/api/createNewShape.js +41 -0
  169. package/plugins/draw/src/api/createNewShape.test.js +63 -0
  170. package/plugins/draw/src/api/editFeature.js +7 -4
  171. package/plugins/draw/src/api/editFeature.test.js +16 -0
  172. package/plugins/draw/src/api/newLine.js +2 -40
  173. package/plugins/draw/src/api/newPoint.js +42 -0
  174. package/plugins/draw/src/api/newPoint.test.js +117 -0
  175. package/plugins/draw/src/api/newPolygon.js +2 -40
  176. package/plugins/draw/src/api/setStyle.js +43 -0
  177. package/plugins/draw/src/api/setStyle.test.js +75 -0
  178. package/plugins/draw/src/events.js +17 -10
  179. package/plugins/draw/src/events.test.js +14 -0
  180. package/plugins/draw/src/manifest.js +18 -6
  181. package/plugins/draw/src/manifest.test.js +32 -1
  182. package/plugins/draw/src/reducer.test.js +7 -0
  183. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  184. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  185. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  186. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  187. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  188. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  189. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  190. package/plugins/interact/dist/umd/index.js +1 -1
  191. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  192. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  193. package/plugins/interact/src/manifest.js +1 -1
  194. package/plugins/interact/src/utils/featureQueries.js +18 -0
  195. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  196. package/plugins/interact/src/utils/spatial.js +9 -3
  197. package/plugins/interact/src/utils/spatial.test.js +12 -1
  198. package/plugins/map-key/dist/css/index.css +68 -0
  199. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  200. package/plugins/map-key/dist/esm/index.js +1 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  202. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  203. package/plugins/map-key/dist/umd/index.js +2 -0
  204. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  205. package/plugins/map-key/package.json +28 -0
  206. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  208. package/plugins/map-key/src/components/Key/Key.jsx +43 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +21 -14
  210. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  212. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.jsx +26 -0
  214. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  215. package/plugins/map-key/src/components/Key/KeySvg.jsx +41 -0
  216. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  217. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +22 -0
  218. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.jsx +7 -2
  220. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +8 -2
  221. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +26 -0
  222. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.jsx +8 -3
  224. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +17 -6
  225. package/plugins/map-key/src/components/Key/MapKey.jsx +53 -0
  226. package/plugins/map-key/src/components/Key/MapKey.test.jsx +147 -0
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  228. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  229. package/plugins/map-key/src/index.js +13 -0
  230. package/plugins/map-key/src/index.test.js +31 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  232. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  233. package/plugins/map-key/src/manifest.js +30 -0
  234. package/plugins/map-key/src/manifest.test.js +63 -0
  235. package/plugins/map-key/src/mapKey.scss +4 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  237. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  238. package/plugins/map-key/src/registry/index.js +3 -0
  239. package/plugins/map-key/src/registry/index.test.js +15 -0
  240. package/plugins/menu/dist/css/index.css +80 -0
  241. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  242. package/plugins/menu/dist/esm/index.js +1 -0
  243. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  244. package/plugins/menu/dist/umd/index.js +2 -0
  245. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  246. package/plugins/menu/package.json +28 -0
  247. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  249. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  250. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  251. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  252. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  254. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  255. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  257. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  258. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  259. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  260. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  261. package/plugins/menu/src/index.js +13 -0
  262. package/plugins/menu/src/index.test.js +31 -0
  263. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  264. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  265. package/plugins/menu/src/initialise/createMenu.js +10 -0
  266. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  267. package/plugins/menu/src/manifest.js +35 -0
  268. package/plugins/menu/src/manifest.test.js +63 -0
  269. package/plugins/menu/src/menu.scss +4 -0
  270. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  271. package/plugins/menu/src/reducers/pluginState.js +35 -0
  272. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  274. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  275. package/plugins/menu/src/registry/index.js +3 -0
  276. package/plugins/menu/src/registry/index.test.js +15 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  278. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  279. package/plugins/search/dist/css/index.css +1 -1
  280. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  281. package/plugins/search/dist/esm/index.js +1 -1
  282. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  283. package/plugins/search/dist/umd/index.js +1 -1
  284. package/plugins/search/src/Search.jsx +2 -3
  285. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  286. package/plugins/search/src/index.js +0 -1
  287. package/plugins/search/src/index.test.js +2 -4
  288. package/plugins/search/src/manifest.js +1 -1
  289. package/plugins/search/src/search.scss +17 -16
  290. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  292. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  294. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  296. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  298. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  299. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  301. package/providers/maplibre/dist/umd/index.js +1 -1
  302. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  303. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  304. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  305. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  306. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  307. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  308. package/providers/maplibre/src/utils/patternImages.js +1 -1
  309. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  310. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  311. package/rollup.esm.mjs +11 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Hints/Hints.test.jsx +13 -0
  314. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
  315. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
  316. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  317. package/src/App/components/Panel/Panel.jsx +65 -8
  318. package/src/App/components/Panel/Panel.module.scss +49 -15
  319. package/src/App/components/Panel/Panel.test.jsx +114 -1
  320. package/src/App/components/Tabs/Tabs.jsx +43 -17
  321. package/src/App/components/Tabs/Tabs.module.scss +32 -5
  322. package/src/App/components/Tabs/Tabs.test.jsx +40 -10
  323. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  324. package/src/App/hooks/useFeatureItems.js +16 -7
  325. package/src/App/hooks/useFeatureItems.test.js +57 -0
  326. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  327. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  328. package/src/App/layout/Layout.jsx +9 -10
  329. package/src/App/layout/Layout.test.jsx +2 -0
  330. package/src/App/layout/layout.module.scss +31 -17
  331. package/src/App/registry/pluginRegistry.js +1 -1
  332. package/src/App/registry/pluginRegistry.test.js +12 -0
  333. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  334. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  335. package/src/App/renderer/groupByKey.js +28 -0
  336. package/src/App/renderer/groupByKey.test.js +39 -0
  337. package/src/App/renderer/groupIntoTabs.js +42 -0
  338. package/src/App/renderer/groupIntoTabs.test.js +95 -0
  339. package/src/App/renderer/mapButtons.js +97 -129
  340. package/src/App/renderer/mapButtons.test.js +67 -83
  341. package/src/App/renderer/mapControls.js +4 -1
  342. package/src/App/renderer/mapControls.test.js +32 -0
  343. package/src/App/renderer/mapPanels.js +53 -11
  344. package/src/App/renderer/mapPanels.test.js +95 -2
  345. package/src/App/renderer/orderItems.js +28 -0
  346. package/src/App/renderer/orderItems.test.js +50 -0
  347. package/src/App/renderer/slotAggregator.js +2 -18
  348. package/src/App/renderer/slotHelpers.js +3 -1
  349. package/src/App/renderer/slotHelpers.test.js +5 -0
  350. package/src/App/renderer/slots.js +3 -0
  351. package/src/App/store/AppProvider.jsx +1 -0
  352. package/src/App/store/appDispatchMiddleware.js +1 -1
  353. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  354. package/src/config/appConfig.js +2 -2
  355. package/src/config/events.js +2 -0
  356. package/src/scss/settings/_colors.scss +1 -0
  357. package/src/scss/settings/_dimensions.scss +2 -3
  358. package/src/scss/settings/_transition.scss +1 -1
  359. package/src/services/eventBus.js +44 -0
  360. package/src/services/eventBus.test.js +83 -0
  361. package/src/services/symbolRegistry.js +1 -1
  362. package/src/types.js +31 -3
  363. package/webpack.umd.mjs +2 -0
  364. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  365. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  366. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  367. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  368. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  369. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  370. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  371. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  372. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  373. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  374. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -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
@@ -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
+ ])
@@ -49,6 +49,14 @@ describe('built-in rules (synchronous, independently floored, flip-guarded)', ()
49
49
  expect(onStrokeChange).not.toHaveBeenCalled()
50
50
  })
51
51
 
52
+ test('a non-Polygon/LineString geometry type (e.g. Point) floors at 0 via the MIN_VERTICES fallback, and never flips', () => {
53
+ const { onStrokeChange, onPlaceChange, checks } = setup()
54
+ const point = { type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] } }
55
+ expect(() => checks.update({ feature: point, numVertices: 0 })).not.toThrow()
56
+ expect(onStrokeChange).not.toHaveBeenCalled()
57
+ expect(onPlaceChange).not.toHaveBeenCalled()
58
+ })
59
+
52
60
  test('numVertices defaults to 0 when omitted from context', () => {
53
61
  const { onStrokeChange, checks } = setup()
54
62
  checks.update({ feature: closingEdgeCrosses })
@@ -156,4 +164,50 @@ describe('reset / destroy', () => {
156
164
  jest.runAllTimers()
157
165
  expect(onGeometryChange).not.toHaveBeenCalled()
158
166
  })
167
+
168
+ // Belt-and-braces: recompute()/runUserRule() re-check `pending` themselves rather than
169
+ // trusting that cancelFrame() actually stopped the callback — simulated here by making
170
+ // cancelAnimationFrame a no-op, so an already-queued frame still fires after reset()/
171
+ // destroy() have cleared `pending`. The guards must make that a safe no-op.
172
+ test('a frame that still fires despite cancellation is a no-op, guarded by pending being cleared', () => {
173
+ const noopCancel = jest.fn()
174
+ const originalCaf = globalThis.cancelAnimationFrame
175
+ globalThis.cancelAnimationFrame = noopCancel
176
+ try {
177
+ const { onStrokeChange, onPlaceChange, checks } = setup()
178
+ const onGeometryChange = jest.fn(() => true)
179
+ checks.update({ feature: square, numVertices: 3, onGeometryChange })
180
+ checks.reset() // calls our no-op cancel — the queued frame is NOT actually removed
181
+ expect(noopCancel).toHaveBeenCalled()
182
+ onStrokeChange.mockClear()
183
+ onPlaceChange.mockClear()
184
+ expect(() => jest.runAllTimers()).not.toThrow()
185
+ expect(onGeometryChange).not.toHaveBeenCalled() // guard returns before reading pending's fields
186
+ expect(onStrokeChange).not.toHaveBeenCalled()
187
+ expect(onPlaceChange).not.toHaveBeenCalled()
188
+ } finally {
189
+ globalThis.cancelAnimationFrame = originalCaf
190
+ }
191
+ })
192
+
193
+ // recompute() carries its own `if (!pending) return` guard too, distinct from
194
+ // runUserRule()'s — reachable when the user's own onGeometryChange callback
195
+ // reentrantly clears `pending` (e.g. via reset()) partway through the very frame that's
196
+ // evaluating it, before runUserRule() reaches its trailing recompute() call.
197
+ test('recompute() guards against pending being cleared reentrantly by the user callback', () => {
198
+ const { onStrokeChange, onPlaceChange, checks } = setup()
199
+ const onGeometryChange = jest.fn(() => {
200
+ checks.reset() // reentrant — clears `pending` mid-evaluation, frame is already null by now
201
+ return true
202
+ })
203
+ checks.update({ feature: square, numVertices: 3, onGeometryChange })
204
+ onStrokeChange.mockClear()
205
+ onPlaceChange.mockClear()
206
+ expect(() => jest.runAllTimers()).not.toThrow()
207
+ expect(onGeometryChange).toHaveBeenCalledTimes(1)
208
+ // recompute()'s trailing call (after the reentrant reset) is a safe no-op — no further
209
+ // gate changes are reported off a pending the reentrant reset() already cleared.
210
+ expect(onStrokeChange).not.toHaveBeenCalled()
211
+ expect(onPlaceChange).not.toHaveBeenCalled()
212
+ })
159
213
  })
@@ -1,4 +1,4 @@
1
- import { createLiveStroke } from './liveStroke.js'
1
+ import { createLiveStroke, requestFrame, cancelFrame } from './liveStroke.js'
2
2
 
3
3
  const poly = (ring) => ({ type: 'Feature', geometry: { type: 'Polygon', coordinates: [ring] } })
4
4
 
@@ -105,6 +105,45 @@ describe('custom validate function', () => {
105
105
  })
106
106
  })
107
107
 
108
+ describe('requestFrame / cancelFrame (engine-agnostic frame scheduling)', () => {
109
+ test('uses requestAnimationFrame/cancelAnimationFrame when available', () => {
110
+ const raf = jest.spyOn(globalThis, 'requestAnimationFrame')
111
+ const caf = jest.spyOn(globalThis, 'cancelAnimationFrame')
112
+ const cb = jest.fn()
113
+ const id = requestFrame(cb)
114
+ expect(raf).toHaveBeenCalledWith(cb)
115
+ cancelFrame(id)
116
+ expect(caf).toHaveBeenCalledWith(id)
117
+ raf.mockRestore()
118
+ caf.mockRestore()
119
+ })
120
+
121
+ test('falls back to setTimeout/clearTimeout when rAF is unavailable (e.g. SSR)', () => {
122
+ const rafDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'requestAnimationFrame')
123
+ const cafDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'cancelAnimationFrame')
124
+ // jsdom defines these as configurable globals — delete rather than assign undefined,
125
+ // so `typeof requestAnimationFrame` genuinely reads 'undefined' inside the module.
126
+ delete globalThis.requestAnimationFrame
127
+ delete globalThis.cancelAnimationFrame
128
+ try {
129
+ const cb = jest.fn()
130
+ requestFrame(cb)
131
+ expect(cb).not.toHaveBeenCalled()
132
+ jest.advanceTimersByTime(16)
133
+ expect(cb).toHaveBeenCalledTimes(1)
134
+
135
+ const cb2 = jest.fn()
136
+ const id2 = requestFrame(cb2)
137
+ cancelFrame(id2)
138
+ jest.advanceTimersByTime(16)
139
+ expect(cb2).not.toHaveBeenCalled()
140
+ } finally {
141
+ Object.defineProperty(globalThis, 'requestAnimationFrame', rafDescriptor)
142
+ Object.defineProperty(globalThis, 'cancelAnimationFrame', cafDescriptor)
143
+ }
144
+ })
145
+ })
146
+
108
147
  describe('set / reset / destroy', () => {
109
148
  test('set() applies through the flip guard and drops any pending frame', () => {
110
149
  const { onChange, stroke } = setup()
@@ -147,4 +186,28 @@ describe('set / reset / destroy', () => {
147
186
  jest.runAllTimers()
148
187
  expect(onGeometryChange).not.toHaveBeenCalled()
149
188
  })
189
+
190
+ // Belt-and-braces: runUserRule() re-checks `pending` itself rather than trusting that
191
+ // cancelFrame() actually stopped the callback — simulated here by making
192
+ // cancelAnimationFrame a no-op, so the already-queued frame still fires after
193
+ // cancelPending() has cleared `pending`. The guard must make that a safe no-op rather
194
+ // than reading a null pending's properties.
195
+ test('a frame that still fires despite cancellation is a no-op, guarded by pending being cleared', () => {
196
+ const noopCancel = jest.fn()
197
+ const originalCaf = globalThis.cancelAnimationFrame
198
+ globalThis.cancelAnimationFrame = noopCancel
199
+ try {
200
+ const { onChange, stroke } = setup()
201
+ const onGeometryChange = jest.fn(() => true)
202
+ stroke.update({ feature: square, numVertices: 3, onGeometryChange })
203
+ stroke.destroy() // calls our no-op cancel — the queued frame is NOT actually removed
204
+ expect(noopCancel).toHaveBeenCalled()
205
+ onChange.mockClear()
206
+ expect(() => jest.runAllTimers()).not.toThrow()
207
+ expect(onGeometryChange).not.toHaveBeenCalled() // guard returns before reading pending's fields
208
+ expect(onChange).not.toHaveBeenCalled()
209
+ } finally {
210
+ globalThis.cancelAnimationFrame = originalCaf
211
+ }
212
+ })
150
213
  })