squirreling 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +2 -2
- package/src/backend/dataSource.js +2 -2
- package/src/expression/evaluate.js +32 -1
- package/src/expression/spatial.js +1471 -0
- package/src/types.d.ts +16 -0
- package/src/validation.js +39 -2
- package/src/validationErrors.js +16 -0
|
@@ -0,0 +1,1471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { SpatialFunc, SqlPrimitive } from '../types.js'
|
|
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
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Test if a line segment intersects a ring boundary.
|
|
211
|
+
*
|
|
212
|
+
* @param {number[]} a
|
|
213
|
+
* @param {number[]} b
|
|
214
|
+
* @param {number[][]} ring
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
*/
|
|
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
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Test if a linestring intersects a polygon.
|
|
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).
|
|
247
|
+
*
|
|
248
|
+
* @param {number[][]} line1
|
|
249
|
+
* @param {number[][]} line2
|
|
250
|
+
* @returns {boolean}
|
|
251
|
+
*/
|
|
252
|
+
function linesIntersect(line1, line2) {
|
|
253
|
+
for (let i = 0; i < line1.length - 1; i++) {
|
|
254
|
+
for (let j = 0; j < line2.length - 1; j++) {
|
|
255
|
+
if (segmentsIntersect(line1[i], line1[i + 1], line2[j], line2[j + 1])) return true
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Test if two polygons share any space.
|
|
263
|
+
*
|
|
264
|
+
* @param {number[][][]} rings1
|
|
265
|
+
* @param {number[][][]} rings2
|
|
266
|
+
* @returns {boolean}
|
|
267
|
+
*/
|
|
268
|
+
function polygonsIntersect(rings1, rings2) {
|
|
269
|
+
// Check if any vertex of polygon1 is inside polygon2
|
|
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
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Test if all points of a linestring are inside a polygon.
|
|
288
|
+
*
|
|
289
|
+
* @param {number[][]} line
|
|
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
|
|
301
|
+
}
|
|
302
|
+
return true
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Test if all points of a linestring are strictly in the polygon interior.
|
|
307
|
+
*
|
|
308
|
+
* @param {number[][]} line
|
|
309
|
+
* @param {number[][][]} rings
|
|
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
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Test if polygon A contains polygon B properly (no boundary contact).
|
|
345
|
+
*
|
|
346
|
+
* @param {number[][][]} ringsA
|
|
347
|
+
* @param {number[][][]} ringsB
|
|
348
|
+
* @returns {boolean}
|
|
349
|
+
*/
|
|
350
|
+
function polygonContainsPolygonProperly(ringsA, ringsB) {
|
|
351
|
+
for (const pt of ringsB[0]) {
|
|
352
|
+
if (!pointInPolygonInterior(pt, ringsA)) return false
|
|
353
|
+
}
|
|
354
|
+
for (let i = 0; i < ringsB[0].length - 1; i++) {
|
|
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
|
|
357
|
+
}
|
|
358
|
+
return true
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Test if two rings are equal (same vertices, possibly different starting point).
|
|
363
|
+
*
|
|
364
|
+
* @param {number[][]} ring1
|
|
365
|
+
* @param {number[][]} ring2
|
|
366
|
+
* @returns {boolean}
|
|
367
|
+
*/
|
|
368
|
+
function ringsEqual(ring1, ring2) {
|
|
369
|
+
if (ring1.length !== ring2.length) return false
|
|
370
|
+
// Try every rotation
|
|
371
|
+
const n = ring1.length - 1 // closed ring, last = first
|
|
372
|
+
for (let offset = 0; offset < n; offset++) {
|
|
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
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (match) return true
|
|
383
|
+
}
|
|
384
|
+
// Try reverse direction
|
|
385
|
+
for (let offset = 0; offset < n; offset++) {
|
|
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
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (match) return true
|
|
396
|
+
}
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// Minimum distance between geometries
|
|
402
|
+
// ============================================================================
|
|
403
|
+
|
|
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
|
+
/**
|
|
424
|
+
* Get all line segments from a geometry.
|
|
425
|
+
*
|
|
426
|
+
* @param {GeoJsonGeometry} geom
|
|
427
|
+
* @returns {Array<[number[], number[]]>}
|
|
428
|
+
*/
|
|
429
|
+
function getSegments(geom) {
|
|
430
|
+
/** @type {Array<[number[], number[]]>} */
|
|
431
|
+
const segments = []
|
|
432
|
+
/**
|
|
433
|
+
* @param {number[][]} coords
|
|
434
|
+
*/
|
|
435
|
+
function addLine(coords) {
|
|
436
|
+
for (let i = 0; i < coords.length - 1; i++) {
|
|
437
|
+
segments.push([coords[i], coords[i + 1]])
|
|
438
|
+
}
|
|
439
|
+
}
|
|
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
|
+
|
|
508
|
+
// Segment-to-segment
|
|
509
|
+
for (const [a1, a2] of segsA) {
|
|
510
|
+
for (const [b1, b2] of segsB) {
|
|
511
|
+
min = Math.min(min, segmentToSegmentDist(a1, a2, b1, b2))
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Point-to-segment
|
|
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
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ============================================================================
|
|
544
|
+
// Spatial predicate dispatch - decompose to primitive type pairs
|
|
545
|
+
// ============================================================================
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Decompose Multi* and GeometryCollection into simple geometries.
|
|
549
|
+
*
|
|
550
|
+
* @param {GeoJsonGeometry} geom
|
|
551
|
+
* @returns {GeoJsonGeometry[]}
|
|
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
|
|
799
|
+
* @returns {boolean}
|
|
800
|
+
*/
|
|
801
|
+
function simplePairInteriorsIntersect(a, b) {
|
|
802
|
+
const ta = a.type
|
|
803
|
+
const tb = b.type
|
|
804
|
+
|
|
805
|
+
if (ta === 'Point' && tb === 'Point') {
|
|
806
|
+
// Points have no interior dimension per se, but two equal points "touch" is not
|
|
807
|
+
// standard. By definition, ST_Touches returns false for Point/Point.
|
|
808
|
+
return true // If they intersect at all, their "interiors" intersect
|
|
809
|
+
}
|
|
810
|
+
if (ta === 'Point' && tb === 'LineString') {
|
|
811
|
+
// Interior of a linestring excludes endpoints
|
|
812
|
+
return pointInLineInterior(a.coordinates, b.coordinates)
|
|
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)
|
|
829
|
+
}
|
|
830
|
+
if (ta === 'Polygon' && tb === 'LineString') {
|
|
831
|
+
return lineInteriorIntersectsPolygonInterior(b.coordinates, a.coordinates)
|
|
832
|
+
}
|
|
833
|
+
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
834
|
+
return polygonInteriorsIntersect(a.coordinates, b.coordinates)
|
|
835
|
+
}
|
|
836
|
+
return false
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* @param {number[]} point
|
|
841
|
+
* @param {number[][]} line
|
|
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
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* @param {number[][]} line1
|
|
853
|
+
* @param {number[][]} line2
|
|
854
|
+
* @returns {boolean}
|
|
855
|
+
*/
|
|
856
|
+
function linesShareInterior(line1, line2) {
|
|
857
|
+
// Check if any interior point of one line lies on the other line's interior
|
|
858
|
+
for (let i = 0; i < line1.length - 1; i++) {
|
|
859
|
+
for (let j = 0; j < line2.length - 1; j++) {
|
|
860
|
+
if (segmentsIntersect(line1[i], line1[i + 1], line2[j], line2[j + 1])) {
|
|
861
|
+
// Find the intersection point and check if it's interior to both
|
|
862
|
+
// For simplicity, check segment midpoints
|
|
863
|
+
const mid1 = [(line1[i][0] + line1[i + 1][0]) / 2, (line1[i][1] + line1[i + 1][1]) / 2]
|
|
864
|
+
if (pointOnLine(mid1, line2) && pointInLineInterior(mid1, line1) && pointInLineInterior(mid1, line2)) {
|
|
865
|
+
return true
|
|
866
|
+
}
|
|
867
|
+
const mid2 = [(line2[j][0] + line2[j + 1][0]) / 2, (line2[j][1] + line2[j + 1][1]) / 2]
|
|
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
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return false
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Compute intersection point of two segments (if they intersect at a single point).
|
|
884
|
+
*
|
|
885
|
+
* @param {number[]} p1
|
|
886
|
+
* @param {number[]} p2
|
|
887
|
+
* @param {number[]} p3
|
|
888
|
+
* @param {number[]} p4
|
|
889
|
+
* @returns {number[] | null}
|
|
890
|
+
*/
|
|
891
|
+
function segmentIntersectionPoint(p1, p2, p3, p4) {
|
|
892
|
+
const d1x = p2[0] - p1[0], d1y = p2[1] - p1[1]
|
|
893
|
+
const d2x = p4[0] - p3[0], d2y = p4[1] - p3[1]
|
|
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]
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* @param {number[][]} line
|
|
902
|
+
* @param {number[][][]} rings
|
|
903
|
+
* @returns {boolean}
|
|
904
|
+
*/
|
|
905
|
+
function lineInteriorIntersectsPolygonInterior(line, rings) {
|
|
906
|
+
// Check if any interior point of the line is inside the polygon interior
|
|
907
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
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
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* @param {number[][][]} rings1
|
|
920
|
+
* @param {number[][][]} rings2
|
|
921
|
+
* @returns {boolean}
|
|
922
|
+
*/
|
|
923
|
+
function polygonInteriorsIntersect(rings1, rings2) {
|
|
924
|
+
// Check if any vertex of polygon1 is inside polygon2's interior
|
|
925
|
+
for (const pt of rings1[0]) {
|
|
926
|
+
if (pointInPolygonInterior(pt, rings2)) return true
|
|
927
|
+
}
|
|
928
|
+
// Check if any vertex of polygon2 is inside polygon1's interior
|
|
929
|
+
for (const pt of rings2[0]) {
|
|
930
|
+
if (pointInPolygonInterior(pt, rings1)) return true
|
|
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
|
|
940
|
+
}
|
|
941
|
+
return false
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ============================================================================
|
|
945
|
+
// ST_Overlaps
|
|
946
|
+
// ============================================================================
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* @param {GeoJsonGeometry} a
|
|
950
|
+
* @param {GeoJsonGeometry} b
|
|
951
|
+
* @returns {boolean}
|
|
952
|
+
*/
|
|
953
|
+
function stOverlaps(a, b) {
|
|
954
|
+
// Overlaps requires same dimension, and that each geometry has some part
|
|
955
|
+
// inside the other and some part outside
|
|
956
|
+
const dimA = geometryDimension(a)
|
|
957
|
+
const dimB = geometryDimension(b)
|
|
958
|
+
if (dimA !== dimB) return false
|
|
959
|
+
if (!stIntersects(a, b)) return false
|
|
960
|
+
if (stEquals(a, b)) return false
|
|
961
|
+
// Must not be containment
|
|
962
|
+
if (stContains(a, b) || stContains(b, a)) return false
|
|
963
|
+
return true
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* @param {GeoJsonGeometry} geom
|
|
968
|
+
* @returns {number}
|
|
969
|
+
*/
|
|
970
|
+
function geometryDimension(geom) {
|
|
971
|
+
switch (geom.type) {
|
|
972
|
+
case 'Point':
|
|
973
|
+
case 'MultiPoint':
|
|
974
|
+
return 0
|
|
975
|
+
case 'LineString':
|
|
976
|
+
case 'MultiLineString':
|
|
977
|
+
return 1
|
|
978
|
+
case 'Polygon':
|
|
979
|
+
case 'MultiPolygon':
|
|
980
|
+
return 2
|
|
981
|
+
case 'GeometryCollection': {
|
|
982
|
+
let max = 0
|
|
983
|
+
for (const g of geom.geometries) {
|
|
984
|
+
max = Math.max(max, geometryDimension(g))
|
|
985
|
+
}
|
|
986
|
+
return max
|
|
987
|
+
}
|
|
988
|
+
default:
|
|
989
|
+
return 0
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ============================================================================
|
|
994
|
+
// ST_Equals
|
|
995
|
+
// ============================================================================
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* @param {GeoJsonGeometry} a
|
|
999
|
+
* @param {GeoJsonGeometry} b
|
|
1000
|
+
* @returns {boolean}
|
|
1001
|
+
*/
|
|
1002
|
+
function stEquals(a, b) {
|
|
1003
|
+
const partsA = decompose(a)
|
|
1004
|
+
const partsB = decompose(b)
|
|
1005
|
+
|
|
1006
|
+
if (partsA.length !== partsB.length) return false
|
|
1007
|
+
|
|
1008
|
+
// For each simple geometry in A, find a matching one in B
|
|
1009
|
+
const used = new Set()
|
|
1010
|
+
for (const pa of partsA) {
|
|
1011
|
+
let found = false
|
|
1012
|
+
for (let i = 0; i < partsB.length; i++) {
|
|
1013
|
+
if (used.has(i)) continue
|
|
1014
|
+
if (simpleGeomEqual(pa, partsB[i])) {
|
|
1015
|
+
used.add(i)
|
|
1016
|
+
found = true
|
|
1017
|
+
break
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (!found) return false
|
|
1021
|
+
}
|
|
1022
|
+
return true
|
|
1023
|
+
}
|
|
1024
|
+
|
|
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
|
+
// ============================================================================
|
|
1083
|
+
// ST_Crosses
|
|
1084
|
+
// ============================================================================
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* @param {GeoJsonGeometry} a
|
|
1088
|
+
* @param {GeoJsonGeometry} b
|
|
1089
|
+
* @returns {boolean}
|
|
1090
|
+
*/
|
|
1091
|
+
function stCrosses(a, b) {
|
|
1092
|
+
// Crosses: interiors intersect, and the intersection has lower dimension
|
|
1093
|
+
// than the maximum of the two geometries' dimensions
|
|
1094
|
+
const dimA = geometryDimension(a)
|
|
1095
|
+
const dimB = geometryDimension(b)
|
|
1096
|
+
|
|
1097
|
+
if (!stIntersects(a, b)) return false
|
|
1098
|
+
|
|
1099
|
+
// Point/Point or Polygon/Polygon cannot cross
|
|
1100
|
+
if (dimA === dimB && dimA !== 1) return false
|
|
1101
|
+
|
|
1102
|
+
// Line/Line: they cross if they intersect at a point (not overlap)
|
|
1103
|
+
if (dimA === 1 && dimB === 1) {
|
|
1104
|
+
// They cross if they intersect but neither contains the other
|
|
1105
|
+
// and the intersection is a set of points (not line segments)
|
|
1106
|
+
return stIntersects(a, b) && !stContains(a, b) && !stContains(b, a) && !stTouches(a, b)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Point/Line, Point/Polygon: point "in interior"
|
|
1110
|
+
if (dimA === 0 && dimB >= 1) {
|
|
1111
|
+
const partsA = decompose(a)
|
|
1112
|
+
for (const pa of partsA) {
|
|
1113
|
+
if (b.type === 'LineString' || b.type === 'MultiLineString') {
|
|
1114
|
+
const partsB = decompose(b)
|
|
1115
|
+
for (const pb of partsB) {
|
|
1116
|
+
if (pointInLineInterior(pa.coordinates, pb.coordinates)) return true
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (b.type === 'Polygon' || b.type === 'MultiPolygon') {
|
|
1120
|
+
const partsB = decompose(b)
|
|
1121
|
+
for (const pb of partsB) {
|
|
1122
|
+
if (pointInPolygonInterior(pa.coordinates, pb.coordinates)) return true
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return false
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Line/Polygon: line crosses polygon if part of line is inside and part is outside
|
|
1130
|
+
if (dimA === 1 && dimB === 2) {
|
|
1131
|
+
return stIntersects(a, b) && !stContains(b, a)
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Symmetric cases
|
|
1135
|
+
if (dimA >= 1 && dimB === 0) return stCrosses(b, a)
|
|
1136
|
+
if (dimA === 2 && dimB === 1) return stCrosses(b, a)
|
|
1137
|
+
|
|
1138
|
+
return false
|
|
1139
|
+
}
|
|
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
|
+
}
|