@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,90 @@
1
+ import Feature from 'ol/Feature.js'
2
+ import Polygon from 'ol/geom/Polygon.js'
3
+ import LineString from 'ol/geom/LineString.js'
4
+ import Style from 'ol/style/Style.js'
5
+ import { createUndoStack } from '../../../utils/undoStack.js'
6
+
7
+ /**
8
+ * Shared test doubles for the OL adapter. The fake map uses an identity
9
+ * mapping between map coordinates and screen pixels so hit-test assertions
10
+ * read naturally: a vertex at [10, 20] sits at pixel (10, 20).
11
+ */
12
+
13
+ export const createEmitter = () => {
14
+ const listeners = {}
15
+ return {
16
+ on: jest.fn((type, h) => { (listeners[type] ??= []).push(h) }),
17
+ un: jest.fn((type, h) => { listeners[type] = (listeners[type] ?? []).filter(x => x !== h) }),
18
+ once: jest.fn((type, h) => { (listeners[`once:${type}`] ??= []).push(h) }),
19
+ emit (type, e) {
20
+ (listeners[type] ?? []).forEach(h => h(e));
21
+ (listeners[`once:${type}`] ?? []).splice(0).forEach(h => h(e))
22
+ }
23
+ }
24
+ }
25
+
26
+ export const createFakeMap = ({ center = [50, 50] } = {}) => {
27
+ const emitter = createEmitter()
28
+ const viewport = document.createElement('div')
29
+ document.body.appendChild(viewport)
30
+ const view = { ...createEmitter(), getCenter: () => center, getAnimating: jest.fn(() => false) }
31
+ return {
32
+ ...emitter,
33
+ interactions: [],
34
+ layers: [],
35
+ addInteraction (i) { this.interactions.push(i) },
36
+ removeInteraction: jest.fn(function (i) { this.interactions = this.interactions.filter(x => x !== i) }),
37
+ addLayer (l) { this.layers.push(l) },
38
+ removeLayer: jest.fn(function (l) { this.layers = this.layers.filter(x => x !== l) }),
39
+ getViewport: () => viewport,
40
+ getPixelFromCoordinate: (c) => [c[0], c[1]],
41
+ getCoordinateFromPixel: (p) => [p[0], p[1]],
42
+ getEventPixel: (e) => [e.clientX, e.clientY],
43
+ getView: () => view,
44
+ render: jest.fn()
45
+ }
46
+ }
47
+
48
+ // Manager double matching the OLDrawManager surface EditMode/DrawMode consume
49
+ export const createFakeManager = () => {
50
+ const bus = createEmitter()
51
+ return {
52
+ on: bus.on,
53
+ off: bus.un,
54
+ emit: jest.fn(bus.emit),
55
+ // Real Style instances — OL asserts on setStyle args; identify them by reference
56
+ styles: {
57
+ editFeatureStyle: new Style({}),
58
+ editFeatureStyleInvalid: new Style({}),
59
+ vertexStyle: new Style({}),
60
+ midpointStyle: new Style({}),
61
+ selectedVertexStyle: new Style({}),
62
+ selectedMidpointStyle: new Style({}),
63
+ createSketchStyle: jest.fn(() => () => [])
64
+ },
65
+ colors: { editVertex: 'rgba(29,112,184,1)' },
66
+ undoStack: createUndoStack(() => {})
67
+ }
68
+ }
69
+
70
+ export const polygonFeature = (ring, id = 'f1') => {
71
+ const feature = new Feature(new Polygon([ring]))
72
+ feature.setId(id)
73
+ return feature
74
+ }
75
+
76
+ export const lineFeature = (coords, id = 'f1') => {
77
+ const feature = new Feature(new LineString(coords))
78
+ feature.setId(id)
79
+ return feature
80
+ }
81
+
82
+ export const createContainer = () => {
83
+ const container = document.createElement('div')
84
+ container.tabIndex = 0
85
+ document.body.appendChild(container)
86
+ return container
87
+ }
88
+
89
+ // jsdom has no PointerEvent/TouchEvent constructors — build plain events with the fields the handlers read
90
+ export const domEvent = (type, props) => Object.assign(new Event(type, { bubbles: true }), props)
@@ -0,0 +1,178 @@
1
+ import VectorLayer from 'ol/layer/Vector.js'
2
+ import { createFeatureStore } from './featureStore.js'
3
+ import { createUndoStack } from '../../../utils/undoStack.js'
4
+ import { createStyles } from './styles.js'
5
+ import { resolveColors } from '../../../utils/resolveColors.js'
6
+ import { createSnapManager } from '../snap/snapManager.js'
7
+ import { createDrawMode } from '../draw/DrawMode.js'
8
+ import { createEditMode } from '../edit/EditMode.js'
9
+ import { TOLERANCES } from '../defaults.js'
10
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
11
+ import { STYLES_CHANGED_EVENT } from './internalEvents.js'
12
+
13
+ /**
14
+ * Mode machine for the OL draw plugin.
15
+ *
16
+ * Owns the VectorSource/Layer, undo stack, and current mode instance.
17
+ * Exposes a minimal on/off/emit event bus for internal plugin communication
18
+ * (separate from the public eventBus used for consumer-facing events).
19
+ *
20
+ * Consumer-facing events are always emitted via eventBus by events.js after
21
+ * listening to the manager's internal events.
22
+ */
23
+ export class OLDrawManager {
24
+ constructor (map, pluginConfig = {}) {
25
+ this._map = map
26
+ this._pluginConfig = pluginConfig
27
+ this._mode = 'disabled'
28
+ this._modeInstance = null
29
+ this._listeners = new Map()
30
+
31
+ this.store = createFeatureStore()
32
+ this.undoStack = createUndoStack((length) => this.emit(ADAPTER_EVENTS.UNDO_CHANGE, length))
33
+
34
+ this.colors = resolveColors(null, pluginConfig)
35
+ this.styles = createStyles(this.colors)
36
+ this.snap = createSnapManager(map, pluginConfig.snapLayers ?? null, this.colors, pluginConfig.snapRadius ?? TOLERANCES.snapRadius)
37
+
38
+ this._layer = new VectorLayer({
39
+ source: this.store.source,
40
+ style: this.styles.createFeatureStyle(),
41
+ zIndex: 100
42
+ })
43
+ this._layer.set('layerId', 'draw')
44
+ map.addLayer(this._layer)
45
+ }
46
+
47
+ // --- Color / style updates ---
48
+
49
+ setMapStyle (mapStyle) {
50
+ this.colors = resolveColors(mapStyle, this._pluginConfig)
51
+ this.styles = createStyles(this.colors)
52
+ this._layer.setStyle(this.styles.createFeatureStyle())
53
+ this.store.source.changed()
54
+ this.snap?.updateColors(this.colors)
55
+ this.emit(STYLES_CHANGED_EVENT, this.styles)
56
+ }
57
+
58
+ // --- Internal event bus ---
59
+
60
+ on (type, handler) {
61
+ if (!this._listeners.has(type)) {
62
+ this._listeners.set(type, new Set())
63
+ }
64
+ this._listeners.get(type).add(handler)
65
+ }
66
+
67
+ off (type, handler) {
68
+ this._listeners.get(type)?.delete(handler)
69
+ }
70
+
71
+ emit (type, detail) {
72
+ const handlers = this._listeners.get(type)
73
+ // Array.from, not [...handlers] — see the comment in utils/eventBus.js:
74
+ // under a loose-mode Babel build (Docusaurus's docs site), spreading a Set
75
+ // compiles to [].concat(handlers), which doesn't flatten it — it appends
76
+ // the whole Set as one non-function element, and h(...) throws.
77
+ if (handlers) { Array.from(handlers).forEach(h => h(detail)) }
78
+ }
79
+
80
+ // --- Mode machine ---
81
+
82
+ async changeMode (modeName, options = {}) {
83
+ this._modeInstance?.destroy()
84
+ this._modeInstance = null
85
+ this._mode = modeName
86
+
87
+ const isDrawMode = modeName === 'draw_polygon' || modeName === 'draw_line' || modeName === 'edit_vertex'
88
+ this.snap?.setIndicatorActive(isDrawMode)
89
+
90
+ const modeOptions = { ...options, snap: this.snap }
91
+
92
+ if (modeName === 'draw_polygon' || modeName === 'draw_line') {
93
+ this._modeInstance = createDrawMode({ map: this._map, manager: this, options: modeOptions })
94
+ } else if (modeName === 'edit_vertex') {
95
+ this._modeInstance = createEditMode({ map: this._map, manager: this, options: modeOptions })
96
+ } else {
97
+ // disabled — no mode instance needed
98
+ }
99
+ // Reattach snap interaction after mode's interactions are added so it
100
+ // processes pointermove first (OL: last-added interaction = first to handle events).
101
+ this.snap?.reattach()
102
+ }
103
+
104
+ getMode () {
105
+ return this._mode
106
+ }
107
+
108
+ // --- High-level operations called by events.js ---
109
+
110
+ done () {
111
+ this._modeInstance?.done()
112
+ }
113
+
114
+ cancel () {
115
+ this._modeInstance?.cancel()
116
+ this.changeMode('disabled')
117
+ }
118
+
119
+ undo () {
120
+ this._modeInstance?.undo()
121
+ }
122
+
123
+ deleteVertex () {
124
+ this._modeInstance?.deleteVertex()
125
+ }
126
+
127
+ nudgeSelectedVertex (dx, dy, isLargeStep) {
128
+ this._modeInstance?.nudgeSelectedVertex?.(dx, dy, isLargeStep)
129
+ }
130
+
131
+ // Show/hide the dashed invalid stroke on the active draw sketch or edit feature.
132
+ setInvalid (invalid) {
133
+ this._modeInstance?.setInvalid?.(invalid)
134
+ }
135
+
136
+ // Tag the active draw sketch with a custom style property (e.g. split's
137
+ // valid/invalid line colour). No-op in edit mode or without a mode instance.
138
+ setDrawingPreviewProperty (property, value) {
139
+ this._modeInstance?.setDrawingPreviewProperty?.(property, value)
140
+ }
141
+
142
+ setInterfaceType (type) {
143
+ this._modeInstance?.setInterfaceType?.(type)
144
+ // Parity with the ML adapter: an explicit interface-type write is echoed on
145
+ // the bus so events.js can relay it as draw:interfacetypechange.
146
+ this.emit(ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE, { interfaceType: type })
147
+ }
148
+
149
+ // --- Feature store delegation ---
150
+
151
+ get (id) {
152
+ return this.store.get(id)
153
+ }
154
+
155
+ add (geojsonFeature) {
156
+ return this.store.add(geojsonFeature)
157
+ }
158
+
159
+ delete (id) {
160
+ return this.store.remove(id)
161
+ }
162
+
163
+ deleteAll () {
164
+ return this.store.clear()
165
+ }
166
+
167
+ // --- Cleanup ---
168
+
169
+ remove () {
170
+ this._modeInstance?.destroy()
171
+ this._modeInstance = null
172
+ this.snap?.destroy()
173
+ this.snap = null
174
+ this.store.clear()
175
+ this._map.removeLayer(this._layer)
176
+ this._listeners.clear()
177
+ }
178
+ }
@@ -0,0 +1,153 @@
1
+ import { OLDrawManager } from './OLDrawManager.js'
2
+ import { STYLES_CHANGED_EVENT } from './internalEvents.js'
3
+ import { createDrawMode } from '../draw/DrawMode.js'
4
+ import { createEditMode } from '../edit/EditMode.js'
5
+ import { createSnapManager } from '../snap/snapManager.js'
6
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
7
+ import { createFakeMap } from '../__helpers__/harness.js'
8
+ import { TOLERANCES } from '../defaults.js'
9
+
10
+ jest.mock('../draw/DrawMode.js', () => ({
11
+ createDrawMode: jest.fn(() => ({ destroy: jest.fn(), done: jest.fn(), cancel: jest.fn(), undo: jest.fn(), deleteVertex: jest.fn(), nudgeSelectedVertex: jest.fn(), setInterfaceType: jest.fn(), setInvalid: jest.fn(), setDrawingPreviewProperty: jest.fn() }))
12
+ }))
13
+ jest.mock('../edit/EditMode.js', () => ({
14
+ createEditMode: jest.fn(() => ({ destroy: jest.fn(), done: jest.fn(), cancel: jest.fn(), undo: jest.fn(), deleteVertex: jest.fn(), nudgeSelectedVertex: jest.fn(), setInterfaceType: jest.fn(), setInvalid: jest.fn() }))
15
+ }))
16
+ jest.mock('../snap/snapManager.js', () => ({
17
+ createSnapManager: jest.fn(() => ({ setIndicatorActive: jest.fn(), reattach: jest.fn(), updateColors: jest.fn(), destroy: jest.fn() }))
18
+ }))
19
+
20
+ const geojson = { type: 'Feature', id: 'f1', properties: {}, geometry: { type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 0]]] } }
21
+
22
+ const setup = (pluginConfig) => {
23
+ const map = createFakeMap()
24
+ const manager = new OLDrawManager(map, pluginConfig)
25
+ return { map, manager }
26
+ }
27
+
28
+ afterEach(() => jest.clearAllMocks())
29
+
30
+ test('sets up the draw layer and the snap manager from the plugin config', () => {
31
+ const { map } = setup({ snapLayers: ['boundaries'], snapRadius: 20 })
32
+ expect(map.layers[0].get('layerId')).toBe('draw')
33
+ expect(createSnapManager).toHaveBeenCalledWith(map, ['boundaries'], expect.any(Object), 20)
34
+
35
+ setup() // defaults
36
+ expect(createSnapManager).toHaveBeenLastCalledWith(expect.anything(), null, expect.any(Object), TOLERANCES.snapRadius)
37
+ })
38
+
39
+ test('map style changes rebuild the styles and notify modes and snap', () => {
40
+ const { map, manager } = setup()
41
+ const onStyles = jest.fn()
42
+ manager.on(STYLES_CHANGED_EVENT, onStyles)
43
+ const before = manager.styles
44
+ manager.setMapStyle({ id: 'dark', mapColorScheme: 'dark' })
45
+ expect(manager.styles).not.toBe(before)
46
+ expect(map.layers[0].getStyle()).toEqual(expect.any(Function))
47
+ expect(manager.snap.updateColors).toHaveBeenCalledWith(manager.colors)
48
+ expect(onStyles).toHaveBeenCalledWith(manager.styles)
49
+ })
50
+
51
+ describe('mode machine', () => {
52
+ test.each([
53
+ ['draw_polygon', createDrawMode],
54
+ ['draw_line', createDrawMode],
55
+ ['edit_vertex', createEditMode]
56
+ ])('%s creates its mode with snap injected, activates the indicator and reattaches snap last', async (name, factory) => {
57
+ const { map, manager } = setup()
58
+ await manager.changeMode(name, { featureId: 'f1' })
59
+ expect(manager.getMode()).toBe(name)
60
+ expect(factory).toHaveBeenCalledWith({ map, manager, options: { featureId: 'f1', snap: manager.snap } })
61
+ expect(manager.snap.setIndicatorActive).toHaveBeenCalledWith(true)
62
+ expect(manager.snap.reattach).toHaveBeenCalled()
63
+ })
64
+
65
+ test('disabled destroys the previous mode and deactivates the indicator', async () => {
66
+ const { manager } = setup()
67
+ await manager.changeMode('draw_polygon')
68
+ const instance = createDrawMode.mock.results[0].value
69
+ await manager.changeMode('disabled')
70
+ expect(instance.destroy).toHaveBeenCalled()
71
+ expect(manager.getMode()).toBe('disabled')
72
+ expect(manager.snap.setIndicatorActive).toHaveBeenLastCalledWith(false)
73
+ })
74
+
75
+ test('operations delegate to the current mode instance and are safe without one', async () => {
76
+ const { manager } = setup()
77
+ manager.done(); manager.undo(); manager.deleteVertex(); manager.nudgeSelectedVertex(1, 0, true); manager.setInterfaceType('touch'); manager.setInvalid(true) // no mode — no throw
78
+ manager.setDrawingPreviewProperty('splitter', 'valid') // no mode — no throw
79
+
80
+ await manager.changeMode('draw_polygon')
81
+ const instance = createDrawMode.mock.results[0].value
82
+ manager.done()
83
+ manager.undo()
84
+ manager.deleteVertex()
85
+ manager.nudgeSelectedVertex(1, 0, true)
86
+ const emitted = jest.fn()
87
+ manager.on('interfacetypechange', emitted)
88
+ manager.setInterfaceType('touch')
89
+ manager.setInvalid(true)
90
+ manager.setDrawingPreviewProperty('splitter', 'valid')
91
+ expect(instance.done).toHaveBeenCalled()
92
+ expect(instance.undo).toHaveBeenCalled()
93
+ expect(instance.deleteVertex).toHaveBeenCalled()
94
+ expect(instance.nudgeSelectedVertex).toHaveBeenCalledWith(1, 0, true)
95
+ expect(instance.setInterfaceType).toHaveBeenCalledWith('touch')
96
+ // Parity with ML: an explicit interface-type write is echoed on the bus.
97
+ expect(emitted).toHaveBeenCalledWith({ interfaceType: 'touch' })
98
+ expect(instance.setInvalid).toHaveBeenCalledWith(true)
99
+ expect(instance.setDrawingPreviewProperty).toHaveBeenCalledWith('splitter', 'valid')
100
+
101
+ manager.cancel()
102
+ expect(instance.cancel).toHaveBeenCalled()
103
+ expect(manager.getMode()).toBe('disabled')
104
+ })
105
+ })
106
+
107
+ test('undo stack changes are published on the adapter bus; off unsubscribes', () => {
108
+ const { manager } = setup()
109
+ const onUndoChange = jest.fn()
110
+ manager.on(ADAPTER_EVENTS.UNDO_CHANGE, onUndoChange)
111
+ manager.undoStack.push({ type: 'draw_vertex' })
112
+ expect(onUndoChange).toHaveBeenCalledWith(1)
113
+ manager.off(ADAPTER_EVENTS.UNDO_CHANGE, onUndoChange)
114
+ manager.undoStack.push({ type: 'draw_vertex' })
115
+ expect(onUndoChange).toHaveBeenCalledTimes(1)
116
+ })
117
+
118
+ test('several handlers can subscribe to the same event type and all fire', () => {
119
+ const { manager } = setup()
120
+ const h1 = jest.fn()
121
+ const h2 = jest.fn()
122
+ manager.on(ADAPTER_EVENTS.UNDO_CHANGE, h1)
123
+ manager.on(ADAPTER_EVENTS.UNDO_CHANGE, h2) // same type reuses the existing handler set
124
+ manager.undoStack.push({ type: 'draw_vertex' })
125
+ expect(h1).toHaveBeenCalledWith(1)
126
+ expect(h2).toHaveBeenCalledWith(1)
127
+ })
128
+
129
+ test('feature store delegation works with GeoJSON in and out', () => {
130
+ const { manager } = setup()
131
+ manager.add(geojson)
132
+ expect(manager.get('f1')).toMatchObject({ id: 'f1' })
133
+ manager.delete('f1')
134
+ expect(manager.get('f1')).toBeNull()
135
+ manager.add(geojson)
136
+ manager.deleteAll()
137
+ expect(manager.get('f1')).toBeNull()
138
+ })
139
+
140
+ test('remove tears everything down and silences the bus', async () => {
141
+ const { map, manager } = setup()
142
+ await manager.changeMode('draw_polygon')
143
+ const instance = createDrawMode.mock.results[0].value
144
+ const snap = manager.snap
145
+ const listener = jest.fn()
146
+ manager.on(STYLES_CHANGED_EVENT, listener)
147
+ manager.remove()
148
+ expect(instance.destroy).toHaveBeenCalled()
149
+ expect(snap.destroy).toHaveBeenCalled()
150
+ expect(map.removeLayer).toHaveBeenCalled()
151
+ manager.emit(STYLES_CHANGED_EVENT, {})
152
+ expect(listener).not.toHaveBeenCalled()
153
+ })
@@ -0,0 +1,69 @@
1
+ import VectorSource from 'ol/source/Vector.js'
2
+ import GeoJSON from 'ol/format/GeoJSON.js'
3
+
4
+ const PROJECTION = 'EPSG:27700'
5
+
6
+ // No coordinate transformation: data and map both use BNG.
7
+ const format = new GeoJSON({ dataProjection: PROJECTION, featureProjection: PROJECTION })
8
+
9
+ /**
10
+ * Wraps an OL VectorSource with a GeoJSON-oriented API keyed by feature ID.
11
+ * All GeoJSON in and out uses EPSG:27700 coordinates (BNG easting/northing).
12
+ */
13
+ export const createFeatureStore = () => {
14
+ const source = new VectorSource()
15
+
16
+ return {
17
+ /** The underlying VectorSource, passed to OL interactions and layers. */
18
+ source,
19
+
20
+ /** Get the raw OL Feature by ID, or null. */
21
+ getOL (id) {
22
+ return source.getFeatureById(String(id)) ?? null
23
+ },
24
+
25
+ /** Add or replace a GeoJSON feature. Returns the OL Feature. */
26
+ add (geojsonFeature) {
27
+ const existing = this.getOL(geojsonFeature.id)
28
+ if (existing) {
29
+ source.removeFeature(existing)
30
+ }
31
+ const olFeature = format.readFeature(geojsonFeature)
32
+ source.addFeature(olFeature)
33
+ return olFeature
34
+ },
35
+
36
+ /** Get a GeoJSON feature by ID, or null. */
37
+ get (id) {
38
+ const feature = this.getOL(id)
39
+ return feature ? format.writeFeatureObject(feature) : null
40
+ },
41
+
42
+ /** Remove one feature, or several, by ID. Matches mapbox-gl-draw's delete(ids),
43
+ * which accepts a single id or an array — deleteFeature.js passes either. */
44
+ remove (id) {
45
+ const ids = Array.isArray(id) ? id : [id]
46
+ for (const featureId of ids) {
47
+ const feature = this.getOL(featureId)
48
+ if (feature) {
49
+ source.removeFeature(feature)
50
+ }
51
+ }
52
+ },
53
+
54
+ /** Remove all features. */
55
+ clear () {
56
+ source.clear()
57
+ },
58
+
59
+ /** Convert an OL Feature to a GeoJSON object. */
60
+ toGeoJSON (olFeature) {
61
+ return format.writeFeatureObject(olFeature)
62
+ },
63
+
64
+ /** Convert a GeoJSON object to an OL Feature (no side effects). */
65
+ fromGeoJSON (geojsonFeature) {
66
+ return format.readFeature(geojsonFeature)
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,58 @@
1
+ import { createFeatureStore } from './featureStore.js'
2
+
3
+ const geojson = (id, coords = [[[0, 0], [10, 0], [10, 10], [0, 0]]]) => ({
4
+ type: 'Feature',
5
+ id,
6
+ properties: { label: 'field' },
7
+ geometry: { type: 'Polygon', coordinates: coords }
8
+ })
9
+
10
+ test('add returns the OL feature, retrievable by id in both OL and GeoJSON form', () => {
11
+ const store = createFeatureStore()
12
+ const olFeature = store.add(geojson('f1'))
13
+ expect(store.getOL('f1')).toBe(olFeature)
14
+ expect(store.get('f1')).toMatchObject({ id: 'f1', properties: { label: 'field' } })
15
+ expect(store.get('f1').geometry.coordinates[0]).toHaveLength(4)
16
+ })
17
+
18
+ test('adding the same id replaces the existing feature instead of duplicating', () => {
19
+ const store = createFeatureStore()
20
+ store.add(geojson('f1'))
21
+ store.add(geojson('f1', [[[0, 0], [99, 0], [99, 99], [0, 0]]]))
22
+ expect(store.source.getFeatures()).toHaveLength(1)
23
+ expect(store.get('f1').geometry.coordinates[0][1]).toEqual([99, 0])
24
+ })
25
+
26
+ test('unknown ids come back null', () => {
27
+ const store = createFeatureStore()
28
+ expect(store.getOL('missing')).toBeNull()
29
+ expect(store.get('missing')).toBeNull()
30
+ })
31
+
32
+ test('remove deletes by id and tolerates unknown ids; clear empties the source', () => {
33
+ const store = createFeatureStore()
34
+ store.add(geojson('f1'))
35
+ store.add(geojson('f2'))
36
+ store.remove('f1')
37
+ store.remove('missing') // no throw
38
+ expect(store.source.getFeatures()).toHaveLength(1)
39
+ store.clear()
40
+ expect(store.source.getFeatures()).toHaveLength(0)
41
+ })
42
+
43
+ test('remove accepts an array of ids and deletes all of them', () => {
44
+ const store = createFeatureStore()
45
+ store.add(geojson('f1'))
46
+ store.add(geojson('f2'))
47
+ store.add(geojson('f3'))
48
+ store.remove(['f1', 'f2', 'missing'])
49
+ expect(store.source.getFeatures()).toHaveLength(1)
50
+ expect(store.get('f3')).not.toBeNull()
51
+ })
52
+
53
+ test('toGeoJSON / fromGeoJSON round-trip without touching the source', () => {
54
+ const store = createFeatureStore()
55
+ const olFeature = store.fromGeoJSON(geojson('f9'))
56
+ expect(store.source.getFeatures()).toHaveLength(0)
57
+ expect(store.toGeoJSON(olFeature)).toMatchObject({ id: 'f9', geometry: { type: 'Polygon' } })
58
+ })
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Manager-internal event names (these never cross the adapter boundary —
3
+ * the shared contract lives in src/adapterEvents.js).
4
+ */
5
+
6
+ // Fired by OLDrawManager when the resolved colour set / style instances are
7
+ // rebuilt after a map style change; modes re-style their layers in response.
8
+ export const STYLES_CHANGED_EVENT = 'styleschanged'