Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/beta/draw-ol/src/core/OLDrawManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export class OLDrawManager {
this._modeInstance = null
this._mode = modeName

const isDrawMode = modeName === 'draw_polygon' || modeName === 'draw_line' || modeName === 'edit_vertex'
this.snap?.setIndicatorActive(isDrawMode)

const modeOptions = { ...options, snap: this.snap }

if (modeName === 'draw_polygon' || modeName === 'draw_line') {
Expand Down
106 changes: 57 additions & 49 deletions plugins/beta/draw-ol/src/draw/DrawMode.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,48 @@
import Draw from 'ol/interaction/Draw.js'
import { noModifierKeys } from 'ol/events/condition.js'
import { createDrawInput } from './drawInput.js'
import { getCoords } from '../utils/geometryHelpers.js'

/**
* Draw mode — handles draw_polygon and draw_line.
*
* OL's Draw interaction handles all pointer/mouse behaviour natively.
* drawInput.js handles touch/keyboard/button input.
*
* @returns {{ done, cancel, undo, destroy }}
*/
const SNAP_TOLERANCE_PX = 12
const MIN_VERTICES = { Polygon: 3, LineString: 2 }

const canFinish = (geometryType, sketchFeature) => {
if (!sketchFeature) { return false }
const geom = sketchFeature.getGeometry()
const coords = getCoords({ type: geometryType, coordinates: geom.getCoordinates() })
// OL keeps a trailing rubber-band coordinate; subtract 1 to get real vertex count
return coords.length - 1 >= MIN_VERTICES[geometryType]
}

// OL closes Polygon rings by appending v1: [...placed, rubber_band, v1_closing]; last placed is 3 from end.
const POLY_LAST_PLACED_OFFSET = 3

const getLastPlacedCoord = (geom) => {
if (geom.getType() === 'Polygon') {
const ring = geom.getCoordinates()[0] || []
return ring.length >= POLY_LAST_PLACED_OFFSET ? ring[ring.length - POLY_LAST_PLACED_OFFSET] : null
}
const coords = geom.getCoordinates()
return coords.length >= 2 ? coords[coords.length - 2] : null
}

const DUPLICATE_TOLERANCE_PX = 2

const buildCondition = (map, geometryType, getSketchFeature) => (e) => {
if (!noModifierKeys(e)) { return false }
const sf = getSketchFeature()
if (!sf || canFinish(geometryType, sf)) { return true }
const prev = getLastPlacedCoord(sf.getGeometry())
if (!prev) { return true }
const pp = map.getPixelFromCoordinate(prev)
if (!pp) { return true }
const dx = e.pixel[0] - pp[0]; const dy = e.pixel[1] - pp[1]
return dx * dx + dy * dy > DUPLICATE_TOLERANCE_PX * DUPLICATE_TOLERANCE_PX
}

export const createDrawMode = ({ map, manager, options }) => {
const {
geometryType, // 'Polygon' | 'LineString'
geometryType,
featureId,
properties = {},
container,
Expand All @@ -22,28 +52,28 @@ export const createDrawMode = ({ map, manager, options }) => {
snap
} = options

let sketchFeature = null

const drawInteraction = new Draw({
type: geometryType,
style: manager.styles.createSketchStyle(),
stopClick: true,
// minPoints defaults: 3 for Polygon, 2 for LineString — OL handles this
// snapTolerance: how close to first point to auto-close polygon
snapTolerance: 12
snapTolerance: SNAP_TOLERANCE_PX,
condition: buildCondition(map, geometryType, () => sketchFeature)
})
map.addInteraction(drawInteraction)

// Track vertex count for the Done button enabled state
let sketchFeature = null
// OL internal: overlay_ is the private VectorLayer used for the sketch geometry.
// updateWhileAnimating_ forces per-frame redraws during view animations (keyboard pan).
// Without this, geom.setCoordinates() calls are ignored while the ANIMATING hint is set.
// Check ol/interaction/Draw.js and ol/layer/BaseVector.js if this breaks after an OL upgrade.
drawInteraction.overlay_.updateWhileAnimating_ = true

const updateVertexCount = () => {
if (!sketchFeature) {
return
}
if (!sketchFeature) { return }
const geom = sketchFeature.getGeometry()
const coords = getCoords({ type: geometryType, coordinates: geom.getCoordinates() })
// OL always keeps a trailing rubber-band coordinate; subtract 1
const numVertices = Math.max(0, coords.length - 1)
manager.emit('vertexchange', { numVertices })
manager.emit('vertexchange', { numVertices: Math.max(0, coords.length - 1) })
}

drawInteraction.on('drawstart', (e) => {
Expand All @@ -56,14 +86,11 @@ export const createDrawMode = ({ map, manager, options }) => {
olFeature.setId(String(featureId))
olFeature.setProperties(properties)
manager.store.source.addFeature(olFeature)
const geojson = manager.store.toGeoJSON(olFeature)
manager.emit('create', geojson)
manager.emit('create', manager.store.toGeoJSON(olFeature))
// Mode switches to disabled in events.js after receiving 'create'
})

drawInteraction.on('drawabort', () => {
manager.emit('cancel')
})
drawInteraction.on('drawabort', () => { manager.emit('cancel') })

const input = createDrawInput({
drawInteraction,
Expand All @@ -74,36 +101,17 @@ export const createDrawMode = ({ map, manager, options }) => {
addVertexButtonId,
mapProvider,
snap,
onUndo: () => {
drawInteraction.removeLastPoint()
updateVertexCount()
}
onUndo: () => { drawInteraction.removeLastPoint(); updateVertexCount() },
canFinish: () => canFinish(geometryType, sketchFeature)
}
})

return {
done () {
// Validate minimum points before finishing
if (sketchFeature) {
const geom = sketchFeature.getGeometry()
const coords = getCoords({ type: geometryType, coordinates: geom.getCoordinates() })
const min = geometryType === 'Polygon' ? 4 : 3 // +1 for rubber band
if (coords.length < min) {
return
}
}
drawInteraction.finishDrawing()
if (canFinish(geometryType, sketchFeature)) { drawInteraction.finishDrawing() }
},

cancel () {
drawInteraction.abortDrawing()
},

undo () {
drawInteraction.removeLastPoint()
updateVertexCount()
},

cancel () { drawInteraction.abortDrawing() },
undo () { drawInteraction.removeLastPoint(); updateVertexCount() },
destroy () {
input.destroy()
map.removeInteraction(drawInteraction)
Expand Down
90 changes: 64 additions & 26 deletions plugins/beta/draw-ol/src/draw/drawInput.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
/**
* Input handling for draw mode: touch, keyboard, and button events.
*
* Mouse/pointer drawing is handled entirely by OL's Draw interaction.
* This module handles the crosshair-based input path (touch + keyboard)
* and the Done / Add Point / Cancel button wiring.
*/

import { coordToPixel, pixelDist } from '../utils/olCoords.js'

const SNAP_TOLERANCE = 12 // pixels
// Minimum ring length to allow snap-to-close (placed vertices + rubber-band)
const MIN_SKETCH_COORDS = { Polygon: 4, LineString: 3 }
const DUPLICATE_TOLERANCE_PX = 2
// OL Polygon ring layout after addToDrawing_: [...committed, rubber_band, closing_v1]
const POLY_COMMITTED_OFFSET = 3
const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])

const isCloseToFirstVertex = (map, coord, sketchCoords, geometryType) => {
if (geometryType !== 'Polygon' || sketchCoords.length < 4) {
if (geometryType !== 'Polygon' || sketchCoords.length < MIN_SKETCH_COORDS.Polygon) {
return false
}
const firstCoord = sketchCoords[0]
Expand Down Expand Up @@ -44,6 +41,17 @@ const applyRubberbanding = (geom, centerCoord) => {
}
}

// Returns the last vertex committed by OL's Draw interaction (not the rubber-band or
// the closing copy that OL appends to Polygon rings).
const getLastCommittedVertex = (geom) => {
if (geom.getType() === 'Polygon') {
const ring = geom.getCoordinates()[0] || []
return ring.length >= POLY_COMMITTED_OFFSET ? ring[ring.length - POLY_COMMITTED_OFFSET] : null
}
const coords = geom.getCoordinates()
return coords.length >= 2 ? coords[coords.length - 2] : null
}

const wireInputEvents = ({
container, addVertexButtonId, olView, onUndo,
getInterfaceType, setInterfaceType, clearLastCoord,
Expand Down Expand Up @@ -117,14 +125,8 @@ const wireInputEvents = ({
}
}

/**
* @param {object} params
* @param {import('ol/interaction/Draw').default} params.drawInteraction
* @param {object} params.options - { container, interfaceType, addVertexButtonId, mapProvider, snap }
* @returns {{ getInterfaceType: () => string, destroy: () => void }}
*/
export const createDrawInput = ({ drawInteraction, options }) => {
const { container, addVertexButtonId, mapProvider, snap, onUndo } = options
const { container, addVertexButtonId, mapProvider, snap, onUndo, canFinish } = options
let interfaceType = options.interfaceType
let sketchFeature = null
let lastPlacedCoord = null
Expand Down Expand Up @@ -156,6 +158,38 @@ export const createDrawInput = ({ drawInteraction, options }) => {
applyRubberbanding(geom, centerCoord)
}

// Returns true if the vertex was handled as a close/finish attempt (caller should not append).
const tryClose = (geom, sketchCoords, coord) => {
if (lastPlacedCoord && lastPlacedCoord[0] === coord[0] && lastPlacedCoord[1] === coord[1]) {
// Same position as last placed: don't duplicate. Close only if enough real vertices exist.
if (canFinish?.()) { drawInteraction.finishDrawing() }
lastPlacedCoord = null
return true
}
if (isCloseToFirstVertex(drawInteraction.getMap(), coord, sketchCoords, geom.getType())) {
drawInteraction.finishDrawing()
return true
}
// When the add-vertex button overlays the map (touch UI), OL's native pointer handler
// and this button click handler both fire for the same tap. Detect that OL already
// committed a vertex at coord's position and skip the duplicate appendCoordinates,
// but register coord as lastPlacedCoord so a second tap at the same position can close.
const map = drawInteraction.getMap()
const lastCommitted = getLastCommittedVertex(geom)
if (lastCommitted) {
const p1 = map.getPixelFromCoordinate(lastCommitted)
const p2 = map.getPixelFromCoordinate(coord)
if (p1 && p2) {
const dx = p1[0] - p2[0]; const dy = p1[1] - p2[1]
if (dx * dx + dy * dy < DUPLICATE_TOLERANCE_PX * DUPLICATE_TOLERANCE_PX) {
lastPlacedCoord = coord
return true
}
}
}
return false
}

const placeVertex = () => {
const raw = mapProvider.getCenter()
const coord = (interfaceType !== 'pointer' && snap) ? snap.apply(raw) : raw
Expand All @@ -164,24 +198,19 @@ export const createDrawInput = ({ drawInteraction, options }) => {
const geom = sketchFeature.getGeometry()
const rawCoords = geom.getCoordinates()
const sketchCoords = geom.getType() === 'Polygon' ? (rawCoords[0] || []) : rawCoords
if (lastPlacedCoord && lastPlacedCoord[0] === coord[0] && lastPlacedCoord[1] === coord[1]) {
drawInteraction.finishDrawing()
lastPlacedCoord = null
return
}
if (isCloseToFirstVertex(drawInteraction.getMap(), coord, sketchCoords, geom.getType())) {
drawInteraction.finishDrawing()
return
}
if (tryClose(geom, sketchCoords, coord)) { return }
}
drawInteraction.appendCoordinates([coord])
lastPlacedCoord = coord
}

const map = drawInteraction.getMap()
const olView = map?.getView()

const events = wireInputEvents({
container,
addVertexButtonId,
olView: drawInteraction.getMap()?.getView(),
olView,
onUndo,
getInterfaceType: () => interfaceType,
setInterfaceType: (t) => { interfaceType = t },
Expand All @@ -190,10 +219,19 @@ export const createDrawInput = ({ drawInteraction, options }) => {
placeVertex
})

// change:center fires once when a keyboard pan animation starts; postrender tracks each frame.
const onMapRender = () => {
if (interfaceType !== 'pointer' && olView?.getAnimating()) {
updateRubberbanding()
}
}
map?.on('postrender', onMapRender)

return {
getInterfaceType: () => interfaceType,
destroy () {
events.destroy()
map?.un('postrender', onMapRender)
}
}
}
6 changes: 5 additions & 1 deletion plugins/beta/draw-ol/src/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export const manifest = {
id: 'drawDeletePoint',
label: 'Delete point',
iconId: 'trash',
enableWhen: ({ pluginState }) => pluginState.selectedVertexIndex >= 0 && pluginState.numVertices > 2,
enableWhen: ({ pluginState }) => {
if (pluginState.selectedVertexIndex < 0) { return false }
const isPolygon = pluginState.feature?.geometry?.type === 'Polygon'
return isPolygon ? pluginState.numVertices > 3 : pluginState.numVertices > 2 // NOSONAR
},
hiddenWhen: ({ pluginState }) => pluginState.mode !== 'edit_vertex'
}],
mobile: { slot: 'bottom-right' },
Expand Down
2 changes: 1 addition & 1 deletion plugins/beta/draw-ol/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const initialState = {
hasSnapLayers: false
}

const setMode = (state, payload) => ({ ...state, mode: payload })
const setMode = (state, payload) => ({ ...state, mode: payload, numVertices: null })

const setFeature = (state, payload) => ({
...state,
Expand Down
Loading
Loading