@defra/interactive-map 0.0.26-alpha → 0.0.27-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 (92) hide show
  1. package/dist/esm/im-core.js +1 -1
  2. package/dist/esm/im-shell.js +1 -1
  3. package/dist/umd/im-core.js +1 -1
  4. package/dist/umd/index.js +1 -1
  5. package/docs/assets/css/docusaurus.scss +4 -0
  6. package/package.json +1 -1
  7. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  8. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  9. package/plugins/beta/draw-ml/src/defaults.js +14 -14
  10. package/plugins/beta/draw-ml/src/mapboxDraw.js +5 -0
  11. package/plugins/beta/draw-ml/src/mapboxSnap.js +1 -1
  12. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +10 -2
  13. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +2 -0
  14. package/plugins/beta/draw-ml/src/reducer.js +4 -1
  15. package/plugins/beta/draw-ml/src/styles.js +42 -39
  16. package/plugins/beta/draw-ol/dist/css/index.css +13 -0
  17. package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -0
  18. package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -0
  19. package/plugins/beta/draw-ol/dist/esm/geometryHelpers.js +1 -0
  20. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -0
  21. package/plugins/beta/draw-ol/dist/esm/index.js +1 -0
  22. package/plugins/beta/draw-ol/src/DrawInit.jsx +67 -0
  23. package/plugins/beta/draw-ol/src/api/addFeature.js +29 -0
  24. package/plugins/beta/draw-ol/src/api/deleteFeature.js +11 -0
  25. package/plugins/beta/draw-ol/src/api/editFeature.js +50 -0
  26. package/plugins/beta/draw-ol/src/api/newLine.js +44 -0
  27. package/plugins/beta/draw-ol/src/api/newPolygon.js +44 -0
  28. package/plugins/beta/draw-ol/src/core/OLDrawManager.js +150 -0
  29. package/plugins/beta/draw-ol/src/core/featureStore.js +65 -0
  30. package/plugins/beta/draw-ol/src/core/styles.js +98 -0
  31. package/plugins/beta/draw-ol/src/core/undoStack.js +31 -0
  32. package/plugins/beta/draw-ol/src/defaults.js +16 -0
  33. package/plugins/beta/draw-ol/src/draw/DrawMode.js +113 -0
  34. package/plugins/beta/draw-ol/src/draw/drawInput.js +199 -0
  35. package/plugins/beta/draw-ol/src/draw.scss +17 -0
  36. package/plugins/beta/draw-ol/src/edit/EditMode.js +424 -0
  37. package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +227 -0
  38. package/plugins/beta/draw-ol/src/edit/midpointLayer.js +57 -0
  39. package/plugins/beta/draw-ol/src/edit/touchHandler.js +171 -0
  40. package/plugins/beta/draw-ol/src/edit/undoOps.js +95 -0
  41. package/plugins/beta/draw-ol/src/edit/vertexHitTest.js +72 -0
  42. package/plugins/beta/draw-ol/src/edit/vertexLayer.js +49 -0
  43. package/plugins/beta/draw-ol/src/edit/vertexOps.js +99 -0
  44. package/plugins/beta/draw-ol/src/events.js +114 -0
  45. package/plugins/beta/draw-ol/src/index.js +12 -0
  46. package/plugins/beta/draw-ol/src/manifest.js +121 -0
  47. package/plugins/beta/draw-ol/src/olDraw.js +38 -0
  48. package/plugins/beta/draw-ol/src/reducer.js +41 -0
  49. package/plugins/beta/draw-ol/src/snap/snapEngine.js +109 -0
  50. package/plugins/beta/draw-ol/src/snap/snapGeometry.js +200 -0
  51. package/plugins/beta/draw-ol/src/snap/snapIndicator.js +81 -0
  52. package/plugins/beta/draw-ol/src/snap/snapInteraction.js +56 -0
  53. package/plugins/beta/draw-ol/src/snap/snapManager.js +90 -0
  54. package/plugins/beta/draw-ol/src/utils/flattenStyleProperties.js +47 -0
  55. package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +97 -0
  56. package/plugins/beta/draw-ol/src/utils/olCoords.js +35 -0
  57. package/plugins/beta/draw-ol/src/utils/resolveColors.js +39 -0
  58. package/plugins/beta/draw-ol/src/utils/spatial.js +28 -0
  59. package/plugins/beta/draw-ol/src/utils/touchTarget.js +61 -0
  60. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  61. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  62. package/plugins/interact/src/defaults.js +1 -0
  63. package/plugins/interact/src/hooks/useInteractionHandlers.js +150 -43
  64. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +240 -8
  65. package/plugins/interact/src/reducer.js +7 -0
  66. package/plugins/interact/src/reducer.test.js +16 -0
  67. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  68. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  69. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  70. package/providers/beta/openlayers/src/appEvents.js +9 -13
  71. package/providers/beta/openlayers/src/appEvents.test.js +25 -17
  72. package/providers/beta/openlayers/src/mapEvents.js +32 -16
  73. package/providers/beta/openlayers/src/mapEvents.test.js +43 -10
  74. package/providers/beta/openlayers/src/openlayersProvider.js +45 -26
  75. package/providers/beta/openlayers/src/openlayersProvider.test.js +12 -34
  76. package/providers/beta/openlayers/src/utils/highlightFeatures.js +245 -0
  77. package/providers/beta/openlayers/src/utils/hoverCursor.js +65 -0
  78. package/providers/beta/openlayers/src/utils/hoverCursor.test.js +235 -0
  79. package/providers/beta/openlayers/src/utils/openLayersFixes.js +11 -0
  80. package/providers/beta/openlayers/src/utils/openLayersFixes.test.js +53 -0
  81. package/providers/beta/openlayers/src/utils/queryFeatures.js +77 -0
  82. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +181 -0
  83. package/providers/beta/openlayers/src/utils/tileLayers.js +19 -1
  84. package/providers/beta/openlayers/src/utils/tileLayers.test.js +74 -3
  85. package/providers/beta/openlayers/src/utils/vtTileFragments.js +86 -0
  86. package/rollup.esm.mjs +6 -0
  87. package/src/App/controls/keyboardActions.js +2 -2
  88. package/src/InteractiveMap/InteractiveMap.js +38 -10
  89. package/src/InteractiveMap/InteractiveMap.test.js +282 -356
  90. package/src/InteractiveMap/historyManager.js +7 -0
  91. package/src/InteractiveMap/historyManager.test.js +11 -0
  92. package/webpack.dev.mjs +3 -1
