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.
@@ -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
+ }