@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.
Files changed (93) 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/api/map-style-config.md +30 -0
  6. package/docs/assets/css/docusaurus.scss +4 -0
  7. package/package.json +1 -1
  8. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  9. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  10. package/plugins/beta/draw-ml/src/defaults.js +14 -14
  11. package/plugins/beta/draw-ml/src/mapboxDraw.js +5 -0
  12. package/plugins/beta/draw-ml/src/mapboxSnap.js +1 -1
  13. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +10 -2
  14. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +2 -0
  15. package/plugins/beta/draw-ml/src/reducer.js +4 -1
  16. package/plugins/beta/draw-ml/src/styles.js +42 -39
  17. package/plugins/beta/draw-ol/dist/css/index.css +13 -0
  18. package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -0
  19. package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -0
  20. package/plugins/beta/draw-ol/dist/esm/geometryHelpers.js +1 -0
  21. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -0
  22. package/plugins/beta/draw-ol/dist/esm/index.js +1 -0
  23. package/plugins/beta/draw-ol/src/DrawInit.jsx +67 -0
  24. package/plugins/beta/draw-ol/src/api/addFeature.js +29 -0
  25. package/plugins/beta/draw-ol/src/api/deleteFeature.js +11 -0
  26. package/plugins/beta/draw-ol/src/api/editFeature.js +50 -0
  27. package/plugins/beta/draw-ol/src/api/newLine.js +44 -0
  28. package/plugins/beta/draw-ol/src/api/newPolygon.js +44 -0
  29. package/plugins/beta/draw-ol/src/core/OLDrawManager.js +150 -0
  30. package/plugins/beta/draw-ol/src/core/featureStore.js +65 -0
  31. package/plugins/beta/draw-ol/src/core/styles.js +98 -0
  32. package/plugins/beta/draw-ol/src/core/undoStack.js +31 -0
  33. package/plugins/beta/draw-ol/src/defaults.js +16 -0
  34. package/plugins/beta/draw-ol/src/draw/DrawMode.js +113 -0
  35. package/plugins/beta/draw-ol/src/draw/drawInput.js +199 -0
  36. package/plugins/beta/draw-ol/src/draw.scss +17 -0
  37. package/plugins/beta/draw-ol/src/edit/EditMode.js +424 -0
  38. package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +227 -0
  39. package/plugins/beta/draw-ol/src/edit/midpointLayer.js +57 -0
  40. package/plugins/beta/draw-ol/src/edit/touchHandler.js +171 -0
  41. package/plugins/beta/draw-ol/src/edit/undoOps.js +95 -0
  42. package/plugins/beta/draw-ol/src/edit/vertexHitTest.js +72 -0
  43. package/plugins/beta/draw-ol/src/edit/vertexLayer.js +49 -0
  44. package/plugins/beta/draw-ol/src/edit/vertexOps.js +99 -0
  45. package/plugins/beta/draw-ol/src/events.js +114 -0
  46. package/plugins/beta/draw-ol/src/index.js +12 -0
  47. package/plugins/beta/draw-ol/src/manifest.js +121 -0
  48. package/plugins/beta/draw-ol/src/olDraw.js +38 -0
  49. package/plugins/beta/draw-ol/src/reducer.js +41 -0
  50. package/plugins/beta/draw-ol/src/snap/snapEngine.js +109 -0
  51. package/plugins/beta/draw-ol/src/snap/snapGeometry.js +200 -0
  52. package/plugins/beta/draw-ol/src/snap/snapIndicator.js +81 -0
  53. package/plugins/beta/draw-ol/src/snap/snapInteraction.js +56 -0
  54. package/plugins/beta/draw-ol/src/snap/snapManager.js +90 -0
  55. package/plugins/beta/draw-ol/src/utils/flattenStyleProperties.js +47 -0
  56. package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +97 -0
  57. package/plugins/beta/draw-ol/src/utils/olCoords.js +35 -0
  58. package/plugins/beta/draw-ol/src/utils/resolveColors.js +39 -0
  59. package/plugins/beta/draw-ol/src/utils/spatial.js +28 -0
  60. package/plugins/beta/draw-ol/src/utils/touchTarget.js +61 -0
  61. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  62. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  63. package/plugins/interact/src/defaults.js +1 -0
  64. package/plugins/interact/src/hooks/useInteractionHandlers.js +150 -43
  65. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +240 -8
  66. package/plugins/interact/src/reducer.js +7 -0
  67. package/plugins/interact/src/reducer.test.js +16 -0
  68. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  69. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  70. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  71. package/providers/beta/openlayers/src/appEvents.js +9 -13
  72. package/providers/beta/openlayers/src/appEvents.test.js +27 -18
  73. package/providers/beta/openlayers/src/mapEvents.js +34 -20
  74. package/providers/beta/openlayers/src/mapEvents.test.js +65 -6
  75. package/providers/beta/openlayers/src/openlayersProvider.js +45 -26
  76. package/providers/beta/openlayers/src/openlayersProvider.test.js +12 -34
  77. package/providers/beta/openlayers/src/utils/highlightFeatures.js +245 -0
  78. package/providers/beta/openlayers/src/utils/hoverCursor.js +65 -0
  79. package/providers/beta/openlayers/src/utils/hoverCursor.test.js +235 -0
  80. package/providers/beta/openlayers/src/utils/openLayersFixes.js +11 -0
  81. package/providers/beta/openlayers/src/utils/openLayersFixes.test.js +53 -0
  82. package/providers/beta/openlayers/src/utils/queryFeatures.js +77 -0
  83. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +181 -0
  84. package/providers/beta/openlayers/src/utils/tileLayers.js +23 -5
  85. package/providers/beta/openlayers/src/utils/tileLayers.test.js +81 -5
  86. package/providers/beta/openlayers/src/utils/vtTileFragments.js +86 -0
  87. package/rollup.esm.mjs +6 -0
  88. package/src/App/controls/keyboardActions.js +2 -2
  89. package/src/InteractiveMap/InteractiveMap.js +38 -10
  90. package/src/InteractiveMap/InteractiveMap.test.js +282 -356
  91. package/src/InteractiveMap/historyManager.js +7 -0
  92. package/src/InteractiveMap/historyManager.test.js +11 -0
  93. 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
+ }