/* * GeodesicLine.js * Transcription of GeodesicLine.[ch]pp into JavaScript. * * See the documentation for the C++ class. The conversion is a literal * conversion from C++. * * The algorithms are derived in * * Charles F. F. Karney, * Algorithms for geodesics, J. Geodesy 87, 43-55 (2013); * https://doi.org/10.1007/s00190-012-0578-z * Addenda: https://geographiclib.sourceforge.io/geod-addenda.html * * Copyright (c) Charles Karney (2011-2019) and licensed * under the MIT/X11 License. For more information, see * https://geographiclib.sourceforge.io/ */ // Load AFTER GeographicLib/Math.js, GeographicLib/Geodesic.js (function( g, /** * @exports GeographicLib/GeodesicLine * @description Solve geodesic problems on a single geodesic line via the * {@link module:GeographicLib/GeodesicLine.GeodesicLine GeodesicLine} * class. */ l, m) { /** * @class * @property {number} a the equatorial radius (meters). * @property {number} f the flattening. * @property {number} lat1 the initial latitude (degrees). * @property {number} lon1 the initial longitude (degrees). * @property {number} azi1 the initial azimuth (degrees). * @property {number} salp1 the sine of the azimuth at the first point. * @property {number} calp1 the cosine the azimuth at the first point. * @property {number} s13 the distance to point 3 (meters). * @property {number} a13 the arc length to point 3 (degrees). * @property {bitmask} caps the capabilities of the object. * @summary Initialize a GeodesicLine object. For details on the caps * parameter, see {@tutorial 2-interface}, "The outmask and caps * parameters". * @classdesc Performs geodesic calculations along a given geodesic line. * This object is usually instantiated by * {@link module:GeographicLib/Geodesic.Geodesic#Line Geodesic.Line}. * The methods * {@link module:GeographicLib/Geodesic.Geodesic#DirectLine * Geodesic.DirectLine} and * {@link module:GeographicLib/Geodesic.Geodesic#InverseLine * Geodesic.InverseLine} set in addition the position of a reference point * 3. * @param {object} geod a {@link module:GeographicLib/Geodesic.Geodesic * Geodesic} object. * @param {number} lat1 the latitude of the first point in degrees. * @param {number} lon1 the longitude of the first point in degrees. * @param {number} azi1 the azimuth at the first point in degrees. * @param {bitmask} [caps = STANDARD | DISTANCE_IN] which capabilities to * include; LATITUDE | AZIMUTH are always included. */ l.GeodesicLine = function(geod, lat1, lon1, azi1, caps, salp1, calp1) { var t, cbet1, sbet1, eps, s, c; if (!caps) caps = g.STANDARD | g.DISTANCE_IN; this.a = geod.a; this.f = geod.f; this._b = geod._b; this._c2 = geod._c2; this._f1 = geod._f1; this.caps = caps | g.LATITUDE | g.AZIMUTH | g.LONG_UNROLL; this.lat1 = m.LatFix(lat1); this.lon1 = lon1; if (typeof salp1 === 'undefined' || typeof calp1 === 'undefined') { this.azi1 = m.AngNormalize(azi1); t = m.sincosd(m.AngRound(this.azi1)); this.salp1 = t.s; this.calp1 = t.c; } else { this.azi1 = azi1; this.salp1 = salp1; this.calp1 = calp1; } t = m.sincosd(m.AngRound(this.lat1)); sbet1 = this._f1 * t.s; cbet1 = t.c; // norm(sbet1, cbet1); t = m.hypot(sbet1, cbet1); sbet1 /= t; cbet1 /= t; // Ensure cbet1 = +epsilon at poles cbet1 = Math.max(g.tiny_, cbet1); this._dn1 = Math.sqrt(1 + geod._ep2 * m.sq(sbet1)); // Evaluate alp0 from sin(alp1) * cos(bet1) = sin(alp0), this._salp0 = this.salp1 * cbet1; // alp0 in [0, pi/2 - |bet1|] // Alt: calp0 = hypot(sbet1, calp1 * cbet1). The following // is slightly better (consider the case salp1 = 0). this._calp0 = m.hypot(this.calp1, this.salp1 * sbet1); // Evaluate sig with tan(bet1) = tan(sig1) * cos(alp1). // sig = 0 is nearest northward crossing of equator. // With bet1 = 0, alp1 = pi/2, we have sig1 = 0 (equatorial line). // With bet1 = pi/2, alp1 = -pi, sig1 = pi/2 // With bet1 = -pi/2, alp1 = 0 , sig1 = -pi/2 // Evaluate omg1 with tan(omg1) = sin(alp0) * tan(sig1). // With alp0 in (0, pi/2], quadrants for sig and omg coincide. // No atan2(0,0) ambiguity at poles since cbet1 = +epsilon. // With alp0 = 0, omg1 = 0 for alp1 = 0, omg1 = pi for alp1 = pi. this._ssig1 = sbet1; this._somg1 = this._salp0 * sbet1; this._csig1 = this._comg1 = sbet1 !== 0 || this.calp1 !== 0 ? cbet1 * this.calp1 : 1; // norm(this._ssig1, this._csig1); // sig1 in (-pi, pi] t = m.hypot(this._ssig1, this._csig1); this._ssig1 /= t; this._csig1 /= t; // norm(this._somg1, this._comg1); -- don't need to normalize! this._k2 = m.sq(this._calp0) * geod._ep2; eps = this._k2 / (2 * (1 + Math.sqrt(1 + this._k2)) + this._k2); if (this.caps & g.CAP_C1) { this._A1m1 = g.A1m1f(eps); this._C1a = new Array(g.nC1_ + 1); g.C1f(eps, this._C1a); this._B11 = g.SinCosSeries(true, this._ssig1, this._csig1, this._C1a); s = Math.sin(this._B11); c = Math.cos(this._B11); // tau1 = sig1 + B11 this._stau1 = this._ssig1 * c + this._csig1 * s; this._ctau1 = this._csig1 * c - this._ssig1 * s; // Not necessary because C1pa reverts C1a // _B11 = -SinCosSeries(true, _stau1, _ctau1, _C1pa); } if (this.caps & g.CAP_C1p) { this._C1pa = new Array(g.nC1p_ + 1); g.C1pf(eps, this._C1pa); } if (this.caps & g.CAP_C2) { this._A2m1 = g.A2m1f(eps); this._C2a = new Array(g.nC2_ + 1); g.C2f(eps, this._C2a); this._B21 = g.SinCosSeries(true, this._ssig1, this._csig1, this._C2a); } if (this.caps & g.CAP_C3) { this._C3a = new Array(g.nC3_); geod.C3f(eps, this._C3a); this._A3c = -this.f * this._salp0 * geod.A3f(eps); this._B31 = g.SinCosSeries(true, this._ssig1, this._csig1, this._C3a); } if (this.caps & g.CAP_C4) { this._C4a = new Array(g.nC4_); // all the elements of _C4a are used geod.C4f(eps, this._C4a); // Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0) this._A4 = m.sq(this.a) * this._calp0 * this._salp0 * geod._e2; this._B41 = g.SinCosSeries(false, this._ssig1, this._csig1, this._C4a); } this.a13 = this.s13 = Number.NaN; }; /** * @summary Find the position on the line (general case). * @param {bool} arcmode is the next parameter an arc length? * @param {number} s12_a12 the (arcmode ? arc length : distance) from the * first point to the second in (arcmode ? degrees : meters). * @param {bitmask} [outmask = STANDARD] which results to include; this is * subject to the capabilities of the object. * @returns {object} the requested results. * @description The lat1, lon1, azi1, and a12 fields of the result are * always set; s12 is included if arcmode is false. For details on the * outmask parameter, see {@tutorial 2-interface}, "The outmask and caps * parameters". */ l.GeodesicLine.prototype.GenPosition = function(arcmode, s12_a12, outmask) { var vals = {}, sig12, ssig12, csig12, B12, AB1, ssig2, csig2, tau12, s, c, serr, omg12, lam12, lon12, E, sbet2, cbet2, somg2, comg2, salp2, calp2, dn2, B22, AB2, J12, t, B42, salp12, calp12; if (!outmask) outmask = g.STANDARD; else if (outmask === g.LONG_UNROLL) outmask |= g.STANDARD; outmask &= this.caps & g.OUT_MASK; vals.lat1 = this.lat1; vals.azi1 = this.azi1; vals.lon1 = outmask & g.LONG_UNROLL ? this.lon1 : m.AngNormalize(this.lon1); if (arcmode) vals.a12 = s12_a12; else vals.s12 = s12_a12; if (!( arcmode || (this.caps & g.DISTANCE_IN & g.OUT_MASK) )) { // Uninitialized or impossible distance calculation requested vals.a12 = Number.NaN; return vals; } // Avoid warning about uninitialized B12. B12 = 0; AB1 = 0; if (arcmode) { // Interpret s12_a12 as spherical arc length sig12 = s12_a12 * m.degree; t = m.sincosd(s12_a12); ssig12 = t.s; csig12 = t.c; } else { // Interpret s12_a12 as distance tau12 = s12_a12 / (this._b * (1 + this._A1m1)); s = Math.sin(tau12); c = Math.cos(tau12); // tau2 = tau1 + tau12 B12 = -g.SinCosSeries(true, this._stau1 * c + this._ctau1 * s, this._ctau1 * c - this._stau1 * s, this._C1pa); sig12 = tau12 - (B12 - this._B11); ssig12 = Math.sin(sig12); csig12 = Math.cos(sig12); if (Math.abs(this.f) > 0.01) { // Reverted distance series is inaccurate for |f| > 1/100, so correct // sig12 with 1 Newton iteration. The following table shows the // approximate maximum error for a = WGS_a() and various f relative to // GeodesicExact. // erri = the error in the inverse solution (nm) // errd = the error in the direct solution (series only) (nm) // errda = the error in the direct solution // (series + 1 Newton) (nm) // // f erri errd errda // -1/5 12e6 1.2e9 69e6 // -1/10 123e3 12e6 765e3 // -1/20 1110 108e3 7155 // -1/50 18.63 200.9 27.12 // -1/100 18.63 23.78 23.37 // -1/150 18.63 21.05 20.26 // 1/150 22.35 24.73 25.83 // 1/100 22.35 25.03 25.31 // 1/50 29.80 231.9 30.44 // 1/20 5376 146e3 10e3 // 1/10 829e3 22e6 1.5e6 // 1/5 157e6 3.8e9 280e6 ssig2 = this._ssig1 * csig12 + this._csig1 * ssig12; csig2 = this._csig1 * csig12 - this._ssig1 * ssig12; B12 = g.SinCosSeries(true, ssig2, csig2, this._C1a); serr = (1 + this._A1m1) * (sig12 + (B12 - this._B11)) - s12_a12 / this._b; sig12 = sig12 - serr / Math.sqrt(1 + this._k2 * m.sq(ssig2)); ssig12 = Math.sin(sig12); csig12 = Math.cos(sig12); // Update B12 below } } // sig2 = sig1 + sig12 ssig2 = this._ssig1 * csig12 + this._csig1 * ssig12; csig2 = this._csig1 * csig12 - this._ssig1 * ssig12; dn2 = Math.sqrt(1 + this._k2 * m.sq(ssig2)); if (outmask & (g.DISTANCE | g.REDUCEDLENGTH | g.GEODESICSCALE)) { if (arcmode || Math.abs(this.f) > 0.01) B12 = g.SinCosSeries(true, ssig2, csig2, this._C1a); AB1 = (1 + this._A1m1) * (B12 - this._B11); } // sin(bet2) = cos(alp0) * sin(sig2) sbet2 = this._calp0 * ssig2; // Alt: cbet2 = hypot(csig2, salp0 * ssig2); cbet2 = m.hypot(this._salp0, this._calp0 * csig2); if (cbet2 === 0) // I.e., salp0 = 0, csig2 = 0. Break the degeneracy in this case cbet2 = csig2 = g.tiny_; // tan(alp0) = cos(sig2)*tan(alp2) salp2 = this._salp0; calp2 = this._calp0 * csig2; // No need to normalize if (arcmode && (outmask & g.DISTANCE)) vals.s12 = this._b * ((1 + this._A1m1) * sig12 + AB1); if (outmask & g.LONGITUDE) { // tan(omg2) = sin(alp0) * tan(sig2) somg2 = this._salp0 * ssig2; comg2 = csig2; // No need to normalize E = m.copysign(1, this._salp0); // omg12 = omg2 - omg1 omg12 = outmask & g.LONG_UNROLL ? E * (sig12 - (Math.atan2(ssig2, csig2) - Math.atan2(this._ssig1, this._csig1)) + (Math.atan2(E * somg2, comg2) - Math.atan2(E * this._somg1, this._comg1))) : Math.atan2(somg2 * this._comg1 - comg2 * this._somg1, comg2 * this._comg1 + somg2 * this._somg1); lam12 = omg12 + this._A3c * ( sig12 + (g.SinCosSeries(true, ssig2, csig2, this._C3a) - this._B31)); lon12 = lam12 / m.degree; vals.lon2 = outmask & g.LONG_UNROLL ? this.lon1 + lon12 : m.AngNormalize(m.AngNormalize(this.lon1) + m.AngNormalize(lon12)); } if (outmask & g.LATITUDE) vals.lat2 = m.atan2d(sbet2, this._f1 * cbet2); if (outmask & g.AZIMUTH) vals.azi2 = m.atan2d(salp2, calp2); if (outmask & (g.REDUCEDLENGTH | g.GEODESICSCALE)) { B22 = g.SinCosSeries(true, ssig2, csig2, this._C2a); AB2 = (1 + this._A2m1) * (B22 - this._B21); J12 = (this._A1m1 - this._A2m1) * sig12 + (AB1 - AB2); if (outmask & g.REDUCEDLENGTH) // Add parens around (_csig1 * ssig2) and (_ssig1 * csig2) to ensure // accurate cancellation in the case of coincident points. vals.m12 = this._b * (( dn2 * (this._csig1 * ssig2) - this._dn1 * (this._ssig1 * csig2)) - this._csig1 * csig2 * J12); if (outmask & g.GEODESICSCALE) { t = this._k2 * (ssig2 - this._ssig1) * (ssig2 + this._ssig1) / (this._dn1 + dn2); vals.M12 = csig12 + (t * ssig2 - csig2 * J12) * this._ssig1 / this._dn1; vals.M21 = csig12 - (t * this._ssig1 - this._csig1 * J12) * ssig2 / dn2; } } if (outmask & g.AREA) { B42 = g.SinCosSeries(false, ssig2, csig2, this._C4a); if (this._calp0 === 0 || this._salp0 === 0) { // alp12 = alp2 - alp1, used in atan2 so no need to normalize salp12 = salp2 * this.calp1 - calp2 * this.salp1; calp12 = calp2 * this.calp1 + salp2 * this.salp1; } else { // tan(alp) = tan(alp0) * sec(sig) // tan(alp2-alp1) = (tan(alp2) -tan(alp1)) / (tan(alp2)*tan(alp1)+1) // = calp0 * salp0 * (csig1-csig2) / (salp0^2 + calp0^2 * csig1*csig2) // If csig12 > 0, write // csig1 - csig2 = ssig12 * (csig1 * ssig12 / (1 + csig12) + ssig1) // else // csig1 - csig2 = csig1 * (1 - csig12) + ssig12 * ssig1 // No need to normalize salp12 = this._calp0 * this._salp0 * (csig12 <= 0 ? this._csig1 * (1 - csig12) + ssig12 * this._ssig1 : ssig12 * (this._csig1 * ssig12 / (1 + csig12) + this._ssig1)); calp12 = m.sq(this._salp0) + m.sq(this._calp0) * this._csig1 * csig2; } vals.S12 = this._c2 * Math.atan2(salp12, calp12) + this._A4 * (B42 - this._B41); } if (!arcmode) vals.a12 = sig12 / m.degree; return vals; }; /** * @summary Find the position on the line given s12. * @param {number} s12 the distance from the first point to the second in * meters. * @param {bitmask} [outmask = STANDARD] which results to include; this is * subject to the capabilities of the object. * @returns {object} the requested results. * @description The lat1, lon1, azi1, s12, and a12 fields of the result are * always set; s12 is included if arcmode is false. For details on the * outmask parameter, see {@tutorial 2-interface}, "The outmask and caps * parameters". */ l.GeodesicLine.prototype.Position = function(s12, outmask) { return this.GenPosition(false, s12, outmask); }; /** * @summary Find the position on the line given a12. * @param {number} a12 the arc length from the first point to the second in * degrees. * @param {bitmask} [outmask = STANDARD] which results to include; this is * subject to the capabilities of the object. * @returns {object} the requested results. * @description The lat1, lon1, azi1, and a12 fields of the result are * always set. For details on the outmask parameter, see {@tutorial * 2-interface}, "The outmask and caps parameters". */ l.GeodesicLine.prototype.ArcPosition = function(a12, outmask) { return this.GenPosition(true, a12, outmask); }; /** * @summary Specify position of point 3 in terms of either distance or arc * length. * @param {bool} arcmode boolean flag determining the meaning of the second * parameter; if arcmode is false, then the GeodesicLine object must have * been constructed with caps |= DISTANCE_IN. * @param {number} s13_a13 if arcmode is false, this is the distance from * point 1 to point 3 (meters); otherwise it is the arc length from * point 1 to point 3 (degrees); it can be negative. */ l.GeodesicLine.prototype.GenSetDistance = function(arcmode, s13_a13) { if (arcmode) this.SetArc(s13_a13); else this.SetDistance(s13_a13); }; /** * @summary Specify position of point 3 in terms distance. * @param {number} s13 the distance from point 1 to point 3 (meters); it * can be negative. */ l.GeodesicLine.prototype.SetDistance = function(s13) { var r; this.s13 = s13; r = this.GenPosition(false, this.s13, g.ARC); this.a13 = 0 + r.a12; // the 0+ converts undefined into NaN }; /** * @summary Specify position of point 3 in terms of arc length. * @param {number} a13 the arc length from point 1 to point 3 (degrees); * it can be negative. */ l.GeodesicLine.prototype.SetArc = function(a13) { var r; this.a13 = a13; r = this.GenPosition(true, this.a13, g.DISTANCE); this.s13 = 0 + r.s12; // the 0+ converts undefined into NaN }; })(GeographicLib.Geodesic, GeographicLib.GeodesicLine, GeographicLib.Math);