@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.
Files changed (44) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.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 +27 -3
  6. package/docs/assets/images/hero.png +0 -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/DrawInit.jsx +11 -0
  11. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -25
  12. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +1 -1
  13. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +5 -4
  14. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +49 -21
  15. package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -1
  16. package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -1
  17. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  18. package/plugins/beta/draw-ol/src/core/OLDrawManager.js +3 -0
  19. package/plugins/beta/draw-ol/src/draw/DrawMode.js +57 -49
  20. package/plugins/beta/draw-ol/src/draw/drawInput.js +76 -32
  21. package/plugins/beta/draw-ol/src/edit/EditMode.js +4 -4
  22. package/plugins/beta/draw-ol/src/manifest.js +5 -1
  23. package/plugins/beta/draw-ol/src/reducer.js +1 -1
  24. package/plugins/beta/draw-ol/src/snap/snapEngine.js +75 -31
  25. package/plugins/beta/draw-ol/src/snap/snapGeometry.js +23 -46
  26. package/plugins/beta/draw-ol/src/snap/snapInteraction.js +30 -29
  27. package/plugins/beta/draw-ol/src/snap/snapManager.js +10 -1
  28. package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +9 -3
  29. package/plugins/beta/draw-ol/src/utils/olCoords.js +2 -2
  30. package/plugins/beta/draw-ol/src/utils/spatial.js +4 -8
  31. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  32. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  33. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  34. package/providers/beta/openlayers/src/utils/tileLayers.js +29 -2
  35. package/providers/beta/openlayers/src/utils/tileLayers.test.js +53 -1
  36. package/src/App/components/MapButton/MapButton.jsx +14 -10
  37. package/src/App/components/MapButton/MapButton.module.scss +1 -1
  38. package/src/App/components/MapButton/MapButton.test.jsx +1 -1
  39. package/src/App/components/PopupMenu/PopupMenu.test.jsx +79 -0
  40. package/src/App/components/PopupMenu/usePopupMenu.js +61 -12
  41. package/src/types.js +12 -3
  42. package/src/utils/findNextTabStop.js +3 -3
  43. package/src/utils/findNextTabStop.test.js +25 -1
  44. 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
- * Test all vertices and edges of a coordinate ring/line for snap candidates.
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 getBestPair = (flat, start, numPairs, edgeCount, query, toleranceSq) => {
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 bestOf(best, getBestEdge(flat, start, numPairs, edgeCount, query, toleranceSq))
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
- Point (geom, query, toleranceSq) {
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
- LineString (geom, query, toleranceSq) {
113
+ lineString: (geom, query, toleranceSq) => {
124
114
  return testCoords(geom.getCoordinates(), query, toleranceSq, false)
125
115
  },
126
- LinearRing (geom, query, toleranceSq) {
116
+ linearRing: (geom, query, toleranceSq) => {
127
117
  return testCoords(geom.getCoordinates(), query, toleranceSq, true)
128
118
  },
129
- Polygon (geom, query, toleranceSq) {
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
- MultiLineString (geom, query, toleranceSq) {
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
- MultiPolygon (geom, query, toleranceSq) {
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
- return null
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 = bestOf(best, { type: 'vertex', coord: [flat[0], flat[1]], distSq: dSq })
160
+ best = { type: 'vertex', coord: [flat[0], flat[1]], distSq: dSq }
184
161
  }
185
162
  } else if (type === 'LineString') {
186
- best = bestOf(best, testFlatCoords(flat, 0, flat.length, query, toleranceSq, false))
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
- // No action
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
- export const createSnapInteraction = (engine, indicator, snapRadius) => {
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 (!interaction.getActive()) {
23
- return true
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
- const interaction = createSnapInteraction(engine, indicator, snapRadius)
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) return []
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) return []
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) return []
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.sqrt((start[0] - p[0]) ** 2 + (start[1] - p[1]) ** 2)
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
  }