squirreling 0.9.3 → 0.9.4
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/package.json +2 -2
- package/src/expression/geometry.d.ts +71 -0
- package/src/expression/spatial.equality.js +98 -0
- package/src/expression/spatial.geometry.js +620 -0
- package/src/expression/spatial.js +194 -1300
- package/src/expression/wkt.js +222 -0
|
@@ -1,434 +1,109 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// GeoJSON geometry helpers
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {{ type: string, coordinates: any, geometries?: GeoJsonGeometry[] }} GeoJsonGeometry
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const EPSILON = 1e-10
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Normalize a geometry value. Accepts GeoJSON objects.
|
|
17
|
-
* Returns null if the value is not a valid geometry.
|
|
18
|
-
*
|
|
19
|
-
* @param {SqlPrimitive} val
|
|
20
|
-
* @returns {GeoJsonGeometry | null}
|
|
21
|
-
*/
|
|
22
|
-
function toGeometry(val) {
|
|
23
|
-
if (val == null) return null
|
|
24
|
-
if (typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val)) {
|
|
25
|
-
// eslint-disable-next-line no-extra-parens
|
|
26
|
-
const geom = /** @type {GeoJsonGeometry} */ (val)
|
|
27
|
-
if (typeof geom.type === 'string' && geom.coordinates !== undefined) {
|
|
28
|
-
return geom
|
|
29
|
-
}
|
|
30
|
-
// GeometryCollection has geometries instead of coordinates
|
|
31
|
-
if (geom.type === 'GeometryCollection' && Array.isArray(geom.geometries)) {
|
|
32
|
-
return geom
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// Core geometric algorithms
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Compute the squared distance between two 2D points.
|
|
44
|
-
*
|
|
45
|
-
* @param {number[]} a
|
|
46
|
-
* @param {number[]} b
|
|
47
|
-
* @returns {number}
|
|
48
|
-
*/
|
|
49
|
-
function distSq(a, b) {
|
|
50
|
-
const dx = a[0] - b[0]
|
|
51
|
-
const dy = a[1] - b[1]
|
|
52
|
-
return dx * dx + dy * dy
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Compute the Euclidean distance between two 2D points.
|
|
57
|
-
*
|
|
58
|
-
* @param {number[]} a
|
|
59
|
-
* @param {number[]} b
|
|
60
|
-
* @returns {number}
|
|
61
|
-
*/
|
|
62
|
-
function pointDist(a, b) {
|
|
63
|
-
return Math.sqrt(distSq(a, b))
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Minimum distance from point p to line segment [a, b].
|
|
68
|
-
*
|
|
69
|
-
* @param {number[]} p
|
|
70
|
-
* @param {number[]} a
|
|
71
|
-
* @param {number[]} b
|
|
72
|
-
* @returns {number}
|
|
73
|
-
*/
|
|
74
|
-
function pointToSegmentDist(p, a, b) {
|
|
75
|
-
const dx = b[0] - a[0]
|
|
76
|
-
const dy = b[1] - a[1]
|
|
77
|
-
const lenSq = dx * dx + dy * dy
|
|
78
|
-
if (lenSq === 0) return pointDist(p, a)
|
|
79
|
-
let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq
|
|
80
|
-
t = Math.max(0, Math.min(1, t))
|
|
81
|
-
return pointDist(p, [a[0] + t * dx, a[1] + t * dy])
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Test whether two line segments [p1,p2] and [p3,p4] intersect.
|
|
86
|
-
* Returns true if they share any point (including endpoints).
|
|
87
|
-
*
|
|
88
|
-
* @param {number[]} p1
|
|
89
|
-
* @param {number[]} p2
|
|
90
|
-
* @param {number[]} p3
|
|
91
|
-
* @param {number[]} p4
|
|
92
|
-
* @returns {boolean}
|
|
93
|
-
*/
|
|
94
|
-
function segmentsIntersect(p1, p2, p3, p4) {
|
|
95
|
-
const d1 = cross(p3, p4, p1)
|
|
96
|
-
const d2 = cross(p3, p4, p2)
|
|
97
|
-
const d3 = cross(p1, p2, p3)
|
|
98
|
-
const d4 = cross(p1, p2, p4)
|
|
99
|
-
|
|
100
|
-
if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) &&
|
|
101
|
-
(d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
|
|
102
|
-
return true
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (Math.abs(d1) < EPSILON && onSegment(p3, p4, p1)) return true
|
|
106
|
-
if (Math.abs(d2) < EPSILON && onSegment(p3, p4, p2)) return true
|
|
107
|
-
if (Math.abs(d3) < EPSILON && onSegment(p1, p2, p3)) return true
|
|
108
|
-
if (Math.abs(d4) < EPSILON && onSegment(p1, p2, p4)) return true
|
|
109
|
-
|
|
110
|
-
return false
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Cross product of vectors (b-a) and (c-a).
|
|
115
|
-
*
|
|
116
|
-
* @param {number[]} a
|
|
117
|
-
* @param {number[]} b
|
|
118
|
-
* @param {number[]} c
|
|
119
|
-
* @returns {number}
|
|
120
|
-
*/
|
|
121
|
-
function cross(a, b, c) {
|
|
122
|
-
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Check if point c lies on segment [a, b], assuming collinearity.
|
|
127
|
-
*
|
|
128
|
-
* @param {number[]} a
|
|
129
|
-
* @param {number[]} b
|
|
130
|
-
* @param {number[]} c
|
|
131
|
-
* @returns {boolean}
|
|
132
|
-
*/
|
|
133
|
-
function onSegment(a, b, c) {
|
|
134
|
-
return Math.min(a[0], b[0]) - EPSILON <= c[0] && c[0] <= Math.max(a[0], b[0]) + EPSILON &&
|
|
135
|
-
Math.min(a[1], b[1]) - EPSILON <= c[1] && c[1] <= Math.max(a[1], b[1]) + EPSILON
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Point-in-polygon test using ray casting.
|
|
140
|
-
* ring is an array of [x, y] coords (closed ring, first = last).
|
|
141
|
-
*
|
|
142
|
-
* @param {number[]} point
|
|
143
|
-
* @param {number[][]} ring
|
|
144
|
-
* @returns {boolean}
|
|
145
|
-
*/
|
|
146
|
-
function pointInRing(point, ring) {
|
|
147
|
-
const [px, py] = point
|
|
148
|
-
let inside = false
|
|
149
|
-
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
150
|
-
const xi = ring[i][0], yi = ring[i][1]
|
|
151
|
-
const xj = ring[j][0], yj = ring[j][1]
|
|
152
|
-
if (yi > py !== yj > py &&
|
|
153
|
-
px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
|
|
154
|
-
inside = !inside
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return inside
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Test if point is on the boundary of a ring.
|
|
162
|
-
*
|
|
163
|
-
* @param {number[]} point
|
|
164
|
-
* @param {number[][]} ring
|
|
165
|
-
* @returns {boolean}
|
|
166
|
-
*/
|
|
167
|
-
function pointOnRingBoundary(point, ring) {
|
|
168
|
-
for (let i = 0; i < ring.length - 1; i++) {
|
|
169
|
-
if (pointToSegmentDist(point, ring[i], ring[i + 1]) < EPSILON) {
|
|
170
|
-
return true
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return false
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Test if point is inside a polygon (array of rings).
|
|
178
|
-
* First ring is exterior, rest are holes.
|
|
179
|
-
*
|
|
180
|
-
* @param {number[]} point
|
|
181
|
-
* @param {number[][][]} rings
|
|
182
|
-
* @returns {boolean}
|
|
183
|
-
*/
|
|
184
|
-
function pointInPolygon(point, rings) {
|
|
185
|
-
if (!pointInRing(point, rings[0]) && !pointOnRingBoundary(point, rings[0])) return false
|
|
186
|
-
for (let i = 1; i < rings.length; i++) {
|
|
187
|
-
// Point must not be inside a hole (but can be on hole boundary)
|
|
188
|
-
if (pointInRing(point, rings[i]) && !pointOnRingBoundary(point, rings[i])) return false
|
|
189
|
-
}
|
|
190
|
-
return true
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Test if point is strictly inside a polygon (not on boundary).
|
|
195
|
-
*
|
|
196
|
-
* @param {number[]} point
|
|
197
|
-
* @param {number[][][]} rings
|
|
198
|
-
* @returns {boolean}
|
|
199
|
-
*/
|
|
200
|
-
function pointInPolygonInterior(point, rings) {
|
|
201
|
-
if (!pointInRing(point, rings[0])) return false
|
|
202
|
-
if (pointOnRingBoundary(point, rings[0])) return false
|
|
203
|
-
for (let i = 1; i < rings.length; i++) {
|
|
204
|
-
if (pointInRing(point, rings[i])) return false
|
|
205
|
-
}
|
|
206
|
-
return true
|
|
207
|
-
}
|
|
1
|
+
import { geomToWkt, parseWkt } from './wkt.js'
|
|
2
|
+
import { distSq, pointInPolygon, pointLineRelation, pointToSegmentDistSq, simpleIntersects, simplePairContainment, simplePairRelation } from './spatial.geometry.js'
|
|
3
|
+
import { simpleGeomEqual } from './spatial.equality.js'
|
|
208
4
|
|
|
209
5
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* @param {number[]} a
|
|
213
|
-
* @param {number[]} b
|
|
214
|
-
* @param {number[][]} ring
|
|
215
|
-
* @returns {boolean}
|
|
6
|
+
* @import { SpatialFunc, SqlPrimitive } from '../types.js'
|
|
7
|
+
* @import { Geometry, Point, SimpleGeometry } from './geometry.js'
|
|
216
8
|
*/
|
|
217
|
-
function segmentIntersectsRing(a, b, ring) {
|
|
218
|
-
for (let i = 0; i < ring.length - 1; i++) {
|
|
219
|
-
if (segmentsIntersect(a, b, ring[i], ring[i + 1])) return true
|
|
220
|
-
}
|
|
221
|
-
return false
|
|
222
|
-
}
|
|
223
9
|
|
|
224
10
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* @param {number[][]} line
|
|
228
|
-
* @param {number[][][]} rings
|
|
229
|
-
* @returns {boolean}
|
|
230
|
-
*/
|
|
231
|
-
function lineIntersectsPolygon(line, rings) {
|
|
232
|
-
// Check if any point of the line is inside the polygon
|
|
233
|
-
for (const pt of line) {
|
|
234
|
-
if (pointInPolygon(pt, rings)) return true
|
|
235
|
-
}
|
|
236
|
-
// Check if any segment of the line intersects any ring edge
|
|
237
|
-
for (let i = 0; i < line.length - 1; i++) {
|
|
238
|
-
for (const ring of rings) {
|
|
239
|
-
if (segmentIntersectsRing(line[i], line[i + 1], ring)) return true
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return false
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Test if two linestrings share any point (intersect).
|
|
11
|
+
* Evaluate a spatial predicate function.
|
|
247
12
|
*
|
|
248
|
-
* @param {
|
|
249
|
-
* @param {
|
|
250
|
-
* @
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {SpatialFunc} options.funcName
|
|
15
|
+
* @param {SqlPrimitive[]} options.args
|
|
16
|
+
* @returns {SqlPrimitive}
|
|
251
17
|
*/
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
18
|
+
export function evaluateSpatialFunc({ funcName, args }) {
|
|
19
|
+
// Singleton functions
|
|
20
|
+
if (funcName === 'ST_GEOMFROMTEXT') {
|
|
21
|
+
if (args[0] == null) return null
|
|
22
|
+
return parseWkt(String(args[0]))
|
|
257
23
|
}
|
|
258
|
-
return false
|
|
259
|
-
}
|
|
260
24
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
for (const pt of rings1[0]) {
|
|
271
|
-
if (pointInPolygon(pt, rings2)) return true
|
|
272
|
-
}
|
|
273
|
-
// Check if any vertex of polygon2 is inside polygon1
|
|
274
|
-
for (const pt of rings2[0]) {
|
|
275
|
-
if (pointInPolygon(pt, rings1)) return true
|
|
276
|
-
}
|
|
277
|
-
// Check if any edge of polygon1 intersects any edge of polygon2
|
|
278
|
-
for (let i = 0; i < rings1[0].length - 1; i++) {
|
|
279
|
-
for (let j = 0; j < rings2[0].length - 1; j++) {
|
|
280
|
-
if (segmentsIntersect(rings1[0][i], rings1[0][i + 1], rings2[0][j], rings2[0][j + 1])) return true
|
|
25
|
+
if (funcName === 'ST_MAKEENVELOPE') {
|
|
26
|
+
if (args[0] == null || args[1] == null || args[2] == null || args[3] == null) return null
|
|
27
|
+
const xmin = Number(args[0])
|
|
28
|
+
const ymin = Number(args[1])
|
|
29
|
+
const xmax = Number(args[2])
|
|
30
|
+
const ymax = Number(args[3])
|
|
31
|
+
return {
|
|
32
|
+
type: 'Polygon',
|
|
33
|
+
coordinates: [[[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax], [xmin, ymin]]],
|
|
281
34
|
}
|
|
282
35
|
}
|
|
283
|
-
return false
|
|
284
|
-
}
|
|
285
36
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
* @param {number[][][]} rings
|
|
291
|
-
* @returns {boolean}
|
|
292
|
-
*/
|
|
293
|
-
function lineInsidePolygon(line, rings) {
|
|
294
|
-
for (const pt of line) {
|
|
295
|
-
if (!pointInPolygon(pt, rings)) return false
|
|
296
|
-
}
|
|
297
|
-
// Also check that no segment crosses a hole boundary from inside to outside
|
|
298
|
-
for (let i = 0; i < line.length - 1; i++) {
|
|
299
|
-
const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
|
|
300
|
-
if (!pointInPolygon(mid, rings)) return false
|
|
37
|
+
const geomA = toGeometry(args[0])
|
|
38
|
+
if (funcName === 'ST_ASTEXT') {
|
|
39
|
+
if (geomA == null) return null
|
|
40
|
+
return geomToWkt(geomA)
|
|
301
41
|
}
|
|
302
|
-
return true
|
|
303
|
-
}
|
|
304
42
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
* @returns {boolean}
|
|
311
|
-
*/
|
|
312
|
-
function lineInPolygonInterior(line, rings) {
|
|
313
|
-
for (const pt of line) {
|
|
314
|
-
if (!pointInPolygonInterior(pt, rings)) return false
|
|
315
|
-
}
|
|
316
|
-
for (let i = 0; i < line.length - 1; i++) {
|
|
317
|
-
const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
|
|
318
|
-
if (!pointInPolygonInterior(mid, rings)) return false
|
|
319
|
-
}
|
|
320
|
-
return true
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Test if polygon A contains polygon B (all of B is inside A).
|
|
325
|
-
*
|
|
326
|
-
* @param {number[][][]} ringsA
|
|
327
|
-
* @param {number[][][]} ringsB
|
|
328
|
-
* @returns {boolean}
|
|
329
|
-
*/
|
|
330
|
-
function polygonContainsPolygon(ringsA, ringsB) {
|
|
331
|
-
// Every vertex of B's exterior must be inside A
|
|
332
|
-
for (const pt of ringsB[0]) {
|
|
333
|
-
if (!pointInPolygon(pt, ringsA)) return false
|
|
334
|
-
}
|
|
335
|
-
// Check that edges of B don't cross outside A
|
|
336
|
-
for (let i = 0; i < ringsB[0].length - 1; i++) {
|
|
337
|
-
const mid = [(ringsB[0][i][0] + ringsB[0][i + 1][0]) / 2, (ringsB[0][i][1] + ringsB[0][i + 1][1]) / 2]
|
|
338
|
-
if (!pointInPolygon(mid, ringsA)) return false
|
|
339
|
-
}
|
|
340
|
-
return true
|
|
341
|
-
}
|
|
43
|
+
// Predicate functions (require two geometries)
|
|
44
|
+
const geomB = toGeometry(args[1])
|
|
45
|
+
if (geomA == null || geomB == null) return null
|
|
46
|
+
const a = decompose(geomA)
|
|
47
|
+
const b = decompose(geomB)
|
|
342
48
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
49
|
+
switch (funcName) {
|
|
50
|
+
case 'ST_INTERSECTS': return simpleIntersects(a, b)
|
|
51
|
+
case 'ST_CONTAINS': return stContains(a, b)
|
|
52
|
+
case 'ST_CONTAINSPROPERLY': return stContainsProperly(a, b)
|
|
53
|
+
case 'ST_WITHIN': return stContains(b, a) // inverse of contains
|
|
54
|
+
case 'ST_OVERLAPS': return stOverlaps(a, b)
|
|
55
|
+
case 'ST_TOUCHES': return stTouches(a, b)
|
|
56
|
+
case 'ST_EQUALS': return stEquals(a, b)
|
|
57
|
+
case 'ST_CROSSES': return stCrosses(a, b)
|
|
58
|
+
case 'ST_COVERS': return stContains(a, b) // TODO: handle boundary
|
|
59
|
+
case 'ST_COVEREDBY': return stContains(b, a) // inverse of covers
|
|
60
|
+
case 'ST_DWITHIN': {
|
|
61
|
+
if (args[2] == null) return null
|
|
62
|
+
const dist = Number(args[2])
|
|
63
|
+
return stDWithin(a, b, dist)
|
|
353
64
|
}
|
|
354
|
-
|
|
355
|
-
const mid = [(ringsB[0][i][0] + ringsB[0][i + 1][0]) / 2, (ringsB[0][i][1] + ringsB[0][i + 1][1]) / 2]
|
|
356
|
-
if (!pointInPolygonInterior(mid, ringsA)) return false
|
|
65
|
+
default: return null
|
|
357
66
|
}
|
|
358
|
-
return true
|
|
359
67
|
}
|
|
360
68
|
|
|
361
69
|
/**
|
|
362
|
-
*
|
|
70
|
+
* Normalize a geometry value. Accepts GeoJSON objects.
|
|
71
|
+
* Returns null if the value is not a valid geometry.
|
|
363
72
|
*
|
|
364
|
-
* @param {
|
|
365
|
-
* @
|
|
366
|
-
* @returns {boolean}
|
|
73
|
+
* @param {SqlPrimitive} val
|
|
74
|
+
* @returns {Geometry | null}
|
|
367
75
|
*/
|
|
368
|
-
function
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
let match = true
|
|
374
|
-
for (let i = 0; i < n; i++) {
|
|
375
|
-
const j = (i + offset) % n
|
|
376
|
-
if (Math.abs(ring1[i][0] - ring2[j][0]) > EPSILON ||
|
|
377
|
-
Math.abs(ring1[i][1] - ring2[j][1]) > EPSILON) {
|
|
378
|
-
match = false
|
|
379
|
-
break
|
|
380
|
-
}
|
|
76
|
+
function toGeometry(val) {
|
|
77
|
+
if (typeof val === 'object' && val != null && 'type' in val) {
|
|
78
|
+
if (val.type === 'GeometryCollection' && Array.isArray(val.geometries)) {
|
|
79
|
+
// eslint-disable-next-line no-extra-parens
|
|
80
|
+
return /** @type {Geometry} */ (val)
|
|
381
81
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
let match = true
|
|
387
|
-
for (let i = 0; i < n; i++) {
|
|
388
|
-
const j = (n - i + offset) % n
|
|
389
|
-
if (Math.abs(ring1[i][0] - ring2[j][0]) > EPSILON ||
|
|
390
|
-
Math.abs(ring1[i][1] - ring2[j][1]) > EPSILON) {
|
|
391
|
-
match = false
|
|
392
|
-
break
|
|
393
|
-
}
|
|
82
|
+
const geometryTypes = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon']
|
|
83
|
+
if (geometryTypes.includes(val.type) && Array.isArray(val.coordinates)) {
|
|
84
|
+
// eslint-disable-next-line no-extra-parens
|
|
85
|
+
return /** @type {Geometry} */ (val)
|
|
394
86
|
}
|
|
395
|
-
if (match) return true
|
|
396
87
|
}
|
|
397
|
-
return
|
|
88
|
+
return null
|
|
398
89
|
}
|
|
399
90
|
|
|
400
91
|
// ============================================================================
|
|
401
92
|
// Minimum distance between geometries
|
|
402
93
|
// ============================================================================
|
|
403
94
|
|
|
404
|
-
/**
|
|
405
|
-
* Get all coordinates from a geometry as a flat list of [x, y] points.
|
|
406
|
-
*
|
|
407
|
-
* @param {GeoJsonGeometry} geom
|
|
408
|
-
* @returns {number[][]}
|
|
409
|
-
*/
|
|
410
|
-
function getPoints(geom) {
|
|
411
|
-
switch (geom.type) {
|
|
412
|
-
case 'Point': return [geom.coordinates]
|
|
413
|
-
case 'MultiPoint': return geom.coordinates
|
|
414
|
-
case 'LineString': return geom.coordinates
|
|
415
|
-
case 'MultiLineString': return geom.coordinates.flat()
|
|
416
|
-
case 'Polygon': return geom.coordinates[0]
|
|
417
|
-
case 'MultiPolygon': return geom.coordinates.flatMap((/** @type {number[][][]} */ p) => p[0])
|
|
418
|
-
case 'GeometryCollection': return geom.geometries.flatMap((/** @type {GeoJsonGeometry} */ g) => getPoints(g))
|
|
419
|
-
default: return []
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
95
|
/**
|
|
424
96
|
* Get all line segments from a geometry.
|
|
425
97
|
*
|
|
426
|
-
* @param {
|
|
427
|
-
* @returns {Array<[number[], number[]]
|
|
98
|
+
* @param {SimpleGeometry[]} geoms
|
|
99
|
+
* @returns {{ segments: Array<[number[], number[]]>, points: number[][] }}
|
|
428
100
|
*/
|
|
429
|
-
function getSegments(
|
|
101
|
+
function getSegments(geoms) {
|
|
430
102
|
/** @type {Array<[number[], number[]]>} */
|
|
431
103
|
const segments = []
|
|
104
|
+
/** @type {number[][]} */
|
|
105
|
+
const points = []
|
|
106
|
+
|
|
432
107
|
/**
|
|
433
108
|
* @param {number[][]} coords
|
|
434
109
|
*/
|
|
@@ -436,509 +111,130 @@ function getSegments(geom) {
|
|
|
436
111
|
for (let i = 0; i < coords.length - 1; i++) {
|
|
437
112
|
segments.push([coords[i], coords[i + 1]])
|
|
438
113
|
}
|
|
114
|
+
points.push(...coords)
|
|
439
115
|
}
|
|
440
|
-
switch (geom.type) {
|
|
441
|
-
case 'LineString': addLine(geom.coordinates); break
|
|
442
|
-
case 'MultiLineString': geom.coordinates.forEach((/** @type {number[][]} */ l) => addLine(l)); break
|
|
443
|
-
case 'Polygon': geom.coordinates.forEach((/** @type {number[][]} */ r) => addLine(r)); break
|
|
444
|
-
case 'MultiPolygon': geom.coordinates.forEach((/** @type {number[][][]} */ p) => p.forEach((/** @type {number[][]} */ r) => addLine(r))); break
|
|
445
|
-
case 'GeometryCollection': geom.geometries.forEach((/** @type {GeoJsonGeometry} */ g) => segments.push(...getSegments(g))); break
|
|
446
|
-
}
|
|
447
|
-
return segments
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Minimum distance between two segments.
|
|
452
|
-
*
|
|
453
|
-
* @param {number[]} a1
|
|
454
|
-
* @param {number[]} a2
|
|
455
|
-
* @param {number[]} b1
|
|
456
|
-
* @param {number[]} b2
|
|
457
|
-
* @returns {number}
|
|
458
|
-
*/
|
|
459
|
-
function segmentToSegmentDist(a1, a2, b1, b2) {
|
|
460
|
-
if (segmentsIntersect(a1, a2, b1, b2)) return 0
|
|
461
|
-
return Math.min(
|
|
462
|
-
pointToSegmentDist(a1, b1, b2),
|
|
463
|
-
pointToSegmentDist(a2, b1, b2),
|
|
464
|
-
pointToSegmentDist(b1, a1, a2),
|
|
465
|
-
pointToSegmentDist(b2, a1, a2)
|
|
466
|
-
)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Compute the minimum distance between two geometries.
|
|
471
|
-
*
|
|
472
|
-
* @param {GeoJsonGeometry} a
|
|
473
|
-
* @param {GeoJsonGeometry} b
|
|
474
|
-
* @returns {number}
|
|
475
|
-
*/
|
|
476
|
-
function geometryDistance(a, b) {
|
|
477
|
-
// Handle geometries that contain each other
|
|
478
|
-
if (a.type === 'Polygon' || a.type === 'MultiPolygon') {
|
|
479
|
-
const pts = getPoints(b)
|
|
480
|
-
for (const pt of pts) {
|
|
481
|
-
if (a.type === 'Polygon' && pointInPolygon(pt, a.coordinates)) return 0
|
|
482
|
-
if (a.type === 'MultiPolygon') {
|
|
483
|
-
for (const poly of a.coordinates) {
|
|
484
|
-
if (pointInPolygon(pt, poly)) return 0
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
if (b.type === 'Polygon' || b.type === 'MultiPolygon') {
|
|
490
|
-
const pts = getPoints(a)
|
|
491
|
-
for (const pt of pts) {
|
|
492
|
-
if (b.type === 'Polygon' && pointInPolygon(pt, b.coordinates)) return 0
|
|
493
|
-
if (b.type === 'MultiPolygon') {
|
|
494
|
-
for (const poly of b.coordinates) {
|
|
495
|
-
if (pointInPolygon(pt, poly)) return 0
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const segsA = getSegments(a)
|
|
502
|
-
const segsB = getSegments(b)
|
|
503
|
-
const ptsA = getPoints(a)
|
|
504
|
-
const ptsB = getPoints(b)
|
|
505
|
-
|
|
506
|
-
let min = Infinity
|
|
507
116
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
117
|
+
for (const geom of geoms) {
|
|
118
|
+
if (geom.type === 'Point') points.push(geom.coordinates)
|
|
119
|
+
else if (geom.type === 'LineString') addLine(geom.coordinates)
|
|
120
|
+
else if (geom.type === 'Polygon') geom.coordinates.forEach(addLine)
|
|
513
121
|
}
|
|
514
122
|
|
|
515
|
-
|
|
516
|
-
if (segsB.length > 0) {
|
|
517
|
-
for (const pt of ptsA) {
|
|
518
|
-
for (const [b1, b2] of segsB) {
|
|
519
|
-
min = Math.min(min, pointToSegmentDist(pt, b1, b2))
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (segsA.length > 0) {
|
|
524
|
-
for (const pt of ptsB) {
|
|
525
|
-
for (const [a1, a2] of segsA) {
|
|
526
|
-
min = Math.min(min, pointToSegmentDist(pt, a1, a2))
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Point-to-point fallback
|
|
532
|
-
if (segsA.length === 0 && segsB.length === 0) {
|
|
533
|
-
for (const pa of ptsA) {
|
|
534
|
-
for (const pb of ptsB) {
|
|
535
|
-
min = Math.min(min, pointDist(pa, pb))
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return min
|
|
123
|
+
return { segments, points }
|
|
541
124
|
}
|
|
542
125
|
|
|
543
|
-
// ============================================================================
|
|
544
|
-
// Spatial predicate dispatch - decompose to primitive type pairs
|
|
545
|
-
// ============================================================================
|
|
546
|
-
|
|
547
126
|
/**
|
|
548
|
-
*
|
|
127
|
+
* Test whether two geometries are within a given distance of each other.
|
|
128
|
+
* Intersecting geometries have distance 0. For non-intersecting geometries,
|
|
129
|
+
* the minimum distance is always at an endpoint, so point-to-segment suffices.
|
|
549
130
|
*
|
|
550
|
-
* @param {
|
|
551
|
-
* @
|
|
552
|
-
|
|
553
|
-
function decompose(geom) {
|
|
554
|
-
switch (geom.type) {
|
|
555
|
-
case 'MultiPoint':
|
|
556
|
-
return geom.coordinates.map((/** @type {number[]} */ c) => ({ type: 'Point', coordinates: c }))
|
|
557
|
-
case 'MultiLineString':
|
|
558
|
-
return geom.coordinates.map((/** @type {number[][]} */ c) => ({ type: 'LineString', coordinates: c }))
|
|
559
|
-
case 'MultiPolygon':
|
|
560
|
-
return geom.coordinates.map((/** @type {number[][][]} */ c) => ({ type: 'Polygon', coordinates: c }))
|
|
561
|
-
case 'GeometryCollection':
|
|
562
|
-
return geom.geometries.flatMap((/** @type {GeoJsonGeometry} */ g) => decompose(g))
|
|
563
|
-
default:
|
|
564
|
-
return [geom]
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// ============================================================================
|
|
569
|
-
// ST_Intersects
|
|
570
|
-
// ============================================================================
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* @param {GeoJsonGeometry} a
|
|
574
|
-
* @param {GeoJsonGeometry} b
|
|
575
|
-
* @returns {boolean}
|
|
576
|
-
*/
|
|
577
|
-
function stIntersects(a, b) {
|
|
578
|
-
const partsA = decompose(a)
|
|
579
|
-
const partsB = decompose(b)
|
|
580
|
-
for (const pa of partsA) {
|
|
581
|
-
for (const pb of partsB) {
|
|
582
|
-
if (simplePairIntersects(pa, pb)) return true
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
return false
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* @param {GeoJsonGeometry} a
|
|
590
|
-
* @param {GeoJsonGeometry} b
|
|
591
|
-
* @returns {boolean}
|
|
592
|
-
*/
|
|
593
|
-
function simplePairIntersects(a, b) {
|
|
594
|
-
const ta = a.type
|
|
595
|
-
const tb = b.type
|
|
596
|
-
|
|
597
|
-
if (ta === 'Point' && tb === 'Point') {
|
|
598
|
-
return pointDist(a.coordinates, b.coordinates) < EPSILON
|
|
599
|
-
}
|
|
600
|
-
if (ta === 'Point' && tb === 'LineString') {
|
|
601
|
-
return pointOnLine(a.coordinates, b.coordinates)
|
|
602
|
-
}
|
|
603
|
-
if (ta === 'LineString' && tb === 'Point') {
|
|
604
|
-
return pointOnLine(b.coordinates, a.coordinates)
|
|
605
|
-
}
|
|
606
|
-
if (ta === 'Point' && tb === 'Polygon') {
|
|
607
|
-
return pointInPolygon(a.coordinates, b.coordinates)
|
|
608
|
-
}
|
|
609
|
-
if (ta === 'Polygon' && tb === 'Point') {
|
|
610
|
-
return pointInPolygon(b.coordinates, a.coordinates)
|
|
611
|
-
}
|
|
612
|
-
if (ta === 'LineString' && tb === 'LineString') {
|
|
613
|
-
return linesIntersect(a.coordinates, b.coordinates)
|
|
614
|
-
}
|
|
615
|
-
if (ta === 'LineString' && tb === 'Polygon') {
|
|
616
|
-
return lineIntersectsPolygon(a.coordinates, b.coordinates)
|
|
617
|
-
}
|
|
618
|
-
if (ta === 'Polygon' && tb === 'LineString') {
|
|
619
|
-
return lineIntersectsPolygon(b.coordinates, a.coordinates)
|
|
620
|
-
}
|
|
621
|
-
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
622
|
-
return polygonsIntersect(a.coordinates, b.coordinates)
|
|
623
|
-
}
|
|
624
|
-
return false
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Test if point is on a linestring.
|
|
629
|
-
*
|
|
630
|
-
* @param {number[]} point
|
|
631
|
-
* @param {number[][]} line
|
|
632
|
-
* @returns {boolean}
|
|
633
|
-
*/
|
|
634
|
-
function pointOnLine(point, line) {
|
|
635
|
-
for (let i = 0; i < line.length - 1; i++) {
|
|
636
|
-
if (pointToSegmentDist(point, line[i], line[i + 1]) < EPSILON) return true
|
|
637
|
-
}
|
|
638
|
-
return false
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// ============================================================================
|
|
642
|
-
// ST_Contains
|
|
643
|
-
// ============================================================================
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* @param {GeoJsonGeometry} a
|
|
647
|
-
* @param {GeoJsonGeometry} b
|
|
648
|
-
* @returns {boolean}
|
|
649
|
-
*/
|
|
650
|
-
function stContains(a, b) {
|
|
651
|
-
// Every part of b must be inside some part of a
|
|
652
|
-
const partsB = decompose(b)
|
|
653
|
-
for (const pb of partsB) {
|
|
654
|
-
if (!simpleContainedByAny(a, pb)) return false
|
|
655
|
-
}
|
|
656
|
-
return true
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* @param {GeoJsonGeometry} a
|
|
661
|
-
* @param {GeoJsonGeometry} b - simple geometry
|
|
662
|
-
* @returns {boolean}
|
|
663
|
-
*/
|
|
664
|
-
function simpleContainedByAny(a, b) {
|
|
665
|
-
const partsA = decompose(a)
|
|
666
|
-
for (const pa of partsA) {
|
|
667
|
-
if (simplePairContains(pa, b)) return true
|
|
668
|
-
}
|
|
669
|
-
return false
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* @param {GeoJsonGeometry} a
|
|
674
|
-
* @param {GeoJsonGeometry} b
|
|
675
|
-
* @returns {boolean}
|
|
676
|
-
*/
|
|
677
|
-
function simplePairContains(a, b) {
|
|
678
|
-
const ta = a.type
|
|
679
|
-
const tb = b.type
|
|
680
|
-
|
|
681
|
-
if (ta === 'Point' && tb === 'Point') {
|
|
682
|
-
return pointDist(a.coordinates, b.coordinates) < EPSILON
|
|
683
|
-
}
|
|
684
|
-
if (ta === 'LineString' && tb === 'Point') {
|
|
685
|
-
return pointOnLine(b.coordinates, a.coordinates)
|
|
686
|
-
}
|
|
687
|
-
if (ta === 'Polygon' && tb === 'Point') {
|
|
688
|
-
return pointInPolygon(b.coordinates, a.coordinates)
|
|
689
|
-
}
|
|
690
|
-
if (ta === 'Polygon' && tb === 'LineString') {
|
|
691
|
-
return lineInsidePolygon(b.coordinates, a.coordinates)
|
|
692
|
-
}
|
|
693
|
-
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
694
|
-
return polygonContainsPolygon(a.coordinates, b.coordinates)
|
|
695
|
-
}
|
|
696
|
-
if (ta === 'LineString' && tb === 'LineString') {
|
|
697
|
-
// Line A contains line B if every point of B is on A
|
|
698
|
-
for (const pt of b.coordinates) {
|
|
699
|
-
if (!pointOnLine(pt, a.coordinates)) return false
|
|
700
|
-
}
|
|
701
|
-
return true
|
|
702
|
-
}
|
|
703
|
-
return false
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// ============================================================================
|
|
707
|
-
// ST_ContainsProperly
|
|
708
|
-
// ============================================================================
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* @param {GeoJsonGeometry} a
|
|
712
|
-
* @param {GeoJsonGeometry} b
|
|
713
|
-
* @returns {boolean}
|
|
714
|
-
*/
|
|
715
|
-
function stContainsProperly(a, b) {
|
|
716
|
-
const partsB = decompose(b)
|
|
717
|
-
for (const pb of partsB) {
|
|
718
|
-
if (!simpleContainedByAnyProperly(a, pb)) return false
|
|
719
|
-
}
|
|
720
|
-
return true
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* @param {GeoJsonGeometry} a
|
|
725
|
-
* @param {GeoJsonGeometry} b
|
|
726
|
-
* @returns {boolean}
|
|
727
|
-
*/
|
|
728
|
-
function simpleContainedByAnyProperly(a, b) {
|
|
729
|
-
const partsA = decompose(a)
|
|
730
|
-
for (const pa of partsA) {
|
|
731
|
-
if (simplePairContainsProperly(pa, b)) return true
|
|
732
|
-
}
|
|
733
|
-
return false
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* @param {GeoJsonGeometry} a
|
|
738
|
-
* @param {GeoJsonGeometry} b
|
|
739
|
-
* @returns {boolean}
|
|
740
|
-
*/
|
|
741
|
-
function simplePairContainsProperly(a, b) {
|
|
742
|
-
const ta = a.type
|
|
743
|
-
const tb = b.type
|
|
744
|
-
|
|
745
|
-
if (ta === 'Polygon' && tb === 'Point') {
|
|
746
|
-
return pointInPolygonInterior(b.coordinates, a.coordinates)
|
|
747
|
-
}
|
|
748
|
-
if (ta === 'Polygon' && tb === 'LineString') {
|
|
749
|
-
return lineInPolygonInterior(b.coordinates, a.coordinates)
|
|
750
|
-
}
|
|
751
|
-
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
752
|
-
return polygonContainsPolygonProperly(a.coordinates, b.coordinates)
|
|
753
|
-
}
|
|
754
|
-
// Points and lines have no interior in the topological sense for "contains properly"
|
|
755
|
-
return false
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// ============================================================================
|
|
759
|
-
// ST_Within (inverse of ST_Contains)
|
|
760
|
-
// ============================================================================
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* @param {GeoJsonGeometry} a
|
|
764
|
-
* @param {GeoJsonGeometry} b
|
|
765
|
-
* @returns {boolean}
|
|
766
|
-
*/
|
|
767
|
-
function stWithin(a, b) {
|
|
768
|
-
return stContains(b, a)
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// ============================================================================
|
|
772
|
-
// ST_Touches
|
|
773
|
-
// ============================================================================
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* @param {GeoJsonGeometry} a
|
|
777
|
-
* @param {GeoJsonGeometry} b
|
|
778
|
-
* @returns {boolean}
|
|
779
|
-
*/
|
|
780
|
-
function stTouches(a, b) {
|
|
781
|
-
// Geometries touch if they intersect but their interiors do not
|
|
782
|
-
if (!stIntersects(a, b)) return false
|
|
783
|
-
|
|
784
|
-
const partsA = decompose(a)
|
|
785
|
-
const partsB = decompose(b)
|
|
786
|
-
for (const pa of partsA) {
|
|
787
|
-
for (const pb of partsB) {
|
|
788
|
-
if (simplePairInteriorsIntersect(pa, pb)) return false
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
return true
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* Test if interiors of two simple geometries share any point.
|
|
796
|
-
*
|
|
797
|
-
* @param {GeoJsonGeometry} a
|
|
798
|
-
* @param {GeoJsonGeometry} b
|
|
131
|
+
* @param {SimpleGeometry[]} a
|
|
132
|
+
* @param {SimpleGeometry[]} b
|
|
133
|
+
* @param {number} distance
|
|
799
134
|
* @returns {boolean}
|
|
800
135
|
*/
|
|
801
|
-
function
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (ta === 'LineString' && tb === 'Point') {
|
|
815
|
-
return pointInLineInterior(b.coordinates, a.coordinates)
|
|
816
|
-
}
|
|
817
|
-
if (ta === 'Point' && tb === 'Polygon') {
|
|
818
|
-
return pointInPolygonInterior(a.coordinates, b.coordinates)
|
|
819
|
-
}
|
|
820
|
-
if (ta === 'Polygon' && tb === 'Point') {
|
|
821
|
-
return pointInPolygonInterior(b.coordinates, a.coordinates)
|
|
822
|
-
}
|
|
823
|
-
if (ta === 'LineString' && tb === 'LineString') {
|
|
824
|
-
// Check if lines share interior points (not just endpoints)
|
|
825
|
-
return linesShareInterior(a.coordinates, b.coordinates)
|
|
826
|
-
}
|
|
827
|
-
if (ta === 'LineString' && tb === 'Polygon') {
|
|
828
|
-
return lineInteriorIntersectsPolygonInterior(a.coordinates, b.coordinates)
|
|
136
|
+
function stDWithin(a, b, distance) {
|
|
137
|
+
if (distance < 0) return false
|
|
138
|
+
if (simpleIntersects(a, b)) return true
|
|
139
|
+
|
|
140
|
+
const distanceSq = distance * distance
|
|
141
|
+
const { points: ptsA, segments: segsA } = getSegments(a)
|
|
142
|
+
const { points: ptsB, segments: segsB } = getSegments(b)
|
|
143
|
+
|
|
144
|
+
// Point-to-point
|
|
145
|
+
for (const pa of ptsA) {
|
|
146
|
+
for (const pb of ptsB) {
|
|
147
|
+
if (distSq(pa, pb) <= distanceSq) return true
|
|
148
|
+
}
|
|
829
149
|
}
|
|
830
|
-
|
|
831
|
-
|
|
150
|
+
|
|
151
|
+
// Point-to-segment
|
|
152
|
+
for (const pt of ptsA) {
|
|
153
|
+
for (const [b1, b2] of segsB) {
|
|
154
|
+
if (pointToSegmentDistSq(pt, b1, b2) <= distanceSq) return true
|
|
155
|
+
}
|
|
832
156
|
}
|
|
833
|
-
|
|
834
|
-
|
|
157
|
+
for (const pt of ptsB) {
|
|
158
|
+
for (const [a1, a2] of segsA) {
|
|
159
|
+
if (pointToSegmentDistSq(pt, a1, a2) <= distanceSq) return true
|
|
160
|
+
}
|
|
835
161
|
}
|
|
162
|
+
|
|
836
163
|
return false
|
|
837
164
|
}
|
|
838
165
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
* @returns {boolean}
|
|
843
|
-
*/
|
|
844
|
-
function pointInLineInterior(point, line) {
|
|
845
|
-
// Interior of line excludes endpoints
|
|
846
|
-
if (pointDist(point, line[0]) < EPSILON) return false
|
|
847
|
-
if (pointDist(point, line[line.length - 1]) < EPSILON) return false
|
|
848
|
-
return pointOnLine(point, line)
|
|
849
|
-
}
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Spatial predicate dispatch - decompose to primitive type pairs
|
|
168
|
+
// ============================================================================
|
|
850
169
|
|
|
851
170
|
/**
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
* @
|
|
171
|
+
* Decompose Multi* and GeometryCollection into simple geometries.
|
|
172
|
+
*
|
|
173
|
+
* @param {Geometry} geom
|
|
174
|
+
* @returns {SimpleGeometry[]}
|
|
855
175
|
*/
|
|
856
|
-
function
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (pointOnLine(mid2, line1) && pointInLineInterior(mid2, line1) && pointInLineInterior(mid2, line2)) {
|
|
869
|
-
return true
|
|
870
|
-
}
|
|
871
|
-
// Also check the actual intersection point
|
|
872
|
-
const ip = segmentIntersectionPoint(line1[i], line1[i + 1], line2[j], line2[j + 1])
|
|
873
|
-
if (ip && pointInLineInterior(ip, line1) && pointInLineInterior(ip, line2)) {
|
|
874
|
-
return true
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
}
|
|
176
|
+
function decompose(geom) {
|
|
177
|
+
switch (geom.type) {
|
|
178
|
+
case 'MultiPoint':
|
|
179
|
+
return geom.coordinates.map(c => ({ type: 'Point', coordinates: c }))
|
|
180
|
+
case 'MultiLineString':
|
|
181
|
+
return geom.coordinates.map(c => ({ type: 'LineString', coordinates: c }))
|
|
182
|
+
case 'MultiPolygon':
|
|
183
|
+
return geom.coordinates.map(c => ({ type: 'Polygon', coordinates: c }))
|
|
184
|
+
case 'GeometryCollection':
|
|
185
|
+
return geom.geometries.flatMap(decompose)
|
|
186
|
+
default:
|
|
187
|
+
return [geom]
|
|
878
188
|
}
|
|
879
|
-
return false
|
|
880
189
|
}
|
|
881
190
|
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// ST_Contains
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
882
195
|
/**
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
* @
|
|
886
|
-
* @param {number[]} p2
|
|
887
|
-
* @param {number[]} p3
|
|
888
|
-
* @param {number[]} p4
|
|
889
|
-
* @returns {number[] | null}
|
|
196
|
+
* @param {SimpleGeometry[]} a
|
|
197
|
+
* @param {SimpleGeometry[]} b
|
|
198
|
+
* @returns {boolean}
|
|
890
199
|
*/
|
|
891
|
-
function
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const denom = d1x * d2y - d1y * d2x
|
|
895
|
-
if (Math.abs(denom) < EPSILON) return null // parallel
|
|
896
|
-
const t = ((p3[0] - p1[0]) * d2y - (p3[1] - p1[1]) * d2x) / denom
|
|
897
|
-
return [p1[0] + t * d1x, p1[1] + t * d1y]
|
|
200
|
+
function stContains(a, b) {
|
|
201
|
+
// Every part of b must be inside some part of a
|
|
202
|
+
return b.every(pb => a.some(pa => simplePairContainment(pa, pb) !== 'OUTSIDE'))
|
|
898
203
|
}
|
|
899
204
|
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// ST_ContainsProperly
|
|
207
|
+
// ============================================================================
|
|
208
|
+
|
|
900
209
|
/**
|
|
901
|
-
* @param {
|
|
902
|
-
* @param {
|
|
210
|
+
* @param {SimpleGeometry[]} a
|
|
211
|
+
* @param {SimpleGeometry[]} b
|
|
903
212
|
* @returns {boolean}
|
|
904
213
|
*/
|
|
905
|
-
function
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
|
|
909
|
-
if (pointInPolygonInterior(mid, rings)) return true
|
|
910
|
-
}
|
|
911
|
-
// Also check interior points of the line
|
|
912
|
-
for (let i = 1; i < line.length - 1; i++) {
|
|
913
|
-
if (pointInPolygonInterior(line[i], rings)) return true
|
|
914
|
-
}
|
|
915
|
-
return false
|
|
214
|
+
function stContainsProperly(a, b) {
|
|
215
|
+
// Every part of b must be strictly inside some part of a
|
|
216
|
+
return b.every(pb => a.some(pa => simplePairContainment(pa, pb) === 'INSIDE'))
|
|
916
217
|
}
|
|
917
218
|
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// ST_Touches
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
918
223
|
/**
|
|
919
|
-
* @param {
|
|
920
|
-
* @param {
|
|
224
|
+
* @param {SimpleGeometry[]} a
|
|
225
|
+
* @param {SimpleGeometry[]} b
|
|
921
226
|
* @returns {boolean}
|
|
922
227
|
*/
|
|
923
|
-
function
|
|
924
|
-
|
|
925
|
-
for (const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
// Check if any edge midpoints are inside the other polygon's interior
|
|
933
|
-
for (let i = 0; i < rings1[0].length - 1; i++) {
|
|
934
|
-
const mid = [(rings1[0][i][0] + rings1[0][i + 1][0]) / 2, (rings1[0][i][1] + rings1[0][i + 1][1]) / 2]
|
|
935
|
-
if (pointInPolygonInterior(mid, rings2)) return true
|
|
936
|
-
}
|
|
937
|
-
for (let i = 0; i < rings2[0].length - 1; i++) {
|
|
938
|
-
const mid = [(rings2[0][i][0] + rings2[0][i + 1][0]) / 2, (rings2[0][i][1] + rings2[0][i + 1][1]) / 2]
|
|
939
|
-
if (pointInPolygonInterior(mid, rings1)) return true
|
|
228
|
+
function stTouches(a, b) {
|
|
229
|
+
let intersects = false
|
|
230
|
+
for (const pa of a) {
|
|
231
|
+
for (const pb of b) {
|
|
232
|
+
const rel = simplePairRelation(pa, pb)
|
|
233
|
+
if (rel === 'INSIDE') return false
|
|
234
|
+
if (rel === 'BOUNDARY') intersects = true
|
|
235
|
+
}
|
|
940
236
|
}
|
|
941
|
-
return
|
|
237
|
+
return intersects
|
|
942
238
|
}
|
|
943
239
|
|
|
944
240
|
// ============================================================================
|
|
@@ -946,8 +242,8 @@ function polygonInteriorsIntersect(rings1, rings2) {
|
|
|
946
242
|
// ============================================================================
|
|
947
243
|
|
|
948
244
|
/**
|
|
949
|
-
* @param {
|
|
950
|
-
* @param {
|
|
245
|
+
* @param {SimpleGeometry[]} a
|
|
246
|
+
* @param {SimpleGeometry[]} b
|
|
951
247
|
* @returns {boolean}
|
|
952
248
|
*/
|
|
953
249
|
function stOverlaps(a, b) {
|
|
@@ -956,7 +252,7 @@ function stOverlaps(a, b) {
|
|
|
956
252
|
const dimA = geometryDimension(a)
|
|
957
253
|
const dimB = geometryDimension(b)
|
|
958
254
|
if (dimA !== dimB) return false
|
|
959
|
-
if (!
|
|
255
|
+
if (!simpleIntersects(a, b)) return false
|
|
960
256
|
if (stEquals(a, b)) return false
|
|
961
257
|
// Must not be containment
|
|
962
258
|
if (stContains(a, b) || stContains(b, a)) return false
|
|
@@ -964,30 +260,23 @@ function stOverlaps(a, b) {
|
|
|
964
260
|
}
|
|
965
261
|
|
|
966
262
|
/**
|
|
967
|
-
* @param {
|
|
263
|
+
* @param {SimpleGeometry[]} parts
|
|
968
264
|
* @returns {number}
|
|
969
265
|
*/
|
|
970
|
-
function geometryDimension(
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
case 'GeometryCollection': {
|
|
982
|
-
let max = 0
|
|
983
|
-
for (const g of geom.geometries) {
|
|
984
|
-
max = Math.max(max, geometryDimension(g))
|
|
266
|
+
function geometryDimension(parts) {
|
|
267
|
+
let max = 0
|
|
268
|
+
for (const geom of parts) {
|
|
269
|
+
switch (geom.type) {
|
|
270
|
+
case 'Point':
|
|
271
|
+
break
|
|
272
|
+
case 'LineString':
|
|
273
|
+
if (max < 1) max = 1
|
|
274
|
+
break
|
|
275
|
+
case 'Polygon':
|
|
276
|
+
return 2
|
|
985
277
|
}
|
|
986
|
-
return max
|
|
987
|
-
}
|
|
988
|
-
default:
|
|
989
|
-
return 0
|
|
990
278
|
}
|
|
279
|
+
return max
|
|
991
280
|
}
|
|
992
281
|
|
|
993
282
|
// ============================================================================
|
|
@@ -995,23 +284,20 @@ function geometryDimension(geom) {
|
|
|
995
284
|
// ============================================================================
|
|
996
285
|
|
|
997
286
|
/**
|
|
998
|
-
* @param {
|
|
999
|
-
* @param {
|
|
287
|
+
* @param {SimpleGeometry[]} a
|
|
288
|
+
* @param {SimpleGeometry[]} b
|
|
1000
289
|
* @returns {boolean}
|
|
1001
290
|
*/
|
|
1002
291
|
function stEquals(a, b) {
|
|
1003
|
-
|
|
1004
|
-
const partsB = decompose(b)
|
|
1005
|
-
|
|
1006
|
-
if (partsA.length !== partsB.length) return false
|
|
292
|
+
if (a.length !== b.length) return false
|
|
1007
293
|
|
|
1008
|
-
// For each simple geometry in
|
|
294
|
+
// For each simple geometry in a, find a matching one in b
|
|
1009
295
|
const used = new Set()
|
|
1010
|
-
for (const pa of
|
|
296
|
+
for (const pa of a) {
|
|
1011
297
|
let found = false
|
|
1012
|
-
for (let i = 0; i <
|
|
298
|
+
for (let i = 0; i < b.length; i++) {
|
|
1013
299
|
if (used.has(i)) continue
|
|
1014
|
-
if (simpleGeomEqual(pa,
|
|
300
|
+
if (simpleGeomEqual(pa, b[i])) {
|
|
1015
301
|
used.add(i)
|
|
1016
302
|
found = true
|
|
1017
303
|
break
|
|
@@ -1022,70 +308,13 @@ function stEquals(a, b) {
|
|
|
1022
308
|
return true
|
|
1023
309
|
}
|
|
1024
310
|
|
|
1025
|
-
/**
|
|
1026
|
-
* @param {GeoJsonGeometry} a
|
|
1027
|
-
* @param {GeoJsonGeometry} b
|
|
1028
|
-
* @returns {boolean}
|
|
1029
|
-
*/
|
|
1030
|
-
function simpleGeomEqual(a, b) {
|
|
1031
|
-
if (a.type !== b.type) return false
|
|
1032
|
-
switch (a.type) {
|
|
1033
|
-
case 'Point':
|
|
1034
|
-
return pointDist(a.coordinates, b.coordinates) < EPSILON
|
|
1035
|
-
case 'LineString':
|
|
1036
|
-
return lineStringEqual(a.coordinates, b.coordinates)
|
|
1037
|
-
case 'Polygon':
|
|
1038
|
-
return polygonEqual(a.coordinates, b.coordinates)
|
|
1039
|
-
default:
|
|
1040
|
-
return false
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* @param {number[][]} a
|
|
1046
|
-
* @param {number[][]} b
|
|
1047
|
-
* @returns {boolean}
|
|
1048
|
-
*/
|
|
1049
|
-
function lineStringEqual(a, b) {
|
|
1050
|
-
if (a.length !== b.length) return false
|
|
1051
|
-
// Forward
|
|
1052
|
-
let forward = true
|
|
1053
|
-
for (let i = 0; i < a.length; i++) {
|
|
1054
|
-
if (Math.abs(a[i][0] - b[i][0]) > EPSILON || Math.abs(a[i][1] - b[i][1]) > EPSILON) {
|
|
1055
|
-
forward = false
|
|
1056
|
-
break
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
if (forward) return true
|
|
1060
|
-
// Reverse
|
|
1061
|
-
for (let i = 0; i < a.length; i++) {
|
|
1062
|
-
if (Math.abs(a[i][0] - b[a.length - 1 - i][0]) > EPSILON || Math.abs(a[i][1] - b[a.length - 1 - i][1]) > EPSILON) {
|
|
1063
|
-
return false
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
return true
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* @param {number[][][]} a
|
|
1071
|
-
* @param {number[][][]} b
|
|
1072
|
-
* @returns {boolean}
|
|
1073
|
-
*/
|
|
1074
|
-
function polygonEqual(a, b) {
|
|
1075
|
-
if (a.length !== b.length) return false
|
|
1076
|
-
for (let i = 0; i < a.length; i++) {
|
|
1077
|
-
if (!ringsEqual(a[i], b[i])) return false
|
|
1078
|
-
}
|
|
1079
|
-
return true
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
311
|
// ============================================================================
|
|
1083
312
|
// ST_Crosses
|
|
1084
313
|
// ============================================================================
|
|
1085
314
|
|
|
1086
315
|
/**
|
|
1087
|
-
* @param {
|
|
1088
|
-
* @param {
|
|
316
|
+
* @param {SimpleGeometry[]} a
|
|
317
|
+
* @param {SimpleGeometry[]} b
|
|
1089
318
|
* @returns {boolean}
|
|
1090
319
|
*/
|
|
1091
320
|
function stCrosses(a, b) {
|
|
@@ -1094,7 +323,7 @@ function stCrosses(a, b) {
|
|
|
1094
323
|
const dimA = geometryDimension(a)
|
|
1095
324
|
const dimB = geometryDimension(b)
|
|
1096
325
|
|
|
1097
|
-
if (!
|
|
326
|
+
if (!simpleIntersects(a, b)) return false
|
|
1098
327
|
|
|
1099
328
|
// Point/Point or Polygon/Polygon cannot cross
|
|
1100
329
|
if (dimA === dimB && dimA !== 1) return false
|
|
@@ -1103,23 +332,20 @@ function stCrosses(a, b) {
|
|
|
1103
332
|
if (dimA === 1 && dimB === 1) {
|
|
1104
333
|
// They cross if they intersect but neither contains the other
|
|
1105
334
|
// and the intersection is a set of points (not line segments)
|
|
1106
|
-
return
|
|
335
|
+
return !stContains(a, b) && !stContains(b, a) && !stTouches(a, b)
|
|
1107
336
|
}
|
|
1108
337
|
|
|
1109
338
|
// Point/Line, Point/Polygon: point "in interior"
|
|
1110
339
|
if (dimA === 0 && dimB >= 1) {
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (
|
|
340
|
+
for (const pa of a) {
|
|
341
|
+
// eslint-disable-next-line no-extra-parens
|
|
342
|
+
const point = /** @type {Point} */ (pa)
|
|
343
|
+
for (const pb of b) {
|
|
344
|
+
if (pb.type === 'LineString') {
|
|
345
|
+
if (pointLineRelation(point.coordinates, pb.coordinates) === 'INSIDE') return true
|
|
1117
346
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const partsB = decompose(b)
|
|
1121
|
-
for (const pb of partsB) {
|
|
1122
|
-
if (pointInPolygonInterior(pa.coordinates, pb.coordinates)) return true
|
|
347
|
+
if (pb.type === 'Polygon') {
|
|
348
|
+
if (pointInPolygon(point.coordinates, pb.coordinates) === 'INSIDE') return true
|
|
1123
349
|
}
|
|
1124
350
|
}
|
|
1125
351
|
}
|
|
@@ -1128,7 +354,7 @@ function stCrosses(a, b) {
|
|
|
1128
354
|
|
|
1129
355
|
// Line/Polygon: line crosses polygon if part of line is inside and part is outside
|
|
1130
356
|
if (dimA === 1 && dimB === 2) {
|
|
1131
|
-
return
|
|
357
|
+
return !stContains(b, a)
|
|
1132
358
|
}
|
|
1133
359
|
|
|
1134
360
|
// Symmetric cases
|
|
@@ -1137,335 +363,3 @@ function stCrosses(a, b) {
|
|
|
1137
363
|
|
|
1138
364
|
return false
|
|
1139
365
|
}
|
|
1140
|
-
|
|
1141
|
-
// ============================================================================
|
|
1142
|
-
// ST_Covers / ST_CoveredBy
|
|
1143
|
-
// ============================================================================
|
|
1144
|
-
|
|
1145
|
-
/**
|
|
1146
|
-
* ST_Covers: a covers b if no point of b is outside a.
|
|
1147
|
-
* Similar to ST_Contains but allows b to be on boundary.
|
|
1148
|
-
*
|
|
1149
|
-
* @param {GeoJsonGeometry} a
|
|
1150
|
-
* @param {GeoJsonGeometry} b
|
|
1151
|
-
* @returns {boolean}
|
|
1152
|
-
*/
|
|
1153
|
-
function stCovers(a, b) {
|
|
1154
|
-
// ST_Covers is the same as ST_Contains for most purposes
|
|
1155
|
-
// The difference is subtle in edge cases with boundaries
|
|
1156
|
-
return stContains(a, b)
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
/**
|
|
1160
|
-
* @param {GeoJsonGeometry} a
|
|
1161
|
-
* @param {GeoJsonGeometry} b
|
|
1162
|
-
* @returns {boolean}
|
|
1163
|
-
*/
|
|
1164
|
-
function stCoveredBy(a, b) {
|
|
1165
|
-
return stCovers(b, a)
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// ============================================================================
|
|
1169
|
-
// ST_DWithin
|
|
1170
|
-
// ============================================================================
|
|
1171
|
-
|
|
1172
|
-
/**
|
|
1173
|
-
* @param {GeoJsonGeometry} a
|
|
1174
|
-
* @param {GeoJsonGeometry} b
|
|
1175
|
-
* @param {number} distance
|
|
1176
|
-
* @returns {boolean}
|
|
1177
|
-
*/
|
|
1178
|
-
function stDWithin(a, b, distance) {
|
|
1179
|
-
return geometryDistance(a, b) <= distance
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// ============================================================================
|
|
1183
|
-
// WKT parsing (ST_GeomFromText)
|
|
1184
|
-
// ============================================================================
|
|
1185
|
-
|
|
1186
|
-
/**
|
|
1187
|
-
* Parse a WKT string into a GeoJSON geometry.
|
|
1188
|
-
*
|
|
1189
|
-
* @param {string} wkt
|
|
1190
|
-
* @returns {GeoJsonGeometry | null}
|
|
1191
|
-
*/
|
|
1192
|
-
function parseWkt(wkt) {
|
|
1193
|
-
const s = wkt.trim()
|
|
1194
|
-
const upper = s.toUpperCase()
|
|
1195
|
-
|
|
1196
|
-
if (upper.startsWith('POINT')) {
|
|
1197
|
-
const coords = parseWktCoordinate(s.slice(5).trim())
|
|
1198
|
-
if (!coords) return null
|
|
1199
|
-
return { type: 'Point', coordinates: coords }
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
if (upper.startsWith('MULTIPOINT')) {
|
|
1203
|
-
const inner = extractParens(s.slice(10).trim())
|
|
1204
|
-
if (inner == null) return null
|
|
1205
|
-
const coords = parseWktCoordinateList(inner)
|
|
1206
|
-
if (!coords) return null
|
|
1207
|
-
return { type: 'MultiPoint', coordinates: coords }
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
if (upper.startsWith('MULTILINESTRING')) {
|
|
1211
|
-
const inner = extractParens(s.slice(15).trim())
|
|
1212
|
-
if (inner == null) return null
|
|
1213
|
-
const rings = parseWktRingList(inner)
|
|
1214
|
-
if (!rings) return null
|
|
1215
|
-
return { type: 'MultiLineString', coordinates: rings }
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
if (upper.startsWith('MULTIPOLYGON')) {
|
|
1219
|
-
const inner = extractParens(s.slice(12).trim())
|
|
1220
|
-
if (inner == null) return null
|
|
1221
|
-
const polys = parseWktPolygonList(inner)
|
|
1222
|
-
if (!polys) return null
|
|
1223
|
-
return { type: 'MultiPolygon', coordinates: polys }
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
if (upper.startsWith('LINESTRING')) {
|
|
1227
|
-
const inner = extractParens(s.slice(10).trim())
|
|
1228
|
-
if (inner == null) return null
|
|
1229
|
-
const coords = parseWktCoordinateList(inner)
|
|
1230
|
-
if (!coords) return null
|
|
1231
|
-
return { type: 'LineString', coordinates: coords }
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
if (upper.startsWith('POLYGON')) {
|
|
1235
|
-
const inner = extractParens(s.slice(7).trim())
|
|
1236
|
-
if (inner == null) return null
|
|
1237
|
-
const rings = parseWktRingList(inner)
|
|
1238
|
-
if (!rings) return null
|
|
1239
|
-
return { type: 'Polygon', coordinates: rings }
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
return null
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
/**
|
|
1246
|
-
* Extract content inside outer parentheses.
|
|
1247
|
-
*
|
|
1248
|
-
* @param {string} s
|
|
1249
|
-
* @returns {string | null}
|
|
1250
|
-
*/
|
|
1251
|
-
function extractParens(s) {
|
|
1252
|
-
const trimmed = s.trim()
|
|
1253
|
-
if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) return null
|
|
1254
|
-
return trimmed.slice(1, -1).trim()
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
/**
|
|
1258
|
-
* Parse a single coordinate like "(1 2)" or "1 2".
|
|
1259
|
-
*
|
|
1260
|
-
* @param {string} s
|
|
1261
|
-
* @returns {number[] | null}
|
|
1262
|
-
*/
|
|
1263
|
-
function parseWktCoordinate(s) {
|
|
1264
|
-
const inner = s.trim().replace(/^\(/, '').replace(/\)$/, '').trim()
|
|
1265
|
-
const parts = inner.split(/\s+/)
|
|
1266
|
-
if (parts.length < 2) return null
|
|
1267
|
-
const x = Number(parts[0])
|
|
1268
|
-
const y = Number(parts[1])
|
|
1269
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
1270
|
-
return [x, y]
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
/**
|
|
1274
|
-
* Parse a comma-separated list of coordinates like "1 2, 3 4, 5 6".
|
|
1275
|
-
*
|
|
1276
|
-
* @param {string} s
|
|
1277
|
-
* @returns {number[][] | null}
|
|
1278
|
-
*/
|
|
1279
|
-
function parseWktCoordinateList(s) {
|
|
1280
|
-
const parts = s.split(',')
|
|
1281
|
-
/** @type {number[][]} */
|
|
1282
|
-
const coords = []
|
|
1283
|
-
for (const part of parts) {
|
|
1284
|
-
const trimmed = part.trim().replace(/^\(/, '').replace(/\)$/, '').trim()
|
|
1285
|
-
const nums = trimmed.split(/\s+/)
|
|
1286
|
-
if (nums.length < 2) return null
|
|
1287
|
-
const x = Number(nums[0])
|
|
1288
|
-
const y = Number(nums[1])
|
|
1289
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
1290
|
-
coords.push([x, y])
|
|
1291
|
-
}
|
|
1292
|
-
return coords.length > 0 ? coords : null
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
/**
|
|
1296
|
-
* Parse a list of rings like "(1 2, 3 4), (5 6, 7 8)".
|
|
1297
|
-
*
|
|
1298
|
-
* @param {string} s
|
|
1299
|
-
* @returns {number[][][] | null}
|
|
1300
|
-
*/
|
|
1301
|
-
function parseWktRingList(s) {
|
|
1302
|
-
/** @type {number[][][]} */
|
|
1303
|
-
const rings = []
|
|
1304
|
-
const ringStrs = splitTopLevel(s)
|
|
1305
|
-
for (const ringStr of ringStrs) {
|
|
1306
|
-
const inner = extractParens(ringStr.trim())
|
|
1307
|
-
if (inner == null) return null
|
|
1308
|
-
const coords = parseWktCoordinateList(inner)
|
|
1309
|
-
if (!coords) return null
|
|
1310
|
-
rings.push(coords)
|
|
1311
|
-
}
|
|
1312
|
-
return rings.length > 0 ? rings : null
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
/**
|
|
1316
|
-
* Parse a list of polygons like "((ring1), (ring2)), ((ring3))".
|
|
1317
|
-
*
|
|
1318
|
-
* @param {string} s
|
|
1319
|
-
* @returns {number[][][][] | null}
|
|
1320
|
-
*/
|
|
1321
|
-
function parseWktPolygonList(s) {
|
|
1322
|
-
/** @type {number[][][][]} */
|
|
1323
|
-
const polys = []
|
|
1324
|
-
const polyStrs = splitTopLevel(s)
|
|
1325
|
-
for (const polyStr of polyStrs) {
|
|
1326
|
-
const inner = extractParens(polyStr.trim())
|
|
1327
|
-
if (inner == null) return null
|
|
1328
|
-
const rings = parseWktRingList(inner)
|
|
1329
|
-
if (!rings) return null
|
|
1330
|
-
polys.push(rings)
|
|
1331
|
-
}
|
|
1332
|
-
return polys.length > 0 ? polys : null
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
/**
|
|
1336
|
-
* Split a string by commas at the top-level (not inside parentheses).
|
|
1337
|
-
*
|
|
1338
|
-
* @param {string} s
|
|
1339
|
-
* @returns {string[]}
|
|
1340
|
-
*/
|
|
1341
|
-
function splitTopLevel(s) {
|
|
1342
|
-
/** @type {string[]} */
|
|
1343
|
-
const parts = []
|
|
1344
|
-
let depth = 0
|
|
1345
|
-
let start = 0
|
|
1346
|
-
for (let i = 0; i < s.length; i++) {
|
|
1347
|
-
if (s[i] === '(') depth++
|
|
1348
|
-
else if (s[i] === ')') depth--
|
|
1349
|
-
else if (s[i] === ',' && depth === 0) {
|
|
1350
|
-
parts.push(s.slice(start, i))
|
|
1351
|
-
start = i + 1
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
parts.push(s.slice(start))
|
|
1355
|
-
return parts
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// ============================================================================
|
|
1359
|
-
// WKT serialization (ST_AsText)
|
|
1360
|
-
// ============================================================================
|
|
1361
|
-
|
|
1362
|
-
/**
|
|
1363
|
-
* Convert a GeoJSON geometry to WKT.
|
|
1364
|
-
*
|
|
1365
|
-
* @param {GeoJsonGeometry} geom
|
|
1366
|
-
* @returns {string}
|
|
1367
|
-
*/
|
|
1368
|
-
function geomToWkt(geom) {
|
|
1369
|
-
switch (geom.type) {
|
|
1370
|
-
case 'Point':
|
|
1371
|
-
return `POINT (${coordToWkt(geom.coordinates)})`
|
|
1372
|
-
case 'MultiPoint':
|
|
1373
|
-
return `MULTIPOINT (${geom.coordinates.map((/** @type {number[]} */ c) => `(${coordToWkt(c)})`).join(', ')})`
|
|
1374
|
-
case 'LineString':
|
|
1375
|
-
return `LINESTRING (${coordListToWkt(geom.coordinates)})`
|
|
1376
|
-
case 'MultiLineString':
|
|
1377
|
-
return `MULTILINESTRING (${geom.coordinates.map((/** @type {number[][]} */ l) => `(${coordListToWkt(l)})`).join(', ')})`
|
|
1378
|
-
case 'Polygon':
|
|
1379
|
-
return `POLYGON (${geom.coordinates.map((/** @type {number[][]} */ r) => `(${coordListToWkt(r)})`).join(', ')})`
|
|
1380
|
-
case 'MultiPolygon':
|
|
1381
|
-
return `MULTIPOLYGON (${geom.coordinates.map((/** @type {number[][][]} */ p) => `(${p.map((/** @type {number[][]} */ r) => `(${coordListToWkt(r)})`).join(', ')})`).join(', ')})`
|
|
1382
|
-
case 'GeometryCollection':
|
|
1383
|
-
return `GEOMETRYCOLLECTION (${(geom.geometries || []).map((/** @type {GeoJsonGeometry} */ g) => geomToWkt(g)).join(', ')})`
|
|
1384
|
-
default:
|
|
1385
|
-
return ''
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
/**
|
|
1390
|
-
* Format a single coordinate to WKT.
|
|
1391
|
-
*
|
|
1392
|
-
* @param {number[]} coord
|
|
1393
|
-
* @returns {string}
|
|
1394
|
-
*/
|
|
1395
|
-
function coordToWkt(coord) {
|
|
1396
|
-
return `${coord[0]} ${coord[1]}`
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
/**
|
|
1400
|
-
* Format a coordinate list to WKT.
|
|
1401
|
-
*
|
|
1402
|
-
* @param {number[][]} coords
|
|
1403
|
-
* @returns {string}
|
|
1404
|
-
*/
|
|
1405
|
-
function coordListToWkt(coords) {
|
|
1406
|
-
return coords.map(c => `${c[0]} ${c[1]}`).join(', ')
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// ============================================================================
|
|
1410
|
-
// Public API
|
|
1411
|
-
// ============================================================================
|
|
1412
|
-
|
|
1413
|
-
/**
|
|
1414
|
-
* Evaluate a spatial predicate function.
|
|
1415
|
-
*
|
|
1416
|
-
* @param {Object} options
|
|
1417
|
-
* @param {SpatialFunc} options.funcName
|
|
1418
|
-
* @param {SqlPrimitive[]} options.args
|
|
1419
|
-
* @returns {SqlPrimitive}
|
|
1420
|
-
*/
|
|
1421
|
-
export function evaluateSpatialFunc({ funcName, args }) {
|
|
1422
|
-
// Constructor / accessor functions (don't require two geometries)
|
|
1423
|
-
if (funcName === 'ST_GEOMFROMTEXT') {
|
|
1424
|
-
if (args[0] == null) return null
|
|
1425
|
-
return parseWkt(String(args[0]))
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
if (funcName === 'ST_MAKEENVELOPE') {
|
|
1429
|
-
if (args[0] == null || args[1] == null || args[2] == null || args[3] == null) return null
|
|
1430
|
-
const xmin = Number(args[0])
|
|
1431
|
-
const ymin = Number(args[1])
|
|
1432
|
-
const xmax = Number(args[2])
|
|
1433
|
-
const ymax = Number(args[3])
|
|
1434
|
-
return {
|
|
1435
|
-
type: 'Polygon',
|
|
1436
|
-
coordinates: [[[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax], [xmin, ymin]]],
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
if (funcName === 'ST_ASTEXT') {
|
|
1441
|
-
const geom = toGeometry(args[0])
|
|
1442
|
-
if (geom == null) return null
|
|
1443
|
-
return geomToWkt(geom)
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// Predicate functions (require two geometries)
|
|
1447
|
-
const a = toGeometry(args[0])
|
|
1448
|
-
const b = toGeometry(args[1])
|
|
1449
|
-
|
|
1450
|
-
if (a == null || b == null) return null
|
|
1451
|
-
|
|
1452
|
-
switch (funcName) {
|
|
1453
|
-
case 'ST_INTERSECTS': return stIntersects(a, b)
|
|
1454
|
-
case 'ST_CONTAINS': return stContains(a, b)
|
|
1455
|
-
case 'ST_CONTAINSPROPERLY': return stContainsProperly(a, b)
|
|
1456
|
-
case 'ST_WITHIN': return stWithin(a, b)
|
|
1457
|
-
case 'ST_OVERLAPS': return stOverlaps(a, b)
|
|
1458
|
-
case 'ST_TOUCHES': return stTouches(a, b)
|
|
1459
|
-
case 'ST_EQUALS': return stEquals(a, b)
|
|
1460
|
-
case 'ST_CROSSES': return stCrosses(a, b)
|
|
1461
|
-
case 'ST_COVERS': return stCovers(a, b)
|
|
1462
|
-
case 'ST_COVEREDBY': return stCoveredBy(a, b)
|
|
1463
|
-
case 'ST_DWITHIN': {
|
|
1464
|
-
if (args[2] == null) return null
|
|
1465
|
-
const dist = Number(args[2])
|
|
1466
|
-
return stDWithin(a, b, dist)
|
|
1467
|
-
}
|
|
1468
|
-
default:
|
|
1469
|
-
return null
|
|
1470
|
-
}
|
|
1471
|
-
}
|