squirreling 0.9.3 → 0.9.5
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 +1 -1
- package/package.json +2 -2
- package/src/expression/date.js +60 -0
- package/src/expression/evaluate.js +10 -2
- package/src/parse/expression.js +42 -1
- package/src/spatial/bbox.js +53 -0
- package/src/spatial/equality.js +98 -0
- package/src/spatial/geometry.d.ts +78 -0
- package/src/spatial/operations.js +371 -0
- package/src/spatial/pointRelations.js +102 -0
- package/src/spatial/primitives.js +27 -0
- package/src/spatial/segments.js +139 -0
- package/src/spatial/spatial.js +367 -0
- package/src/spatial/wkt.js +222 -0
- package/src/validation.js +24 -1
- package/src/expression/spatial.js +0 -1471
|
@@ -0,0 +1,371 @@
|
|
|
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'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @import { Relation, SimpleGeometry } from './geometry.js'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Test if a linestring intersects a polygon.
|
|
12
|
+
*
|
|
13
|
+
* @param {number[][]} line
|
|
14
|
+
* @param {number[][][]} rings
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function lineIntersectsPolygon(line, rings) {
|
|
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.
|
|
23
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
24
|
+
for (const ring of rings) {
|
|
25
|
+
if (segmentIntersectsRing(line[i], line[i + 1], ring)) return true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Test if two linestrings share any point (intersect).
|
|
33
|
+
*
|
|
34
|
+
* @param {number[][]} line1
|
|
35
|
+
* @param {number[][]} line2
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
function linesIntersect(line1, line2) {
|
|
39
|
+
for (let i = 0; i < line1.length - 1; i++) {
|
|
40
|
+
for (let j = 0; j < line2.length - 1; j++) {
|
|
41
|
+
if (segmentsIntersect(line1[i], line1[i + 1], line2[j], line2[j + 1])) return true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
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
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Classify containment of a linestring within a polygon.
|
|
58
|
+
* Returns 'INSIDE' if entirely in interior, 'BOUNDARY' if inside but
|
|
59
|
+
* touches boundary, 'OUTSIDE' if any part is outside.
|
|
60
|
+
*
|
|
61
|
+
* @param {number[][]} line
|
|
62
|
+
* @param {number[][][]} rings
|
|
63
|
+
* @returns {Relation}
|
|
64
|
+
*/
|
|
65
|
+
function polygonContainsLine(line, rings) {
|
|
66
|
+
/** @type {Relation} */
|
|
67
|
+
let result = 'INSIDE'
|
|
68
|
+
for (const pt of line) {
|
|
69
|
+
const rel = pointInPolygon(pt, rings)
|
|
70
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
71
|
+
if (rel === 'BOUNDARY') result = 'BOUNDARY'
|
|
72
|
+
}
|
|
73
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
74
|
+
const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
|
|
75
|
+
const rel = pointInPolygon(mid, rings)
|
|
76
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
77
|
+
if (rel === 'BOUNDARY') result = 'BOUNDARY'
|
|
78
|
+
}
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Classify containment of polygon B within polygon A.
|
|
84
|
+
* Returns 'INSIDE' if entirely in interior, 'BOUNDARY' if inside but
|
|
85
|
+
* touches boundary, 'OUTSIDE' if any part is outside.
|
|
86
|
+
*
|
|
87
|
+
* @param {number[][][]} ringsA
|
|
88
|
+
* @param {number[][][]} ringsB
|
|
89
|
+
* @returns {Relation}
|
|
90
|
+
*/
|
|
91
|
+
function polygonContainsPolygon(ringsA, ringsB) {
|
|
92
|
+
/** @type {Relation} */
|
|
93
|
+
let result = 'INSIDE'
|
|
94
|
+
for (const pt of ringsB[0]) {
|
|
95
|
+
const rel = pointInPolygon(pt, ringsA)
|
|
96
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
97
|
+
if (rel === 'BOUNDARY') result = 'BOUNDARY'
|
|
98
|
+
}
|
|
99
|
+
for (let i = 0; i < ringsB[0].length - 1; i++) {
|
|
100
|
+
const mid = [(ringsB[0][i][0] + ringsB[0][i + 1][0]) / 2, (ringsB[0][i][1] + ringsB[0][i + 1][1]) / 2]
|
|
101
|
+
const rel = pointInPolygon(mid, ringsA)
|
|
102
|
+
if (rel === 'OUTSIDE') return 'OUTSIDE'
|
|
103
|
+
if (rel === 'BOUNDARY') result = 'BOUNDARY'
|
|
104
|
+
}
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Classify the relationship between two linestrings.
|
|
110
|
+
* Returns INSIDE if interiors share a point, BOUNDARY if they only meet
|
|
111
|
+
* at endpoints, OUTSIDE if disjoint.
|
|
112
|
+
*
|
|
113
|
+
* @param {number[][]} line1
|
|
114
|
+
* @param {number[][]} line2
|
|
115
|
+
* @returns {Relation}
|
|
116
|
+
*/
|
|
117
|
+
function lineLineRelation(line1, line2) {
|
|
118
|
+
let boundary = false
|
|
119
|
+
for (let i = 0; i < line1.length - 1; i++) {
|
|
120
|
+
for (let j = 0; j < line2.length - 1; j++) {
|
|
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'
|
|
125
|
+
boundary = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return boundary ? 'BOUNDARY' : 'OUTSIDE'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Classify the relationship between a linestring and a polygon.
|
|
133
|
+
* Returns INSIDE if line interior enters polygon interior, BOUNDARY if
|
|
134
|
+
* they only share boundary points, OUTSIDE if disjoint.
|
|
135
|
+
*
|
|
136
|
+
* @param {number[][]} line
|
|
137
|
+
* @param {number[][][]} rings
|
|
138
|
+
* @returns {Relation}
|
|
139
|
+
*/
|
|
140
|
+
function linePolygonRelation(line, rings) {
|
|
141
|
+
let boundary = false
|
|
142
|
+
// Check segment midpoints and interior vertices
|
|
143
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
144
|
+
const mid = [(line[i][0] + line[i + 1][0]) / 2, (line[i][1] + line[i + 1][1]) / 2]
|
|
145
|
+
const midRel = pointInPolygon(mid, rings)
|
|
146
|
+
if (midRel === 'INSIDE') return 'INSIDE'
|
|
147
|
+
if (midRel === 'BOUNDARY') boundary = true
|
|
148
|
+
}
|
|
149
|
+
// Check interior vertices of the line
|
|
150
|
+
for (let i = 1; i < line.length - 1; i++) {
|
|
151
|
+
const rel = pointInPolygon(line[i], rings)
|
|
152
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
153
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
154
|
+
}
|
|
155
|
+
// Check line endpoints
|
|
156
|
+
for (const pt of [line[0], line[line.length - 1]]) {
|
|
157
|
+
const rel = pointInPolygon(pt, rings)
|
|
158
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
159
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
160
|
+
}
|
|
161
|
+
// Check if any edge of the polygon rings intersects the line
|
|
162
|
+
if (!boundary) {
|
|
163
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
164
|
+
for (const ring of rings) {
|
|
165
|
+
if (segmentIntersectsRing(line[i], line[i + 1], ring)) {
|
|
166
|
+
boundary = true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return boundary ? 'BOUNDARY' : 'OUTSIDE'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Classify the relationship between two polygons.
|
|
176
|
+
* Returns INSIDE if interiors share area, BOUNDARY if they only share
|
|
177
|
+
* boundary points/edges, OUTSIDE if disjoint.
|
|
178
|
+
*
|
|
179
|
+
* @param {number[][][]} rings1
|
|
180
|
+
* @param {number[][][]} rings2
|
|
181
|
+
* @returns {Relation}
|
|
182
|
+
*/
|
|
183
|
+
function polygonPolygonRelation(rings1, rings2) {
|
|
184
|
+
let boundary = false
|
|
185
|
+
// Check vertices of polygon1 against polygon2
|
|
186
|
+
for (const pt of rings1[0]) {
|
|
187
|
+
const rel = pointInPolygon(pt, rings2)
|
|
188
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
189
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
190
|
+
}
|
|
191
|
+
// Check vertices of polygon2 against polygon1
|
|
192
|
+
for (const pt of rings2[0]) {
|
|
193
|
+
const rel = pointInPolygon(pt, rings1)
|
|
194
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
195
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
196
|
+
}
|
|
197
|
+
// Check edge midpoints of polygon1 against polygon2
|
|
198
|
+
for (let i = 0; i < rings1[0].length - 1; i++) {
|
|
199
|
+
const mid = [(rings1[0][i][0] + rings1[0][i + 1][0]) / 2, (rings1[0][i][1] + rings1[0][i + 1][1]) / 2]
|
|
200
|
+
const rel = pointInPolygon(mid, rings2)
|
|
201
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
202
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
203
|
+
}
|
|
204
|
+
// Check edge midpoints of polygon2 against polygon1
|
|
205
|
+
for (let i = 0; i < rings2[0].length - 1; i++) {
|
|
206
|
+
const mid = [(rings2[0][i][0] + rings2[0][i + 1][0]) / 2, (rings2[0][i][1] + rings2[0][i + 1][1]) / 2]
|
|
207
|
+
const rel = pointInPolygon(mid, rings1)
|
|
208
|
+
if (rel === 'INSIDE') return 'INSIDE'
|
|
209
|
+
if (rel === 'BOUNDARY') boundary = true
|
|
210
|
+
}
|
|
211
|
+
// Check edge-edge intersections
|
|
212
|
+
if (!boundary) {
|
|
213
|
+
for (let i = 0; i < rings1[0].length - 1; i++) {
|
|
214
|
+
for (let j = 0; j < rings2[0].length - 1; j++) {
|
|
215
|
+
if (segmentsIntersect(rings1[0][i], rings1[0][i + 1], rings2[0][j], rings2[0][j + 1])) {
|
|
216
|
+
boundary = true
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return boundary ? 'BOUNDARY' : 'OUTSIDE'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {SimpleGeometry[]} partsA
|
|
226
|
+
* @param {SimpleGeometry[]} partsB
|
|
227
|
+
* @returns {boolean}
|
|
228
|
+
*/
|
|
229
|
+
export function intersects(partsA, partsB) {
|
|
230
|
+
for (const pa of partsA) {
|
|
231
|
+
for (const pb of partsB) {
|
|
232
|
+
if (pairIntersects(pa, pb)) return true
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {SimpleGeometry} a
|
|
240
|
+
* @param {SimpleGeometry} b
|
|
241
|
+
* @returns {boolean}
|
|
242
|
+
*/
|
|
243
|
+
function pairIntersects(a, b) {
|
|
244
|
+
if (!bboxOverlap(a, b)) return false
|
|
245
|
+
const ta = a.type
|
|
246
|
+
const tb = b.type
|
|
247
|
+
|
|
248
|
+
if (ta === 'Point' && tb === 'Point') {
|
|
249
|
+
return distSq(a.coordinates, b.coordinates) < EPSILON_SQ
|
|
250
|
+
}
|
|
251
|
+
if (ta === 'Point' && tb === 'LineString') {
|
|
252
|
+
return pointOnLine(a.coordinates, b.coordinates)
|
|
253
|
+
}
|
|
254
|
+
if (ta === 'LineString' && tb === 'Point') {
|
|
255
|
+
return pointOnLine(b.coordinates, a.coordinates)
|
|
256
|
+
}
|
|
257
|
+
if (ta === 'Point' && tb === 'Polygon') {
|
|
258
|
+
return pointInPolygon(a.coordinates, b.coordinates) !== 'OUTSIDE'
|
|
259
|
+
}
|
|
260
|
+
if (ta === 'Polygon' && tb === 'Point') {
|
|
261
|
+
return pointInPolygon(b.coordinates, a.coordinates) !== 'OUTSIDE'
|
|
262
|
+
}
|
|
263
|
+
if (ta === 'LineString' && tb === 'LineString') {
|
|
264
|
+
return linesIntersect(a.coordinates, b.coordinates)
|
|
265
|
+
}
|
|
266
|
+
if (ta === 'LineString' && tb === 'Polygon') {
|
|
267
|
+
return lineIntersectsPolygon(a.coordinates, b.coordinates)
|
|
268
|
+
}
|
|
269
|
+
if (ta === 'Polygon' && tb === 'LineString') {
|
|
270
|
+
return lineIntersectsPolygon(b.coordinates, a.coordinates)
|
|
271
|
+
}
|
|
272
|
+
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
273
|
+
return polygonPolygonRelation(a.coordinates, b.coordinates) !== 'OUTSIDE'
|
|
274
|
+
}
|
|
275
|
+
return false
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Classify the relationship between two simple geometries.
|
|
280
|
+
* Returns 'INSIDE' if interiors intersect, 'BOUNDARY' if they
|
|
281
|
+
* intersect only at boundaries, or 'OUTSIDE' if they don't intersect.
|
|
282
|
+
*
|
|
283
|
+
* @param {SimpleGeometry} a
|
|
284
|
+
* @param {SimpleGeometry} b
|
|
285
|
+
* @returns {Relation}
|
|
286
|
+
*/
|
|
287
|
+
export function pairRelation(a, b) {
|
|
288
|
+
if (!bboxOverlap(a, b)) return 'OUTSIDE'
|
|
289
|
+
const ta = a.type
|
|
290
|
+
const tb = b.type
|
|
291
|
+
|
|
292
|
+
// Point / Point
|
|
293
|
+
if (ta === 'Point' && tb === 'Point') {
|
|
294
|
+
return distSq(a.coordinates, b.coordinates) < EPSILON_SQ ? 'INSIDE' : 'OUTSIDE'
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Point / LineString
|
|
298
|
+
if (ta === 'Point' && tb === 'LineString') {
|
|
299
|
+
return pointLineRelation(a.coordinates, b.coordinates)
|
|
300
|
+
}
|
|
301
|
+
if (ta === 'LineString' && tb === 'Point') {
|
|
302
|
+
return pointLineRelation(b.coordinates, a.coordinates)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Point / Polygon
|
|
306
|
+
if (ta === 'Point' && tb === 'Polygon') {
|
|
307
|
+
return pointInPolygon(a.coordinates, b.coordinates)
|
|
308
|
+
}
|
|
309
|
+
if (ta === 'Polygon' && tb === 'Point') {
|
|
310
|
+
return pointInPolygon(b.coordinates, a.coordinates)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// LineString / LineString
|
|
314
|
+
if (ta === 'LineString' && tb === 'LineString') {
|
|
315
|
+
return lineLineRelation(a.coordinates, b.coordinates)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// LineString / Polygon
|
|
319
|
+
if (ta === 'LineString' && tb === 'Polygon') {
|
|
320
|
+
return linePolygonRelation(a.coordinates, b.coordinates)
|
|
321
|
+
}
|
|
322
|
+
if (ta === 'Polygon' && tb === 'LineString') {
|
|
323
|
+
return linePolygonRelation(b.coordinates, a.coordinates)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Polygon / Polygon
|
|
327
|
+
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
328
|
+
return polygonPolygonRelation(a.coordinates, b.coordinates)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return 'OUTSIDE'
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Classify containment of b within a.
|
|
336
|
+
* Returns 'INSIDE' if b is strictly in a's interior, 'BOUNDARY' if b is
|
|
337
|
+
* inside a but touches a's boundary, 'OUTSIDE' if any part of b is outside a.
|
|
338
|
+
*
|
|
339
|
+
* @param {SimpleGeometry} a
|
|
340
|
+
* @param {SimpleGeometry} b
|
|
341
|
+
* @returns {Relation}
|
|
342
|
+
*/
|
|
343
|
+
export function pairContainment(a, b) {
|
|
344
|
+
if (!bboxOverlap(a, b)) return 'OUTSIDE'
|
|
345
|
+
const ta = a.type
|
|
346
|
+
const tb = b.type
|
|
347
|
+
|
|
348
|
+
if (ta === 'Point' && tb === 'Point') {
|
|
349
|
+
return distSq(a.coordinates, b.coordinates) < EPSILON_SQ ? 'BOUNDARY' : 'OUTSIDE'
|
|
350
|
+
}
|
|
351
|
+
if (ta === 'LineString' && tb === 'Point') {
|
|
352
|
+
return pointLineRelation(b.coordinates, a.coordinates)
|
|
353
|
+
}
|
|
354
|
+
if (ta === 'Polygon' && tb === 'Point') {
|
|
355
|
+
return pointInPolygon(b.coordinates, a.coordinates)
|
|
356
|
+
}
|
|
357
|
+
if (ta === 'Polygon' && tb === 'LineString') {
|
|
358
|
+
return polygonContainsLine(b.coordinates, a.coordinates)
|
|
359
|
+
}
|
|
360
|
+
if (ta === 'Polygon' && tb === 'Polygon') {
|
|
361
|
+
return polygonContainsPolygon(a.coordinates, b.coordinates)
|
|
362
|
+
}
|
|
363
|
+
if (ta === 'LineString' && tb === 'LineString') {
|
|
364
|
+
// Line A contains line B if every point of B is on A
|
|
365
|
+
for (const pt of b.coordinates) {
|
|
366
|
+
if (!pointOnLine(pt, a.coordinates)) return 'OUTSIDE'
|
|
367
|
+
}
|
|
368
|
+
return 'BOUNDARY'
|
|
369
|
+
}
|
|
370
|
+
return 'OUTSIDE'
|
|
371
|
+
}
|
|
@@ -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
|
+
}
|