@defra/interactive-map 0.0.31-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/README.md +2 -2
- package/assets/images/bright-ofm-thumb.jpg +0 -0
- package/assets/images/dark-ofm-thumb.jpg +0 -0
- package/assets/images/road-ofm-thumb.jpg +0 -0
- package/assets/templates/add-polygons.njk +143 -0
- package/assets/templates/add-symbols.njk +119 -0
- package/assets/templates/{map.njk → basic-map.njk} +9 -17
- package/assets/templates/button-map.njk +56 -0
- package/assets/templates/draw-tools.njk +161 -0
- package/assets/templates/marker-panel.njk +91 -0
- package/assets/templates/place-marker.njk +104 -0
- package/assets/templates/search-control.njk +98 -0
- package/assets/templates/select-feature.njk +101 -0
- package/assets/templates/style-switcher.njk +87 -0
- package/assets/templates/toggle-marker-label.njk +74 -0
- package/dist/css/index.css +1 -1
- 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 +27 -3
- package/docs/api.md +55 -0
- package/docs/assets/css/docusaurus.scss +22 -6
- package/docs/assets/images/add-polygons.jpg +0 -0
- package/docs/assets/images/add-symbols.jpg +0 -0
- package/docs/assets/images/hero.png +0 -0
- package/docs/assets/images/place-marker.jpg +0 -0
- package/docs/assets/images/screenshot.jpg +0 -0
- package/docs/examples/add-marker-with-panel.mdx +1 -1
- package/docs/examples/add-polygons.mdx +225 -0
- package/docs/examples/add-symbols.mdx +177 -0
- package/docs/examples/{draw.mdx → draw-tools.mdx} +2 -2
- package/docs/examples/index.mdx +38 -2
- package/docs/examples/place-marker.mdx +151 -0
- package/docs/plugins/datasets.md +144 -96
- package/docs/plugins/interact.md +1 -4
- package/docusaurus.config.cjs +5 -2
- package/govuk-prototype-kit.config.json +52 -2
- package/package.json +1 -1
- package/plugins/beta/datasets/dist/css/index.css +1 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +27 -9
- package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +46 -4
- package/plugins/beta/datasets/src/api/setFeatureVisibility.js +10 -1
- package/plugins/beta/datasets/src/api/setFeatureVisibility.test.js +48 -6
- package/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss +12 -12
- package/plugins/beta/datasets/src/reducers/datasetsToMenu.js +11 -6
- package/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js +49 -0
- package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.js +8 -1
- package/plugins/beta/datasets/src/reducers/pluginState.js +1 -1
- package/plugins/beta/datasets/src/registry/dataset.js +7 -3
- package/plugins/beta/datasets/src/registry/dataset.test.js +14 -0
- package/plugins/beta/datasets/src/registry/dynamicGeoJson.js +1 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/api/addFeature.js +12 -3
- package/plugins/beta/draw-es/src/events.js +8 -3
- package/plugins/beta/draw-es/src/events.test.js +2 -0
- package/plugins/beta/draw-es/src/graphic.js +26 -2
- 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 +19 -0
- package/plugins/beta/draw-ml/src/api/newLine.js +1 -0
- package/plugins/beta/draw-ml/src/api/newPolygon.js +1 -0
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +52 -37
- 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 +53 -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/edit/keyboardHandler.js +4 -2
- 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/plugins/beta/map-styles/src/mapStyles.scss +41 -41
- 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/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +1 -3
- package/plugins/interact/src/InteractInit.test.js +0 -2
- package/plugins/interact/src/events.test.js +1 -1
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +4 -10
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +5 -10
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -10
- package/plugins/interact/src/hooks/useHighlightSync.test.js +2 -21
- package/plugins/interact/src/hooks/useInteractionHandlers.js +5 -11
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -25
- package/plugins/interact/src/reducer.js +4 -22
- package/plugins/interact/src/reducer.test.js +11 -39
- package/plugins/search/dist/css/index.css +5 -2
- package/plugins/search/src/components/Form/Form.module.scss +5 -2
- package/plugins/search/src/search.scss +4 -4
- 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/CrossHair/CrossHair.module.scss +8 -8
- 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/App/hooks/useLayoutMeasurements.js +10 -2
- package/src/App/hooks/useResizeObserver.js +4 -2
- package/src/InteractiveMap/InteractiveMap.js +4 -4
- package/src/InteractiveMap/InteractiveMap.test.js +3 -3
- package/src/InteractiveMap/behaviourController.js +3 -1
- package/src/InteractiveMap/behaviourController.test.js +6 -3
- package/src/InteractiveMap/domStateManager.js +2 -0
- package/src/config/events.js +21 -3
- 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
- package/docs/assets/images/screens-blue.jpg +0 -0
- package/docs/assets/images/screens-white.jpg +0 -0
|
@@ -1,21 +1,68 @@
|
|
|
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
|
-
|
|
1
|
+
import { transform as projTransform } from 'ol/proj.js'
|
|
15
2
|
import VectorLayer from 'ol/layer/Vector.js'
|
|
16
3
|
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
17
4
|
import { testOLFeature, testRenderFeature } from './snapGeometry.js'
|
|
18
5
|
|
|
6
|
+
// VectorTile features are clipped to tile extents, producing artificial straight edges at tile
|
|
7
|
+
// boundaries. MVT tiles also carry a buffer region (geometry from adjacent tiles extending
|
|
8
|
+
// past the tile boundary). Both the exact boundary and the buffer zone must be filtered.
|
|
9
|
+
// Standard MVT buffer is 128 out of 4096 tile coordinate units.
|
|
10
|
+
const TILE_BOUNDARY_EPS = 1 // source-projection units of margin beyond the buffer
|
|
11
|
+
const MVT_BUFFER_UNITS = 128
|
|
12
|
+
const MVT_TILE_EXTENT = 4096
|
|
13
|
+
|
|
14
|
+
const buildTileBoundaryState = (layer, map) => {
|
|
15
|
+
const source = layer.getSource()
|
|
16
|
+
const tileGrid = source?.getTileGrid()
|
|
17
|
+
if (!tileGrid) { return null }
|
|
18
|
+
const viewProj = map.getView().getProjection()
|
|
19
|
+
const sourceProj = source.getProjection() ?? viewProj
|
|
20
|
+
const zoom = tileGrid.getZForResolution(map.getView().getResolution(), 0)
|
|
21
|
+
return { tileGrid, viewProj, sourceProj, zoom }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isOnTileBoundary = (state, coord) => {
|
|
25
|
+
if (!state) { return false }
|
|
26
|
+
const { tileGrid, viewProj, sourceProj, zoom } = state
|
|
27
|
+
const c = projTransform(coord, viewProj, sourceProj)
|
|
28
|
+
const tileCoord = tileGrid.getTileCoordForCoordAndZ(c, zoom)
|
|
29
|
+
const [minX, minY, maxX, maxY] = tileGrid.getTileCoordExtent(tileCoord)
|
|
30
|
+
// Buffer zone in source projection: edges within this distance of a tile boundary are
|
|
31
|
+
// MVT buffer clip artefacts (geometry from the adjacent tile included for rendering overlap).
|
|
32
|
+
const tileBuffer = (maxX - minX) * (MVT_BUFFER_UNITS / MVT_TILE_EXTENT)
|
|
33
|
+
const eps = tileBuffer + TILE_BOUNDARY_EPS
|
|
34
|
+
return (
|
|
35
|
+
Math.abs(c[0] - minX) < eps ||
|
|
36
|
+
Math.abs(c[0] - maxX) < eps ||
|
|
37
|
+
Math.abs(c[1] - minY) < eps ||
|
|
38
|
+
Math.abs(c[1] - maxY) < eps
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Two same-style fill polygons share an invisible boundary — snapping to it is confusing.
|
|
43
|
+
// Detect this by projecting a test point slightly past the snap position (away from the cursor)
|
|
44
|
+
// and checking whether the same fill layer covers that side too.
|
|
45
|
+
// If it does → same-style invisible boundary → skip.
|
|
46
|
+
// If not (empty space, road, different layer) → visible outer edge → include.
|
|
47
|
+
const INVISIBLE_FILL_MIN_DIST_SQ = 1 // (1m)² — below this, cursor is on the edge, skip direction check
|
|
48
|
+
|
|
49
|
+
const isInvisibleFillBoundary = (edgeCoord, cursorCoord, layerId, map, vtLayers, resolution) => {
|
|
50
|
+
const dx = edgeCoord[0] - cursorCoord[0]
|
|
51
|
+
const dy = edgeCoord[1] - cursorCoord[1]
|
|
52
|
+
const distSq = dx * dx + dy * dy
|
|
53
|
+
if (distSq < INVISIBLE_FILL_MIN_DIST_SQ) { return false }
|
|
54
|
+
const dist = Math.sqrt(distSq)
|
|
55
|
+
const step = resolution * 4
|
|
56
|
+
const testCoord = [edgeCoord[0] + (dx / dist) * step, edgeCoord[1] + (dy / dist) * step]
|
|
57
|
+
const testPixel = map.getPixelFromCoordinate(testCoord)
|
|
58
|
+
if (!testPixel) { return false }
|
|
59
|
+
return !!map.forEachFeatureAtPixel(
|
|
60
|
+
testPixel,
|
|
61
|
+
(f) => f.get('mapbox-layer')?.id === layerId,
|
|
62
|
+
{ hitTolerance: 2, layerFilter: (l) => vtLayers.includes(l) }
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
19
66
|
const pickBest = (a, b) => {
|
|
20
67
|
if (!b) { return a }
|
|
21
68
|
if (!a) { return b }
|
|
@@ -53,17 +100,9 @@ export const createSnapEngine = (map, snapLayers = []) => {
|
|
|
53
100
|
|
|
54
101
|
setLayers(snapLayers)
|
|
55
102
|
|
|
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
103
|
const query = (coord, radiusPx) => {
|
|
63
104
|
const resolution = map.getView().getResolution()
|
|
64
|
-
if (!resolution) {
|
|
65
|
-
return null
|
|
66
|
-
}
|
|
105
|
+
if (!resolution) { return null }
|
|
67
106
|
const toleranceMapUnits = radiusPx * resolution
|
|
68
107
|
const toleranceSq = toleranceMapUnits * toleranceMapUnits
|
|
69
108
|
const ext = [
|
|
@@ -75,27 +114,32 @@ export const createSnapEngine = (map, snapLayers = []) => {
|
|
|
75
114
|
|
|
76
115
|
let best = null
|
|
77
116
|
|
|
78
|
-
// --- OL VectorLayer sources ---
|
|
79
117
|
for (const layer of olLayers) {
|
|
80
118
|
const source = layer.getSource()
|
|
81
|
-
if (!source) {
|
|
82
|
-
continue
|
|
83
|
-
}
|
|
119
|
+
if (!source) { continue }
|
|
84
120
|
for (const feature of source.getFeaturesInExtent(ext)) {
|
|
85
121
|
best = pickBest(best, testOLFeature(feature, coord, toleranceSq))
|
|
86
122
|
}
|
|
87
123
|
}
|
|
88
124
|
|
|
89
|
-
// --- VectorTile layers ---
|
|
90
125
|
if (vtLayerNames.size > 0) {
|
|
91
126
|
const vtLayers = getVTLayers(map)
|
|
92
127
|
const pixel = vtLayers.length > 0 ? map.getPixelFromCoordinate(coord) : null
|
|
93
128
|
if (pixel) {
|
|
129
|
+
const tileBoundaryStates = new Map()
|
|
94
130
|
map.forEachFeatureAtPixel(
|
|
95
131
|
pixel,
|
|
96
|
-
(feature,
|
|
97
|
-
|
|
98
|
-
|
|
132
|
+
(feature, layer) => {
|
|
133
|
+
const mapboxLayer = feature.get('mapbox-layer')
|
|
134
|
+
if (!vtLayerNames.has(mapboxLayer?.id)) { return }
|
|
135
|
+
const candidate = testRenderFeature(feature, coord, toleranceSq)
|
|
136
|
+
if (!candidate) { return }
|
|
137
|
+
if (!tileBoundaryStates.has(layer)) {
|
|
138
|
+
tileBoundaryStates.set(layer, buildTileBoundaryState(layer, map))
|
|
139
|
+
}
|
|
140
|
+
if (isOnTileBoundary(tileBoundaryStates.get(layer), candidate.coord)) { return }
|
|
141
|
+
if (candidate.type === 'edge' && mapboxLayer?.type === 'fill' && isInvisibleFillBoundary(candidate.coord, coord, mapboxLayer.id, map, vtLayers, resolution)) { return }
|
|
142
|
+
best = pickBest(best, candidate)
|
|
99
143
|
},
|
|
100
144
|
{ hitTolerance: radiusPx, layerFilter: (l) => vtLayers.includes(l) }
|
|
101
145
|
)
|
|
@@ -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
|
}
|
|
@@ -1,84 +1,84 @@
|
|
|
1
1
|
.im-c-map-styles {
|
|
2
|
-
|
|
2
|
+
display: block;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
.im-c-map-styles__inner {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-wrap: wrap;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
.im-c-map-styles__heading {
|
|
11
|
-
|
|
11
|
+
padding: var(--primary-gap) 0;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
.im-c-map-styles__item {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
display: flex;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
padding: calc(var(--divider-gap) / 2);
|
|
18
|
+
flex: 0 0 33.3333%;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
min-width: 90px;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
.im-c-map-styles__button {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
align-items: center;
|
|
27
|
+
border: 0;
|
|
28
|
+
margin: 0;
|
|
29
|
+
color: var(--foreground-color);
|
|
30
|
+
background-color: transparent;
|
|
31
|
+
cursor: pointer;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.im-c-map-styles__image {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
position: relative;
|
|
36
|
+
padding: 2px;
|
|
37
|
+
border: 3px solid transparent;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
.im-c-map-styles__image svg {
|
|
41
|
-
|
|
41
|
+
fill: currentColor;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
.im-c-map-styles__image-bg {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
fill: none;
|
|
46
|
+
stroke: var(--image-placeholder-color);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
.im-c-map-styles img {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
display: block;
|
|
51
|
+
// border: 1px solid var(--image-placeholder-color);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// Text size
|
|
55
55
|
.im-c-map-styles__group:not(:first-child) {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
margin-top: var(--divider-gap);
|
|
57
|
+
border-top: 1px solid var(--content-border-color);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// State
|
|
61
61
|
.im-c-map-styles__button[aria-pressed="true"] .im-c-map-styles__image {
|
|
62
|
-
|
|
62
|
+
border-color: var(--foreground-color);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
.im-c-map-styles__button:focus {
|
|
66
|
-
|
|
66
|
+
outline: 0;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
.im-c-map-styles__button[data-focus-visible="true"] {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
.im-c-map-styles__image::after {
|
|
71
|
+
content: '';
|
|
72
|
+
position: absolute;
|
|
73
|
+
top: -3px;
|
|
74
|
+
right: -3px;
|
|
75
|
+
bottom: -3px;
|
|
76
|
+
left: -3px;
|
|
77
|
+
outline: 3px solid var(--focus-outline-color);
|
|
78
|
+
border: 3px solid var(--focus-border-color);
|
|
79
|
+
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
.im-c-map-styles__button:hover:not([aria-pressed="true"]) .im-c-map-styles__image {
|
|
83
|
-
|
|
83
|
+
border-color: var(--button-hover-color);
|
|
84
84
|
}
|