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