@jarenjs/core 0.9.2 → 0.34.2
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/ARCHITECTURE.md +229 -26
- package/README.md +64 -10
- package/dist/types/array.d.ts +13 -0
- package/dist/types/cache.d.ts +166 -0
- package/dist/types/chunk.d.ts +102 -0
- package/dist/types/color.d.ts +64 -0
- package/dist/types/convert/convert.d.ts +33 -0
- package/dist/types/convert/currency.d.ts +47 -0
- package/dist/types/convert/index.d.ts +9 -0
- package/dist/types/convert/registry.d.ts +55 -0
- package/dist/types/dates/civil.d.ts +146 -0
- package/dist/types/dates/duration.d.ts +83 -0
- package/dist/types/dates/format.d.ts +59 -0
- package/dist/types/dates/index.d.ts +4 -0
- package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
- package/dist/types/errors.d.ts +77 -0
- package/dist/types/finance/amortization.d.ts +51 -0
- package/dist/types/finance/bond.d.ts +45 -0
- package/dist/types/finance/cashflow.d.ts +44 -0
- package/dist/types/finance/depreciation.d.ts +26 -0
- package/dist/types/finance/index.d.ts +16 -0
- package/dist/types/finance/indicators.d.ts +76 -0
- package/dist/types/finance/interest.d.ts +44 -0
- package/dist/types/finance/returns.d.ts +43 -0
- package/dist/types/finance/tvm.d.ts +50 -0
- package/dist/types/geo/angle.d.ts +4 -0
- package/dist/types/geo/bbox.d.ts +32 -0
- package/dist/types/geo/distance.d.ts +67 -0
- package/dist/types/geo/geohash.d.ts +63 -0
- package/dist/types/geo/geojson.d.ts +108 -0
- package/dist/types/geo/index-tree.d.ts +52 -0
- package/dist/types/geo/index.d.ts +11 -0
- package/dist/types/geo/mercator.d.ts +57 -0
- package/dist/types/geo/predicates.d.ts +44 -0
- package/dist/types/geo/ring.d.ts +80 -0
- package/dist/types/geo/simplify.d.ts +31 -0
- package/dist/types/geo/valid.d.ts +24 -0
- package/dist/types/geo/wkt.d.ts +16 -0
- package/dist/types/math/float64.d.ts +126 -3
- package/dist/types/math/format.d.ts +58 -0
- package/dist/types/math/index.d.ts +5 -0
- package/dist/types/math/mat4.d.ts +60 -0
- package/dist/types/math/project.d.ts +54 -0
- package/dist/types/math/solve.d.ts +67 -0
- package/dist/types/math/word.d.ts +82 -0
- package/dist/types/message.d.ts +52 -0
- package/dist/types/object.d.ts +151 -0
- package/dist/types/scan.d.ts +30 -0
- package/dist/types/schema.d.ts +24 -0
- package/dist/types/string.d.ts +96 -0
- package/dist/types/text/base64.d.ts +6 -4
- package/dist/types/text/email.d.ts +0 -1
- package/dist/types/text/host.d.ts +39 -8
- package/dist/types/text/i18n.d.ts +13 -1
- package/dist/types/text/index.d.ts +1 -0
- package/dist/types/text/misc.d.ts +15 -1
- package/dist/types/text/punycode.d.ts +45 -85
- package/docs/CONVERT.md +45 -0
- package/docs/DATES.md +78 -0
- package/docs/FINANCE.md +59 -0
- package/docs/GEO.md +114 -0
- package/docs/MATH.md +75 -0
- package/package.json +57 -4
- package/src/array.js +16 -0
- package/src/cache.js +206 -0
- package/src/chunk.js +159 -0
- package/src/color.js +125 -0
- package/src/convert/convert.js +59 -0
- package/src/convert/currency.js +74 -0
- package/src/convert/index.js +11 -0
- package/src/convert/registry.js +213 -0
- package/src/dates/civil.js +360 -0
- package/src/dates/duration.js +225 -0
- package/src/dates/format.js +238 -0
- package/src/dates/index.js +30 -0
- package/src/dates/rfc3339.js +621 -0
- package/src/errors.js +102 -0
- package/src/finance/amortization.js +50 -0
- package/src/finance/bond.js +100 -0
- package/src/finance/cashflow.js +122 -0
- package/src/finance/depreciation.js +54 -0
- package/src/finance/index.js +27 -0
- package/src/finance/indicators.js +207 -0
- package/src/finance/interest.js +63 -0
- package/src/finance/returns.js +90 -0
- package/src/finance/tvm.js +94 -0
- package/src/function.js +0 -2
- package/src/geo/angle.js +12 -0
- package/src/geo/bbox.js +80 -0
- package/src/geo/distance.js +155 -0
- package/src/geo/geohash.js +224 -0
- package/src/geo/geojson.js +361 -0
- package/src/geo/index-tree.js +294 -0
- package/src/geo/index.js +52 -0
- package/src/geo/mercator.js +124 -0
- package/src/geo/predicates.js +313 -0
- package/src/geo/ring.js +182 -0
- package/src/geo/simplify.js +130 -0
- package/src/geo/valid.js +142 -0
- package/src/geo/wkt.js +262 -0
- package/src/math/float64.js +228 -11
- package/src/math/format.js +157 -0
- package/src/math/index.js +5 -0
- package/src/math/mat4.js +131 -0
- package/src/math/project.js +49 -0
- package/src/math/solve.js +112 -0
- package/src/math/word.js +177 -0
- package/src/message.js +119 -0
- package/src/object.js +329 -0
- package/src/scan.js +42 -0
- package/src/schema.js +37 -0
- package/src/string.js +189 -0
- package/src/text/base64.js +15 -52
- package/src/text/email.js +0 -5
- package/src/text/host.js +515 -140
- package/src/text/i18n.js +13 -6
- package/src/text/identifiers.js +1 -1
- package/src/text/index.js +1 -0
- package/src/text/misc.js +81 -5
- package/src/text/punycode.js +235 -323
- package/src/dates.js +0 -371
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region Robust geometric predicates
|
|
4
|
+
// The orientation test every spatial operation rests on: is point c left
|
|
5
|
+
// of, right of, or exactly on the directed line a->b? Point-in-polygon,
|
|
6
|
+
// segment intersection, convex hull and ring winding are all sign tests
|
|
7
|
+
// on this one determinant.
|
|
8
|
+
//
|
|
9
|
+
// Computing it directly in floating point is WRONG, not merely imprecise.
|
|
10
|
+
// The subtractions and products each round, and on near-collinear input
|
|
11
|
+
// the accumulated error can exceed the determinant itself, so the sign
|
|
12
|
+
// flips. A predicate that returns the wrong sign is not a small error:
|
|
13
|
+
// it makes a containment test contradict itself and a clipper emit
|
|
14
|
+
// self-intersecting output.
|
|
15
|
+
//
|
|
16
|
+
// The insidious part is that it does not fail on ordinary data. It needs
|
|
17
|
+
// coordinates that are close to collinear relative to their magnitude —
|
|
18
|
+
// which is exactly what real survey and OpenStreetMap data contains, and
|
|
19
|
+
// exactly what a hand-written test does not. So the exact path is here
|
|
20
|
+
// from the start rather than added after something looked correct.
|
|
21
|
+
//
|
|
22
|
+
// The method is Shewchuk's adaptive precision arithmetic (Jonathan R.
|
|
23
|
+
// Shewchuk, "Adaptive Precision Floating-Point Arithmetic and Fast
|
|
24
|
+
// Robust Geometric Predicates", 1996): evaluate with a cheap error
|
|
25
|
+
// bound, and only when the sign is not certain fall back to exact
|
|
26
|
+
// arithmetic carried in non-overlapping floating-point expansions. The
|
|
27
|
+
// common case pays one comparison over the naive form; the exact path
|
|
28
|
+
// runs only where the naive form would have been wrong.
|
|
29
|
+
|
|
30
|
+
// The unit roundoff for IEEE doubles, and Shewchuk's derived bounds.
|
|
31
|
+
const EPSILON = 1.1102230246251565e-16; // 2^-53
|
|
32
|
+
const CCW_ERRBOUND_A = (3 + 16 * EPSILON) * EPSILON;
|
|
33
|
+
const CCW_ERRBOUND_B = (2 + 12 * EPSILON) * EPSILON;
|
|
34
|
+
const CCW_ERRBOUND_C = (9 + 64 * EPSILON) * EPSILON * EPSILON;
|
|
35
|
+
const RESULT_ERRBOUND = (3 + 8 * EPSILON) * EPSILON;
|
|
36
|
+
|
|
37
|
+
// Dekker's splitter, 2^ceil(53/2) + 1: splits a double into two halves
|
|
38
|
+
// whose product with another half is exact.
|
|
39
|
+
const SPLITTER = 134217729;
|
|
40
|
+
|
|
41
|
+
// Scratch expansions. Reused across calls to keep the exact path
|
|
42
|
+
// allocation-free; the predicate is synchronous and never re-entered.
|
|
43
|
+
const B = new Float64Array(4);
|
|
44
|
+
const C1 = new Float64Array(8);
|
|
45
|
+
const C2 = new Float64Array(12);
|
|
46
|
+
const D = new Float64Array(16);
|
|
47
|
+
const U = new Float64Array(4);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sum two expansions, eliminating zero components (Shewchuk's
|
|
51
|
+
* `fast_expansion_sum_zeroelim`). Both inputs are non-overlapping and
|
|
52
|
+
* increasing in magnitude; so is the result.
|
|
53
|
+
* @param {number} elen
|
|
54
|
+
* @param {Float64Array} e
|
|
55
|
+
* @param {number} flen
|
|
56
|
+
* @param {Float64Array} f
|
|
57
|
+
* @param {Float64Array} h - output
|
|
58
|
+
* @returns {number} the number of components written
|
|
59
|
+
*/
|
|
60
|
+
function expansionSum(elen, e, flen, f, h) {
|
|
61
|
+
let enow = e[0];
|
|
62
|
+
let fnow = f[0];
|
|
63
|
+
let eindex = 0;
|
|
64
|
+
let findex = 0;
|
|
65
|
+
let Q, Qnew, hh, bvirt;
|
|
66
|
+
if ((fnow > enow) === (fnow > -enow)) {
|
|
67
|
+
Q = enow;
|
|
68
|
+
enow = e[++eindex];
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
Q = fnow;
|
|
72
|
+
fnow = f[++findex];
|
|
73
|
+
}
|
|
74
|
+
let hindex = 0;
|
|
75
|
+
if (eindex < elen && findex < flen) {
|
|
76
|
+
if ((fnow > enow) === (fnow > -enow)) {
|
|
77
|
+
Qnew = enow + Q;
|
|
78
|
+
hh = Q - (Qnew - enow);
|
|
79
|
+
enow = e[++eindex];
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
Qnew = fnow + Q;
|
|
83
|
+
hh = Q - (Qnew - fnow);
|
|
84
|
+
fnow = f[++findex];
|
|
85
|
+
}
|
|
86
|
+
Q = Qnew;
|
|
87
|
+
if (hh !== 0)
|
|
88
|
+
h[hindex++] = hh;
|
|
89
|
+
while (eindex < elen && findex < flen) {
|
|
90
|
+
if ((fnow > enow) === (fnow > -enow)) {
|
|
91
|
+
Qnew = Q + enow;
|
|
92
|
+
bvirt = Qnew - Q;
|
|
93
|
+
hh = Q - (Qnew - bvirt) + (enow - bvirt);
|
|
94
|
+
enow = e[++eindex];
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
Qnew = Q + fnow;
|
|
98
|
+
bvirt = Qnew - Q;
|
|
99
|
+
hh = Q - (Qnew - bvirt) + (fnow - bvirt);
|
|
100
|
+
fnow = f[++findex];
|
|
101
|
+
}
|
|
102
|
+
Q = Qnew;
|
|
103
|
+
if (hh !== 0)
|
|
104
|
+
h[hindex++] = hh;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
while (eindex < elen) {
|
|
108
|
+
Qnew = Q + enow;
|
|
109
|
+
bvirt = Qnew - Q;
|
|
110
|
+
hh = Q - (Qnew - bvirt) + (enow - bvirt);
|
|
111
|
+
enow = e[++eindex];
|
|
112
|
+
Q = Qnew;
|
|
113
|
+
if (hh !== 0)
|
|
114
|
+
h[hindex++] = hh;
|
|
115
|
+
}
|
|
116
|
+
while (findex < flen) {
|
|
117
|
+
Qnew = Q + fnow;
|
|
118
|
+
bvirt = Qnew - Q;
|
|
119
|
+
hh = Q - (Qnew - bvirt) + (fnow - bvirt);
|
|
120
|
+
fnow = f[++findex];
|
|
121
|
+
Q = Qnew;
|
|
122
|
+
if (hh !== 0)
|
|
123
|
+
h[hindex++] = hh;
|
|
124
|
+
}
|
|
125
|
+
if (Q !== 0 || hindex === 0)
|
|
126
|
+
h[hindex++] = Q;
|
|
127
|
+
return hindex;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The approximate value of an expansion: its components summed.
|
|
132
|
+
* @param {number} elen
|
|
133
|
+
* @param {Float64Array} e
|
|
134
|
+
* @returns {number}
|
|
135
|
+
*/
|
|
136
|
+
function estimate(elen, e) {
|
|
137
|
+
let Q = e[0];
|
|
138
|
+
for (let i = 1; i < elen; i++)
|
|
139
|
+
Q += e[i];
|
|
140
|
+
return Q;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The exact stage: the determinant as an expansion, returning its
|
|
145
|
+
* (exactly correct) leading component.
|
|
146
|
+
* @returns {number}
|
|
147
|
+
*/
|
|
148
|
+
function orient2dExact(ax, ay, bx, by, cx, cy, detsum) {
|
|
149
|
+
let acx, acy, bcx, bcy, acxtail, acytail, bcxtail, bcytail;
|
|
150
|
+
let s1, s0, t1, t0, u3, bvirt, c, ahi, alo, bhi, blo, _i, _j, _0;
|
|
151
|
+
|
|
152
|
+
acx = ax - cx;
|
|
153
|
+
bcx = bx - cx;
|
|
154
|
+
acy = ay - cy;
|
|
155
|
+
bcy = by - cy;
|
|
156
|
+
|
|
157
|
+
// two_product(acx, bcy) and two_product(acy, bcx), exactly
|
|
158
|
+
s1 = acx * bcy;
|
|
159
|
+
c = SPLITTER * acx;
|
|
160
|
+
ahi = c - (c - acx);
|
|
161
|
+
alo = acx - ahi;
|
|
162
|
+
c = SPLITTER * bcy;
|
|
163
|
+
bhi = c - (c - bcy);
|
|
164
|
+
blo = bcy - bhi;
|
|
165
|
+
s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
166
|
+
t1 = acy * bcx;
|
|
167
|
+
c = SPLITTER * acy;
|
|
168
|
+
ahi = c - (c - acy);
|
|
169
|
+
alo = acy - ahi;
|
|
170
|
+
c = SPLITTER * bcx;
|
|
171
|
+
bhi = c - (c - bcx);
|
|
172
|
+
blo = bcx - bhi;
|
|
173
|
+
t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
|
|
174
|
+
|
|
175
|
+
// two_two_diff(s1, s0, t1, t0) -> B
|
|
176
|
+
_i = s0 - t0;
|
|
177
|
+
bvirt = s0 - _i;
|
|
178
|
+
B[0] = s0 - (_i + bvirt) + (bvirt - t0);
|
|
179
|
+
_j = s1 + _i;
|
|
180
|
+
bvirt = _j - s1;
|
|
181
|
+
_0 = s1 - (_j - bvirt) + (_i - bvirt);
|
|
182
|
+
_i = _0 - t1;
|
|
183
|
+
bvirt = _0 - _i;
|
|
184
|
+
B[1] = _0 - (_i + bvirt) + (bvirt - t1);
|
|
185
|
+
u3 = _j + _i;
|
|
186
|
+
bvirt = u3 - _j;
|
|
187
|
+
B[2] = _j - (u3 - bvirt) + (_i - bvirt);
|
|
188
|
+
B[3] = u3;
|
|
189
|
+
|
|
190
|
+
let det = estimate(4, B);
|
|
191
|
+
let errbound = CCW_ERRBOUND_B * detsum;
|
|
192
|
+
if (det >= errbound || -det >= errbound)
|
|
193
|
+
return det;
|
|
194
|
+
|
|
195
|
+
// recover the exact tails of the four differences
|
|
196
|
+
bvirt = ax - acx;
|
|
197
|
+
acxtail = ax - (acx + bvirt) + (bvirt - cx);
|
|
198
|
+
bvirt = bx - bcx;
|
|
199
|
+
bcxtail = bx - (bcx + bvirt) + (bvirt - cx);
|
|
200
|
+
bvirt = ay - acy;
|
|
201
|
+
acytail = ay - (acy + bvirt) + (bvirt - cy);
|
|
202
|
+
bvirt = by - bcy;
|
|
203
|
+
bcytail = by - (bcy + bvirt) + (bvirt - cy);
|
|
204
|
+
|
|
205
|
+
if (acxtail === 0 && acytail === 0 && bcxtail === 0 && bcytail === 0)
|
|
206
|
+
return det; // the differences were exact, so B is the exact answer
|
|
207
|
+
|
|
208
|
+
errbound = CCW_ERRBOUND_C * detsum + RESULT_ERRBOUND * Math.abs(det);
|
|
209
|
+
det += (acx * bcytail + bcy * acxtail) - (acy * bcxtail + bcx * acytail);
|
|
210
|
+
if (det >= errbound || -det >= errbound)
|
|
211
|
+
return det;
|
|
212
|
+
|
|
213
|
+
// the full exact determinant, accumulated through the expansions
|
|
214
|
+
let len = expansionSum(4, B, twoProduct(acxtail, bcy, U), U, C1);
|
|
215
|
+
len = expansionSum(len, C1, twoProduct(acx, bcytail, U), U, C2);
|
|
216
|
+
const c1len = len;
|
|
217
|
+
len = expansionSum(c1len, C2, twoProduct(acytail, bcx, U, true), U, C1);
|
|
218
|
+
len = expansionSum(len, C1, twoProduct(acy, bcxtail, U, true), U, D);
|
|
219
|
+
return D[len - 1];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Exact product of two doubles as a two-component expansion, written
|
|
224
|
+
* into `out`. With `negate` the product's sign is flipped, which is how
|
|
225
|
+
* the subtracted half of the determinant is folded into a sum.
|
|
226
|
+
* @returns {number} always 2
|
|
227
|
+
*/
|
|
228
|
+
function twoProduct(a, b, out, negate = false) {
|
|
229
|
+
const x = a * b;
|
|
230
|
+
let c = SPLITTER * a;
|
|
231
|
+
const ahi = c - (c - a);
|
|
232
|
+
const alo = a - ahi;
|
|
233
|
+
c = SPLITTER * b;
|
|
234
|
+
const bhi = c - (c - b);
|
|
235
|
+
const blo = b - bhi;
|
|
236
|
+
const y = alo * blo - (x - ahi * bhi - alo * bhi - ahi * blo);
|
|
237
|
+
out[0] = negate ? -y : y;
|
|
238
|
+
out[1] = negate ? -x : x;
|
|
239
|
+
return 2;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The orientation of the triple (a, b, c): **positive** when c lies to
|
|
244
|
+
* the left of the directed line a->b (counter-clockwise), **negative**
|
|
245
|
+
* when it lies to the right, and **exactly zero** when the three points
|
|
246
|
+
* are collinear.
|
|
247
|
+
*
|
|
248
|
+
* The sign is always correct. The magnitude is twice the signed area of
|
|
249
|
+
* the triangle, but only approximately — callers must use this for its
|
|
250
|
+
* sign, and `ringArea` for a measurement.
|
|
251
|
+
*
|
|
252
|
+
* Coordinates are raw numbers rather than vector objects so a GeoJSON
|
|
253
|
+
* position (`[lon, lat]`) can be passed straight through.
|
|
254
|
+
*
|
|
255
|
+
* @param {number} ax
|
|
256
|
+
* @param {number} ay
|
|
257
|
+
* @param {number} bx
|
|
258
|
+
* @param {number} by
|
|
259
|
+
* @param {number} cx
|
|
260
|
+
* @param {number} cy
|
|
261
|
+
* @returns {number} positive, negative, or zero
|
|
262
|
+
* @example
|
|
263
|
+
* orient2d(0, 0, 1, 0, 0, 1); // > 0 — (0,1) is left of the x axis
|
|
264
|
+
* orient2d(0, 0, 1, 0, 0, -1); // < 0
|
|
265
|
+
* orient2d(0, 0, 2, 2, 1, 1); // 0 — exactly collinear
|
|
266
|
+
*/
|
|
267
|
+
export function orient2d(ax, ay, bx, by, cx, cy) {
|
|
268
|
+
// the same two halves the exact stage forms, in the same order: a
|
|
269
|
+
// mismatch here would make the filter and the fallback disagree about
|
|
270
|
+
// which direction is counter-clockwise
|
|
271
|
+
const detleft = (ax - cx) * (by - cy);
|
|
272
|
+
const detright = (ay - cy) * (bx - cx);
|
|
273
|
+
const det = detleft - detright;
|
|
274
|
+
|
|
275
|
+
// the cheap filter: when the two halves have opposite signs, or one is
|
|
276
|
+
// zero, the subtraction cannot have destroyed the sign
|
|
277
|
+
const detsum = detleft > 0
|
|
278
|
+
? (detright <= 0 ? det : detleft + detright)
|
|
279
|
+
: detleft < 0
|
|
280
|
+
? (detright >= 0 ? det : -detleft - detright)
|
|
281
|
+
: det;
|
|
282
|
+
if (detleft === 0 || detright === 0 || (detleft > 0) !== (detright > 0))
|
|
283
|
+
return det;
|
|
284
|
+
|
|
285
|
+
const errbound = CCW_ERRBOUND_A * detsum;
|
|
286
|
+
if (det >= errbound || -det >= errbound)
|
|
287
|
+
return det;
|
|
288
|
+
|
|
289
|
+
return orient2dExact(ax, ay, bx, by, cx, cy, detsum);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The same determinant without the exact fallback: fast, and **wrong on
|
|
294
|
+
* near-collinear input**.
|
|
295
|
+
*
|
|
296
|
+
* This exists for the one case where that is acceptable — a bounding
|
|
297
|
+
* pre-filter whose answer is checked by `orient2d` afterwards. Anything
|
|
298
|
+
* that decides containment, winding or intersection must use `orient2d`;
|
|
299
|
+
* a wrong sign there is a wrong answer, not a rounding error.
|
|
300
|
+
*
|
|
301
|
+
* @param {number} ax
|
|
302
|
+
* @param {number} ay
|
|
303
|
+
* @param {number} bx
|
|
304
|
+
* @param {number} by
|
|
305
|
+
* @param {number} cx
|
|
306
|
+
* @param {number} cy
|
|
307
|
+
* @returns {number}
|
|
308
|
+
*/
|
|
309
|
+
export function orient2dFast(ax, ay, bx, by, cx, cy) {
|
|
310
|
+
return (ax - cx) * (by - cy) - (ay - cy) * (bx - cx);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
//#endregion
|
package/src/geo/ring.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region Rings, areas and containment
|
|
4
|
+
// A GeoJSON linear ring is an array of at least four positions whose
|
|
5
|
+
// first and last are identical (RFC 7946 section 3.1.6). A Polygon's
|
|
6
|
+
// `coordinates` is an array of rings — exterior first, holes after — so
|
|
7
|
+
// everything here takes plain position arrays and needs no knowledge of
|
|
8
|
+
// the `type` discriminator.
|
|
9
|
+
//
|
|
10
|
+
// The two invariants JSON Schema provably cannot express live here:
|
|
11
|
+
// `isRingClosed` and `ringWinding`. The official GeoJSON schema says so
|
|
12
|
+
// outright, which is why every validator in the ecosystem bolts on code
|
|
13
|
+
// of its own — and unclosed rings and reversed winding (which makes a
|
|
14
|
+
// renderer fill the whole globe instead of the polygon) are among the
|
|
15
|
+
// commonest defects in real data.
|
|
16
|
+
|
|
17
|
+
import { orient2d } from './predicates.js';
|
|
18
|
+
import { EARTH_RADIUS } from './distance.js';
|
|
19
|
+
import { DEG } from './angle.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether a ring is closed: its last position repeats its first.
|
|
23
|
+
*
|
|
24
|
+
* Compares longitude and latitude only. A position may legally carry a
|
|
25
|
+
* third element (altitude), and RFC 7946 does not require it to match
|
|
26
|
+
* for the ring to close.
|
|
27
|
+
*
|
|
28
|
+
* @param {Array<number[]>} ring - positions
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
* @example
|
|
31
|
+
* isRingClosed([[0,0],[1,0],[1,1],[0,0]]); // true
|
|
32
|
+
* isRingClosed([[0,0],[1,0],[1,1]]); // false
|
|
33
|
+
*/
|
|
34
|
+
export function isRingClosed(ring) {
|
|
35
|
+
const n = ring.length;
|
|
36
|
+
if (n < 2)
|
|
37
|
+
return false;
|
|
38
|
+
const first = ring[0];
|
|
39
|
+
const last = ring[n - 1];
|
|
40
|
+
return first[0] === last[0] && first[1] === last[1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Twice the signed planar area of a ring (the shoelace sum), in squared
|
|
45
|
+
* degrees. **Positive is counter-clockwise.**
|
|
46
|
+
*
|
|
47
|
+
* The sign is the useful part and is exact for the purpose: it is a sum
|
|
48
|
+
* of `orient2d` determinants about the ring's first vertex, so a ring
|
|
49
|
+
* whose vertices are near-collinear still winds the way this says. The
|
|
50
|
+
* magnitude is a planar quantity in degree-space and is NOT an area on
|
|
51
|
+
* the Earth — use `sphericalRingArea` for that.
|
|
52
|
+
*
|
|
53
|
+
* @param {Array<number[]>} ring - positions
|
|
54
|
+
* @returns {number} twice the signed area; positive = counter-clockwise
|
|
55
|
+
*/
|
|
56
|
+
export function ringSignedArea(ring) {
|
|
57
|
+
const n = ring.length;
|
|
58
|
+
if (n < 3)
|
|
59
|
+
return 0;
|
|
60
|
+
// fan the triangles from vertex 0 so each term is an orientation
|
|
61
|
+
// determinant rather than a bare cross product
|
|
62
|
+
const [ox, oy] = ring[0];
|
|
63
|
+
let sum = 0;
|
|
64
|
+
for (let i = 1; i < n - 1; i++) {
|
|
65
|
+
const a = ring[i];
|
|
66
|
+
const b = ring[i + 1];
|
|
67
|
+
sum += orient2d(ox, oy, a[0], a[1], b[0], b[1]);
|
|
68
|
+
}
|
|
69
|
+
return sum;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The winding direction of a ring: `1` counter-clockwise, `-1`
|
|
74
|
+
* clockwise, `0` degenerate (zero area).
|
|
75
|
+
*
|
|
76
|
+
* RFC 7946 section 3.1.6 requires exterior rings to be counter-clockwise
|
|
77
|
+
* and holes clockwise — the right-hand rule. Parsers are told to accept
|
|
78
|
+
* non-conforming data, so this reports rather than rejects.
|
|
79
|
+
*
|
|
80
|
+
* @param {Array<number[]>} ring - positions
|
|
81
|
+
* @returns {number} 1, -1 or 0
|
|
82
|
+
*/
|
|
83
|
+
export function ringWinding(ring) {
|
|
84
|
+
return Math.sign(ringSignedArea(ring));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Geodesic area of a ring on the sphere, in square metres, always
|
|
89
|
+
* non-negative.
|
|
90
|
+
*
|
|
91
|
+
* Uses the spherical excess of the polygon (the standard formula for a
|
|
92
|
+
* ring of great-circle edges), so unlike `ringSignedArea` this is a real
|
|
93
|
+
* measurement. Accurate to the sphere approximation — under half a
|
|
94
|
+
* percent, the same bound as `haversineDistance`.
|
|
95
|
+
*
|
|
96
|
+
* @param {Array<number[]>} ring - positions
|
|
97
|
+
* @param {number} [radius]
|
|
98
|
+
* @returns {number} square metres
|
|
99
|
+
*/
|
|
100
|
+
export function sphericalRingArea(ring, radius = EARTH_RADIUS) {
|
|
101
|
+
const n = ring.length;
|
|
102
|
+
if (n < 4)
|
|
103
|
+
return 0; // fewer than 3 distinct positions bounds no area
|
|
104
|
+
let total = 0;
|
|
105
|
+
// consecutive edges share a vertex, so its sine is carried forward
|
|
106
|
+
// rather than recomputed: one transcendental per vertex, not two
|
|
107
|
+
let sinA = Math.sin(ring[0][1] * DEG);
|
|
108
|
+
for (let i = 0; i < n - 1; i++) {
|
|
109
|
+
const a = ring[i];
|
|
110
|
+
const b = ring[i + 1];
|
|
111
|
+
const sinB = Math.sin(b[1] * DEG);
|
|
112
|
+
total += (b[0] - a[0]) * DEG * (2 + sinA + sinB);
|
|
113
|
+
sinA = sinB;
|
|
114
|
+
}
|
|
115
|
+
return Math.abs(total * radius * radius * 0.5);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether a position lies inside a ring, by the even-odd (crossing)
|
|
120
|
+
* rule. A position exactly on an edge or vertex counts as **inside**,
|
|
121
|
+
* which is the convention that makes adjacent polygons tile without
|
|
122
|
+
* gaps.
|
|
123
|
+
*
|
|
124
|
+
* The ray cast runs in the ring's own coordinate space, so this is a
|
|
125
|
+
* planar test on longitude/latitude. That is what GeoJSON consumers
|
|
126
|
+
* mean by containment, and it is correct for any ring that does not
|
|
127
|
+
* cross the antimeridian — which RFC 7946 tells producers to cut.
|
|
128
|
+
*
|
|
129
|
+
* @param {number} x - longitude
|
|
130
|
+
* @param {number} y - latitude
|
|
131
|
+
* @param {Array<number[]>} ring - positions
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
export function pointInRing(x, y, ring) {
|
|
135
|
+
const n = ring.length;
|
|
136
|
+
if (n < 3)
|
|
137
|
+
return false;
|
|
138
|
+
let inside = false;
|
|
139
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
140
|
+
const xi = ring[i][0];
|
|
141
|
+
const yi = ring[i][1];
|
|
142
|
+
const xj = ring[j][0];
|
|
143
|
+
const yj = ring[j][1];
|
|
144
|
+
// Two cheap tests decide whether the exact predicate is needed at
|
|
145
|
+
// all. An edge that neither straddles the ray nor spans the point
|
|
146
|
+
// can affect neither answer, and on a large ring that is almost
|
|
147
|
+
// every edge - so the robust orientation runs a handful of times
|
|
148
|
+
// instead of twice per vertex.
|
|
149
|
+
const straddles = (yi > y) !== (yj > y);
|
|
150
|
+
const spans = (xi < x) === (x <= xj) && (yi < y) === (y <= yj);
|
|
151
|
+
if (!straddles && !spans)
|
|
152
|
+
continue;
|
|
153
|
+
const side = orient2d(xi, yi, xj, yj, x, y);
|
|
154
|
+
// exactly on this edge, and within its extent rather than its line
|
|
155
|
+
if (side === 0 && spans)
|
|
156
|
+
return true;
|
|
157
|
+
// the crossing test itself: is the edge to the right of the point
|
|
158
|
+
if (straddles && side !== 0 && (side > 0) === (yj > yi))
|
|
159
|
+
inside = !inside;
|
|
160
|
+
}
|
|
161
|
+
return inside;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Whether a position lies inside a polygon: inside its exterior ring and
|
|
166
|
+
* outside every hole.
|
|
167
|
+
* @param {number} x - longitude
|
|
168
|
+
* @param {number} y - latitude
|
|
169
|
+
* @param {Array<Array<number[]>>} rings - a GeoJSON Polygon's coordinates
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
*/
|
|
172
|
+
export function pointInPolygon(x, y, rings) {
|
|
173
|
+
if (rings.length === 0 || !pointInRing(x, y, rings[0]))
|
|
174
|
+
return false;
|
|
175
|
+
for (let i = 1; i < rings.length; i++) {
|
|
176
|
+
if (pointInRing(x, y, rings[i]))
|
|
177
|
+
return false; // in a hole
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
//#endregion
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region Line simplification
|
|
4
|
+
// Real geographic data is drawn at a resolution nobody is looking at. A
|
|
5
|
+
// national boundary from OpenStreetMap carries tens of thousands of
|
|
6
|
+
// vertices; rendered 400 pixels wide, the overwhelming majority land on
|
|
7
|
+
// the same pixel as their neighbour. Emitting them anyway costs path
|
|
8
|
+
// bytes, parse time and paint time for a picture that is byte-identical.
|
|
9
|
+
//
|
|
10
|
+
// Douglas-Peucker is the classic answer and the right one here: it keeps
|
|
11
|
+
// the vertices that carry the shape (every corner, every peninsula) and
|
|
12
|
+
// drops the ones that only fill in a straight run, so what survives is
|
|
13
|
+
// what a reader would have drawn. The tolerance is a distance in the
|
|
14
|
+
// input's own units — feed it projected unit-square coordinates and the
|
|
15
|
+
// tolerance is a fraction of the frame, which is the only unit a
|
|
16
|
+
// renderer can reason about.
|
|
17
|
+
//
|
|
18
|
+
// The implementation is iterative rather than recursive. A degenerate
|
|
19
|
+
// input (a coastline digitized as one long near-straight run) recurses
|
|
20
|
+
// once per vertex, and at OSM scale that overflows the stack — a real
|
|
21
|
+
// failure, not a theoretical one.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Squared distance from a point to a segment. Squared throughout: the
|
|
25
|
+
* comparison against a tolerance is monotonic in the square, so the
|
|
26
|
+
* square root would be one transcendental per vertex per level for an
|
|
27
|
+
* answer nobody reads.
|
|
28
|
+
*/
|
|
29
|
+
function segmentDistanceSq(px, py, ax, ay, bx, by) {
|
|
30
|
+
let dx = bx - ax;
|
|
31
|
+
let dy = by - ay;
|
|
32
|
+
if (dx !== 0 || dy !== 0) {
|
|
33
|
+
const t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy);
|
|
34
|
+
if (t > 1) {
|
|
35
|
+
ax = bx;
|
|
36
|
+
ay = by;
|
|
37
|
+
}
|
|
38
|
+
else if (t > 0) {
|
|
39
|
+
ax += dx * t;
|
|
40
|
+
ay += dy * t;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
dx = px - ax;
|
|
44
|
+
dy = py - ay;
|
|
45
|
+
return dx * dx + dy * dy;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Simplify a line with the Douglas-Peucker algorithm, keeping every
|
|
50
|
+
* vertex further than `tolerance` from the line its neighbours describe.
|
|
51
|
+
*
|
|
52
|
+
* The first and last positions always survive, so a **ring stays closed**
|
|
53
|
+
* — simplification can never turn a valid polygon into an invalid one.
|
|
54
|
+
* Positions are returned by reference, not copied, so any third element
|
|
55
|
+
* (altitude) rides along untouched.
|
|
56
|
+
*
|
|
57
|
+
* @param {Array<number[]>} line - positions
|
|
58
|
+
* @param {number} tolerance - in the input's own coordinate units
|
|
59
|
+
* @returns {Array<number[]>} a new array; the input is not modified
|
|
60
|
+
* @example
|
|
61
|
+
* simplifyLine([[0,0],[1,0.001],[2,0]], 0.01); // [[0,0],[2,0]]
|
|
62
|
+
* simplifyLine([[0,0],[1,1],[2,0]], 0.01); // all three: the corner carries the shape
|
|
63
|
+
*/
|
|
64
|
+
export function simplifyLine(line, tolerance) {
|
|
65
|
+
const n = line.length;
|
|
66
|
+
if (n < 3 || !(tolerance > 0))
|
|
67
|
+
return line.slice();
|
|
68
|
+
|
|
69
|
+
const toleranceSq = tolerance * tolerance;
|
|
70
|
+
const keep = new Uint8Array(n);
|
|
71
|
+
keep[0] = 1;
|
|
72
|
+
keep[n - 1] = 1;
|
|
73
|
+
|
|
74
|
+
// an explicit stack of [first, last] spans still to examine
|
|
75
|
+
const stack = [0, n - 1];
|
|
76
|
+
while (stack.length > 0) {
|
|
77
|
+
const last = /** @type {number} */(stack.pop());
|
|
78
|
+
const first = /** @type {number} */(stack.pop());
|
|
79
|
+
if (last - first < 2)
|
|
80
|
+
continue;
|
|
81
|
+
const ax = line[first][0];
|
|
82
|
+
const ay = line[first][1];
|
|
83
|
+
const bx = line[last][0];
|
|
84
|
+
const by = line[last][1];
|
|
85
|
+
let furthest = -1;
|
|
86
|
+
let furthestSq = toleranceSq;
|
|
87
|
+
for (let i = first + 1; i < last; i++) {
|
|
88
|
+
const d = segmentDistanceSq(line[i][0], line[i][1], ax, ay, bx, by);
|
|
89
|
+
if (d > furthestSq) {
|
|
90
|
+
furthestSq = d;
|
|
91
|
+
furthest = i;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// nothing in this span strays far enough: the whole run collapses to
|
|
95
|
+
// its endpoints, and neither half needs looking at
|
|
96
|
+
if (furthest < 0)
|
|
97
|
+
continue;
|
|
98
|
+
keep[furthest] = 1;
|
|
99
|
+
stack.push(first, furthest, furthest, last);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const out = [];
|
|
103
|
+
for (let i = 0; i < n; i++) {
|
|
104
|
+
if (keep[i] === 1)
|
|
105
|
+
out.push(line[i]);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Simplify a linear ring, refusing to destroy it.
|
|
112
|
+
*
|
|
113
|
+
* A ring needs four positions to bound a surface, so when the tolerance
|
|
114
|
+
* would leave fewer the ring is returned **unsimplified** rather than
|
|
115
|
+
* degenerate. Dropping a small island entirely is a defensible rendering
|
|
116
|
+
* choice, but silently emitting a two-vertex "polygon" is not — a caller
|
|
117
|
+
* wanting the island gone should filter by area first.
|
|
118
|
+
*
|
|
119
|
+
* @param {Array<number[]>} ring - positions, first === last
|
|
120
|
+
* @param {number} tolerance
|
|
121
|
+
* @returns {Array<number[]>}
|
|
122
|
+
*/
|
|
123
|
+
export function simplifyRing(ring, tolerance) {
|
|
124
|
+
if (ring.length < 5)
|
|
125
|
+
return ring.slice();
|
|
126
|
+
const simplified = simplifyLine(ring, tolerance);
|
|
127
|
+
return simplified.length >= 4 ? simplified : ring.slice();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|