@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,63 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Interest math: simple and compound interest, nominal
|
|
4
|
+
* ↔ effective rate conversion, and continuous compounding. All pure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Simple interest accrued (not the total).
|
|
9
|
+
* @param {number} principal @param {number} rate per period @param {number} time periods
|
|
10
|
+
* @returns {number}
|
|
11
|
+
*/
|
|
12
|
+
export function simpleInterest(principal, rate, time) {
|
|
13
|
+
return principal * rate * time;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compound amount (principal + interest) after `time` years compounded
|
|
18
|
+
* `periodsPerYear` times per year.
|
|
19
|
+
* @param {number} principal @param {number} annualRate @param {number} time years
|
|
20
|
+
* @param {number} [periodsPerYear]
|
|
21
|
+
* @returns {number}
|
|
22
|
+
*/
|
|
23
|
+
export function compoundAmount(principal, annualRate, time, periodsPerYear = 1) {
|
|
24
|
+
return principal * Math.pow(1 + annualRate / periodsPerYear, periodsPerYear * time);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compound interest earned (amount − principal).
|
|
29
|
+
* @param {number} principal @param {number} annualRate @param {number} time
|
|
30
|
+
* @param {number} [periodsPerYear]
|
|
31
|
+
* @returns {number}
|
|
32
|
+
*/
|
|
33
|
+
export function compoundInterest(principal, annualRate, time, periodsPerYear = 1) {
|
|
34
|
+
return compoundAmount(principal, annualRate, time, periodsPerYear) - principal;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Effective annual rate (APY/EAR) from a nominal annual rate compounded
|
|
39
|
+
* `periodsPerYear` times.
|
|
40
|
+
* @param {number} nominal @param {number} periodsPerYear
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
export function nominalToEffective(nominal, periodsPerYear) {
|
|
44
|
+
return Math.pow(1 + nominal / periodsPerYear, periodsPerYear) - 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Nominal annual rate from an effective annual rate.
|
|
49
|
+
* @param {number} effective @param {number} periodsPerYear
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
export function effectiveToNominal(effective, periodsPerYear) {
|
|
53
|
+
return periodsPerYear * (Math.pow(1 + effective, 1 / periodsPerYear) - 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Continuously-compounded amount: `P·e^(rt)`.
|
|
58
|
+
* @param {number} principal @param {number} annualRate @param {number} time
|
|
59
|
+
* @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
export function continuousCompound(principal, annualRate, time) {
|
|
62
|
+
return principal * Math.exp(annualRate * time);
|
|
63
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Return & risk statistics: CAGR, holding-period
|
|
4
|
+
* return, volatility, Sharpe ratio and maximum drawdown. Pure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compound annual growth rate.
|
|
9
|
+
* @param {number} begin @param {number} end @param {number} years
|
|
10
|
+
* @returns {number}
|
|
11
|
+
*/
|
|
12
|
+
export function cagr(begin, end, years) {
|
|
13
|
+
if (begin <= 0 || years <= 0) return NaN;
|
|
14
|
+
return Math.pow(end / begin, 1 / years) - 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Holding-period return, including any income.
|
|
19
|
+
* @param {number} begin @param {number} end @param {number} [income]
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
export function holdingPeriodReturn(begin, end, income = 0) {
|
|
23
|
+
if (begin === 0) return NaN;
|
|
24
|
+
return (end - begin + income) / begin;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert a price series to a per-step simple-return series.
|
|
29
|
+
* @param {ArrayLike<number>} prices
|
|
30
|
+
* @returns {number[]}
|
|
31
|
+
*/
|
|
32
|
+
export function returnsOf(prices) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (let i = 1; i < prices.length; i++) {
|
|
35
|
+
out.push((prices[i] - prices[i - 1]) / prices[i - 1]);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Volatility: the (sample) standard deviation of a return series.
|
|
42
|
+
* @param {ArrayLike<number>} returns
|
|
43
|
+
* @param {boolean} [population] use population (÷N) instead of sample (÷N−1)
|
|
44
|
+
* @returns {number}
|
|
45
|
+
*/
|
|
46
|
+
export function volatility(returns, population = false) {
|
|
47
|
+
const n = returns.length;
|
|
48
|
+
if (n < 2) return 0;
|
|
49
|
+
let mean = 0;
|
|
50
|
+
for (let i = 0; i < n; i++) mean += returns[i];
|
|
51
|
+
mean /= n;
|
|
52
|
+
let variance = 0;
|
|
53
|
+
for (let i = 0; i < n; i++) variance += (returns[i] - mean) ** 2;
|
|
54
|
+
return Math.sqrt(variance / (population ? n : n - 1));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sharpe ratio: mean excess return over its volatility.
|
|
59
|
+
* @param {ArrayLike<number>} returns
|
|
60
|
+
* @param {number} [riskFree] per-step risk-free rate
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
export function sharpe(returns, riskFree = 0) {
|
|
64
|
+
const n = returns.length;
|
|
65
|
+
if (n === 0) return NaN;
|
|
66
|
+
let mean = 0;
|
|
67
|
+
for (let i = 0; i < n; i++) mean += returns[i];
|
|
68
|
+
mean /= n;
|
|
69
|
+
const vol = volatility(returns);
|
|
70
|
+
return vol === 0 ? NaN : (mean - riskFree) / vol;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Maximum drawdown of an equity curve: the largest peak-to-trough drop as
|
|
75
|
+
* a positive fraction (0.2 = a 20% drawdown).
|
|
76
|
+
* @param {ArrayLike<number>} values
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
export function maxDrawdown(values) {
|
|
80
|
+
let peak = -Infinity;
|
|
81
|
+
let maxDd = 0;
|
|
82
|
+
for (let i = 0; i < values.length; i++) {
|
|
83
|
+
if (values[i] > peak) peak = values[i];
|
|
84
|
+
if (peak > 0) {
|
|
85
|
+
const dd = (peak - values[i]) / peak;
|
|
86
|
+
if (dd > maxDd) maxDd = dd;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return maxDd;
|
|
90
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Time Value of Money. The five TVM quantities — N,
|
|
4
|
+
* I/Y, PV, PMT, FV — obey one identity; each function here solves for its
|
|
5
|
+
* own variable. Sign convention follows the standard cash-flow model
|
|
6
|
+
* (money in is positive, money out negative), matching Excel/HP-12C:
|
|
7
|
+
*
|
|
8
|
+
* PV·(1+i)^n + PMT·(1 + i·type)·((1+i)^n − 1)/i + FV = 0
|
|
9
|
+
*
|
|
10
|
+
* `type` is 0 for an ordinary annuity (payment at period end) or 1 for an
|
|
11
|
+
* annuity-due (payment at period begin). Rates are per period; a caller
|
|
12
|
+
* with an annual rate and monthly periods passes `rate/12`.
|
|
13
|
+
*
|
|
14
|
+
* Pure and zero-dep except `math/solve` for the iterative `rate`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { newtonRaphson, bisect } from '../math/solve.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Future value.
|
|
21
|
+
* @param {number} rate @param {number} nper @param {number} pmt
|
|
22
|
+
* @param {number} [pv] @param {number} [type]
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function fv(rate, nper, pmt, pv = 0, type = 0) {
|
|
26
|
+
if (rate === 0) return -(pv + pmt * nper);
|
|
27
|
+
const g = Math.pow(1 + rate, nper);
|
|
28
|
+
return -(pv * g + pmt * (1 + rate * type) * (g - 1) / rate);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Present value.
|
|
33
|
+
* @param {number} rate @param {number} nper @param {number} pmt
|
|
34
|
+
* @param {number} [fval] @param {number} [type]
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
export function pv(rate, nper, pmt, fval = 0, type = 0) {
|
|
38
|
+
if (rate === 0) return -(fval + pmt * nper);
|
|
39
|
+
const g = Math.pow(1 + rate, nper);
|
|
40
|
+
return -(fval + pmt * (1 + rate * type) * (g - 1) / rate) / g;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Payment per period.
|
|
45
|
+
* @param {number} rate @param {number} nper @param {number} pval
|
|
46
|
+
* @param {number} [fval] @param {number} [type]
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
export function pmt(rate, nper, pval, fval = 0, type = 0) {
|
|
50
|
+
if (nper === 0) return NaN;
|
|
51
|
+
if (rate === 0) return -(pval + fval) / nper;
|
|
52
|
+
const g = Math.pow(1 + rate, nper);
|
|
53
|
+
return -(pval * g + fval) / ((1 + rate * type) * (g - 1) / rate);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Number of periods.
|
|
58
|
+
* @param {number} rate @param {number} pmtv @param {number} pval
|
|
59
|
+
* @param {number} [fval] @param {number} [type]
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
export function nper(rate, pmtv, pval, fval = 0, type = 0) {
|
|
63
|
+
if (rate === 0) {
|
|
64
|
+
if (pmtv === 0) return NaN;
|
|
65
|
+
return -(pval + fval) / pmtv;
|
|
66
|
+
}
|
|
67
|
+
const a = pmtv * (1 + rate * type);
|
|
68
|
+
const num = a - fval * rate;
|
|
69
|
+
const den = a + pval * rate;
|
|
70
|
+
return Math.log(num / den) / Math.log(1 + rate);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Periodic interest rate, solved iteratively (Newton, bisection
|
|
75
|
+
* fallback). Returns `NaN` if no rate is found.
|
|
76
|
+
* @param {number} nperv @param {number} pmtv @param {number} pval
|
|
77
|
+
* @param {number} [fval] @param {number} [type] @param {number} [guess]
|
|
78
|
+
* @returns {number}
|
|
79
|
+
*/
|
|
80
|
+
export function rate(nperv, pmtv, pval, fval = 0, type = 0, guess = 0.1) {
|
|
81
|
+
const f = (r) => {
|
|
82
|
+
if (r === 0) return pval + pmtv * nperv + fval;
|
|
83
|
+
const g = Math.pow(1 + r, nperv);
|
|
84
|
+
return pval * g + pmtv * (1 + r * type) * (g - 1) / r + fval;
|
|
85
|
+
};
|
|
86
|
+
const df = (r) => {
|
|
87
|
+
const h = 1e-6;
|
|
88
|
+
return (f(r + h) - f(r - h)) / (2 * h);
|
|
89
|
+
};
|
|
90
|
+
const n = newtonRaphson(f, df, guess, { tol: 1e-9, maxIter: 100 });
|
|
91
|
+
if (n.converged && isFinite(n.root)) return n.root;
|
|
92
|
+
const b = bisect(f, -0.9999, 1e6, { tol: 1e-9, maxIter: 200 });
|
|
93
|
+
return b.converged ? b.root : NaN;
|
|
94
|
+
}
|
package/src/function.js
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
* @param {string | string[] | undefined} _key a key or an array of keys
|
|
15
15
|
* @returns {boolean} returns always true
|
|
16
16
|
*/
|
|
17
|
-
// eslint-disable-next-line no-unused-vars
|
|
18
17
|
export function trueThat(whatever, _path = undefined, _root = undefined, _key = undefined) {
|
|
19
18
|
const that = true;
|
|
20
19
|
return whatever === true || that;
|
|
@@ -29,7 +28,6 @@ export function trueThat(whatever, _path = undefined, _root = undefined, _key =
|
|
|
29
28
|
* @param {string | string[] | undefined} _key a key or an array of keys
|
|
30
29
|
* @returns {boolean} returns always true
|
|
31
30
|
*/
|
|
32
|
-
// eslint-disable-next-line no-unused-vars
|
|
33
31
|
export function falseThat(whatever, _path = undefined, _root = undefined, _key = undefined) {
|
|
34
32
|
// eslint-disable-next-line no-constant-binary-expression
|
|
35
33
|
return false && whatever;
|
package/src/geo/angle.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
// Degree↔radian conversion factors shared by every geo module that
|
|
4
|
+
// trades between RFC 7946's decimal degrees and the radians the
|
|
5
|
+
// trigonometry needs. Internal to the kernel: the barrel does not
|
|
6
|
+
// re-export them, because callers hold degrees end to end.
|
|
7
|
+
|
|
8
|
+
/** Radians per degree — multiply degrees by this to get radians. */
|
|
9
|
+
export const DEG = Math.PI / 180;
|
|
10
|
+
|
|
11
|
+
/** Degrees per radian — multiply radians by this to get degrees. */
|
|
12
|
+
export const RAD = 180 / Math.PI;
|
package/src/geo/bbox.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region Bounding boxes
|
|
4
|
+
// A GeoJSON `bbox` is a flat array `[west, south, east, north]` (RFC
|
|
5
|
+
// 7946 section 5): all axes of the most south-westerly position, then
|
|
6
|
+
// all axes of the most north-easterly. It stays a plain array here, so a
|
|
7
|
+
// computed box can be written straight into a document as its `bbox`
|
|
8
|
+
// member.
|
|
9
|
+
//
|
|
10
|
+
// Boxes are the cheap half of every spatial test. Overlap between two
|
|
11
|
+
// boxes is four comparisons, so it rejects the overwhelming majority of
|
|
12
|
+
// candidate pairs before any real geometry runs — the same role
|
|
13
|
+
// `equirectDistance` plays for distance, and the shape a spatial index
|
|
14
|
+
// ultimately accelerates.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The bounding box of a list of positions, as `[west, south, east, north]`.
|
|
18
|
+
* Returns null for an empty list — there is no box that bounds nothing,
|
|
19
|
+
* and an all-Infinity placeholder would silently intersect everything.
|
|
20
|
+
* @param {Array<number[]>} positions
|
|
21
|
+
* @returns {number[] | null}
|
|
22
|
+
*/
|
|
23
|
+
export function bboxOfPositions(positions) {
|
|
24
|
+
const n = positions.length;
|
|
25
|
+
if (n === 0)
|
|
26
|
+
return null;
|
|
27
|
+
let west = Infinity;
|
|
28
|
+
let south = Infinity;
|
|
29
|
+
let east = -Infinity;
|
|
30
|
+
let north = -Infinity;
|
|
31
|
+
for (let i = 0; i < n; i++) {
|
|
32
|
+
const x = positions[i][0];
|
|
33
|
+
const y = positions[i][1];
|
|
34
|
+
if (x < west) west = x;
|
|
35
|
+
if (x > east) east = x;
|
|
36
|
+
if (y < south) south = y;
|
|
37
|
+
if (y > north) north = y;
|
|
38
|
+
}
|
|
39
|
+
return [west, south, east, north];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether two bounding boxes share any area. Boxes that merely touch
|
|
44
|
+
* along an edge count as intersecting, matching `pointInRing`'s
|
|
45
|
+
* treatment of a boundary as inside.
|
|
46
|
+
* @param {number[]} a - [west, south, east, north]
|
|
47
|
+
* @param {number[]} b
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function bboxIntersects(a, b) {
|
|
51
|
+
return a[0] <= b[2] && b[0] <= a[2] && a[1] <= b[3] && b[1] <= a[3];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether a bounding box contains a position (edges included).
|
|
56
|
+
* @param {number[]} box - [west, south, east, north]
|
|
57
|
+
* @param {number} x - longitude
|
|
58
|
+
* @param {number} y - latitude
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
export function bboxContains(box, x, y) {
|
|
62
|
+
return x >= box[0] && x <= box[2] && y >= box[1] && y <= box[3];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The smallest box containing both inputs.
|
|
67
|
+
* @param {number[]} a
|
|
68
|
+
* @param {number[]} b
|
|
69
|
+
* @returns {number[]}
|
|
70
|
+
*/
|
|
71
|
+
export function bboxUnion(a, b) {
|
|
72
|
+
return [
|
|
73
|
+
a[0] < b[0] ? a[0] : b[0],
|
|
74
|
+
a[1] < b[1] ? a[1] : b[1],
|
|
75
|
+
a[2] > b[2] ? a[2] : b[2],
|
|
76
|
+
a[3] > b[3] ? a[3] : b[3],
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region Geodesic measurement
|
|
4
|
+
// Distance, bearing and destination on the WGS 84 sphere, over GeoJSON
|
|
5
|
+
// positions: `[longitude, latitude]` in decimal degrees, in that order
|
|
6
|
+
// (RFC 7946 section 3.1.1 — the order everyone gets backwards).
|
|
7
|
+
//
|
|
8
|
+
// Why this module has to exist at all: longitude degrees are not a fixed
|
|
9
|
+
// distance. A degree of longitude spans ~111 km at the equator and ~68 km
|
|
10
|
+
// at 52°N, so treating a position as a planar x/y point and taking the
|
|
11
|
+
// Euclidean norm is **64% wrong over 1 km at 52°N**. There is no
|
|
12
|
+
// "close enough" version of that mistake.
|
|
13
|
+
//
|
|
14
|
+
// Two formulas, because they trade accuracy against cost differently:
|
|
15
|
+
//
|
|
16
|
+
// haversine 13.6 ns/op, <0.5% error anywhere on the sphere
|
|
17
|
+
// equirectangular 2.0 ns/op, 0.02% at 430 km, 12.4% intercontinental
|
|
18
|
+
//
|
|
19
|
+
// So `equirectDistance` is the screening form — the cheap test that
|
|
20
|
+
// decides which candidates deserve the real one — and `haversineDistance`
|
|
21
|
+
// is the answer. That is the same build-then-probe shape the query
|
|
22
|
+
// engine's hash join uses, and the reason both are exported rather than
|
|
23
|
+
// one being hidden.
|
|
24
|
+
//
|
|
25
|
+
// The ellipsoid is deliberately not modelled. Vincenty is accurate to
|
|
26
|
+
// 0.5 mm but fails to converge near antipodal points, and Karney's 2013
|
|
27
|
+
// algorithm that fixes that is a much larger body of code; the sphere is
|
|
28
|
+
// within half a percent everywhere and is what GeoJSON consumers expect.
|
|
29
|
+
// A caller needing survey accuracy needs a geodesy library, and this
|
|
30
|
+
// says so rather than pretending.
|
|
31
|
+
|
|
32
|
+
import { DEG, RAD } from './angle.js';
|
|
33
|
+
|
|
34
|
+
/** IUGG mean Earth radius in metres — the sphere GeoJSON distances use. */
|
|
35
|
+
export const EARTH_RADIUS = 6371008.8;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Great-circle distance between two positions, in metres.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} lon1 - longitude of the first position, degrees
|
|
41
|
+
* @param {number} lat1 - latitude of the first position, degrees
|
|
42
|
+
* @param {number} lon2 - longitude of the second position, degrees
|
|
43
|
+
* @param {number} lat2 - latitude of the second position, degrees
|
|
44
|
+
* @param {number} [radius] - sphere radius, to answer in other units
|
|
45
|
+
* @returns {number} distance in metres (or in `radius`'s unit)
|
|
46
|
+
* @example
|
|
47
|
+
* haversineDistance(4.9041, 52.3676, 2.3522, 48.8566); // 429_862 (Amsterdam-Paris)
|
|
48
|
+
*/
|
|
49
|
+
export function haversineDistance(lon1, lat1, lon2, lat2, radius = EARTH_RADIUS) {
|
|
50
|
+
const p1 = lat1 * DEG;
|
|
51
|
+
const p2 = lat2 * DEG;
|
|
52
|
+
const dp = (lat2 - lat1) * DEG;
|
|
53
|
+
const dl = (lon2 - lon1) * DEG;
|
|
54
|
+
const sdp = Math.sin(dp * 0.5);
|
|
55
|
+
const sdl = Math.sin(dl * 0.5);
|
|
56
|
+
const a = sdp * sdp + Math.cos(p1) * Math.cos(p2) * sdl * sdl;
|
|
57
|
+
// asin form rather than atan2: same result, one fewer transcendental
|
|
58
|
+
return 2 * radius * Math.asin(Math.sqrt(a < 1 ? a : 1));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Equirectangular approximation of the distance between two positions,
|
|
63
|
+
* in metres — the **screening** form.
|
|
64
|
+
*
|
|
65
|
+
* Accurate to about 0.02% over a few hundred kilometres and roughly 12%
|
|
66
|
+
* across a hemisphere, for about a seventh of the cost. Use it to reject
|
|
67
|
+
* candidates that cannot be within a radius, then confirm the survivors
|
|
68
|
+
* with `haversineDistance`; do not report it as a measurement.
|
|
69
|
+
*
|
|
70
|
+
* @param {number} lon1
|
|
71
|
+
* @param {number} lat1
|
|
72
|
+
* @param {number} lon2
|
|
73
|
+
* @param {number} lat2
|
|
74
|
+
* @param {number} [radius]
|
|
75
|
+
* @returns {number} approximate distance
|
|
76
|
+
*/
|
|
77
|
+
export function equirectDistance(lon1, lat1, lon2, lat2, radius = EARTH_RADIUS) {
|
|
78
|
+
// the longitude difference is scaled by the cosine of the mean
|
|
79
|
+
// latitude, which is the whole correction the planar form is missing
|
|
80
|
+
const x = (lon2 - lon1) * DEG * Math.cos((lat1 + lat2) * 0.5 * DEG);
|
|
81
|
+
const y = (lat2 - lat1) * DEG;
|
|
82
|
+
return radius * Math.sqrt(x * x + y * y);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initial bearing from one position to another: degrees clockwise from
|
|
87
|
+
* true north, in `[0, 360)`.
|
|
88
|
+
*
|
|
89
|
+
* This is the bearing at the *start* of the great circle, and it changes
|
|
90
|
+
* along the path — a course of constant bearing is a rhumb line, which
|
|
91
|
+
* is a different curve.
|
|
92
|
+
*
|
|
93
|
+
* @param {number} lon1
|
|
94
|
+
* @param {number} lat1
|
|
95
|
+
* @param {number} lon2
|
|
96
|
+
* @param {number} lat2
|
|
97
|
+
* @returns {number} degrees in [0, 360)
|
|
98
|
+
*/
|
|
99
|
+
export function initialBearing(lon1, lat1, lon2, lat2) {
|
|
100
|
+
const p1 = lat1 * DEG;
|
|
101
|
+
const p2 = lat2 * DEG;
|
|
102
|
+
const dl = (lon2 - lon1) * DEG;
|
|
103
|
+
const y = Math.sin(dl) * Math.cos(p2);
|
|
104
|
+
const x = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(dl);
|
|
105
|
+
const deg = Math.atan2(y, x) * RAD;
|
|
106
|
+
return (deg + 360) % 360;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The position reached by travelling `distance` metres from a position
|
|
111
|
+
* along a constant initial bearing (a great-circle course).
|
|
112
|
+
*
|
|
113
|
+
* @param {number} lon - starting longitude, degrees
|
|
114
|
+
* @param {number} lat - starting latitude, degrees
|
|
115
|
+
* @param {number} bearing - degrees clockwise from true north
|
|
116
|
+
* @param {number} distance - metres
|
|
117
|
+
* @param {number} [radius]
|
|
118
|
+
* @returns {[number, number]} the destination as a GeoJSON position
|
|
119
|
+
*/
|
|
120
|
+
export function destinationPoint(lon, lat, bearing, distance, radius = EARTH_RADIUS) {
|
|
121
|
+
const d = distance / radius;
|
|
122
|
+
const b = bearing * DEG;
|
|
123
|
+
const p1 = lat * DEG;
|
|
124
|
+
const l1 = lon * DEG;
|
|
125
|
+
const sinP1 = Math.sin(p1);
|
|
126
|
+
const cosP1 = Math.cos(p1);
|
|
127
|
+
const sinD = Math.sin(d);
|
|
128
|
+
const cosD = Math.cos(d);
|
|
129
|
+
const sinP2 = sinP1 * cosD + cosP1 * sinD * Math.cos(b);
|
|
130
|
+
const p2 = Math.asin(sinP2 < -1 ? -1 : sinP2 > 1 ? 1 : sinP2);
|
|
131
|
+
const l2 = l1 + Math.atan2(
|
|
132
|
+
Math.sin(b) * sinD * cosP1,
|
|
133
|
+
cosD - sinP1 * sinP2);
|
|
134
|
+
// normalize longitude back into [-180, 180]
|
|
135
|
+
return [((l2 * RAD + 540) % 360) - 180, p2 * RAD];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Total great-circle length of a line of positions, in metres. An empty
|
|
140
|
+
* or single-position line has length 0.
|
|
141
|
+
* @param {Array<number[]>} positions - GeoJSON positions
|
|
142
|
+
* @param {number} [radius]
|
|
143
|
+
* @returns {number}
|
|
144
|
+
*/
|
|
145
|
+
export function lineLength(positions, radius = EARTH_RADIUS) {
|
|
146
|
+
let total = 0;
|
|
147
|
+
for (let i = 1; i < positions.length; i++) {
|
|
148
|
+
const a = positions[i - 1];
|
|
149
|
+
const b = positions[i];
|
|
150
|
+
total += haversineDistance(a[0], a[1], b[0], b[1], radius);
|
|
151
|
+
}
|
|
152
|
+
return total;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
//#endregion
|