@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,294 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region Static bounding-box index
|
|
4
|
+
// A packed Hilbert R-tree over bounding boxes: build once, query many.
|
|
5
|
+
// Scanning n boxes to find the ones overlapping a query box is O(n); this
|
|
6
|
+
// is O(log n + k) for k hits, which is the difference between a spatial
|
|
7
|
+
// join being usable and not.
|
|
8
|
+
//
|
|
9
|
+
// **Static** is the deliberate part. A dynamic R-tree must keep its nodes
|
|
10
|
+
// splittable, which costs both memory and insert time; a compiled query's
|
|
11
|
+
// geometry never changes after the index is built, so the tree can be
|
|
12
|
+
// packed into flat typed arrays with no per-node objects at all. The
|
|
13
|
+
// tree is therefore built bottom-up: leaves are sorted along a Hilbert
|
|
14
|
+
// curve — which keeps boxes that are near each other in space near each
|
|
15
|
+
// other in the array — and then every `nodeSize` consecutive entries
|
|
16
|
+
// become a parent.
|
|
17
|
+
//
|
|
18
|
+
// The query returns **candidates**, not answers. Box overlap is a
|
|
19
|
+
// necessary condition for containment and for real intersection, never a
|
|
20
|
+
// sufficient one, so a caller confirms each candidate with the exact
|
|
21
|
+
// predicate. That is what makes indexing safe to apply automatically:
|
|
22
|
+
// the index can only ever remove work, never change a result.
|
|
23
|
+
|
|
24
|
+
/** Children per node. 16 is the usual sweet spot for cache behaviour. */
|
|
25
|
+
const DEFAULT_NODE_SIZE = 16;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Interleave the low 16 bits of x and y into a Hilbert distance.
|
|
29
|
+
* Boxes ordered by this stay spatially clustered, which is what makes
|
|
30
|
+
* the packed parents tight.
|
|
31
|
+
*
|
|
32
|
+
* Bit-parallel: all sixteen levels of the per-bit quadrant rotation run
|
|
33
|
+
* at once as mask arithmetic (the public-domain transform from
|
|
34
|
+
* rawrunprotected/hilbert_curves), which is what makes computing 100k of
|
|
35
|
+
* these a millisecond instead of the build's dominant cost. The values
|
|
36
|
+
* are identical to the classical per-bit walk of the curve.
|
|
37
|
+
*
|
|
38
|
+
* @param {number} x - 0..65535
|
|
39
|
+
* @param {number} y - 0..65535
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
export function hilbertDistance(x, y) {
|
|
43
|
+
let a = x ^ y;
|
|
44
|
+
let b = 0xFFFF ^ a;
|
|
45
|
+
let c = 0xFFFF ^ (x | y);
|
|
46
|
+
let d = x & (y ^ 0xFFFF);
|
|
47
|
+
let A = a | (b >> 1);
|
|
48
|
+
let B = (a >> 1) ^ a;
|
|
49
|
+
let C = ((c >> 1) ^ (b & (d >> 1))) ^ c;
|
|
50
|
+
let D = ((a & (c >> 1)) ^ (d >> 1)) ^ d;
|
|
51
|
+
|
|
52
|
+
a = A; b = B; c = C; d = D;
|
|
53
|
+
A = (a & (a >> 2)) ^ (b & (b >> 2));
|
|
54
|
+
B = (a & (b >> 2)) ^ (b & ((a ^ b) >> 2));
|
|
55
|
+
C ^= (a & (c >> 2)) ^ (b & (d >> 2));
|
|
56
|
+
D ^= (b & (c >> 2)) ^ ((a ^ b) & (d >> 2));
|
|
57
|
+
|
|
58
|
+
a = A; b = B; c = C; d = D;
|
|
59
|
+
A = (a & (a >> 4)) ^ (b & (b >> 4));
|
|
60
|
+
B = (a & (b >> 4)) ^ (b & ((a ^ b) >> 4));
|
|
61
|
+
C ^= (a & (c >> 4)) ^ (b & (d >> 4));
|
|
62
|
+
D ^= (b & (c >> 4)) ^ ((a ^ b) & (d >> 4));
|
|
63
|
+
|
|
64
|
+
a = A; b = B; c = C; d = D;
|
|
65
|
+
C ^= (a & (c >> 8)) ^ (b & (d >> 8));
|
|
66
|
+
D ^= (b & (c >> 8)) ^ ((a ^ b) & (d >> 8));
|
|
67
|
+
|
|
68
|
+
a = C ^ (C >> 1);
|
|
69
|
+
b = D ^ (D >> 1);
|
|
70
|
+
|
|
71
|
+
let i0 = x ^ y;
|
|
72
|
+
let i1 = b | (0xFFFF ^ (i0 | a));
|
|
73
|
+
|
|
74
|
+
i0 = (i0 | (i0 << 8)) & 0x00FF00FF;
|
|
75
|
+
i0 = (i0 | (i0 << 4)) & 0x0F0F0F0F;
|
|
76
|
+
i0 = (i0 | (i0 << 2)) & 0x33333333;
|
|
77
|
+
i0 = (i0 | (i0 << 1)) & 0x55555555;
|
|
78
|
+
|
|
79
|
+
i1 = (i1 | (i1 << 8)) & 0x00FF00FF;
|
|
80
|
+
i1 = (i1 | (i1 << 4)) & 0x0F0F0F0F;
|
|
81
|
+
i1 = (i1 | (i1 << 2)) & 0x33333333;
|
|
82
|
+
i1 = (i1 | (i1 << 1)) & 0x55555555;
|
|
83
|
+
|
|
84
|
+
return ((i1 << 1) | i0) >>> 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A queryable static index over bounding boxes.
|
|
89
|
+
* @typedef {Object} BboxIndex
|
|
90
|
+
* @property {number} size - how many boxes were indexed
|
|
91
|
+
* @property {(minX: number, minY: number, maxX: number, maxY: number) => number[]} search
|
|
92
|
+
* the indexes of every box whose bounds overlap the query box
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build a static index over `[west, south, east, north]` boxes.
|
|
97
|
+
*
|
|
98
|
+
* The returned `search` yields **candidate** indexes into the original
|
|
99
|
+
* array: every box that truly overlaps is included, and boxes that merely
|
|
100
|
+
* share a tree node may be too. Confirm each with the exact test.
|
|
101
|
+
*
|
|
102
|
+
* A null entry (a value with no positions, so no box) is indexed as a
|
|
103
|
+
* degenerate box that overlaps nothing, so it can never be a candidate
|
|
104
|
+
* while the array indexes stay aligned with the caller's data.
|
|
105
|
+
*
|
|
106
|
+
* @param {Array<number[] | null>} boxes
|
|
107
|
+
* @param {number} [nodeSize] - children per node
|
|
108
|
+
* @returns {BboxIndex}
|
|
109
|
+
* @example
|
|
110
|
+
* const index = createBboxIndex(regions.map(bboxOf));
|
|
111
|
+
* for (const i of index.search(...bboxOf(point))) confirm(regions[i]);
|
|
112
|
+
*/
|
|
113
|
+
export function createBboxIndex(boxes, nodeSize = DEFAULT_NODE_SIZE) {
|
|
114
|
+
const count = boxes.length;
|
|
115
|
+
if (count === 0)
|
|
116
|
+
return { size: 0, search: () => [] };
|
|
117
|
+
|
|
118
|
+
const size = Math.max(2, Math.min(nodeSize | 0, 65535));
|
|
119
|
+
|
|
120
|
+
// level 0 is the leaves; each level packs the previous one
|
|
121
|
+
const levelBounds = [];
|
|
122
|
+
let n = count;
|
|
123
|
+
let total = n;
|
|
124
|
+
levelBounds.push(n * 4);
|
|
125
|
+
do {
|
|
126
|
+
n = Math.ceil(n / size);
|
|
127
|
+
total += n;
|
|
128
|
+
levelBounds.push(total * 4);
|
|
129
|
+
} while (n !== 1);
|
|
130
|
+
|
|
131
|
+
const bounds = new Float64Array(total * 4);
|
|
132
|
+
const indices = new Uint32Array(total);
|
|
133
|
+
|
|
134
|
+
let minX = Infinity;
|
|
135
|
+
let minY = Infinity;
|
|
136
|
+
let maxX = -Infinity;
|
|
137
|
+
let maxY = -Infinity;
|
|
138
|
+
for (let i = 0; i < count; i++) {
|
|
139
|
+
const box = boxes[i];
|
|
140
|
+
if (box === null || box === undefined)
|
|
141
|
+
continue;
|
|
142
|
+
if (box[0] < minX) minX = box[0];
|
|
143
|
+
if (box[1] < minY) minY = box[1];
|
|
144
|
+
if (box[2] > maxX) maxX = box[2];
|
|
145
|
+
if (box[3] > maxY) maxY = box[3];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Sort a permutation along the Hilbert curve of the box centres, then
|
|
149
|
+
// write the bounds once, already in leaf order. The sort itself moves
|
|
150
|
+
// only a key and an index per swap — permuting the four-wide bounds
|
|
151
|
+
// rows through every partition swap is what made the build memory-bound.
|
|
152
|
+
const order = new Uint32Array(count);
|
|
153
|
+
for (let i = 0; i < count; i++)
|
|
154
|
+
order[i] = i;
|
|
155
|
+
if (count > 1 && Number.isFinite(minX)) {
|
|
156
|
+
const width = maxX - minX || 1;
|
|
157
|
+
const height = maxY - minY || 1;
|
|
158
|
+
const hilbert = new Uint32Array(count);
|
|
159
|
+
for (let i = 0; i < count; i++) {
|
|
160
|
+
const box = boxes[i];
|
|
161
|
+
if (box === null || box === undefined) {
|
|
162
|
+
// empties sort to the end (a real box may share this key, which
|
|
163
|
+
// is harmless: an empty's bounds can never match a query)
|
|
164
|
+
hilbert[i] = 0xFFFFFFFF;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const cx = Math.floor(65535 * ((box[0] + box[2]) / 2 - minX) / width);
|
|
168
|
+
const cy = Math.floor(65535 * ((box[1] + box[3]) / 2 - minY) / height);
|
|
169
|
+
hilbert[i] = hilbertDistance(cx, cy);
|
|
170
|
+
}
|
|
171
|
+
sortOrder(hilbert, order, 0, count - 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < count; i++) {
|
|
175
|
+
const at = order[i];
|
|
176
|
+
const box = boxes[at];
|
|
177
|
+
const p = i * 4;
|
|
178
|
+
if (box === null || box === undefined) {
|
|
179
|
+
// a box that overlaps nothing: min above max on both axes
|
|
180
|
+
bounds[p] = Infinity;
|
|
181
|
+
bounds[p + 1] = Infinity;
|
|
182
|
+
bounds[p + 2] = -Infinity;
|
|
183
|
+
bounds[p + 3] = -Infinity;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
bounds[p] = box[0];
|
|
187
|
+
bounds[p + 1] = box[1];
|
|
188
|
+
bounds[p + 2] = box[2];
|
|
189
|
+
bounds[p + 3] = box[3];
|
|
190
|
+
}
|
|
191
|
+
indices[i] = at;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// pack each level into the next
|
|
195
|
+
let readAt = 0;
|
|
196
|
+
for (let level = 0; level < levelBounds.length - 1; level++) {
|
|
197
|
+
const levelEnd = levelBounds[level] / 4;
|
|
198
|
+
let writeAt = levelEnd;
|
|
199
|
+
while (readAt < levelEnd) {
|
|
200
|
+
const nodeStart = readAt;
|
|
201
|
+
let nodeMinX = Infinity;
|
|
202
|
+
let nodeMinY = Infinity;
|
|
203
|
+
let nodeMaxX = -Infinity;
|
|
204
|
+
let nodeMaxY = -Infinity;
|
|
205
|
+
for (let i = 0; i < size && readAt < levelEnd; i++, readAt++) {
|
|
206
|
+
const p = readAt * 4;
|
|
207
|
+
if (bounds[p] < nodeMinX) nodeMinX = bounds[p];
|
|
208
|
+
if (bounds[p + 1] < nodeMinY) nodeMinY = bounds[p + 1];
|
|
209
|
+
if (bounds[p + 2] > nodeMaxX) nodeMaxX = bounds[p + 2];
|
|
210
|
+
if (bounds[p + 3] > nodeMaxY) nodeMaxY = bounds[p + 3];
|
|
211
|
+
}
|
|
212
|
+
// a parent records where its children start, not a data index
|
|
213
|
+
indices[writeAt] = nodeStart;
|
|
214
|
+
const q = writeAt * 4;
|
|
215
|
+
bounds[q] = nodeMinX;
|
|
216
|
+
bounds[q + 1] = nodeMinY;
|
|
217
|
+
bounds[q + 2] = nodeMaxX;
|
|
218
|
+
bounds[q + 3] = nodeMaxY;
|
|
219
|
+
writeAt++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const rootStart = total - 1;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
size: count,
|
|
227
|
+
search(qMinX, qMinY, qMaxX, qMaxY) {
|
|
228
|
+
const out = [];
|
|
229
|
+
// an explicit stack of [nodeIndex, levelEnd] rather than recursion
|
|
230
|
+
const stack = [rootStart, levelBounds.length - 1];
|
|
231
|
+
while (stack.length > 0) {
|
|
232
|
+
const level = stack.pop();
|
|
233
|
+
const nodeIndex = stack.pop();
|
|
234
|
+
const isLeafLevel = level === 0;
|
|
235
|
+
const end = Math.min(
|
|
236
|
+
nodeIndex + size,
|
|
237
|
+
levelBounds[level] / 4);
|
|
238
|
+
for (let pos = nodeIndex; pos < end; pos++) {
|
|
239
|
+
const p = pos * 4;
|
|
240
|
+
if (qMaxX < bounds[p] || qMaxY < bounds[p + 1]
|
|
241
|
+
|| qMinX > bounds[p + 2] || qMinY > bounds[p + 3])
|
|
242
|
+
continue;
|
|
243
|
+
if (isLeafLevel)
|
|
244
|
+
out.push(indices[pos]);
|
|
245
|
+
else {
|
|
246
|
+
stack.push(indices[pos]);
|
|
247
|
+
stack.push(level - 1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// In-place quicksort of the permutation by Hilbert distance: each swap
|
|
257
|
+
// moves one key and one index, nothing wider. Small partitions finish
|
|
258
|
+
// by insertion sort, which beats partitioning once a run fits in cache.
|
|
259
|
+
function sortOrder(hilbert, order, left, right) {
|
|
260
|
+
if (right - left < 20) {
|
|
261
|
+
for (let i = left + 1; i <= right; i++) {
|
|
262
|
+
const h = hilbert[i];
|
|
263
|
+
const n = order[i];
|
|
264
|
+
let j = i - 1;
|
|
265
|
+
while (j >= left && hilbert[j] > h) {
|
|
266
|
+
hilbert[j + 1] = hilbert[j];
|
|
267
|
+
order[j + 1] = order[j];
|
|
268
|
+
j--;
|
|
269
|
+
}
|
|
270
|
+
hilbert[j + 1] = h;
|
|
271
|
+
order[j + 1] = n;
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const pivot = hilbert[(left + right) >> 1];
|
|
276
|
+
let i = left - 1;
|
|
277
|
+
let j = right + 1;
|
|
278
|
+
for (;;) {
|
|
279
|
+
do i++; while (hilbert[i] < pivot);
|
|
280
|
+
do j--; while (hilbert[j] > pivot);
|
|
281
|
+
if (i >= j)
|
|
282
|
+
break;
|
|
283
|
+
const h = hilbert[i];
|
|
284
|
+
hilbert[i] = hilbert[j];
|
|
285
|
+
hilbert[j] = h;
|
|
286
|
+
const n = order[i];
|
|
287
|
+
order[i] = order[j];
|
|
288
|
+
order[j] = n;
|
|
289
|
+
}
|
|
290
|
+
sortOrder(hilbert, order, left, j);
|
|
291
|
+
sortOrder(hilbert, order, j + 1, right);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//#endregion
|
package/src/geo/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region @jarenjs/core/geo
|
|
4
|
+
// The suite's spatial kernel. There is no geometry type: the
|
|
5
|
+
// representation is GeoJSON (RFC 7946), whose positions are
|
|
6
|
+
// `[longitude, latitude]` arrays and whose rings are arrays of those —
|
|
7
|
+
// already JSON items, so they are patchable, schema-checkable and
|
|
8
|
+
// addressable by pointer and path. A wrapper class would break all four,
|
|
9
|
+
// the same way it does for dates.
|
|
10
|
+
//
|
|
11
|
+
// Everything here therefore takes plain numbers and plain arrays. A
|
|
12
|
+
// Polygon's `coordinates` IS an array of rings, so a function that takes
|
|
13
|
+
// a ring takes GeoJSON without needing to know the `type` discriminator;
|
|
14
|
+
// walking a tagged geometry belongs to the layer above.
|
|
15
|
+
//
|
|
16
|
+
// angle.js the degree↔radian factors the trigonometry shares
|
|
17
|
+
// predicates.js robust orientation — the sign every test rests on
|
|
18
|
+
// distance.js great-circle measurement over the WGS 84 sphere
|
|
19
|
+
// ring.js ring closure, winding, area and containment
|
|
20
|
+
// bbox.js bounding boxes: the cheap half of every spatial test
|
|
21
|
+
// geohash.js the string encoding that needs no new vocabulary
|
|
22
|
+
// geojson.js the one layer that knows the `type` discriminator
|
|
23
|
+
// valid.js the one-call structural judgment (rings must close)
|
|
24
|
+
// wkt.js a validity tester for the databases' text encoding
|
|
25
|
+
// index-tree.js a static packed-Hilbert box index for spatial joins
|
|
26
|
+
// mercator.js the projection out, for anything that draws a map
|
|
27
|
+
// simplify.js dropping the vertices that land on the same pixel
|
|
28
|
+
//
|
|
29
|
+
// RFC 7946 removed coordinate-reference-system support and mandates WGS
|
|
30
|
+
// 84 in decimal degrees, so there is deliberately no SRID table and no
|
|
31
|
+
// reprojection *in*: conformance removes the need rather than an
|
|
32
|
+
// omission hiding it. The one projection that does exist goes the other
|
|
33
|
+
// way — a sphere has to become a rectangle before anyone can look at it
|
|
34
|
+
// — and it comes with the rule that keeps the two apart: **never
|
|
35
|
+
// measure on a projected coordinate.** `geometryArea` and
|
|
36
|
+
// `haversineDistance` work on the sphere; `projectMercator` is for
|
|
37
|
+
// drawing, and a Mercator "area" is off by a factor of fourteen at
|
|
38
|
+
// Greenland.
|
|
39
|
+
|
|
40
|
+
export * from './predicates.js';
|
|
41
|
+
export * from './distance.js';
|
|
42
|
+
export * from './ring.js';
|
|
43
|
+
export * from './bbox.js';
|
|
44
|
+
export * from './geohash.js';
|
|
45
|
+
export * from './geojson.js';
|
|
46
|
+
export * from './valid.js';
|
|
47
|
+
export * from './wkt.js';
|
|
48
|
+
export * from './index-tree.js';
|
|
49
|
+
export * from './mercator.js';
|
|
50
|
+
export * from './simplify.js';
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region Web Mercator
|
|
4
|
+
// The projection maps go out through, and the only one this module
|
|
5
|
+
// family carries. RFC 7946 fixes the coordinate reference *system* to
|
|
6
|
+
// WGS 84, so there is nothing to configure on the way in; a projection
|
|
7
|
+
// is what happens on the way **out**, when a sphere has to become a
|
|
8
|
+
// rectangle somebody can draw.
|
|
9
|
+
//
|
|
10
|
+
// Web Mercator (EPSG:3857) is the choice because it is what every tile
|
|
11
|
+
// server, every slippy map and every reader's intuition already uses.
|
|
12
|
+
// Its costs are worth stating rather than discovering: it is conformal
|
|
13
|
+
// (shapes stay locally correct, which is why it looks right) but wildly
|
|
14
|
+
// area-distorting toward the poles — Greenland renders about the size of
|
|
15
|
+
// Africa and is fourteen times smaller. **Never measure on a projected
|
|
16
|
+
// coordinate.** `geometryArea` and `haversineDistance` work on the
|
|
17
|
+
// sphere for exactly this reason; these functions are for drawing.
|
|
18
|
+
//
|
|
19
|
+
// Latitude is clamped to ±85.051129°, where the projection reaches a
|
|
20
|
+
// square. Beyond that y runs to infinity, so the poles are not
|
|
21
|
+
// representable and a caller asking for them gets the edge instead of a
|
|
22
|
+
// NaN that would poison a whole path string.
|
|
23
|
+
|
|
24
|
+
import { clamp01 } from '../math/float64.js';
|
|
25
|
+
import { DEG, RAD } from './angle.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The latitude where Web Mercator's y reaches the edge of its square.
|
|
29
|
+
* Poleward of this the projection is unbounded.
|
|
30
|
+
*/
|
|
31
|
+
export const MERCATOR_MAX_LAT = 85.05112877980659;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Project a position into the Web Mercator unit square: x and y both in
|
|
35
|
+
* `[0, 1]`, with **y increasing southward** so the result is already in
|
|
36
|
+
* screen order and a renderer needs no flip.
|
|
37
|
+
*
|
|
38
|
+
* Latitude is clamped, longitude is not: a latitude past the Mercator
|
|
39
|
+
* limit has a defensible answer (the edge) while a longitude past ±180
|
|
40
|
+
* is simply invalid under RFC 7946, and folding it silently into range
|
|
41
|
+
* would move a shape to the wrong side of the world rather than let the
|
|
42
|
+
* defect show.
|
|
43
|
+
*
|
|
44
|
+
* @param {number} lon - longitude in degrees, `[-180, 180]`
|
|
45
|
+
* @param {number} lat - latitude in degrees, clamped to ±85.051129
|
|
46
|
+
* @returns {[number, number]} `[x, y]` in the unit square
|
|
47
|
+
* @example
|
|
48
|
+
* projectMercator(0, 0); // [0.5, 0.5] — null island is the centre
|
|
49
|
+
* projectMercator(-180, 85.05112877980659); // [0, 0] — the north-west corner
|
|
50
|
+
*/
|
|
51
|
+
export function projectMercator(lon, lat) {
|
|
52
|
+
const clamped = lat > MERCATOR_MAX_LAT
|
|
53
|
+
? MERCATOR_MAX_LAT
|
|
54
|
+
: (lat < -MERCATOR_MAX_LAT ? -MERCATOR_MAX_LAT : lat);
|
|
55
|
+
const x = (lon + 180) / 360;
|
|
56
|
+
const s = Math.sin(clamped * DEG);
|
|
57
|
+
// atanh(sin lat), spelled out: the log form is the standard one and
|
|
58
|
+
// avoids a tan that runs to infinity as the latitude nears the pole.
|
|
59
|
+
// Clamped because at exactly ±MERCATOR_MAX_LAT the true answer is 0 or
|
|
60
|
+
// 1 and the arithmetic lands an ULP outside — so the clamp is the more
|
|
61
|
+
// accurate value, not a fudge, and it makes the `[0, 1]` range a fact.
|
|
62
|
+
const y = clamp01(0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI));
|
|
63
|
+
return [x, y];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The inverse of {@link projectMercator}: a unit-square point back to a
|
|
68
|
+
* position. Needed to turn a click or a viewport corner back into
|
|
69
|
+
* longitude and latitude.
|
|
70
|
+
*
|
|
71
|
+
* @param {number} x - 0..1
|
|
72
|
+
* @param {number} y - 0..1, increasing southward
|
|
73
|
+
* @returns {[number, number]} `[longitude, latitude]` in degrees
|
|
74
|
+
*/
|
|
75
|
+
export function unprojectMercator(x, y) {
|
|
76
|
+
const lon = x * 360 - 180;
|
|
77
|
+
const lat = (2 * Math.atan(Math.exp((0.5 - y) * 2 * Math.PI)) - Math.PI / 2) * RAD;
|
|
78
|
+
return [lon, lat];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A projection fitted to a geographic bounding box: the transform that
|
|
83
|
+
* maps positions inside `bbox` into `[0, 1]` on both axes, **without
|
|
84
|
+
* distorting the aspect ratio**, centring whichever axis has room left
|
|
85
|
+
* over.
|
|
86
|
+
*
|
|
87
|
+
* Stretching a map to fill its frame is the single most common way to
|
|
88
|
+
* make one look wrong, so the fit is uniform by construction and the
|
|
89
|
+
* spare room becomes margin rather than distortion. `aspect` is the
|
|
90
|
+
* width:height ratio of the frame the result will be drawn into: the fit
|
|
91
|
+
* happens in a box `aspect` wide and 1 tall, then x is divided back down
|
|
92
|
+
* so both outputs are in `[0, 1]` — the same unit-square convention the
|
|
93
|
+
* treemap uses, where the renderer maps height from width by the same
|
|
94
|
+
* ratio.
|
|
95
|
+
*
|
|
96
|
+
* @param {number[]} bbox - `[west, south, east, north]`
|
|
97
|
+
* @param {number} [aspect] - frame width:height (default 1, a square)
|
|
98
|
+
* @returns {(lon: number, lat: number) => [number, number]}
|
|
99
|
+
* @example
|
|
100
|
+
* const fit = fitMercator([4, 52, 5, 53], 1.6);
|
|
101
|
+
* fit(4.5, 52.5); // near [0.5, 0.5] whatever the box's shape
|
|
102
|
+
*/
|
|
103
|
+
export function fitMercator(bbox, aspect = 1) {
|
|
104
|
+
const [x0, y1] = projectMercator(bbox[0], bbox[1]); // west, south -> y is max
|
|
105
|
+
const [x1, y0] = projectMercator(bbox[2], bbox[3]); // east, north -> y is min
|
|
106
|
+
const width = x1 - x0;
|
|
107
|
+
const height = y1 - y0;
|
|
108
|
+
// one scale for both axes — whichever runs out of frame first sets it
|
|
109
|
+
const scale = Math.min(
|
|
110
|
+
width > 0 ? aspect / width : Infinity,
|
|
111
|
+
height > 0 ? 1 / height : Infinity);
|
|
112
|
+
// a degenerate box (one position, or a box with no extent either way)
|
|
113
|
+
// has nothing to scale by; centre everything rather than divide by zero
|
|
114
|
+
if (!(scale > 0) || !Number.isFinite(scale))
|
|
115
|
+
return () => [0.5, 0.5];
|
|
116
|
+
const padX = (aspect - width * scale) / 2;
|
|
117
|
+
const padY = (1 - height * scale) / 2;
|
|
118
|
+
return (lon, lat) => {
|
|
119
|
+
const [px, py] = projectMercator(lon, lat);
|
|
120
|
+
return [((px - x0) * scale + padX) / aspect, (py - y0) * scale + padY];
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//#endregion
|