@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.
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,224 @@
1
+ //@ts-check
2
+
3
+ //#region Geohash
4
+ // A geohash is a base-32 string over the bits of a recursive
5
+ // longitude/latitude bisection: each character adds five bits, so each
6
+ // character is a finer cell, and a shared prefix means a shared cell.
7
+ //
8
+ // That last property is why this belongs in the suite rather than in a
9
+ // spatial library. A geohash is a **string**, so it needs no new
10
+ // vocabulary anywhere: proximity is `$starts-with`, spatial bucketing is
11
+ // `$groupby` on a `$substring` prefix, and a sorted index over the hash
12
+ // is a spatial index. Nothing else in this module family integrates so
13
+ // cheaply.
14
+ //
15
+ // The trade is that cells are rectangles in degree space, so they are
16
+ // not equal-area — a cell at 60°N is about half the width of one at the
17
+ // equator — and neighbouring points can land either side of a cell
18
+ // boundary. Use it for bucketing and for a first pass, not as a distance.
19
+
20
+ const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
21
+
22
+ // reverse lookup, built once
23
+ const DECODE = new Map();
24
+ for (let i = 0; i < BASE32.length; i++)
25
+ DECODE.set(BASE32[i], i);
26
+
27
+ /**
28
+ * Approximate cell size in degrees per precision, as
29
+ * `[lonWidth, latHeight]`. Index 0 is unused so the array indexes by
30
+ * precision directly.
31
+ */
32
+ const CELL_SIZE = (() => {
33
+ const out = [null];
34
+ for (let p = 1; p <= 12; p++) {
35
+ // five bits per character, alternating and starting on longitude, so
36
+ // an odd precision gives longitude the extra bit
37
+ const lonBits = Math.ceil((p * 5) / 2);
38
+ const latBits = Math.floor((p * 5) / 2);
39
+ out.push([360 / (2 ** lonBits), 180 / (2 ** latBits)]);
40
+ }
41
+ return out;
42
+ })();
43
+
44
+ /**
45
+ * Encode a position as a geohash string.
46
+ *
47
+ * @param {number} lon - longitude, degrees
48
+ * @param {number} lat - latitude, degrees
49
+ * @param {number} [precision] - characters, 1-12 (default 9, about 5 m)
50
+ * @returns {string}
51
+ * @example
52
+ * geohashEncode(4.9041, 52.3676, 5); // 'u173z' (Amsterdam)
53
+ */
54
+ export function geohashEncode(lon, lat, precision = 9) {
55
+ let lonMin = -180;
56
+ let lonMax = 180;
57
+ let latMin = -90;
58
+ let latMax = 90;
59
+ let hash = '';
60
+ let bits = 0;
61
+ let ch = 0;
62
+ let evenBit = true; // longitude first
63
+ while (hash.length < precision) {
64
+ if (evenBit) {
65
+ const mid = (lonMin + lonMax) / 2;
66
+ if (lon >= mid) {
67
+ ch = ch * 2 + 1;
68
+ lonMin = mid;
69
+ }
70
+ else {
71
+ ch *= 2;
72
+ lonMax = mid;
73
+ }
74
+ }
75
+ else {
76
+ const mid = (latMin + latMax) / 2;
77
+ if (lat >= mid) {
78
+ ch = ch * 2 + 1;
79
+ latMin = mid;
80
+ }
81
+ else {
82
+ ch *= 2;
83
+ latMax = mid;
84
+ }
85
+ }
86
+ evenBit = !evenBit;
87
+ if (++bits === 5) {
88
+ hash += BASE32[ch];
89
+ bits = 0;
90
+ ch = 0;
91
+ }
92
+ }
93
+ return hash;
94
+ }
95
+
96
+ /**
97
+ * Whether a string is a well-formed geohash: non-empty and every
98
+ * character in the base-32 alphabet (lowercase; `a`, `i`, `l` and `o`
99
+ * are deliberately absent from it). Any length is valid — each
100
+ * character only names a finer cell.
101
+ * @param {string} hash
102
+ * @returns {boolean}
103
+ * @example
104
+ * isValidGeohash('u173z'); // true (Amsterdam)
105
+ * isValidGeohash('u17a'); // false ('a' is not in the alphabet)
106
+ */
107
+ export function isValidGeohash(hash) {
108
+ if (typeof hash !== 'string' || hash.length === 0)
109
+ return false;
110
+ for (let i = 0; i < hash.length; i++) {
111
+ if (!DECODE.has(hash[i]))
112
+ return false;
113
+ }
114
+ return true;
115
+ }
116
+
117
+ /**
118
+ * The bounding box of a geohash cell, as `[west, south, east, north]`.
119
+ * Returns null for a string containing a character outside the base-32
120
+ * alphabet (`a`, `i`, `l` and `o` are deliberately absent).
121
+ * @param {string} hash
122
+ * @returns {number[] | null}
123
+ */
124
+ export function geohashBounds(hash) {
125
+ if (typeof hash !== 'string' || hash.length === 0)
126
+ return null;
127
+ let lonMin = -180;
128
+ let lonMax = 180;
129
+ let latMin = -90;
130
+ let latMax = 90;
131
+ let evenBit = true;
132
+ for (let i = 0; i < hash.length; i++) {
133
+ const value = DECODE.get(hash[i]);
134
+ if (value === undefined)
135
+ return null;
136
+ for (let n = 4; n >= 0; n--) {
137
+ const bit = (value >> n) & 1;
138
+ if (evenBit) {
139
+ const mid = (lonMin + lonMax) / 2;
140
+ if (bit === 1) lonMin = mid;
141
+ else lonMax = mid;
142
+ }
143
+ else {
144
+ const mid = (latMin + latMax) / 2;
145
+ if (bit === 1) latMin = mid;
146
+ else latMax = mid;
147
+ }
148
+ evenBit = !evenBit;
149
+ }
150
+ }
151
+ return [lonMin, latMin, lonMax, latMax];
152
+ }
153
+
154
+ /**
155
+ * The centre of a geohash cell, as a GeoJSON position. Null for an
156
+ * invalid hash.
157
+ *
158
+ * Decoding is lossy by construction — a hash names a cell, not a point —
159
+ * so this is the cell's centre and the error is bounded by
160
+ * {@link geohashCellSize}.
161
+ *
162
+ * @param {string} hash
163
+ * @returns {[number, number] | null}
164
+ */
165
+ export function geohashDecode(hash) {
166
+ const box = geohashBounds(hash);
167
+ if (box === null)
168
+ return null;
169
+ return [(box[0] + box[2]) / 2, (box[1] + box[3]) / 2];
170
+ }
171
+
172
+ /**
173
+ * Approximate cell size at a precision, as `[lonWidth, latHeight]` in
174
+ * degrees. Null outside 1-12.
175
+ * @param {number} precision
176
+ * @returns {number[] | null}
177
+ */
178
+ export function geohashCellSize(precision) {
179
+ return CELL_SIZE[precision] ?? null;
180
+ }
181
+
182
+ /**
183
+ * The eight geohash cells surrounding one, plus the cell itself, as a
184
+ * nine-element array. Cells that would fall off the poles are omitted,
185
+ * so the result may be shorter.
186
+ *
187
+ * This is what makes a prefix search safe near a boundary: two points
188
+ * metres apart can sit in different cells, so a proximity query tests
189
+ * the neighbourhood rather than the single cell.
190
+ *
191
+ * @param {string} hash
192
+ * @returns {string[]}
193
+ */
194
+ export function geohashNeighbours(hash) {
195
+ const box = geohashBounds(hash);
196
+ if (box === null)
197
+ return [];
198
+ const precision = hash.length;
199
+ const lonStep = box[2] - box[0];
200
+ const latStep = box[3] - box[1];
201
+ const lon = (box[0] + box[2]) / 2;
202
+ const lat = (box[1] + box[3]) / 2;
203
+ const out = [];
204
+ const seen = new Set();
205
+ for (let dy = 1; dy >= -1; dy--) {
206
+ for (let dx = -1; dx <= 1; dx++) {
207
+ const ny = lat + dy * latStep;
208
+ if (ny > 90 || ny < -90)
209
+ continue; // past a pole there is no neighbouring cell
210
+ // longitude wraps at the antimeridian
211
+ let nx = lon + dx * lonStep;
212
+ if (nx > 180) nx -= 360;
213
+ else if (nx < -180) nx += 360;
214
+ const cell = geohashEncode(nx, ny, precision);
215
+ if (!seen.has(cell)) {
216
+ seen.add(cell);
217
+ out.push(cell);
218
+ }
219
+ }
220
+ }
221
+ return out;
222
+ }
223
+
224
+ //#endregion
@@ -0,0 +1,361 @@
1
+ //@ts-check
2
+
3
+ //#region GeoJSON traversal
4
+ // The layer that knows about the `type` discriminator. Everything else
5
+ // in this module family works on positions and rings — which ARE a
6
+ // GeoJSON geometry's `coordinates` — so this is the only place that has
7
+ // to care whether it is holding a Point, a Feature or a whole
8
+ // FeatureCollection.
9
+ //
10
+ // Every function here accepts the three shapes a caller realistically
11
+ // has: a bare **position** (`[lon, lat]`), a **geometry**, or a
12
+ // **Feature**/**FeatureCollection** wrapping one. Refusing the wrappers
13
+ // would push the unwrapping into every call site, and a query operator
14
+ // cannot ask its author to unwrap first.
15
+ //
16
+ // Nothing here validates. A malformed object yields null or 0 rather
17
+ // than throwing, because the schema artifacts are where GeoJSON is
18
+ // judged; a measurement function that also validated would report the
19
+ // same defect twice in two vocabularies.
20
+
21
+ import { haversineDistance, lineLength, EARTH_RADIUS } from './distance.js';
22
+ import { sphericalRingArea, pointInPolygon, isRingClosed } from './ring.js';
23
+
24
+ /**
25
+ * Whether a value looks like a bare position: an array of at least two
26
+ * finite numbers.
27
+ * @param {any} value
28
+ * @returns {boolean}
29
+ */
30
+ export function isPosition(value) {
31
+ return Array.isArray(value) && value.length >= 2
32
+ && typeof value[0] === 'number' && typeof value[1] === 'number';
33
+ }
34
+
35
+ /**
36
+ * The geometry inside a value: the value itself when it is already one,
37
+ * a Feature's `geometry`, or null when there is none (including a
38
+ * Feature whose geometry is legitimately null).
39
+ *
40
+ * A FeatureCollection has no single geometry and yields null — use
41
+ * {@link eachPosition} or {@link bboxOf}, which handle collections.
42
+ *
43
+ * @param {any} value
44
+ * @returns {object | null}
45
+ */
46
+ export function geometryOf(value) {
47
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
48
+ return null;
49
+ if (value.type === 'Feature')
50
+ return geometryOf(value.geometry);
51
+ return typeof value.type === 'string' && value.type !== 'FeatureCollection'
52
+ ? value
53
+ : null;
54
+ }
55
+
56
+ /**
57
+ * Call `visit(position)` for every position in a GeoJSON value, in
58
+ * document order. Accepts a bare position, a geometry, a Feature, a
59
+ * FeatureCollection or a GeometryCollection.
60
+ * @param {any} value
61
+ * @param {(position: number[]) => void} visit
62
+ */
63
+ export function eachPosition(value, visit) {
64
+ if (isPosition(value)) {
65
+ visit(/** @type {number[]} */(value));
66
+ return;
67
+ }
68
+ if (value === null || typeof value !== 'object')
69
+ return;
70
+ if (Array.isArray(value)) { // a nest of coordinates
71
+ // a ring or line is an array OF positions, which is the overwhelming
72
+ // majority of what this walks; classifying each member here saves a
73
+ // call frame and a re-entry per vertex
74
+ for (let i = 0; i < value.length; i++) {
75
+ const item = value[i];
76
+ if (isPosition(item))
77
+ visit(item);
78
+ else
79
+ eachPosition(item, visit);
80
+ }
81
+ return;
82
+ }
83
+ if (value.type === 'FeatureCollection') {
84
+ const features = value.features;
85
+ if (Array.isArray(features)) {
86
+ for (let i = 0; i < features.length; i++)
87
+ eachPosition(features[i], visit);
88
+ }
89
+ return;
90
+ }
91
+ if (value.type === 'Feature') {
92
+ eachPosition(value.geometry, visit);
93
+ return;
94
+ }
95
+ if (value.type === 'GeometryCollection') {
96
+ const geometries = value.geometries;
97
+ if (Array.isArray(geometries)) {
98
+ for (let i = 0; i < geometries.length; i++)
99
+ eachPosition(geometries[i], visit);
100
+ }
101
+ return;
102
+ }
103
+ eachPosition(value.coordinates, visit);
104
+ }
105
+
106
+ /**
107
+ * The bounding box of any GeoJSON value, as `[west, south, east, north]`.
108
+ * Null when the value contains no positions.
109
+ *
110
+ * A `bbox` member already present on the value is ignored: it is an
111
+ * optimization the producer may have got wrong, and recomputing is the
112
+ * only way to be sure.
113
+ *
114
+ * @param {any} value
115
+ * @returns {number[] | null}
116
+ */
117
+ export function bboxOf(value) {
118
+ // four scalars rather than a box per position: the obvious spelling
119
+ // (union a fresh [x,y,x,y] into an accumulator) allocates twice for
120
+ // every vertex, which on a 2000-vertex ring is 4000 short-lived arrays
121
+ let west = Infinity;
122
+ let south = Infinity;
123
+ let east = -Infinity;
124
+ let north = -Infinity;
125
+ let seen = false;
126
+ eachPosition(value, (p) => {
127
+ const x = p[0];
128
+ const y = p[1];
129
+ seen = true;
130
+ if (x < west) west = x;
131
+ if (x > east) east = x;
132
+ if (y < south) south = y;
133
+ if (y > north) north = y;
134
+ });
135
+ return seen ? [west, south, east, north] : null;
136
+ }
137
+
138
+ /**
139
+ * Every position of a GeoJSON value, flattened into one array.
140
+ * @param {any} value
141
+ * @returns {Array<number[]>}
142
+ */
143
+ export function positionsOf(value) {
144
+ const out = [];
145
+ eachPosition(value, (p) => out.push(p));
146
+ return out;
147
+ }
148
+
149
+ /**
150
+ * Geodesic area of a value in square metres: the sum over its polygons
151
+ * of the exterior ring's area less its holes'. Anything with no surface
152
+ * — a point, a line — has area 0 rather than being an error.
153
+ * @param {any} value
154
+ * @param {number} [radius]
155
+ * @returns {number}
156
+ */
157
+ export function geometryArea(value, radius = EARTH_RADIUS) {
158
+ if (value === null || typeof value !== 'object')
159
+ return 0;
160
+ if (value.type === 'FeatureCollection') {
161
+ let sum = 0;
162
+ for (const feature of value.features ?? [])
163
+ sum += geometryArea(feature, radius);
164
+ return sum;
165
+ }
166
+ const geometry = geometryOf(value);
167
+ if (geometry === null)
168
+ return 0;
169
+ if (geometry.type === 'GeometryCollection') {
170
+ let sum = 0;
171
+ for (const inner of geometry.geometries ?? [])
172
+ sum += geometryArea(inner, radius);
173
+ return sum;
174
+ }
175
+ if (geometry.type === 'Polygon')
176
+ return polygonArea(geometry.coordinates, radius);
177
+ if (geometry.type === 'MultiPolygon') {
178
+ let sum = 0;
179
+ for (const rings of geometry.coordinates ?? [])
180
+ sum += polygonArea(rings, radius);
181
+ return sum;
182
+ }
183
+ return 0;
184
+ }
185
+
186
+ // exterior ring less holes
187
+ function polygonArea(rings, radius) {
188
+ if (!Array.isArray(rings) || rings.length === 0)
189
+ return 0;
190
+ let area = sphericalRingArea(rings[0], radius);
191
+ for (let i = 1; i < rings.length; i++)
192
+ area -= sphericalRingArea(rings[i], radius);
193
+ return area > 0 ? area : 0;
194
+ }
195
+
196
+ /**
197
+ * Great-circle length of a value in metres: the sum of its line
198
+ * lengths, and of its polygon ring perimeters. A point has length 0.
199
+ * @param {any} value
200
+ * @param {number} [radius]
201
+ * @returns {number}
202
+ */
203
+ export function geometryLength(value, radius = EARTH_RADIUS) {
204
+ if (value === null || typeof value !== 'object')
205
+ return 0;
206
+ if (value.type === 'FeatureCollection') {
207
+ let sum = 0;
208
+ for (const feature of value.features ?? [])
209
+ sum += geometryLength(feature, radius);
210
+ return sum;
211
+ }
212
+ const geometry = geometryOf(value);
213
+ if (geometry === null)
214
+ return 0;
215
+ const { type, coordinates } = geometry;
216
+ if (type === 'GeometryCollection') {
217
+ let sum = 0;
218
+ for (const inner of geometry.geometries ?? [])
219
+ sum += geometryLength(inner, radius);
220
+ return sum;
221
+ }
222
+ if (type === 'LineString')
223
+ return lineLength(coordinates ?? [], radius);
224
+ if (type === 'MultiLineString' || type === 'Polygon') {
225
+ let sum = 0;
226
+ for (const line of coordinates ?? [])
227
+ sum += lineLength(line, radius);
228
+ return sum;
229
+ }
230
+ if (type === 'MultiPolygon') {
231
+ let sum = 0;
232
+ for (const rings of coordinates ?? []) {
233
+ for (const ring of rings ?? [])
234
+ sum += lineLength(ring, radius);
235
+ }
236
+ return sum;
237
+ }
238
+ return 0;
239
+ }
240
+
241
+ /**
242
+ * The centroid of a value's positions, as a GeoJSON position. Null when
243
+ * there are none.
244
+ *
245
+ * This is the mean of the vertices, NOT the area-weighted centre of
246
+ * mass: it is the cheap answer, it always lies in the convex hull, and
247
+ * for a concave shape it can fall outside the polygon. Labelling and
248
+ * clustering want this; a physics centre of mass does not.
249
+ *
250
+ * @param {any} value
251
+ * @returns {[number, number] | null}
252
+ */
253
+ export function centroidOf(value) {
254
+ // typed accumulators, not closure variables: a captured `sum += p[0]`
255
+ // writes a boxed heap number per vertex, and this is two of those per
256
+ // vertex on a walk whose other work is four comparisons
257
+ const acc = new Float64Array(3);
258
+ eachPosition(value, (p) => {
259
+ acc[0] += p[0];
260
+ acc[1] += p[1];
261
+ acc[2]++;
262
+ });
263
+ return acc[2] === 0 ? null : [acc[0] / acc[2], acc[1] / acc[2]];
264
+ }
265
+
266
+ /**
267
+ * Whether a position lies inside a value's surface. Only polygons have
268
+ * an inside, so a point or a line always answers false.
269
+ * @param {any} value - a Polygon, MultiPolygon, Feature or collection
270
+ * @param {number} lon
271
+ * @param {number} lat
272
+ * @returns {boolean}
273
+ */
274
+ export function containsPosition(value, lon, lat) {
275
+ if (value === null || typeof value !== 'object')
276
+ return false;
277
+ if (value.type === 'FeatureCollection') {
278
+ for (const feature of value.features ?? []) {
279
+ if (containsPosition(feature, lon, lat))
280
+ return true;
281
+ }
282
+ return false;
283
+ }
284
+ const geometry = geometryOf(value);
285
+ if (geometry === null)
286
+ return false;
287
+ if (geometry.type === 'GeometryCollection') {
288
+ for (const inner of geometry.geometries ?? []) {
289
+ if (containsPosition(inner, lon, lat))
290
+ return true;
291
+ }
292
+ return false;
293
+ }
294
+ if (geometry.type === 'Polygon')
295
+ return pointInPolygon(lon, lat, geometry.coordinates ?? []);
296
+ if (geometry.type === 'MultiPolygon') {
297
+ for (const rings of geometry.coordinates ?? []) {
298
+ if (pointInPolygon(lon, lat, rings))
299
+ return true;
300
+ }
301
+ return false;
302
+ }
303
+ return false;
304
+ }
305
+
306
+ /**
307
+ * The distance in metres between two GeoJSON values, measured between
308
+ * their **representative positions**: a bare position or Point is
309
+ * itself, anything else is its centroid. Null when either side has no
310
+ * positions.
311
+ *
312
+ * This is deliberately not the minimum distance between two shapes,
313
+ * which needs point-to-segment geodesics and is a different (and much
314
+ * larger) piece of work. Callers wanting "how far is this point from
315
+ * that region" should test containment first.
316
+ *
317
+ * @param {any} a
318
+ * @param {any} b
319
+ * @param {number} [radius]
320
+ * @returns {number | null}
321
+ */
322
+ export function geoDistance(a, b, radius = EARTH_RADIUS) {
323
+ const pa = isPosition(a) ? a : centroidOf(a);
324
+ const pb = isPosition(b) ? b : centroidOf(b);
325
+ if (pa === null || pb === null)
326
+ return null;
327
+ return haversineDistance(pa[0], pa[1], pb[0], pb[1], radius);
328
+ }
329
+
330
+ /**
331
+ * Whether every linear ring of a value is closed — the invariant the
332
+ * portable GeoJSON schema cannot express. True for values with no rings.
333
+ * @param {any} value
334
+ * @returns {boolean}
335
+ */
336
+ export function ringsClosed(value) {
337
+ const geometry = geometryOf(value);
338
+ if (value !== null && typeof value === 'object' && value.type === 'FeatureCollection') {
339
+ for (const feature of value.features ?? []) {
340
+ if (!ringsClosed(feature))
341
+ return false;
342
+ }
343
+ return true;
344
+ }
345
+ if (geometry === null)
346
+ return true;
347
+ if (geometry.type === 'GeometryCollection') {
348
+ for (const inner of geometry.geometries ?? []) {
349
+ if (!ringsClosed(inner))
350
+ return false;
351
+ }
352
+ return true;
353
+ }
354
+ if (geometry.type === 'Polygon')
355
+ return (geometry.coordinates ?? []).every(isRingClosed);
356
+ if (geometry.type === 'MultiPolygon')
357
+ return (geometry.coordinates ?? []).every((rings) => (rings ?? []).every(isRingClosed));
358
+ return true;
359
+ }
360
+
361
+ //#endregion