squirreling 0.9.2 → 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/README.md +2 -0
- package/package.json +3 -3
- package/src/backend/dataSource.js +2 -2
- package/src/expression/evaluate.js +32 -1
- 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 +365 -0
- package/src/expression/wkt.js +222 -0
- package/src/types.d.ts +16 -0
- package/src/validation.js +39 -2
- package/src/validationErrors.js +16 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Relation, SimpleGeometry } from './geometry.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const EPSILON = 1e-10
|
|
6
|
+
export const EPSILON_SQ = EPSILON * EPSILON
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{ minX: number, minY: number, maxX: number, maxY: number }} BBox
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @type {WeakMap<SimpleGeometry, BBox>} */
|
|
13
|
+
const bboxCache = new WeakMap()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute the axis-aligned bounding box of a simple geometry.
|
|
17
|
+
* Results are cached per geometry object.
|
|
18
|
+
*
|
|
19
|
+
* @param {SimpleGeometry} geom
|
|
20
|
+
* @returns {BBox}
|
|
21
|
+
*/
|
|
22
|
+
export function bbox(geom) {
|
|
23
|
+
let b = bboxCache.get(geom)
|
|
24
|
+
if (b) return b
|
|
25
|
+
if (geom.type === 'Point') {
|
|
26
|
+
const [x, y] = geom.coordinates
|
|
27
|
+
b = { minX: x, minY: y, maxX: x, maxY: y }
|
|
28
|
+
} else {
|
|
29
|
+
/** @type {number[][]} */
|
|
30
|
+
const points = geom.type === 'LineString'
|
|
31
|
+
? geom.coordinates
|
|
32
|
+
: geom.coordinates[0] // outer ring
|
|
33
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
|
34
|
+
for (const p of points) {
|
|
35
|
+
if (p[0] < minX) minX = p[0]
|
|
36
|
+
if (p[1] < minY) minY = p[1]
|
|
37
|
+
if (p[0] > maxX) maxX = p[0]
|
|
38
|
+
if (p[1] > maxY) maxY = p[1]
|
|
39
|
+
}
|
|
40
|
+
b = { minX, minY, maxX, maxY }
|
|
41
|
+
}
|
|
42
|
+
bboxCache.set(geom, b)
|
|
43
|
+
return b
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Test whether two bounding boxes overlap.
|
|
48
|
+
*
|
|
49
|
+
* @param {BBox} a
|
|
50
|
+
* @param {BBox} b
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
export function bboxOverlap(a, b) {
|
|
54
|
+
return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compute the squared distance between two 2D points.
|
|
59
|
+
*
|
|
60
|
+
* @param {number[]} a
|
|
61
|
+
* @param {number[]} b
|
|
62
|
+
* @returns {number}
|
|
63
|
+
*/
|
|
64
|
+
export function distSq(a, b) {
|
|
65
|
+
const dx = a[0] - b[0]
|
|
66
|
+
const dy = a[1] - b[1]
|
|
67
|
+
return dx * dx + dy * dy
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Squared minimum distance from point p to line segment [a, b].
|
|
72
|
+
*
|
|
73
|
+
* @param {number[]} p
|
|
74
|
+
* @param {number[]} a
|
|
75
|
+
* @param {number[]} b
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
export function pointToSegmentDistSq(p, a, b) {
|
|
79
|
+
const dx = b[0] - a[0]
|
|
80
|
+
const dy = b[1] - a[1]
|
|
81
|
+
const lenSq = dx * dx + dy * dy
|
|
82
|
+
if (lenSq === 0) return distSq(p, a)
|
|
83
|
+
let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq
|
|
84
|
+
t = Math.max(0, Math.min(1, t))
|
|
85
|
+
const ddx = p[0] - a[0] - t * dx
|
|
86
|
+
const ddy = p[1] - a[1] - t * dy
|
|
87
|
+
return ddx * ddx + ddy * ddy
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Test whether two line segments [p1,p2] and [p3,p4] intersect.
|
|
92
|
+
* Returns true if they share any point (including endpoints).
|
|
93
|
+
*
|
|
94
|
+
* @param {number[]} p1
|
|
95
|
+
* @param {number[]} p2
|
|
96
|
+
* @param {number[]} p3
|
|
97
|
+
* @param {number[]} p4
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function segmentsIntersect(p1, p2, p3, p4) {
|
|
101
|
+
const d1 = cross(p3, p4, p1)
|
|
102
|
+
const d2 = cross(p3, p4, p2)
|
|
103
|
+
const d3 = cross(p1, p2, p3)
|
|
104
|
+
const d4 = cross(p1, p2, p4)
|
|
105
|
+
|
|
106
|
+
if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) &&
|
|
107
|
+
(d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (Math.abs(d1) < EPSILON && onSegment(p3, p4, p1)) return true
|
|
112
|
+
if (Math.abs(d2) < EPSILON && onSegment(p3, p4, p2)) return true
|
|
113
|
+
if (Math.abs(d3) < EPSILON && onSegment(p1, p2, p3)) return true
|
|
114
|
+
if (Math.abs(d4) < EPSILON && onSegment(p1, p2, p4)) return true
|
|
115
|
+
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Cross product of vectors (b-a) and (c-a).
|
|
121
|
+
*
|
|
122
|
+
* @param {number[]} a
|
|
123
|
+
* @param {number[]} b
|
|
124
|
+
* @param {number[]} c
|
|
125
|
+
* @returns {number}
|
|
126
|
+
*/
|
|
127
|
+
function cross(a, b, c) {
|
|
128
|
+
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if point c lies on segment [a, b], assuming collinearity.
|
|
133
|
+
*
|
|
134
|
+
* @param {number[]} a
|
|
135
|
+
* @param {number[]} b
|
|
136
|
+
* @param {number[]} c
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
function onSegment(a, b, c) {
|
|
140
|
+
return Math.min(a[0], b[0]) - c[0] <= EPSILON && c[0] - Math.max(a[0], b[0]) <= EPSILON &&
|
|
141
|
+
Math.min(a[1], b[1]) - c[1] <= EPSILON && c[1] - Math.max(a[1], b[1]) <= EPSILON
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Classify a point relative to a ring: 'OUTSIDE', 'BOUNDARY', or 'INSIDE'.
|
|
146
|
+
* Combines ray casting with boundary distance check in a single pass.
|
|
147
|
+
* ring is an array of [x, y] coords (closed ring, first = last).
|
|
148
|
+
*
|
|
149
|
+
* @param {number[]} point
|
|
150
|
+
* @param {number[][]} ring
|
|
151
|
+
* @returns {Relation}
|
|
152
|
+
*/
|
|
153
|
+
function pointInRing(point, ring) {
|
|
154
|
+
const [px, py] = point
|
|
155
|
+
let inside = false
|
|
156
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
157
|
+
if (pointToSegmentDistSq(point, ring[j], ring[i]) < EPSILON_SQ) {
|
|
158
|
+
return 'BOUNDARY'
|
|
159
|
+
}
|
|
160
|
+
const [xi, yi] = ring[i]
|
|
161
|
+
const [xj, yj] = ring[j]
|
|
162
|
+
if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
|
|
163
|
+
inside = !inside
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return inside ? 'INSIDE' : 'OUTSIDE'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Classify a point relative to a polygon: 'OUTSIDE', 'BOUNDARY', or 'INSIDE'.
|
|
171
|
+
* First ring is exterior, rest are holes.
|
|
172
|
+
*
|
|
173
|
+
* @param {number[]} point
|
|
174
|
+
* @param {number[][][]} rings
|
|
175
|
+
* @returns {Relation}
|
|
176
|
+
*/
|
|
177
|
+
export function pointInPolygon(point, rings) {
|
|
178
|
+
const rel = pointInRing(point, rings[0])
|
|
179
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
180
|
+
if (rel === 'BOUNDARY') return 'BOUNDARY'
|
|
181
|
+
for (let i = 1; i < rings.length; i++) {
|
|
182
|
+
const holeRel = pointInRing(point, rings[i])
|
|
183
|
+
if (holeRel === 'INSIDE') return 'OUTSIDE'
|
|
184
|
+
if (holeRel === 'BOUNDARY') return 'BOUNDARY'
|
|
185
|
+
}
|
|
186
|
+
return 'INSIDE'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Test if a line segment intersects a ring boundary.
|
|
191
|
+
*
|
|
192
|
+
* @param {number[]} a
|
|
193
|
+
* @param {number[]} b
|
|
194
|
+
* @param {number[][]} ring
|
|
195
|
+
* @returns {boolean}
|
|
196
|
+
*/
|
|
197
|
+
function segmentIntersectsRing(a, b, ring) {
|
|
198
|
+
for (let i = 0; i < ring.length - 1; i++) {
|
|
199
|
+
if (segmentsIntersect(a, b, ring[i], ring[i + 1])) return true
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Test if a linestring intersects a polygon.
|
|
206
|
+
*
|
|
207
|
+
* @param {number[][]} line
|
|
208
|
+
* @param {number[][][]} rings
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
function lineIntersectsPolygon(line, rings) {
|
|
212
|
+
// Check if any point of the line is inside the polygon
|
|
213
|
+
for (const pt of line) {
|
|
214
|
+
if (pointInPolygon(pt, rings) !== 'OUTSIDE') return true
|
|
215
|
+
}
|
|
216
|
+
// Check if any segment of the line intersects any ring edge
|
|
217
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
218
|
+
for (const ring of rings) {
|
|
219
|
+
if (segmentIntersectsRing(line[i], line[i + 1], ring)) return true
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Test if two linestrings share any point (intersect).
|
|
227
|
+
*
|
|
228
|
+
* @param {number[][]} line1
|
|
229
|
+
* @param {number[][]} line2
|
|
230
|
+
* @returns {boolean}
|
|
231
|
+
*/
|
|
232
|
+
function linesIntersect(line1, line2) {
|
|
233
|
+
for (let i = 0; i < line1.length - 1; i++) {
|
|
234
|
+
for (let j = 0; j < line2.length - 1; j++) {
|
|
235
|
+
if (segmentsIntersect(line1[i], line1[i + 1], line2[j], line2[j + 1])) return true
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Classify containment of a linestring within a polygon.
|
|
243
|
+
* Returns 'INSIDE' if entirely in interior, 'BOUNDARY' if inside but
|
|
244
|
+
* touches boundary, 'OUTSIDE' if any part is outside.
|
|
245
|
+
*
|
|
246
|
+
* @param {number[][]} line
|
|
247
|
+
* @param {number[][][]} rings
|
|
248
|
+
* @returns {Relation}
|
|
249
|
+
*/
|
|
250
|
+
function polygonContainsLine(line, rings) {
|
|
251
|
+
/** @type {Relation} */
|
|
252
|
+
let result = 'INSIDE'
|
|
253
|
+
for (const pt of line) {
|
|
254
|
+
const rel = pointInPolygon(pt, rings)
|
|
255
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
256
|
+
if (rel === 'BOUNDARY') result = 'BOUNDARY'
|
|
257
|
+
}
|
|
258
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
259
|
+
const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
|
|
260
|
+
const rel = pointInPolygon(mid, rings)
|
|
261
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
262
|
+
if (rel === 'BOUNDARY') result = 'BOUNDARY'
|
|
263
|
+
}
|
|
264
|
+
return result
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Classify containment of polygon B within polygon A.
|
|
269
|
+
* Returns 'INSIDE' if entirely in interior, 'BOUNDARY' if inside but
|
|
270
|
+
* touches boundary, 'OUTSIDE' if any part is outside.
|
|
271
|
+
*
|
|
272
|
+
* @param {number[][][]} ringsA
|
|
273
|
+
* @param {number[][][]} ringsB
|
|
274
|
+
* @returns {Relation}
|
|
275
|
+
*/
|
|
276
|
+
function polygonContainsPolygon(ringsA, ringsB) {
|
|
277
|
+
/** @type {Relation} */
|
|
278
|
+
let result = 'INSIDE'
|
|
279
|
+
for (const pt of ringsB[0]) {
|
|
280
|
+
const rel = pointInPolygon(pt, ringsA)
|
|
281
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
282
|
+
if (rel === 'BOUNDARY') result = 'BOUNDARY'
|
|
283
|
+
}
|
|
284
|
+
for (let i = 0; i < ringsB[0].length - 1; i++) {
|
|
285
|
+
const mid = [(ringsB[0][i][0] + ringsB[0][i + 1][0]) / 2, (ringsB[0][i][1] + ringsB[0][i + 1][1]) / 2]
|
|
286
|
+
const rel = pointInPolygon(mid, ringsA)
|
|
287
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
288
|
+
if (rel === 'BOUNDARY') result = 'BOUNDARY'
|
|
289
|
+
}
|
|
290
|
+
return result
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Test if point is on a linestring.
|
|
295
|
+
*
|
|
296
|
+
* @param {number[]} point
|
|
297
|
+
* @param {number[][]} line
|
|
298
|
+
* @returns {boolean}
|
|
299
|
+
*/
|
|
300
|
+
function pointOnLine(point, line) {
|
|
301
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
302
|
+
if (pointToSegmentDistSq(point, line[i], line[i + 1]) < EPSILON_SQ) return true
|
|
303
|
+
}
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Compute intersection point of two segments (if they intersect at a single point).
|
|
309
|
+
*
|
|
310
|
+
* @param {number[]} p1
|
|
311
|
+
* @param {number[]} p2
|
|
312
|
+
* @param {number[]} p3
|
|
313
|
+
* @param {number[]} p4
|
|
314
|
+
* @returns {number[] | null}
|
|
315
|
+
*/
|
|
316
|
+
function segmentIntersectionPoint(p1, p2, p3, p4) {
|
|
317
|
+
const d1x = p2[0] - p1[0], d1y = p2[1] - p1[1]
|
|
318
|
+
const d2x = p4[0] - p3[0], d2y = p4[1] - p3[1]
|
|
319
|
+
const denom = d1x * d2y - d1y * d2x
|
|
320
|
+
if (Math.abs(denom) < EPSILON) return null // parallel
|
|
321
|
+
const t = ((p3[0] - p1[0]) * d2y - (p3[1] - p1[1]) * d2x) / denom
|
|
322
|
+
return [p1[0] + t * d1x, p1[1] + t * d1y]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Classify a point relative to a linestring.
|
|
327
|
+
*
|
|
328
|
+
* @param {number[]} point
|
|
329
|
+
* @param {number[][]} line
|
|
330
|
+
* @returns {Relation}
|
|
331
|
+
*/
|
|
332
|
+
export function pointLineRelation(point, line) {
|
|
333
|
+
// Check endpoints first
|
|
334
|
+
if (distSq(point, line[0]) < EPSILON_SQ) return 'BOUNDARY'
|
|
335
|
+
if (distSq(point, line[line.length - 1]) < EPSILON_SQ) return 'BOUNDARY'
|
|
336
|
+
// Check if on any segment
|
|
337
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
338
|
+
if (pointToSegmentDistSq(point, line[i], line[i + 1]) < EPSILON_SQ) return 'INSIDE'
|
|
339
|
+
}
|
|
340
|
+
return 'OUTSIDE'
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Classify the relationship between two linestrings.
|
|
345
|
+
* Returns INSIDE if interiors share a point, BOUNDARY if they only meet
|
|
346
|
+
* at endpoints, OUTSIDE if disjoint.
|
|
347
|
+
*
|
|
348
|
+
* @param {number[][]} line1
|
|
349
|
+
* @param {number[][]} line2
|
|
350
|
+
* @returns {Relation}
|
|
351
|
+
*/
|
|
352
|
+
function lineLineRelation(line1, line2) {
|
|
353
|
+
let boundary = false
|
|
354
|
+
for (let i = 0; i < line1.length - 1; i++) {
|
|
355
|
+
for (let j = 0; j < line2.length - 1; j++) {
|
|
356
|
+
if (!segmentsIntersect(line1[i], line1[i + 1], line2[j], line2[j + 1])) continue
|
|
357
|
+
// Segments intersect, check if the intersection is interior to both lines
|
|
358
|
+
// Check segment midpoints
|
|
359
|
+
const mid1 = [(line1[i][0] + line1[i + 1][0]) / 2, (line1[i][1] + line1[i + 1][1]) / 2]
|
|
360
|
+
if (pointLineRelation(mid1, line1) === 'INSIDE' && pointLineRelation(mid1, line2) === 'INSIDE') {
|
|
361
|
+
return 'INSIDE'
|
|
362
|
+
}
|
|
363
|
+
const mid2 = [(line2[j][0] + line2[j + 1][0]) / 2, (line2[j][1] + line2[j + 1][1]) / 2]
|
|
364
|
+
if (pointLineRelation(mid2, line1) === 'INSIDE' && pointLineRelation(mid2, line2) === 'INSIDE') {
|
|
365
|
+
return 'INSIDE'
|
|
366
|
+
}
|
|
367
|
+
// Check actual intersection point
|
|
368
|
+
const ip = segmentIntersectionPoint(line1[i], line1[i + 1], line2[j], line2[j + 1])
|
|
369
|
+
if (ip) {
|
|
370
|
+
if (pointLineRelation(ip, line1) === 'INSIDE' && pointLineRelation(ip, line2) === 'INSIDE') {
|
|
371
|
+
return 'INSIDE'
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
boundary = true
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return boundary ? 'BOUNDARY' : 'OUTSIDE'
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Classify the relationship between a linestring and a polygon.
|
|
382
|
+
* Returns INSIDE if line interior enters polygon interior, BOUNDARY if
|
|
383
|
+
* they only share boundary points, OUTSIDE if disjoint.
|
|
384
|
+
*
|
|
385
|
+
* @param {number[][]} line
|
|
386
|
+
* @param {number[][][]} rings
|
|
387
|
+
* @returns {Relation}
|
|
388
|
+
*/
|
|
389
|
+
function linePolygonRelation(line, rings) {
|
|
390
|
+
let boundary = false
|
|
391
|
+
// Check segment midpoints and interior vertices
|
|
392
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
393
|
+
const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
|
|
394
|
+
const midRel = pointInPolygon(mid, rings)
|
|
395
|
+
if (midRel === 'INSIDE') return 'INSIDE'
|
|
396
|
+
if (midRel === 'BOUNDARY') boundary = true
|
|
397
|
+
}
|
|
398
|
+
// Check interior vertices of the line
|
|
399
|
+
for (let i = 1; i < line.length - 1; i++) {
|
|
400
|
+
const rel = pointInPolygon(line[i], rings)
|
|
401
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
402
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
403
|
+
}
|
|
404
|
+
// Check line endpoints
|
|
405
|
+
for (const pt of [line[0], line[line.length - 1]]) {
|
|
406
|
+
const rel = pointInPolygon(pt, rings)
|
|
407
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
408
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
409
|
+
}
|
|
410
|
+
// Check if any edge of the polygon rings intersects the line
|
|
411
|
+
if (!boundary) {
|
|
412
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
413
|
+
for (const ring of rings) {
|
|
414
|
+
if (segmentIntersectsRing(line[i], line[i + 1], ring)) {
|
|
415
|
+
boundary = true
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return boundary ? 'BOUNDARY' : 'OUTSIDE'
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Classify the relationship between two polygons.
|
|
425
|
+
* Returns INSIDE if interiors share area, BOUNDARY if they only share
|
|
426
|
+
* boundary points/edges, OUTSIDE if disjoint.
|
|
427
|
+
*
|
|
428
|
+
* @param {number[][][]} rings1
|
|
429
|
+
* @param {number[][][]} rings2
|
|
430
|
+
* @returns {Relation}
|
|
431
|
+
*/
|
|
432
|
+
function polygonPolygonRelation(rings1, rings2) {
|
|
433
|
+
let boundary = false
|
|
434
|
+
// Check vertices of polygon1 against polygon2
|
|
435
|
+
for (const pt of rings1[0]) {
|
|
436
|
+
const rel = pointInPolygon(pt, rings2)
|
|
437
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
438
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
439
|
+
}
|
|
440
|
+
// Check vertices of polygon2 against polygon1
|
|
441
|
+
for (const pt of rings2[0]) {
|
|
442
|
+
const rel = pointInPolygon(pt, rings1)
|
|
443
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
444
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
445
|
+
}
|
|
446
|
+
// Check edge midpoints of polygon1 against polygon2
|
|
447
|
+
for (let i = 0; i < rings1[0].length - 1; i++) {
|
|
448
|
+
const mid = [(rings1[0][i][0] + rings1[0][i + 1][0]) / 2, (rings1[0][i][1] + rings1[0][i + 1][1]) / 2]
|
|
449
|
+
const rel = pointInPolygon(mid, rings2)
|
|
450
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
451
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
452
|
+
}
|
|
453
|
+
// Check edge midpoints of polygon2 against polygon1
|
|
454
|
+
for (let i = 0; i < rings2[0].length - 1; i++) {
|
|
455
|
+
const mid = [(rings2[0][i][0] + rings2[0][i + 1][0]) / 2, (rings2[0][i][1] + rings2[0][i + 1][1]) / 2]
|
|
456
|
+
const rel = pointInPolygon(mid, rings1)
|
|
457
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
458
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
459
|
+
}
|
|
460
|
+
// Check edge-edge intersections
|
|
461
|
+
if (!boundary) {
|
|
462
|
+
for (let i = 0; i < rings1[0].length - 1; i++) {
|
|
463
|
+
for (let j = 0; j < rings2[0].length - 1; j++) {
|
|
464
|
+
if (segmentsIntersect(rings1[0][i], rings1[0][i + 1], rings2[0][j], rings2[0][j + 1])) {
|
|
465
|
+
boundary = true
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return boundary ? 'BOUNDARY' : 'OUTSIDE'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {SimpleGeometry[]} partsA
|
|
475
|
+
* @param {SimpleGeometry[]} partsB
|
|
476
|
+
* @returns {boolean}
|
|
477
|
+
*/
|
|
478
|
+
export function simpleIntersects(partsA, partsB) {
|
|
479
|
+
for (const pa of partsA) {
|
|
480
|
+
for (const pb of partsB) {
|
|
481
|
+
if (simplePairIntersects(pa, pb)) return true
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return false
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* @param {SimpleGeometry} a
|
|
489
|
+
* @param {SimpleGeometry} b
|
|
490
|
+
* @returns {boolean}
|
|
491
|
+
*/
|
|
492
|
+
function simplePairIntersects(a, b) {
|
|
493
|
+
if (!bboxOverlap(bbox(a), bbox(b))) return false
|
|
494
|
+
const ta = a.type
|
|
495
|
+
const tb = b.type
|
|
496
|
+
|
|
497
|
+
if (ta === 'Point' && tb === 'Point') {
|
|
498
|
+
return distSq(a.coordinates, b.coordinates) < EPSILON_SQ
|
|
499
|
+
}
|
|
500
|
+
if (ta === 'Point' && tb === 'LineString') {
|
|
501
|
+
return pointOnLine(a.coordinates, b.coordinates)
|
|
502
|
+
}
|
|
503
|
+
if (ta === 'LineString' && tb === 'Point') {
|
|
504
|
+
return pointOnLine(b.coordinates, a.coordinates)
|
|
505
|
+
}
|
|
506
|
+
if (ta === 'Point' && tb === 'Polygon') {
|
|
507
|
+
return pointInPolygon(a.coordinates, b.coordinates) !== 'OUTSIDE'
|
|
508
|
+
}
|
|
509
|
+
if (ta === 'Polygon' && tb === 'Point') {
|
|
510
|
+
return pointInPolygon(b.coordinates, a.coordinates) !== 'OUTSIDE'
|
|
511
|
+
}
|
|
512
|
+
if (ta === 'LineString' && tb === 'LineString') {
|
|
513
|
+
return linesIntersect(a.coordinates, b.coordinates)
|
|
514
|
+
}
|
|
515
|
+
if (ta === 'LineString' && tb === 'Polygon') {
|
|
516
|
+
return lineIntersectsPolygon(a.coordinates, b.coordinates)
|
|
517
|
+
}
|
|
518
|
+
if (ta === 'Polygon' && tb === 'LineString') {
|
|
519
|
+
return lineIntersectsPolygon(b.coordinates, a.coordinates)
|
|
520
|
+
}
|
|
521
|
+
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
522
|
+
return polygonPolygonRelation(a.coordinates, b.coordinates) !== 'OUTSIDE'
|
|
523
|
+
}
|
|
524
|
+
return false
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Classify the relationship between two simple geometries.
|
|
529
|
+
* Returns 'INSIDE' if interiors intersect, 'BOUNDARY' if they
|
|
530
|
+
* intersect only at boundaries, or 'OUTSIDE' if they don't intersect.
|
|
531
|
+
*
|
|
532
|
+
* @param {SimpleGeometry} a
|
|
533
|
+
* @param {SimpleGeometry} b
|
|
534
|
+
* @returns {Relation}
|
|
535
|
+
*/
|
|
536
|
+
export function simplePairRelation(a, b) {
|
|
537
|
+
if (!bboxOverlap(bbox(a), bbox(b))) return 'OUTSIDE'
|
|
538
|
+
const ta = a.type
|
|
539
|
+
const tb = b.type
|
|
540
|
+
|
|
541
|
+
// Point / Point
|
|
542
|
+
if (ta === 'Point' && tb === 'Point') {
|
|
543
|
+
return distSq(a.coordinates, b.coordinates) < EPSILON_SQ ? 'INSIDE' : 'OUTSIDE'
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Point / LineString
|
|
547
|
+
if (ta === 'Point' && tb === 'LineString') {
|
|
548
|
+
return pointLineRelation(a.coordinates, b.coordinates)
|
|
549
|
+
}
|
|
550
|
+
if (ta === 'LineString' && tb === 'Point') {
|
|
551
|
+
return pointLineRelation(b.coordinates, a.coordinates)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Point / Polygon
|
|
555
|
+
if (ta === 'Point' && tb === 'Polygon') {
|
|
556
|
+
return pointInPolygon(a.coordinates, b.coordinates)
|
|
557
|
+
}
|
|
558
|
+
if (ta === 'Polygon' && tb === 'Point') {
|
|
559
|
+
return pointInPolygon(b.coordinates, a.coordinates)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// LineString / LineString
|
|
563
|
+
if (ta === 'LineString' && tb === 'LineString') {
|
|
564
|
+
return lineLineRelation(a.coordinates, b.coordinates)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// LineString / Polygon
|
|
568
|
+
if (ta === 'LineString' && tb === 'Polygon') {
|
|
569
|
+
return linePolygonRelation(a.coordinates, b.coordinates)
|
|
570
|
+
}
|
|
571
|
+
if (ta === 'Polygon' && tb === 'LineString') {
|
|
572
|
+
return linePolygonRelation(b.coordinates, a.coordinates)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Polygon / Polygon
|
|
576
|
+
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
577
|
+
return polygonPolygonRelation(a.coordinates, b.coordinates)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return 'OUTSIDE'
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Classify containment of b within a.
|
|
585
|
+
* Returns 'INSIDE' if b is strictly in a's interior, 'BOUNDARY' if b is
|
|
586
|
+
* inside a but touches a's boundary, 'OUTSIDE' if any part of b is outside a.
|
|
587
|
+
*
|
|
588
|
+
* @param {SimpleGeometry} a
|
|
589
|
+
* @param {SimpleGeometry} b
|
|
590
|
+
* @returns {Relation}
|
|
591
|
+
*/
|
|
592
|
+
export function simplePairContainment(a, b) {
|
|
593
|
+
if (!bboxOverlap(bbox(a), bbox(b))) return 'OUTSIDE'
|
|
594
|
+
const ta = a.type
|
|
595
|
+
const tb = b.type
|
|
596
|
+
|
|
597
|
+
if (ta === 'Point' && tb === 'Point') {
|
|
598
|
+
return distSq(a.coordinates, b.coordinates) < EPSILON_SQ ? 'BOUNDARY' : 'OUTSIDE'
|
|
599
|
+
}
|
|
600
|
+
if (ta === 'LineString' && tb === 'Point') {
|
|
601
|
+
return pointLineRelation(b.coordinates, a.coordinates)
|
|
602
|
+
}
|
|
603
|
+
if (ta === 'Polygon' && tb === 'Point') {
|
|
604
|
+
return pointInPolygon(b.coordinates, a.coordinates)
|
|
605
|
+
}
|
|
606
|
+
if (ta === 'Polygon' && tb === 'LineString') {
|
|
607
|
+
return polygonContainsLine(b.coordinates, a.coordinates)
|
|
608
|
+
}
|
|
609
|
+
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
610
|
+
return polygonContainsPolygon(a.coordinates, b.coordinates)
|
|
611
|
+
}
|
|
612
|
+
if (ta === 'LineString' && tb === 'LineString') {
|
|
613
|
+
// Line A contains line B if every point of B is on A
|
|
614
|
+
for (const pt of b.coordinates) {
|
|
615
|
+
if (!pointOnLine(pt, a.coordinates)) return 'OUTSIDE'
|
|
616
|
+
}
|
|
617
|
+
return 'BOUNDARY'
|
|
618
|
+
}
|
|
619
|
+
return 'OUTSIDE'
|
|
620
|
+
}
|