@defra/interactive-map 0.0.25-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.
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/map-style-config.md +30 -0
- package/docs/assets/css/docusaurus.scss +4 -0
- package/package.json +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/defaults.js +14 -14
- package/plugins/beta/draw-ml/src/mapboxDraw.js +5 -0
- package/plugins/beta/draw-ml/src/mapboxSnap.js +1 -1
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +10 -2
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +2 -0
- package/plugins/beta/draw-ml/src/reducer.js +4 -1
- package/plugins/beta/draw-ml/src/styles.js +42 -39
- package/plugins/beta/draw-ol/dist/css/index.css +13 -0
- package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/geometryHelpers.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/index.js +1 -0
- package/plugins/beta/draw-ol/src/DrawInit.jsx +67 -0
- package/plugins/beta/draw-ol/src/api/addFeature.js +29 -0
- package/plugins/beta/draw-ol/src/api/deleteFeature.js +11 -0
- package/plugins/beta/draw-ol/src/api/editFeature.js +50 -0
- package/plugins/beta/draw-ol/src/api/newLine.js +44 -0
- package/plugins/beta/draw-ol/src/api/newPolygon.js +44 -0
- package/plugins/beta/draw-ol/src/core/OLDrawManager.js +150 -0
- package/plugins/beta/draw-ol/src/core/featureStore.js +65 -0
- package/plugins/beta/draw-ol/src/core/styles.js +98 -0
- package/plugins/beta/draw-ol/src/core/undoStack.js +31 -0
- package/plugins/beta/draw-ol/src/defaults.js +16 -0
- package/plugins/beta/draw-ol/src/draw/DrawMode.js +113 -0
- package/plugins/beta/draw-ol/src/draw/drawInput.js +199 -0
- package/plugins/beta/draw-ol/src/draw.scss +17 -0
- package/plugins/beta/draw-ol/src/edit/EditMode.js +424 -0
- package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +227 -0
- package/plugins/beta/draw-ol/src/edit/midpointLayer.js +57 -0
- package/plugins/beta/draw-ol/src/edit/touchHandler.js +171 -0
- package/plugins/beta/draw-ol/src/edit/undoOps.js +95 -0
- package/plugins/beta/draw-ol/src/edit/vertexHitTest.js +72 -0
- package/plugins/beta/draw-ol/src/edit/vertexLayer.js +49 -0
- package/plugins/beta/draw-ol/src/edit/vertexOps.js +99 -0
- package/plugins/beta/draw-ol/src/events.js +114 -0
- package/plugins/beta/draw-ol/src/index.js +12 -0
- package/plugins/beta/draw-ol/src/manifest.js +121 -0
- package/plugins/beta/draw-ol/src/olDraw.js +38 -0
- package/plugins/beta/draw-ol/src/reducer.js +41 -0
- package/plugins/beta/draw-ol/src/snap/snapEngine.js +109 -0
- package/plugins/beta/draw-ol/src/snap/snapGeometry.js +200 -0
- package/plugins/beta/draw-ol/src/snap/snapIndicator.js +81 -0
- package/plugins/beta/draw-ol/src/snap/snapInteraction.js +56 -0
- package/plugins/beta/draw-ol/src/snap/snapManager.js +90 -0
- package/plugins/beta/draw-ol/src/utils/flattenStyleProperties.js +47 -0
- package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +97 -0
- package/plugins/beta/draw-ol/src/utils/olCoords.js +35 -0
- package/plugins/beta/draw-ol/src/utils/resolveColors.js +39 -0
- package/plugins/beta/draw-ol/src/utils/spatial.js +28 -0
- package/plugins/beta/draw-ol/src/utils/touchTarget.js +61 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/src/defaults.js +1 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +150 -43
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +240 -8
- package/plugins/interact/src/reducer.js +7 -0
- package/plugins/interact/src/reducer.test.js +16 -0
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/appEvents.js +9 -13
- package/providers/beta/openlayers/src/appEvents.test.js +27 -18
- package/providers/beta/openlayers/src/mapEvents.js +34 -20
- package/providers/beta/openlayers/src/mapEvents.test.js +65 -6
- package/providers/beta/openlayers/src/openlayersProvider.js +45 -26
- package/providers/beta/openlayers/src/openlayersProvider.test.js +12 -34
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +245 -0
- package/providers/beta/openlayers/src/utils/hoverCursor.js +65 -0
- package/providers/beta/openlayers/src/utils/hoverCursor.test.js +235 -0
- package/providers/beta/openlayers/src/utils/openLayersFixes.js +11 -0
- package/providers/beta/openlayers/src/utils/openLayersFixes.test.js +53 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.js +77 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +181 -0
- package/providers/beta/openlayers/src/utils/tileLayers.js +23 -5
- package/providers/beta/openlayers/src/utils/tileLayers.test.js +81 -5
- package/providers/beta/openlayers/src/utils/vtTileFragments.js +86 -0
- package/rollup.esm.mjs +6 -0
- package/src/App/controls/keyboardActions.js +2 -2
- package/src/InteractiveMap/InteractiveMap.js +38 -10
- package/src/InteractiveMap/InteractiveMap.test.js +282 -356
- package/src/InteractiveMap/historyManager.js +7 -0
- package/src/InteractiveMap/historyManager.test.js +11 -0
- package/webpack.dev.mjs +3 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCoords,
|
|
3
|
+
getRingSegments,
|
|
4
|
+
getSegmentForIndex,
|
|
5
|
+
getModifiableCoords
|
|
6
|
+
} from '../utils/geometryHelpers.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Delete the vertex at `selectedIndex` from the OL feature's geometry.
|
|
10
|
+
* Respects minimum vertex counts (3 for closed rings, 2 for lines).
|
|
11
|
+
*
|
|
12
|
+
* @returns {{ deletedIndex: number, deletedCoord: number[] } | null} undo payload, or null if not deleted
|
|
13
|
+
*/
|
|
14
|
+
export const deleteVertex = (olFeature, selectedIndex) => {
|
|
15
|
+
const geom = olFeature.getGeometry()
|
|
16
|
+
const geojsonGeom = { type: geom.getType(), coordinates: geom.getCoordinates() }
|
|
17
|
+
const coords = getCoords(geojsonGeom)
|
|
18
|
+
const segments = getRingSegments(geojsonGeom)
|
|
19
|
+
const result = getSegmentForIndex(segments, selectedIndex)
|
|
20
|
+
if (!result) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { segment } = result
|
|
25
|
+
const minVertices = segment.closed ? 3 : 2
|
|
26
|
+
if (segment.length <= minVertices) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const deletedCoord = [...coords[selectedIndex]]
|
|
31
|
+
const ring = getModifiableCoords(geojsonGeom, segment.path)
|
|
32
|
+
ring.splice(result.localIdx, 1)
|
|
33
|
+
if (segment.closed) {
|
|
34
|
+
ring[ring.length - 1] = [...ring[0]]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
geom.setCoordinates(geojsonGeom.coordinates)
|
|
38
|
+
return { deletedIndex: selectedIndex, deletedCoord }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Insert a vertex after `afterIndex` in the OL feature's geometry.
|
|
43
|
+
* Used when the user activates a midpoint (touch tap or keyboard insert).
|
|
44
|
+
*
|
|
45
|
+
* @param {number[][]} midpoints - current midpoint array (from midpointLayer)
|
|
46
|
+
* @param {number} midpointFlatIndex - flat index (vertexCount + midpointOffset)
|
|
47
|
+
* @param {number} vertexCount - number of actual vertices
|
|
48
|
+
* @param {number[][]} vertices - current vertex array
|
|
49
|
+
* @returns {{ insertedIndex: number } | null}
|
|
50
|
+
*/
|
|
51
|
+
export const insertAtMidpoint = (olFeature, midpoints, midpointFlatIndex, vertexCount) => {
|
|
52
|
+
const midpointLocalIdx = midpointFlatIndex - vertexCount
|
|
53
|
+
const midCoord = midpoints[midpointLocalIdx]
|
|
54
|
+
if (!midCoord) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const geom = olFeature.getGeometry()
|
|
59
|
+
const geojsonGeom = { type: geom.getType(), coordinates: geom.getCoordinates() }
|
|
60
|
+
const segments = getRingSegments(geojsonGeom)
|
|
61
|
+
|
|
62
|
+
// Map midpoint local index to insertion position in the coordinate array
|
|
63
|
+
let midpointCounter = 0
|
|
64
|
+
for (const seg of segments) {
|
|
65
|
+
const segMidpoints = seg.closed ? seg.length : seg.length - 1
|
|
66
|
+
if (midpointLocalIdx < midpointCounter + segMidpoints) {
|
|
67
|
+
const localMidIdx = midpointLocalIdx - midpointCounter
|
|
68
|
+
const insertLocalIdx = localMidIdx + 1
|
|
69
|
+
const insertGlobalIdx = seg.start + insertLocalIdx
|
|
70
|
+
const ring = getModifiableCoords(geojsonGeom, seg.path)
|
|
71
|
+
ring.splice(insertLocalIdx, 0, [...midCoord])
|
|
72
|
+
geom.setCoordinates(geojsonGeom.coordinates)
|
|
73
|
+
return { insertedIndex: insertGlobalIdx }
|
|
74
|
+
}
|
|
75
|
+
midpointCounter += segMidpoints
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Move vertex at `index` to `newCoord` in the OL feature's geometry.
|
|
83
|
+
*/
|
|
84
|
+
export const moveVertex = (olFeature, index, newCoord) => {
|
|
85
|
+
const geom = olFeature.getGeometry()
|
|
86
|
+
const geojsonGeom = { type: geom.getType(), coordinates: geom.getCoordinates() }
|
|
87
|
+
const segments = getRingSegments(geojsonGeom)
|
|
88
|
+
const result = getSegmentForIndex(segments, index)
|
|
89
|
+
if (!result) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ring = getModifiableCoords(geojsonGeom, result.segment.path)
|
|
94
|
+
ring[result.localIdx] = [...newCoord]
|
|
95
|
+
if (result.segment.closed && result.localIdx === 0) {
|
|
96
|
+
ring[ring.length - 1] = [...newCoord]
|
|
97
|
+
}
|
|
98
|
+
geom.setCoordinates(geojsonGeom.coordinates)
|
|
99
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Button and manager event wiring for the OL draw plugin.
|
|
3
|
+
* Mirrors draw-ml/events.js structure but uses manager.on/off instead of map.on/off.
|
|
4
|
+
*/
|
|
5
|
+
export function attachEvents ({ pluginState, mapProvider, buttonConfig, eventBus }) {
|
|
6
|
+
const { drawDone, drawCancel, drawUndo, drawDeletePoint, drawSnap } = buttonConfig
|
|
7
|
+
const { draw } = mapProvider
|
|
8
|
+
const { dispatch, feature, tempFeature } = pluginState
|
|
9
|
+
|
|
10
|
+
const resetState = () => {
|
|
11
|
+
draw.undoStack.clear()
|
|
12
|
+
dispatch({ type: 'SET_MODE', payload: null })
|
|
13
|
+
dispatch({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null } })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// --- Button handlers ---
|
|
17
|
+
|
|
18
|
+
const handleDone = () => {
|
|
19
|
+
draw.undoStack.clear()
|
|
20
|
+
draw.done()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const handleCancel = () => {
|
|
24
|
+
const mode = draw.getMode()
|
|
25
|
+
if (mode === 'edit_vertex' && tempFeature?.id) {
|
|
26
|
+
// Restore original geometry by re-adding the pre-edit feature
|
|
27
|
+
draw.add(feature)
|
|
28
|
+
}
|
|
29
|
+
draw.cancel()
|
|
30
|
+
resetState()
|
|
31
|
+
eventBus.emit('draw:cancelled', feature)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handleUndo = () => {
|
|
35
|
+
draw.undo()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleDeleteVertex = () => {
|
|
39
|
+
draw.deleteVertex()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleSnap = () => {
|
|
43
|
+
const newSnapState = !pluginState.snap
|
|
44
|
+
dispatch({ type: 'TOGGLE_SNAP' })
|
|
45
|
+
draw.snap?.setActive(newSnapState)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Manager event handlers ---
|
|
49
|
+
|
|
50
|
+
const onCreate = (geojsonFeature) => {
|
|
51
|
+
resetState()
|
|
52
|
+
draw.changeMode('disabled')
|
|
53
|
+
eventBus.emit('draw:created', geojsonFeature)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const onEditFinish = (geojsonFeature) => {
|
|
57
|
+
resetState()
|
|
58
|
+
draw.changeMode('disabled')
|
|
59
|
+
eventBus.emit('draw:edited', geojsonFeature)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const onCancel = () => {
|
|
63
|
+
// Fired by draw.cancel() / drawabort — only when not user-initiated via button
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const onVertexSelection = (e) => {
|
|
67
|
+
dispatch({ type: 'SET_SELECTED_VERTEX_INDEX', payload: e })
|
|
68
|
+
eventBus.emit('draw:vertexselection', e)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const onVertexChange = (e) => {
|
|
72
|
+
dispatch({ type: 'SET_SELECTED_VERTEX_INDEX', payload: { index: -1, numVertices: e.numVertices } })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const onUndoChange = (length) => {
|
|
76
|
+
dispatch({ type: 'SET_UNDO_STACK_LENGTH', payload: length })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const onUpdate = (geojsonFeature) => {
|
|
80
|
+
eventBus.emit('draw:updated', geojsonFeature)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Wire up ---
|
|
84
|
+
|
|
85
|
+
drawDone.onClick = handleDone
|
|
86
|
+
drawCancel.onClick = handleCancel
|
|
87
|
+
drawUndo.onClick = handleUndo
|
|
88
|
+
if (drawDeletePoint) drawDeletePoint.onClick = handleDeleteVertex
|
|
89
|
+
if (drawSnap) drawSnap.onClick = handleSnap
|
|
90
|
+
|
|
91
|
+
draw.on('create', onCreate)
|
|
92
|
+
draw.on('editfinish', onEditFinish)
|
|
93
|
+
draw.on('cancel', onCancel)
|
|
94
|
+
draw.on('vertexselection', onVertexSelection)
|
|
95
|
+
draw.on('vertexchange', onVertexChange)
|
|
96
|
+
draw.on('undochange', onUndoChange)
|
|
97
|
+
draw.on('update', onUpdate)
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
drawDone.onClick = null
|
|
101
|
+
drawCancel.onClick = null
|
|
102
|
+
drawUndo.onClick = null
|
|
103
|
+
if (drawDeletePoint) drawDeletePoint.onClick = null
|
|
104
|
+
if (drawSnap) drawSnap.onClick = null
|
|
105
|
+
|
|
106
|
+
draw.off('create', onCreate)
|
|
107
|
+
draw.off('editfinish', onEditFinish)
|
|
108
|
+
draw.off('cancel', onCancel)
|
|
109
|
+
draw.off('vertexselection', onVertexSelection)
|
|
110
|
+
draw.off('vertexchange', onVertexChange)
|
|
111
|
+
draw.off('undochange', onUndoChange)
|
|
112
|
+
draw.off('update', onUpdate)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import './draw.scss'
|
|
2
|
+
|
|
3
|
+
export default function createPlugin (options = {}) {
|
|
4
|
+
return {
|
|
5
|
+
...options,
|
|
6
|
+
id: 'draw',
|
|
7
|
+
load: async () => {
|
|
8
|
+
const module = (await import(/* webpackChunkName: "im-draw-ol-plugin" */ './manifest.js')).manifest
|
|
9
|
+
return module
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { initialState, actions } from './reducer.js'
|
|
2
|
+
import { DrawInit } from './DrawInit.jsx'
|
|
3
|
+
import { newPolygon } from './api/newPolygon.js'
|
|
4
|
+
import { newLine } from './api/newLine.js'
|
|
5
|
+
import { editFeature } from './api/editFeature.js'
|
|
6
|
+
import { addFeature } from './api/addFeature.js'
|
|
7
|
+
import { deleteFeature } from './api/deleteFeature.js'
|
|
8
|
+
|
|
9
|
+
const createButtonSlots = (showLabel) => ({
|
|
10
|
+
mobile: { slot: 'actions', showLabel },
|
|
11
|
+
tablet: { slot: 'actions', showLabel },
|
|
12
|
+
desktop: { slot: 'actions', showLabel }
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const manifest = {
|
|
16
|
+
reducer: {
|
|
17
|
+
initialState,
|
|
18
|
+
actions
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
InitComponent: DrawInit,
|
|
22
|
+
|
|
23
|
+
buttons: [
|
|
24
|
+
{
|
|
25
|
+
id: 'drawCancel',
|
|
26
|
+
label: 'Cancel',
|
|
27
|
+
variant: 'tertiary',
|
|
28
|
+
hiddenWhen: ({ pluginState }) => !pluginState.mode,
|
|
29
|
+
...createButtonSlots(true)
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'drawAddPoint',
|
|
33
|
+
label: 'Add point',
|
|
34
|
+
variant: 'primary',
|
|
35
|
+
hiddenWhen: ({ appState, pluginState }) =>
|
|
36
|
+
!['draw_polygon', 'draw_line'].includes(pluginState.mode) || appState.interfaceType !== 'touch',
|
|
37
|
+
...createButtonSlots(true)
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'drawDone',
|
|
41
|
+
label: 'Done',
|
|
42
|
+
variant: 'primary',
|
|
43
|
+
hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
|
|
44
|
+
enableWhen: ({ pluginState }) => {
|
|
45
|
+
if (pluginState.mode === 'draw_polygon') {
|
|
46
|
+
return pluginState.numVertices >= 3 // NOSONAR
|
|
47
|
+
}
|
|
48
|
+
if (pluginState.mode === 'draw_line') {
|
|
49
|
+
return pluginState.numVertices >= 2 // NOSONAR
|
|
50
|
+
}
|
|
51
|
+
if (pluginState.mode === 'edit_vertex') {
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
},
|
|
56
|
+
...createButtonSlots(true)
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'drawMenu',
|
|
60
|
+
label: 'Menu',
|
|
61
|
+
iconId: 'menu',
|
|
62
|
+
hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
|
|
63
|
+
menuItems: [{
|
|
64
|
+
id: 'drawUndo',
|
|
65
|
+
label: 'Undo',
|
|
66
|
+
iconId: 'undo',
|
|
67
|
+
hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
|
|
68
|
+
enableWhen: ({ pluginState }) => {
|
|
69
|
+
if (['draw_polygon', 'draw_line'].includes(pluginState.mode)) {
|
|
70
|
+
return pluginState.numVertices > 0
|
|
71
|
+
}
|
|
72
|
+
return pluginState.undoStackLength > 0
|
|
73
|
+
}
|
|
74
|
+
}, {
|
|
75
|
+
id: 'drawSnap',
|
|
76
|
+
label: 'Snap to feature',
|
|
77
|
+
iconId: 'magnet',
|
|
78
|
+
hiddenWhen: ({ pluginState }) => !pluginState.mode || !pluginState.hasSnapLayers,
|
|
79
|
+
pressedWhen: ({ pluginState }) => !!pluginState.snap
|
|
80
|
+
}, {
|
|
81
|
+
id: 'drawDeletePoint',
|
|
82
|
+
label: 'Delete point',
|
|
83
|
+
iconId: 'trash',
|
|
84
|
+
enableWhen: ({ pluginState }) => pluginState.selectedVertexIndex >= 0 && pluginState.numVertices > 2,
|
|
85
|
+
hiddenWhen: ({ pluginState }) => pluginState.mode !== 'edit_vertex'
|
|
86
|
+
}],
|
|
87
|
+
mobile: { slot: 'bottom-right' },
|
|
88
|
+
tablet: { slot: 'top-middle' },
|
|
89
|
+
desktop: { slot: 'top-middle' }
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
|
|
93
|
+
keyboardShortcuts: [{
|
|
94
|
+
id: 'drawStart',
|
|
95
|
+
group: 'Drawing',
|
|
96
|
+
title: 'Edit vertex',
|
|
97
|
+
command: '<kbd>Spacebar</kbd></dd>'
|
|
98
|
+
}],
|
|
99
|
+
|
|
100
|
+
icons: [{
|
|
101
|
+
id: 'menu',
|
|
102
|
+
svgContent: '<path d="m6 9 6 6 6-6"/>'
|
|
103
|
+
}, {
|
|
104
|
+
id: 'undo',
|
|
105
|
+
svgContent: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/>'
|
|
106
|
+
}, {
|
|
107
|
+
id: 'magnet',
|
|
108
|
+
svgContent: '<path d="m12 15 4 4"/><path d="M2.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.029-6.029a1 1 0 1 1 3 3l-6.029 6.029a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.365-6.367A1 1 0 0 0 8.716 4.282z"/><path d="m5 8 4 4"/>'
|
|
109
|
+
}, {
|
|
110
|
+
id: 'trash',
|
|
111
|
+
svgContent: '<path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>'
|
|
112
|
+
}],
|
|
113
|
+
|
|
114
|
+
api: {
|
|
115
|
+
newPolygon,
|
|
116
|
+
newLine,
|
|
117
|
+
editFeature,
|
|
118
|
+
addFeature,
|
|
119
|
+
deleteFeature
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { OLDrawManager } from './core/OLDrawManager.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates the OLDrawManager, attaches it to mapProvider, and wires
|
|
5
|
+
* app-level events (MAP_SET_SIZE for scale-aware touch targets,
|
|
6
|
+
* MAP_SET_STYLE for dynamic color updates).
|
|
7
|
+
*
|
|
8
|
+
* @returns {{ remove: () => void }}
|
|
9
|
+
*/
|
|
10
|
+
export const createOLDraw = ({ mapProvider, events, eventBus, pluginConfig = {}, mapStyle = null }) => {
|
|
11
|
+
const { map } = mapProvider
|
|
12
|
+
const manager = new OLDrawManager(map, pluginConfig)
|
|
13
|
+
|
|
14
|
+
if (mapStyle) {
|
|
15
|
+
manager.setMapStyle(mapStyle)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
mapProvider.draw = manager
|
|
19
|
+
|
|
20
|
+
const handleSetMapSize = (size) => {
|
|
21
|
+
mapProvider.drawScale = { small: 1, medium: 1.5, large: 2 }[size] ?? 1
|
|
22
|
+
}
|
|
23
|
+
eventBus.on(events.MAP_SET_SIZE, handleSetMapSize)
|
|
24
|
+
|
|
25
|
+
const handleSetMapStyle = (newMapStyle) => {
|
|
26
|
+
manager.setMapStyle(newMapStyle)
|
|
27
|
+
}
|
|
28
|
+
eventBus.on(events.MAP_SET_STYLE, handleSetMapStyle)
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
remove () {
|
|
32
|
+
eventBus.off(events.MAP_SET_SIZE, handleSetMapSize)
|
|
33
|
+
eventBus.off(events.MAP_SET_STYLE, handleSetMapStyle)
|
|
34
|
+
manager.remove()
|
|
35
|
+
mapProvider.draw = null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const initialState = {
|
|
2
|
+
mode: null,
|
|
3
|
+
feature: null,
|
|
4
|
+
tempFeature: null,
|
|
5
|
+
selectedVertexIndex: -1,
|
|
6
|
+
numVertices: null,
|
|
7
|
+
undoStackLength: 0,
|
|
8
|
+
snap: false,
|
|
9
|
+
hasSnapLayers: false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const setMode = (state, payload) => ({ ...state, mode: payload })
|
|
13
|
+
|
|
14
|
+
const setFeature = (state, payload) => ({
|
|
15
|
+
...state,
|
|
16
|
+
feature: payload.feature === undefined ? state.feature : payload.feature,
|
|
17
|
+
tempFeature: payload.tempFeature === undefined ? state.tempFeature : payload.tempFeature
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const setSelectedVertexIndex = (state, payload) => ({
|
|
21
|
+
...state,
|
|
22
|
+
selectedVertexIndex: payload.index,
|
|
23
|
+
numVertices: payload.numVertices
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const setUndoStackLength = (state, payload) => ({ ...state, undoStackLength: payload })
|
|
27
|
+
|
|
28
|
+
const toggleSnap = (state) => ({ ...state, snap: !state.snap })
|
|
29
|
+
|
|
30
|
+
const setHasSnapLayers = (state, payload) => ({ ...state, hasSnapLayers: !!payload })
|
|
31
|
+
|
|
32
|
+
const actions = {
|
|
33
|
+
SET_MODE: setMode,
|
|
34
|
+
SET_FEATURE: setFeature,
|
|
35
|
+
SET_SELECTED_VERTEX_INDEX: setSelectedVertexIndex,
|
|
36
|
+
SET_UNDO_STACK_LENGTH: setUndoStackLength,
|
|
37
|
+
TOGGLE_SNAP: toggleSnap,
|
|
38
|
+
SET_HAS_SNAP_LAYERS: setHasSnapLayers
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { initialState, actions }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snap candidate engine.
|
|
3
|
+
*
|
|
4
|
+
* Accepts two kinds of entry in snapLayers:
|
|
5
|
+
* - string → a VectorTile style-layer name (matched via feature.get('layer'))
|
|
6
|
+
* All VectorTileLayers on the map are searched; only features whose
|
|
7
|
+
* style-layer name is in the set are tested.
|
|
8
|
+
* - OL VectorLayer instance → all features in that layer's source are tested.
|
|
9
|
+
*
|
|
10
|
+
* query() is synchronous and uses:
|
|
11
|
+
* - VectorSource.getFeaturesInExtent() for OL vector layers (internal rBush, fast)
|
|
12
|
+
* - map.forEachFeatureAtPixel() for VectorTile layers (rendered tile data)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import VectorLayer from 'ol/layer/Vector.js'
|
|
16
|
+
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
17
|
+
import { testOLFeature, testRenderFeature } from './snapGeometry.js'
|
|
18
|
+
|
|
19
|
+
const pickBest = (a, b) => {
|
|
20
|
+
if (!b) { return a }
|
|
21
|
+
if (!a) { return b }
|
|
22
|
+
if (a.type === 'vertex' && b.type === 'edge') { return a }
|
|
23
|
+
if (a.type === 'edge' && b.type === 'vertex') { return b }
|
|
24
|
+
return b.distSq < a.distSq ? b : a
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Collected on each query — VectorTileLayers are replaced when the map style changes
|
|
28
|
+
const getVTLayers = (map) => {
|
|
29
|
+
const layers = []
|
|
30
|
+
map.getLayers().forEach(l => {
|
|
31
|
+
if (l instanceof VectorTileLayer) { layers.push(l) }
|
|
32
|
+
})
|
|
33
|
+
return layers
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const createSnapEngine = (map, snapLayers = []) => {
|
|
37
|
+
let vtLayerNames = new Set()
|
|
38
|
+
let olLayers = []
|
|
39
|
+
|
|
40
|
+
const setLayers = (layers) => {
|
|
41
|
+
vtLayerNames = new Set()
|
|
42
|
+
olLayers = []
|
|
43
|
+
for (const entry of layers ?? []) {
|
|
44
|
+
if (typeof entry === 'string') {
|
|
45
|
+
vtLayerNames.add(entry)
|
|
46
|
+
} else if (entry instanceof VectorLayer) {
|
|
47
|
+
olLayers.push(entry)
|
|
48
|
+
} else {
|
|
49
|
+
// unsupported layer type — skip
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setLayers(snapLayers)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find the nearest snap candidate to coord within radiusPx screen pixels.
|
|
58
|
+
* @param {number[]} coord - map coordinate [x, y]
|
|
59
|
+
* @param {number} radiusPx - tolerance in screen pixels
|
|
60
|
+
* @returns {{ type: 'vertex'|'edge', coord: number[] } | null}
|
|
61
|
+
*/
|
|
62
|
+
const query = (coord, radiusPx) => {
|
|
63
|
+
const resolution = map.getView().getResolution()
|
|
64
|
+
if (!resolution) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
const toleranceMapUnits = radiusPx * resolution
|
|
68
|
+
const toleranceSq = toleranceMapUnits * toleranceMapUnits
|
|
69
|
+
const ext = [
|
|
70
|
+
coord[0] - toleranceMapUnits,
|
|
71
|
+
coord[1] - toleranceMapUnits,
|
|
72
|
+
coord[0] + toleranceMapUnits,
|
|
73
|
+
coord[1] + toleranceMapUnits
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
let best = null
|
|
77
|
+
|
|
78
|
+
// --- OL VectorLayer sources ---
|
|
79
|
+
for (const layer of olLayers) {
|
|
80
|
+
const source = layer.getSource()
|
|
81
|
+
if (!source) {
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
for (const feature of source.getFeaturesInExtent(ext)) {
|
|
85
|
+
best = pickBest(best, testOLFeature(feature, coord, toleranceSq))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- VectorTile layers ---
|
|
90
|
+
if (vtLayerNames.size > 0) {
|
|
91
|
+
const vtLayers = getVTLayers(map)
|
|
92
|
+
const pixel = vtLayers.length > 0 ? map.getPixelFromCoordinate(coord) : null
|
|
93
|
+
if (pixel) {
|
|
94
|
+
map.forEachFeatureAtPixel(
|
|
95
|
+
pixel,
|
|
96
|
+
(feature, _layer) => {
|
|
97
|
+
if (!vtLayerNames.has(feature.get('mapbox-layer')?.id)) { return }
|
|
98
|
+
best = pickBest(best, testRenderFeature(feature, coord, toleranceSq))
|
|
99
|
+
},
|
|
100
|
+
{ hitTolerance: radiusPx, layerFilter: (l) => vtLayers.includes(l) }
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return best ? { type: best.type, coord: best.coord } : null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { query, setLayers }
|
|
109
|
+
}
|