@defra/interactive-map 0.0.36-alpha → 0.0.38-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 (248) hide show
  1. package/assets/templates/draw-tools.njk +4 -3
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api.md +32 -0
  8. package/docs/examples/draw-tools.mdx +0 -4
  9. package/docs/plugins/draw.md +482 -0
  10. package/docs/plugins.md +2 -6
  11. package/govuk-prototype-kit.config.json +3 -2
  12. package/package.json +2 -4
  13. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  14. package/plugins/beta/draw-es/src/events.js +1 -1
  15. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  18. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  19. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  20. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  21. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  22. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  23. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +13 -0
  24. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  26. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  27. package/plugins/draw/dist/esm/index.js +1 -0
  28. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  29. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  30. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  31. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  32. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  33. package/plugins/draw/dist/umd/index.js +2 -0
  34. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  35. package/plugins/draw/src/DrawInit.jsx +89 -0
  36. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  37. package/plugins/draw/src/adapterEvents.js +127 -0
  38. package/plugins/draw/src/adapterEvents.test.js +20 -0
  39. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  40. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  41. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  42. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  43. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  44. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  45. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  46. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  47. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  48. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  49. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  95. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  96. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  97. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  98. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  99. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  100. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  101. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  102. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  103. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  104. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  105. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  106. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  107. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  108. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  109. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  110. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  111. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  112. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  113. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  114. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  115. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  118. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  119. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  120. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  121. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  144. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  146. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  148. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  149. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  156. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  157. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  158. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  159. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  163. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  164. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  165. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  166. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  167. package/plugins/draw/src/api/addFeature.js +22 -0
  168. package/plugins/draw/src/api/addFeature.test.js +41 -0
  169. package/plugins/draw/src/api/deleteFeature.js +11 -0
  170. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  171. package/plugins/draw/src/api/editFeature.js +54 -0
  172. package/plugins/draw/src/api/editFeature.test.js +134 -0
  173. package/plugins/draw/src/api/merge.js +30 -0
  174. package/plugins/draw/src/api/merge.test.js +57 -0
  175. package/plugins/draw/src/api/newLine.js +41 -0
  176. package/plugins/draw/src/api/newLine.test.js +93 -0
  177. package/plugins/draw/src/api/newPolygon.js +41 -0
  178. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  179. package/plugins/draw/src/api/split.js +117 -0
  180. package/plugins/draw/src/api/split.test.js +204 -0
  181. package/plugins/draw/src/defaults.js +52 -0
  182. package/plugins/draw/src/defaults.test.js +18 -0
  183. package/plugins/draw/src/draw.scss +43 -0
  184. package/plugins/draw/src/events.js +237 -0
  185. package/plugins/draw/src/events.test.js +369 -0
  186. package/plugins/draw/src/index.js +10 -0
  187. package/plugins/draw/src/index.test.js +24 -0
  188. package/plugins/draw/src/manifest.js +170 -0
  189. package/plugins/draw/src/manifest.test.js +147 -0
  190. package/plugins/draw/src/reducer.js +69 -0
  191. package/plugins/draw/src/reducer.test.js +85 -0
  192. package/plugins/draw/src/utils/debounce.js +16 -0
  193. package/plugins/draw/src/utils/debounce.test.js +40 -0
  194. package/plugins/draw/src/utils/eventBus.js +36 -0
  195. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  196. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  197. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  198. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  199. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  200. package/plugins/draw/src/utils/resolveColors.js +39 -0
  201. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  202. package/plugins/draw/src/utils/spatial.js +243 -0
  203. package/plugins/draw/src/utils/spatial.test.js +243 -0
  204. package/plugins/draw/src/utils/touchTarget.js +95 -0
  205. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  206. package/plugins/draw/src/utils/undoStack.js +31 -0
  207. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  208. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  209. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  210. package/plugins/draw/src/validation/liveStroke.js +85 -0
  211. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  212. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  213. package/plugins/draw/src/validation/rules.js +186 -0
  214. package/plugins/draw/src/validation/rules.test.js +120 -0
  215. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  216. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  217. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  218. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  219. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  220. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  221. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  222. package/plugins/search/dist/css/index.css +1 -317
  223. package/rollup.esm.mjs +35 -28
  224. package/sonar-project.properties +2 -2
  225. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  226. package/src/App/components/Hints/Hints.module.scss +5 -4
  227. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  228. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  229. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  230. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  231. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  232. package/src/App/hooks/useHintsAPI.js +67 -0
  233. package/src/App/hooks/useHintsAPI.test.js +145 -0
  234. package/src/App/renderer/PluginInits.jsx +4 -0
  235. package/src/App/renderer/PluginInits.test.jsx +7 -1
  236. package/src/InteractiveMap/InteractiveMap.js +20 -0
  237. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  238. package/src/config/events.js +4 -0
  239. package/src/scss/settings/_dimensions.scss +3 -0
  240. package/src/services/announcer.js +38 -3
  241. package/src/services/announcer.test.js +133 -1
  242. package/src/services/hints.js +3 -0
  243. package/src/services/hints.test.js +19 -0
  244. package/src/types.js +12 -0
  245. package/src/utils/detectInterfaceType.js +5 -1
  246. package/src/utils/isMac.js +15 -0
  247. package/src/utils/isMac.test.js +37 -0
  248. package/webpack.umd.mjs +1 -0
