Permalink
| /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
| /* Latitude/longitude spherical geodesy tools (c) Chris Veness 2002-2017 */ | |
| /* MIT Licence */ | |
| /* www.movable-type.co.uk/scripts/latlong.html */ | |
| /* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html */ | |
| /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
| 'use strict'; | |
| if (typeof module!='undefined' && module.exports) var Dms = require('./dms'); // ≡ import Dms from 'dms.js' | |
| /** | |
| * Library of geodesy functions for operations on a spherical earth model. | |
| * | |
| * @module latlon-spherical | |
| * @requires dms | |
| */ | |
| /** | |
| * Creates a LatLon point on the earth's surface at the specified latitude / longitude. | |
| * | |
| * @constructor | |
| * @param {number} lat - Latitude in degrees. | |
| * @param {number} lon - Longitude in degrees. | |
| * | |
| * @example | |
| * var p1 = new LatLon(52.205, 0.119); | |
| */ | |
| function LatLon(lat, lon) { | |
| // allow instantiation without 'new' | |
| if (!(this instanceof LatLon)) return new LatLon(lat, lon); | |
| this.lat = Number(lat); | |
| this.lon = Number(lon); | |
| } | |
| /** | |
| * Returns the distance from ‘this’ point to destination point (using haversine formula). | |
| * | |
| * @param {LatLon} point - Latitude/longitude of destination point. | |
| * @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres). | |
| * @returns {number} Distance between this point and destination point, in same units as radius. | |
| * | |
| * @example | |
| * var p1 = new LatLon(52.205, 0.119); | |
| * var p2 = new LatLon(48.857, 2.351); | |
| * var d = p1.distanceTo(p2); // 404.3 km | |
| */ | |
| LatLon.prototype.distanceTo = function(point, radius) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| radius = (radius === undefined) ? 6371e3 : Number(radius); | |
| var R = radius; | |
| var φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians(); | |
| var φ2 = point.lat.toRadians(), λ2 = point.lon.toRadians(); | |
| var Δφ = φ2 - φ1; | |
| var Δλ = λ2 - λ1; | |
| var a = Math.sin(Δφ/2) * Math.sin(Δφ/2) | |
| + Math.cos(φ1) * Math.cos(φ2) | |
| * Math.sin(Δλ/2) * Math.sin(Δλ/2); | |
| var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); | |
| var d = R * c; | |
| return d; | |
| }; | |
| /** | |
| * Returns the (initial) bearing from ‘this’ point to destination point. | |
| * | |
| * @param {LatLon} point - Latitude/longitude of destination point. | |
| * @returns {number} Initial bearing in degrees from north. | |
| * | |
| * @example | |
| * var p1 = new LatLon(52.205, 0.119); | |
| * var p2 = new LatLon(48.857, 2.351); | |
| * var b1 = p1.bearingTo(p2); // 156.2° | |
| */ | |
| LatLon.prototype.bearingTo = function(point) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| var φ1 = this.lat.toRadians(), φ2 = point.lat.toRadians(); | |
| var Δλ = (point.lon-this.lon).toRadians(); | |
| // see http://mathforum.org/library/drmath/view/55417.html | |
| var y = Math.sin(Δλ) * Math.cos(φ2); | |
| var x = Math.cos(φ1)*Math.sin(φ2) - | |
| Math.sin(φ1)*Math.cos(φ2)*Math.cos(Δλ); | |
| var θ = Math.atan2(y, x); | |
| return (θ.toDegrees()+360) % 360; | |
| }; | |
| /** | |
| * Returns final bearing arriving at destination destination point from ‘this’ point; the final bearing | |
| * will differ from the initial bearing by varying degrees according to distance and latitude. | |
| * | |
| * @param {LatLon} point - Latitude/longitude of destination point. | |
| * @returns {number} Final bearing in degrees from north. | |
| * | |
| * @example | |
| * var p1 = new LatLon(52.205, 0.119); | |
| * var p2 = new LatLon(48.857, 2.351); | |
| * var b2 = p1.finalBearingTo(p2); // 157.9° | |
| */ | |
| LatLon.prototype.finalBearingTo = function(point) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| // get initial bearing from destination point to this point & reverse it by adding 180° | |
| return ( point.bearingTo(this)+180 ) % 360; | |
| }; | |
| /** | |
| * Returns the midpoint between ‘this’ point and the supplied point. | |
| * | |
| * @param {LatLon} point - Latitude/longitude of destination point. | |
| * @returns {LatLon} Midpoint between this point and the supplied point. | |
| * | |
| * @example | |
| * var p1 = new LatLon(52.205, 0.119); | |
| * var p2 = new LatLon(48.857, 2.351); | |
| * var pMid = p1.midpointTo(p2); // 50.5363°N, 001.2746°E | |
| */ | |
| LatLon.prototype.midpointTo = function(point) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| // φm = atan2( sinφ1 + sinφ2, √( (cosφ1 + cosφ2⋅cosΔλ) ⋅ (cosφ1 + cosφ2⋅cosΔλ) ) + cos²φ2⋅sin²Δλ ) | |
| // λm = λ1 + atan2(cosφ2⋅sinΔλ, cosφ1 + cosφ2⋅cosΔλ) | |
| // see http://mathforum.org/library/drmath/view/51822.html for derivation | |
| var φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians(); | |
| var φ2 = point.lat.toRadians(); | |
| var Δλ = (point.lon-this.lon).toRadians(); | |
| var Bx = Math.cos(φ2) * Math.cos(Δλ); | |
| var By = Math.cos(φ2) * Math.sin(Δλ); | |
| var x = Math.sqrt((Math.cos(φ1) + Bx) * (Math.cos(φ1) + Bx) + By * By); | |
| var y = Math.sin(φ1) + Math.sin(φ2); | |
| var φ3 = Math.atan2(y, x); | |
| var λ3 = λ1 + Math.atan2(By, Math.cos(φ1) + Bx); | |
| return new LatLon(φ3.toDegrees(), (λ3.toDegrees()+540)%360-180); // normalise to −180..+180° | |
| }; | |
| /** | |
| * Returns the point at given fraction between ‘this’ point and specified 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 | |
| * let p1 = new LatLon(52.205, 0.119); | |
| * let p2 = new LatLon(48.857, 2.351); | |
| * let pMid = p1.intermediatePointTo(p2, 0.25); // 51.3721°N, 000.7073°E | |
| */ | |
| LatLon.prototype.intermediatePointTo = function(point, fraction) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| var φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians(); | |
| var φ2 = point.lat.toRadians(), λ2 = point.lon.toRadians(); | |
| var sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1), sinλ1 = Math.sin(λ1), cosλ1 = Math.cos(λ1); | |
| var sinφ2 = Math.sin(φ2), cosφ2 = Math.cos(φ2), sinλ2 = Math.sin(λ2), cosλ2 = Math.cos(λ2); | |
| // distance between points | |
| var Δφ = φ2 - φ1; | |
| var Δλ = λ2 - λ1; | |
| var a = Math.sin(Δφ/2) * Math.sin(Δφ/2) | |
| + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2); | |
| var δ = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); | |
| var A = Math.sin((1-fraction)*δ) / Math.sin(δ); | |
| var B = Math.sin(fraction*δ) / Math.sin(δ); | |
| var x = A * cosφ1 * cosλ1 + B * cosφ2 * cosλ2; | |
| var y = A * cosφ1 * sinλ1 + B * cosφ2 * sinλ2; | |
| var z = A * sinφ1 + B * sinφ2; | |
| var φ3 = Math.atan2(z, Math.sqrt(x*x + y*y)); | |
| var λ3 = Math.atan2(y, x); | |
| return new LatLon(φ3.toDegrees(), (λ3.toDegrees()+540)%360-180); // normalise lon to −180..+180° | |
| }; | |
| /** | |
| * Returns the destination point from ‘this’ point having travelled the given distance on the | |
| * given initial bearing (bearing normally varies around path followed). | |
| * | |
| * @param {number} distance - Distance travelled, in same units as earth radius (default: metres). | |
| * @param {number} bearing - Initial bearing in degrees from north. | |
| * @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres). | |
| * @returns {LatLon} Destination point. | |
| * | |
| * @example | |
| * var p1 = new LatLon(51.4778, -0.0015); | |
| * var p2 = p1.destinationPoint(7794, 300.7); // 51.5135°N, 000.0983°W | |
| */ | |
| LatLon.prototype.destinationPoint = function(distance, bearing, radius) { | |
| radius = (radius === undefined) ? 6371e3 : Number(radius); | |
| // sinφ2 = sinφ1⋅cosδ + cosφ1⋅sinδ⋅cosθ | |
| // tanΔλ = sinθ⋅sinδ⋅cosφ1 / cosδ−sinφ1⋅sinφ2 | |
| // see www.edwilliams.org/avform.htm#LL | |
| var δ = Number(distance) / radius; // angular distance in radians | |
| var θ = Number(bearing).toRadians(); | |
| var φ1 = this.lat.toRadians(); | |
| var λ1 = this.lon.toRadians(); | |
| var sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1); | |
| var sinδ = Math.sin(δ), cosδ = Math.cos(δ); | |
| var sinθ = Math.sin(θ), cosθ = Math.cos(θ); | |
| var sinφ2 = sinφ1*cosδ + cosφ1*sinδ*cosθ; | |
| var φ2 = Math.asin(sinφ2); | |
| var y = sinθ * sinδ * cosφ1; | |
| var x = cosδ - sinφ1 * sinφ2; | |
| var λ2 = λ1 + Math.atan2(y, x); | |
| return new LatLon(φ2.toDegrees(), (λ2.toDegrees()+540)%360-180); // normalise to −180..+180° | |
| }; | |
| /** | |
| * Returns the point of intersection of two paths defined by point and bearing. | |
| * | |
| * @param {LatLon} p1 - First point. | |
| * @param {number} brng1 - Initial bearing from first point. | |
| * @param {LatLon} p2 - Second point. | |
| * @param {number} brng2 - Initial bearing from second point. | |
| * @returns {LatLon|null} Destination point (null if no unique intersection defined). | |
| * | |
| * @example | |
| * var p1 = LatLon(51.8853, 0.2545), brng1 = 108.547; | |
| * var p2 = LatLon(49.0034, 2.5735), brng2 = 32.435; | |
| * var pInt = LatLon.intersection(p1, brng1, p2, brng2); // 50.9078°N, 004.5084°E | |
| */ | |
| LatLon.intersection = function(p1, brng1, p2, brng2) { | |
| if (!(p1 instanceof LatLon)) throw new TypeError('p1 is not LatLon object'); | |
| if (!(p2 instanceof LatLon)) throw new TypeError('p2 is not LatLon object'); | |
| // see www.edwilliams.org/avform.htm#Intersection | |
| var φ1 = p1.lat.toRadians(), λ1 = p1.lon.toRadians(); | |
| var φ2 = p2.lat.toRadians(), λ2 = p2.lon.toRadians(); | |
| var θ13 = Number(brng1).toRadians(), θ23 = Number(brng2).toRadians(); | |
| var Δφ = φ2-φ1, Δλ = λ2-λ1; | |
| // angular distance p1-p2 | |
| var δ12 = 2*Math.asin( Math.sqrt( Math.sin(Δφ/2)*Math.sin(Δφ/2) | |
| + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)*Math.sin(Δλ/2) ) ); | |
| if (δ12 == 0) return null; | |
| // initial/final bearings between points | |
| var θa = Math.acos( ( Math.sin(φ2) - Math.sin(φ1)*Math.cos(δ12) ) / ( Math.sin(δ12)*Math.cos(φ1) ) ); | |
| if (isNaN(θa)) θa = 0; // protect against rounding | |
| var θb = Math.acos( ( Math.sin(φ1) - Math.sin(φ2)*Math.cos(δ12) ) / ( Math.sin(δ12)*Math.cos(φ2) ) ); | |
| var θ12 = Math.sin(λ2-λ1)>0 ? θa : 2*Math.PI-θa; | |
| var θ21 = Math.sin(λ2-λ1)>0 ? 2*Math.PI-θb : θb; | |
| var α1 = θ13 - θ12; // angle 2-1-3 | |
| var α2 = θ21 - θ23; // angle 1-2-3 | |
| if (Math.sin(α1)==0 && Math.sin(α2)==0) return null; // infinite intersections | |
| if (Math.sin(α1)*Math.sin(α2) < 0) return null; // ambiguous intersection | |
| var α3 = Math.acos( -Math.cos(α1)*Math.cos(α2) + Math.sin(α1)*Math.sin(α2)*Math.cos(δ12) ); | |
| var δ13 = Math.atan2( Math.sin(δ12)*Math.sin(α1)*Math.sin(α2), Math.cos(α2)+Math.cos(α1)*Math.cos(α3) ); | |
| var φ3 = Math.asin( Math.sin(φ1)*Math.cos(δ13) + Math.cos(φ1)*Math.sin(δ13)*Math.cos(θ13) ); | |
| var Δλ13 = Math.atan2( Math.sin(θ13)*Math.sin(δ13)*Math.cos(φ1), Math.cos(δ13)-Math.sin(φ1)*Math.sin(φ3) ); | |
| var λ3 = λ1 + Δλ13; | |
| return new LatLon(φ3.toDegrees(), (λ3.toDegrees()+540)%360-180); // normalise to −180..+180° | |
| }; | |
| /** | |
| * Returns (signed) distance from ‘this’ point to great circle defined by start-point and end-point. | |
| * | |
| * @param {LatLon} pathStart - Start point of great circle path. | |
| * @param {LatLon} pathEnd - End point of great circle path. | |
| * @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres). | |
| * @returns {number} Distance to great circle (-ve if to left, +ve if to right of path). | |
| * | |
| * @example | |
| * var pCurrent = new LatLon(53.2611, -0.7972); | |
| * var p1 = new LatLon(53.3206, -1.7297); | |
| * var p2 = new LatLon(53.1887, 0.1334); | |
| * var d = pCurrent.crossTrackDistanceTo(p1, p2); // -307.5 m | |
| */ | |
| LatLon.prototype.crossTrackDistanceTo = function(pathStart, pathEnd, radius) { | |
| if (!(pathStart instanceof LatLon)) throw new TypeError('pathStart is not LatLon object'); | |
| if (!(pathEnd instanceof LatLon)) throw new TypeError('pathEnd is not LatLon object'); | |
| var R = (radius === undefined) ? 6371e3 : Number(radius); | |
| var δ13 = pathStart.distanceTo(this, R) / R; | |
| var θ13 = pathStart.bearingTo(this).toRadians(); | |
| var θ12 = pathStart.bearingTo(pathEnd).toRadians(); | |
| var δxt = Math.asin(Math.sin(δ13) * Math.sin(θ13-θ12)); | |
| return δxt * R; | |
| }; | |
| /** | |
| * Returns how far ‘this’ point is along a path from from start-point, heading towards end-point. | |
| * That is, if a perpendicular is drawn from ‘this’ point to the (great circle) path, the along-track | |
| * distance is the distance from the start point to where the perpendicular crosses the path. | |
| * | |
| * @param {LatLon} pathStart - Start point of great circle path. | |
| * @param {LatLon} pathEnd - End point of great circle path. | |
| * @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres). | |
| * @returns {number} Distance along great circle to point nearest ‘this’ point. | |
| * | |
| * @example | |
| * var pCurrent = new LatLon(53.2611, -0.7972); | |
| * var p1 = new LatLon(53.3206, -1.7297); | |
| * var p2 = new LatLon(53.1887, 0.1334); | |
| * var d = pCurrent.alongTrackDistanceTo(p1, p2); // 62.331 km | |
| */ | |
| LatLon.prototype.alongTrackDistanceTo = function(pathStart, pathEnd, radius) { | |
| if (!(pathStart instanceof LatLon)) throw new TypeError('pathStart is not LatLon object'); | |
| if (!(pathEnd instanceof LatLon)) throw new TypeError('pathEnd is not LatLon object'); | |
| var R = (radius === undefined) ? 6371e3 : Number(radius); | |
| var δ13 = pathStart.distanceTo(this, R) / R; | |
| var θ13 = pathStart.bearingTo(this).toRadians(); | |
| var θ12 = pathStart.bearingTo(pathEnd).toRadians(); | |
| var δxt = Math.asin(Math.sin(δ13) * Math.sin(θ13-θ12)); | |
| var δat = Math.acos(Math.cos(δ13) / Math.abs(Math.cos(δxt))); | |
| return δat*Math.sign(Math.cos(θ12-θ13)) * R; | |
| }; | |
| /** | |
| * Returns maximum latitude reached when travelling on a great circle on given bearing from this | |
| * point ('Clairaut's formula'). Negate the result for the minimum latitude (in the Southern | |
| * hemisphere). | |
| * | |
| * The maximum latitude is independent of longitude; it will be the same for all points on a given | |
| * latitude. | |
| * | |
| * @param {number} bearing - Initial bearing. | |
| * @param {number} latitude - Starting latitude. | |
| */ | |
| LatLon.prototype.maxLatitude = function(bearing) { | |
| var θ = Number(bearing).toRadians(); | |
| var φ = this.lat.toRadians(); | |
| var φMax = Math.acos(Math.abs(Math.sin(θ)*Math.cos(φ))); | |
| return φMax.toDegrees(); | |
| }; | |
| /** | |
| * Returns the pair of meridians at which a great circle defined by two points crosses the given | |
| * latitude. If the great circle doesn't reach the given latitude, null is returned. | |
| * | |
| * @param {LatLon} point1 - First point defining great circle. | |
| * @param {LatLon} point2 - Second point defining great circle. | |
| * @param {number} latitude - Latitude crossings are to be determined for. | |
| * @returns {Object|null} Object containing { lon1, lon2 } or null if given latitude not reached. | |
| */ | |
| LatLon.crossingParallels = function(point1, point2, latitude) { | |
| var φ = Number(latitude).toRadians(); | |
| var φ1 = point1.lat.toRadians(); | |
| var λ1 = point1.lon.toRadians(); | |
| var φ2 = point2.lat.toRadians(); | |
| var λ2 = point2.lon.toRadians(); | |
| var Δλ = λ2 - λ1; | |
| var x = Math.sin(φ1) * Math.cos(φ2) * Math.cos(φ) * Math.sin(Δλ); | |
| var y = Math.sin(φ1) * Math.cos(φ2) * Math.cos(φ) * Math.cos(Δλ) - Math.cos(φ1) * Math.sin(φ2) * Math.cos(φ); | |
| var z = Math.cos(φ1) * Math.cos(φ2) * Math.sin(φ) * Math.sin(Δλ); | |
| if (z*z > x*x + y*y) return null; // great circle doesn't reach latitude | |
| var λm = Math.atan2(-y, x); // longitude at max latitude | |
| var Δλi = Math.acos(z / Math.sqrt(x*x+y*y)); // Δλ from λm to intersection points | |
| var λi1 = λ1 + λm - Δλi; | |
| var λi2 = λ1 + λm + Δλi; | |
| return { lon1: (λi1.toDegrees()+540)%360-180, lon2: (λi2.toDegrees()+540)%360-180 }; // normalise to −180..+180° | |
| }; | |
| /* Rhumb - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
| /** | |
| * Returns the distance travelling from ‘this’ point to destination point along a rhumb line. | |
| * | |
| * @param {LatLon} point - Latitude/longitude of destination point. | |
| * @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres). | |
| * @returns {number} Distance in km between this point and destination point (same units as radius). | |
| * | |
| * @example | |
| * var p1 = new LatLon(51.127, 1.338); | |
| * var p2 = new LatLon(50.964, 1.853); | |
| * var d = p1.distanceTo(p2); // 40.31 km | |
| */ | |
| LatLon.prototype.rhumbDistanceTo = function(point, radius) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| radius = (radius === undefined) ? 6371e3 : Number(radius); | |
| // see www.edwilliams.org/avform.htm#Rhumb | |
| var R = radius; | |
| var φ1 = this.lat.toRadians(), φ2 = point.lat.toRadians(); | |
| var Δφ = φ2 - φ1; | |
| var Δλ = Math.abs(point.lon-this.lon).toRadians(); | |
| // if dLon over 180° take shorter rhumb line across the anti-meridian: | |
| if (Δλ > Math.PI) Δλ -= 2*Math.PI; | |
| // on Mercator projection, longitude distances shrink by latitude; q is the 'stretch factor' | |
| // q becomes ill-conditioned along E-W line (0/0); use empirical tolerance to avoid it | |
| var Δψ = Math.log(Math.tan(φ2/2+Math.PI/4)/Math.tan(φ1/2+Math.PI/4)); | |
| var q = Math.abs(Δψ) > 10e-12 ? Δφ/Δψ : Math.cos(φ1); | |
| // distance is pythagoras on 'stretched' Mercator projection | |
| var δ = Math.sqrt(Δφ*Δφ + q*q*Δλ*Δλ); // angular distance in radians | |
| var dist = δ * R; | |
| return dist; | |
| }; | |
| /** | |
| * Returns the bearing from ‘this’ point to destination point along a rhumb line. | |
| * | |
| * @param {LatLon} point - Latitude/longitude of destination point. | |
| * @returns {number} Bearing in degrees from north. | |
| * | |
| * @example | |
| * var p1 = new LatLon(51.127, 1.338); | |
| * var p2 = new LatLon(50.964, 1.853); | |
| * var d = p1.rhumbBearingTo(p2); // 116.7 m | |
| */ | |
| LatLon.prototype.rhumbBearingTo = function(point) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| var φ1 = this.lat.toRadians(), φ2 = point.lat.toRadians(); | |
| var Δλ = (point.lon-this.lon).toRadians(); | |
| // if dLon over 180° take shorter rhumb line across the anti-meridian: | |
| if (Δλ > Math.PI) Δλ -= 2*Math.PI; | |
| if (Δλ < -Math.PI) Δλ += 2*Math.PI; | |
| var Δψ = Math.log(Math.tan(φ2/2+Math.PI/4)/Math.tan(φ1/2+Math.PI/4)); | |
| var θ = Math.atan2(Δλ, Δψ); | |
| return (θ.toDegrees()+360) % 360; | |
| }; | |
| /** | |
| * Returns the destination point having travelled along a rhumb line from ‘this’ point the given | |
| * distance on the given bearing. | |
| * | |
| * @param {number} distance - Distance travelled, in same units as earth radius (default: metres). | |
| * @param {number} bearing - Bearing in degrees from north. | |
| * @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres). | |
| * @returns {LatLon} Destination point. | |
| * | |
| * @example | |
| * var p1 = new LatLon(51.127, 1.338); | |
| * var p2 = p1.rhumbDestinationPoint(40300, 116.7); // 50.9642°N, 001.8530°E | |
| */ | |
| LatLon.prototype.rhumbDestinationPoint = function(distance, bearing, radius) { | |
| radius = (radius === undefined) ? 6371e3 : Number(radius); | |
| var δ = Number(distance) / radius; // angular distance in radians | |
| var φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians(); | |
| var θ = Number(bearing).toRadians(); | |
| var Δφ = δ * Math.cos(θ); | |
| var φ2 = φ1 + Δφ; | |
| // check for some daft bugger going past the pole, normalise latitude if so | |
| if (Math.abs(φ2) > Math.PI/2) φ2 = φ2>0 ? Math.PI-φ2 : -Math.PI-φ2; | |
| var Δψ = Math.log(Math.tan(φ2/2+Math.PI/4)/Math.tan(φ1/2+Math.PI/4)); | |
| var q = Math.abs(Δψ) > 10e-12 ? Δφ / Δψ : Math.cos(φ1); // E-W course becomes ill-conditioned with 0/0 | |
| var Δλ = δ*Math.sin(θ)/q; | |
| var λ2 = λ1 + Δλ; | |
| return new LatLon(φ2.toDegrees(), (λ2.toDegrees()+540) % 360 - 180); // normalise to −180..+180° | |
| }; | |
| /** | |
| * Returns the loxodromic midpoint (along a rhumb line) between ‘this’ point and second point. | |
| * | |
| * @param {LatLon} point - Latitude/longitude of second point. | |
| * @returns {LatLon} Midpoint between this point and second point. | |
| * | |
| * @example | |
| * var p1 = new LatLon(51.127, 1.338); | |
| * var p2 = new LatLon(50.964, 1.853); | |
| * var pMid = p1.rhumbMidpointTo(p2); // 51.0455°N, 001.5957°E | |
| */ | |
| LatLon.prototype.rhumbMidpointTo = function(point) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| // http://mathforum.org/kb/message.jspa?messageID=148837 | |
| var φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians(); | |
| var φ2 = point.lat.toRadians(), λ2 = point.lon.toRadians(); | |
| if (Math.abs(λ2-λ1) > Math.PI) λ1 += 2*Math.PI; // crossing anti-meridian | |
| var φ3 = (φ1+φ2)/2; | |
| var f1 = Math.tan(Math.PI/4 + φ1/2); | |
| var f2 = Math.tan(Math.PI/4 + φ2/2); | |
| var f3 = Math.tan(Math.PI/4 + φ3/2); | |
| var λ3 = ( (λ2-λ1)*Math.log(f3) + λ1*Math.log(f2) - λ2*Math.log(f1) ) / Math.log(f2/f1); | |
| if (!isFinite(λ3)) λ3 = (λ1+λ2)/2; // parallel of latitude | |
| var p = LatLon(φ3.toDegrees(), (λ3.toDegrees()+540)%360-180); // normalise to −180..+180° | |
| return p; | |
| }; | |
| /* Area - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
| /** | |
| * Calculates the area of a spherical polygon where the sides of the polygon are great circle | |
| * arcs joining the vertices. | |
| * | |
| * @param {LatLon[]} polygon - Array of points defining vertices of the polygon | |
| * @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres). | |
| * @returns {number} The area of the polygon, in the same units as radius. | |
| * | |
| * @example | |
| * var polygon = [new LatLon(0,0), new LatLon(1,0), new LatLon(0,1)]; | |
| * var area = LatLon.areaOf(polygon); // 6.18e9 m² | |
| */ | |
| LatLon.areaOf = function(polygon, radius) { | |
| // uses method due to Karney: osgeo-org.1560.x6.nabble.com/Area-of-a-spherical-polygon-td3841625.html; | |
| // for each edge of the polygon, tan(E/2) = tan(Δλ/2)·(tan(φ1/2) + tan(φ2/2)) / (1 + tan(φ1/2)·tan(φ2/2)) | |
| // where E is the spherical excess of the trapezium obtained by extending the edge to the equator | |
| var R = (radius === undefined) ? 6371e3 : Number(radius); | |
| // close polygon so that last point equals first point | |
| var closed = polygon[0].equals(polygon[polygon.length-1]); | |
| if (!closed) polygon.push(polygon[0]); | |
| var nVertices = polygon.length - 1; | |
| var S = 0; // spherical excess in steradians | |
| for (var v=0; v<nVertices; v++) { | |
| var φ1 = polygon[v].lat.toRadians(); | |
| var φ2 = polygon[v+1].lat.toRadians(); | |
| var Δλ = (polygon[v+1].lon - polygon[v].lon).toRadians(); | |
| var E = 2 * Math.atan2(Math.tan(Δλ/2) * (Math.tan(φ1/2)+Math.tan(φ2/2)), 1 + Math.tan(φ1/2)*Math.tan(φ2/2)); | |
| S += E; | |
| } | |
| if (isPoleEnclosedBy(polygon)) S = Math.abs(S) - 2*Math.PI; | |
| var A = Math.abs(S * R*R); // area in units of R | |
| if (!closed) polygon.pop(); // restore polygon to pristine condition | |
| return A; | |
| // returns whether polygon encloses pole: sum of course deltas around pole is 0° rather than | |
| // normal ±360°: blog.element84.com/determining-if-a-spherical-polygon-contains-a-pole.html | |
| function isPoleEnclosedBy(polygon) { | |
| // TODO: any better test than this? | |
| var ΣΔ = 0; | |
| var prevBrng = polygon[0].bearingTo(polygon[1]); | |
| for (var v=0; v<polygon.length-1; v++) { | |
| var initBrng = polygon[v].bearingTo(polygon[v+1]); | |
| var finalBrng = polygon[v].finalBearingTo(polygon[v+1]); | |
| ΣΔ += (initBrng - prevBrng + 540) % 360 - 180; | |
| ΣΔ += (finalBrng - initBrng + 540) % 360 - 180; | |
| prevBrng = finalBrng; | |
| } | |
| var initBrng = polygon[0].bearingTo(polygon[1]); | |
| ΣΔ += (initBrng - prevBrng + 540) % 360 - 180; | |
| // TODO: fix (intermittant) edge crossing pole - eg (85,90), (85,0), (85,-90) | |
| var enclosed = Math.abs(ΣΔ) < 90; // 0°-ish | |
| return enclosed; | |
| } | |
| }; | |
| /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
| /** | |
| * Checks if another point is equal to ‘this’ point. | |
| * | |
| * @param {LatLon} point - Point to be compared against this point. | |
| * @returns {bool} True if points are identical. | |
| * | |
| * @example | |
| * var p1 = new LatLon(52.205, 0.119); | |
| * var p2 = new LatLon(52.205, 0.119); | |
| * var equal = p1.equals(p2); // true | |
| */ | |
| LatLon.prototype.equals = function(point) { | |
| if (!(point instanceof LatLon)) throw new TypeError('point is not LatLon object'); | |
| if (this.lat != point.lat) return false; | |
| if (this.lon != point.lon) return false; | |
| return true; | |
| }; | |
| /** | |
| * Returns a string representation of ‘this’ point, formatted as degrees, degrees+minutes, or | |
| * degrees+minutes+seconds. | |
| * | |
| * @param {string} [format=dms] - Format point as 'd', 'dm', 'dms'. | |
| * @param {number} [dp=0|2|4] - Number of decimal places to use - default 0 for dms, 2 for dm, 4 for d. | |
| * @returns {string} Comma-separated latitude/longitude. | |
| */ | |
| LatLon.prototype.toString = function(format, dp) { | |
| return Dms.toLat(this.lat, format, dp) + ', ' + Dms.toLon(this.lon, format, dp); | |
| }; | |
| /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
| /** Extend Number object with method to convert numeric degrees to radians */ | |
| if (Number.prototype.toRadians === undefined) { | |
| Number.prototype.toRadians = function() { return this * Math.PI / 180; }; | |
| } | |
| /** Extend Number object with method to convert radians to numeric (signed) degrees */ | |
| if (Number.prototype.toDegrees === undefined) { | |
| Number.prototype.toDegrees = function() { return this * 180 / Math.PI; }; | |
| } | |
| /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
| if (typeof module != 'undefined' && module.exports) module.exports = LatLon; // ≡ export default LatLon |