@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.
- 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,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
|