@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,23 @@
1
+ export const DisabledMode = {
2
+ onSetup () {
3
+ return {} // Return empty state
4
+ },
5
+
6
+ onClick () {
7
+ // Prevent feature selection
8
+ return false
9
+ },
10
+
11
+ onKeyUp () {
12
+ return false
13
+ },
14
+
15
+ onDrag () {
16
+ return false
17
+ },
18
+
19
+ toDisplayFeatures (_state, geojson, display) {
20
+ geojson.properties.active = 'false'
21
+ display(geojson)
22
+ }
23
+ }
@@ -0,0 +1,21 @@
1
+ import { DisabledMode } from './disabledMode.js'
2
+
3
+ describe('DisabledMode', () => {
4
+ test('onSetup returns an empty state', () => {
5
+ expect(DisabledMode.onSetup()).toEqual({})
6
+ })
7
+
8
+ test('interaction handlers are disabled (return false to block selection/drag/keys)', () => {
9
+ expect(DisabledMode.onClick()).toBe(false)
10
+ expect(DisabledMode.onKeyUp()).toBe(false)
11
+ expect(DisabledMode.onDrag()).toBe(false)
12
+ })
13
+
14
+ test('toDisplayFeatures marks the feature inactive and still displays it', () => {
15
+ const display = jest.fn()
16
+ const geojson = { properties: {} }
17
+ DisabledMode.toDisplayFeatures({}, geojson, display)
18
+ expect(geojson.properties.active).toBe('false')
19
+ expect(display).toHaveBeenCalledWith(geojson)
20
+ })
21
+ })
@@ -0,0 +1,15 @@
1
+ import MapboxDraw from '@mapbox/mapbox-gl-draw'
2
+ import { isValidLineClick } from '../../../utils/spatial.js'
3
+ import { createDrawMode } from './createDrawMode.js'
4
+
5
+ // Extend the built-in mode via the package's public API (MapboxDraw.modes) rather than a deep internal import
6
+ export const DrawLineMode = createDrawMode(MapboxDraw.modes.draw_line_string, {
7
+ featureProp: 'line',
8
+ geometryType: 'LineString',
9
+ getCoords: (feature) => feature.coordinates,
10
+ validateClick: (feature) => isValidLineClick(feature.coordinates),
11
+ excludeFeatureIdFromSetup: true, // DrawLineString interprets featureId as "continue existing"
12
+ finishOnInvalidClick: true, // Clicking same spot (like double-click) finishes the line
13
+ // Display coords during drawing: [v0...vN, rubber_band]
14
+ getPlacedCoords: (geojson) => geojson.geometry.coordinates.slice(0, -1)
15
+ })
@@ -0,0 +1,37 @@
1
+ import { setup, firedWith, DrawLineMode } from './drawMode/__helpers__/harness.js'
2
+
3
+ const drawVertexMarkers = (ctx, state, geometry) => {
4
+ const display = jest.fn()
5
+ ctx.toDisplayFeatures(state, { type: 'Feature', properties: { id: state.line.id }, geometry }, display)
6
+ return display.mock.calls.map(([f]) => f)
7
+ .filter((f) => f.properties.meta === 'draw-vertex')
8
+ .map((m) => m.geometry.coordinates)
9
+ }
10
+
11
+ describe('DrawLineMode config', () => {
12
+ test('excludeFeatureIdFromSetup starts a fresh line feature even when a featureId is supplied', () => {
13
+ const { state } = setup(DrawLineMode)
14
+ expect(state.line).toBeDefined()
15
+ expect(state.featureId).toBe('shape-1')
16
+ })
17
+
18
+ test('getCoords reads the line coordinates directly (not a ring)', () => {
19
+ const { ctx, state } = setup(DrawLineMode)
20
+ ctx.doClick(state)
21
+ expect(state.line.coordinates[0]).toEqual([5, 5]) // first placed vertex, at map centre
22
+ })
23
+
24
+ test('getPlacedCoords marks every placed vertex, excluding the rubber band', () => {
25
+ const { ctx, state } = setup(DrawLineMode)
26
+ // Display coords: [v0..vN, rubber_band] → placed = slice(0, -1)
27
+ expect(drawVertexMarkers(ctx, state, { type: 'LineString', coordinates: [[5, 5], [8, 8]] })).toEqual([[5, 5]])
28
+ })
29
+
30
+ test('finishOnInvalidClick finishes the line when the same spot is clicked twice', () => {
31
+ const { ctx, state } = setup(DrawLineMode)
32
+ ctx.doClick(state)
33
+ ctx.doClick(state) // same spot → finish (like a double-click)
34
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(1)
35
+ expect(ctx.changeMode).toHaveBeenCalledWith('simple_select', { featureIds: [state.line.id] })
36
+ })
37
+ })
@@ -0,0 +1,115 @@
1
+ import { DrawPolygonMode } from '../../drawPolygonMode.js'
2
+ import { DrawLineMode } from '../../drawLineMode.js'
3
+ import { createUndoStack } from '../../../../../utils/undoStack.js'
4
+ import PolygonFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/polygon.js'
5
+ import LineStringFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/line_string.js'
6
+
7
+ /**
8
+ * Shared test harness for the createDrawMode handler modules. Exercises the real
9
+ * DrawPolygonMode / DrawLineMode (real mapbox-gl-draw parent modes and feature
10
+ * classes) over test doubles for the map, store and DOM. Excluded from coverage.
11
+ */
12
+
13
+ export { DrawPolygonMode, DrawLineMode }
14
+ export const CENTER = { lng: 5, lat: 5 }
15
+
16
+ const createMap = () => {
17
+ const canvas = document.createElement('canvas')
18
+ const listeners = {}
19
+ return {
20
+ _undoInProgress: false,
21
+ _undoStack: null,
22
+ _snapInstance: null,
23
+ doubleClickZoom: { disable: jest.fn(), enable: jest.fn() },
24
+ getCanvas: () => canvas,
25
+ getCenter: () => ({ ...CENTER }),
26
+ project: jest.fn(() => ({ x: 50, y: 50 })),
27
+ unproject: jest.fn(() => ({ ...CENTER })),
28
+ fire: jest.fn(function (type, e) { (listeners[type] ?? []).forEach((h) => h(e)) }),
29
+ on: jest.fn((type, h) => { (listeners[type] ??= []).push(h) }),
30
+ off: jest.fn((type, h) => { listeners[type] = (listeners[type] ?? []).filter((x) => x !== h) })
31
+ }
32
+ }
33
+
34
+ // The `this` context mapbox-gl-draw gives a mode: map, ctx accessors and the mode's own methods
35
+ const createModeContext = (mode) => {
36
+ const map = createMap()
37
+ const features = new Map()
38
+ const store = {
39
+ add: jest.fn((f) => features.set(f.id, f)),
40
+ delete: jest.fn((ids) => ids.forEach((id) => features.delete(id))),
41
+ render: jest.fn(),
42
+ featureChanged: jest.fn(),
43
+ getInitialConfigValue: jest.fn(() => true)
44
+ }
45
+ const ctx = {
46
+ map,
47
+ _ctx: { store, api: { changeMode: jest.fn(), add: jest.fn(), delete: jest.fn() }, options: {} },
48
+ addFeature: (f) => store.add(f),
49
+ getFeature: (id) => features.get(id),
50
+ deleteFeature: jest.fn((ids) => ids.forEach((id) => features.delete(id))),
51
+ clearSelectedFeatures: jest.fn(),
52
+ updateUIClasses: jest.fn(),
53
+ activateUIButton: jest.fn(),
54
+ setActionableState: jest.fn(),
55
+ changeMode: jest.fn()
56
+ }
57
+ ctx.newFeature = (geojson) => geojson.geometry.type === 'Polygon'
58
+ ? new PolygonFeature(ctx._ctx, geojson)
59
+ : new LineStringFeature(ctx._ctx, geojson)
60
+ Object.assign(ctx, mode)
61
+ contexts.push(ctx)
62
+ return ctx
63
+ }
64
+
65
+ // Remove window/container/map listeners registered by onSetup so tests don't leak into each other
66
+ const contexts = []
67
+ const removeListeners = (ctx) => ctx._listeners?.forEach(([t, e, h]) =>
68
+ t.removeEventListener ? t.removeEventListener(e, h) : t.off(e, h))
69
+
70
+ const createContainer = () => {
71
+ const container = document.createElement('div')
72
+ container.tabIndex = 0
73
+ const marker = document.createElement('div')
74
+ marker.id = 'vertex-marker'
75
+ container.appendChild(marker)
76
+ const button = document.createElement('button')
77
+ button.id = 'add-vertex'
78
+ container.appendChild(button)
79
+ document.body.appendChild(container)
80
+ return { container, marker, button }
81
+ }
82
+
83
+ export const setup = (mode, options = {}) => {
84
+ const ctx = createModeContext(mode)
85
+ const dom = createContainer()
86
+ ctx.map._undoStack = createUndoStack(() => {})
87
+ const state = ctx.onSetup({
88
+ container: dom.container,
89
+ vertexMarkerId: 'vertex-marker',
90
+ addVertexButtonId: 'add-vertex',
91
+ interfaceType: 'mouse',
92
+ featureId: 'shape-1',
93
+ properties: { label: 'field' },
94
+ ...options
95
+ })
96
+ return { ctx, state, ...dom }
97
+ }
98
+
99
+ export const clickEvent = (ctx, lng, lat, overrides = {}) => ({
100
+ lngLat: { lng, lat },
101
+ point: { x: lng, y: lat },
102
+ originalEvent: { button: 0, target: ctx.map.getCanvas(), ...overrides }
103
+ })
104
+
105
+ export const clickAt = (ctx, state, lng, lat) => ctx.onClick(state, clickEvent(ctx, lng, lat))
106
+
107
+ export const firedWith = (map, type) => map.fire.mock.calls.filter(([t]) => t === type).map(([, e]) => e)
108
+
109
+ export const activeSnap = () => ({ status: true, snapStatus: true, snapCoords: [9, 9], snapToClosestPoint: jest.fn() })
110
+
111
+ afterEach(() => {
112
+ contexts.splice(0).forEach(removeListeners)
113
+ document.body.innerHTML = ''
114
+ jest.useRealTimers()
115
+ })
@@ -0,0 +1,193 @@
1
+ import {
2
+ getSnapInstance, isSnapActive, isSnapEnabled, createSnappedEvent, createSnappedClickEvent
3
+ } from '../../utils/snapHelpers.js'
4
+ import { attemptPlacement } from '../../../../validation/validateGeometry.js'
5
+
6
+ // The commit-level geometrychange payload for a vertex commit: the placed-only
7
+ // geometry (trailing rubber-band point dropped so validation tests the committed shape).
8
+ const placedDrawGeometryChange = (feature, getCoords, phase) => {
9
+ const placed = getCoords(feature).slice(0, -1)
10
+ const type = feature.toGeoJSON().geometry.type
11
+ const geometry = type === 'Polygon'
12
+ ? { type: 'Polygon', coordinates: [placed] }
13
+ : { type: 'LineString', coordinates: placed }
14
+ return { feature: { type: 'Feature', geometry, properties: {} }, phase, vertexIndex: Math.max(0, placed.length - 1) }
15
+ }
16
+
17
+ // Fire a commit-level geometrychange for validation, deferred a tick so it runs after
18
+ // the current click settles.
19
+ const scheduleDrawValidation = (map, getFeature, getCoords, state, phase) => {
20
+ setTimeout(() => {
21
+ const feature = getFeature(state)
22
+ if (!feature) { return }
23
+ map.fire('draw.geometrychange', placedDrawGeometryChange(feature, getCoords, phase))
24
+ }, 0)
25
+ }
26
+
27
+ // Re-id a freshly created feature to the caller's requested id.
28
+ const reidCreatedFeature = (api, feature, featureId) => {
29
+ api.delete(feature.id)
30
+ feature.id = featureId
31
+ api.add(feature, { userProperties: true })
32
+ }
33
+
34
+ // Guards, notifications, and small wiring handlers shared by the click paths.
35
+ const createClickHelpers = ({ geometryType, getFeature, getCoords }) => ({
36
+ // Non-drawing clicks: secondary buttons, clicks during an undo, or off-canvas.
37
+ _isIgnorableClick (e) {
38
+ return e.originalEvent.button > 0 || this.map._undoInProgress || e.originalEvent.target !== this.map.getCanvas()
39
+ },
40
+
41
+ // Attempt a placement through the shared hard rules + user callback. On a veto
42
+ // the vertex never appears and a draw.placementblocked event carries the
43
+ // reason. The trailing rubber-band coord is dropped so the candidate is placed
44
+ // vertices + the point about to be placed.
45
+ _canPlaceVertex (state, point) {
46
+ const feature = getFeature(state)
47
+ if (!feature || !point) { return true }
48
+ const result = attemptPlacement({
49
+ placed: getCoords(feature).slice(0, -1),
50
+ point,
51
+ geometryType,
52
+ onGeometryChange: this.map._drawGeometryValidator
53
+ })
54
+ if (!result.valid) {
55
+ this.map.fire('draw.placementblocked', result.blocked)
56
+ }
57
+ return result.valid
58
+ },
59
+
60
+ dispatchVertexChange (coords) {
61
+ // Both polygon ring and LineString store [v0...vN, rubber_band] during drawing — subtract 1 to get placed vertex count
62
+ this.map.fire('draw.vertexchange', {
63
+ numVertecies: Math.max(0, coords.length - 1)
64
+ })
65
+ },
66
+
67
+ // Emit a commit-level geometrychange after a vertex commit (placement or undo)
68
+ // so the validation layer can gate the Done button.
69
+ emitDrawValidation (state, phase = 'commit-add') {
70
+ scheduleDrawValidation(this.map, getFeature, getCoords, state, phase)
71
+ },
72
+
73
+ onTap () {
74
+
75
+ },
76
+
77
+ onVertexButtonClick (state, e) {
78
+ // Only trigger for the specific add vertex button, and skip during undo
79
+ if (state.addVertexButtonId && !this.map._undoInProgress && e.target.closest(`#${state.addVertexButtonId}`)) {
80
+ this.doClick(state)
81
+ }
82
+ },
83
+
84
+ onCreate (state, e) {
85
+ reidCreatedFeature(this._ctx.api, e.features[0], state.featureId)
86
+ }
87
+ })
88
+
89
+ // The click paths themselves: mouse clicks and the simulated crosshair click
90
+ // (touch / keyboard / add-vertex button).
91
+ const createClickActions = ({ ParentMode, getFeature, getCoords, validateClick, finishOnInvalidClick }) => {
92
+ // Snap is inactive: sync the trailing rubber-band point to the click, then guard
93
+ // against a duplicate-coordinate click reaching ParentMode, which would trigger a
94
+ // changeMode chain and cause a runtime error on coords.length access (polygon).
95
+ // Returns false when the click should be dropped without placing a vertex.
96
+ const prepareUnsnappedClick = (state, e) => {
97
+ const coords = getCoords(getFeature(state))
98
+ if (coords.length > 0) {
99
+ coords[coords.length - 1] = [e.lngLat.lng, e.lngLat.lat]
100
+ }
101
+ return finishOnInvalidClick || validateClick(getFeature(state))
102
+ }
103
+
104
+ return {
105
+ onClick (state, e) {
106
+ // Skip non-primary clicks, undo operations, or clicks outside canvas
107
+ if (this._isIgnorableClick(e)) {
108
+ return
109
+ }
110
+ // Block a finish/close gesture (clicking a placed vertex) while the shape is invalid.
111
+ if (this.map._drawGeometryValid === false && e.featureTarget?.properties?.meta === 'vertex') {
112
+ return
113
+ }
114
+ const snap = getSnapInstance(this.map)
115
+ if (isSnapEnabled(state) && isSnapActive(snap)) {
116
+ e = createSnappedEvent(e, snap)
117
+ } else if (!prepareUnsnappedClick(state, e)) {
118
+ return
119
+ } else {
120
+ // No action
121
+ }
122
+ // Hard gate: a placement the rules or the user callback veto never appears, so
123
+ // an unrecoverable state (e.g. a self-crossing path) can't be drawn forward.
124
+ if (!this._canPlaceVertex(state, [e.lngLat.lng, e.lngLat.lat])) {
125
+ return
126
+ }
127
+ const coordsBefore = getCoords(getFeature(state)).length
128
+ ParentMode.onClick.call(this, state, e)
129
+ // Push undo and update count if a vertex was added
130
+ if (getCoords(getFeature(state)).length > coordsBefore) {
131
+ this.pushDrawUndo(state)
132
+ this.dispatchVertexChange(getCoords(getFeature(state)))
133
+ this.emitDrawValidation(state)
134
+ }
135
+ },
136
+
137
+ doClick (state) {
138
+ // Skip during undo operation
139
+ if (this.map._undoInProgress) {
140
+ return
141
+ }
142
+
143
+ const feature = getFeature(state)
144
+ const coords = getCoords(feature)
145
+ this.dispatchVertexChange(coords)
146
+
147
+ if (!validateClick(feature)) {
148
+ // For lines: clicking same spot (like double-click) should finish the line — but
149
+ // only when the geometry is valid, so an invalid line can't be completed.
150
+ // isValidLineClick only returns false with 2+ coords, so coords.length is always > 1 here.
151
+ if (finishOnInvalidClick && this.map._drawGeometryValid !== false) {
152
+ coords.pop()
153
+ this.map.fire('draw.create', { features: [feature.toGeoJSON()] })
154
+ this.changeMode('simple_select', { featureIds: [feature.id] })
155
+ }
156
+ return
157
+ }
158
+
159
+ const snap = getSnapInstance(this.map)
160
+ const snappedEvent = isSnapEnabled(state) && createSnappedClickEvent(this.map, snap)
161
+
162
+ // Hard-gate parity with onClick: touch/keyboard/add-button placements are vetoed
163
+ // at the point that would actually be committed (snapped or map centre).
164
+ const target = snappedEvent ? snappedEvent.lngLat : this.map.getCenter()
165
+ if (!this._canPlaceVertex(state, [target.lng, target.lat])) {
166
+ return
167
+ }
168
+
169
+ if (snappedEvent) {
170
+ ParentMode.onClick.call(this, state, snappedEvent)
171
+ this._ctx.store.render()
172
+ } else {
173
+ this._simulateMouse('click', ParentMode.onClick, state)
174
+ }
175
+
176
+ // Push undo and update count if a vertex was added. A validated click always
177
+ // adds one vertex via the parent mode, so this runs on every successful doClick.
178
+ const newCoords = getCoords(getFeature(state))
179
+ this.pushDrawUndo(state)
180
+ this.dispatchVertexChange(newCoords)
181
+ this.emitDrawValidation(state)
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Click / vertex-placement handling for the shared draw mode: mouse clicks, the
188
+ * add-vertex button, and the draw.create re-id step. Part of createDrawMode.
189
+ */
190
+ export const createClickHandlers = (deps) => ({
191
+ ...createClickHelpers(deps),
192
+ ...createClickActions(deps)
193
+ })
@@ -0,0 +1,241 @@
1
+ import { setup, clickAt, clickEvent, firedWith, activeSnap, DrawPolygonMode, DrawLineMode, CENTER } from './__helpers__/harness.js'
2
+
3
+ describe('mouse clicks (polygon)', () => {
4
+ test('each click at a new position places a vertex, fires vertexchange and pushes an undo op', () => {
5
+ const { ctx, state } = setup(DrawPolygonMode)
6
+ clickAt(ctx, state, 0, 0)
7
+ clickAt(ctx, state, 10, 0)
8
+
9
+ // Ring is [v0, v1, rubber_band]
10
+ expect(state.polygon.coordinates[0]).toHaveLength(3)
11
+ expect(state.polygon.coordinates[0][0]).toEqual([0, 0])
12
+ expect(state.polygon.coordinates[0][1]).toEqual([10, 0])
13
+ expect(firedWith(ctx.map, 'draw.vertexchange').pop()).toEqual({ numVertecies: 2 })
14
+ expect(ctx.map._undoStack.length).toBe(2)
15
+ })
16
+
17
+ test('clicking the same position again is rejected', () => {
18
+ const { ctx, state } = setup(DrawPolygonMode)
19
+ clickAt(ctx, state, 0, 0)
20
+ clickAt(ctx, state, 0, 0)
21
+ expect(state.polygon.coordinates[0]).toHaveLength(2)
22
+ expect(ctx.map._undoStack.length).toBe(1)
23
+ })
24
+
25
+ test('rejects a click that would make the drawn path cross itself', () => {
26
+ const { ctx, state } = setup(DrawPolygonMode)
27
+ clickAt(ctx, state, 0, 0)
28
+ clickAt(ctx, state, 10, 10)
29
+ clickAt(ctx, state, 10, 0)
30
+ const lenBefore = state.polygon.coordinates[0].length
31
+ clickAt(ctx, state, 0, 10) // the new edge would cross the (0,0)-(10,10) edge
32
+ expect(state.polygon.coordinates[0].length).toBe(lenBefore) // vertex never placed
33
+ })
34
+
35
+ test('a rejected placement fires draw.placementblocked with the reason', () => {
36
+ const { ctx, state } = setup(DrawPolygonMode)
37
+ clickAt(ctx, state, 0, 0)
38
+ clickAt(ctx, state, 10, 10)
39
+ clickAt(ctx, state, 10, 0)
40
+ clickAt(ctx, state, 0, 10)
41
+ expect(firedWith(ctx.map, 'draw.placementblocked').pop()).toEqual(expect.objectContaining({
42
+ phase: 'place',
43
+ mode: 'draw_polygon',
44
+ vertexIndex: 3,
45
+ reason: expect.any(String),
46
+ feature: expect.objectContaining({ type: 'Feature' })
47
+ }))
48
+ })
49
+
50
+ test('the user callback can veto a mouse placement (and receives phase "place")', () => {
51
+ const { ctx, state } = setup(DrawPolygonMode)
52
+ ctx.map._drawGeometryValidator = jest.fn((event) =>
53
+ event.phase === 'place' ? { valid: false, reason: 'outside region' } : { valid: true })
54
+ clickAt(ctx, state, 0, 0)
55
+ expect(state.polygon.coordinates[0]).toHaveLength(0) // first vertex vetoed
56
+ expect(firedWith(ctx.map, 'draw.placementblocked').pop()).toEqual(
57
+ expect.objectContaining({ reason: 'outside region', vertexIndex: 0 }))
58
+ })
59
+
60
+ test('non-primary buttons, undo-in-progress and off-canvas clicks are ignored', () => {
61
+ const { ctx, state } = setup(DrawPolygonMode)
62
+ ctx.onClick(state, clickEvent(ctx, 0, 0, { button: 2 }))
63
+ ctx.map._undoInProgress = true
64
+ clickAt(ctx, state, 0, 0)
65
+ ctx.map._undoInProgress = false
66
+ ctx.onClick(state, clickEvent(ctx, 0, 0, { target: document.body }))
67
+ expect(state.polygon.coordinates[0]).toHaveLength(0)
68
+ })
69
+
70
+ test('with snap active the vertex is placed at the snapped position', () => {
71
+ const { ctx, state } = setup(DrawPolygonMode, { getSnapEnabled: () => true })
72
+ ctx.map._snapInstance = activeSnap()
73
+ clickAt(ctx, state, 0, 0)
74
+ expect(state.polygon.coordinates[0][0]).toEqual([9, 9])
75
+ })
76
+
77
+ test('onTap is disabled', () => {
78
+ const { ctx, state } = setup(DrawPolygonMode)
79
+ expect(ctx.onTap(state, clickEvent(ctx, 0, 0))).toBeUndefined()
80
+ expect(state.polygon.coordinates[0]).toHaveLength(0)
81
+ })
82
+
83
+ test('placing a vertex emits a deferred commit-level geometrychange for validation', () => {
84
+ jest.useFakeTimers()
85
+ const { ctx, state } = setup(DrawPolygonMode)
86
+ clickAt(ctx, state, 0, 0)
87
+ clickAt(ctx, state, 10, 0)
88
+ jest.runAllTimers()
89
+ const geomChange = firedWith(ctx.map, 'draw.geometrychange').pop()
90
+ expect(geomChange).toEqual(expect.objectContaining({ phase: 'commit-add', feature: expect.any(Object) }))
91
+ jest.useRealTimers()
92
+ })
93
+
94
+ test('emitDrawValidation does not fire without a feature', () => {
95
+ jest.useFakeTimers()
96
+ const { ctx } = setup(DrawPolygonMode)
97
+ ctx.emitDrawValidation({}) // no polygon on the state
98
+ jest.runAllTimers()
99
+ expect(firedWith(ctx.map, 'draw.geometrychange')).toHaveLength(0)
100
+ jest.useRealTimers()
101
+ })
102
+
103
+ test('a line placement emits a LineString geometrychange', () => {
104
+ jest.useFakeTimers()
105
+ const { ctx, state } = setup(DrawLineMode)
106
+ clickAt(ctx, state, 0, 0)
107
+ clickAt(ctx, state, 10, 0)
108
+ jest.runAllTimers()
109
+ const geom = firedWith(ctx.map, 'draw.geometrychange').pop()
110
+ expect(geom.feature.geometry.type).toBe('LineString')
111
+ jest.useRealTimers()
112
+ })
113
+
114
+ test('blocks a close gesture (vertex click) while the geometry is invalid', () => {
115
+ const { ctx, state } = setup(DrawPolygonMode)
116
+ clickAt(ctx, state, 0, 0)
117
+ clickAt(ctx, state, 10, 0)
118
+ clickAt(ctx, state, 5, 10)
119
+ ctx.map._drawGeometryValid = false
120
+ const lenBefore = state.polygon.coordinates[0].length
121
+ ctx.onClick(state, { ...clickEvent(ctx, 0, 0), featureTarget: { properties: { meta: 'vertex' } } })
122
+ expect(state.polygon.coordinates[0].length).toBe(lenBefore) // finish gesture ignored
123
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(0)
124
+ })
125
+ })
126
+
127
+ describe('add-vertex button and doClick', () => {
128
+ test('clicking the add-vertex button places a vertex at map center', () => {
129
+ const { ctx, state, button } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
130
+ ctx.vertexButtonClickHandler({ target: button })
131
+ expect(state.polygon.coordinates[0][0]).toEqual([CENTER.lng, CENTER.lat])
132
+ expect(ctx.map._undoStack.length).toBe(1)
133
+ })
134
+
135
+ test('does not finish a line via doClick while the geometry is invalid', () => {
136
+ const { ctx, state } = setup(DrawLineMode)
137
+ clickAt(ctx, state, 0, 0)
138
+ clickAt(ctx, state, 10, 0)
139
+ ctx.map._drawGeometryValid = false
140
+ // Rubber-band duplicates the last placed vertex → a finish gesture.
141
+ state.line.setCoordinates([[0, 0], [10, 0], [10, 0]])
142
+ ctx.doClick(state)
143
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(0)
144
+ })
145
+
146
+ test('clicks elsewhere, without a button id, or during undo do nothing', () => {
147
+ const { ctx, state } = setup(DrawPolygonMode)
148
+ ctx.vertexButtonClickHandler({ target: document.body })
149
+ ctx.map._undoInProgress = true
150
+ ctx.doClick(state)
151
+ ctx.map._undoInProgress = false
152
+ const noButton = setup(DrawPolygonMode, { addVertexButtonId: null })
153
+ noButton.ctx.vertexButtonClickHandler({ target: document.body })
154
+ expect(state.polygon.coordinates[0]).toHaveLength(0)
155
+ expect(noButton.state.polygon.coordinates[0]).toHaveLength(0)
156
+ })
157
+
158
+ test('doClick rejects a placement that would make the drawn path cross itself', () => {
159
+ const { ctx, state } = setup(DrawPolygonMode)
160
+ clickAt(ctx, state, 0, 0)
161
+ clickAt(ctx, state, 10, 0)
162
+ clickAt(ctx, state, 10, -10)
163
+ const lenBefore = state.polygon.coordinates[0].length
164
+ ctx.doClick(state) // map centre is (5,5): the new edge would cross (0,0)-(10,0)
165
+ expect(state.polygon.coordinates[0].length).toBe(lenBefore)
166
+ expect(firedWith(ctx.map, 'draw.placementblocked').length).toBeGreaterThan(0)
167
+ })
168
+
169
+ test('the user callback can veto a doClick placement', () => {
170
+ const { ctx, state } = setup(DrawPolygonMode)
171
+ ctx.map._drawGeometryValidator = () => ({ valid: false, reason: 'outside region' })
172
+ ctx.doClick(state)
173
+ expect(state.polygon.coordinates[0]).toHaveLength(0)
174
+ expect(firedWith(ctx.map, 'draw.placementblocked').pop()).toEqual(
175
+ expect.objectContaining({ reason: 'outside region', phase: 'place' }))
176
+ })
177
+
178
+ test('doClick places at the snapped position when snapping', () => {
179
+ const { ctx, state } = setup(DrawPolygonMode, { getSnapEnabled: () => true })
180
+ ctx.map._snapInstance = activeSnap()
181
+ ctx.doClick(state)
182
+ expect(state.polygon.coordinates[0][0]).toEqual([9, 9])
183
+ expect(ctx._ctx.store.render).toHaveBeenCalled()
184
+ })
185
+
186
+ test('line: activating add-vertex at the same spot twice finishes the line', () => {
187
+ const { ctx, state } = setup(DrawLineMode)
188
+ ctx.doClick(state)
189
+ ctx.doClick(state)
190
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(1)
191
+ expect(ctx.changeMode).toHaveBeenCalledWith('simple_select', { featureIds: [state.line.id] })
192
+ })
193
+
194
+ test('polygon: a repeated add-vertex at the same spot is rejected without finishing', () => {
195
+ const { ctx, state } = setup(DrawPolygonMode)
196
+ ctx.doClick(state)
197
+ ctx.doClick(state)
198
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(0)
199
+ expect(ctx.changeMode).not.toHaveBeenCalled()
200
+ })
201
+ })
202
+
203
+ describe('line mode mouse interactions', () => {
204
+ test('mouse clicks place line vertices; moving then undoing keeps the rubber band on the last vertex', () => {
205
+ const { ctx, state } = setup(DrawLineMode)
206
+ clickAt(ctx, state, 0, 0)
207
+ clickAt(ctx, state, 10, 0)
208
+ clickAt(ctx, state, 10, 10)
209
+ expect(state.line.coordinates.map(([x]) => x)).toEqual([0, 10, 10, 10]) // 3 placed + rubber band
210
+
211
+ ctx.onMouseMove(state, { lngLat: { lng: 3, lat: 3 }, point: { x: 3, y: 3 } })
212
+ expect(state.line.coordinates.at(-1)).toEqual([3, 3])
213
+ expect(firedWith(ctx.map, 'draw.geometrychange').length).toBeGreaterThan(0)
214
+
215
+ ctx.map._undoStack.push({ type: 'draw_vertex' })
216
+ ctx.undoVertex(state)
217
+ const coords = state.line.coordinates
218
+ expect(coords[coords.length - 1]).toEqual(coords[coords.length - 2])
219
+ })
220
+
221
+ test('a repeated mouse click at the last vertex adds no new vertex and does not finish the line', () => {
222
+ const { ctx, state } = setup(DrawLineMode)
223
+ clickAt(ctx, state, 0, 0)
224
+ clickAt(ctx, state, 10, 0)
225
+ const before = state.line.coordinates.length
226
+ clickAt(ctx, state, 10, 0)
227
+ expect(state.line.coordinates.length).toBe(before)
228
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(0)
229
+ })
230
+ })
231
+
232
+ describe('onCreate (draw.create re-id)', () => {
233
+ test('re-ids the created feature to the requested featureId', () => {
234
+ const { ctx } = setup(DrawPolygonMode)
235
+ const feature = { id: 'temp-id' }
236
+ ctx.map.fire('draw.create', { features: [feature] })
237
+ expect(ctx._ctx.api.delete).toHaveBeenCalledWith('temp-id')
238
+ expect(feature.id).toBe('shape-1')
239
+ expect(ctx._ctx.api.add).toHaveBeenCalledWith(feature, { userProperties: true })
240
+ })
241
+ })