@defra/interactive-map 0.0.36-alpha → 0.0.38-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/assets/templates/draw-tools.njk +4 -3
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api.md +32 -0
  8. package/docs/examples/draw-tools.mdx +0 -4
  9. package/docs/plugins/draw.md +482 -0
  10. package/docs/plugins.md +2 -6
  11. package/govuk-prototype-kit.config.json +3 -2
  12. package/package.json +2 -4
  13. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  14. package/plugins/beta/draw-es/src/events.js +1 -1
  15. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  18. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  19. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  20. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  21. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  22. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  23. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +13 -0
  24. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  26. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  27. package/plugins/draw/dist/esm/index.js +1 -0
  28. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  29. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  30. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  31. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  32. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  33. package/plugins/draw/dist/umd/index.js +2 -0
  34. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  35. package/plugins/draw/src/DrawInit.jsx +89 -0
  36. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  37. package/plugins/draw/src/adapterEvents.js +127 -0
  38. package/plugins/draw/src/adapterEvents.test.js +20 -0
  39. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  40. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  41. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  42. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  43. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  44. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  45. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  46. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  47. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  48. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  49. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  95. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  96. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  97. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  98. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  99. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  100. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  101. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  102. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  103. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  104. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  105. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  106. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  107. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  108. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  109. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  110. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  111. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  112. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  113. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  114. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  115. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  118. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  119. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  120. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  121. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  144. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  146. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  148. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  149. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  156. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  157. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  158. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  159. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  163. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  164. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  165. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  166. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  167. package/plugins/draw/src/api/addFeature.js +22 -0
  168. package/plugins/draw/src/api/addFeature.test.js +41 -0
  169. package/plugins/draw/src/api/deleteFeature.js +11 -0
  170. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  171. package/plugins/draw/src/api/editFeature.js +54 -0
  172. package/plugins/draw/src/api/editFeature.test.js +134 -0
  173. package/plugins/draw/src/api/merge.js +30 -0
  174. package/plugins/draw/src/api/merge.test.js +57 -0
  175. package/plugins/draw/src/api/newLine.js +41 -0
  176. package/plugins/draw/src/api/newLine.test.js +93 -0
  177. package/plugins/draw/src/api/newPolygon.js +41 -0
  178. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  179. package/plugins/draw/src/api/split.js +117 -0
  180. package/plugins/draw/src/api/split.test.js +204 -0
  181. package/plugins/draw/src/defaults.js +52 -0
  182. package/plugins/draw/src/defaults.test.js +18 -0
  183. package/plugins/draw/src/draw.scss +43 -0
  184. package/plugins/draw/src/events.js +237 -0
  185. package/plugins/draw/src/events.test.js +369 -0
  186. package/plugins/draw/src/index.js +10 -0
  187. package/plugins/draw/src/index.test.js +24 -0
  188. package/plugins/draw/src/manifest.js +170 -0
  189. package/plugins/draw/src/manifest.test.js +147 -0
  190. package/plugins/draw/src/reducer.js +69 -0
  191. package/plugins/draw/src/reducer.test.js +85 -0
  192. package/plugins/draw/src/utils/debounce.js +16 -0
  193. package/plugins/draw/src/utils/debounce.test.js +40 -0
  194. package/plugins/draw/src/utils/eventBus.js +36 -0
  195. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  196. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  197. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  198. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  199. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  200. package/plugins/draw/src/utils/resolveColors.js +39 -0
  201. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  202. package/plugins/draw/src/utils/spatial.js +243 -0
  203. package/plugins/draw/src/utils/spatial.test.js +243 -0
  204. package/plugins/draw/src/utils/touchTarget.js +95 -0
  205. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  206. package/plugins/draw/src/utils/undoStack.js +31 -0
  207. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  208. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  209. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  210. package/plugins/draw/src/validation/liveStroke.js +85 -0
  211. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  212. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  213. package/plugins/draw/src/validation/rules.js +186 -0
  214. package/plugins/draw/src/validation/rules.test.js +120 -0
  215. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  216. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  217. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  218. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  219. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  220. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  221. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  222. package/plugins/search/dist/css/index.css +1 -317
  223. package/rollup.esm.mjs +35 -28
  224. package/sonar-project.properties +2 -2
  225. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  226. package/src/App/components/Hints/Hints.module.scss +5 -4
  227. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  228. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  229. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  230. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  231. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  232. package/src/App/hooks/useHintsAPI.js +67 -0
  233. package/src/App/hooks/useHintsAPI.test.js +145 -0
  234. package/src/App/renderer/PluginInits.jsx +4 -0
  235. package/src/App/renderer/PluginInits.test.jsx +7 -1
  236. package/src/InteractiveMap/InteractiveMap.js +20 -0
  237. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  238. package/src/config/events.js +4 -0
  239. package/src/scss/settings/_dimensions.scss +3 -0
  240. package/src/services/announcer.js +38 -3
  241. package/src/services/announcer.test.js +133 -1
  242. package/src/services/hints.js +3 -0
  243. package/src/services/hints.test.js +19 -0
  244. package/src/types.js +12 -0
  245. package/src/utils/detectInterfaceType.js +5 -1
  246. package/src/utils/isMac.js +15 -0
  247. package/src/utils/isMac.test.js +37 -0
  248. package/webpack.umd.mjs +1 -0