@@ -0,0 +1,44 @@
1
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
2
+
3
+ /**
4
+ * Programmatically start drawing a new line.
5
+ *
6
+ * @param {object} context - plugin context
7
+ * @param {string} featureId - unique ID for this feature
8
+ * @param {object} options - { snapLayers, stroke, fill, strokeWidth, properties }
9
+ */
10
+ export const newLine = (
11
+ { appState, appConfig, mapState, pluginState, mapProvider, services },
12
+ featureId,
13
+ options = {}
14
+ ) => {
15
+ const { dispatch } = pluginState
16
+ const { draw } = mapProvider
17
+ const { eventBus } = services
18
+
19
+ if (!draw) {
20
+ return
21
+ }
22
+
23
+ eventBus.emit('draw:started', { mode: 'draw_line' })
24
+
25
+ const { stroke, fill, strokeWidth, properties: customProperties, ...modeOptions } = options
26
+ const properties = {
27
+ ...customProperties,
28
+ ...flattenStyleProperties({ stroke, fill, strokeWidth })
29
+ }
30
+
31
+ draw.changeMode('draw_line', {
32
+ container: appState.layoutRefs.viewportRef.current,
33
+ interfaceType: appState.interfaceType,
34
+ addVertexButtonId: `${appConfig.id}-draw-add-point`,
35
+ featureId,
36
+ geometryType: 'LineString',
37
+ properties,
38
+ mapProvider,
39
+ crossHair: mapState.crossHair,
40
+ ...modeOptions
41
+ })
42
+
43
+ dispatch({ type: 'SET_MODE', payload: 'draw_line' })
44
+ }
@@ -0,0 +1,44 @@
1
+ import { flattenStyleProperties } from '../utils/flattenStyleProperties.js'
2
+
3
+ /**
4
+ * Programmatically start drawing a new polygon.
5
+ *
6
+ * @param {object} context - plugin context
7
+ * @param {string} featureId - unique ID for this feature
8
+ * @param {object} options - { snapLayers, stroke, fill, strokeWidth, properties }
9
+ */
10
+ export const newPolygon = (
11
+ { appState, appConfig, mapState, pluginState, mapProvider, services },
12
+ featureId,
13
+ options = {}
14
+ ) => {
15
+ const { dispatch } = pluginState
16
+ const { draw } = mapProvider
17
+ const { eventBus } = services
18
+
19
+ if (!draw) {
20
+ return
21
+ }
22
+
23
+ eventBus.emit('draw:started', { mode: 'draw_polygon' })
24
+
25
+ const { stroke, fill, strokeWidth, properties: customProperties, ...modeOptions } = options
26
+ const properties = {
27
+ ...customProperties,
28
+ ...flattenStyleProperties({ stroke, fill, strokeWidth })
29
+ }
30
+
31
+ draw.changeMode('draw_polygon', {
32
+ container: appState.layoutRefs.viewportRef.current,
33
+ interfaceType: appState.interfaceType,
34
+ addVertexButtonId: `${appConfig.id}-draw-add-point`,
35
+ featureId,
36
+ geometryType: 'Polygon',
37
+ properties,
38
+ mapProvider,
39
+ crossHair: mapState.crossHair,
40
+ ...modeOptions
41
+ })
42
+
43
+ dispatch({ type: 'SET_MODE', payload: 'draw_polygon' })
44
+ }
@@ -0,0 +1,150 @@
1
+ import VectorLayer from 'ol/layer/Vector.js'
2
+ import { createFeatureStore } from './featureStore.js'
3
+ import { createUndoStack } from './undoStack.js'
4
+ import { createStyles } from './styles.js'
5
+ import { resolveColors } from '../utils/resolveColors.js'
6
+ import { createSnapManager } from '../snap/snapManager.js'
7
+ import { DEFAULTS } from '../defaults.js'
8
+
9
+ /**
10
+ * Mode machine for the OL draw plugin.
11
+ *
12
+ * Owns the VectorSource/Layer, undo stack, and current mode instance.
13
+ * Exposes a minimal on/off/emit event bus for internal plugin communication
14
+ * (separate from the public eventBus used for consumer-facing events).
15
+ *
16
+ * Consumer-facing events are always emitted via eventBus by events.js after
17
+ * listening to the manager's internal events.
18
+ */
19
+ export class OLDrawManager {
20
+ constructor (map, pluginConfig = {}) {
21
+ this._map = map
22
+ this._pluginConfig = pluginConfig
23
+ this._mode = 'disabled'
24
+ this._modeInstance = null
25
+ this._listeners = new Map()
26
+
27
+ this.store = createFeatureStore()
28
+ this.undoStack = createUndoStack((length) => this.emit('undochange', length))
29
+
30
+ this.colors = resolveColors(null, pluginConfig)
31
+ this.styles = createStyles(this.colors)
32
+ this.snap = createSnapManager(map, pluginConfig.snapLayers ?? null, this.colors, pluginConfig.snapRadius ?? DEFAULTS.snapRadius)
33
+
34
+ this._layer = new VectorLayer({
35
+ source: this.store.source,
36
+ style: this.styles.createFeatureStyle(),
37
+ zIndex: 100
38
+ })
39
+ this._layer.set('layerId', 'draw')
40
+ map.addLayer(this._layer)
41
+ }
42
+
43
+ // --- Color / style updates ---
44
+
45
+ setMapStyle (mapStyle) {
46
+ this.colors = resolveColors(mapStyle, this._pluginConfig)
47
+ this.styles = createStyles(this.colors)
48
+ this._layer.setStyle(this.styles.createFeatureStyle())
49
+ this.store.source.changed()
50
+ this.snap?.updateColors(this.colors)
51
+ this.emit('styleschanged', this.styles)
52
+ }
53
+
54
+ // --- Internal event bus ---
55
+
56
+ on (type, handler) {
57
+ if (!this._listeners.has(type)) {
58
+ this._listeners.set(type, new Set())
59
+ }
60
+ this._listeners.get(type).add(handler)
61
+ }
62
+
63
+ off (type, handler) {
64
+ this._listeners.get(type)?.delete(handler)
65
+ }
66
+
67
+ emit (type, detail) {
68
+ this._listeners.get(type)?.forEach(h => h(detail))
69
+ }
70
+
71
+ // --- Mode machine ---
72
+
73
+ async changeMode (modeName, options = {}) {
74
+ this._modeInstance?.destroy()
75
+ this._modeInstance = null
76
+ this._mode = modeName
77
+
78
+ const modeOptions = { ...options, snap: this.snap }
79
+
80
+ if (modeName === 'draw_polygon' || modeName === 'draw_line') {
81
+ const { createDrawMode } = await import('../draw/DrawMode.js')
82
+ this._modeInstance = createDrawMode({ map: this._map, manager: this, options: modeOptions })
83
+ } else if (modeName === 'edit_vertex') {
84
+ const { createEditMode } = await import('../edit/EditMode.js')
85
+ this._modeInstance = createEditMode({ map: this._map, manager: this, options: modeOptions })
86
+ } else {
87
+ // disabled — no mode instance needed
88
+ }
89
+ // Reattach snap interaction after mode's interactions are added so it
90
+ // processes pointermove first (OL: last-added interaction = first to handle events).
91
+ this.snap?.reattach()
92
+ }
93
+
94
+ getMode () {
95
+ return this._mode
96
+ }
97
+
98
+ // --- High-level operations called by events.js ---
99
+
100
+ done () {
101
+ this._modeInstance?.done()
102
+ }
103
+
104
+ cancel () {
105
+ this._modeInstance?.cancel()
106
+ this.changeMode('disabled')
107
+ }
108
+
109
+ undo () {
110
+ this._modeInstance?.undo()
111
+ }
112
+
113
+ deleteVertex () {
114
+ this._modeInstance?.deleteVertex()
115
+ }
116
+
117
+ setInterfaceType (type) {
118
+ this._modeInstance?.setInterfaceType?.(type)
119
+ }
120
+
121
+ // --- Feature store delegation ---
122
+
123
+ get (id) {
124
+ return this.store.get(id)
125
+ }
126
+
127
+ add (geojsonFeature) {
128
+ return this.store.add(geojsonFeature)
129
+ }
130
+
131
+ delete (id) {
132
+ return this.store.remove(id)
133
+ }
134
+
135
+ deleteAll () {
136
+ return this.store.clear()
137
+ }
138
+
139
+ // --- Cleanup ---
140
+
141
+ remove () {
142
+ this._modeInstance?.destroy()
143
+ this._modeInstance = null
144
+ this.snap?.destroy()
145
+ this.snap = null
146
+ this.store.clear()
147
+ this._map.removeLayer(this._layer)
148
+ this._listeners.clear()
149
+ }
150
+ }
@@ -0,0 +1,65 @@
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 a feature by ID. */
43
+ remove (id) {
44
+ const feature = this.getOL(id)
45
+ if (feature) {
46
+ source.removeFeature(feature)
47
+ }
48
+ },
49
+
50
+ /** Remove all features. */
51
+ clear () {
52
+ source.clear()
53
+ },
54
+
55
+ /** Convert an OL Feature to a GeoJSON object. */
56
+ toGeoJSON (olFeature) {
57
+ return format.writeFeatureObject(olFeature)
58
+ },
59
+
60
+ /** Convert a GeoJSON object to an OL Feature (no side effects). */
61
+ fromGeoJSON (geojsonFeature) {
62
+ return format.readFeature(geojsonFeature)
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,98 @@
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
+
6
+ const selectedVertexRadii = { outer: 11, mid: 8, inner: 6 }
7
+ const selectedMidpointRadii = { outer: 9, mid: 6, inner: 4 }
8
+
9
+ const fillArc = (ctx, cx, cy, radius, fillStyle) => {
10
+ ctx.beginPath()
11
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2)
12
+ ctx.fillStyle = fillStyle
13
+ ctx.fill()
14
+ }
15
+
16
+ // Custom renderer draws all arcs at the same (cx,cy) so concentric rings never
17
+ // drift at fractional CSS scales (e.g. 1.5×) the way separate drawImage calls can.
18
+ const makeRingRenderer = ({ outer, mid, inner }, colors, innerKey) => (pixelCoordinates, state) => {
19
+ const ctx = state.context
20
+ const pr = state.pixelRatio
21
+ const [cx, cy] = /** @type {number[]} */ (pixelCoordinates)
22
+ ctx.save()
23
+ fillArc(ctx, cx, cy, outer * pr, colors.editActive)
24
+ fillArc(ctx, cx, cy, mid * pr, colors.editHalo)
25
+ fillArc(ctx, cx, cy, inner * pr, colors[innerKey])
26
+ ctx.restore()
27
+ }
28
+
29
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1)
30
+
31
+ /**
32
+ * Create all draw-ol style instances for the given resolved color set.
33
+ *
34
+ * @param {object} colors - Output of resolveColors()
35
+ * @returns {{ vertexStyle, selectedVertexStyle, midpointStyle, selectedMidpointStyle,
36
+ * editFeatureStyle, createSketchStyle, createFeatureStyle }}
37
+ */
38
+ export const createStyles = (colors) => {
39
+ const vertexStyle = new Style({
40
+ image: new CircleStyle({
41
+ radius: 6,
42
+ fill: new Fill({ color: colors.editVertex })
43
+ })
44
+ })
45
+
46
+ const selectedVertexStyle = new Style({ renderer: makeRingRenderer(selectedVertexRadii, colors, 'editVertex') })
47
+
48
+ const midpointStyle = new Style({
49
+ image: new CircleStyle({
50
+ radius: 4,
51
+ fill: new Fill({ color: colors.editMidpoint })
52
+ })
53
+ })
54
+
55
+ const selectedMidpointStyle = new Style({ renderer: makeRingRenderer(selectedMidpointRadii, colors, 'editMidpoint') })
56
+
57
+ const editFeatureStyle = new Style({
58
+ stroke: new Stroke({ color: colors.editStroke, width: 2 }),
59
+ fill: new Fill({ color: colors.shapeFill })
60
+ })
61
+
62
+ const sketchLineStyle = new Style({
63
+ stroke: new Stroke({ color: colors.editStroke, width: 2 }),
64
+ fill: new Fill({ color: colors.shapeFill })
65
+ })
66
+
67
+ const sketchPointStyle = new Style({
68
+ image: new CircleStyle({
69
+ radius: 5,
70
+ fill: new Fill({ color: colors.editVertex })
71
+ })
72
+ })
73
+
74
+ const createSketchStyle = () => (feature) =>
75
+ feature.getGeometry().getType() === 'Point' ? [sketchPointStyle] : [sketchLineStyle]
76
+
77
+ const createFeatureStyle = () => (feature) => {
78
+ const p = feature.getProperties()
79
+ const id = colors.mapStyleId
80
+ const stroke = (id && p[`stroke${capitalize(id)}`]) || p.stroke || colors.shapeStroke
81
+ const fill = (id && p[`fill${capitalize(id)}`]) || p.fill || colors.shapeFill
82
+ const strokeWidth = p.strokeWidth || colors.strokeWidth
83
+ return [new Style({
84
+ stroke: new Stroke({ color: stroke, width: strokeWidth }),
85
+ fill: new Fill({ color: fill })
86
+ })]
87
+ }
88
+
89
+ return {
90
+ vertexStyle,
91
+ selectedVertexStyle,
92
+ midpointStyle,
93
+ selectedMidpointStyle,
94
+ editFeatureStyle,
95
+ createSketchStyle,
96
+ createFeatureStyle
97
+ }
98
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Undo stack for draw operations.
3
+ * Calls onChange(length) whenever the stack changes so the UI can update.
4
+ *
5
+ * @param {(length: number) => void} onChange
6
+ */
7
+ export const createUndoStack = (onChange) => {
8
+ const stack = []
9
+
10
+ return {
11
+ push (operation) {
12
+ stack.push(operation)
13
+ onChange(stack.length)
14
+ },
15
+
16
+ pop () {
17
+ const op = stack.pop()
18
+ onChange(stack.length)
19
+ return op
20
+ },
21
+
22
+ clear () {
23
+ stack.length = 0
24
+ onChange(stack.length)
25
+ },
26
+
27
+ get length () {
28
+ return stack.length
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,16 @@
1
+ export const DEFAULTS = {
2
+ editStroke: { light: '#1a65a6', dark: '#ffffff' },
3
+ editVertex: { light: '#1a65a6', dark: '#ffffff' },
4
+ editMidpoint: { light: '#1a65a6', dark: '#ffffff' },
5
+ editHalo: { light: '#ffffff', dark: 'rgba(11,12,12,1)' },
6
+ editActive: { light: '#000000', dark: '#ffffff' },
7
+ splitInvalid: { light: 'rgba(29,112,184,1)', dark: 'rgba(29,112,184,1)' },
8
+ splitValid: { light: 'rgba(29,112,184,1)', dark: 'rgba(29,112,184,1)' },
9
+ shapeStroke: '#1a65a6',
10
+ shapeFill: 'rgba(26,101,166,0.1)',
11
+ strokeWidth: 2,
12
+ snapVertex: 'rgba(212,53,28,0.5)',
13
+ snapMidpoint: 'rgba(40,161,151,1)',
14
+ snapEdge: 'rgba(29,112,184,0.5)',
15
+ snapRadius: 10
16
+ }
@@ -0,0 +1,113 @@
1
+ import Draw from 'ol/interaction/Draw.js'
2
+ import { createDrawInput } from './drawInput.js'
3
+ import { getCoords } from '../utils/geometryHelpers.js'
4
+
5
+ /**
6
+ * Draw mode — handles draw_polygon and draw_line.
7
+ *
8
+ * OL's Draw interaction handles all pointer/mouse behaviour natively.
9
+ * drawInput.js handles touch/keyboard/button input.
10
+ *
11
+ * @returns {{ done, cancel, undo, destroy }}
12
+ */
13
+ export const createDrawMode = ({ map, manager, options }) => {
14
+ const {
15
+ geometryType, // 'Polygon' | 'LineString'
16
+ featureId,
17
+ properties = {},
18
+ container,
19
+ interfaceType,
20
+ addVertexButtonId,
21
+ mapProvider,
22
+ snap
23
+ } = options
24
+
25
+ const drawInteraction = new Draw({
26
+ type: geometryType,
27
+ style: manager.styles.createSketchStyle(),
28
+ stopClick: true,
29
+ // minPoints defaults: 3 for Polygon, 2 for LineString — OL handles this
30
+ // snapTolerance: how close to first point to auto-close polygon
31
+ snapTolerance: 12
32
+ })
33
+ map.addInteraction(drawInteraction)
34
+
35
+ // Track vertex count for the Done button enabled state
36
+ let sketchFeature = null
37
+
38
+ const updateVertexCount = () => {
39
+ if (!sketchFeature) {
40
+ return
41
+ }
42
+ const geom = sketchFeature.getGeometry()
43
+ const coords = getCoords({ type: geometryType, coordinates: geom.getCoordinates() })
44
+ // OL always keeps a trailing rubber-band coordinate; subtract 1
45
+ const numVertices = Math.max(0, coords.length - 1)
46
+ manager.emit('vertexchange', { numVertices })
47
+ }
48
+
49
+ drawInteraction.on('drawstart', (e) => {
50
+ sketchFeature = e.feature
51
+ sketchFeature.getGeometry().on('change', updateVertexCount)
52
+ })
53
+
54
+ drawInteraction.on('drawend', (e) => {
55
+ const olFeature = e.feature
56
+ olFeature.setId(String(featureId))
57
+ olFeature.setProperties(properties)
58
+ manager.store.source.addFeature(olFeature)
59
+ const geojson = manager.store.toGeoJSON(olFeature)
60
+ manager.emit('create', geojson)
61
+ // Mode switches to disabled in events.js after receiving 'create'
62
+ })
63
+
64
+ drawInteraction.on('drawabort', () => {
65
+ manager.emit('cancel')
66
+ })
67
+
68
+ const input = createDrawInput({
69
+ drawInteraction,
70
+ manager,
71
+ options: {
72
+ container,
73
+ interfaceType,
74
+ addVertexButtonId,
75
+ mapProvider,
76
+ snap,
77
+ onUndo: () => {
78
+ drawInteraction.removeLastPoint()
79
+ updateVertexCount()
80
+ }
81
+ }
82
+ })
83
+
84
+ return {
85
+ done () {
86
+ // Validate minimum points before finishing
87
+ if (sketchFeature) {
88
+ const geom = sketchFeature.getGeometry()
89
+ const coords = getCoords({ type: geometryType, coordinates: geom.getCoordinates() })
90
+ const min = geometryType === 'Polygon' ? 4 : 3 // +1 for rubber band
91
+ if (coords.length < min) {
92
+ return
93
+ }
94
+ }
95
+ drawInteraction.finishDrawing()
96
+ },
97
+
98
+ cancel () {
99
+ drawInteraction.abortDrawing()
100
+ },
101
+
102
+ undo () {
103
+ drawInteraction.removeLastPoint()
104
+ updateVertexCount()
105
+ },
106
+
107
+ destroy () {
108
+ input.destroy()
109
+ map.removeInteraction(drawInteraction)
110
+ sketchFeature = null
111
+ }
112
+ }
113
+ }