@defra/interactive-map 0.0.37-alpha → 0.0.39-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 (247) 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/datasets/src/registry/dataset.js +3 -1
  21. package/plugins/datasets/src/registry/datasetRegistry.js +30 -24
  22. package/plugins/datasets/src/registry/datasetRegistry.test.js +25 -18
  23. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  24. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  25. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  26. package/plugins/draw/dist/esm/index.js +1 -0
  27. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  28. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  29. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  30. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  31. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  32. package/plugins/draw/dist/umd/index.js +2 -0
  33. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  34. package/plugins/draw/src/DrawInit.jsx +89 -0
  35. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  36. package/plugins/draw/src/adapterEvents.js +127 -0
  37. package/plugins/draw/src/adapterEvents.test.js +20 -0
  38. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  39. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  40. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  41. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  42. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  43. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  44. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  45. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  46. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  47. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  48. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  49. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  89. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  95. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  96. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  97. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  98. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  99. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  100. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  101. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  102. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  103. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  104. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  105. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  106. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  107. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  108. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  109. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  110. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  111. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  112. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  113. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  114. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  115. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  118. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  119. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  120. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  121. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  144. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  146. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  147. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  148. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  149. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  156. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  157. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  158. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  159. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  163. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  164. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  165. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  166. package/plugins/draw/src/api/addFeature.js +22 -0
  167. package/plugins/draw/src/api/addFeature.test.js +41 -0
  168. package/plugins/draw/src/api/deleteFeature.js +11 -0
  169. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  170. package/plugins/draw/src/api/editFeature.js +54 -0
  171. package/plugins/draw/src/api/editFeature.test.js +134 -0
  172. package/plugins/draw/src/api/merge.js +30 -0
  173. package/plugins/draw/src/api/merge.test.js +57 -0
  174. package/plugins/draw/src/api/newLine.js +41 -0
  175. package/plugins/draw/src/api/newLine.test.js +93 -0
  176. package/plugins/draw/src/api/newPolygon.js +41 -0
  177. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  178. package/plugins/draw/src/api/split.js +117 -0
  179. package/plugins/draw/src/api/split.test.js +204 -0
  180. package/plugins/draw/src/defaults.js +52 -0
  181. package/plugins/draw/src/defaults.test.js +18 -0
  182. package/plugins/draw/src/draw.scss +43 -0
  183. package/plugins/draw/src/events.js +237 -0
  184. package/plugins/draw/src/events.test.js +369 -0
  185. package/plugins/draw/src/index.js +10 -0
  186. package/plugins/draw/src/index.test.js +24 -0
  187. package/plugins/draw/src/manifest.js +170 -0
  188. package/plugins/draw/src/manifest.test.js +147 -0
  189. package/plugins/draw/src/reducer.js +69 -0
  190. package/plugins/draw/src/reducer.test.js +85 -0
  191. package/plugins/draw/src/utils/debounce.js +16 -0
  192. package/plugins/draw/src/utils/debounce.test.js +40 -0
  193. package/plugins/draw/src/utils/eventBus.js +36 -0
  194. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  195. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  196. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  197. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  198. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  199. package/plugins/draw/src/utils/resolveColors.js +39 -0
  200. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  201. package/plugins/draw/src/utils/spatial.js +243 -0
  202. package/plugins/draw/src/utils/spatial.test.js +243 -0
  203. package/plugins/draw/src/utils/touchTarget.js +95 -0
  204. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  205. package/plugins/draw/src/utils/undoStack.js +31 -0
  206. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  207. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  208. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  209. package/plugins/draw/src/validation/liveStroke.js +85 -0
  210. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  211. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  212. package/plugins/draw/src/validation/rules.js +186 -0
  213. package/plugins/draw/src/validation/rules.test.js +120 -0
  214. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  215. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  216. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  217. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  218. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  219. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  220. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  221. package/plugins/search/dist/css/index.css +1 -317
  222. package/rollup.esm.mjs +35 -28
  223. package/sonar-project.properties +2 -2
  224. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  225. package/src/App/components/Hints/Hints.module.scss +5 -4
  226. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  227. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  228. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  229. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  230. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  231. package/src/App/hooks/useHintsAPI.js +67 -0
  232. package/src/App/hooks/useHintsAPI.test.js +145 -0
  233. package/src/App/renderer/PluginInits.jsx +4 -0
  234. package/src/App/renderer/PluginInits.test.jsx +7 -1
  235. package/src/InteractiveMap/InteractiveMap.js +20 -0
  236. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  237. package/src/config/events.js +4 -0
  238. package/src/scss/settings/_dimensions.scss +3 -0
  239. package/src/services/announcer.js +38 -3
  240. package/src/services/announcer.test.js +133 -1
  241. package/src/services/hints.js +3 -0
  242. package/src/services/hints.test.js +19 -0
  243. package/src/types.js +12 -0
  244. package/src/utils/detectInterfaceType.js +5 -1
  245. package/src/utils/isMac.js +15 -0
  246. package/src/utils/isMac.test.js +37 -0
  247. package/webpack.umd.mjs +1 -0
