@defra/interactive-map 0.0.37-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 (244) 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-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  15. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  17. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  18. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  19. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  20. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  21. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  22. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  23. package/plugins/draw/dist/esm/index.js +1 -0
  24. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  26. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  27. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  28. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  29. package/plugins/draw/dist/umd/index.js +2 -0
  30. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  31. package/plugins/draw/src/DrawInit.jsx +89 -0
  32. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  33. package/plugins/draw/src/adapterEvents.js +127 -0
  34. package/plugins/draw/src/adapterEvents.test.js +20 -0
  35. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  36. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  37. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  38. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  39. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  40. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  41. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  42. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  43. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  44. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  45. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  46. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  47. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  48. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  49. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  86. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  87. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  88. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  89. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  95. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  96. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  97. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  98. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  99. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  100. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  101. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  102. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  103. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  104. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  105. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  106. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  107. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  108. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  109. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  110. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  111. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  112. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  113. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  114. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  115. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  118. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  119. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  120. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  121. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  144. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  145. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  146. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  147. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  148. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  149. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  156. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  157. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  158. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  159. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  163. package/plugins/draw/src/api/addFeature.js +22 -0
  164. package/plugins/draw/src/api/addFeature.test.js +41 -0
  165. package/plugins/draw/src/api/deleteFeature.js +11 -0
  166. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  167. package/plugins/draw/src/api/editFeature.js +54 -0
  168. package/plugins/draw/src/api/editFeature.test.js +134 -0
  169. package/plugins/draw/src/api/merge.js +30 -0
  170. package/plugins/draw/src/api/merge.test.js +57 -0
  171. package/plugins/draw/src/api/newLine.js +41 -0
  172. package/plugins/draw/src/api/newLine.test.js +93 -0
  173. package/plugins/draw/src/api/newPolygon.js +41 -0
  174. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  175. package/plugins/draw/src/api/split.js +117 -0
  176. package/plugins/draw/src/api/split.test.js +204 -0
  177. package/plugins/draw/src/defaults.js +52 -0
  178. package/plugins/draw/src/defaults.test.js +18 -0
  179. package/plugins/draw/src/draw.scss +43 -0
  180. package/plugins/draw/src/events.js +237 -0
  181. package/plugins/draw/src/events.test.js +369 -0
  182. package/plugins/draw/src/index.js +10 -0
  183. package/plugins/draw/src/index.test.js +24 -0
  184. package/plugins/draw/src/manifest.js +170 -0
  185. package/plugins/draw/src/manifest.test.js +147 -0
  186. package/plugins/draw/src/reducer.js +69 -0
  187. package/plugins/draw/src/reducer.test.js +85 -0
  188. package/plugins/draw/src/utils/debounce.js +16 -0
  189. package/plugins/draw/src/utils/debounce.test.js +40 -0
  190. package/plugins/draw/src/utils/eventBus.js +36 -0
  191. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  192. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  193. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  194. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  195. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  196. package/plugins/draw/src/utils/resolveColors.js +39 -0
  197. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  198. package/plugins/draw/src/utils/spatial.js +243 -0
  199. package/plugins/draw/src/utils/spatial.test.js +243 -0
  200. package/plugins/draw/src/utils/touchTarget.js +95 -0
  201. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  202. package/plugins/draw/src/utils/undoStack.js +31 -0
  203. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  204. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  205. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  206. package/plugins/draw/src/validation/liveStroke.js +85 -0
  207. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  208. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  209. package/plugins/draw/src/validation/rules.js +186 -0
  210. package/plugins/draw/src/validation/rules.test.js +120 -0
  211. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  212. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  213. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  214. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  215. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  216. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  217. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  218. package/plugins/search/dist/css/index.css +1 -317
  219. package/rollup.esm.mjs +35 -28
  220. package/sonar-project.properties +2 -2
  221. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  222. package/src/App/components/Hints/Hints.module.scss +5 -4
  223. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  224. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  225. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  226. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  227. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  228. package/src/App/hooks/useHintsAPI.js +67 -0
  229. package/src/App/hooks/useHintsAPI.test.js +145 -0
  230. package/src/App/renderer/PluginInits.jsx +4 -0
  231. package/src/App/renderer/PluginInits.test.jsx +7 -1
  232. package/src/InteractiveMap/InteractiveMap.js +20 -0
  233. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  234. package/src/config/events.js +4 -0
  235. package/src/scss/settings/_dimensions.scss +3 -0
  236. package/src/services/announcer.js +38 -3
  237. package/src/services/announcer.test.js +133 -1
  238. package/src/services/hints.js +3 -0
  239. package/src/services/hints.test.js +19 -0
  240. package/src/types.js +12 -0
  241. package/src/utils/detectInterfaceType.js +5 -1
  242. package/src/utils/isMac.js +15 -0
  243. package/src/utils/isMac.test.js +37 -0
  244. package/webpack.umd.mjs +1 -0
