@jarenjs/core 0.9.2 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/ARCHITECTURE.md +229 -26
  2. package/README.md +64 -10
  3. package/dist/types/array.d.ts +13 -0
  4. package/dist/types/cache.d.ts +166 -0
  5. package/dist/types/chunk.d.ts +102 -0
  6. package/dist/types/color.d.ts +64 -0
  7. package/dist/types/convert/convert.d.ts +33 -0
  8. package/dist/types/convert/currency.d.ts +47 -0
  9. package/dist/types/convert/index.d.ts +9 -0
  10. package/dist/types/convert/registry.d.ts +55 -0
  11. package/dist/types/dates/civil.d.ts +146 -0
  12. package/dist/types/dates/duration.d.ts +83 -0
  13. package/dist/types/dates/format.d.ts +59 -0
  14. package/dist/types/dates/index.d.ts +4 -0
  15. package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
  16. package/dist/types/errors.d.ts +77 -0
  17. package/dist/types/finance/amortization.d.ts +51 -0
  18. package/dist/types/finance/bond.d.ts +45 -0
  19. package/dist/types/finance/cashflow.d.ts +44 -0
  20. package/dist/types/finance/depreciation.d.ts +26 -0
  21. package/dist/types/finance/index.d.ts +16 -0
  22. package/dist/types/finance/indicators.d.ts +76 -0
  23. package/dist/types/finance/interest.d.ts +44 -0
  24. package/dist/types/finance/returns.d.ts +43 -0
  25. package/dist/types/finance/tvm.d.ts +50 -0
  26. package/dist/types/geo/angle.d.ts +4 -0
  27. package/dist/types/geo/bbox.d.ts +32 -0
  28. package/dist/types/geo/distance.d.ts +67 -0
  29. package/dist/types/geo/geohash.d.ts +63 -0
  30. package/dist/types/geo/geojson.d.ts +108 -0
  31. package/dist/types/geo/index-tree.d.ts +52 -0
  32. package/dist/types/geo/index.d.ts +11 -0
  33. package/dist/types/geo/mercator.d.ts +57 -0
  34. package/dist/types/geo/predicates.d.ts +44 -0
  35. package/dist/types/geo/ring.d.ts +80 -0
  36. package/dist/types/geo/simplify.d.ts +31 -0
  37. package/dist/types/geo/valid.d.ts +24 -0
  38. package/dist/types/geo/wkt.d.ts +16 -0
  39. package/dist/types/math/float64.d.ts +126 -3
  40. package/dist/types/math/format.d.ts +58 -0
  41. package/dist/types/math/index.d.ts +5 -0
  42. package/dist/types/math/mat4.d.ts +60 -0
  43. package/dist/types/math/project.d.ts +54 -0
  44. package/dist/types/math/solve.d.ts +67 -0
  45. package/dist/types/math/word.d.ts +82 -0
  46. package/dist/types/message.d.ts +52 -0
  47. package/dist/types/object.d.ts +151 -0
  48. package/dist/types/scan.d.ts +30 -0
  49. package/dist/types/schema.d.ts +24 -0
  50. package/dist/types/string.d.ts +96 -0
  51. package/dist/types/text/base64.d.ts +6 -4
  52. package/dist/types/text/email.d.ts +0 -1
  53. package/dist/types/text/host.d.ts +39 -8
  54. package/dist/types/text/i18n.d.ts +13 -1
  55. package/dist/types/text/index.d.ts +1 -0
  56. package/dist/types/text/misc.d.ts +15 -1
  57. package/dist/types/text/punycode.d.ts +45 -85
  58. package/docs/CONVERT.md +45 -0
  59. package/docs/DATES.md +78 -0
  60. package/docs/FINANCE.md +59 -0
  61. package/docs/GEO.md +114 -0
  62. package/docs/MATH.md +75 -0
  63. package/package.json +57 -4
  64. package/src/array.js +16 -0
  65. package/src/cache.js +206 -0
  66. package/src/chunk.js +159 -0
  67. package/src/color.js +125 -0
  68. package/src/convert/convert.js +59 -0
  69. package/src/convert/currency.js +74 -0
  70. package/src/convert/index.js +11 -0
  71. package/src/convert/registry.js +213 -0
  72. package/src/dates/civil.js +360 -0
  73. package/src/dates/duration.js +225 -0
  74. package/src/dates/format.js +238 -0
  75. package/src/dates/index.js +30 -0
  76. package/src/dates/rfc3339.js +621 -0
  77. package/src/errors.js +102 -0
  78. package/src/finance/amortization.js +50 -0
  79. package/src/finance/bond.js +100 -0
  80. package/src/finance/cashflow.js +122 -0
  81. package/src/finance/depreciation.js +54 -0
  82. package/src/finance/index.js +27 -0
  83. package/src/finance/indicators.js +207 -0
  84. package/src/finance/interest.js +63 -0
  85. package/src/finance/returns.js +90 -0
  86. package/src/finance/tvm.js +94 -0
  87. package/src/function.js +0 -2
  88. package/src/geo/angle.js +12 -0
  89. package/src/geo/bbox.js +80 -0
  90. package/src/geo/distance.js +155 -0
  91. package/src/geo/geohash.js +224 -0
  92. package/src/geo/geojson.js +361 -0
  93. package/src/geo/index-tree.js +294 -0
  94. package/src/geo/index.js +52 -0
  95. package/src/geo/mercator.js +124 -0
  96. package/src/geo/predicates.js +313 -0
  97. package/src/geo/ring.js +182 -0
  98. package/src/geo/simplify.js +130 -0
  99. package/src/geo/valid.js +142 -0
  100. package/src/geo/wkt.js +262 -0
  101. package/src/math/float64.js +228 -11
  102. package/src/math/format.js +157 -0
  103. package/src/math/index.js +5 -0
  104. package/src/math/mat4.js +131 -0
  105. package/src/math/project.js +49 -0
  106. package/src/math/solve.js +112 -0
  107. package/src/math/word.js +177 -0
  108. package/src/message.js +119 -0
  109. package/src/object.js +329 -0
  110. package/src/scan.js +42 -0
  111. package/src/schema.js +37 -0
  112. package/src/string.js +189 -0
  113. package/src/text/base64.js +15 -52
  114. package/src/text/email.js +0 -5
  115. package/src/text/host.js +515 -140
  116. package/src/text/i18n.js +13 -6
  117. package/src/text/identifiers.js +1 -1
  118. package/src/text/index.js +1 -0
  119. package/src/text/misc.js +81 -5
  120. package/src/text/punycode.js +235 -323
  121. 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
@@ -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