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