squirreling 0.9.4 → 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/{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/validation.js +24 -1
- /package/src/{expression → spatial}/wkt.js +0 -0
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
147
147
|
- String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`
|
|
148
148
|
- Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
|
|
149
149
|
- Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
150
|
-
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
|
|
150
|
+
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_PART`, `DATE_TRUNC`, `EXTRACT`, `INTERVAL`
|
|
151
151
|
- Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
|
|
152
152
|
- Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_SORT`, `CARDINALITY`
|
|
153
153
|
- Regex: `REGEXP_SUBSTR`, `REGEXP_REPLACE`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "25.3.
|
|
40
|
+
"@types/node": "25.3.3",
|
|
41
41
|
"@vitest/coverage-v8": "4.0.18",
|
|
42
42
|
"eslint": "9.39.2",
|
|
43
43
|
"eslint-plugin-jsdoc": "62.7.1",
|
package/src/expression/date.js
CHANGED
|
@@ -40,6 +40,66 @@ export function applyIntervalToDate(dateVal, value, unit, op) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Truncate a date to the given precision
|
|
45
|
+
* @param {SqlPrimitive} precision - the unit to truncate to (year, month, day, hour, minute, second)
|
|
46
|
+
* @param {SqlPrimitive} dateVal - the date value to truncate
|
|
47
|
+
* @returns {Date | string | null}
|
|
48
|
+
*/
|
|
49
|
+
export function dateTrunc(precision, dateVal) {
|
|
50
|
+
if (precision == null || dateVal == null) return null
|
|
51
|
+
const date = toDate(dateVal)
|
|
52
|
+
if (date == null) return null
|
|
53
|
+
|
|
54
|
+
const unit = String(precision).toUpperCase()
|
|
55
|
+
if (unit === 'YEAR') {
|
|
56
|
+
date.setUTCMonth(0, 1)
|
|
57
|
+
date.setUTCHours(0, 0, 0, 0)
|
|
58
|
+
} else if (unit === 'MONTH') {
|
|
59
|
+
date.setUTCDate(1)
|
|
60
|
+
date.setUTCHours(0, 0, 0, 0)
|
|
61
|
+
} else if (unit === 'DAY') {
|
|
62
|
+
date.setUTCHours(0, 0, 0, 0)
|
|
63
|
+
} else if (unit === 'HOUR') {
|
|
64
|
+
date.setUTCMinutes(0, 0, 0)
|
|
65
|
+
} else if (unit === 'MINUTE') {
|
|
66
|
+
date.setUTCSeconds(0, 0)
|
|
67
|
+
} else if (unit === 'SECOND') {
|
|
68
|
+
date.setUTCMilliseconds(0)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Return in same format as input
|
|
72
|
+
if (dateVal instanceof Date) return date
|
|
73
|
+
if (String(dateVal).includes('T')) {
|
|
74
|
+
return date.toISOString()
|
|
75
|
+
} else {
|
|
76
|
+
return date.toISOString().split('T')[0]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract a field from a date value
|
|
82
|
+
* @param {SqlPrimitive} field - the field to extract (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW, EPOCH)
|
|
83
|
+
* @param {SqlPrimitive} dateVal - the date value to extract from
|
|
84
|
+
* @returns {number | null}
|
|
85
|
+
*/
|
|
86
|
+
export function extractField(field, dateVal) {
|
|
87
|
+
if (field == null || dateVal == null) return null
|
|
88
|
+
const date = toDate(dateVal)
|
|
89
|
+
if (date == null) return null
|
|
90
|
+
|
|
91
|
+
const unit = String(field).toUpperCase()
|
|
92
|
+
if (unit === 'YEAR') return date.getUTCFullYear()
|
|
93
|
+
if (unit === 'MONTH') return date.getUTCMonth() + 1
|
|
94
|
+
if (unit === 'DAY') return date.getUTCDate()
|
|
95
|
+
if (unit === 'HOUR') return date.getUTCHours()
|
|
96
|
+
if (unit === 'MINUTE') return date.getUTCMinutes()
|
|
97
|
+
if (unit === 'SECOND') return date.getUTCSeconds()
|
|
98
|
+
if (unit === 'DOW') return date.getUTCDay()
|
|
99
|
+
if (unit === 'EPOCH') return date.getTime() / 1000
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
43
103
|
/**
|
|
44
104
|
* @param {SqlPrimitive} val
|
|
45
105
|
* @returns {Date | null}
|
|
@@ -6,10 +6,10 @@ import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc
|
|
|
6
6
|
import { aggregateError, argValueError, castError } from '../validationErrors.js'
|
|
7
7
|
import { derivedAlias } from './alias.js'
|
|
8
8
|
import { applyBinaryOp } from './binary.js'
|
|
9
|
-
import { applyIntervalToDate } from './date.js'
|
|
9
|
+
import { applyIntervalToDate, dateTrunc, extractField } from './date.js'
|
|
10
10
|
import { evaluateMathFunc } from './math.js'
|
|
11
11
|
import { evaluateRegexpFunc } from './regexp.js'
|
|
12
|
-
import { evaluateSpatialFunc } from '
|
|
12
|
+
import { evaluateSpatialFunc } from '../spatial/spatial.js'
|
|
13
13
|
import { evaluateStringFunc } from './strings.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -271,6 +271,14 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
271
271
|
return val1 == val2 ? null : val1
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
if (funcName === 'DATE_TRUNC') {
|
|
275
|
+
return dateTrunc(args[0], args[1])
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (funcName === 'EXTRACT' || funcName === 'DATE_PART') {
|
|
279
|
+
return extractField(args[0], args[1])
|
|
280
|
+
}
|
|
281
|
+
|
|
274
282
|
if (funcName === 'CURRENT_DATE') {
|
|
275
283
|
return new Date().toISOString().split('T')[0]
|
|
276
284
|
}
|
package/src/parse/expression.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
syntaxError,
|
|
5
5
|
unknownFunctionError,
|
|
6
6
|
} from '../parseErrors.js'
|
|
7
|
-
import { isIntervalUnit, isKnownFunction } from '../validation.js'
|
|
7
|
+
import { RESERVED_KEYWORDS, isExtractField, isIntervalUnit, isKnownFunction } from '../validation.js'
|
|
8
8
|
import { parseComparison } from './comparison.js'
|
|
9
9
|
import { parseFunctionCall } from './functions.js'
|
|
10
10
|
import { parseSelectInternal } from './parse.js'
|
|
@@ -70,6 +70,36 @@ export function parsePrimary(state) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// EXTRACT(field FROM expr)
|
|
74
|
+
if (tok.value === 'EXTRACT' && next.type === 'paren' && next.value === '(') {
|
|
75
|
+
consume(state) // EXTRACT
|
|
76
|
+
consume(state) // '('
|
|
77
|
+
const fieldTok = current(state)
|
|
78
|
+
const isValidType = fieldTok.type === 'keyword' || fieldTok.type === 'identifier'
|
|
79
|
+
if (!isValidType || !isExtractField(fieldTok.value)) {
|
|
80
|
+
throw syntaxError({
|
|
81
|
+
expected: 'extract field (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW, EPOCH)',
|
|
82
|
+
received: `"${fieldTok.value}"`,
|
|
83
|
+
positionStart: fieldTok.positionStart,
|
|
84
|
+
positionEnd: fieldTok.positionEnd,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
consume(state) // field
|
|
88
|
+
expect(state, 'keyword', 'FROM')
|
|
89
|
+
const expr = parseExpression(state)
|
|
90
|
+
expect(state, 'paren', ')')
|
|
91
|
+
return {
|
|
92
|
+
type: 'function',
|
|
93
|
+
name: 'EXTRACT',
|
|
94
|
+
args: [
|
|
95
|
+
{ type: 'literal', value: fieldTok.value, positionStart: fieldTok.positionStart, positionEnd: fieldTok.positionEnd },
|
|
96
|
+
expr,
|
|
97
|
+
],
|
|
98
|
+
positionStart,
|
|
99
|
+
positionEnd: state.lastPos,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
73
103
|
// function call
|
|
74
104
|
if (next.type === 'paren' && next.value === '(') {
|
|
75
105
|
const funcName = tok.value
|
|
@@ -220,6 +250,17 @@ export function parsePrimary(state) {
|
|
|
220
250
|
if (tok.value === 'INTERVAL') {
|
|
221
251
|
return parseInterval(state)
|
|
222
252
|
}
|
|
253
|
+
|
|
254
|
+
// Non-reserved keywords can be used as identifiers (e.g. column aliases)
|
|
255
|
+
if (!RESERVED_KEYWORDS.has(tok.value)) {
|
|
256
|
+
consume(state)
|
|
257
|
+
return {
|
|
258
|
+
type: 'identifier',
|
|
259
|
+
name: tok.originalValue ?? tok.value,
|
|
260
|
+
positionStart,
|
|
261
|
+
positionEnd: state.lastPos,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
223
264
|
}
|
|
224
265
|
|
|
225
266
|
if (tok.type === 'operator' && tok.value === '-') {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BBox, SimpleGeometry } from './geometry.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const EPSILON = 1e-10
|
|
6
|
+
export const EPSILON_SQ = EPSILON * EPSILON
|
|
7
|
+
|
|
8
|
+
/** @type {WeakMap<SimpleGeometry, BBox>} */
|
|
9
|
+
const bboxCache = new WeakMap()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Test whether two bounding boxes overlap.
|
|
13
|
+
*
|
|
14
|
+
* @param {SimpleGeometry} a
|
|
15
|
+
* @param {SimpleGeometry} b
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
export function bboxOverlap(a, b) {
|
|
19
|
+
const aBox = bbox(a)
|
|
20
|
+
const bBox = bbox(b)
|
|
21
|
+
return aBox.minX <= bBox.maxX && aBox.maxX >= bBox.minX && aBox.minY <= bBox.maxY && aBox.maxY >= bBox.minY
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compute the axis-aligned bounding box of a simple geometry.
|
|
26
|
+
* Results are cached per geometry object.
|
|
27
|
+
*
|
|
28
|
+
* @param {SimpleGeometry} geom
|
|
29
|
+
* @returns {BBox}
|
|
30
|
+
*/
|
|
31
|
+
function bbox(geom) {
|
|
32
|
+
let b = bboxCache.get(geom)
|
|
33
|
+
if (b) return b
|
|
34
|
+
if (geom.type === 'Point') {
|
|
35
|
+
const [x, y] = geom.coordinates
|
|
36
|
+
b = { minX: x, minY: y, maxX: x, maxY: y }
|
|
37
|
+
} else {
|
|
38
|
+
/** @type {number[][]} */
|
|
39
|
+
const points = geom.type === 'LineString'
|
|
40
|
+
? geom.coordinates
|
|
41
|
+
: geom.coordinates[0] // outer ring
|
|
42
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
|
43
|
+
for (const p of points) {
|
|
44
|
+
if (p[0] < minX) minX = p[0]
|
|
45
|
+
if (p[1] < minY) minY = p[1]
|
|
46
|
+
if (p[0] > maxX) maxX = p[0]
|
|
47
|
+
if (p[1] > maxY) maxY = p[1]
|
|
48
|
+
}
|
|
49
|
+
b = { minX, minY, maxX, maxY }
|
|
50
|
+
}
|
|
51
|
+
bboxCache.set(geom, b)
|
|
52
|
+
return b
|
|
53
|
+
}
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* @import { SimpleGeometry } from './geometry.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { EPSILON, EPSILON_SQ, distSq } from './
|
|
5
|
+
import { EPSILON, EPSILON_SQ, distSq } from './primitives.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {SimpleGeometry} a
|
|
9
9
|
* @param {SimpleGeometry} b
|
|
10
10
|
* @returns {boolean}
|
|
11
11
|
*/
|
|
12
|
-
export function
|
|
12
|
+
export function geometryEqual(a, b) {
|
|
13
13
|
if (a.type === 'Point' && b.type === 'Point') {
|
|
14
14
|
return distSq(a.coordinates, b.coordinates) < EPSILON_SQ
|
|
15
15
|
} else if (a.type === 'LineString' && b.type === 'LineString') {
|
|
@@ -25,7 +25,7 @@ export function simpleGeomEqual(a, b) {
|
|
|
25
25
|
* @param {number[][]} b
|
|
26
26
|
* @returns {boolean}
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
function lineEqual(a, b) {
|
|
29
29
|
if (a.length !== b.length) return false
|
|
30
30
|
// Forward
|
|
31
31
|
let forward = true
|
|
@@ -50,7 +50,7 @@ export function lineEqual(a, b) {
|
|
|
50
50
|
* @param {number[][][]} b
|
|
51
51
|
* @returns {boolean}
|
|
52
52
|
*/
|
|
53
|
-
|
|
53
|
+
function polygonEqual(a, b) {
|
|
54
54
|
if (a.length !== b.length) return false
|
|
55
55
|
for (let i = 0; i < a.length; i++) {
|
|
56
56
|
if (!ringsEqual(a[i], b[i])) return false
|
|
@@ -65,7 +65,7 @@ export function polygonEqual(a, b) {
|
|
|
65
65
|
* @param {number[][]} ring2
|
|
66
66
|
* @returns {boolean}
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
function ringsEqual(ring1, ring2) {
|
|
69
69
|
if (ring1.length !== ring2.length) return false
|
|
70
70
|
// Try every rotation
|
|
71
71
|
const n = ring1.length - 1 // closed ring, last = first
|
|
@@ -15,6 +15,13 @@ export type Geometry =
|
|
|
15
15
|
*/
|
|
16
16
|
export type SimpleGeometry = Point | LineString | Polygon
|
|
17
17
|
|
|
18
|
+
export interface BBox {
|
|
19
|
+
minX: number
|
|
20
|
+
minY: number
|
|
21
|
+
maxX: number
|
|
22
|
+
maxY: number
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
/**
|
|
19
26
|
* Boundary relationship between two geometries.
|
|
20
27
|
*/
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { geometryEqual } from './equality.js'
|
|
2
|
+
import { intersects, pairContainment, pairRelation } from './operations.js'
|
|
3
|
+
import { pointInPolygon, pointLineRelation, pointToSegmentDistSq } from './pointRelations.js'
|
|
4
|
+
import { distSq } from './primitives.js'
|
|
1
5
|
import { geomToWkt, parseWkt } from './wkt.js'
|
|
2
|
-
import { distSq, pointInPolygon, pointLineRelation, pointToSegmentDistSq, simpleIntersects, simplePairContainment, simplePairRelation } from './spatial.geometry.js'
|
|
3
|
-
import { simpleGeomEqual } from './spatial.equality.js'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* @import { SpatialFunc, SqlPrimitive } from '../types.js'
|
|
@@ -47,7 +49,7 @@ export function evaluateSpatialFunc({ funcName, args }) {
|
|
|
47
49
|
const b = decompose(geomB)
|
|
48
50
|
|
|
49
51
|
switch (funcName) {
|
|
50
|
-
case 'ST_INTERSECTS': return
|
|
52
|
+
case 'ST_INTERSECTS': return intersects(a, b)
|
|
51
53
|
case 'ST_CONTAINS': return stContains(a, b)
|
|
52
54
|
case 'ST_CONTAINSPROPERLY': return stContainsProperly(a, b)
|
|
53
55
|
case 'ST_WITHIN': return stContains(b, a) // inverse of contains
|
|
@@ -135,7 +137,7 @@ function getSegments(geoms) {
|
|
|
135
137
|
*/
|
|
136
138
|
function stDWithin(a, b, distance) {
|
|
137
139
|
if (distance < 0) return false
|
|
138
|
-
if (
|
|
140
|
+
if (intersects(a, b)) return true
|
|
139
141
|
|
|
140
142
|
const distanceSq = distance * distance
|
|
141
143
|
const { points: ptsA, segments: segsA } = getSegments(a)
|
|
@@ -199,7 +201,7 @@ function decompose(geom) {
|
|
|
199
201
|
*/
|
|
200
202
|
function stContains(a, b) {
|
|
201
203
|
// Every part of b must be inside some part of a
|
|
202
|
-
return b.every(pb => a.some(pa =>
|
|
204
|
+
return b.every(pb => a.some(pa => pairContainment(pa, pb) !== 'OUTSIDE'))
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
// ============================================================================
|
|
@@ -213,7 +215,7 @@ function stContains(a, b) {
|
|
|
213
215
|
*/
|
|
214
216
|
function stContainsProperly(a, b) {
|
|
215
217
|
// Every part of b must be strictly inside some part of a
|
|
216
|
-
return b.every(pb => a.some(pa =>
|
|
218
|
+
return b.every(pb => a.some(pa => pairContainment(pa, pb) === 'INSIDE'))
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
// ============================================================================
|
|
@@ -229,7 +231,7 @@ function stTouches(a, b) {
|
|
|
229
231
|
let intersects = false
|
|
230
232
|
for (const pa of a) {
|
|
231
233
|
for (const pb of b) {
|
|
232
|
-
const rel =
|
|
234
|
+
const rel = pairRelation(pa, pb)
|
|
233
235
|
if (rel === 'INSIDE') return false
|
|
234
236
|
if (rel === 'BOUNDARY') intersects = true
|
|
235
237
|
}
|
|
@@ -252,7 +254,7 @@ function stOverlaps(a, b) {
|
|
|
252
254
|
const dimA = geometryDimension(a)
|
|
253
255
|
const dimB = geometryDimension(b)
|
|
254
256
|
if (dimA !== dimB) return false
|
|
255
|
-
if (!
|
|
257
|
+
if (!intersects(a, b)) return false
|
|
256
258
|
if (stEquals(a, b)) return false
|
|
257
259
|
// Must not be containment
|
|
258
260
|
if (stContains(a, b) || stContains(b, a)) return false
|
|
@@ -297,7 +299,7 @@ function stEquals(a, b) {
|
|
|
297
299
|
let found = false
|
|
298
300
|
for (let i = 0; i < b.length; i++) {
|
|
299
301
|
if (used.has(i)) continue
|
|
300
|
-
if (
|
|
302
|
+
if (geometryEqual(pa, b[i])) {
|
|
301
303
|
used.add(i)
|
|
302
304
|
found = true
|
|
303
305
|
break
|
|
@@ -323,7 +325,7 @@ function stCrosses(a, b) {
|
|
|
323
325
|
const dimA = geometryDimension(a)
|
|
324
326
|
const dimB = geometryDimension(b)
|
|
325
327
|
|
|
326
|
-
if (!
|
|
328
|
+
if (!intersects(a, b)) return false
|
|
327
329
|
|
|
328
330
|
// Point/Point or Polygon/Polygon cannot cross
|
|
329
331
|
if (dimA === dimB && dimA !== 1) return false
|
package/src/validation.js
CHANGED
|
@@ -112,6 +112,14 @@ export function isIntervalUnit(name) {
|
|
|
112
112
|
return ['DAY', 'MONTH', 'YEAR', 'HOUR', 'MINUTE', 'SECOND'].includes(name)
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} name
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
export function isExtractField(name) {
|
|
120
|
+
return ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'DOW', 'EPOCH'].includes(name)
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
/**
|
|
116
124
|
* @param {string} name
|
|
117
125
|
* @returns {name is StringFunc}
|
|
@@ -159,6 +167,9 @@ export const FUNCTION_ARG_COUNTS = {
|
|
|
159
167
|
CURRENT_DATE: { min: 0, max: 0 },
|
|
160
168
|
CURRENT_TIME: { min: 0, max: 0 },
|
|
161
169
|
CURRENT_TIMESTAMP: { min: 0, max: 0 },
|
|
170
|
+
DATE_TRUNC: { min: 2, max: 2 },
|
|
171
|
+
DATE_PART: { min: 2, max: 2 },
|
|
172
|
+
EXTRACT: { min: 2, max: 2 },
|
|
162
173
|
|
|
163
174
|
// Math functions
|
|
164
175
|
FLOOR: { min: 1, max: 1 },
|
|
@@ -292,7 +303,7 @@ export function isKnownFunction(funcName, functions) {
|
|
|
292
303
|
|
|
293
304
|
// Date/time, JSON, conditional, and CAST functions
|
|
294
305
|
if ([
|
|
295
|
-
'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
|
|
306
|
+
'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DATE_TRUNC', 'DATE_PART', 'EXTRACT',
|
|
296
307
|
'JSON_VALUE', 'JSON_QUERY', 'JSON_OBJECT',
|
|
297
308
|
'ARRAY_LENGTH', 'ARRAY_POSITION', 'ARRAY_SORT', 'CARDINALITY',
|
|
298
309
|
'COALESCE', 'NULLIF', 'CAST',
|
|
@@ -308,6 +319,18 @@ export function isKnownFunction(funcName, functions) {
|
|
|
308
319
|
return false
|
|
309
320
|
}
|
|
310
321
|
|
|
322
|
+
// Reserved keywords that cannot be used as identifiers in expressions.
|
|
323
|
+
// Non-reserved keywords (e.g. DAY, MONTH, FILTER, ASC) can be used as column alias references.
|
|
324
|
+
export const RESERVED_KEYWORDS = new Set([
|
|
325
|
+
'SELECT', 'FROM', 'WHERE', 'WITH',
|
|
326
|
+
'AND', 'OR', 'NOT', 'IS', 'LIKE', 'IN', 'BETWEEN',
|
|
327
|
+
'TRUE', 'FALSE', 'NULL',
|
|
328
|
+
'EXISTS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'INTERVAL',
|
|
329
|
+
'GROUP', 'BY', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
|
|
330
|
+
'AS', 'ALL', 'DISTINCT',
|
|
331
|
+
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'FULL', 'OUTER', 'ON',
|
|
332
|
+
])
|
|
333
|
+
|
|
311
334
|
// Keywords that cannot be used as implicit aliases after a column
|
|
312
335
|
export const RESERVED_AFTER_COLUMN = new Set([
|
|
313
336
|
'FROM', 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
|
|
File without changes
|