-
Notifications
You must be signed in to change notification settings - Fork 204
/
latlon-ellipsoidal-vincenty.js
333 lines (285 loc) · 15.3 KB
/
latlon-ellipsoidal-vincenty.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Vincenty Direct and Inverse Solution of Geodesics on the Ellipsoid (c) Chris Veness 2002-2020 */
/* MIT Licence */
/* www.movable-type.co.uk/scripts/latlong-ellipsoidal-vincenty.html */
/* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-ellipsoidal-vincenty */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
import LatLonEllipsoidal, { Dms } from './latlon-ellipsoidal.js';
const π = Math.PI;
const ε = Number.EPSILON;
/**
* Distances & bearings between points, and destination points given start points & initial bearings,
* calculated on an ellipsoidal earth model using ‘direct and inverse solutions of geodesics on the
* ellipsoid’ devised by Thaddeus Vincenty.
*
* From: T Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid with application of
* nested equations", Survey Review, vol XXIII no 176, 1975. www.ngs.noaa.gov/PUBS_LIB/inverse.pdf.
*
* @module latlon-ellipsoidal-vincenty
*/
/* LatLonEllipsoidal_Vincenty - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/**
* Extends LatLonEllipsoidal with methods for calculating distances and bearings between points, and
* destination points given distances and initial bearings, accurate to within 0.5mm distance,
* 0.000015″ bearing.
*
* By default, these calculations are made on a WGS-84 ellipsoid. For geodesic calculations on other
* ellipsoids, monkey-patch the LatLon point by setting the datum of ‘this’ point to make it appear
* as a LatLonEllipsoidal_Datum or LatLonEllipsoidal_ReferenceFrame point: e.g.
*
* import LatLon, { Dms } from '../latlon-ellipsoidal-vincenty.js';
* import { datums } from '../latlon-ellipsoidal-datum.js';
* const le = new LatLon(50.065716, -5.713824); // in OSGB-36
* const jog = new LatLon(58.644399, -3.068521); // in OSGB-36
* le.datum = datums.OSGB36; // source point determines ellipsoid to use
* const d = le.distanceTo(jog); // = 969982.014; 27.848m more than on WGS-84 ellipsoid
*
* @extends LatLonEllipsoidal
*/
class LatLonEllipsoidal_Vincenty extends LatLonEllipsoidal {
/**
* Returns the distance between ‘this’ point and destination point along a geodesic on the
* surface of the ellipsoid, using Vincenty inverse solution.
*
* @param {LatLon} point - Latitude/longitude of destination point.
* @returns {number} Distance in metres between points or NaN if failed to converge.
*
* @example
* const p1 = new LatLon(50.06632, -5.71475);
* const p2 = new LatLon(58.64402, -3.07009);
* const d = p1.distanceTo(p2); // 969,954.166 m
*/
distanceTo(point) {
try {
const dist = this.inverse(point).distance;
return Number(dist.toFixed(3)); // round to 1mm precision
} catch (e) {
if (e instanceof EvalError) return NaN; // λ > π or failed to converge
throw e;
}
}
/**
* Returns the initial bearing (forward azimuth) to travel along a geodesic from ‘this’ point to
* the given point, using Vincenty inverse solution.
*
* @param {LatLon} point - Latitude/longitude of destination point.
* @returns {number} Initial bearing in degrees from north (0°..360°) or NaN if failed to converge.
*
* @example
* const p1 = new LatLon(50.06632, -5.71475);
* const p2 = new LatLon(58.64402, -3.07009);
* const b1 = p1.initialBearingTo(p2); // 9.1419°
*/
initialBearingTo(point) {
try {
const brng = this.inverse(point).initialBearing;
return Number(brng.toFixed(7)); // round to 0.001″ precision
} catch (e) {
if (e instanceof EvalError) return NaN; // λ > π or failed to converge
throw e;
}
}
/**
* Returns the final bearing (reverse azimuth) having travelled along a geodesic from ‘this’
* point to the given point, using Vincenty inverse solution.
*
* @param {LatLon} point - Latitude/longitude of destination point.
* @returns {number} Final bearing in degrees from north (0°..360°) or NaN if failed to converge.
*
* @example
* const p1 = new LatLon(50.06632, -5.71475);
* const p2 = new LatLon(58.64402, -3.07009);
* const b2 = p1.finalBearingTo(p2); // 11.2972°
*/
finalBearingTo(point) {
try {
const brng = this.inverse(point).finalBearing;
return Number(brng.toFixed(7)); // round to 0.001″ precision
} catch (e) {
if (e instanceof EvalError) return NaN; // λ > π or failed to converge
throw e;
}
}
/**
* Returns the destination point having travelled the given distance along a geodesic given by
* initial bearing from ‘this’ point, using Vincenty direct solution.
*
* @param {number} distance - Distance travelled along the geodesic in metres.
* @param {number} initialBearing - Initial bearing in degrees from north.
* @returns {LatLon} Destination point.
*
* @example
* const p1 = new LatLon(-37.95103, 144.42487);
* const p2 = p1.destinationPoint(54972.271, 306.86816); // 37.6528°S, 143.9265°E
*/
destinationPoint(distance, initialBearing) {
return this.direct(Number(distance), Number(initialBearing)).point;
}
/**
* Returns the final bearing (reverse azimuth) having travelled along a geodesic given by initial
* bearing for a given distance from ‘this’ point, using Vincenty direct solution.
* TODO: arg order? (this is consistent with destinationPoint, but perhaps less intuitive)
*
* @param {number} distance - Distance travelled along the geodesic in metres.
* @param {LatLon} initialBearing - Initial bearing in degrees from north.
* @returns {number} Final bearing in degrees from north (0°..360°).
*
* @example
* const p1 = new LatLon(-37.95103, 144.42487);
* const b2 = p1.finalBearingOn(54972.271, 306.86816); // 307.1736°
*/
finalBearingOn(distance, initialBearing) {
const brng = this.direct(Number(distance), Number(initialBearing)).finalBearing;
return Number(brng.toFixed(7)); // round to 0.001″ precision
}
/**
* Returns the point at given fraction between ‘this’ point and given point.
*
* @param {LatLon} point - Latitude/longitude of destination point.
* @param {number} fraction - Fraction between the two points (0 = this point, 1 = specified point).
* @returns {LatLon} Intermediate point between this point and destination point.
*
* @example
* const p1 = new LatLon(50.06632, -5.71475);
* const p2 = new LatLon(58.64402, -3.07009);
* const pInt = p1.intermediatePointTo(p2, 0.5); // 54.3639°N, 004.5304°W
*/
intermediatePointTo(point, fraction) {
if (fraction == 0) return this;
if (fraction == 1) return point;
const inverse = this.inverse(point);
const dist = inverse.distance;
const brng = inverse.initialBearing;
return isNaN(brng) ? this : this.destinationPoint(dist*fraction, brng);
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/**
* Vincenty direct calculation.
*
* Ellipsoid parameters are taken from datum of 'this' point. Height is ignored.
*
* @private
* @param {number} distance - Distance along bearing in metres.
* @param {number} initialBearing - Initial bearing in degrees from north.
* @returns (Object} Object including point (destination point), finalBearing.
* @throws {RangeError} Point must be on surface of ellipsoid.
* @throws {EvalError} Formula failed to converge.
*/
direct(distance, initialBearing) {
if (isNaN(distance)) throw new TypeError(`invalid distance ${distance}`);
if (distance == 0) return { point: this, finalBearing: NaN, iterations: 0 };
if (isNaN(initialBearing)) throw new TypeError(`invalid bearing ${initialBearing}`);
if (this.height != 0) throw new RangeError('point must be on the surface of the ellipsoid');
const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians();
const α1 = Number(initialBearing).toRadians();
const s = Number(distance);
// allow alternative ellipsoid to be specified
const ellipsoid = this.datum ? this.datum.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const { a, b, f } = ellipsoid;
const sinα1 = Math.sin(α1);
const cosα1 = Math.cos(α1);
const tanU1 = (1-f) * Math.tan(φ1), cosU1 = 1 / Math.sqrt((1 + tanU1*tanU1)), sinU1 = tanU1 * cosU1;
const σ1 = Math.atan2(tanU1, cosα1); // σ1 = angular distance on the sphere from the equator to P1
const sinα = cosU1 * sinα1; // α = azimuth of the geodesic at the equator
const cosSqα = 1 - sinα*sinα;
const uSq = cosSqα * (a*a - b*b) / (b*b);
const A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
const B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
let σ = s / (b*A), sinσ = null, cosσ = null, Δσ = null; // σ = angular distance P₁ P₂ on the sphere
let cos2σₘ = null; // σₘ = angular distance on the sphere from the equator to the midpoint of the line
let σʹ = null, iterations = 0;
do {
cos2σₘ = Math.cos(2*σ1 + σ);
sinσ = Math.sin(σ);
cosσ = Math.cos(σ);
Δσ = B*sinσ*(cos2σₘ+B/4*(cosσ*(-1+2*cos2σₘ*cos2σₘ)-
B/6*cos2σₘ*(-3+4*sinσ*sinσ)*(-3+4*cos2σₘ*cos2σₘ)));
σʹ = σ;
σ = s / (b*A) + Δσ;
} while (Math.abs(σ-σʹ) > 1e-12 && ++iterations<100);
if (iterations >= 100) throw new EvalError('Vincenty formula failed to converge'); // not possible?
const x = sinU1*sinσ - cosU1*cosσ*cosα1;
const φ2 = Math.atan2(sinU1*cosσ + cosU1*sinσ*cosα1, (1-f)*Math.sqrt(sinα*sinα + x*x));
const λ = Math.atan2(sinσ*sinα1, cosU1*cosσ - sinU1*sinσ*cosα1);
const C = f/16*cosSqα*(4+f*(4-3*cosSqα));
const L = λ - (1-C) * f * sinα * (σ + C*sinσ*(cos2σₘ+C*cosσ*(-1+2*cos2σₘ*cos2σₘ)));
const λ2 = λ1 + L;
const α2 = Math.atan2(sinα, -x);
const destinationPoint = new LatLonEllipsoidal_Vincenty(φ2.toDegrees(), λ2.toDegrees(), 0, this.datum);
return {
point: destinationPoint,
finalBearing: Dms.wrap360(α2.toDegrees()),
iterations: iterations,
};
}
/**
* Vincenty inverse calculation.
*
* Ellipsoid parameters are taken from datum of 'this' point. Height is ignored.
*
* @private
* @param {LatLon} point - Latitude/longitude of destination point.
* @returns {Object} Object including distance, initialBearing, finalBearing.
* @throws {TypeError} Invalid point.
* @throws {RangeError} Points must be on surface of ellipsoid.
* @throws {EvalError} Formula failed to converge.
*/
inverse(point) {
if (!(point instanceof LatLonEllipsoidal)) throw new TypeError(`invalid point ‘${point}’`);
if (this.height!=0 || point.height!=0) throw new RangeError('point must be on the surface of the ellipsoid');
const p1 = this, p2 = point;
const φ1 = p1.lat.toRadians(), λ1 = p1.lon.toRadians();
const φ2 = p2.lat.toRadians(), λ2 = p2.lon.toRadians();
// allow alternative ellipsoid to be specified
const ellipsoid = this.datum ? this.datum.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const { a, b, f } = ellipsoid;
const L = λ2 - λ1; // L = difference in longitude, U = reduced latitude, defined by tan U = (1-f)·tanφ.
const tanU1 = (1-f) * Math.tan(φ1), cosU1 = 1 / Math.sqrt((1 + tanU1*tanU1)), sinU1 = tanU1 * cosU1;
const tanU2 = (1-f) * Math.tan(φ2), cosU2 = 1 / Math.sqrt((1 + tanU2*tanU2)), sinU2 = tanU2 * cosU2;
const antipodal = Math.abs(L) > π/2 || Math.abs(φ2-φ1) > π/2;
let λ = L, sinλ = null, cosλ = null; // λ = difference in longitude on an auxiliary sphere
let σ = antipodal ? π : 0, sinσ = 0, cosσ = antipodal ? -1 : 1, sinSqσ = null; // σ = angular distance P₁ P₂ on the sphere
let cos2σₘ = 1; // σₘ = angular distance on the sphere from the equator to the midpoint of the line
let sinα = null, cosSqα = 1; // α = azimuth of the geodesic at the equator
let C = null;
let λʹ = null, iterations = 0;
do {
sinλ = Math.sin(λ);
cosλ = Math.cos(λ);
sinSqσ = (cosU2*sinλ) * (cosU2*sinλ) + (cosU1*sinU2-sinU1*cosU2*cosλ) * (cosU1*sinU2-sinU1*cosU2*cosλ);
if (Math.abs(sinSqσ) < ε) break; // co-incident/antipodal points (falls back on λ/σ = L)
sinσ = Math.sqrt(sinSqσ);
cosσ = sinU1*sinU2 + cosU1*cosU2*cosλ;
σ = Math.atan2(sinσ, cosσ);
sinα = cosU1 * cosU2 * sinλ / sinσ;
cosSqα = 1 - sinα*sinα;
cos2σₘ = (cosSqα != 0) ? (cosσ - 2*sinU1*sinU2/cosSqα) : 0; // on equatorial line cos²α = 0 (§6)
C = f/16*cosSqα*(4+f*(4-3*cosSqα));
λʹ = λ;
λ = L + (1-C) * f * sinα * (σ + C*sinσ*(cos2σₘ+C*cosσ*(-1+2*cos2σₘ*cos2σₘ)));
const iterationCheck = antipodal ? Math.abs(λ)-π : Math.abs(λ);
if (iterationCheck > π) throw new EvalError('λ > π');
} while (Math.abs(λ-λʹ) > 1e-12 && ++iterations<1000);
if (iterations >= 1000) throw new EvalError('Vincenty formula failed to converge');
const uSq = cosSqα * (a*a - b*b) / (b*b);
const A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
const B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
const Δσ = B*sinσ*(cos2σₘ+B/4*(cosσ*(-1+2*cos2σₘ*cos2σₘ)-
B/6*cos2σₘ*(-3+4*sinσ*sinσ)*(-3+4*cos2σₘ*cos2σₘ)));
const s = b*A*(σ-Δσ); // s = length of the geodesic
// note special handling of exactly antipodal points where sin²σ = 0 (due to discontinuity
// atan2(0, 0) = 0 but atan2(ε, 0) = π/2 / 90°) - in which case bearing is always meridional,
// due north (or due south!)
// α = azimuths of the geodesic; α2 the direction P₁ P₂ produced
const α1 = Math.abs(sinSqσ) < ε ? 0 : Math.atan2(cosU2*sinλ, cosU1*sinU2-sinU1*cosU2*cosλ);
const α2 = Math.abs(sinSqσ) < ε ? π : Math.atan2(cosU1*sinλ, -sinU1*cosU2+cosU1*sinU2*cosλ);
return {
distance: s,
initialBearing: Math.abs(s) < ε ? NaN : Dms.wrap360(α1.toDegrees()),
finalBearing: Math.abs(s) < ε ? NaN : Dms.wrap360(α2.toDegrees()),
iterations: iterations,
};
}
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
export { LatLonEllipsoidal_Vincenty as default, Dms };