@@ -0,0 +1,140 @@
1
+ import { createDrawInput } from './drawInput.js'
2
+ import { createVertexPlacement } from './vertexPlacement.js'
3
+ import { createFakeMap, createContainer } from '../__helpers__/harness.js'
4
+
5
+ jest.mock('./vertexPlacement.js', () => ({
6
+ createVertexPlacement: jest.fn(() => ({
7
+ placeVertex: jest.fn(),
8
+ updateRubberbanding: jest.fn(),
9
+ clearLastCoord: jest.fn()
10
+ }))
11
+ }))
12
+
13
+ const setup = (interfaceType = 'mouse') => {
14
+ const map = createFakeMap()
15
+ const container = createContainer()
16
+ const button = document.createElement('button')
17
+ button.id = 'add-pt'
18
+ document.body.appendChild(button)
19
+ const onUndo = jest.fn()
20
+ const input = createDrawInput({
21
+ drawInteraction: { getMap: () => map },
22
+ options: { container, addVertexButtonId: 'add-pt', interfaceType, mapProvider: {}, snap: null, onUndo }
23
+ })
24
+ const placement = createVertexPlacement.mock.results.at(-1).value
25
+ liveInputs.push(input)
26
+ return { map, view: map.getView(), container, button, input, placement, onUndo }
27
+ }
28
+
29
+ const liveInputs = []
30
+ afterEach(() => {
31
+ liveInputs.splice(0).forEach((i) => i.destroy())
32
+ document.body.innerHTML = ''
33
+ jest.clearAllMocks()
34
+ })
35
+
36
+ const key = (props) => window.dispatchEvent(new KeyboardEvent('keydown', { cancelable: true, ...props }))
37
+
38
+ test('Enter places a vertex and switches to keyboard — only while focus is inside the viewport', () => {
39
+ const { container, input, placement } = setup()
40
+ key({ key: 'Enter' })
41
+ expect(placement.placeVertex).not.toHaveBeenCalled()
42
+
43
+ container.focus()
44
+ key({ key: 'Enter' })
45
+ expect(placement.placeVertex).toHaveBeenCalled()
46
+ expect(input.getInterfaceType()).toBe('keyboard')
47
+ })
48
+
49
+ test('arrow keys switch to the keyboard interface without placing', () => {
50
+ const { container, input, placement } = setup()
51
+ container.focus()
52
+ key({ key: 'ArrowUp' })
53
+ expect(input.getInterfaceType()).toBe('keyboard')
54
+ expect(placement.placeVertex).not.toHaveBeenCalled()
55
+ })
56
+
57
+ test('ctrl/cmd+z triggers undo', () => {
58
+ const { container, onUndo } = setup()
59
+ container.focus()
60
+ key({ key: 'z', ctrlKey: true })
61
+ expect(onUndo).toHaveBeenCalled()
62
+ })
63
+
64
+ test('the add-vertex button places a vertex; other clicks do not', () => {
65
+ const { button, placement } = setup()
66
+ button.click()
67
+ expect(placement.placeVertex).toHaveBeenCalledTimes(1)
68
+ document.body.click()
69
+ expect(placement.placeVertex).toHaveBeenCalledTimes(1)
70
+ })
71
+
72
+ test('pointer and touch input switch the interface type', () => {
73
+ const { container, input, placement } = setup('keyboard')
74
+ container.dispatchEvent(Object.assign(new Event('pointerdown'), { pointerType: 'mouse' }))
75
+ expect(input.getInterfaceType()).toBe('mouse')
76
+ expect(placement.clearLastCoord).toHaveBeenCalled()
77
+
78
+ container.dispatchEvent(new Event('touchstart'))
79
+ expect(input.getInterfaceType()).toBe('touch')
80
+ })
81
+
82
+ test('a touch pointerdown is left for the touchstart handler to classify', () => {
83
+ const { container, input, placement } = setup('keyboard')
84
+ container.dispatchEvent(Object.assign(new Event('pointerdown'), { pointerType: 'touch' }))
85
+ expect(input.getInterfaceType()).toBe('keyboard') // onPointerdown ignored the touch pointer
86
+ expect(placement.clearLastCoord).not.toHaveBeenCalled()
87
+ })
88
+
89
+ test('the interface type defaults to mouse when none is supplied', () => {
90
+ const map = createFakeMap()
91
+ const container = createContainer()
92
+ const input = createDrawInput({
93
+ drawInteraction: { getMap: () => map },
94
+ options: { container, addVertexButtonId: 'add-pt', mapProvider: {}, snap: null, onUndo: jest.fn() }
95
+ })
96
+ liveInputs.push(input)
97
+ expect(input.getInterfaceType()).toBe('mouse')
98
+ })
99
+
100
+ test('pointer moves and map pans update the rubber band except for the mouse interface', () => {
101
+ const { container, view, placement } = setup('touch')
102
+ container.dispatchEvent(new Event('pointermove'))
103
+ view.emit('change:center')
104
+ expect(placement.updateRubberbanding).toHaveBeenCalledTimes(2)
105
+
106
+ const mouse = setup('mouse')
107
+ mouse.container.dispatchEvent(new Event('pointermove'))
108
+ mouse.view.emit('change:center')
109
+ expect(mouse.placement.updateRubberbanding).not.toHaveBeenCalled()
110
+ })
111
+
112
+ test('setInterfaceType updates the interface and refreshes the rubber band immediately for non-mouse types, e.g. switching to touch and panning via MoveControl mid-session', () => {
113
+ const { input, placement } = setup('mouse')
114
+ input.setInterfaceType('touch')
115
+ expect(input.getInterfaceType()).toBe('touch')
116
+ expect(placement.updateRubberbanding).toHaveBeenCalledTimes(1)
117
+
118
+ input.setInterfaceType('mouse')
119
+ expect(input.getInterfaceType()).toBe('mouse')
120
+ expect(placement.updateRubberbanding).toHaveBeenCalledTimes(1) // not called again for mouse
121
+ })
122
+
123
+ test('keyboard pan animations keep the rubber band anchored via postrender', () => {
124
+ const { map, view, placement } = setup('keyboard')
125
+ view.getAnimating.mockReturnValue(true)
126
+ map.emit('postrender')
127
+ expect(placement.updateRubberbanding).toHaveBeenCalledTimes(1)
128
+ view.getAnimating.mockReturnValue(false)
129
+ map.emit('postrender')
130
+ expect(placement.updateRubberbanding).toHaveBeenCalledTimes(1)
131
+ })
132
+
133
+ test('destroy removes all listeners', () => {
134
+ const { container, button, input, placement } = setup()
135
+ input.destroy()
136
+ container.focus()
137
+ key({ key: 'Enter' })
138
+ button.click()
139
+ expect(placement.placeVertex).not.toHaveBeenCalled()
140
+ })
@@ -0,0 +1,144 @@
1
+ import { coordToPixel, pixelDist } from '../utils/olCoords.js'
2
+ import { getLastPlacedSketchCoord } from '../utils/sketchHelpers.js'
3
+
4
+ const SNAP_TOLERANCE = 12 // pixels
5
+ // Minimum ring length to allow snap-to-close (placed vertices + rubber-band)
6
+ const MIN_SKETCH_COORDS = { Polygon: 4, LineString: 3 }
7
+ const DUPLICATE_TOLERANCE_PX = 2
8
+
9
+ export const isCloseToFirstVertex = (map, coord, sketchCoords, geometryType) => {
10
+ if (geometryType !== 'Polygon' || sketchCoords.length < MIN_SKETCH_COORDS.Polygon) {
11
+ return false
12
+ }
13
+ const currentPixel = coordToPixel(map, coord)
14
+ const firstPixel = coordToPixel(map, sketchCoords[0])
15
+ if (!currentPixel || !firstPixel) {
16
+ return false
17
+ }
18
+ return pixelDist(currentPixel, firstPixel) < SNAP_TOLERANCE
19
+ }
20
+
21
+ export const applyRubberbanding = (geom, centerCoord) => {
22
+ if (geom.getType() === 'LineString') {
23
+ const updated = [...geom.getCoordinates()]
24
+ updated[updated.length - 1] = centerCoord
25
+ geom.setCoordinates(updated)
26
+ } else if (geom.getType() === 'Polygon') {
27
+ const updated = geom.getCoordinates().map((ring, i) => {
28
+ if (i !== 0) {
29
+ return ring
30
+ }
31
+ const r = [...ring]
32
+ r[r.length - 1] = centerCoord
33
+ return r
34
+ })
35
+ geom.setCoordinates(updated)
36
+ } else {
37
+ // No action
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Close/finish handling for a placed coordinate. Pure with respect to the
43
+ * lastPlacedCoord bookkeeping: returns the updated value alongside whether the
44
+ * coordinate was consumed as a close/finish attempt (caller must not append).
45
+ *
46
+ * @returns {{ handled: boolean, lastPlacedCoord: number[] | null }}
47
+ */
48
+ const tryClose = ({ drawInteraction, canFinish, geom, sketchCoords, coord, lastPlacedCoord }) => {
49
+ if (lastPlacedCoord && lastPlacedCoord[0] === coord[0] && lastPlacedCoord[1] === coord[1]) {
50
+ // Same position as last placed: don't duplicate. Close only if enough real vertices exist.
51
+ if (canFinish?.()) { drawInteraction.finishDrawing() }
52
+ return { handled: true, lastPlacedCoord: null }
53
+ }
54
+ const map = drawInteraction.getMap()
55
+ if (isCloseToFirstVertex(map, coord, sketchCoords, geom.getType())) {
56
+ drawInteraction.finishDrawing()
57
+ return { handled: true, lastPlacedCoord }
58
+ }
59
+ // When the add-vertex button overlays the map (touch UI), OL's native pointer handler
60
+ // and this button click handler both fire for the same tap. Detect that OL already
61
+ // committed a vertex at coord's position and skip the duplicate appendCoordinates,
62
+ // but register coord as lastPlacedCoord so a second tap at the same position can close.
63
+ const lastCommitted = getLastPlacedSketchCoord(geom)
64
+ if (lastCommitted) {
65
+ const p1 = map.getPixelFromCoordinate(lastCommitted)
66
+ const p2 = map.getPixelFromCoordinate(coord)
67
+ if (p1 && p2) {
68
+ const dx = p1[0] - p2[0]; const dy = p1[1] - p2[1]
69
+ if (dx * dx + dy * dy < DUPLICATE_TOLERANCE_PX * DUPLICATE_TOLERANCE_PX) {
70
+ return { handled: true, lastPlacedCoord: coord }
71
+ }
72
+ }
73
+ }
74
+ return { handled: false, lastPlacedCoord }
75
+ }
76
+
77
+ /**
78
+ * Crosshair-driven vertex placement for touch/keyboard drawing.
79
+ *
80
+ * Owns the sketch-feature and last-placed-coordinate bookkeeping via the Draw
81
+ * interaction's lifecycle events, and implements placing a vertex at the map
82
+ * center: rubber-band updates, duplicate suppression when OL already committed
83
+ * the same tap, close-on-repeat and close-near-first-vertex behaviour.
84
+ *
85
+ * @returns {{ placeVertex, updateRubberbanding, clearLastCoord }}
86
+ */
87
+ export const createVertexPlacement = ({ drawInteraction, mapProvider, snap, canFinish, canPlace, getInterfaceType }) => {
88
+ let sketchFeature = null
89
+ let lastPlacedCoord = null
90
+
91
+ const resetSketch = (feature = null) => {
92
+ sketchFeature = feature
93
+ lastPlacedCoord = null
94
+ }
95
+ drawInteraction.on('drawstart', (e) => resetSketch(e.feature))
96
+ drawInteraction.on('drawend', () => resetSketch())
97
+ drawInteraction.on('drawabort', () => resetSketch())
98
+
99
+ const snappedCenter = () => {
100
+ const raw = mapProvider.getCenter()
101
+ return (getInterfaceType() !== 'mouse' && snap) ? snap.apply(raw) : raw
102
+ }
103
+
104
+ const updateRubberbanding = () => {
105
+ if (!sketchFeature) {
106
+ // No sketch yet — update snap indicator at crosshair position so targets are
107
+ // visible before the first vertex is placed (touch/keyboard only; mouse uses
108
+ // the OL snap interaction's pointermove handler instead).
109
+ if (getInterfaceType() !== 'mouse' && snap) {
110
+ snap.apply(mapProvider.getCenter())
111
+ }
112
+ return
113
+ }
114
+ const geom = sketchFeature.getGeometry()
115
+ if (!geom.getCoordinates().length) {
116
+ return
117
+ }
118
+ applyRubberbanding(geom, snappedCenter())
119
+ }
120
+
121
+ const placeVertex = () => {
122
+ const coord = snappedCenter()
123
+ snap?.hideIndicator()
124
+ if (sketchFeature) {
125
+ const geom = sketchFeature.getGeometry()
126
+ const rawCoords = geom.getCoordinates()
127
+ const sketchCoords = geom.getType() === 'Polygon' ? (rawCoords[0] || []) : rawCoords
128
+ const result = tryClose({ drawInteraction, canFinish, geom, sketchCoords, coord, lastPlacedCoord })
129
+ lastPlacedCoord = result.lastPlacedCoord
130
+ if (result.handled) { return }
131
+ }
132
+ // appendCoordinates bypasses the Draw interaction's condition, so the placement
133
+ // gate must run here explicitly — touch/keyboard parity with the mouse path.
134
+ if (canPlace && !canPlace(coord)) { return }
135
+ drawInteraction.appendCoordinates([coord])
136
+ lastPlacedCoord = coord
137
+ }
138
+
139
+ return {
140
+ placeVertex,
141
+ updateRubberbanding,
142
+ clearLastCoord () { lastPlacedCoord = null }
143
+ }
144
+ }
@@ -0,0 +1,196 @@
1
+ import Point from 'ol/geom/Point.js'
2
+ import Polygon from 'ol/geom/Polygon.js'
3
+ import Feature from 'ol/Feature.js'
4
+ import { createVertexPlacement, applyRubberbanding, isCloseToFirstVertex } from './vertexPlacement.js'
5
+ import { createFakeMap, createEmitter, polygonFeature, lineFeature } from '../__helpers__/harness.js'
6
+
7
+ const CENTER = [5, 5]
8
+
9
+ const setup = ({ interfaceType = 'touch', snap = null, canFinish = () => true, canPlace } = {}) => {
10
+ const map = createFakeMap()
11
+ const bus = createEmitter()
12
+ const drawInteraction = {
13
+ on: bus.on,
14
+ emit: bus.emit,
15
+ getMap: () => map,
16
+ appendCoordinates: jest.fn(),
17
+ finishDrawing: jest.fn()
18
+ }
19
+ const placement = createVertexPlacement({
20
+ drawInteraction,
21
+ mapProvider: { getCenter: () => CENTER },
22
+ snap,
23
+ canFinish,
24
+ canPlace,
25
+ getInterfaceType: () => interfaceType
26
+ })
27
+ const startSketch = (feature) => { bus.emit('drawstart', { feature }); return feature }
28
+ return { drawInteraction, placement, startSketch, bus }
29
+ }
30
+
31
+ describe('placing vertices', () => {
32
+ test('appends the crosshair position, snapped when a snap manager is active', () => {
33
+ const { placement, drawInteraction } = setup()
34
+ placement.placeVertex()
35
+ expect(drawInteraction.appendCoordinates).toHaveBeenCalledWith([CENTER])
36
+
37
+ const snap = { apply: jest.fn(() => [9, 9]), hideIndicator: jest.fn() }
38
+ const snapped = setup({ snap })
39
+ snapped.placement.placeVertex()
40
+ expect(snapped.drawInteraction.appendCoordinates).toHaveBeenCalledWith([[9, 9]])
41
+ expect(snap.hideIndicator).toHaveBeenCalled()
42
+ })
43
+
44
+ test('a canPlace veto rejects the placement — the vertex is never appended', () => {
45
+ const canPlace = jest.fn(() => false)
46
+ const { placement, drawInteraction } = setup({ canPlace })
47
+ placement.placeVertex()
48
+ expect(canPlace).toHaveBeenCalledWith(CENTER)
49
+ expect(drawInteraction.appendCoordinates).not.toHaveBeenCalled()
50
+ })
51
+
52
+ test('a canPlace veto does not block a finish tap (close runs before the gate)', () => {
53
+ let allow = true
54
+ const { placement, drawInteraction, startSketch } = setup({ canPlace: () => allow })
55
+ startSketch(lineFeature([[0, 0], [50, 50]]))
56
+ placement.placeVertex() // placed at the crosshair
57
+ allow = false
58
+ placement.placeVertex() // same spot again → a finish attempt, not a placement
59
+ expect(drawInteraction.finishDrawing).toHaveBeenCalledTimes(1)
60
+ })
61
+
62
+ test('the mouse interface ignores snapping (the OL snap interaction covers it)', () => {
63
+ const snap = { apply: jest.fn(() => [9, 9]), hideIndicator: jest.fn() }
64
+ const { placement, drawInteraction } = setup({ interfaceType: 'mouse', snap })
65
+ placement.placeVertex()
66
+ expect(drawInteraction.appendCoordinates).toHaveBeenCalledWith([CENTER])
67
+ })
68
+
69
+ test('a second tap at the same spot finishes the shape instead of duplicating — when finishable', () => {
70
+ const { placement, drawInteraction, startSketch } = setup()
71
+ startSketch(lineFeature([[0, 0], [50, 50]]))
72
+ placement.placeVertex()
73
+ placement.placeVertex()
74
+ expect(drawInteraction.appendCoordinates).toHaveBeenCalledTimes(1)
75
+ expect(drawInteraction.finishDrawing).toHaveBeenCalledTimes(1)
76
+
77
+ const unfinishable = setup({ canFinish: () => false })
78
+ unfinishable.startSketch(lineFeature([[0, 0], [50, 50]]))
79
+ unfinishable.placement.placeVertex()
80
+ unfinishable.placement.placeVertex()
81
+ unfinishable.placement.placeVertex() // duplicate reset the bookkeeping — placed again
82
+ expect(unfinishable.drawInteraction.finishDrawing).not.toHaveBeenCalled()
83
+ expect(unfinishable.drawInteraction.appendCoordinates).toHaveBeenCalledTimes(2)
84
+ })
85
+
86
+ test('placing near the first polygon vertex closes the ring', () => {
87
+ const { placement, drawInteraction, startSketch } = setup()
88
+ startSketch(polygonFeature([[0, 0], [30, 0], [30, 30], [8, 8], [0, 0]])) // center [5,5] within 12px of [0,0]
89
+ placement.placeVertex()
90
+ expect(drawInteraction.finishDrawing).toHaveBeenCalled()
91
+ expect(drawInteraction.appendCoordinates).not.toHaveBeenCalled()
92
+ })
93
+
94
+ test('a vertex OL already committed at the same position is not appended twice', () => {
95
+ const { placement, drawInteraction, startSketch } = setup()
96
+ startSketch(lineFeature([[0, 0], [5, 6], [50, 50]])) // last committed [5,6] within 2px of center
97
+ placement.placeVertex()
98
+ expect(drawInteraction.appendCoordinates).not.toHaveBeenCalled()
99
+ placement.placeVertex() // now registered as last placed — same spot again closes
100
+ expect(drawInteraction.finishDrawing).toHaveBeenCalled()
101
+ })
102
+
103
+ test('the first vertex of a fresh sketch appends — nothing is committed yet', () => {
104
+ const { placement, drawInteraction, startSketch } = setup()
105
+ startSketch(lineFeature([[0, 0]])) // only the rubber-band coord, no committed vertex
106
+ placement.placeVertex()
107
+ expect(drawInteraction.appendCoordinates).toHaveBeenCalledWith([CENTER])
108
+ })
109
+
110
+ test('an unprojectable committed vertex skips the duplicate-tolerance check', () => {
111
+ const { placement, drawInteraction, startSketch } = setup()
112
+ startSketch(lineFeature([[0, 0], [5, 6], [50, 50]]))
113
+ drawInteraction.getMap().getPixelFromCoordinate = () => null // mid view transition
114
+ placement.placeVertex()
115
+ expect(drawInteraction.appendCoordinates).toHaveBeenCalledWith([CENTER])
116
+ })
117
+
118
+ test('placing into an empty polygon sketch (no ring yet) appends without error', () => {
119
+ const { placement, drawInteraction, startSketch } = setup()
120
+ startSketch(new Feature(new Polygon([]))) // ring not created yet
121
+ placement.placeVertex()
122
+ expect(drawInteraction.appendCoordinates).toHaveBeenCalledWith([CENTER])
123
+ })
124
+
125
+ test('clearLastCoord and sketch end/abort reset the duplicate bookkeeping', () => {
126
+ const { placement, drawInteraction, startSketch, bus } = setup()
127
+ startSketch(lineFeature([[0, 0], [50, 50]]))
128
+ placement.placeVertex()
129
+ placement.clearLastCoord()
130
+ placement.placeVertex()
131
+ expect(drawInteraction.appendCoordinates).toHaveBeenCalledTimes(2)
132
+
133
+ bus.emit('drawend')
134
+ placement.placeVertex() // no sketch — plain append
135
+ expect(drawInteraction.appendCoordinates).toHaveBeenCalledTimes(3)
136
+ bus.emit('drawabort')
137
+ })
138
+ })
139
+
140
+ describe('rubber-banding', () => {
141
+ test('moves the trailing coordinate of a line or polygon ring to the crosshair', () => {
142
+ const { placement, startSketch } = setup()
143
+ const line = startSketch(lineFeature([[0, 0], [50, 50]]))
144
+ placement.updateRubberbanding()
145
+ expect(line.getGeometry().getCoordinates()).toEqual([[0, 0], CENTER])
146
+
147
+ const poly = setup()
148
+ const hole = [[10, 10], [12, 10], [12, 12], [10, 10]]
149
+ const feature = poly.startSketch(new Feature(new Polygon([[[0, 0], [30, 0], [50, 50], [0, 0]], hole])))
150
+ poly.placement.updateRubberbanding()
151
+ expect(feature.getGeometry().getCoordinates()[0].at(-1)).toEqual(CENTER)
152
+ expect(feature.getGeometry().getCoordinates()[1]).toEqual(hole) // inner rings untouched
153
+ })
154
+
155
+ test('without a sketch it only refreshes the snap indicator at the crosshair (non-mouse)', () => {
156
+ const snap = { apply: jest.fn((c) => c), hideIndicator: jest.fn() }
157
+ const { placement } = setup({ snap })
158
+ placement.updateRubberbanding()
159
+ expect(snap.apply).toHaveBeenCalledWith(CENTER)
160
+
161
+ const mouse = setup({ interfaceType: 'mouse', snap })
162
+ snap.apply.mockClear()
163
+ mouse.placement.updateRubberbanding()
164
+ expect(snap.apply).not.toHaveBeenCalled()
165
+ })
166
+
167
+ test('an empty sketch geometry is left alone', () => {
168
+ const { placement, startSketch } = setup()
169
+ const line = startSketch(lineFeature([]))
170
+ placement.updateRubberbanding() // must not throw
171
+ expect(line.getGeometry().getCoordinates()).toEqual([])
172
+ })
173
+
174
+ test('applyRubberbanding ignores unsupported geometry types', () => {
175
+ const point = new Point([1, 1])
176
+ applyRubberbanding(point, CENTER)
177
+ expect(point.getCoordinates()).toEqual([1, 1])
178
+ })
179
+ })
180
+
181
+ describe('isCloseToFirstVertex', () => {
182
+ const map = createFakeMap()
183
+
184
+ test('only closes polygons with enough vertices, within the snap tolerance', () => {
185
+ const ring = [[0, 0], [30, 0], [30, 30], [8, 8], [0, 0]]
186
+ expect(isCloseToFirstVertex(map, [5, 5], ring, 'Polygon')).toBe(true)
187
+ expect(isCloseToFirstVertex(map, [20, 20], ring, 'Polygon')).toBe(false)
188
+ expect(isCloseToFirstVertex(map, [5, 5], ring, 'LineString')).toBe(false)
189
+ expect(isCloseToFirstVertex(map, [5, 5], [[0, 0], [30, 0]], 'Polygon')).toBe(false)
190
+ })
191
+
192
+ test('is false when a coordinate cannot be projected to a pixel', () => {
193
+ const nullMap = { getPixelFromCoordinate: () => null }
194
+ expect(isCloseToFirstVertex(nullMap, [5, 5], [[0, 0], [1, 1], [2, 2], [3, 3]], 'Polygon')).toBe(false)
195
+ })
196
+ })