@@ -0,0 +1,55 @@
1
+ import VectorSource from 'ol/source/Vector.js'
2
+ import VectorLayer from 'ol/layer/Vector.js'
3
+ import Feature from 'ol/Feature.js'
4
+ import Point from 'ol/geom/Point.js'
5
+
6
+ // Above the vertex (102) and midpoint (101) handle layers
7
+ const ACTIVE_LAYER_Z_INDEX = 103
8
+
9
+ /**
10
+ * Overlay layer rendering the currently selected vertex or midpoint handle.
11
+ * The vertex/midpoint layers hide their own circle at the selected index and
12
+ * this layer draws the highlighted version on top.
13
+ *
14
+ * @param {import('ol/Map').default} map
15
+ * @param {() => object} getStyles - returns the manager's current style set
16
+ * (read at render time so style hot-swaps apply without rewiring)
17
+ * @returns {{ update: (state) => void, remove: () => void }}
18
+ */
19
+ export const createActiveVertexLayer = (map, getStyles) => {
20
+ const source = new VectorSource()
21
+ const layer = new VectorLayer({ source, zIndex: ACTIVE_LAYER_Z_INDEX })
22
+ map.addLayer(layer)
23
+
24
+ const selectedCoordAndStyle = ({ selectedVertexIndex, selectedVertexType, vertices, midpoints }) => {
25
+ const styles = getStyles()
26
+ if (selectedVertexType === 'vertex') {
27
+ return { coord: vertices[selectedVertexIndex], style: styles.selectedVertexStyle }
28
+ }
29
+ if (selectedVertexType === 'midpoint') {
30
+ return { coord: midpoints[selectedVertexIndex - vertices.length], style: styles.selectedMidpointStyle }
31
+ }
32
+ return { coord: null, style: null }
33
+ }
34
+
35
+ return {
36
+ update (state) {
37
+ source.clear()
38
+ if (state.selectedVertexIndex < 0) {
39
+ return
40
+ }
41
+ const { coord, style } = selectedCoordAndStyle(state)
42
+ if (!coord) {
43
+ return
44
+ }
45
+ const feature = new Feature({ geometry: new Point(coord) })
46
+ feature.setStyle(style)
47
+ source.addFeature(feature)
48
+ },
49
+
50
+ remove () {
51
+ source.clear()
52
+ map.removeLayer(layer)
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,53 @@
1
+ import { createActiveVertexLayer } from './activeVertexLayer.js'
2
+ import { createFakeMap, createFakeManager } from '../__helpers__/harness.js'
3
+
4
+ const setup = () => {
5
+ const map = createFakeMap()
6
+ const manager = createFakeManager()
7
+ const activeLayer = createActiveVertexLayer(map, () => manager.styles)
8
+ const source = map.layers[0].getSource()
9
+ const stateWith = (overrides) => ({
10
+ selectedVertexIndex: -1,
11
+ selectedVertexType: null,
12
+ vertices: [[0, 0], [10, 0], [10, 10]],
13
+ midpoints: [[5, 0], [10, 5], [5, 5]],
14
+ ...overrides
15
+ })
16
+ return { map, manager, activeLayer, source, stateWith }
17
+ }
18
+
19
+ test('renders the selected vertex with the selected-vertex style', () => {
20
+ const { activeLayer, source, manager, stateWith } = setup()
21
+ activeLayer.update(stateWith({ selectedVertexIndex: 1, selectedVertexType: 'vertex' }))
22
+ const features = source.getFeatures()
23
+ expect(features).toHaveLength(1)
24
+ expect(features[0].getGeometry().getCoordinates()).toEqual([10, 0])
25
+ expect(features[0].getStyle()).toBe(manager.styles.selectedVertexStyle)
26
+ })
27
+
28
+ test('renders a selected midpoint by its local index with the midpoint style', () => {
29
+ const { activeLayer, source, manager, stateWith } = setup()
30
+ activeLayer.update(stateWith({ selectedVertexIndex: 4, selectedVertexType: 'midpoint' }))
31
+ const features = source.getFeatures()
32
+ expect(features[0].getGeometry().getCoordinates()).toEqual([10, 5])
33
+ expect(features[0].getStyle()).toBe(manager.styles.selectedMidpointStyle)
34
+ })
35
+
36
+ test('renders nothing without a selection, for an unknown type, or for a missing coordinate', () => {
37
+ const { activeLayer, source, stateWith } = setup()
38
+ activeLayer.update(stateWith({ selectedVertexIndex: 1, selectedVertexType: 'vertex' }))
39
+ activeLayer.update(stateWith({}))
40
+ expect(source.getFeatures()).toHaveLength(0)
41
+ activeLayer.update(stateWith({ selectedVertexIndex: 0, selectedVertexType: 'segment' }))
42
+ expect(source.getFeatures()).toHaveLength(0)
43
+ activeLayer.update(stateWith({ selectedVertexIndex: 99, selectedVertexType: 'vertex' }))
44
+ expect(source.getFeatures()).toHaveLength(0)
45
+ })
46
+
47
+ test('remove clears the source and detaches the layer', () => {
48
+ const { map, activeLayer, source, stateWith } = setup()
49
+ activeLayer.update(stateWith({ selectedVertexIndex: 0, selectedVertexType: 'vertex' }))
50
+ activeLayer.remove()
51
+ expect(source.getFeatures()).toHaveLength(0)
52
+ expect(map.removeLayer).toHaveBeenCalled()
53
+ })
@@ -0,0 +1,157 @@
1
+ import { coordToPixel } from '../utils/olCoords.js'
2
+ import { spatialNavigate } from '../../../utils/spatial.js'
3
+ import { wireNudge } from './nudge.js'
4
+
5
+ const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])
6
+ const INTERACTIVE_TAGS = new Set(['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'A'])
7
+
8
+ const selectNearest = (map, getState, setState) => {
9
+ const { vertices, midpoints } = getState()
10
+ if (!vertices.length) {
11
+ return
12
+ }
13
+ const centerPx = coordToPixel(map, map.getView().getCenter())
14
+ if (!centerPx) {
15
+ return
16
+ }
17
+ const allPixels = [
18
+ ...vertices.map(c => coordToPixel(map, c)),
19
+ ...midpoints.map(c => coordToPixel(map, c))
20
+ ].filter(Boolean).map(p => [p.x, p.y])
21
+ const idx = spatialNavigate([centerPx.x, centerPx.y], allPixels, undefined)
22
+ setState({ selectedVertexIndex: idx, selectedVertexType: idx < vertices.length ? 'vertex' : 'midpoint' })
23
+ }
24
+
25
+ const navigateTo = (direction, map, getState, setState) => {
26
+ const { selectedVertexIndex, vertices, midpoints } = getState()
27
+ if (!vertices.length) {
28
+ return
29
+ }
30
+ const allCoords = [...vertices, ...midpoints]
31
+ const allPixels = allCoords.map(c => coordToPixel(map, c)).filter(Boolean).map(p => [p.x, p.y])
32
+ const startPx = selectedVertexIndex >= 0
33
+ ? allPixels[selectedVertexIndex]
34
+ : (() => {
35
+ const c = coordToPixel(map, map.getView().getCenter())
36
+ return c ? [c.x, c.y] : null
37
+ })()
38
+ if (!startPx) {
39
+ return
40
+ }
41
+ const idx = spatialNavigate(startPx, allPixels, direction)
42
+ setState({ selectedVertexIndex: idx, selectedVertexType: idx < vertices.length ? 'vertex' : 'midpoint' })
43
+ }
44
+
45
+ const isInteractiveElementFocused = (appViewport) => {
46
+ const el = document.activeElement
47
+ if (!el || el === document.body) {
48
+ return false
49
+ }
50
+ if (appViewport.contains(el)) {
51
+ return false
52
+ }
53
+ return INTERACTIVE_TAGS.has(el.tagName) || el.isContentEditable || el.hasAttribute('tabindex')
54
+ }
55
+
56
+ const buildKeydownHandler = ({ map, getState, setState, nudge, keyMove, onUndo, onKeyboardActive, isFocused }) => {
57
+ const handleArrowKey = (e) => {
58
+ if (e.altKey) {
59
+ e.preventDefault()
60
+ e.stopPropagation()
61
+ navigateTo(e.key, map, getState, setState)
62
+ } else if (getState().selectedVertexIndex >= 0) {
63
+ e.preventDefault()
64
+ e.stopPropagation()
65
+ nudge(e)
66
+ } else {
67
+ // No action: arrow with no selection and no alt modifier
68
+ }
69
+ }
70
+
71
+ const handleKey = (e) => {
72
+ onKeyboardActive?.()
73
+ if (e.key === ' ') {
74
+ e.preventDefault()
75
+ if (getState().selectedVertexIndex < 0) {
76
+ selectNearest(map, getState, setState)
77
+ }
78
+ } else if (ARROW_KEYS.has(e.key)) {
79
+ handleArrowKey(e)
80
+ } else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
81
+ const tag = document.activeElement?.tagName
82
+ if (!INTERACTIVE_TAGS.has(tag)) {
83
+ e.preventDefault()
84
+ e.stopPropagation()
85
+ onUndo()
86
+ }
87
+ } else {
88
+ // No action
89
+ }
90
+ }
91
+
92
+ return (e) => {
93
+ if (isFocused()) {
94
+ return
95
+ }
96
+ if (e.key === 'Escape' && getState().selectedVertexIndex >= 0) {
97
+ e.preventDefault()
98
+ keyMove.start = null
99
+ keyMove.index = null
100
+ setState({ selectedVertexIndex: -1, selectedVertexType: null })
101
+ } else {
102
+ handleKey(e)
103
+ }
104
+ }
105
+ }
106
+
107
+ const buildKeyupHandler = ({ snap, keyMove, onVertexMoved, onDeleted, isFocused }) => (e) => {
108
+ if (isFocused()) {
109
+ return
110
+ }
111
+ if (ARROW_KEYS.has(e.key) && keyMove.start && keyMove.index != null) {
112
+ snap?.hideIndicator()
113
+ onVertexMoved({ vertexIndex: keyMove.index, previousCoord: keyMove.start })
114
+ keyMove.start = null
115
+ keyMove.index = null
116
+ }
117
+ if (e.key === 'Delete') {
118
+ onDeleted()
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Keyboard handler for edit mode.
124
+ *
125
+ * Space — select nearest vertex or midpoint to crosshair (only when nothing selected)
126
+ * Alt+Arrow — navigate to next vertex/midpoint in that direction (requires selection)
127
+ * Arrow — move selected vertex; if midpoint selected, inserts it as a vertex and moves it
128
+ * Shift+Arrow — same but fine nudge (1px vs 5px)
129
+ * Delete — delete selected vertex (no-op on midpoints)
130
+ * Ctrl/Cmd+Z — undo
131
+ *
132
+ * Midpoints remain midpoints until moved — navigating to a midpoint (Space/Alt+Arrow) does not
133
+ * convert it. Only pressing a plain/Shift arrow converts it.
134
+ *
135
+ * @param {{ map, getState, setState, snap, onVertexMoved, onInserted, onDeleted, onUndo, onKeyboardActive }} options
136
+ * @returns {{ nudgeByDelta: (dx: number, dy: number, isLargeStep: boolean) => void, destroy: () => void }}
137
+ */
138
+ export const createKeyboardHandler = (options) => {
139
+ const { map, snap, getState, setState, onVertexMoved, onInserted, onDeleted, onUndo, onKeyboardActive } = options
140
+ const { nudge, keyMove, nudgeByDelta } = wireNudge({ map, snap, getState, setState, onInserted, onVertexMoved })
141
+ const appViewport = map.getViewport().closest('[role="application"]') ?? map.getViewport()
142
+ const isFocused = () => isInteractiveElementFocused(appViewport)
143
+
144
+ const onKeydown = buildKeydownHandler({ map, getState, setState, nudge, keyMove, onUndo, onKeyboardActive, isFocused })
145
+ const onKeyup = buildKeyupHandler({ snap, keyMove, onVertexMoved, onDeleted, isFocused })
146
+
147
+ globalThis.addEventListener('keydown', onKeydown, { capture: true })
148
+ globalThis.addEventListener('keyup', onKeyup, { capture: true })
149
+
150
+ return {
151
+ nudgeByDelta,
152
+ destroy () {
153
+ globalThis.removeEventListener('keydown', onKeydown, { capture: true })
154
+ globalThis.removeEventListener('keyup', onKeyup, { capture: true })
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,195 @@
1
+ import { createKeyboardHandler } from './keyboardHandler.js'
2
+ import { createFakeMap, polygonFeature } from '../__helpers__/harness.js'
3
+
4
+ const RING = [[0, 0], [100, 0], [100, 100], [0, 0]]
5
+
6
+ const setup = () => {
7
+ const map = createFakeMap({ center: [98, 98] }) // crosshair near vertex [100, 100]
8
+ const state = {
9
+ olFeature: polygonFeature(RING),
10
+ selectedVertexIndex: -1,
11
+ selectedVertexType: null,
12
+ vertices: [[0, 0], [100, 0], [100, 100]],
13
+ midpoints: [[50, 0], [100, 50], [50, 50]]
14
+ }
15
+ const setState = jest.fn((updates) => Object.assign(state, updates))
16
+ const callbacks = {
17
+ onVertexMoved: jest.fn(),
18
+ onInserted: jest.fn(),
19
+ onDeleted: jest.fn(),
20
+ onUndo: jest.fn(),
21
+ onKeyboardActive: jest.fn()
22
+ }
23
+ const handler = createKeyboardHandler({ map, getState: () => state, setState, snap: null, ...callbacks })
24
+ liveHandlers.push(handler)
25
+ return { map, state, setState, handler, ...callbacks }
26
+ }
27
+
28
+ const liveHandlers = []
29
+ afterEach(() => {
30
+ liveHandlers.splice(0).forEach((h) => h.destroy())
31
+ document.body.innerHTML = ''
32
+ })
33
+
34
+ const key = (type, props) => window.dispatchEvent(new KeyboardEvent(type, { cancelable: true, ...props }))
35
+
36
+ test('nudgeByDelta (exposed for MoveControl) moves the selected vertex and reports it via onVertexMoved', () => {
37
+ const { state, handler, onVertexMoved } = setup()
38
+ Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
39
+ handler.nudgeByDelta(1, 0, true)
40
+ expect(state.vertices[1]).not.toEqual([100, 0])
41
+ expect(onVertexMoved).toHaveBeenCalledWith(expect.objectContaining({ vertexIndex: 1 }))
42
+ })
43
+
44
+ test('Space selects the vertex or midpoint nearest the crosshair, only when nothing is selected', () => {
45
+ const { state, setState, onKeyboardActive } = setup()
46
+ key('keydown', { key: ' ' })
47
+ expect(state.selectedVertexIndex).toBe(2) // [100, 100] is nearest [98, 98]
48
+ expect(state.selectedVertexType).toBe('vertex')
49
+ expect(onKeyboardActive).toHaveBeenCalled()
50
+ setState.mockClear()
51
+ key('keydown', { key: ' ' })
52
+ expect(setState).not.toHaveBeenCalled()
53
+ })
54
+
55
+ test('Space selects a midpoint when it is the handle nearest the crosshair', () => {
56
+ const map = createFakeMap({ center: [100, 48] }) // crosshair beside midpoint [100, 50]
57
+ const state = {
58
+ olFeature: polygonFeature(RING),
59
+ selectedVertexIndex: -1,
60
+ selectedVertexType: null,
61
+ vertices: [[0, 0], [100, 0], [100, 100]],
62
+ midpoints: [[50, 0], [100, 50], [50, 50]]
63
+ }
64
+ const setState = jest.fn((updates) => Object.assign(state, updates))
65
+ const handler = createKeyboardHandler({
66
+ map,
67
+ getState: () => state,
68
+ setState,
69
+ snap: null,
70
+ onVertexMoved: jest.fn(),
71
+ onInserted: jest.fn(),
72
+ onDeleted: jest.fn(),
73
+ onUndo: jest.fn(),
74
+ onKeyboardActive: jest.fn()
75
+ })
76
+ liveHandlers.push(handler)
77
+ key('keydown', { key: ' ' })
78
+ expect(state.selectedVertexIndex).toBe(4) // midpoint [100, 50]
79
+ expect(state.selectedVertexType).toBe('midpoint')
80
+ })
81
+
82
+ test('Alt+Arrow navigates the selection to the nearest handle in that direction', () => {
83
+ const { state } = setup()
84
+ Object.assign(state, { selectedVertexIndex: 2, selectedVertexType: 'vertex' })
85
+ key('keydown', { key: 'ArrowUp', altKey: true })
86
+ expect(state.selectedVertexIndex).toBe(4) // midpoint [100, 50] is nearest above [100, 100]
87
+ expect(state.selectedVertexType).toBe('midpoint')
88
+ })
89
+
90
+ test('Alt+Arrow with nothing selected navigates relative to the crosshair', () => {
91
+ const { state } = setup()
92
+ key('keydown', { key: 'ArrowUp', altKey: true }) // no selection → start from crosshair [98, 98]
93
+ expect(state.selectedVertexIndex).toBe(4) // midpoint [100, 50] above the crosshair
94
+ expect(state.selectedVertexType).toBe('midpoint')
95
+ })
96
+
97
+ test('Alt+Arrow can land the selection on a vertex', () => {
98
+ const { state } = setup()
99
+ Object.assign(state, { selectedVertexIndex: 4, selectedVertexType: 'midpoint' }) // midpoint [100, 50]
100
+ key('keydown', { key: 'ArrowUp', altKey: true })
101
+ expect(state.selectedVertexIndex).toBe(1) // vertex [100, 0] directly above
102
+ expect(state.selectedVertexType).toBe('vertex')
103
+ })
104
+
105
+ test('a plain arrow nudges the selected vertex, and keyup commits a single move op', () => {
106
+ const { state, onVertexMoved } = setup()
107
+ Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
108
+ key('keydown', { key: 'ArrowRight' })
109
+ key('keydown', { key: 'ArrowRight' })
110
+ expect(state.vertices[1]).toEqual([110, 0])
111
+
112
+ key('keyup', { key: 'ArrowRight' })
113
+ expect(onVertexMoved).toHaveBeenCalledWith({ vertexIndex: 1, previousCoord: [100, 0] })
114
+ key('keyup', { key: 'ArrowRight' }) // no pending move — not committed twice
115
+ expect(onVertexMoved).toHaveBeenCalledTimes(1)
116
+ })
117
+
118
+ test('arrows without a selection do nothing', () => {
119
+ const { setState } = setup()
120
+ key('keydown', { key: 'ArrowRight' })
121
+ expect(setState).not.toHaveBeenCalled()
122
+ })
123
+
124
+ test('Escape clears the selection and any pending nudge', () => {
125
+ const { state, setState, onVertexMoved } = setup()
126
+ Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
127
+ key('keydown', { key: 'ArrowRight' })
128
+ key('keydown', { key: 'Escape' })
129
+ expect(setState).toHaveBeenLastCalledWith({ selectedVertexIndex: -1, selectedVertexType: null })
130
+ key('keyup', { key: 'ArrowRight' })
131
+ expect(onVertexMoved).not.toHaveBeenCalled()
132
+ })
133
+
134
+ test('Delete deletes, ctrl/cmd+z undoes — but not while typing in an input inside the viewport', () => {
135
+ const { map, onDeleted, onUndo } = setup()
136
+ key('keyup', { key: 'Delete' })
137
+ expect(onDeleted).toHaveBeenCalled()
138
+ key('keydown', { key: 'z', ctrlKey: true })
139
+ expect(onUndo).toHaveBeenCalledTimes(1)
140
+
141
+ const input = document.createElement('input')
142
+ map.getViewport().appendChild(input)
143
+ input.focus()
144
+ key('keydown', { key: 'z', ctrlKey: true })
145
+ expect(onUndo).toHaveBeenCalledTimes(1)
146
+ })
147
+
148
+ test('keys are ignored while an interactive element outside the viewport has focus', () => {
149
+ const { onDeleted, onKeyboardActive } = setup()
150
+ const button = document.createElement('button')
151
+ document.body.appendChild(button)
152
+ button.focus()
153
+ key('keyup', { key: 'Delete' })
154
+ key('keydown', { key: ' ' })
155
+ expect(onDeleted).not.toHaveBeenCalled()
156
+ expect(onKeyboardActive).not.toHaveBeenCalled()
157
+ })
158
+
159
+ test('keys are ignored while a tabindex-focusable non-interactive element outside the viewport has focus', () => {
160
+ const { onDeleted } = setup()
161
+ const div = document.createElement('div')
162
+ div.tabIndex = 0 // focusable via tabindex, not one of the interactive tags
163
+ document.body.appendChild(div)
164
+ div.focus()
165
+ key('keyup', { key: 'Delete' })
166
+ expect(onDeleted).not.toHaveBeenCalled()
167
+ })
168
+
169
+ test('unhandled keys do nothing', () => {
170
+ const { setState } = setup()
171
+ key('keydown', { key: 'a' })
172
+ expect(setState).not.toHaveBeenCalled()
173
+ })
174
+
175
+ test('destroy removes the window listeners', () => {
176
+ const { handler, onDeleted } = setup()
177
+ handler.destroy()
178
+ key('keyup', { key: 'Delete' })
179
+ expect(onDeleted).not.toHaveBeenCalled()
180
+ })
181
+
182
+ test('selection and navigation are no-ops without handles or without projectable pixels', () => {
183
+ const { state, setState } = setup()
184
+ state.vertices = []
185
+ key('keydown', { key: ' ' })
186
+ key('keydown', { key: 'ArrowUp', altKey: true })
187
+ expect(setState).not.toHaveBeenCalled()
188
+
189
+ const { state: s2, setState: set2, map } = setup()
190
+ map.getPixelFromCoordinate = () => null // e.g. mid view transition
191
+ key('keydown', { key: ' ' })
192
+ key('keydown', { key: 'ArrowUp', altKey: true }) // no selection → start px unprojectable
193
+ expect(set2).not.toHaveBeenCalled()
194
+ expect(s2.selectedVertexIndex).toBe(-1)
195
+ })
@@ -0,0 +1,57 @@
1
+ import VectorSource from 'ol/source/Vector.js'
2
+ import VectorLayer from 'ol/layer/Vector.js'
3
+ import Feature from 'ol/Feature.js'
4
+ import Point from 'ol/geom/Point.js'
5
+ import { getMidpoints } from '../utils/geometryHelpers.js'
6
+
7
+ /**
8
+ * Manages a dedicated overlay layer for midpoint handles in edit mode.
9
+ * Midpoints are always visible (unlike OL Modify's native midpoints which
10
+ * only appear when the pointer is near a segment). The selected midpoint is
11
+ * rendered by the separate active-selection layer in EditMode (zIndex 103).
12
+ */
13
+ export const createMidpointLayer = (map, midpointStyle) => {
14
+ let currentStyle = midpointStyle
15
+ let selectedIndex = -1
16
+ const source = new VectorSource()
17
+ const layer = new VectorLayer({
18
+ source,
19
+ style: (feature) => feature.get('midpointIndex') === selectedIndex ? null : [currentStyle],
20
+ zIndex: 101
21
+ })
22
+ map.addLayer(layer)
23
+
24
+ return {
25
+ update (geom) {
26
+ source.clear()
27
+ const midpoints = getMidpoints(geom)
28
+ const features = midpoints.map((coord, i) => {
29
+ const f = new Feature({ geometry: new Point(coord) })
30
+ f.set('midpointIndex', i)
31
+ return f
32
+ })
33
+ source.addFeatures(features)
34
+ },
35
+
36
+ setSelected (index) {
37
+ selectedIndex = index
38
+ source.changed()
39
+ },
40
+
41
+ updateStyle (newMidpointStyle) {
42
+ currentStyle = newMidpointStyle
43
+ source.changed()
44
+ },
45
+
46
+ getCoords () {
47
+ return source.getFeatures()
48
+ .sort((a, b) => a.get('midpointIndex') - b.get('midpointIndex'))
49
+ .map(f => f.getGeometry().getCoordinates())
50
+ },
51
+
52
+ remove () {
53
+ source.clear()
54
+ map.removeLayer(layer)
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,42 @@
1
+ import Style from 'ol/style/Style.js'
2
+ import { createMidpointLayer } from './midpointLayer.js'
3
+ import { createFakeMap } from '../__helpers__/harness.js'
4
+
5
+ const GEOM = { type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 0]]] }
6
+
7
+ const setup = () => {
8
+ const map = createFakeMap()
9
+ const style = new Style({})
10
+ const handles = createMidpointLayer(map, style)
11
+ const layer = map.layers[0]
12
+ return { map, style, handles, layer, source: layer.getSource() }
13
+ }
14
+
15
+ test('update renders one handle per segment midpoint, retrievable in index order', () => {
16
+ const { handles, source } = setup()
17
+ handles.update(GEOM)
18
+ expect(source.getFeatures()).toHaveLength(3)
19
+ expect(handles.getCoords()).toEqual([[5, 0], [10, 5], [5, 5]])
20
+ })
21
+
22
+ test('the selected midpoint is hidden here and styles hot-swap', () => {
23
+ const { handles, layer, style, source } = setup()
24
+ handles.update(GEOM)
25
+ handles.setSelected(0)
26
+ const styleFn = layer.getStyle()
27
+ const featureAt = (i) => source.getFeatures().find(f => f.get('midpointIndex') === i)
28
+ expect(styleFn(featureAt(0))).toBeNull()
29
+ expect(styleFn(featureAt(1))).toEqual([style])
30
+
31
+ const newStyle = new Style({})
32
+ handles.updateStyle(newStyle)
33
+ expect(styleFn(featureAt(1))).toEqual([newStyle])
34
+ })
35
+
36
+ test('remove clears and detaches the layer', () => {
37
+ const { map, handles, source } = setup()
38
+ handles.update(GEOM)
39
+ handles.remove()
40
+ expect(source.getFeatures()).toHaveLength(0)
41
+ expect(map.layers).toHaveLength(0)
42
+ })
@@ -0,0 +1,83 @@
1
+ import Modify from 'ol/interaction/Modify.js'
2
+ import Collection from 'ol/Collection.js'
3
+ import { findNearest, PIXEL_TOLERANCE } from './vertexHitTest.js'
4
+
5
+ /**
6
+ * Derive the undo operation from the vertex arrays before and after an OL
7
+ * Modify drag. A grown array means a midpoint drag inserted a vertex; an
8
+ * equal-length array with a changed coordinate means a vertex move.
9
+ *
10
+ * @param {number[][]} prevCoords - vertices snapshotted at modifystart
11
+ * @param {number[][]} newCoords - vertices after modifyend
12
+ * @returns {{ type: string, vertexIndex: number, previousCoord?: number[] } | null}
13
+ */
14
+ export const deriveModifyOp = (prevCoords, newCoords) => {
15
+ if (newCoords.length > prevCoords.length) {
16
+ const insertedIdx = newCoords.findIndex((c, i) => c[0] !== prevCoords[i]?.[0])
17
+ return { type: 'insert_vertex', vertexIndex: Math.max(0, insertedIdx) }
18
+ }
19
+ if (newCoords.length === prevCoords.length) {
20
+ const movedIdx = newCoords.findIndex((c, i) => c[0] !== prevCoords[i][0] || c[1] !== prevCoords[i][1])
21
+ if (movedIdx >= 0) {
22
+ return { type: 'move_vertex', vertexIndex: movedIdx, previousCoord: prevCoords[movedIdx] }
23
+ }
24
+ }
25
+ return null
26
+ }
27
+
28
+ // Only activate Modify when clicking on a vertex/midpoint handle, never for the
29
+ // touch interface (touchHandler covers touch drags via the SVG offset target)
30
+ export const buildModifyCondition = ({ map, getState }) => (mapBrowserEvent) => {
31
+ const { interfaceType, vertices, midpoints } = getState()
32
+ if (interfaceType === 'touch') {
33
+ return false
34
+ }
35
+ const olPixel = map.getEventPixel(mapBrowserEvent.originalEvent)
36
+ return findNearest(map, vertices, midpoints, { x: olPixel[0], y: olPixel[1] }) !== null
37
+ }
38
+
39
+ /**
40
+ * OL Modify interaction wiring for edit mode.
41
+ *
42
+ * Modify handles pointer vertex dragging and midpoint insertion natively; this
43
+ * wrapper restricts it to clicks on a handle (buildModifyCondition) and reports
44
+ * each completed drag via `onModifyEnd(prevCoords)` with the vertices
45
+ * snapshotted at drag start.
46
+ *
47
+ * @returns {{ destroy: () => void }}
48
+ */
49
+ export const createModifyInteraction = ({ map, olFeature, getState, onModifyEnd }) => {
50
+ const condition = buildModifyCondition({ map, getState })
51
+
52
+ const modifyInteraction = new Modify({
53
+ features: new Collection([olFeature]),
54
+ style: () => [], // vertex circles rendered by vertexLayer instead
55
+ pixelTolerance: PIXEL_TOLERANCE,
56
+ condition
57
+ })
58
+ map.addInteraction(modifyInteraction)
59
+
60
+ let startCoords = null
61
+
62
+ modifyInteraction.on('modifystart', () => {
63
+ if (getState().interfaceType === 'touch') {
64
+ return
65
+ }
66
+ startCoords = getState().vertices.map(c => [...c])
67
+ })
68
+
69
+ modifyInteraction.on('modifyend', () => {
70
+ if (getState().interfaceType === 'touch') {
71
+ return
72
+ }
73
+ const prevCoords = startCoords
74
+ startCoords = null
75
+ onModifyEnd(prevCoords)
76
+ })
77
+
78
+ return {
79
+ destroy () {
80
+ map.removeInteraction(modifyInteraction)
81
+ }
82
+ }
83
+ }