@@ -0,0 +1,155 @@
1
+ import Style from 'ol/style/Style.js'
2
+ import Fill from 'ol/style/Fill.js'
3
+ import Stroke from 'ol/style/Stroke.js'
4
+ import CircleStyle from 'ol/style/Circle.js'
5
+ import MultiPoint from 'ol/geom/MultiPoint.js'
6
+ import { SIZES } from '../defaults.js'
7
+ import { getPlacedSketchCoords } from '../utils/sketchHelpers.js'
8
+
9
+ const HALO_RADIUS_OFFSET = 3
10
+
11
+ const selectedVertexRadii = { outer: SIZES.vertexHaloRadius + HALO_RADIUS_OFFSET, mid: SIZES.vertexHaloRadius, inner: SIZES.vertexRadius }
12
+ const selectedMidpointRadii = { outer: SIZES.midpointHaloRadius + HALO_RADIUS_OFFSET, mid: SIZES.midpointHaloRadius, inner: SIZES.midpointRadius }
13
+
14
+ const fillArc = (ctx, cx, cy, radius, fillStyle) => {
15
+ ctx.beginPath()
16
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2)
17
+ ctx.fillStyle = fillStyle
18
+ ctx.fill()
19
+ }
20
+
21
+ // Custom renderer draws all arcs at the same (cx,cy) so concentric rings never
22
+ // drift at fractional CSS scales (e.g. 1.5×) the way separate drawImage calls can.
23
+ const makeRingRenderer = ({ outer, mid, inner }, colors, innerKey) => (pixelCoordinates, state) => {
24
+ const ctx = state.context
25
+ const pr = state.pixelRatio
26
+ const [cx, cy] = /** @type {number[]} */ (pixelCoordinates)
27
+ ctx.save()
28
+ fillArc(ctx, cx, cy, outer * pr, colors.editActive)
29
+ fillArc(ctx, cx, cy, mid * pr, colors.editHalo)
30
+ fillArc(ctx, cx, cy, inner * pr, colors[innerKey])
31
+ ctx.restore()
32
+ }
33
+
34
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1)
35
+
36
+ // Shared by edit-mode vertices and in-progress sketch vertices so they always look the same
37
+ const createVertexStyles = (colors) => {
38
+ const vertexImage = new CircleStyle({
39
+ radius: SIZES.vertexRadius,
40
+ fill: new Fill({ color: colors.editVertex })
41
+ })
42
+ return {
43
+ vertexImage,
44
+ vertexStyle: new Style({ image: vertexImage }),
45
+ selectedVertexStyle: new Style({ renderer: makeRingRenderer(selectedVertexRadii, colors, 'editVertex') })
46
+ }
47
+ }
48
+
49
+ const createMidpointStyles = (colors) => ({
50
+ midpointStyle: new Style({
51
+ image: new CircleStyle({
52
+ radius: SIZES.midpointRadius,
53
+ fill: new Fill({ color: colors.editMidpoint })
54
+ })
55
+ }),
56
+ selectedMidpointStyle: new Style({ renderer: makeRingRenderer(selectedMidpointRadii, colors, 'editMidpoint') })
57
+ })
58
+
59
+ // Split-line preview colours: valid is solid, invalid is dashed — matching
60
+ // ML's stroke-valid-splitter / stroke-invalid-splitter layers.
61
+ const createSketchLineStyles = (colors) => ({
62
+ valid: new Style({
63
+ stroke: new Stroke({ color: colors.editStroke, width: 2 }),
64
+ fill: new Fill({ color: colors.editFill })
65
+ }),
66
+ invalid: new Style({
67
+ stroke: new Stroke({ color: colors.invalidStroke, width: 2, lineDash: [2, 4] })
68
+ }),
69
+ splitValid: new Style({
70
+ stroke: new Stroke({ color: colors.splitValid, width: 2 })
71
+ }),
72
+ splitInvalid: new Style({
73
+ stroke: new Stroke({ color: colors.splitInvalid, width: 2, lineDash: [2, 4] })
74
+ })
75
+ })
76
+
77
+ /**
78
+ * Create all draw-ol style instances for the given resolved color set.
79
+ *
80
+ * @param {object} colors - Output of resolveColors()
81
+ * @returns {{ vertexStyle, selectedVertexStyle, midpointStyle, selectedMidpointStyle,
82
+ * editFeatureStyle, createSketchStyle, createFeatureStyle }}
83
+ */
84
+ export const createStyles = (colors) => {
85
+ const { vertexImage, vertexStyle, selectedVertexStyle } = createVertexStyles(colors)
86
+ const { midpointStyle, selectedMidpointStyle } = createMidpointStyles(colors)
87
+
88
+ const editFeatureStyle = new Style({
89
+ stroke: new Stroke({ color: colors.editStroke, width: 2 }),
90
+ fill: new Fill({ color: colors.editFill })
91
+ })
92
+
93
+ // Dashed variant shown while the edited/drawn shape is invalid — no fill, so an
94
+ // invalid shape reads as an outline only.
95
+ const editFeatureStyleInvalid = new Style({
96
+ stroke: new Stroke({ color: colors.invalidStroke, width: 2, lineDash: [2, 4] })
97
+ })
98
+
99
+ const sketchLineStyles = createSketchLineStyles(colors)
100
+
101
+ // Reused across renders — the geometry function runs every frame while sketching,
102
+ // so mutate one MultiPoint (setCoordinates bumps its revision, keeping OL's
103
+ // render caches correct) instead of allocating a new one per frame
104
+ const sketchVertices = new MultiPoint([])
105
+ const sketchVertexStyle = new Style({
106
+ image: vertexImage,
107
+ geometry: (feature) => {
108
+ const coords = getPlacedSketchCoords(feature.getGeometry())
109
+ if (!coords.length) {
110
+ return null
111
+ }
112
+ sketchVertices.setCoordinates(coords)
113
+ return sketchVertices
114
+ }
115
+ })
116
+
117
+ // No style for the Point sketch (cursor-following ghost marker); placed
118
+ // vertices get markers on the sketch feature instead. geometryType filters
119
+ // out the extra LineString sketch OL renders alongside a Polygon sketch,
120
+ // so vertices aren't drawn twice. `invalid` swaps to the dashed line style.
121
+ // A 'splitter' property on the feature (set via setDrawingPreviewProperty)
122
+ // overrides both with the split-specific valid/invalid colours.
123
+ const createSketchStyle = (geometryType, invalid = false) => (feature) => {
124
+ const type = feature.getGeometry().getType()
125
+ if (type === 'Point') { return [] }
126
+ const splitter = feature.get('splitter')
127
+ let lineStyle = invalid ? sketchLineStyles.invalid : sketchLineStyles.valid
128
+ if (splitter === 'valid') { lineStyle = sketchLineStyles.splitValid }
129
+ if (splitter === 'invalid') { lineStyle = sketchLineStyles.splitInvalid }
130
+ return type === geometryType ? [lineStyle, sketchVertexStyle] : [lineStyle]
131
+ }
132
+
133
+ const createFeatureStyle = () => (feature) => {
134
+ const p = feature.getProperties()
135
+ const id = colors.mapStyleId
136
+ const stroke = (id && p[`stroke${capitalize(id)}`]) || p.stroke || colors.shapeStroke
137
+ const fill = (id && p[`fill${capitalize(id)}`]) || p.fill || colors.shapeFill
138
+ const strokeWidth = p.strokeWidth || colors.strokeWidth
139
+ return [new Style({
140
+ stroke: new Stroke({ color: stroke, width: strokeWidth }),
141
+ fill: new Fill({ color: fill })
142
+ })]
143
+ }
144
+
145
+ return {
146
+ vertexStyle,
147
+ selectedVertexStyle,
148
+ midpointStyle,
149
+ selectedMidpointStyle,
150
+ editFeatureStyle,
151
+ editFeatureStyleInvalid,
152
+ createSketchStyle,
153
+ createFeatureStyle
154
+ }
155
+ }
@@ -0,0 +1,143 @@
1
+ import Point from 'ol/geom/Point.js'
2
+ import LineString from 'ol/geom/LineString.js'
3
+ import Feature from 'ol/Feature.js'
4
+ import { createStyles } from './styles.js'
5
+ import { SIZES } from '../defaults.js'
6
+ import { polygonFeature, lineFeature } from '../__helpers__/harness.js'
7
+
8
+ const colors = {
9
+ editStroke: '#e5',
10
+ editFill: '#ef',
11
+ editVertex: '#ev',
12
+ editMidpoint: '#em',
13
+ editActive: '#ea',
14
+ editHalo: '#eh',
15
+ invalidStroke: '#is',
16
+ splitValid: '#sv',
17
+ splitInvalid: '#si',
18
+ shapeStroke: '#ss',
19
+ shapeFill: '#sf',
20
+ strokeWidth: 3,
21
+ mapStyleId: 'road'
22
+ }
23
+
24
+ const styles = createStyles(colors)
25
+
26
+ // Records each canvas fill as { radius, fillStyle } so ring order/colours can be asserted
27
+ const fakeCanvas = () => {
28
+ const fills = []
29
+ const ctx = {
30
+ fillStyle: null,
31
+ save: jest.fn(),
32
+ restore: jest.fn(),
33
+ beginPath: jest.fn(),
34
+ arc: jest.fn((cx, cy, radius) => { ctx.lastArc = { cx, cy, radius } }),
35
+ fill: jest.fn(() => fills.push({ ...ctx.lastArc, fillStyle: ctx.fillStyle }))
36
+ }
37
+ return { ctx, fills }
38
+ }
39
+
40
+ describe('handle styles', () => {
41
+ test('vertex and midpoint handles use the configured radii and colours', () => {
42
+ expect(styles.vertexStyle.getImage().getRadius()).toBe(SIZES.vertexRadius)
43
+ expect(styles.vertexStyle.getImage().getFill().getColor()).toBe(colors.editVertex)
44
+ expect(styles.midpointStyle.getImage().getRadius()).toBe(SIZES.midpointRadius)
45
+ expect(styles.midpointStyle.getImage().getFill().getColor()).toBe(colors.editMidpoint)
46
+ })
47
+
48
+ test('selected handles render three concentric rings in one canvas pass, scaled by pixelRatio', () => {
49
+ const { ctx, fills } = fakeCanvas()
50
+ styles.selectedVertexStyle.getRenderer()([10, 20], { context: ctx, pixelRatio: 2 })
51
+ expect(fills).toEqual([
52
+ { cx: 10, cy: 20, radius: (SIZES.vertexHaloRadius + 3) * 2, fillStyle: colors.editActive },
53
+ { cx: 10, cy: 20, radius: SIZES.vertexHaloRadius * 2, fillStyle: colors.editHalo },
54
+ { cx: 10, cy: 20, radius: SIZES.vertexRadius * 2, fillStyle: colors.editVertex }
55
+ ])
56
+
57
+ const midpoint = fakeCanvas()
58
+ styles.selectedMidpointStyle.getRenderer()([0, 0], { context: midpoint.ctx, pixelRatio: 1 })
59
+ expect(midpoint.fills.map(f => f.radius)).toEqual(
60
+ [SIZES.midpointHaloRadius + 3, SIZES.midpointHaloRadius, SIZES.midpointRadius])
61
+ expect(midpoint.fills[2].fillStyle).toBe(colors.editMidpoint)
62
+ })
63
+
64
+ test('the edited feature gets the edit stroke and fill', () => {
65
+ expect(styles.editFeatureStyle.getStroke().getColor()).toBe(colors.editStroke)
66
+ expect(styles.editFeatureStyle.getFill().getColor()).toBe(colors.editFill)
67
+ })
68
+
69
+ test('the invalid edited feature gets a dashed stroke and no fill', () => {
70
+ expect(styles.editFeatureStyleInvalid.getStroke().getColor()).toBe(colors.invalidStroke)
71
+ expect(styles.editFeatureStyleInvalid.getStroke().getLineDash()).toEqual([2, 4])
72
+ expect(styles.editFeatureStyleInvalid.getFill()).toBeNull()
73
+ })
74
+ })
75
+
76
+ describe('sketch styles while drawing', () => {
77
+ const polygonStyleFn = styles.createSketchStyle('Polygon')
78
+
79
+ test('the cursor-follow point renders nothing; the companion line gets stroke only', () => {
80
+ expect(polygonStyleFn(new Feature(new Point([0, 0])))).toEqual([])
81
+ expect(polygonStyleFn(new Feature(new LineString([[0, 0], [1, 1]])))).toHaveLength(1)
82
+ })
83
+
84
+ test('the invalid sketch renders a dashed line in the invalid colour with no fill', () => {
85
+ const invalidStyleFn = styles.createSketchStyle('Polygon', true)
86
+ const [lineStyle] = invalidStyleFn(new Feature(new LineString([[0, 0], [1, 1]])))
87
+ expect(lineStyle.getStroke().getColor()).toBe(colors.invalidStroke)
88
+ expect(lineStyle.getStroke().getLineDash()).toEqual([2, 4])
89
+ expect(lineStyle.getFill()).toBeNull()
90
+ })
91
+
92
+ test('a splitter-tagged sketch renders the split colours (valid solid, invalid dashed), overriding invalid', () => {
93
+ const valid = lineFeature([[0, 0], [1, 1]])
94
+ valid.set('splitter', 'valid')
95
+ const [validStyle] = styles.createSketchStyle('LineString')(valid)
96
+ expect(validStyle.getStroke().getColor()).toBe(colors.splitValid)
97
+ expect(validStyle.getStroke().getLineDash()).toBeNull()
98
+
99
+ const invalid = lineFeature([[0, 0], [1, 1]])
100
+ invalid.set('splitter', 'invalid')
101
+ const [invalidStyle] = styles.createSketchStyle('LineString', true)(invalid)
102
+ expect(invalidStyle.getStroke().getColor()).toBe(colors.splitInvalid)
103
+ expect(invalidStyle.getStroke().getLineDash()).toEqual([2, 4])
104
+ })
105
+
106
+ test('placed vertices render with the shared vertex image on a reused MultiPoint', () => {
107
+ const sketch = polygonFeature([[0, 0], [10, 0], [5, 5], [0, 0]]) // 2 placed + rubber + closing
108
+ const [, vertexStyle] = polygonStyleFn(sketch)
109
+ expect(vertexStyle.getImage()).toBe(styles.vertexStyle.getImage())
110
+ const geometry = vertexStyle.getGeometry()(sketch)
111
+ expect(geometry.getCoordinates()).toEqual([[0, 0], [10, 0]])
112
+ expect(vertexStyle.getGeometry()(sketch)).toBe(geometry) // instance reused per render
113
+
114
+ const empty = lineFeature([[9, 9]]) // rubber band only — nothing placed
115
+ expect(styles.createSketchStyle('LineString')(empty)[1].getGeometry()(empty)).toBeNull()
116
+ })
117
+ })
118
+
119
+ describe('createFeatureStyle (inactive shapes)', () => {
120
+ const styleFor = (properties) => styles.createFeatureStyle()({ getProperties: () => properties })[0]
121
+
122
+ test('falls back to the configured shape colours', () => {
123
+ const style = styleFor({})
124
+ expect(style.getStroke().getColor()).toBe(colors.shapeStroke)
125
+ expect(style.getStroke().getWidth()).toBe(colors.strokeWidth)
126
+ expect(style.getFill().getColor()).toBe(colors.shapeFill)
127
+ })
128
+
129
+ test('per-feature properties override, and map-style-specific properties win overall', () => {
130
+ const style = styleFor({ stroke: '#f1', fill: '#f2', strokeWidth: 7, strokeRoad: '#r1', fillRoad: '#r2' })
131
+ expect(style.getStroke().getColor()).toBe('#r1')
132
+ expect(style.getFill().getColor()).toBe('#r2')
133
+ expect(style.getStroke().getWidth()).toBe(7)
134
+
135
+ expect(styleFor({ stroke: '#f1' }).getStroke().getColor()).toBe('#f1')
136
+ })
137
+
138
+ test('without a map style id only the generic properties apply', () => {
139
+ const plain = createStyles({ ...colors, mapStyleId: null })
140
+ const style = plain.createFeatureStyle()({ getProperties: () => ({ strokeRoad: '#r1' }) })[0]
141
+ expect(style.getStroke().getColor()).toBe(colors.shapeStroke)
142
+ })
143
+ })
@@ -0,0 +1 @@
1
+ export { COLORS, SIZES, TOLERANCES, KEYBOARD, MAP_SIZE_SCALES } from '../../defaults.js'
@@ -0,0 +1,272 @@
1
+ import Draw from 'ol/interaction/Draw.js'
2
+ import { noModifierKeys } from 'ol/events/condition.js'
3
+ import { createDrawInput } from './drawInput.js'
4
+ import { getPlacedSketchCoords, getLastPlacedSketchCoord } from '../utils/sketchHelpers.js'
5
+ import { TOLERANCES } from '../defaults.js'
6
+ import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
7
+ import { STYLES_CHANGED_EVENT } from '../core/internalEvents.js'
8
+ import { attemptPlacement, MODE_BY_GEOMETRY } from '../../../validation/validateGeometry.js'
9
+ import { MIN_VERTICES } from '../../../validation/rules.js'
10
+ import { createLiveStroke } from '../../../validation/liveStroke.js'
11
+ import { createLiveDrawChecks } from '../../../validation/liveDrawChecks.js'
12
+
13
+ const canFinish = (geometryType, sketchFeature) => {
14
+ if (!sketchFeature) { return false }
15
+ return getPlacedSketchCoords(sketchFeature.getGeometry()).length >= MIN_VERTICES[geometryType]
16
+ }
17
+
18
+ const DUPLICATE_TOLERANCE_PX = 2
19
+
20
+ // Blocks clicks with modifier keys, vetoed placements (the vertex never appears —
21
+ // see canPlaceVertex in createDrawMode), and duplicate clicks on the last placed
22
+ // vertex while the shape is not yet finishable (once finishable, clicking the last
23
+ // vertex is OL's finish gesture).
24
+ export const buildCondition = (map, geometryType, getSketchFeature, canPlaceVertex) => (e) => {
25
+ if (!noModifierKeys(e)) { return false }
26
+ const sf = getSketchFeature()
27
+ if (!canPlaceVertex(e.coordinate)) { return false }
28
+ if (!sf || canFinish(geometryType, sf)) { return true }
29
+ const prev = getLastPlacedSketchCoord(sf.getGeometry())
30
+ if (!prev) { return true }
31
+ const pp = map.getPixelFromCoordinate(prev)
32
+ if (!pp) { return true }
33
+ const dx = e.pixel[0] - pp[0]; const dy = e.pixel[1] - pp[1]
34
+ return dx * dx + dy * dy > DUPLICATE_TOLERANCE_PX * DUPLICATE_TOLERANCE_PX
35
+ }
36
+
37
+ // During drawing the sketch has trailing rubber-band (+ closing, for polygons) coords.
38
+ const TRAILING_COORDS = { Polygon: 2, LineString: 1 }
39
+
40
+ // The placed-only GeoJSON of an in-progress sketch (lon/lat), dropping the trailing
41
+ // cursor-tracking coords so validation tests just the committed vertices.
42
+ const placedFeatureGeoJSON = (store, sketchFeature) => {
43
+ const gj = store.toGeoJSON(sketchFeature)
44
+ const type = gj.geometry.type
45
+ const trailing = TRAILING_COORDS[type]
46
+ const ring = type === 'Polygon' ? gj.geometry.coordinates[0] : gj.geometry.coordinates
47
+ const placed = ring.slice(0, -trailing)
48
+ const geometry = type === 'Polygon'
49
+ ? { type: 'Polygon', coordinates: [placed] }
50
+ : { type: 'LineString', coordinates: placed }
51
+ return { type: 'Feature', geometry, properties: gj.properties }
52
+ }
53
+
54
+ // Attempt a placement (mouse click, crosshair tap, Enter) through the shared
55
+ // hard rules + user callback. On a veto the vertex never appears and a
56
+ // PLACEMENT_BLOCKED event carries the reason.
57
+ export const buildCanPlaceVertex = ({ manager, geometryType, getSketch }) => (coordinate) => {
58
+ const sketch = getSketch()
59
+ const result = attemptPlacement({
60
+ placed: sketch ? getPlacedSketchCoords(sketch.getGeometry()) : [],
61
+ point: coordinate,
62
+ geometryType,
63
+ onGeometryChange: manager._geometryValidator
64
+ })
65
+ if (!result.valid) {
66
+ manager.emit(ADAPTER_EVENTS.PLACEMENT_BLOCKED, result.blocked)
67
+ }
68
+ return result.valid
69
+ }
70
+
71
+ // Build the displayed-geometry payload (placed vertices + cursor) for the live
72
+ // stroke check from the current sketch, plus the placed-vertex count for the
73
+ // minimum-vertices threshold.
74
+ const displayedSketch = (geometryType, sketch) => {
75
+ const geom = sketch.getGeometry()
76
+ const coords = geom.getCoordinates()
77
+ const ring = geometryType === 'Polygon' ? (coords[0] ?? []) : coords
78
+ const geometry = geometryType === 'Polygon'
79
+ ? { type: 'Polygon', coordinates: [ring] }
80
+ : { type: 'LineString', coordinates: ring }
81
+ return { feature: { type: 'Feature', geometry }, numVertices: getPlacedSketchCoords(geom).length }
82
+ }
83
+
84
+ // Commit a finished sketch to the store under the requested id and emit CREATE.
85
+ const finalizeDrawnFeature = (manager, olFeature, featureId, properties) => {
86
+ olFeature.setId(String(featureId))
87
+ olFeature.setProperties(properties)
88
+ manager.store.source.addFeature(olFeature)
89
+ manager.emit(ADAPTER_EVENTS.CREATE, manager.store.toGeoJSON(olFeature))
90
+ }
91
+
92
+ // Wire the OL Draw interaction's lifecycle: track the sketch, react to every sketch
93
+ // geometry change (vertex count + live validity), finalise on end, and report a
94
+ // cancel on abort.
95
+ const attachDrawListeners = (drawInteraction, { manager, featureId, properties, onStart, onSketchChange }) => {
96
+ drawInteraction.on('drawstart', (e) => {
97
+ onStart(e.feature)
98
+ e.feature.getGeometry().on('change', onSketchChange)
99
+ })
100
+ drawInteraction.on('drawend', (e) => finalizeDrawnFeature(manager, e.feature, featureId, properties))
101
+ drawInteraction.on('drawabort', () => { manager.emit(ADAPTER_EVENTS.CANCEL) })
102
+ }
103
+
104
+ // Tracks placed-vertex count and emits VERTEX_CHANGE plus, on each genuine placement,
105
+ // a commit-level GEOMETRY_CHANGE ('commit-add') for validation. Deferred a tick so a
106
+ // rejection's revert runs after the current click settles.
107
+ const createVertexTracker = (manager, getSketch) => {
108
+ let lastPlacedCount = 0
109
+ const emit = (phase, vertexIndex) => {
110
+ setTimeout(() => {
111
+ const sketch = getSketch()
112
+ if (!sketch) { return }
113
+ manager.emit(ADAPTER_EVENTS.GEOMETRY_CHANGE, { feature: placedFeatureGeoJSON(manager.store, sketch), phase, vertexIndex })
114
+ }, 0)
115
+ }
116
+ return {
117
+ resetCount: () => { lastPlacedCount = 0 },
118
+ updateVertexCount: () => {
119
+ const sketch = getSketch()
120
+ if (!sketch) { return }
121
+ const placed = getPlacedSketchCoords(sketch.getGeometry()).length
122
+ manager.emit(ADAPTER_EVENTS.VERTEX_CHANGE, { numVertices: placed })
123
+ if (placed > lastPlacedCount) { emit('commit-add', placed - 1) }
124
+ lastPlacedCount = placed
125
+ },
126
+ // An undo commits a vertex removal, so it must re-validate like any other
127
+ // commit — otherwise the Done gate goes stale.
128
+ emitUndoValidation: () => {
129
+ const sketch = getSketch()
130
+ if (!sketch) { return }
131
+ emit('commit-delete', getPlacedSketchCoords(sketch.getGeometry()).length)
132
+ }
133
+ }
134
+ }
135
+
136
+ // The mode interface consumed by OLDrawManager.
137
+ const buildDrawModeApi = ({ map, manager, drawInteraction, input, geometryType, getSketch, updateVertexCount, emitUndoValidation, onStylesChanged, clearSketch, setInvalid, liveStroke, liveDrawChecks }) => ({
138
+ done () {
139
+ if (canFinish(geometryType, getSketch())) { drawInteraction.finishDrawing() }
140
+ },
141
+ cancel () { drawInteraction.abortDrawing() },
142
+ undo () { drawInteraction.removeLastPoint(); updateVertexCount(); emitUndoValidation() },
143
+ setInvalid,
144
+ setInterfaceType (type) { input.setInterfaceType(type) },
145
+ // Tags the sketch feature so createSketchStyle can pick the split-specific
146
+ // colour (see styles.js); no-op before any sketch exists.
147
+ setDrawingPreviewProperty (property, value) {
148
+ const sketch = getSketch()
149
+ if (!sketch) { return }
150
+ sketch.set(property, value)
151
+ drawInteraction.overlay_.changed()
152
+ },
153
+ destroy () {
154
+ liveStroke.destroy()
155
+ liveDrawChecks.destroy()
156
+ manager.off(STYLES_CHANGED_EVENT, onStylesChanged)
157
+ // Emit the final interfaceType so crosshair visibility is correct on exit.
158
+ manager.emit(ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE, { interfaceType: input.getInterfaceType() })
159
+ input.destroy()
160
+ map.removeInteraction(drawInteraction)
161
+ clearSketch()
162
+ }
163
+ })
164
+
165
+ // Build the touch/keyboard draw input for the interaction.
166
+ const buildDrawInput = ({ drawInteraction, options, geometryType, getSketch, updateVertexCount, emitUndoValidation, canPlaceVertex }) =>
167
+ createDrawInput({
168
+ drawInteraction,
169
+ options: {
170
+ container: options.container,
171
+ interfaceType: options.interfaceType,
172
+ addVertexButtonId: options.addVertexButtonId,
173
+ mapProvider: options.mapProvider,
174
+ snap: options.snap,
175
+ onUndo: () => { drawInteraction.removeLastPoint(); updateVertexCount(); emitUndoValidation() },
176
+ canFinish: () => canFinish(geometryType, getSketch()),
177
+ canPlace: canPlaceVertex
178
+ }
179
+ })
180
+
181
+ export const createDrawMode = ({ map, manager, options }) => {
182
+ const { geometryType, featureId, properties = {} } = options
183
+
184
+ let sketchFeature = null
185
+ let invalid = false
186
+ let currentSketchStyle = manager.styles.createSketchStyle(geometryType)
187
+ const { updateVertexCount, resetCount, emitUndoValidation } = createVertexTracker(manager, () => sketchFeature)
188
+
189
+ const canPlaceVertex = buildCanPlaceVertex({ manager, geometryType, getSketch: () => sketchFeature })
190
+
191
+ const drawInteraction = new Draw({
192
+ type: geometryType,
193
+ style: (feature) => currentSketchStyle(feature),
194
+ stopClick: true,
195
+ snapTolerance: TOLERANCES.snapRadius,
196
+ condition: buildCondition(map, geometryType, () => sketchFeature, canPlaceVertex),
197
+ // Block finish gestures (double-click / click-to-close) while the shape is invalid.
198
+ finishCondition: () => manager._geometryValid !== false
199
+ })
200
+ map.addInteraction(drawInteraction)
201
+
202
+ const setSketchInvalid = (next) => {
203
+ invalid = next
204
+ currentSketchStyle = manager.styles.createSketchStyle(geometryType, invalid)
205
+ drawInteraction.overlay_.changed()
206
+ }
207
+ // liveStroke stays a real createLiveStroke instance (not just a set() sink)
208
+ // since edit mode drives its own separate instance the same way — kept here
209
+ // for consistency with that pattern, even though draw mode only ever calls
210
+ // set() on it (see liveDrawChecks.js, which is flip-guarded itself). There's
211
+ // no equivalent cross-mode instance for Add-point, so that verdict emits directly.
212
+ const liveStroke = createLiveStroke({ onChange: setSketchInvalid })
213
+ // Computes both verdicts from a SINGLE throttled call to the user's callback
214
+ // per sketch change (see liveDrawChecks.js for why that matters).
215
+ const liveDrawChecks = createLiveDrawChecks({
216
+ onStrokeChange: (nextInvalid, reason) => liveStroke.set(nextInvalid, reason),
217
+ onPlaceChange: (vetoed, reason) => manager.emit(ADAPTER_EVENTS.CAN_PLACE_CHANGE, { canPlace: !vetoed, reason })
218
+ })
219
+ const liveMode = MODE_BY_GEOMETRY[geometryType]
220
+ const updateLiveValidity = () => {
221
+ if (!sketchFeature) { return }
222
+ const { feature, numVertices } = displayedSketch(geometryType, sketchFeature)
223
+ liveDrawChecks.update({ feature, context: { mode: liveMode }, numVertices, onGeometryChange: manager._geometryValidator })
224
+ }
225
+
226
+ // Update sketch style when map style changes
227
+ const onStylesChanged = () => {
228
+ currentSketchStyle = manager.styles.createSketchStyle(geometryType, invalid)
229
+ drawInteraction.overlay_.changed()
230
+ }
231
+ manager.on(STYLES_CHANGED_EVENT, onStylesChanged)
232
+ // OL internal: overlay_ is the private VectorLayer used for the sketch geometry.
233
+ // updateWhileAnimating_ forces per-frame redraws during view animations (keyboard pan).
234
+ // Without this, geom.setCoordinates() calls are ignored while the ANIMATING hint is set.
235
+ // Check ol/interaction/Draw.js and ol/layer/BaseVector.js if this breaks after an OL upgrade.
236
+ drawInteraction.overlay_.updateWhileAnimating_ = true
237
+
238
+ attachDrawListeners(drawInteraction, {
239
+ manager,
240
+ featureId,
241
+ properties,
242
+ onStart: (f) => { sketchFeature = f; resetCount() },
243
+ onSketchChange: () => { updateVertexCount(); updateLiveValidity() }
244
+ })
245
+
246
+ const input = buildDrawInput({
247
+ drawInteraction,
248
+ options,
249
+ geometryType,
250
+ getSketch: () => sketchFeature,
251
+ updateVertexCount,
252
+ emitUndoValidation,
253
+ canPlaceVertex
254
+ })
255
+
256
+ return buildDrawModeApi({
257
+ map,
258
+ manager,
259
+ drawInteraction,
260
+ input,
261
+ geometryType,
262
+ getSketch: () => sketchFeature,
263
+ updateVertexCount,
264
+ emitUndoValidation,
265
+ onStylesChanged,
266
+ clearSketch: () => { sketchFeature = null },
267
+ // External writes go through the controller so its cache mirrors the style.
268
+ setInvalid: (next) => liveStroke.set(next),
269
+ liveStroke,
270
+ liveDrawChecks
271
+ })
272
+ }