@defra/interactive-map 0.0.37-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-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/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- 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,243 @@
|
|
|
1
|
+
import polygonSplitter from 'polygon-splitter'
|
|
2
|
+
import turfUnion from '@turf/union'
|
|
3
|
+
import {
|
|
4
|
+
featureCollection as turfFeatureCollection,
|
|
5
|
+
polygon as turfPolygon,
|
|
6
|
+
multiPolygon as turfMultiPolygon,
|
|
7
|
+
lineString as turfLineString,
|
|
8
|
+
multiLineString as turfMultiLineString,
|
|
9
|
+
point as turfPoint,
|
|
10
|
+
multiPoint as turfMultiPoint
|
|
11
|
+
} from '@turf/helpers'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import('geojson').Feature<import('geojson').Polygon>} Polygon
|
|
15
|
+
* @typedef {import('geojson').Feature<import('geojson').LineString>} Line
|
|
16
|
+
* @typedef {import('geojson').Feature<import('geojson').Feature>} Feature
|
|
17
|
+
* @typedef {import('geojson').FeatureCollection<import('geojson').FeatureCollection>} FeatureCollection
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extend a LineString at its endpoints, along their own direction, by a small
|
|
22
|
+
* fraction of the adjacent segment's length. Pure planar vector math — no
|
|
23
|
+
* geodesic assumption, so it works for any coordinate system (lon/lat degrees,
|
|
24
|
+
* projected meters like British National Grid, etc.). Turf's bearing/distance
|
|
25
|
+
* functions assume WGS84 lon/lat input; feeding them projected coordinates
|
|
26
|
+
* silently produces nonsense (large eastings/northings get treated as
|
|
27
|
+
* out-of-range degrees and wrapped), which is why this doesn't use them.
|
|
28
|
+
*
|
|
29
|
+
* @param {Feature<LineString>} line
|
|
30
|
+
* @param {number} fraction - portion of the adjacent segment's length to extend by
|
|
31
|
+
*/
|
|
32
|
+
function extendLine (line, fraction = 0.01) {
|
|
33
|
+
const coords = line.geometry.coordinates.map(c => [...c])
|
|
34
|
+
const last = coords.length - 1
|
|
35
|
+
|
|
36
|
+
const extend = (from, towards) => [
|
|
37
|
+
from[0] + (from[0] - towards[0]) * fraction,
|
|
38
|
+
from[1] + (from[1] - towards[1]) * fraction
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
// Compute both from the original coordinates before assigning either — for a
|
|
42
|
+
// 2-point line, coords[last - 1] is coords[0], so writing coords[0] first
|
|
43
|
+
// would corrupt the reference point the end extension reads.
|
|
44
|
+
const newStart = extend(coords[0], coords[1])
|
|
45
|
+
const newEnd = extend(coords[last], coords[last - 1])
|
|
46
|
+
coords[0] = newStart
|
|
47
|
+
coords[last] = newEnd
|
|
48
|
+
|
|
49
|
+
return turfLineString(coords)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Split a polygon using a line.
|
|
54
|
+
* Only accepts splits that result in exactly two polygons.
|
|
55
|
+
*
|
|
56
|
+
* @param {Feature<Polygon>} polygon
|
|
57
|
+
* @param {Feature<LineString>} line
|
|
58
|
+
* @returns {FeatureCollection<Polygon>|null}
|
|
59
|
+
*/
|
|
60
|
+
const splitPolygon = (polygon, line) => {
|
|
61
|
+
// Extend only start and end vertices
|
|
62
|
+
const extended = extendLine(line) // assume extendLine only touches start/end now
|
|
63
|
+
|
|
64
|
+
let result
|
|
65
|
+
try {
|
|
66
|
+
result = polygonSplitter(polygon, extended)
|
|
67
|
+
} catch {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Must result in exactly 2 polygons
|
|
72
|
+
let polygons = []
|
|
73
|
+
if (result.geometry.type === 'MultiPolygon') {
|
|
74
|
+
if (result.geometry.coordinates.length !== 2) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
polygons = result.geometry.coordinates.map(coords => turfPolygon(coords, polygon.properties))
|
|
78
|
+
} else {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Assign IDs & properties
|
|
83
|
+
const baseId = polygon.id ?? polygon.properties?.id ?? 'poly'
|
|
84
|
+
const features = polygons.map((poly, i) =>
|
|
85
|
+
turfPolygon(
|
|
86
|
+
poly.geometry.coordinates,
|
|
87
|
+
{ ...polygon.properties, id: baseId },
|
|
88
|
+
{ id: `${baseId}-${i + 1}` }
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return turfFeatureCollection(features)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Merge multiple contiguous polygons into a single polygon.
|
|
97
|
+
* Only accepts a merge that results in exactly one polygon — a gap between two
|
|
98
|
+
* of the inputs would make Turf return a MultiPolygon instead, which is rejected.
|
|
99
|
+
*
|
|
100
|
+
* @param {Polygon[]} polygons
|
|
101
|
+
* @returns {Polygon|null}
|
|
102
|
+
*/
|
|
103
|
+
const mergePolygons = (polygons) => {
|
|
104
|
+
let result
|
|
105
|
+
try {
|
|
106
|
+
result = turfUnion(turfFeatureCollection(polygons))
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (result?.geometry?.type !== 'Polygon') {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Assign ID & properties from the first feature, matching splitPolygon's convention.
|
|
116
|
+
const baseId = polygons[0].id ?? polygons[0].properties?.id ?? 'poly'
|
|
117
|
+
return turfPolygon(result.geometry.coordinates, { ...polygons[0].properties, id: baseId }, { id: baseId })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Convert a GeoJSON Feature or geometry-like object into a Turf geometry.
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} featureOrGeom - Either a Feature with a `.geometry` property or a raw GeoJSON geometry object.
|
|
124
|
+
* @returns {Object} Turf geometry (Polygon, LineString, Point, etc.)
|
|
125
|
+
*
|
|
126
|
+
* @throws Will throw if the geometry type is not supported.
|
|
127
|
+
*/
|
|
128
|
+
const toTurfGeometry = (featureOrGeom) => {
|
|
129
|
+
const geom = featureOrGeom.geometry || featureOrGeom
|
|
130
|
+
|
|
131
|
+
switch (geom.type) {
|
|
132
|
+
case 'Polygon':
|
|
133
|
+
return turfPolygon(geom.coordinates)
|
|
134
|
+
case 'MultiPolygon':
|
|
135
|
+
return turfMultiPolygon(geom.coordinates)
|
|
136
|
+
case 'LineString':
|
|
137
|
+
return turfLineString(geom.coordinates)
|
|
138
|
+
case 'MultiLineString':
|
|
139
|
+
return turfMultiLineString(geom.coordinates)
|
|
140
|
+
case 'Point':
|
|
141
|
+
return turfPoint(geom.coordinates)
|
|
142
|
+
case 'MultiPoint':
|
|
143
|
+
return turfMultiPoint(geom.coordinates)
|
|
144
|
+
default:
|
|
145
|
+
throw new Error(`Unsupported geometry type: ${geom.type}`)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const DEGREES_PER_HALF_TURN = 180
|
|
150
|
+
|
|
151
|
+
const haversine = ([lon1, lat1], [lon2, lat2]) => {
|
|
152
|
+
const toRad = deg => deg * Math.PI / DEGREES_PER_HALF_TURN
|
|
153
|
+
const R = 6371000 // meters
|
|
154
|
+
const dLat = toRad(lat2 - lat1)
|
|
155
|
+
const dLon = toRad(lon2 - lon1)
|
|
156
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2
|
|
157
|
+
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const SMALL_RING_LENGTH = 3
|
|
161
|
+
|
|
162
|
+
// True if any two points in the ring are within `tolerance` of each other
|
|
163
|
+
const hasDuplicatePair = (ring, tolerance) => {
|
|
164
|
+
for (let i = 0; i < ring.length; i++) {
|
|
165
|
+
for (let j = i + 1; j < ring.length; j++) {
|
|
166
|
+
if (haversine(ring[i], ring[j]) < tolerance) {
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const isNewCoordinate = (coords, tolerance = 0.01) => {
|
|
175
|
+
const ring = coords[0]
|
|
176
|
+
// First coord is always new
|
|
177
|
+
if (ring.length <= 1) {
|
|
178
|
+
return true
|
|
179
|
+
}
|
|
180
|
+
// For small rings, reject if any two points coincide
|
|
181
|
+
if (ring.length <= SMALL_RING_LENGTH && hasDuplicatePair(ring, tolerance)) {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const isValidLineClick = (coords, tolerance = 0.01) => {
|
|
188
|
+
// First coord is always valid
|
|
189
|
+
if (coords.length <= 1) {
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
// The new coordinate must differ from the previous one
|
|
193
|
+
const last = coords[coords.length - 1]
|
|
194
|
+
const secondLast = coords[coords.length - 2]
|
|
195
|
+
return haversine(last, secondLast) >= tolerance
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const isValidClick = (coords) => {
|
|
199
|
+
// Valid when it's the very first point, or a genuinely new (non-duplicate) coordinate.
|
|
200
|
+
// Callers only pass single-ring polygon coordinates while drawing, so no ring-closure /
|
|
201
|
+
// self-intersection checks are needed here.
|
|
202
|
+
return coords[0].length <= 1 || isNewCoordinate(coords)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const spatialNavigate = (start, pixels, direction) => {
|
|
206
|
+
const quadrant = pixels.filter((p) => {
|
|
207
|
+
const offsetX = Math.abs(p[0] - start[0])
|
|
208
|
+
const offsetY = Math.abs(p[1] - start[1])
|
|
209
|
+
let isQuadrant = false
|
|
210
|
+
if (direction === 'ArrowUp') {
|
|
211
|
+
isQuadrant = p[1] <= start[1] && offsetY >= offsetX
|
|
212
|
+
} else if (direction === 'ArrowDown') {
|
|
213
|
+
isQuadrant = p[1] > start[1] && offsetY >= offsetX
|
|
214
|
+
} else if (direction === 'ArrowLeft') {
|
|
215
|
+
isQuadrant = p[0] <= start[0] && offsetY < offsetX
|
|
216
|
+
} else if (direction === 'ArrowRight') {
|
|
217
|
+
isQuadrant = p[0] > start[0] && offsetY < offsetX
|
|
218
|
+
} else {
|
|
219
|
+
isQuadrant = true
|
|
220
|
+
}
|
|
221
|
+
return isQuadrant && (JSON.stringify(p) !== JSON.stringify(start))
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
if (!quadrant.length) {
|
|
225
|
+
quadrant.push(start)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const pythagorean = (a, b) => Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2))
|
|
229
|
+
const distances = quadrant.map(p => pythagorean(Math.abs(start[0] - p[0]), Math.abs(start[1] - p[1])))
|
|
230
|
+
const closest = quadrant[distances.indexOf(Math.min(...distances))]
|
|
231
|
+
return pixels.findIndex(i => JSON.stringify(i) === JSON.stringify(closest))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
toTurfGeometry,
|
|
236
|
+
splitPolygon,
|
|
237
|
+
mergePolygons,
|
|
238
|
+
extendLine,
|
|
239
|
+
isNewCoordinate,
|
|
240
|
+
isValidClick,
|
|
241
|
+
isValidLineClick,
|
|
242
|
+
spatialNavigate
|
|
243
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import polygonSplitter from 'polygon-splitter'
|
|
2
|
+
import turfUnion from '@turf/union'
|
|
3
|
+
import {
|
|
4
|
+
toTurfGeometry,
|
|
5
|
+
splitPolygon,
|
|
6
|
+
mergePolygons,
|
|
7
|
+
extendLine,
|
|
8
|
+
isNewCoordinate,
|
|
9
|
+
isValidClick,
|
|
10
|
+
isValidLineClick,
|
|
11
|
+
spatialNavigate
|
|
12
|
+
} from './spatial.js'
|
|
13
|
+
|
|
14
|
+
jest.mock('polygon-splitter', () => jest.fn())
|
|
15
|
+
jest.mock('@turf/union', () => jest.fn())
|
|
16
|
+
|
|
17
|
+
beforeEach(() => jest.clearAllMocks())
|
|
18
|
+
|
|
19
|
+
describe('toTurfGeometry', () => {
|
|
20
|
+
test.each([
|
|
21
|
+
['Polygon', { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }],
|
|
22
|
+
['MultiPolygon', { type: 'MultiPolygon', coordinates: [[[[0, 0], [1, 0], [1, 1], [0, 0]]]] }],
|
|
23
|
+
['LineString', { type: 'LineString', coordinates: [[0, 0], [1, 1]] }],
|
|
24
|
+
['MultiLineString', { type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]]] }],
|
|
25
|
+
['Point', { type: 'Point', coordinates: [0, 0] }],
|
|
26
|
+
['MultiPoint', { type: 'MultiPoint', coordinates: [[0, 0], [1, 1]] }]
|
|
27
|
+
])('converts a raw %s geometry', (type, geom) => {
|
|
28
|
+
expect(toTurfGeometry(geom).geometry.type).toBe(type)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('unwraps a Feature via its .geometry property', () => {
|
|
32
|
+
const feature = { geometry: { type: 'Point', coordinates: [0, 0] } }
|
|
33
|
+
expect(toTurfGeometry(feature).geometry.type).toBe('Point')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('throws for unsupported geometry types', () => {
|
|
37
|
+
expect(() => toTurfGeometry({ type: 'GeometryCollection' }))
|
|
38
|
+
.toThrow('Unsupported geometry type: GeometryCollection')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('extendLine', () => {
|
|
43
|
+
test('extends a two-point line at both ends, along each segment\'s own direction', () => {
|
|
44
|
+
const line = { geometry: { coordinates: [[0, 0], [0, 10]] } }
|
|
45
|
+
const result = extendLine(line, 0.1)
|
|
46
|
+
expect(result.geometry.type).toBe('LineString')
|
|
47
|
+
// Start moves further away from the second point (backward); end moves
|
|
48
|
+
// further away from the second-to-last point (forward).
|
|
49
|
+
expect(result.geometry.coordinates).toEqual([[0, -1], [0, 11]])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('extends only the endpoints of a multi-point line', () => {
|
|
53
|
+
const line = { geometry: { coordinates: [[0, 0], [0, 1], [0, 2]] } }
|
|
54
|
+
const result = extendLine(line, 0.1)
|
|
55
|
+
expect(result.geometry.coordinates).toHaveLength(3)
|
|
56
|
+
expect(result.geometry.coordinates[1]).toEqual([0, 1]) // middle vertex untouched
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('is pure planar math — correct at British National Grid scale (large eastings/northings)', () => {
|
|
60
|
+
// Geodesic bearing/destination would misread these as out-of-range lon/lat
|
|
61
|
+
// degrees and wrap them; plain vector extension has no such assumption.
|
|
62
|
+
const line = { geometry: { coordinates: [[337600, 504600], [337650, 504650]] } }
|
|
63
|
+
const result = extendLine(line, 0.1)
|
|
64
|
+
expect(result.geometry.coordinates[0]).toEqual([337595, 504595])
|
|
65
|
+
expect(result.geometry.coordinates[1]).toEqual([337655, 504655])
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('splitPolygon', () => {
|
|
70
|
+
const polygon = {
|
|
71
|
+
id: 'sq',
|
|
72
|
+
properties: { id: 'sq', name: 'field' },
|
|
73
|
+
geometry: { type: 'Polygon', coordinates: [[[0, 0], [4, 0], [4, 4], [0, 4], [0, 0]]] }
|
|
74
|
+
}
|
|
75
|
+
const line = { geometry: { type: 'LineString', coordinates: [[2, -1], [2, 5]] } }
|
|
76
|
+
|
|
77
|
+
const twoPolygonResult = {
|
|
78
|
+
geometry: {
|
|
79
|
+
type: 'MultiPolygon',
|
|
80
|
+
coordinates: [
|
|
81
|
+
[[[0, 0], [2, 0], [2, 4], [0, 4], [0, 0]]],
|
|
82
|
+
[[[2, 0], [4, 0], [4, 4], [2, 4], [2, 0]]]
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test('returns a two-feature collection for a valid split', () => {
|
|
88
|
+
polygonSplitter.mockReturnValue(twoPolygonResult)
|
|
89
|
+
|
|
90
|
+
const result = splitPolygon(polygon, line)
|
|
91
|
+
|
|
92
|
+
expect(result.type).toBe('FeatureCollection')
|
|
93
|
+
expect(result.features).toHaveLength(2)
|
|
94
|
+
expect(result.features[0].id).toBe('sq-1')
|
|
95
|
+
expect(result.features[1].id).toBe('sq-2')
|
|
96
|
+
expect(result.features[0].properties).toMatchObject({ id: 'sq', name: 'field' })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('returns null when the splitter throws', () => {
|
|
100
|
+
polygonSplitter.mockImplementation(() => { throw new Error('bad geometry') })
|
|
101
|
+
expect(splitPolygon(polygon, line)).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('returns null when the result is not a MultiPolygon', () => {
|
|
105
|
+
polygonSplitter.mockReturnValue({ geometry: { type: 'Polygon', coordinates: [] } })
|
|
106
|
+
expect(splitPolygon(polygon, line)).toBeNull()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('returns null when the split does not produce exactly two polygons', () => {
|
|
110
|
+
polygonSplitter.mockReturnValue({ geometry: { type: 'MultiPolygon', coordinates: [[[[0, 0], [1, 0], [1, 1], [0, 0]]]] } })
|
|
111
|
+
expect(splitPolygon(polygon, line)).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('derives the base id from properties.id then a default', () => {
|
|
115
|
+
polygonSplitter.mockReturnValue(twoPolygonResult)
|
|
116
|
+
|
|
117
|
+
const noTopLevelId = { properties: { id: 'pid' }, geometry: polygon.geometry }
|
|
118
|
+
expect(splitPolygon(noTopLevelId, line).features[0].id).toBe('pid-1')
|
|
119
|
+
|
|
120
|
+
const anonymous = { properties: {}, geometry: polygon.geometry }
|
|
121
|
+
expect(splitPolygon(anonymous, line).features[0].id).toBe('poly-1')
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('mergePolygons', () => {
|
|
126
|
+
const squareA = {
|
|
127
|
+
id: 'sq-a',
|
|
128
|
+
properties: { id: 'sq-a', name: 'field' },
|
|
129
|
+
geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]] }
|
|
130
|
+
}
|
|
131
|
+
const squareB = {
|
|
132
|
+
id: 'sq-b',
|
|
133
|
+
properties: { id: 'sq-b' },
|
|
134
|
+
geometry: { type: 'Polygon', coordinates: [[[1, 0], [2, 0], [2, 1], [1, 1], [1, 0]]] }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mergedResult = {
|
|
138
|
+
geometry: { type: 'Polygon', coordinates: [[[0, 0], [2, 0], [2, 1], [0, 1], [0, 0]]] }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
test('returns a single merged polygon for a valid union', () => {
|
|
142
|
+
turfUnion.mockReturnValue(mergedResult)
|
|
143
|
+
|
|
144
|
+
const result = mergePolygons([squareA, squareB])
|
|
145
|
+
|
|
146
|
+
expect(result.geometry.type).toBe('Polygon')
|
|
147
|
+
expect(result.id).toBe('sq-a')
|
|
148
|
+
expect(result.properties).toMatchObject({ id: 'sq-a', name: 'field' })
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('returns null when union throws', () => {
|
|
152
|
+
turfUnion.mockImplementation(() => { throw new Error('bad geometry') })
|
|
153
|
+
expect(mergePolygons([squareA, squareB])).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('returns null when the inputs are not contiguous (MultiPolygon result)', () => {
|
|
157
|
+
turfUnion.mockReturnValue({ geometry: { type: 'MultiPolygon', coordinates: [] } })
|
|
158
|
+
expect(mergePolygons([squareA, squareB])).toBeNull()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('returns null when union returns null', () => {
|
|
162
|
+
turfUnion.mockReturnValue(null)
|
|
163
|
+
expect(mergePolygons([squareA, squareB])).toBeNull()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('derives the base id from properties.id then a default', () => {
|
|
167
|
+
turfUnion.mockReturnValue(mergedResult)
|
|
168
|
+
|
|
169
|
+
const noTopLevelId = { properties: { id: 'pid' }, geometry: squareA.geometry }
|
|
170
|
+
expect(mergePolygons([noTopLevelId, squareB]).id).toBe('pid')
|
|
171
|
+
|
|
172
|
+
const anonymous = { properties: {}, geometry: squareA.geometry }
|
|
173
|
+
expect(mergePolygons([anonymous, squareB]).id).toBe('poly')
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('isNewCoordinate', () => {
|
|
178
|
+
test('is true for a ring with a single point', () => {
|
|
179
|
+
expect(isNewCoordinate([[[0, 0]]])).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('is false when a small ring has coincident points', () => {
|
|
183
|
+
expect(isNewCoordinate([[[0, 0], [0, 0]]])).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('is true when a small ring has distinct points', () => {
|
|
187
|
+
expect(isNewCoordinate([[[0, 0], [10, 10]]])).toBe(true)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('skips the duplicate check for larger rings', () => {
|
|
191
|
+
expect(isNewCoordinate([[[0, 0], [0, 0], [1, 1], [2, 2], [3, 3]]])).toBe(true)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('isValidClick', () => {
|
|
196
|
+
test('is true when the ring has a single point', () => {
|
|
197
|
+
expect(isValidClick([[[0, 0]]])).toBe(true)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('is true for a new (non-duplicate) coordinate', () => {
|
|
201
|
+
expect(isValidClick([[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]])).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('is false when a small ring repeats a coordinate', () => {
|
|
205
|
+
expect(isValidClick([[[0, 0], [0, 0]]])).toBe(false)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('isValidLineClick', () => {
|
|
210
|
+
test('is true for the first coordinate', () => {
|
|
211
|
+
expect(isValidLineClick([[0, 0]])).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('is true when the last two coordinates differ', () => {
|
|
215
|
+
expect(isValidLineClick([[0, 0], [10, 10]])).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('is false when the last two coordinates coincide', () => {
|
|
219
|
+
expect(isValidLineClick([[0, 0], [0, 0]])).toBe(false)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('spatialNavigate', () => {
|
|
224
|
+
const start = [0, 0]
|
|
225
|
+
const pixels = [[0, 0], [0, -10], [0, 10], [-10, 0], [10, 0]]
|
|
226
|
+
|
|
227
|
+
test.each([
|
|
228
|
+
['ArrowUp', 1],
|
|
229
|
+
['ArrowDown', 2],
|
|
230
|
+
['ArrowLeft', 3],
|
|
231
|
+
['ArrowRight', 4]
|
|
232
|
+
])('finds the nearest pixel for %s', (direction, expectedIndex) => {
|
|
233
|
+
expect(spatialNavigate(start, pixels, direction)).toBe(expectedIndex)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('considers all pixels for an unrecognised direction', () => {
|
|
237
|
+
expect(spatialNavigate(start, pixels, 'Tab')).toBe(1)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('falls back to the start point when no pixel is in the quadrant', () => {
|
|
241
|
+
expect(spatialNavigate(start, [[0, 0]], 'ArrowUp')).toBe(0)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG offset vertex target for touch edit mode
|
|
3
|
+
* Shown below the finger so users can accurately reposition vertices without finger occlusion.
|
|
4
|
+
*
|
|
5
|
+
* Colors update dynamically based on current color scheme via CSS custom properties:
|
|
6
|
+
* --draw-halo outer ring (editActive color)
|
|
7
|
+
* --draw-bg inner background (editHalo color)
|
|
8
|
+
* --draw-primary arrow icons and centre dot (editVertex color)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { SIZES } from '../defaults.js'
|
|
12
|
+
|
|
13
|
+
const HALF_SIZE = SIZES.touchTargetSize / 2
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate SVG touch target HTML string based on configured size
|
|
17
|
+
* @returns {string} SVG HTML
|
|
18
|
+
*/
|
|
19
|
+
function generateSvgHtml () {
|
|
20
|
+
const size = SIZES.touchTargetSize
|
|
21
|
+
const half = HALF_SIZE
|
|
22
|
+
return `
|
|
23
|
+
<svg width='${size}' height='${size}' viewBox='0 0 48 48' fill-rule='evenodd'
|
|
24
|
+
style='display:none;position:absolute;top:0;left:0;margin:${half}px 0 0 -${half}px;cursor:grab'
|
|
25
|
+
class='im-draw-touch-target' data-im-draw-touch-target>
|
|
26
|
+
<circle cx='24' cy='24' r='24' fill='var(--draw-halo, #000)'/>
|
|
27
|
+
<path d="M37.543 25H34a1 1 0 1 1 0-2h3.629l-.836-.837a1 1 0 0 1 1.414-1.414l2.5 2.501A1 1 0 0 1 41 24a1 1 0 0 1-.487.858l-2.306 2.306a1 1 0 0 1-1.414-1.414l.75-.75zM23 10.414l-.793.793a1 1 0 0 1-1.414-1.414l2.5-2.5C23.481 7.105 23.734 7 24 7s.519.105.707.293l2.5 2.5a1 1 0 0 1-1.414 1.414L25 10.414V14a1 1 0 1 1-2 0v-3.586zM7 24a1 1 0 0 1 .293-.75l2.5-2.501a1 1 0 0 1 1.414 1.414l-.836.837H14a1 1 0 1 1 0 2h-3.543l.75.75a1 1 0 0 1-1.414 1.414l-2.306-2.306A1 1 0 0 1 7 24zm16.293 16.707l-2.5-2.5a1 1 0 0 1 1.414-1.414l.793.793V34a1 1 0 1 1 2 0v3.586l.793-.793a1 1 0 0 1 1.414 1.414l-2.5 2.5c-.188.188-.441.293-.707.293s-.519-.105-.707-.293zM24 20c2.208 0 4 1.792 4 4s-1.792 4-4 4-4-1.792-4-4 1.792-4 4-4z" fill='var(--draw-bg, #fff)'/>
|
|
28
|
+
</svg>
|
|
29
|
+
`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create or retrieve the touch target SVG element
|
|
34
|
+
* @param {HTMLElement} container - Parent container element
|
|
35
|
+
* @returns {SVGElement} The touch target element
|
|
36
|
+
*/
|
|
37
|
+
export const createTouchTarget = (container) => {
|
|
38
|
+
let el = container.querySelector('[data-im-draw-touch-target]')
|
|
39
|
+
if (!el) {
|
|
40
|
+
container.insertAdjacentHTML('beforeend', generateSvgHtml())
|
|
41
|
+
el = container.querySelector('[data-im-draw-touch-target]')
|
|
42
|
+
}
|
|
43
|
+
return el
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Apply color scheme to touch target SVG
|
|
48
|
+
* @param {SVGElement} el - Touch target element
|
|
49
|
+
* @param {object} colors - Color object with editActive, editHalo, editVertex properties
|
|
50
|
+
*/
|
|
51
|
+
export const applyTouchTargetColors = (el, colors) => {
|
|
52
|
+
if (!el) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
el.style.setProperty('--draw-halo', colors.editActive)
|
|
56
|
+
el.style.setProperty('--draw-bg', colors.editHalo)
|
|
57
|
+
el.style.setProperty('--draw-primary', colors.editVertex)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Show touch target at specified pixel position
|
|
62
|
+
* @param {SVGElement} el - Touch target element
|
|
63
|
+
* @param {object} pixel - Position object with x, y properties
|
|
64
|
+
*/
|
|
65
|
+
export const showTouchTarget = (el, pixel) => {
|
|
66
|
+
if (!pixel || !el) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
el.style.left = `${pixel.x}px`
|
|
70
|
+
el.style.top = `${pixel.y}px`
|
|
71
|
+
el.style.display = 'block'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Hide touch target
|
|
76
|
+
* @param {SVGElement} el - Touch target element
|
|
77
|
+
*/
|
|
78
|
+
export const hideTouchTarget = (el) => {
|
|
79
|
+
if (el) {
|
|
80
|
+
el.style.display = 'none'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if event target is part of the touch target SVG
|
|
86
|
+
* @param {HTMLElement} el - Element to check
|
|
87
|
+
* @returns {boolean} True if element is part of touch target
|
|
88
|
+
*/
|
|
89
|
+
export const isOnTouchTarget = (el) => {
|
|
90
|
+
if (!el) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
const parent = el.parentNode
|
|
94
|
+
return (parent instanceof globalThis.SVGElement) || (parent?.ownerSVGElement != null)
|
|
95
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTouchTarget,
|
|
3
|
+
applyTouchTargetColors,
|
|
4
|
+
showTouchTarget,
|
|
5
|
+
hideTouchTarget,
|
|
6
|
+
isOnTouchTarget
|
|
7
|
+
} from './touchTarget.js'
|
|
8
|
+
|
|
9
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
10
|
+
|
|
11
|
+
describe('createTouchTarget', () => {
|
|
12
|
+
test('inserts the SVG target once and returns it', () => {
|
|
13
|
+
const container = document.createElement('div')
|
|
14
|
+
|
|
15
|
+
const el = createTouchTarget(container)
|
|
16
|
+
|
|
17
|
+
expect(el).not.toBeNull()
|
|
18
|
+
expect(el.hasAttribute('data-im-draw-touch-target')).toBe(true)
|
|
19
|
+
expect(container.querySelectorAll('[data-im-draw-touch-target]')).toHaveLength(1)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('returns the existing target without inserting a second one', () => {
|
|
23
|
+
const container = document.createElement('div')
|
|
24
|
+
const first = createTouchTarget(container)
|
|
25
|
+
const second = createTouchTarget(container)
|
|
26
|
+
|
|
27
|
+
expect(second).toBe(first)
|
|
28
|
+
expect(container.querySelectorAll('[data-im-draw-touch-target]')).toHaveLength(1)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('applyTouchTargetColors', () => {
|
|
33
|
+
test('sets the CSS custom properties from the colours', () => {
|
|
34
|
+
const el = document.createElement('div')
|
|
35
|
+
|
|
36
|
+
applyTouchTargetColors(el, { editActive: 'a', editHalo: 'h', editVertex: 'v' })
|
|
37
|
+
|
|
38
|
+
expect(el.style.getPropertyValue('--draw-halo')).toBe('a')
|
|
39
|
+
expect(el.style.getPropertyValue('--draw-bg')).toBe('h')
|
|
40
|
+
expect(el.style.getPropertyValue('--draw-primary')).toBe('v')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('does nothing for a null element', () => {
|
|
44
|
+
expect(() => applyTouchTargetColors(null, {})).not.toThrow()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('showTouchTarget', () => {
|
|
49
|
+
test('positions and shows the element', () => {
|
|
50
|
+
const el = document.createElement('div')
|
|
51
|
+
|
|
52
|
+
showTouchTarget(el, { x: 10, y: 20 })
|
|
53
|
+
|
|
54
|
+
expect(el.style.left).toBe('10px')
|
|
55
|
+
expect(el.style.top).toBe('20px')
|
|
56
|
+
expect(el.style.display).toBe('block')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('does nothing without a pixel or an element', () => {
|
|
60
|
+
const el = document.createElement('div')
|
|
61
|
+
showTouchTarget(el, null)
|
|
62
|
+
expect(el.style.display).toBe('')
|
|
63
|
+
|
|
64
|
+
expect(() => showTouchTarget(null, { x: 0, y: 0 })).not.toThrow()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('hideTouchTarget', () => {
|
|
69
|
+
test('hides the element', () => {
|
|
70
|
+
const el = document.createElement('div')
|
|
71
|
+
el.style.display = 'block'
|
|
72
|
+
|
|
73
|
+
hideTouchTarget(el)
|
|
74
|
+
|
|
75
|
+
expect(el.style.display).toBe('none')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('does nothing for a null element', () => {
|
|
79
|
+
expect(() => hideTouchTarget(null)).not.toThrow()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('isOnTouchTarget', () => {
|
|
84
|
+
test('returns false for a null element', () => {
|
|
85
|
+
expect(isOnTouchTarget(null)).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('returns false for an element without a parent', () => {
|
|
89
|
+
expect(isOnTouchTarget(document.createElement('div'))).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('returns true when the parent is an SVG element', () => {
|
|
93
|
+
const svg = document.createElementNS(SVG_NS, 'svg')
|
|
94
|
+
const circle = document.createElementNS(SVG_NS, 'circle')
|
|
95
|
+
svg.appendChild(circle)
|
|
96
|
+
|
|
97
|
+
expect(isOnTouchTarget(circle)).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('returns false when the parent is a plain HTML element', () => {
|
|
101
|
+
const div = document.createElement('div')
|
|
102
|
+
const span = document.createElement('span')
|
|
103
|
+
div.appendChild(span)
|
|
104
|
+
|
|
105
|
+
expect(isOnTouchTarget(span)).toBe(false)
|
|
106
|
+
})
|
|
107
|
+
})
|