@defra/interactive-map 0.0.32-alpha → 0.0.33-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/css/index.css +1 -1
- package/dist/esm/im-core.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 +27 -3
- package/docs/assets/images/hero.png +0 -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/DrawInit.jsx +11 -0
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -25
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +1 -1
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +5 -4
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +49 -21
- package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/draw-ol/src/core/OLDrawManager.js +3 -0
- package/plugins/beta/draw-ol/src/draw/DrawMode.js +57 -49
- package/plugins/beta/draw-ol/src/draw/drawInput.js +76 -32
- package/plugins/beta/draw-ol/src/edit/EditMode.js +4 -4
- package/plugins/beta/draw-ol/src/manifest.js +5 -1
- package/plugins/beta/draw-ol/src/reducer.js +1 -1
- package/plugins/beta/draw-ol/src/snap/snapEngine.js +75 -31
- package/plugins/beta/draw-ol/src/snap/snapGeometry.js +23 -46
- package/plugins/beta/draw-ol/src/snap/snapInteraction.js +30 -29
- package/plugins/beta/draw-ol/src/snap/snapManager.js +10 -1
- package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +9 -3
- package/plugins/beta/draw-ol/src/utils/olCoords.js +2 -2
- package/plugins/beta/draw-ol/src/utils/spatial.js +4 -8
- 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/utils/tileLayers.js +29 -2
- package/providers/beta/openlayers/src/utils/tileLayers.test.js +53 -1
- package/src/App/components/MapButton/MapButton.jsx +14 -10
- package/src/App/components/MapButton/MapButton.module.scss +1 -1
- package/src/App/components/MapButton/MapButton.test.jsx +1 -1
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +79 -0
- package/src/App/components/PopupMenu/usePopupMenu.js +61 -12
- package/src/types.js +12 -3
- package/src/utils/findNextTabStop.js +3 -3
- package/src/utils/findNextTabStop.test.js +25 -1
- package/webpack.dev.mjs +1 -0
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure geometry helpers for snap candidate testing.
|
|
3
|
-
* No OL imports, no side effects — only coordinate math.
|
|
4
|
-
*
|
|
5
|
-
* All coordinates are [x, y] pairs in map projection units.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
const dist2 = (a, b) => {
|
|
9
2
|
const dx = a[0] - b[0]
|
|
10
3
|
const dy = a[1] - b[1]
|
|
@@ -41,11 +34,8 @@ const better = (a, b) => {
|
|
|
41
34
|
return b.distSq < a.distSq
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* coords: [[x,y], ...] — OL ring (first === last for closed rings)
|
|
47
|
-
* isClosedRing: if true, OL has duplicated the first coord as last; skip it and wrap edges
|
|
48
|
-
*/
|
|
37
|
+
// OL Vector rings duplicate the first coord as last; VectorTile rings do not.
|
|
38
|
+
// isClosedRing=true: skip the duplicated last vertex, wrap closing edge back to first.
|
|
49
39
|
const testCoords = (coords, query, toleranceSq, isClosedRing) => {
|
|
50
40
|
let best = null
|
|
51
41
|
const n = isClosedRing && coords.length > 1 ? coords.length - 1 : coords.length
|
|
@@ -72,13 +62,6 @@ const testCoords = (coords, query, toleranceSq, isClosedRing) => {
|
|
|
72
62
|
return best
|
|
73
63
|
}
|
|
74
64
|
|
|
75
|
-
/**
|
|
76
|
-
* Test flat coordinate array (stride 2) from a VectorTile RenderFeature.
|
|
77
|
-
* flat: number[] — [x0,y0,x1,y1,...]
|
|
78
|
-
* start/end: index range within flat
|
|
79
|
-
* isClosedRing: VTile polygon rings — first coord is NOT duplicated at end (unlike OL Vector)
|
|
80
|
-
* so treat all coords as unique vertices and add a closing edge back to first
|
|
81
|
-
*/
|
|
82
65
|
const getBestEdge = (flat, start, numPairs, edgeCount, query, toleranceSq) => {
|
|
83
66
|
let best = null
|
|
84
67
|
for (let i = 0; i < edgeCount; i++) {
|
|
@@ -95,7 +78,7 @@ const getBestEdge = (flat, start, numPairs, edgeCount, query, toleranceSq) => {
|
|
|
95
78
|
return best
|
|
96
79
|
}
|
|
97
80
|
|
|
98
|
-
const
|
|
81
|
+
const getBestVertex = (flat, start, numPairs, query, toleranceSq) => {
|
|
99
82
|
let best = null
|
|
100
83
|
for (let i = 0; i < numPairs; i++) {
|
|
101
84
|
const xi = start + i * 2
|
|
@@ -105,7 +88,14 @@ const getBestPair = (flat, start, numPairs, edgeCount, query, toleranceSq) => {
|
|
|
105
88
|
best = bestOf(best, { type: 'vertex', coord: v, distSq: dSq })
|
|
106
89
|
}
|
|
107
90
|
}
|
|
108
|
-
return
|
|
91
|
+
return best
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const getBestPair = (flat, start, numPairs, edgeCount, query, toleranceSq) => {
|
|
95
|
+
return bestOf(
|
|
96
|
+
getBestVertex(flat, start, numPairs, query, toleranceSq),
|
|
97
|
+
getBestEdge(flat, start, numPairs, edgeCount, query, toleranceSq)
|
|
98
|
+
)
|
|
109
99
|
}
|
|
110
100
|
|
|
111
101
|
const testFlatCoords = (flat, start, end, query, toleranceSq, isClosedRing) => {
|
|
@@ -115,32 +105,32 @@ const testFlatCoords = (flat, start, end, query, toleranceSq, isClosedRing) => {
|
|
|
115
105
|
}
|
|
116
106
|
|
|
117
107
|
const olGeomHandlers = {
|
|
118
|
-
|
|
108
|
+
point: (geom, query, toleranceSq) => {
|
|
119
109
|
const c = geom.getCoordinates()
|
|
120
110
|
const dSq = dist2(query, c)
|
|
121
111
|
return dSq <= toleranceSq ? { type: 'vertex', coord: [c[0], c[1]], distSq: dSq } : null
|
|
122
112
|
},
|
|
123
|
-
|
|
113
|
+
lineString: (geom, query, toleranceSq) => {
|
|
124
114
|
return testCoords(geom.getCoordinates(), query, toleranceSq, false)
|
|
125
115
|
},
|
|
126
|
-
|
|
116
|
+
linearRing: (geom, query, toleranceSq) => {
|
|
127
117
|
return testCoords(geom.getCoordinates(), query, toleranceSq, true)
|
|
128
118
|
},
|
|
129
|
-
|
|
119
|
+
polygon: (geom, query, toleranceSq) => {
|
|
130
120
|
let best = null
|
|
131
121
|
for (const ring of geom.getCoordinates()) {
|
|
132
122
|
best = bestOf(best, testCoords(ring, query, toleranceSq, true))
|
|
133
123
|
}
|
|
134
124
|
return best
|
|
135
125
|
},
|
|
136
|
-
|
|
126
|
+
multiLineString: (geom, query, toleranceSq) => {
|
|
137
127
|
let best = null
|
|
138
128
|
for (const line of geom.getCoordinates()) {
|
|
139
129
|
best = bestOf(best, testCoords(line, query, toleranceSq, false))
|
|
140
130
|
}
|
|
141
131
|
return best
|
|
142
132
|
},
|
|
143
|
-
|
|
133
|
+
multiPolygon: (geom, query, toleranceSq) => {
|
|
144
134
|
let best = null
|
|
145
135
|
for (const polygon of geom.getCoordinates()) {
|
|
146
136
|
for (const ring of polygon) {
|
|
@@ -151,27 +141,14 @@ const olGeomHandlers = {
|
|
|
151
141
|
}
|
|
152
142
|
}
|
|
153
143
|
|
|
154
|
-
/**
|
|
155
|
-
* Test an OL Feature (from a VectorSource) against query coord.
|
|
156
|
-
* Handles Point, LineString, LinearRing, Polygon, MultiLineString, MultiPolygon.
|
|
157
|
-
*
|
|
158
|
-
* @returns {{ type: 'vertex'|'edge', coord: number[], distSq: number } | null}
|
|
159
|
-
*/
|
|
160
144
|
export const testOLFeature = (feature, query, toleranceSq) => {
|
|
161
145
|
const geom = feature.getGeometry()
|
|
162
|
-
if (!geom) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const handler = olGeomHandlers[geom.getType()]
|
|
146
|
+
if (!geom) { return null }
|
|
147
|
+
const rawType = geom.getType()
|
|
148
|
+
const handler = olGeomHandlers[rawType[0].toLowerCase() + rawType.slice(1)]
|
|
166
149
|
return handler ? handler(geom, query, toleranceSq) : null
|
|
167
150
|
}
|
|
168
151
|
|
|
169
|
-
/**
|
|
170
|
-
* Test an OL RenderFeature (from a VectorTileSource) against query coord.
|
|
171
|
-
* Handles Point, LineString, Polygon, MultiLineString.
|
|
172
|
-
*
|
|
173
|
-
* @returns {{ type: 'vertex'|'edge', coord: number[], distSq: number } | null}
|
|
174
|
-
*/
|
|
175
152
|
export const testRenderFeature = (feature, query, toleranceSq) => {
|
|
176
153
|
const type = feature.getType()
|
|
177
154
|
const flat = feature.getFlatCoordinates()
|
|
@@ -180,10 +157,10 @@ export const testRenderFeature = (feature, query, toleranceSq) => {
|
|
|
180
157
|
if (type === 'Point') {
|
|
181
158
|
const dSq = dist2(query, flat)
|
|
182
159
|
if (dSq <= toleranceSq) {
|
|
183
|
-
best =
|
|
160
|
+
best = { type: 'vertex', coord: [flat[0], flat[1]], distSq: dSq }
|
|
184
161
|
}
|
|
185
162
|
} else if (type === 'LineString') {
|
|
186
|
-
best =
|
|
163
|
+
best = testFlatCoords(flat, 0, flat.length, query, toleranceSq, false)
|
|
187
164
|
} else if (type === 'Polygon' || type === 'MultiLineString') {
|
|
188
165
|
const ends = feature.getEnds()
|
|
189
166
|
let start = 0
|
|
@@ -193,7 +170,7 @@ export const testRenderFeature = (feature, query, toleranceSq) => {
|
|
|
193
170
|
start = end
|
|
194
171
|
}
|
|
195
172
|
} else {
|
|
196
|
-
//
|
|
173
|
+
// MultiPoint / unknown — no snap candidates
|
|
197
174
|
}
|
|
198
175
|
|
|
199
176
|
return best
|
|
@@ -16,38 +16,39 @@ import Interaction from 'ol/interaction/Interaction.js'
|
|
|
16
16
|
|
|
17
17
|
const SNAP_EVENTS = new Set(['pointermove', 'pointerdrag', 'pointerdown', 'pointerup', 'singleclick', 'click'])
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const processSnapEvent = (mapBrowserEvent, engine, indicator, snapRadius, isIndicatorActive) => {
|
|
20
|
+
const { type } = mapBrowserEvent
|
|
21
|
+
|
|
22
|
+
if (type === 'pointerout' || type === 'pointerleave') {
|
|
23
|
+
indicator.hide()
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!SNAP_EVENTS.has(type)) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = engine.query(mapBrowserEvent.coordinate, snapRadius)
|
|
32
|
+
if (result) {
|
|
33
|
+
mapBrowserEvent.coordinate = result.coord.slice()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Only update indicator during free mouse movement — hide during drag, no-op for clicks
|
|
37
|
+
if (type === 'pointermove' && isIndicatorActive()) {
|
|
38
|
+
result ? indicator.show(result.coord, result.type) : indicator.hide()
|
|
39
|
+
} else if (type === 'pointerdrag') {
|
|
40
|
+
indicator.hide()
|
|
41
|
+
} else {
|
|
42
|
+
// no indicator update for click/down/up events
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const createSnapInteraction = (engine, indicator, snapRadius, isIndicatorActive) => {
|
|
20
47
|
const interaction = new Interaction({
|
|
21
48
|
handleEvent (mapBrowserEvent) {
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const { type } = mapBrowserEvent
|
|
27
|
-
|
|
28
|
-
if (type === 'pointerout' || type === 'pointerleave') {
|
|
29
|
-
indicator.hide()
|
|
30
|
-
return true
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!SNAP_EVENTS.has(type)) {
|
|
34
|
-
return true
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const result = engine.query(mapBrowserEvent.coordinate, snapRadius)
|
|
38
|
-
if (result) {
|
|
39
|
-
mapBrowserEvent.coordinate = result.coord.slice()
|
|
49
|
+
if (interaction.getActive()) {
|
|
50
|
+
processSnapEvent(mapBrowserEvent, engine, indicator, snapRadius, isIndicatorActive)
|
|
40
51
|
}
|
|
41
|
-
|
|
42
|
-
// Only show indicator during free mouse movement — hide during drag and clicks
|
|
43
|
-
if (type === 'pointermove') {
|
|
44
|
-
result ? indicator.show(result.coord, result.type) : indicator.hide()
|
|
45
|
-
} else if (type === 'pointerdrag') {
|
|
46
|
-
indicator.hide()
|
|
47
|
-
} else {
|
|
48
|
-
// No action
|
|
49
|
-
}
|
|
50
|
-
|
|
51
52
|
return true
|
|
52
53
|
}
|
|
53
54
|
})
|
|
@@ -23,7 +23,9 @@ export const createSnapManager = (map, snapLayers, colors, snapRadius) => {
|
|
|
23
23
|
|
|
24
24
|
const engine = createSnapEngine(map, snapLayers)
|
|
25
25
|
const indicator = createSnapIndicator(map, colors)
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
let indicatorActive = false
|
|
28
|
+
const interaction = createSnapInteraction(engine, indicator, snapRadius, () => indicatorActive)
|
|
27
29
|
|
|
28
30
|
map.addInteraction(interaction)
|
|
29
31
|
interaction.setActive(false) // matches reducer initial state: snap: false
|
|
@@ -55,6 +57,13 @@ export const createSnapManager = (map, snapLayers, colors, snapRadius) => {
|
|
|
55
57
|
indicator.hide()
|
|
56
58
|
},
|
|
57
59
|
|
|
60
|
+
setIndicatorActive (value) {
|
|
61
|
+
indicatorActive = value
|
|
62
|
+
if (!value) {
|
|
63
|
+
indicator.hide()
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
58
67
|
setActive (value) {
|
|
59
68
|
active = value
|
|
60
69
|
interaction.setActive(value)
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export const getCoords = (geom) => {
|
|
8
|
-
if (!geom?.coordinates)
|
|
8
|
+
if (!geom?.coordinates) {
|
|
9
|
+
return []
|
|
10
|
+
}
|
|
9
11
|
switch (geom.type) {
|
|
10
12
|
case 'LineString': return geom.coordinates
|
|
11
13
|
case 'Polygon': return geom.coordinates.flatMap(ring => ring.slice(0, -1))
|
|
@@ -20,7 +22,9 @@ export const getCoords = (geom) => {
|
|
|
20
22
|
* { start, length, path, closed }
|
|
21
23
|
*/
|
|
22
24
|
export const getRingSegments = (geom) => {
|
|
23
|
-
if (!geom?.coordinates)
|
|
25
|
+
if (!geom?.coordinates) {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
24
28
|
const segments = []
|
|
25
29
|
let start = 0
|
|
26
30
|
|
|
@@ -80,7 +84,9 @@ export const getModifiableCoords = (geojsonGeometry, path) => {
|
|
|
80
84
|
export const getMidpoints = (geom) => {
|
|
81
85
|
const coords = getCoords(geom)
|
|
82
86
|
const segments = getRingSegments(geom)
|
|
83
|
-
if (!coords.length || !segments.length)
|
|
87
|
+
if (!coords.length || !segments.length) {
|
|
88
|
+
return []
|
|
89
|
+
}
|
|
84
90
|
|
|
85
91
|
const midpoints = []
|
|
86
92
|
for (const seg of segments) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/** OL coordinate [e, n] → screen pixel { x, y } */
|
|
7
7
|
export const coordToPixel = (map, coord) => {
|
|
8
8
|
const px = map.getPixelFromCoordinate(coord)
|
|
9
|
-
if (!px) return null
|
|
9
|
+
if (!px) { return null }
|
|
10
10
|
return { x: px[0], y: px[1] }
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -30,6 +30,6 @@ export const pixelToArray = ({ x, y }) => [x, y]
|
|
|
30
30
|
*/
|
|
31
31
|
export const nudgeCoord = (map, coord, dx, dy) => {
|
|
32
32
|
const px = map.getPixelFromCoordinate(coord)
|
|
33
|
-
if (!px) return coord
|
|
33
|
+
if (!px) { return coord }
|
|
34
34
|
return map.getCoordinateFromPixel([px[0] + dx, px[1] + dy])
|
|
35
35
|
}
|
|
@@ -12,17 +12,13 @@ export const spatialNavigate = (start, pixels, direction) => {
|
|
|
12
12
|
const dx = Math.abs(p[0] - start[0])
|
|
13
13
|
const dy = Math.abs(p[1] - start[1])
|
|
14
14
|
let inQuadrant = false
|
|
15
|
-
if (direction === 'ArrowUp') inQuadrant = p[1] <= start[1] && dy >= dx
|
|
16
|
-
else if (direction === 'ArrowDown') inQuadrant = p[1] > start[1] && dy >= dx
|
|
17
|
-
else if (direction === 'ArrowLeft') inQuadrant = p[0] <= start[0] && dy < dx
|
|
18
|
-
else if (direction === 'ArrowRight') inQuadrant = p[0] > start[0] && dy < dx
|
|
19
|
-
else inQuadrant = true
|
|
15
|
+
if (direction === 'ArrowUp') { inQuadrant = p[1] <= start[1] && dy >= dx } else if (direction === 'ArrowDown') { inQuadrant = p[1] > start[1] && dy >= dx } else if (direction === 'ArrowLeft') { inQuadrant = p[0] <= start[0] && dy < dx } else if (direction === 'ArrowRight') { inQuadrant = p[0] > start[0] && dy < dx } else { inQuadrant = true }
|
|
20
16
|
return inQuadrant && JSON.stringify(p) !== JSON.stringify(start)
|
|
21
17
|
})
|
|
22
18
|
|
|
23
|
-
if (!quadrant.length) quadrant.push(start)
|
|
19
|
+
if (!quadrant.length) { quadrant.push(start) }
|
|
24
20
|
|
|
25
|
-
const dist = (p) => Math.
|
|
26
|
-
const closest = quadrant.reduce((best, p) => dist(p) < dist(best) ? p : best)
|
|
21
|
+
const dist = (p) => Math.hypot(start[0] - p[0], start[1] - p[1])
|
|
22
|
+
const closest = quadrant.reduce((best, p) => dist(p) < dist(best) ? p : best, quadrant[0])
|
|
27
23
|
return pixels.findIndex(p => JSON.stringify(p) === JSON.stringify(closest))
|
|
28
24
|
}
|