@@ -0,0 +1,8 @@
1
+ // Re-export centralized touch target utilities from shared implementation
2
+ export {
3
+ createTouchTarget,
4
+ applyTouchTargetColors,
5
+ showTouchTarget,
6
+ hideTouchTarget,
7
+ isOnTouchTarget
8
+ } from '../../../utils/touchTarget.js'
@@ -0,0 +1,22 @@
1
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
2
+
3
+ export const addFeature = ({ mapProvider, services }, feature) => {
4
+ const { draw } = mapProvider
5
+ const { eventBus } = services
6
+
7
+ if (!draw) {
8
+ return
9
+ }
10
+
11
+ const { stroke, fill, strokeWidth, properties, ...featureRest } = feature
12
+ const flatFeature = {
13
+ ...featureRest,
14
+ properties: {
15
+ ...properties,
16
+ ...flattenStyleProperties({ stroke, fill, strokeWidth })
17
+ }
18
+ }
19
+
20
+ draw.add(flatFeature)
21
+ eventBus.emit('draw:add', flatFeature)
22
+ }
@@ -0,0 +1,41 @@
1
+ import { addFeature } from './addFeature.js'
2
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
3
+
4
+ jest.mock('../utils/flattenStyleProperties.js', () => ({
5
+ flattenStyleProperties: jest.fn(() => ({ _flat: true }))
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('addFeature', () => {
16
+ test('does nothing when there is no draw instance', () => {
17
+ const { context, eventBus } = setup(undefined)
18
+ addFeature(context, { id: 'a' })
19
+ expect(eventBus.emit).not.toHaveBeenCalled()
20
+ })
21
+
22
+ test('flattens style properties and adds the feature', () => {
23
+ const draw = { add: jest.fn() }
24
+ const { context, eventBus } = setup(draw)
25
+ const feature = { id: 'a', geometry: {}, stroke: 'red', fill: 'blue', strokeWidth: 2, properties: { name: 'x' } }
26
+
27
+ addFeature(context, feature)
28
+
29
+ expect(flattenStyleProperties).toHaveBeenCalledWith({ stroke: 'red', fill: 'blue', strokeWidth: 2 })
30
+ const expected = { id: 'a', geometry: {}, properties: { name: 'x', _flat: true } }
31
+ expect(draw.add).toHaveBeenCalledWith(expected)
32
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:add', expected)
33
+ })
34
+
35
+ test('handles a feature without existing properties', () => {
36
+ const draw = { add: jest.fn() }
37
+ const { context } = setup(draw)
38
+ addFeature(context, { id: 'b', geometry: {} })
39
+ expect(draw.add).toHaveBeenCalledWith({ id: 'b', geometry: {}, properties: { _flat: true } })
40
+ })
41
+ })
@@ -0,0 +1,11 @@
1
+ export const deleteFeature = ({ mapProvider, services }, featureId) => {
2
+ const { draw } = mapProvider
3
+ const { eventBus } = services
4
+
5
+ if (!draw) {
6
+ return
7
+ }
8
+
9
+ draw.delete(featureId)
10
+ eventBus.emit('draw:delete', { featureId })
11
+ }
@@ -0,0 +1,24 @@
1
+ import { deleteFeature } from './deleteFeature.js'
2
+
3
+ const setup = (draw) => {
4
+ const eventBus = { emit: jest.fn() }
5
+ return { context: { mapProvider: { draw }, services: { eventBus } }, eventBus }
6
+ }
7
+
8
+ describe('deleteFeature', () => {
9
+ test('does nothing when there is no draw instance', () => {
10
+ const { context, eventBus } = setup(undefined)
11
+ deleteFeature(context, 'id1')
12
+ expect(eventBus.emit).not.toHaveBeenCalled()
13
+ })
14
+
15
+ test('deletes the feature and emits the delete event', () => {
16
+ const draw = { delete: jest.fn() }
17
+ const { context, eventBus } = setup(draw)
18
+
19
+ deleteFeature(context, 'id1')
20
+
21
+ expect(draw.delete).toHaveBeenCalledWith('id1')
22
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:delete', { featureId: 'id1' })
23
+ })
24
+ })
@@ -0,0 +1,54 @@
1
+ import { MAP_SIZE_SCALES } from '../defaults.js'
2
+ import { validateGeometry } from '../validation/validateGeometry.js'
3
+
4
+ export const editFeature = ({ appState, appConfig, mapState, pluginConfig, pluginState, mapProvider, services }, featureId, options = {}) => {
5
+ const { dispatch } = pluginState
6
+ const { draw } = mapProvider
7
+ const { eventBus } = services
8
+
9
+ if (!draw) {
10
+ return false
11
+ }
12
+
13
+ const existingFeature = draw.get(featureId)
14
+ if (!existingFeature) {
15
+ return false
16
+ }
17
+
18
+ // Per-call callback overrides the plugin-level one; events.js reads this on every commit.
19
+ draw._geometryValidator = options.onGeometryChange ?? pluginConfig.onGeometryChange
20
+
21
+ const editModeMap = { LineString: 'edit_line', Polygon: 'edit_polygon' }
22
+ eventBus.emit('draw:editstart', { mode: editModeMap[existingFeature.geometry.type] })
23
+
24
+ const snapLayers = options.snapLayers === undefined ? (pluginConfig.snapLayers ?? null) : options.snapLayers
25
+ draw.setSnapLayers(snapLayers)
26
+ dispatch({ type: 'SET_HAS_SNAP_LAYERS', payload: snapLayers?.length > 0 })
27
+
28
+ draw.changeMode('edit_vertex', {
29
+ container: appState.layoutRefs.viewportRef.current,
30
+ deleteVertexButtonId: `${appConfig.id}-draw-delete-point`,
31
+ undoButtonId: `${appConfig.id}-draw-undo`,
32
+ isPanEnabled: appState.interfaceType !== 'keyboard',
33
+ interfaceType: appState.interfaceType,
34
+ scale: MAP_SIZE_SCALES[mapState.mapSize],
35
+ featureId,
36
+ getSnapEnabled: () => draw.isSnapEnabled()
37
+ })
38
+
39
+ const feature = draw.get(featureId)
40
+ dispatch({
41
+ type: 'SET_FEATURE',
42
+ payload: { feature, tempFeature: feature }
43
+ })
44
+
45
+ dispatch({ type: 'SET_MODE', payload: 'edit_vertex' })
46
+
47
+ // Seed the Done-button gate and the stroke from the feature's starting validity
48
+ // 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 })
50
+ dispatch({ type: 'SET_GEOMETRY_VALID', payload: valid })
51
+ draw.setInvalid?.(!valid)
52
+
53
+ return true
54
+ }
@@ -0,0 +1,134 @@
1
+ import { editFeature } from './editFeature.js'
2
+
3
+ jest.mock('../defaults.js', () => ({ MAP_SIZE_SCALES: { medium: 1.5, large: 2 } }))
4
+
5
+ const makeContext = (overrides = {}) => {
6
+ const dispatch = jest.fn()
7
+ const eventBus = { emit: jest.fn() }
8
+ const draw = {
9
+ get: jest.fn(() => ({ id: 'f1', geometry: { type: 'Polygon' } })),
10
+ setSnapLayers: jest.fn(),
11
+ changeMode: jest.fn(),
12
+ isSnapEnabled: jest.fn(() => true)
13
+ }
14
+ const context = {
15
+ appState: { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'mouse' },
16
+ appConfig: { id: 'app' },
17
+ mapState: { mapSize: 'medium' },
18
+ pluginConfig: { snapLayers: ['pc-layer'] },
19
+ pluginState: { dispatch },
20
+ mapProvider: { draw },
21
+ services: { eventBus },
22
+ ...overrides
23
+ }
24
+ return { context, dispatch, eventBus, draw }
25
+ }
26
+
27
+ beforeEach(() => jest.clearAllMocks())
28
+
29
+ describe('editFeature', () => {
30
+ test('returns false when there is no draw instance', () => {
31
+ const { context } = makeContext({ mapProvider: { draw: null } })
32
+ expect(editFeature(context, 'f1')).toBe(false)
33
+ })
34
+
35
+ test('returns false when the feature does not exist', () => {
36
+ const { context, draw, eventBus } = makeContext()
37
+ draw.get.mockReturnValue(undefined)
38
+ expect(editFeature(context, 'missing')).toBe(false)
39
+ expect(eventBus.emit).not.toHaveBeenCalled()
40
+ })
41
+
42
+ test('enters edit mode for a polygon and wires all options', () => {
43
+ const { context, dispatch, eventBus, draw } = makeContext()
44
+
45
+ const result = editFeature(context, 'f1')
46
+
47
+ expect(result).toBe(true)
48
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:editstart', { mode: 'edit_polygon' })
49
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(['pc-layer'])
50
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: true })
51
+
52
+ expect(draw.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({
53
+ container: 'viewport',
54
+ deleteVertexButtonId: 'app-draw-delete-point',
55
+ undoButtonId: 'app-draw-undo',
56
+ isPanEnabled: true,
57
+ interfaceType: 'mouse',
58
+ scale: 1.5,
59
+ featureId: 'f1'
60
+ }))
61
+
62
+ const opts = draw.changeMode.mock.calls[0][1]
63
+ expect(opts.getSnapEnabled()).toBe(true)
64
+ expect(draw.isSnapEnabled).toHaveBeenCalled()
65
+
66
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_FEATURE', payload: { feature: { id: 'f1', geometry: { type: 'Polygon' } }, tempFeature: { id: 'f1', geometry: { type: 'Polygon' } } } })
67
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: 'edit_vertex' })
68
+ })
69
+
70
+ test('uses edit_line mode for a line string', () => {
71
+ const { context, eventBus, draw } = makeContext()
72
+ draw.get.mockReturnValue({ id: 'f1', geometry: { type: 'LineString' } })
73
+ editFeature(context, 'f1')
74
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:editstart', { mode: 'edit_line' })
75
+ })
76
+
77
+ test('stores a per-call onGeometryChange validator, overriding the plugin-level one', () => {
78
+ const pluginOnGeometryChange = jest.fn()
79
+ const onGeometryChange = jest.fn()
80
+ const { context, draw } = makeContext({ pluginConfig: { snapLayers: ['pc-layer'], onGeometryChange: pluginOnGeometryChange } })
81
+ editFeature(context, 'f1', { onGeometryChange })
82
+ expect(draw._geometryValidator).toBe(onGeometryChange)
83
+ })
84
+
85
+ test('seeds the geometry-valid gate from the feature starting validity', () => {
86
+ const { context, dispatch, draw } = makeContext()
87
+ // A valid square → gate opens.
88
+ draw.get.mockReturnValue({ id: 'f1', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]] } })
89
+ editFeature(context, 'f1')
90
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: true })
91
+ })
92
+
93
+ test('seeds the gate closed for an already self-intersecting feature', () => {
94
+ const { context, dispatch, draw } = makeContext()
95
+ draw.get.mockReturnValue({ id: 'f1', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 1], [1, 0], [0, 1], [0, 0]]] } })
96
+ editFeature(context, 'f1')
97
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
98
+ })
99
+
100
+ test('seeds the stroke alongside the gate — an invalid feature opens edit mode dashed', () => {
101
+ const { context, draw } = makeContext()
102
+ draw.setInvalid = jest.fn()
103
+ draw.get.mockReturnValue({ id: 'f1', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 1], [1, 0], [0, 1], [0, 0]]] } })
104
+ editFeature(context, 'f1')
105
+ expect(draw.setInvalid).toHaveBeenCalledWith(true)
106
+
107
+ draw.setInvalid.mockClear()
108
+ draw.get.mockReturnValue({ id: 'f1', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]] } })
109
+ editFeature(context, 'f1')
110
+ expect(draw.setInvalid).toHaveBeenCalledWith(false)
111
+ })
112
+
113
+ test('disables panning for the keyboard interface', () => {
114
+ const { context, draw } = makeContext({
115
+ appState: { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'keyboard' }
116
+ })
117
+ editFeature(context, 'f1')
118
+ expect(draw.changeMode.mock.calls[0][1].isPanEnabled).toBe(false)
119
+ })
120
+
121
+ test('prefers explicit option snapLayers over the plugin config', () => {
122
+ const { context, draw, dispatch } = makeContext()
123
+ editFeature(context, 'f1', { snapLayers: ['opt-layer'] })
124
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(['opt-layer'])
125
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: true })
126
+ })
127
+
128
+ test('falls back to null snapLayers when neither option nor config is set', () => {
129
+ const { context, draw, dispatch } = makeContext({ pluginConfig: {} })
130
+ editFeature(context, 'f1')
131
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(null)
132
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: false })
133
+ })
134
+ })
@@ -0,0 +1,30 @@
1
+ import { mergePolygons } from '../utils/spatial.js'
2
+
3
+ /**
4
+ * Merge multiple contiguous polygons into a single polygon.
5
+ *
6
+ * Pure computation, like split — this does not touch the feature store. The
7
+ * caller is responsible for deleting the original features and adding the
8
+ * merged result (see the draw:merge event).
9
+ *
10
+ * @param {object} context - plugin context
11
+ * @param {string[]} featureIds - IDs of the polygons to merge
12
+ * @returns {object|null} the merged GeoJSON feature, or null if the merge failed
13
+ */
14
+ export const merge = ({ mapProvider, services }, featureIds) => {
15
+ const { draw } = mapProvider
16
+ const { eventBus } = services
17
+
18
+ if (!draw) {
19
+ return null
20
+ }
21
+
22
+ const polygons = featureIds.map((id) => draw.get(id))
23
+ const feature = mergePolygons(polygons)
24
+
25
+ if (feature) {
26
+ eventBus.emit('draw:merge', { originalFeatureIds: featureIds, feature })
27
+ }
28
+
29
+ return feature
30
+ }
@@ -0,0 +1,57 @@
1
+ import { merge } from './merge.js'
2
+ import { mergePolygons } from '../utils/spatial.js'
3
+
4
+ jest.mock('../utils/spatial.js', () => ({ mergePolygons: jest.fn() }))
5
+
6
+ const makeContext = (overrides = {}) => {
7
+ const eventBus = { emit: jest.fn() }
8
+ const draw = { get: jest.fn((id) => ({ id })) }
9
+ const context = {
10
+ mapProvider: { draw },
11
+ services: { eventBus },
12
+ ...overrides
13
+ }
14
+ return { context, draw, eventBus }
15
+ }
16
+
17
+ beforeEach(() => jest.clearAllMocks())
18
+
19
+ describe('merge', () => {
20
+ test('does nothing when there is no draw instance', () => {
21
+ const { context, eventBus } = makeContext({ mapProvider: { draw: null } })
22
+ expect(merge(context, ['a', 'b'])).toBeNull()
23
+ expect(eventBus.emit).not.toHaveBeenCalled()
24
+ })
25
+
26
+ test('resolves each feature id via the adapter and merges them', () => {
27
+ const { context, draw } = makeContext()
28
+ mergePolygons.mockReturnValue({ id: 'a', geometry: { type: 'Polygon', coordinates: [] } })
29
+
30
+ merge(context, ['a', 'b'])
31
+
32
+ expect(draw.get).toHaveBeenCalledWith('a')
33
+ expect(draw.get).toHaveBeenCalledWith('b')
34
+ expect(mergePolygons).toHaveBeenCalledWith([{ id: 'a' }, { id: 'b' }])
35
+ })
36
+
37
+ test('returns the merged feature and emits draw:merge on success', () => {
38
+ const { context, eventBus } = makeContext()
39
+ const feature = { id: 'a', geometry: { type: 'Polygon', coordinates: [] } }
40
+ mergePolygons.mockReturnValue(feature)
41
+
42
+ const result = merge(context, ['a', 'b'])
43
+
44
+ expect(result).toBe(feature)
45
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:merge', { originalFeatureIds: ['a', 'b'], feature })
46
+ })
47
+
48
+ test('returns null and does not emit when the merge fails', () => {
49
+ const { context, eventBus } = makeContext()
50
+ mergePolygons.mockReturnValue(null)
51
+
52
+ const result = merge(context, ['a', 'b'])
53
+
54
+ expect(result).toBeNull()
55
+ expect(eventBus.emit).not.toHaveBeenCalled()
56
+ })
57
+ })
@@ -0,0 +1,41 @@
1
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
2
+
3
+ // Near-identical to newPolygon.js, differing only in the 'draw_line'/'draw_polygon'
4
+ // mode string — kept as two small, explicit entry points rather than one
5
+ // mode-parameterised factory. Revisit if a third geometry type is ever added.
6
+ export const newLine = ({ appState, appConfig, pluginConfig, pluginState, mapState, mapProvider, services }, featureId, options = {}) => {
7
+ const { dispatch } = pluginState
8
+ const { draw } = mapProvider
9
+ const { eventBus } = services
10
+
11
+ if (!draw) {
12
+ return
13
+ }
14
+
15
+ eventBus.emit('draw:started', { mode: 'draw_line' })
16
+
17
+ const snapLayers = options.snapLayers === undefined ? (pluginConfig.snapLayers ?? null) : options.snapLayers
18
+ draw.setSnapLayers(snapLayers)
19
+ dispatch({ type: 'SET_HAS_SNAP_LAYERS', payload: snapLayers?.length > 0 })
20
+
21
+ const { stroke, fill, strokeWidth, properties: customProperties, onGeometryChange, ...modeOptions } = options
22
+ // Per-call callback overrides the plugin-level one; events.js reads this on every commit.
23
+ draw._geometryValidator = onGeometryChange ?? pluginConfig.onGeometryChange
24
+ const properties = {
25
+ ...customProperties,
26
+ ...flattenStyleProperties({ stroke, fill, strokeWidth })
27
+ }
28
+
29
+ draw.changeMode('draw_line', {
30
+ container: appState.layoutRefs.viewportRef.current,
31
+ addVertexButtonId: `${appConfig.id}-draw-add-point`,
32
+ interfaceType: appState.interfaceType,
33
+ crossHair: mapState.crossHair,
34
+ getSnapEnabled: () => draw.isSnapEnabled(),
35
+ featureId,
36
+ ...modeOptions,
37
+ properties
38
+ })
39
+
40
+ dispatch({ type: 'SET_MODE', payload: 'draw_line' })
41
+ }
@@ -0,0 +1,93 @@
1
+ import { newLine } from './newLine.js'
2
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
3
+
4
+ jest.mock('../utils/flattenStyleProperties.js', () => ({
5
+ flattenStyleProperties: jest.fn(() => ({ _flat: true }))
6
+ }))
7
+
8
+ const makeContext = (overrides = {}) => {
9
+ const dispatch = jest.fn()
10
+ const eventBus = { emit: jest.fn() }
11
+ const draw = { setSnapLayers: jest.fn(), changeMode: jest.fn(), isSnapEnabled: jest.fn(() => false) }
12
+ const context = {
13
+ appState: { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'mouse' },
14
+ appConfig: { id: 'app' },
15
+ pluginConfig: { snapLayers: ['pc-layer'] },
16
+ pluginState: { dispatch },
17
+ mapState: { crossHair: true },
18
+ mapProvider: { draw },
19
+ services: { eventBus },
20
+ ...overrides
21
+ }
22
+ return { context, dispatch, eventBus, draw }
23
+ }
24
+
25
+ beforeEach(() => jest.clearAllMocks())
26
+
27
+ describe('newLine', () => {
28
+ test('does nothing when there is no draw instance', () => {
29
+ const { context, eventBus } = makeContext({ mapProvider: { draw: null } })
30
+ newLine(context, 'f1')
31
+ expect(eventBus.emit).not.toHaveBeenCalled()
32
+ })
33
+
34
+ test('starts line drawing with flattened style properties', () => {
35
+ const { context, dispatch, eventBus, draw } = makeContext()
36
+
37
+ newLine(context, 'f1', { stroke: 'red', fill: 'blue', strokeWidth: 3, properties: { name: 'x' }, extra: 'opt' })
38
+
39
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:started', { mode: 'draw_line' })
40
+ expect(flattenStyleProperties).toHaveBeenCalledWith({ stroke: 'red', fill: 'blue', strokeWidth: 3 })
41
+
42
+ expect(draw.changeMode).toHaveBeenCalledWith('draw_line', expect.objectContaining({
43
+ container: 'viewport',
44
+ addVertexButtonId: 'app-draw-add-point',
45
+ interfaceType: 'mouse',
46
+ crossHair: true,
47
+ featureId: 'f1',
48
+ extra: 'opt',
49
+ properties: { name: 'x', _flat: true }
50
+ }))
51
+
52
+ const opts = draw.changeMode.mock.calls[0][1]
53
+ expect(opts.getSnapEnabled()).toBe(false)
54
+ expect(draw.isSnapEnabled).toHaveBeenCalled()
55
+
56
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: 'draw_line' })
57
+ })
58
+
59
+ test('prefers explicit option snapLayers and flags them', () => {
60
+ const { context, draw, dispatch } = makeContext()
61
+ newLine(context, 'f1', { snapLayers: ['opt'] })
62
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(['opt'])
63
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: true })
64
+ })
65
+
66
+ test('falls back to the plugin config snapLayers', () => {
67
+ const { context, draw } = makeContext()
68
+ newLine(context, 'f1')
69
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(['pc-layer'])
70
+ })
71
+
72
+ test('falls back to null when neither option nor config is set', () => {
73
+ const { context, draw, dispatch } = makeContext({ pluginConfig: {} })
74
+ newLine(context, 'f1')
75
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(null)
76
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: false })
77
+ })
78
+
79
+ test('stores a per-call onGeometryChange validator without leaking it into mode options', () => {
80
+ const { context, draw } = makeContext()
81
+ const onGeometryChange = jest.fn()
82
+ newLine(context, 'f1', { onGeometryChange })
83
+ expect(draw._geometryValidator).toBe(onGeometryChange)
84
+ expect(draw.changeMode.mock.calls[0][1]).not.toHaveProperty('onGeometryChange')
85
+ })
86
+
87
+ test('falls back to the plugin-level onGeometryChange validator', () => {
88
+ const pluginOnGeometryChange = jest.fn()
89
+ const { context, draw } = makeContext({ pluginConfig: { onGeometryChange: pluginOnGeometryChange } })
90
+ newLine(context, 'f1')
91
+ expect(draw._geometryValidator).toBe(pluginOnGeometryChange)
92
+ })
93
+ })
@@ -0,0 +1,41 @@
1
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
2
+
3
+ // Near-identical to newLine.js, differing only in the 'draw_polygon'/'draw_line'
4
+ // mode string — kept as two small, explicit entry points rather than one
5
+ // mode-parameterised factory. Revisit if a third geometry type is ever added.
6
+ export const newPolygon = ({ appState, appConfig, pluginConfig, pluginState, mapState, mapProvider, services }, featureId, options = {}) => {
7
+ const { dispatch } = pluginState
8
+ const { draw } = mapProvider
9
+ const { eventBus } = services
10
+
11
+ if (!draw) {
12
+ return
13
+ }
14
+
15
+ eventBus.emit('draw:started', { mode: 'draw_polygon' })
16
+
17
+ const snapLayers = options.snapLayers === undefined ? (pluginConfig.snapLayers ?? null) : options.snapLayers
18
+ draw.setSnapLayers(snapLayers)
19
+ dispatch({ type: 'SET_HAS_SNAP_LAYERS', payload: snapLayers?.length > 0 })
20
+
21
+ const { stroke, fill, strokeWidth, properties: customProperties, onGeometryChange, ...modeOptions } = options
22
+ // Per-call callback overrides the plugin-level one; events.js reads this on every commit.
23
+ draw._geometryValidator = onGeometryChange ?? pluginConfig.onGeometryChange
24
+ const properties = {
25
+ ...customProperties,
26
+ ...flattenStyleProperties({ stroke, fill, strokeWidth })
27
+ }
28
+
29
+ draw.changeMode('draw_polygon', {
30
+ container: appState.layoutRefs.viewportRef.current,
31
+ addVertexButtonId: `${appConfig.id}-draw-add-point`,
32
+ interfaceType: appState.interfaceType,
33
+ crossHair: mapState.crossHair,
34
+ getSnapEnabled: () => draw.isSnapEnabled(),
35
+ featureId,
36
+ ...modeOptions,
37
+ properties
38
+ })
39
+
40
+ dispatch({ type: 'SET_MODE', payload: 'draw_polygon' })
41
+ }
@@ -0,0 +1,93 @@
1
+ import { newPolygon } from './newPolygon.js'
2
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
3
+
4
+ jest.mock('../utils/flattenStyleProperties.js', () => ({
5
+ flattenStyleProperties: jest.fn(() => ({ _flat: true }))
6
+ }))
7
+
8
+ const makeContext = (overrides = {}) => {
9
+ const dispatch = jest.fn()
10
+ const eventBus = { emit: jest.fn() }
11
+ const draw = { setSnapLayers: jest.fn(), changeMode: jest.fn(), isSnapEnabled: jest.fn(() => true) }
12
+ const context = {
13
+ appState: { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'mouse' },
14
+ appConfig: { id: 'app' },
15
+ pluginConfig: { snapLayers: ['pc-layer'] },
16
+ pluginState: { dispatch },
17
+ mapState: { crossHair: false },
18
+ mapProvider: { draw },
19
+ services: { eventBus },
20
+ ...overrides
21
+ }
22
+ return { context, dispatch, eventBus, draw }
23
+ }
24
+
25
+ beforeEach(() => jest.clearAllMocks())
26
+
27
+ describe('newPolygon', () => {
28
+ test('does nothing when there is no draw instance', () => {
29
+ const { context, eventBus } = makeContext({ mapProvider: { draw: null } })
30
+ newPolygon(context, 'f1')
31
+ expect(eventBus.emit).not.toHaveBeenCalled()
32
+ })
33
+
34
+ test('starts polygon drawing with flattened style properties', () => {
35
+ const { context, dispatch, eventBus, draw } = makeContext()
36
+
37
+ newPolygon(context, 'f1', { stroke: 'red', fill: 'blue', strokeWidth: 3, properties: { name: 'x' }, extra: 'opt' })
38
+
39
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:started', { mode: 'draw_polygon' })
40
+ expect(flattenStyleProperties).toHaveBeenCalledWith({ stroke: 'red', fill: 'blue', strokeWidth: 3 })
41
+
42
+ expect(draw.changeMode).toHaveBeenCalledWith('draw_polygon', expect.objectContaining({
43
+ container: 'viewport',
44
+ addVertexButtonId: 'app-draw-add-point',
45
+ interfaceType: 'mouse',
46
+ crossHair: false,
47
+ featureId: 'f1',
48
+ extra: 'opt',
49
+ properties: { name: 'x', _flat: true }
50
+ }))
51
+
52
+ const opts = draw.changeMode.mock.calls[0][1]
53
+ expect(opts.getSnapEnabled()).toBe(true)
54
+ expect(draw.isSnapEnabled).toHaveBeenCalled()
55
+
56
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: 'draw_polygon' })
57
+ })
58
+
59
+ test('prefers explicit option snapLayers and flags them', () => {
60
+ const { context, draw, dispatch } = makeContext()
61
+ newPolygon(context, 'f1', { snapLayers: ['opt'] })
62
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(['opt'])
63
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: true })
64
+ })
65
+
66
+ test('falls back to the plugin config snapLayers', () => {
67
+ const { context, draw } = makeContext()
68
+ newPolygon(context, 'f1')
69
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(['pc-layer'])
70
+ })
71
+
72
+ test('falls back to null when neither option nor config is set', () => {
73
+ const { context, draw, dispatch } = makeContext({ pluginConfig: {} })
74
+ newPolygon(context, 'f1')
75
+ expect(draw.setSnapLayers).toHaveBeenCalledWith(null)
76
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: false })
77
+ })
78
+
79
+ test('stores a per-call onGeometryChange validator without leaking it into mode options', () => {
80
+ const { context, draw } = makeContext()
81
+ const onGeometryChange = jest.fn()
82
+ newPolygon(context, 'f1', { onGeometryChange })
83
+ expect(draw._geometryValidator).toBe(onGeometryChange)
84
+ expect(draw.changeMode.mock.calls[0][1]).not.toHaveProperty('onGeometryChange')
85
+ })
86
+
87
+ test('falls back to the plugin-level onGeometryChange validator', () => {
88
+ const pluginOnGeometryChange = jest.fn()
89
+ const { context, draw } = makeContext({ pluginConfig: { onGeometryChange: pluginOnGeometryChange } })
90
+ newPolygon(context, 'f1')
91
+ expect(draw._geometryValidator).toBe(pluginOnGeometryChange)
92
+ })
93
+ })