@defra/interactive-map 0.0.36-alpha → 0.0.38-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/assets/templates/draw-tools.njk +4 -3
- 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.md +32 -0
- package/docs/examples/draw-tools.mdx +0 -4
- package/docs/plugins/draw.md +482 -0
- package/docs/plugins.md +2 -6
- package/govuk-prototype-kit.config.json +3 -2
- package/package.json +2 -4
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/events.js +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-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/css/index.css +1 -97
- package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +13 -0
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
- package/plugins/draw/dist/esm/index.js +1 -0
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
- package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/index.js +2 -0
- package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/draw/src/DrawInit.jsx +89 -0
- package/plugins/draw/src/DrawInit.test.jsx +184 -0
- package/plugins/draw/src/adapterEvents.js +127 -0
- package/plugins/draw/src/adapterEvents.test.js +20 -0
- package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
- package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
- package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
- package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
- package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
- package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
- package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
- package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
- package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
- package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
- package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
- package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
- package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
- package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
- package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
- package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
- package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
- package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
- package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
- package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
- package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
- package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
- package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
- package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
- package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
- package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
- package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
- package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
- package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
- package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
- package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
- package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
- package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
- package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
- package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
- package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
- package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
- package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
- package/plugins/draw/src/api/addFeature.js +22 -0
- package/plugins/draw/src/api/addFeature.test.js +41 -0
- package/plugins/draw/src/api/deleteFeature.js +11 -0
- package/plugins/draw/src/api/deleteFeature.test.js +24 -0
- package/plugins/draw/src/api/editFeature.js +54 -0
- package/plugins/draw/src/api/editFeature.test.js +134 -0
- package/plugins/draw/src/api/merge.js +30 -0
- package/plugins/draw/src/api/merge.test.js +57 -0
- package/plugins/draw/src/api/newLine.js +41 -0
- package/plugins/draw/src/api/newLine.test.js +93 -0
- package/plugins/draw/src/api/newPolygon.js +41 -0
- package/plugins/draw/src/api/newPolygon.test.js +93 -0
- package/plugins/draw/src/api/split.js +117 -0
- package/plugins/draw/src/api/split.test.js +204 -0
- package/plugins/draw/src/defaults.js +52 -0
- package/plugins/draw/src/defaults.test.js +18 -0
- package/plugins/draw/src/draw.scss +43 -0
- package/plugins/draw/src/events.js +237 -0
- package/plugins/draw/src/events.test.js +369 -0
- package/plugins/draw/src/index.js +10 -0
- package/plugins/draw/src/index.test.js +24 -0
- package/plugins/draw/src/manifest.js +170 -0
- package/plugins/draw/src/manifest.test.js +147 -0
- package/plugins/draw/src/reducer.js +69 -0
- package/plugins/draw/src/reducer.test.js +85 -0
- package/plugins/draw/src/utils/debounce.js +16 -0
- package/plugins/draw/src/utils/debounce.test.js +40 -0
- package/plugins/draw/src/utils/eventBus.js +36 -0
- package/plugins/draw/src/utils/eventBus.test.js +47 -0
- package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
- package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
- package/plugins/draw/src/utils/getValueForStyle.js +33 -0
- package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
- package/plugins/draw/src/utils/resolveColors.js +39 -0
- package/plugins/draw/src/utils/resolveColors.test.js +34 -0
- package/plugins/draw/src/utils/spatial.js +243 -0
- package/plugins/draw/src/utils/spatial.test.js +243 -0
- package/plugins/draw/src/utils/touchTarget.js +95 -0
- package/plugins/draw/src/utils/touchTarget.test.js +107 -0
- package/plugins/draw/src/utils/undoStack.js +31 -0
- package/plugins/draw/src/utils/undoStack.test.js +49 -0
- package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
- package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
- package/plugins/draw/src/validation/liveStroke.js +85 -0
- package/plugins/draw/src/validation/liveStroke.test.js +150 -0
- package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
- package/plugins/draw/src/validation/rules.js +186 -0
- package/plugins/draw/src/validation/rules.test.js +120 -0
- package/plugins/draw/src/validation/validateGeometry.js +119 -0
- package/plugins/draw/src/validation/validateGeometry.test.js +200 -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/hooks/useInteractionHandlers.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
- package/plugins/interact/src/utils/buildStylesMap.js +1 -0
- package/plugins/search/dist/css/index.css +1 -317
- package/rollup.esm.mjs +35 -28
- package/sonar-project.properties +2 -2
- package/src/App/components/Attributions/Attributions.module.scss +1 -0
- package/src/App/components/Hints/Hints.module.scss +5 -4
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
- package/src/App/components/MoveControl/MoveControl.jsx +73 -9
- package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
- package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
- package/src/App/hooks/useHintsAPI.js +67 -0
- package/src/App/hooks/useHintsAPI.test.js +145 -0
- package/src/App/renderer/PluginInits.jsx +4 -0
- package/src/App/renderer/PluginInits.test.jsx +7 -1
- package/src/InteractiveMap/InteractiveMap.js +20 -0
- package/src/InteractiveMap/InteractiveMap.test.js +7 -0
- package/src/config/events.js +4 -0
- package/src/scss/settings/_dimensions.scss +3 -0
- package/src/services/announcer.js +38 -3
- package/src/services/announcer.test.js +133 -1
- package/src/services/hints.js +3 -0
- package/src/services/hints.test.js +19 -0
- package/src/types.js +12 -0
- package/src/utils/detectInterfaceType.js +5 -1
- package/src/utils/isMac.js +15 -0
- package/src/utils/isMac.test.js +37 -0
- package/webpack.umd.mjs +1 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import MapboxSnap from 'mapbox-gl-snap/dist/esm/MapboxSnap.js'
|
|
2
|
+
import { polygon, lineString } from '@turf/helpers'
|
|
3
|
+
import { TOLERANCES } from '../defaults.js'
|
|
4
|
+
import { SNAP_HELPER_LAYER } from './constants.js'
|
|
5
|
+
|
|
6
|
+
const MAX_SNAP_FEATURE_CACHE = 100
|
|
7
|
+
|
|
8
|
+
// Skip setMapData/drawingSnapCheck when disabled; ensure layer visibility when enabled
|
|
9
|
+
function patchDataMethods (proto, orig) {
|
|
10
|
+
proto.setMapData = function (data) {
|
|
11
|
+
if (!this.status) {
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
const result = orig.setMapData.call(this, data)
|
|
15
|
+
if (data?.features?.length > 0 && this.map?.getLayer(SNAP_HELPER_LAYER)) {
|
|
16
|
+
this.map.setLayoutProperty(SNAP_HELPER_LAYER, 'visibility', 'visible')
|
|
17
|
+
}
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Skip drawingSnapCheck when disabled
|
|
22
|
+
proto.drawingSnapCheck = function () {
|
|
23
|
+
if (!this.status) {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
return orig.drawingSnapCheck.call(this)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fix typo ('coodinates') and validate coordinates; query within a radius bbox
|
|
31
|
+
function patchGeometryMethods (proto, orig) {
|
|
32
|
+
proto.getLines = function (feature, mouse, radiusArg) {
|
|
33
|
+
const geom = feature.geometry
|
|
34
|
+
if (!geom?.coordinates) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
const coords = geom.coordinates
|
|
38
|
+
// Validate that we have actual coordinate arrays
|
|
39
|
+
if (!Array.isArray(coords) || coords.length === 0) {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
if (geom.type === 'MultiPolygon') {
|
|
44
|
+
return coords.filter(c => Array.isArray(c) && c.length > 0).map(c => polygon(c))
|
|
45
|
+
}
|
|
46
|
+
if (geom.type === 'MultiLineString') {
|
|
47
|
+
return coords.filter(c => Array.isArray(c) && c.length > 0).map(c => lineString(c))
|
|
48
|
+
}
|
|
49
|
+
return orig.getLines.call(this, feature, mouse, radiusArg)
|
|
50
|
+
} catch {
|
|
51
|
+
// Invalid geometry - skip this feature
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Query within radius bbox instead of just point, filter to existing layers
|
|
57
|
+
proto.getCloseFeatures = function (e, radiusInMeters) {
|
|
58
|
+
if (!this.status) {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
// Use active layers (per-call override) or fall back to default layers
|
|
62
|
+
const activeLayers = this._activeLayers || this._defaultLayers || []
|
|
63
|
+
this.options.layers = activeLayers.filter(l => this.map.getLayer(l))
|
|
64
|
+
const r = this.options.radius || TOLERANCES.snapRadius
|
|
65
|
+
const origPt = e.point
|
|
66
|
+
e.point = [[origPt.x - r, origPt.y - r], [origPt.x + r, origPt.y + r]]
|
|
67
|
+
const result = orig.getCloseFeatures.call(this, e, radiusInMeters)
|
|
68
|
+
e.point = origPt
|
|
69
|
+
return result
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Custom colors for snap indicators
|
|
74
|
+
function patchColorMethods (proto, orig, colors) {
|
|
75
|
+
proto.searchInVertex = function (...args) {
|
|
76
|
+
const r = orig.searchInVertex.apply(this, args)
|
|
77
|
+
if (r) {
|
|
78
|
+
r.color = colors.vertex
|
|
79
|
+
}
|
|
80
|
+
return r
|
|
81
|
+
}
|
|
82
|
+
proto.searchInMidPoint = function (...args) {
|
|
83
|
+
const r = orig.searchInMidPoint.apply(this, args)
|
|
84
|
+
if (r) {
|
|
85
|
+
r.color = colors.midpoint
|
|
86
|
+
}
|
|
87
|
+
return r
|
|
88
|
+
}
|
|
89
|
+
proto.searchInEdge = function (...args) {
|
|
90
|
+
const r = orig.searchInEdge.apply(this, args)
|
|
91
|
+
if (r) {
|
|
92
|
+
r.color = colors.edge
|
|
93
|
+
}
|
|
94
|
+
return r
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Skip when disabled or zooming; clean up internal arrays to prevent memory accumulation
|
|
99
|
+
function patchSnapMethod (proto, orig) {
|
|
100
|
+
proto.snapToClosestPoint = function (e) {
|
|
101
|
+
if (!this.status || this.map?._isZooming) {
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const result = orig.snapToClosestPoint.call(this, e)
|
|
106
|
+
if (this.closeFeatures?.length > MAX_SNAP_FEATURE_CACHE) {
|
|
107
|
+
this.closeFeatures.length = 0
|
|
108
|
+
}
|
|
109
|
+
if (this.lines?.length > MAX_SNAP_FEATURE_CACHE) {
|
|
110
|
+
this.lines.length = 0
|
|
111
|
+
}
|
|
112
|
+
return result
|
|
113
|
+
} catch {
|
|
114
|
+
// Invalid geometry encountered - clear state and continue
|
|
115
|
+
this.snapStatus = false
|
|
116
|
+
this.snapCoords = null
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Apply patches to MapboxSnap prototype (once only) */
|
|
123
|
+
export function applyMapboxSnapPatches (colors) {
|
|
124
|
+
if (MapboxSnap.prototype.__snapPatched) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
MapboxSnap.prototype.__snapPatched = true
|
|
128
|
+
|
|
129
|
+
const proto = MapboxSnap.prototype
|
|
130
|
+
const orig = {
|
|
131
|
+
setMapData: proto.setMapData,
|
|
132
|
+
drawingSnapCheck: proto.drawingSnapCheck,
|
|
133
|
+
getLines: proto.getLines,
|
|
134
|
+
getCloseFeatures: proto.getCloseFeatures,
|
|
135
|
+
searchInVertex: proto.searchInVertex,
|
|
136
|
+
searchInMidPoint: proto.searchInMidPoint,
|
|
137
|
+
searchInEdge: proto.searchInEdge,
|
|
138
|
+
snapToClosestPoint: proto.snapToClosestPoint
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Disable changeSnappedPoints - we handle snap ourselves in drag handlers
|
|
142
|
+
proto.changeSnappedPoints = () => {}
|
|
143
|
+
|
|
144
|
+
patchDataMethods(proto, orig)
|
|
145
|
+
patchGeometryMethods(proto, orig)
|
|
146
|
+
patchColorMethods(proto, orig, colors)
|
|
147
|
+
patchSnapMethod(proto, orig)
|
|
148
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import MapboxSnap from 'mapbox-gl-snap/dist/esm/MapboxSnap.js'
|
|
2
|
+
import { polygon, lineString } from '@turf/helpers'
|
|
3
|
+
import { applyMapboxSnapPatches } from './prototypePatches.js'
|
|
4
|
+
|
|
5
|
+
const SNAP_LAYER = 'snap-helper-circle'
|
|
6
|
+
const COLORS = { vertex: 'colour-v', midpoint: 'colour-m', edge: 'colour-e' }
|
|
7
|
+
|
|
8
|
+
jest.mock('mapbox-gl-snap/dist/esm/MapboxSnap.js', () => {
|
|
9
|
+
const orig = {
|
|
10
|
+
setMapData: jest.fn(function () { return 'orig-setMapData' }),
|
|
11
|
+
drawingSnapCheck: jest.fn(function () { return 'orig-drawingSnapCheck' }),
|
|
12
|
+
getLines: jest.fn(function () { return 'orig-getLines' }),
|
|
13
|
+
getCloseFeatures: jest.fn(function () { return 'orig-getCloseFeatures' }),
|
|
14
|
+
searchInVertex: jest.fn(function () { return { id: 'v' } }),
|
|
15
|
+
searchInMidPoint: jest.fn(function () { return { id: 'm' } }),
|
|
16
|
+
searchInEdge: jest.fn(function () { return { id: 'e' } }),
|
|
17
|
+
snapToClosestPoint: jest.fn(function () { return 'orig-snap' })
|
|
18
|
+
}
|
|
19
|
+
const MockSnap = jest.fn()
|
|
20
|
+
Object.assign(MockSnap.prototype, orig)
|
|
21
|
+
MockSnap.__orig = orig
|
|
22
|
+
return { __esModule: true, default: MockSnap }
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
jest.mock('@turf/helpers', () => ({
|
|
26
|
+
polygon: jest.fn((c) => ({ poly: c })),
|
|
27
|
+
lineString: jest.fn((c) => ({ line: c }))
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
jest.mock('../defaults.js', () => ({ TOLERANCES: { snapRadius: 12 } }))
|
|
31
|
+
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
applyMapboxSnapPatches(COLORS)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest.clearAllMocks()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('applyMapboxSnapPatches', () => {
|
|
41
|
+
test('patches the prototype only once', () => {
|
|
42
|
+
const patched = MapboxSnap.prototype.setMapData
|
|
43
|
+
applyMapboxSnapPatches(COLORS)
|
|
44
|
+
expect(MapboxSnap.prototype.setMapData).toBe(patched)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('replaces changeSnappedPoints with a no-op', () => {
|
|
48
|
+
expect(MapboxSnap.prototype.changeSnappedPoints()).toBeUndefined()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('setMapData', () => {
|
|
53
|
+
test('returns undefined and skips the original when disabled', () => {
|
|
54
|
+
expect(MapboxSnap.prototype.setMapData.call({ status: false }, {})).toBeUndefined()
|
|
55
|
+
expect(MapboxSnap.__orig.setMapData).not.toHaveBeenCalled()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('delegates and shows the layer when features exist and the layer is present', () => {
|
|
59
|
+
const map = { getLayer: jest.fn(() => ({})), setLayoutProperty: jest.fn() }
|
|
60
|
+
const data = { features: [{}] }
|
|
61
|
+
|
|
62
|
+
const result = MapboxSnap.prototype.setMapData.call({ status: true, map }, data)
|
|
63
|
+
|
|
64
|
+
expect(MapboxSnap.__orig.setMapData).toHaveBeenCalledWith(data)
|
|
65
|
+
expect(result).toBe('orig-setMapData')
|
|
66
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith(SNAP_LAYER, 'visibility', 'visible')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('does not show the layer when there are no features', () => {
|
|
70
|
+
const map = { getLayer: jest.fn(() => ({})), setLayoutProperty: jest.fn() }
|
|
71
|
+
MapboxSnap.prototype.setMapData.call({ status: true, map }, { features: [] })
|
|
72
|
+
expect(map.setLayoutProperty).not.toHaveBeenCalled()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('does not show the layer when it is absent', () => {
|
|
76
|
+
const map = { getLayer: jest.fn(() => null), setLayoutProperty: jest.fn() }
|
|
77
|
+
MapboxSnap.prototype.setMapData.call({ status: true, map }, { features: [{}] })
|
|
78
|
+
expect(map.setLayoutProperty).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('tolerates a missing map and missing data', () => {
|
|
82
|
+
expect(MapboxSnap.prototype.setMapData.call({ status: true }, { features: [{}] })).toBe('orig-setMapData')
|
|
83
|
+
expect(MapboxSnap.prototype.setMapData.call({ status: true }, undefined)).toBe('orig-setMapData')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('drawingSnapCheck', () => {
|
|
88
|
+
test('returns undefined when disabled', () => {
|
|
89
|
+
expect(MapboxSnap.prototype.drawingSnapCheck.call({ status: false })).toBeUndefined()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('delegates to the original when enabled', () => {
|
|
93
|
+
expect(MapboxSnap.prototype.drawingSnapCheck.call({ status: true })).toBe('orig-drawingSnapCheck')
|
|
94
|
+
expect(MapboxSnap.__orig.drawingSnapCheck).toHaveBeenCalled()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('getLines', () => {
|
|
99
|
+
const call = (self, feature) => MapboxSnap.prototype.getLines.call(self, feature, 'mouse', 5)
|
|
100
|
+
|
|
101
|
+
test('returns [] when geometry or coordinates are missing', () => {
|
|
102
|
+
expect(call({}, { geometry: null })).toEqual([])
|
|
103
|
+
expect(call({}, { geometry: {} })).toEqual([])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('returns [] when coordinates are not a non-empty array', () => {
|
|
107
|
+
expect(call({}, { geometry: { coordinates: 'x' } })).toEqual([])
|
|
108
|
+
expect(call({}, { geometry: { coordinates: [] } })).toEqual([])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('maps MultiPolygon coordinates via turf polygon, filtering empties', () => {
|
|
112
|
+
const feature = { geometry: { type: 'MultiPolygon', coordinates: [[[[0, 0]]], []] } }
|
|
113
|
+
expect(call({}, feature)).toEqual([{ poly: [[[0, 0]]] }])
|
|
114
|
+
expect(polygon).toHaveBeenCalledWith([[[0, 0]]])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('maps MultiLineString coordinates via turf lineString', () => {
|
|
118
|
+
const feature = { geometry: { type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]]] } }
|
|
119
|
+
expect(call({}, feature)).toEqual([{ line: [[0, 0], [1, 1]] }])
|
|
120
|
+
expect(lineString).toHaveBeenCalledWith([[0, 0], [1, 1]])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('delegates other geometry types to the original', () => {
|
|
124
|
+
const feature = { geometry: { type: 'Polygon', coordinates: [[0, 0]] } }
|
|
125
|
+
expect(call({}, feature)).toBe('orig-getLines')
|
|
126
|
+
expect(MapboxSnap.__orig.getLines).toHaveBeenCalledWith(feature, 'mouse', 5)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('returns [] when turf throws on invalid geometry', () => {
|
|
130
|
+
polygon.mockImplementationOnce(() => { throw new Error('bad') })
|
|
131
|
+
const feature = { geometry: { type: 'MultiPolygon', coordinates: [[[[0, 0]]]] } }
|
|
132
|
+
expect(call({}, feature)).toEqual([])
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('getCloseFeatures', () => {
|
|
137
|
+
test('returns [] when disabled', () => {
|
|
138
|
+
expect(MapboxSnap.prototype.getCloseFeatures.call({ status: false }, {}, 1)).toEqual([])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('expands the query to a radius bbox and restores the point', () => {
|
|
142
|
+
const map = { getLayer: jest.fn((l) => l === 'layerA') }
|
|
143
|
+
const self = { status: true, map, _activeLayers: ['layerA', 'missing'], options: { radius: 10 } }
|
|
144
|
+
const e = { point: { x: 100, y: 200 } }
|
|
145
|
+
|
|
146
|
+
MapboxSnap.__orig.getCloseFeatures.mockImplementationOnce(function (ev) {
|
|
147
|
+
expect(ev.point).toEqual([[90, 190], [110, 210]])
|
|
148
|
+
return 'orig-getCloseFeatures'
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const result = MapboxSnap.prototype.getCloseFeatures.call(self, e, 3)
|
|
152
|
+
|
|
153
|
+
expect(self.options.layers).toEqual(['layerA'])
|
|
154
|
+
expect(e.point).toEqual({ x: 100, y: 200 })
|
|
155
|
+
expect(result).toBe('orig-getCloseFeatures')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('falls back to default layers and the configured radius', () => {
|
|
159
|
+
const map = { getLayer: jest.fn(() => true) }
|
|
160
|
+
const self = { status: true, map, _activeLayers: null, _defaultLayers: ['b'], options: {} }
|
|
161
|
+
const e = { point: { x: 0, y: 0 } }
|
|
162
|
+
|
|
163
|
+
MapboxSnap.__orig.getCloseFeatures.mockImplementationOnce(function (ev) {
|
|
164
|
+
expect(ev.point).toEqual([[-12, -12], [12, 12]])
|
|
165
|
+
return []
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
MapboxSnap.prototype.getCloseFeatures.call(self, e, 1)
|
|
169
|
+
expect(self.options.layers).toEqual(['b'])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('handles a completely missing layer configuration', () => {
|
|
173
|
+
const map = { getLayer: jest.fn(() => true) }
|
|
174
|
+
const self = { status: true, map, options: {} }
|
|
175
|
+
const e = { point: { x: 0, y: 0 } }
|
|
176
|
+
|
|
177
|
+
MapboxSnap.prototype.getCloseFeatures.call(self, e, 1)
|
|
178
|
+
expect(self.options.layers).toEqual([])
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('colour methods', () => {
|
|
183
|
+
test('searchInVertex tints the result and passes through falsy results', () => {
|
|
184
|
+
expect(MapboxSnap.prototype.searchInVertex.call({}).color).toBe(COLORS.vertex)
|
|
185
|
+
MapboxSnap.__orig.searchInVertex.mockReturnValueOnce(null)
|
|
186
|
+
expect(MapboxSnap.prototype.searchInVertex.call({})).toBeNull()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('searchInMidPoint tints the result and passes through falsy results', () => {
|
|
190
|
+
expect(MapboxSnap.prototype.searchInMidPoint.call({}).color).toBe(COLORS.midpoint)
|
|
191
|
+
MapboxSnap.__orig.searchInMidPoint.mockReturnValueOnce(null)
|
|
192
|
+
expect(MapboxSnap.prototype.searchInMidPoint.call({})).toBeNull()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('searchInEdge tints the result and passes through falsy results', () => {
|
|
196
|
+
expect(MapboxSnap.prototype.searchInEdge.call({}).color).toBe(COLORS.edge)
|
|
197
|
+
MapboxSnap.__orig.searchInEdge.mockReturnValueOnce(null)
|
|
198
|
+
expect(MapboxSnap.prototype.searchInEdge.call({})).toBeNull()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('snapToClosestPoint', () => {
|
|
203
|
+
test('skips and returns undefined when disabled', () => {
|
|
204
|
+
expect(MapboxSnap.prototype.snapToClosestPoint.call({ status: false })).toBeUndefined()
|
|
205
|
+
expect(MapboxSnap.__orig.snapToClosestPoint).not.toHaveBeenCalled()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('skips while the map is zooming', () => {
|
|
209
|
+
expect(MapboxSnap.prototype.snapToClosestPoint.call({ status: true, map: { _isZooming: true } }, {})).toBeUndefined()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('delegates and trims oversized caches', () => {
|
|
213
|
+
const self = { status: true, map: {}, closeFeatures: new Array(101), lines: new Array(101) }
|
|
214
|
+
expect(MapboxSnap.prototype.snapToClosestPoint.call(self, {})).toBe('orig-snap')
|
|
215
|
+
expect(self.closeFeatures).toHaveLength(0)
|
|
216
|
+
expect(self.lines).toHaveLength(0)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('leaves small caches intact', () => {
|
|
220
|
+
const self = { status: true, map: {}, closeFeatures: [1], lines: [2] }
|
|
221
|
+
MapboxSnap.prototype.snapToClosestPoint.call(self, {})
|
|
222
|
+
expect(self.closeFeatures).toHaveLength(1)
|
|
223
|
+
expect(self.lines).toHaveLength(1)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('tolerates absent caches and a missing map', () => {
|
|
227
|
+
expect(MapboxSnap.prototype.snapToClosestPoint.call({ status: true }, {})).toBe('orig-snap')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('clears snap state when the original throws', () => {
|
|
231
|
+
MapboxSnap.__orig.snapToClosestPoint.mockImplementationOnce(() => { throw new Error('x') })
|
|
232
|
+
const self = { status: true, map: {}, snapStatus: true, snapCoords: [1, 2] }
|
|
233
|
+
|
|
234
|
+
expect(MapboxSnap.prototype.snapToClosestPoint.call(self, {})).toBeUndefined()
|
|
235
|
+
expect(self.snapStatus).toBe(false)
|
|
236
|
+
expect(self.snapCoords).toBeNull()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import MapboxSnap from 'mapbox-gl-snap/dist/esm/MapboxSnap.js'
|
|
2
|
+
import { patchSourceData } from './sourceData.js'
|
|
3
|
+
import { SNAP_HELPER_LAYER } from './constants.js'
|
|
4
|
+
|
|
5
|
+
/** Remove any stale snap helper source/layer before creating a new instance */
|
|
6
|
+
function cleanupOldSnap (map) {
|
|
7
|
+
if (map.getLayer(SNAP_HELPER_LAYER)) {
|
|
8
|
+
map.removeLayer(SNAP_HELPER_LAYER)
|
|
9
|
+
}
|
|
10
|
+
if (map.getSource(SNAP_HELPER_LAYER)) {
|
|
11
|
+
map.removeSource(SNAP_HELPER_LAYER)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Snap indicators (and all snap processing) are only meaningful while actively
|
|
16
|
+
// drawing or editing vertices — outside these modes there is nothing to snap to.
|
|
17
|
+
const SNAP_ACTIVE_MODES = new Set(['draw_polygon', 'draw_line', 'edit_vertex'])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Externally-controlled status: ignore library writes so status is only changed
|
|
21
|
+
* via setSnapStatus(). The library otherwise sets status=true on mode/selection change.
|
|
22
|
+
*
|
|
23
|
+
* Also gated on the current draw mode: the library's own mousemove listener
|
|
24
|
+
* (added once in its constructor) calls snapToClosestPoint on every mouse move
|
|
25
|
+
* regardless of draw mode, repainting the snap-helper-circle layer whenever
|
|
26
|
+
* status is true. Without this gate, the marker reappears on the very next
|
|
27
|
+
* mouse move after leaving draw/edit mode, since mapbox-gl-draw's public
|
|
28
|
+
* changeMode API is silent by default and rarely gives us a mode-change event
|
|
29
|
+
* to react to.
|
|
30
|
+
*/
|
|
31
|
+
function defineControlledStatus (snap, initialStatus, draw) {
|
|
32
|
+
let controlledStatus = initialStatus
|
|
33
|
+
|
|
34
|
+
Object.defineProperty(snap, 'status', {
|
|
35
|
+
get () { // nosonar
|
|
36
|
+
return controlledStatus && SNAP_ACTIVE_MODES.has(draw.getMode())
|
|
37
|
+
},
|
|
38
|
+
set () { // nosonar
|
|
39
|
+
// intentionally empty: library writes are ignored
|
|
40
|
+
},
|
|
41
|
+
configurable: true
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
snap.setSnapStatus = (value) => {
|
|
45
|
+
controlledStatus = value
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Store default snap layers and expose a per-call override setter (null resets) */
|
|
50
|
+
function configureSnapLayers (snap, layers) {
|
|
51
|
+
snap._defaultLayers = layers
|
|
52
|
+
snap._activeLayers = null
|
|
53
|
+
|
|
54
|
+
snap.setSnapLayers = (overrideLayers) => {
|
|
55
|
+
if (overrideLayers === null || overrideLayers === undefined) {
|
|
56
|
+
snap._activeLayers = null // Use defaults
|
|
57
|
+
} else if (Array.isArray(overrideLayers)) {
|
|
58
|
+
snap._activeLayers = overrideLayers // Override defaults
|
|
59
|
+
} else {
|
|
60
|
+
// No action
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Create the snap instance once the draw source is available */
|
|
66
|
+
export function createSnapInstance (map, draw, source, config) {
|
|
67
|
+
// Prevent duplicate creation (race between initial poll and style.load)
|
|
68
|
+
if (map._snapInstance || map._snapCreating) {
|
|
69
|
+
return map._snapInstance
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
map._snapCreating = true
|
|
73
|
+
cleanupOldSnap(map)
|
|
74
|
+
patchSourceData(source)
|
|
75
|
+
|
|
76
|
+
/** @type {any} */
|
|
77
|
+
const snap = new MapboxSnap({
|
|
78
|
+
map,
|
|
79
|
+
drawing: draw,
|
|
80
|
+
options: { layers: config.layers, radius: config.radius, rules: config.rules },
|
|
81
|
+
status: config.status,
|
|
82
|
+
onSnapped: config.onSnapped
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
defineControlledStatus(snap, config.status, draw)
|
|
86
|
+
configureSnapLayers(snap, config.layers)
|
|
87
|
+
|
|
88
|
+
// Apply any pending snap layers that were set before the instance was ready
|
|
89
|
+
if (map._pendingSnapLayers !== undefined) {
|
|
90
|
+
snap.setSnapLayers(map._pendingSnapLayers)
|
|
91
|
+
delete map._pendingSnapLayers
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
map._snapInstance = snap
|
|
95
|
+
return snap
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Ensure the snap helper source and layer exist after a style change */
|
|
99
|
+
export function ensureSnapLayer (map) {
|
|
100
|
+
if (!map.getSource(SNAP_HELPER_LAYER)) {
|
|
101
|
+
map.addSource(SNAP_HELPER_LAYER, {
|
|
102
|
+
type: 'geojson',
|
|
103
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
if (!map.getLayer(SNAP_HELPER_LAYER)) {
|
|
107
|
+
map.addLayer({
|
|
108
|
+
id: SNAP_HELPER_LAYER,
|
|
109
|
+
type: 'fill',
|
|
110
|
+
source: SNAP_HELPER_LAYER,
|
|
111
|
+
paint: { 'fill-color': ['get', 'color'] },
|
|
112
|
+
layout: { visibility: map._snapInstance?.status ? 'visible' : 'none' }
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import MapboxSnap from 'mapbox-gl-snap/dist/esm/MapboxSnap.js'
|
|
2
|
+
import { patchSourceData } from './sourceData.js'
|
|
3
|
+
import { createSnapInstance, ensureSnapLayer } from './snapInstance.js'
|
|
4
|
+
|
|
5
|
+
const SNAP_LAYER = 'snap-helper-circle'
|
|
6
|
+
|
|
7
|
+
jest.mock('mapbox-gl-snap/dist/esm/MapboxSnap.js', () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: jest.fn(function (opts) { Object.assign(this, opts) })
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
jest.mock('./sourceData.js', () => ({ patchSourceData: jest.fn() }))
|
|
13
|
+
|
|
14
|
+
const config = { layers: ['a'], radius: 12, rules: ['vertex'], status: false, onSnapped: () => {} }
|
|
15
|
+
|
|
16
|
+
const makeMap = (overrides = {}) => ({
|
|
17
|
+
getLayer: jest.fn(() => null),
|
|
18
|
+
removeLayer: jest.fn(),
|
|
19
|
+
getSource: jest.fn(() => null),
|
|
20
|
+
removeSource: jest.fn(),
|
|
21
|
+
addSource: jest.fn(),
|
|
22
|
+
addLayer: jest.fn(),
|
|
23
|
+
...overrides
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('createSnapInstance', () => {
|
|
31
|
+
test('constructs the snap instance, patches the source and stores it on the map', () => {
|
|
32
|
+
const map = makeMap()
|
|
33
|
+
const draw = { id: 'draw' }
|
|
34
|
+
const source = { id: 'src' }
|
|
35
|
+
|
|
36
|
+
const snap = createSnapInstance(map, draw, source, config)
|
|
37
|
+
|
|
38
|
+
expect(patchSourceData).toHaveBeenCalledWith(source)
|
|
39
|
+
expect(MapboxSnap).toHaveBeenCalledWith({
|
|
40
|
+
map,
|
|
41
|
+
drawing: draw,
|
|
42
|
+
options: { layers: ['a'], radius: 12, rules: ['vertex'] },
|
|
43
|
+
status: false,
|
|
44
|
+
onSnapped: config.onSnapped
|
|
45
|
+
})
|
|
46
|
+
expect(map._snapInstance).toBe(snap)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('returns the existing instance without recreating', () => {
|
|
50
|
+
const map = makeMap({ _snapInstance: { id: 'existing' } })
|
|
51
|
+
expect(createSnapInstance(map, {}, {}, config)).toEqual({ id: 'existing' })
|
|
52
|
+
expect(MapboxSnap).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('bails out while another creation is in progress', () => {
|
|
56
|
+
const map = makeMap({ _snapCreating: true })
|
|
57
|
+
createSnapInstance(map, {}, {}, config)
|
|
58
|
+
expect(MapboxSnap).not.toHaveBeenCalled()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('removes a stale snap layer and source before creating', () => {
|
|
62
|
+
const map = makeMap({ getLayer: jest.fn(() => ({})), getSource: jest.fn(() => ({})) })
|
|
63
|
+
createSnapInstance(map, {}, {}, config)
|
|
64
|
+
expect(map.removeLayer).toHaveBeenCalledWith(SNAP_LAYER)
|
|
65
|
+
expect(map.removeSource).toHaveBeenCalledWith(SNAP_LAYER)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('exposes an externally-controlled status via setSnapStatus', () => {
|
|
69
|
+
const map = makeMap()
|
|
70
|
+
const draw = { getMode: jest.fn(() => 'draw_polygon') }
|
|
71
|
+
const snap = createSnapInstance(map, draw, {}, { ...config, status: false })
|
|
72
|
+
|
|
73
|
+
expect(snap.status).toBe(false)
|
|
74
|
+
snap.status = true // library write ignored
|
|
75
|
+
expect(snap.status).toBe(false)
|
|
76
|
+
snap.setSnapStatus(true)
|
|
77
|
+
expect(snap.status).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('status is false outside draw/edit modes even when the toggle is on', () => {
|
|
81
|
+
const map = makeMap()
|
|
82
|
+
const draw = { getMode: jest.fn(() => 'simple_select') }
|
|
83
|
+
const snap = createSnapInstance(map, draw, {}, { ...config, status: false })
|
|
84
|
+
|
|
85
|
+
snap.setSnapStatus(true)
|
|
86
|
+
expect(snap.status).toBe(false)
|
|
87
|
+
|
|
88
|
+
draw.getMode.mockReturnValue('edit_vertex')
|
|
89
|
+
expect(snap.status).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('setSnapLayers overrides, resets and ignores invalid input', () => {
|
|
93
|
+
const map = makeMap()
|
|
94
|
+
const snap = createSnapInstance(map, {}, {}, { ...config, layers: ['a'] })
|
|
95
|
+
|
|
96
|
+
expect(snap._defaultLayers).toEqual(['a'])
|
|
97
|
+
expect(snap._activeLayers).toBeNull()
|
|
98
|
+
|
|
99
|
+
snap.setSnapLayers(['b', 'c'])
|
|
100
|
+
expect(snap._activeLayers).toEqual(['b', 'c'])
|
|
101
|
+
|
|
102
|
+
snap.setSnapLayers(null)
|
|
103
|
+
expect(snap._activeLayers).toBeNull()
|
|
104
|
+
|
|
105
|
+
snap.setSnapLayers(['d'])
|
|
106
|
+
snap.setSnapLayers(undefined)
|
|
107
|
+
expect(snap._activeLayers).toBeNull()
|
|
108
|
+
|
|
109
|
+
snap.setSnapLayers(['x'])
|
|
110
|
+
snap.setSnapLayers('not-an-array') // no action
|
|
111
|
+
expect(snap._activeLayers).toEqual(['x'])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('applies pending snap layers set before the instance was ready', () => {
|
|
115
|
+
const map = makeMap({ _pendingSnapLayers: ['pending'] })
|
|
116
|
+
const snap = createSnapInstance(map, {}, {}, { ...config, layers: ['default'] })
|
|
117
|
+
|
|
118
|
+
expect(snap._activeLayers).toEqual(['pending'])
|
|
119
|
+
expect(map._pendingSnapLayers).toBeUndefined()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('ensureSnapLayer', () => {
|
|
124
|
+
test('adds the missing source and layer (hidden when snapping is disabled)', () => {
|
|
125
|
+
const map = makeMap()
|
|
126
|
+
ensureSnapLayer(map)
|
|
127
|
+
|
|
128
|
+
expect(map.addSource).toHaveBeenCalledWith(SNAP_LAYER, expect.objectContaining({ type: 'geojson' }))
|
|
129
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
130
|
+
id: SNAP_LAYER,
|
|
131
|
+
layout: { visibility: 'none' }
|
|
132
|
+
}))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('shows the layer when snapping is active', () => {
|
|
136
|
+
const map = makeMap({ _snapInstance: { status: true } })
|
|
137
|
+
ensureSnapLayer(map)
|
|
138
|
+
|
|
139
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
140
|
+
layout: { visibility: 'visible' }
|
|
141
|
+
}))
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('does not re-add an existing source or layer', () => {
|
|
145
|
+
const map = makeMap({ getSource: jest.fn(() => ({})), getLayer: jest.fn(() => ({})) })
|
|
146
|
+
ensureSnapLayer(map)
|
|
147
|
+
|
|
148
|
+
expect(map.addSource).not.toHaveBeenCalled()
|
|
149
|
+
expect(map.addLayer).not.toHaveBeenCalled()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll until checkFn returns truthy, then call onSuccess with the result.
|
|
3
|
+
* A `null` result signals to stop polling; any other falsy value keeps polling.
|
|
4
|
+
*/
|
|
5
|
+
export function pollUntil (checkFn, onSuccess) {
|
|
6
|
+
(function poll () {
|
|
7
|
+
const result = checkFn()
|
|
8
|
+
if (result === null) {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
result ? onSuccess(result) : requestAnimationFrame(poll)
|
|
12
|
+
})()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Patch a GeoJSON source to expose _data for MapboxSnap compatibility.
|
|
17
|
+
* MapboxSnap expects source._data.features but MapLibre doesn't expose this.
|
|
18
|
+
*/
|
|
19
|
+
export function patchSourceData (source) {
|
|
20
|
+
if (!source || (source._data && Array.isArray(source._data?.features))) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let dataCache = { type: 'FeatureCollection', features: [] }
|
|
25
|
+
Object.defineProperty(source, '_data', {
|
|
26
|
+
get: () => dataCache,
|
|
27
|
+
set: (val) => {
|
|
28
|
+
dataCache = (val && typeof val === 'object' && Array.isArray(val.features))
|
|
29
|
+
? val
|
|
30
|
+
: { type: 'FeatureCollection', features: [] }
|
|
31
|
+
},
|
|
32
|
+
configurable: true
|
|
33
|
+
})
|
|
34
|
+
}
|