/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* 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); // a = sin²(Δφ/2) + cos(φ1)⋅cos(φ2)⋅sin²(Δλ/2) // tanδ = √(a) / √(1−a) // see mathforum.org/library/drmath/view/51879.html for derivation 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'); // tanθ = sinΔλ⋅cosφ2 / cosφ1⋅sinφ2 − sinφ1⋅cosφ2⋅cosΔλ // see mathforum.org/library/drmath/view/55417.html for derivation var φ1 = this.lat.toRadians(), φ2 = point.lat.toRadians(); var Δλ = (point.lon-this.lon).toRadians(); 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 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 mathforum.org/library/drmath/view/52049.html for derivation 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'); // see 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