squirreling 0.9.4 → 0.10.0
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 +5 -1
- package/package.json +2 -2
- package/src/backend/dataSource.js +32 -8
- package/src/execute/aggregates.js +10 -2
- package/src/execute/execute.js +19 -16
- package/src/execute/join.js +8 -7
- package/src/expression/date.js +60 -0
- package/src/expression/evaluate.js +14 -4
- package/src/parse/expression.js +42 -1
- package/src/plan/columns.js +36 -18
- package/src/spatial/bbox.js +53 -0
- package/src/{expression/spatial.equality.js → spatial/equality.js} +5 -5
- package/src/{expression → spatial}/geometry.d.ts +7 -0
- package/src/{expression/spatial.geometry.js → spatial/operations.js} +31 -280
- package/src/spatial/pointRelations.js +102 -0
- package/src/spatial/primitives.js +27 -0
- package/src/spatial/segments.js +139 -0
- package/src/{expression → spatial}/spatial.js +12 -10
- package/src/types.d.ts +1 -0
- package/src/validation.js +24 -1
- /package/src/{expression → spatial}/wkt.js +0 -0
|
@@ -1,205 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
}
|
|
1
|
+
import { bboxOverlap } from './bbox.js'
|
|
2
|
+
import { pointInPolygon, pointLineRelation, pointOnLine } from './pointRelations.js'
|
|
3
|
+
import { EPSILON_SQ, distSq } from './primitives.js'
|
|
4
|
+
import { segmentIntersectsRing, segmentTouchPoint, segmentsIntersect } from './segments.js'
|
|
168
5
|
|
|
169
6
|
/**
|
|
170
|
-
*
|
|
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}
|
|
7
|
+
* @import { Relation, SimpleGeometry } from './geometry.js'
|
|
196
8
|
*/
|
|
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
9
|
|
|
204
10
|
/**
|
|
205
11
|
* Test if a linestring intersects a polygon.
|
|
@@ -209,11 +15,11 @@ function segmentIntersectsRing(a, b, ring) {
|
|
|
209
15
|
* @returns {boolean}
|
|
210
16
|
*/
|
|
211
17
|
function lineIntersectsPolygon(line, rings) {
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
18
|
+
// Fast path for common containment queries: if one point is inside or on
|
|
19
|
+
// boundary, the line intersects.
|
|
20
|
+
if (pointInPolygon(line[0], rings) !== 'OUTSIDE') return true
|
|
21
|
+
|
|
22
|
+
// Otherwise, detect crossings/touches against polygon boundaries.
|
|
217
23
|
for (let i = 0; i < line.length - 1; i++) {
|
|
218
24
|
for (const ring of rings) {
|
|
219
25
|
if (segmentIntersectsRing(line[i], line[i + 1], ring)) return true
|
|
@@ -238,6 +44,15 @@ function linesIntersect(line1, line2) {
|
|
|
238
44
|
return false
|
|
239
45
|
}
|
|
240
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @param {number[]} point
|
|
49
|
+
* @param {number[][]} line
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
function isLineEndpoint(point, line) {
|
|
53
|
+
return distSq(point, line[0]) < EPSILON_SQ || distSq(point, line[line.length - 1]) < EPSILON_SQ
|
|
54
|
+
}
|
|
55
|
+
|
|
241
56
|
/**
|
|
242
57
|
* Classify containment of a linestring within a polygon.
|
|
243
58
|
* Returns 'INSIDE' if entirely in interior, 'BOUNDARY' if inside but
|
|
@@ -290,56 +105,6 @@ function polygonContainsPolygon(ringsA, ringsB) {
|
|
|
290
105
|
return result
|
|
291
106
|
}
|
|
292
107
|
|
|
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
108
|
/**
|
|
344
109
|
* Classify the relationship between two linestrings.
|
|
345
110
|
* Returns INSIDE if interiors share a point, BOUNDARY if they only meet
|
|
@@ -353,24 +118,10 @@ function lineLineRelation(line1, line2) {
|
|
|
353
118
|
let boundary = false
|
|
354
119
|
for (let i = 0; i < line1.length - 1; i++) {
|
|
355
120
|
for (let j = 0; j < line2.length - 1; j++) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
}
|
|
121
|
+
const touchPoint = segmentTouchPoint(line1[i], line1[i + 1], line2[j], line2[j + 1])
|
|
122
|
+
if (touchPoint === 'OUTSIDE') continue
|
|
123
|
+
if (touchPoint === 'INSIDE') return 'INSIDE'
|
|
124
|
+
if (!isLineEndpoint(touchPoint, line1) && !isLineEndpoint(touchPoint, line2)) return 'INSIDE'
|
|
374
125
|
boundary = true
|
|
375
126
|
}
|
|
376
127
|
}
|
|
@@ -475,10 +226,10 @@ function polygonPolygonRelation(rings1, rings2) {
|
|
|
475
226
|
* @param {SimpleGeometry[]} partsB
|
|
476
227
|
* @returns {boolean}
|
|
477
228
|
*/
|
|
478
|
-
export function
|
|
229
|
+
export function intersects(partsA, partsB) {
|
|
479
230
|
for (const pa of partsA) {
|
|
480
231
|
for (const pb of partsB) {
|
|
481
|
-
if (
|
|
232
|
+
if (pairIntersects(pa, pb)) return true
|
|
482
233
|
}
|
|
483
234
|
}
|
|
484
235
|
return false
|
|
@@ -489,8 +240,8 @@ export function simpleIntersects(partsA, partsB) {
|
|
|
489
240
|
* @param {SimpleGeometry} b
|
|
490
241
|
* @returns {boolean}
|
|
491
242
|
*/
|
|
492
|
-
function
|
|
493
|
-
if (!bboxOverlap(
|
|
243
|
+
function pairIntersects(a, b) {
|
|
244
|
+
if (!bboxOverlap(a, b)) return false
|
|
494
245
|
const ta = a.type
|
|
495
246
|
const tb = b.type
|
|
496
247
|
|
|
@@ -533,8 +284,8 @@ function simplePairIntersects(a, b) {
|
|
|
533
284
|
* @param {SimpleGeometry} b
|
|
534
285
|
* @returns {Relation}
|
|
535
286
|
*/
|
|
536
|
-
export function
|
|
537
|
-
if (!bboxOverlap(
|
|
287
|
+
export function pairRelation(a, b) {
|
|
288
|
+
if (!bboxOverlap(a, b)) return 'OUTSIDE'
|
|
538
289
|
const ta = a.type
|
|
539
290
|
const tb = b.type
|
|
540
291
|
|
|
@@ -589,8 +340,8 @@ export function simplePairRelation(a, b) {
|
|
|
589
340
|
* @param {SimpleGeometry} b
|
|
590
341
|
* @returns {Relation}
|
|
591
342
|
*/
|
|
592
|
-
export function
|
|
593
|
-
if (!bboxOverlap(
|
|
343
|
+
export function pairContainment(a, b) {
|
|
344
|
+
if (!bboxOverlap(a, b)) return 'OUTSIDE'
|
|
594
345
|
const ta = a.type
|
|
595
346
|
const tb = b.type
|
|
596
347
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Relation } from './geometry.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EPSILON_SQ, distSq } from './primitives.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Classify a point relative to a ring: 'OUTSIDE', 'BOUNDARY', or 'INSIDE'.
|
|
9
|
+
* Combines ray casting with boundary distance check in a single pass.
|
|
10
|
+
* ring is an array of [x, y] coords (closed ring, first = last).
|
|
11
|
+
*
|
|
12
|
+
* @param {number[]} point
|
|
13
|
+
* @param {number[][]} ring
|
|
14
|
+
* @returns {Relation}
|
|
15
|
+
*/
|
|
16
|
+
function pointInRing(point, ring) {
|
|
17
|
+
const [px, py] = point
|
|
18
|
+
let inside = false
|
|
19
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
20
|
+
if (pointToSegmentDistSq(point, ring[j], ring[i]) < EPSILON_SQ) {
|
|
21
|
+
return 'BOUNDARY'
|
|
22
|
+
}
|
|
23
|
+
const [xi, yi] = ring[i]
|
|
24
|
+
const [xj, yj] = ring[j]
|
|
25
|
+
if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
|
|
26
|
+
inside = !inside
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return inside ? 'INSIDE' : 'OUTSIDE'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Classify a point relative to a polygon: 'OUTSIDE', 'BOUNDARY', or 'INSIDE'.
|
|
34
|
+
* First ring is exterior, rest are holes.
|
|
35
|
+
*
|
|
36
|
+
* @param {number[]} point
|
|
37
|
+
* @param {number[][][]} rings
|
|
38
|
+
* @returns {Relation}
|
|
39
|
+
*/
|
|
40
|
+
export function pointInPolygon(point, rings) {
|
|
41
|
+
const rel = pointInRing(point, rings[0])
|
|
42
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
43
|
+
if (rel === 'BOUNDARY') return 'BOUNDARY'
|
|
44
|
+
for (let i = 1; i < rings.length; i++) {
|
|
45
|
+
const holeRel = pointInRing(point, rings[i])
|
|
46
|
+
if (holeRel === 'INSIDE') return 'OUTSIDE'
|
|
47
|
+
if (holeRel === 'BOUNDARY') return 'BOUNDARY'
|
|
48
|
+
}
|
|
49
|
+
return 'INSIDE'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Test if point is on a linestring.
|
|
54
|
+
*
|
|
55
|
+
* @param {number[]} point
|
|
56
|
+
* @param {number[][]} line
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export function pointOnLine(point, line) {
|
|
60
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
61
|
+
if (pointToSegmentDistSq(point, line[i], line[i + 1]) < EPSILON_SQ) return true
|
|
62
|
+
}
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Classify a point relative to a linestring.
|
|
68
|
+
*
|
|
69
|
+
* @param {number[]} point
|
|
70
|
+
* @param {number[][]} line
|
|
71
|
+
* @returns {Relation}
|
|
72
|
+
*/
|
|
73
|
+
export function pointLineRelation(point, line) {
|
|
74
|
+
// Check endpoints first
|
|
75
|
+
if (distSq(point, line[0]) < EPSILON_SQ) return 'BOUNDARY'
|
|
76
|
+
if (distSq(point, line[line.length - 1]) < EPSILON_SQ) return 'BOUNDARY'
|
|
77
|
+
// Check if on any segment
|
|
78
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
79
|
+
if (pointToSegmentDistSq(point, line[i], line[i + 1]) < EPSILON_SQ) return 'INSIDE'
|
|
80
|
+
}
|
|
81
|
+
return 'OUTSIDE'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Squared minimum distance from point p to line segment [a, b].
|
|
86
|
+
*
|
|
87
|
+
* @param {number[]} p
|
|
88
|
+
* @param {number[]} a
|
|
89
|
+
* @param {number[]} b
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
export function pointToSegmentDistSq(p, a, b) {
|
|
93
|
+
const dx = b[0] - a[0]
|
|
94
|
+
const dy = b[1] - a[1]
|
|
95
|
+
const lenSq = dx * dx + dy * dy
|
|
96
|
+
if (lenSq === 0) return distSq(p, a)
|
|
97
|
+
let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq
|
|
98
|
+
t = Math.max(0, Math.min(1, t))
|
|
99
|
+
const ddx = p[0] - a[0] - t * dx
|
|
100
|
+
const ddy = p[1] - a[1] - t * dy
|
|
101
|
+
return ddx * ddx + ddy * ddy
|
|
102
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const EPSILON = 1e-10
|
|
2
|
+
export const EPSILON_SQ = EPSILON * EPSILON
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compute the squared distance between two 2D points.
|
|
6
|
+
*
|
|
7
|
+
* @param {number[]} a
|
|
8
|
+
* @param {number[]} b
|
|
9
|
+
* @returns {number}
|
|
10
|
+
*/
|
|
11
|
+
export function distSq(a, b) {
|
|
12
|
+
const dx = a[0] - b[0]
|
|
13
|
+
const dy = a[1] - b[1]
|
|
14
|
+
return dx * dx + dy * dy
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cross product of vectors (b-a) and (c-a).
|
|
19
|
+
*
|
|
20
|
+
* @param {number[]} a
|
|
21
|
+
* @param {number[]} b
|
|
22
|
+
* @param {number[]} c
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function cross(a, b, c) {
|
|
26
|
+
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
27
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { EPSILON, EPSILON_SQ, cross, distSq } from './primitives.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test whether two line segments [p1,p2] and [p3,p4] intersect.
|
|
5
|
+
* Returns true if they share any point (including endpoints).
|
|
6
|
+
*
|
|
7
|
+
* @param {number[]} p1
|
|
8
|
+
* @param {number[]} p2
|
|
9
|
+
* @param {number[]} p3
|
|
10
|
+
* @param {number[]} p4
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function segmentsIntersect(p1, p2, p3, p4) {
|
|
14
|
+
const d1 = cross(p3, p4, p1)
|
|
15
|
+
const d2 = cross(p3, p4, p2)
|
|
16
|
+
const d3 = cross(p1, p2, p3)
|
|
17
|
+
const d4 = cross(p1, p2, p4)
|
|
18
|
+
|
|
19
|
+
if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) &&
|
|
20
|
+
(d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Math.abs(d1) < EPSILON && onSegment(p3, p4, p1)) return true
|
|
25
|
+
if (Math.abs(d2) < EPSILON && onSegment(p3, p4, p2)) return true
|
|
26
|
+
if (Math.abs(d3) < EPSILON && onSegment(p1, p2, p3)) return true
|
|
27
|
+
if (Math.abs(d4) < EPSILON && onSegment(p1, p2, p4)) return true
|
|
28
|
+
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Test if a line segment intersects a ring boundary.
|
|
34
|
+
*
|
|
35
|
+
* @param {number[]} a
|
|
36
|
+
* @param {number[]} b
|
|
37
|
+
* @param {number[][]} ring
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
export function segmentIntersectsRing(a, b, ring) {
|
|
41
|
+
for (let i = 0; i < ring.length - 1; i++) {
|
|
42
|
+
if (segmentsIntersect(a, b, ring[i], ring[i + 1])) return true
|
|
43
|
+
}
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compute intersection point of two segments (if they intersect at a single point).
|
|
49
|
+
*
|
|
50
|
+
* @param {number[]} p1
|
|
51
|
+
* @param {number[]} p2
|
|
52
|
+
* @param {number[]} p3
|
|
53
|
+
* @param {number[]} p4
|
|
54
|
+
* @returns {number[] | null}
|
|
55
|
+
*/
|
|
56
|
+
export function segmentIntersectionPoint(p1, p2, p3, p4) {
|
|
57
|
+
const d1x = p2[0] - p1[0], d1y = p2[1] - p1[1]
|
|
58
|
+
const d2x = p4[0] - p3[0], d2y = p4[1] - p3[1]
|
|
59
|
+
const denom = d1x * d2y - d1y * d2x
|
|
60
|
+
if (Math.abs(denom) < EPSILON) return null // parallel
|
|
61
|
+
const t = ((p3[0] - p1[0]) * d2y - (p3[1] - p1[1]) * d2x) / denom
|
|
62
|
+
return [p1[0] + t * d1x, p1[1] + t * d1y]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if point p lies on segment [a, b].
|
|
67
|
+
*
|
|
68
|
+
* @param {number[]} a
|
|
69
|
+
* @param {number[]} b
|
|
70
|
+
* @param {number[]} p
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function pointOnSegment(a, b, p) {
|
|
74
|
+
if (Math.abs(cross(a, b, p)) > EPSILON) return false
|
|
75
|
+
return p[0] >= Math.min(a[0], b[0]) - EPSILON &&
|
|
76
|
+
p[0] <= Math.max(a[0], b[0]) + EPSILON &&
|
|
77
|
+
p[1] >= Math.min(a[1], b[1]) - EPSILON &&
|
|
78
|
+
p[1] <= Math.max(a[1], b[1]) + EPSILON
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns the single endpoint touch point for two segments, 'INSIDE' when
|
|
83
|
+
* they intersect at a non-endpoint/proper crossing or overlap by length,
|
|
84
|
+
* and 'OUTSIDE' when they do not intersect.
|
|
85
|
+
*
|
|
86
|
+
* @param {number[]} a1
|
|
87
|
+
* @param {number[]} a2
|
|
88
|
+
* @param {number[]} b1
|
|
89
|
+
* @param {number[]} b2
|
|
90
|
+
* @returns {'INSIDE' | 'OUTSIDE' | number[]}
|
|
91
|
+
*/
|
|
92
|
+
export function segmentTouchPoint(a1, a2, b1, b2) {
|
|
93
|
+
const d1 = cross(b1, b2, a1)
|
|
94
|
+
const d2 = cross(b1, b2, a2)
|
|
95
|
+
const d3 = cross(a1, a2, b1)
|
|
96
|
+
const d4 = cross(a1, a2, b2)
|
|
97
|
+
|
|
98
|
+
if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) &&
|
|
99
|
+
(d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
|
|
100
|
+
return 'INSIDE'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @type {number[] | undefined} */
|
|
104
|
+
let point
|
|
105
|
+
let hasSecondPoint = false
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {number[]} candidate
|
|
109
|
+
*/
|
|
110
|
+
function addPoint(candidate) {
|
|
111
|
+
if (!point) {
|
|
112
|
+
point = candidate
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if (distSq(point, candidate) >= EPSILON_SQ) hasSecondPoint = true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Math.abs(d1) < EPSILON && onSegment(b1, b2, a1)) addPoint(a1)
|
|
119
|
+
if (Math.abs(d2) < EPSILON && onSegment(b1, b2, a2)) addPoint(a2)
|
|
120
|
+
if (Math.abs(d3) < EPSILON && onSegment(a1, a2, b1)) addPoint(b1)
|
|
121
|
+
if (Math.abs(d4) < EPSILON && onSegment(a1, a2, b2)) addPoint(b2)
|
|
122
|
+
|
|
123
|
+
if (!point) return 'OUTSIDE'
|
|
124
|
+
|
|
125
|
+
return hasSecondPoint ? 'INSIDE' : point
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if point c lies on segment [a, b], assuming collinearity.
|
|
130
|
+
*
|
|
131
|
+
* @param {number[]} a
|
|
132
|
+
* @param {number[]} b
|
|
133
|
+
* @param {number[]} c
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
function onSegment(a, b, c) {
|
|
137
|
+
return Math.min(a[0], b[0]) - c[0] <= EPSILON && c[0] - Math.max(a[0], b[0]) <= EPSILON &&
|
|
138
|
+
Math.min(a[1], b[1]) - c[1] <= EPSILON && c[1] - Math.max(a[1], b[1]) <= EPSILON
|
|
139
|
+
}
|