1070 lines
28 KiB
JavaScript
1070 lines
28 KiB
JavaScript
|
/*! geolib 1.3.5 by Manuel Bieh
|
||
|
* A growing library to provide some basic geo functions like distance calculation,
|
||
|
* conversion of decimal coordinates to sexagesimal and vice versa, etc.
|
||
|
* WGS 84 (World Geodetic System 1984)
|
||
|
*
|
||
|
* @author Manuel Bieh
|
||
|
* @url http://www.manuelbieh.com/
|
||
|
* @version 1.3.5
|
||
|
* @license LGPL
|
||
|
**//*global console:true geolib:true require:true module:true window:true global:true define:true*/
|
||
|
(function (global, undefined) {
|
||
|
|
||
|
var radius = 6378137; // Earth radius
|
||
|
var sexagesimalPattern = /^([0-9]{1,3})°\s*([0-9]{1,3})'\s*(([0-9]{1,3}(\.([0-9]{1,2}))?)"\s*)?([NEOSW]?)$/;
|
||
|
var MIN_LAT = -90;
|
||
|
var MAX_LAT = 90;
|
||
|
var MIN_LON = -180;
|
||
|
var MAX_LON = 180;
|
||
|
|
||
|
var geolib = {
|
||
|
|
||
|
decimal: {},
|
||
|
|
||
|
sexagesimal: {},
|
||
|
|
||
|
distance: 0,
|
||
|
|
||
|
measures: {
|
||
|
m: 1,
|
||
|
km: 0.001,
|
||
|
cm: 100,
|
||
|
mm: 1000,
|
||
|
mi: (1 / 1609.344),
|
||
|
sm: (1 / 1852.216),
|
||
|
ft: (100 / 30.48),
|
||
|
"in": (100 / 2.54),
|
||
|
yd: (1 / 0.9144)
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Get the key names for a geo point.
|
||
|
*
|
||
|
* @param object Point position {latitude: 123, longitude: 123, elevation: 123}
|
||
|
* @return object {
|
||
|
* longitude: 'lng|long|longitude',
|
||
|
* latitude: 'lat|latitude',
|
||
|
* elevation: 'alt|altitude|elev|elevation'
|
||
|
* }
|
||
|
*/
|
||
|
getKeys: function(point) {
|
||
|
|
||
|
var latitude = point.hasOwnProperty('lat') ? 'lat' : 'latitude';
|
||
|
|
||
|
var longitude = (point.hasOwnProperty('lng') ? 'lng' : false) ||
|
||
|
(point.hasOwnProperty('long') ? 'long' : false) ||
|
||
|
'longitude';
|
||
|
|
||
|
var elevation = (point.hasOwnProperty('alt') ? 'alt' : false) ||
|
||
|
(point.hasOwnProperty('altitude') ? 'altitude' : false) ||
|
||
|
(point.hasOwnProperty('elev') ? 'elev' : false) ||
|
||
|
'elevation';
|
||
|
|
||
|
return {
|
||
|
latitude: latitude,
|
||
|
longitude: longitude,
|
||
|
elevation: elevation
|
||
|
};
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Calculates geodetic distance between two points specified by latitude/longitude using
|
||
|
* Vincenty inverse formula for ellipsoids
|
||
|
* Vincenty Inverse Solution of Geodesics on the Ellipsoid (c) Chris Veness 2002-2010
|
||
|
* (Licensed under CC BY 3.0)
|
||
|
*
|
||
|
* @param object Start position {latitude: 123, longitude: 123}
|
||
|
* @param object End position {latitude: 123, longitude: 123}
|
||
|
* @param integer Accuracy (in meters)
|
||
|
* @return integer Distance (in meters)
|
||
|
*/
|
||
|
|
||
|
getDistance: function(start, end, accuracy) {
|
||
|
|
||
|
var keys = geolib.getKeys(start);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
var elevation = keys.elevation;
|
||
|
|
||
|
accuracy = Math.floor(accuracy) || 1;
|
||
|
|
||
|
var coord1 = {}, coord2 = {};
|
||
|
coord1[latitude] = geolib.useDecimal(start[latitude]);
|
||
|
coord1[longitude] = geolib.useDecimal(start[longitude]);
|
||
|
|
||
|
coord2[latitude] = geolib.useDecimal(end[latitude]);
|
||
|
coord2[longitude] = geolib.useDecimal(end[longitude]);
|
||
|
|
||
|
var a = 6378137, b = 6356752.314245, f = 1/298.257223563; // WGS-84 ellipsoid params
|
||
|
var L = (coord2[longitude]-coord1[longitude]).toRad();
|
||
|
|
||
|
var cosSigma, sigma, sinAlpha, cosSqAlpha, cos2SigmaM, sinSigma;
|
||
|
|
||
|
var U1 = Math.atan((1-f) * Math.tan(parseFloat(coord1[latitude]).toRad()));
|
||
|
var U2 = Math.atan((1-f) * Math.tan(parseFloat(coord2[latitude]).toRad()));
|
||
|
var sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
|
||
|
var sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
|
||
|
|
||
|
var lambda = L, lambdaP, iterLimit = 100;
|
||
|
do {
|
||
|
var sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda);
|
||
|
sinSigma = (
|
||
|
Math.sqrt(
|
||
|
(
|
||
|
cosU2 * sinLambda
|
||
|
) * (
|
||
|
cosU2 * sinLambda
|
||
|
) + (
|
||
|
cosU1 * sinU2 - sinU1 * cosU2 * cosLambda
|
||
|
) * (
|
||
|
cosU1 * sinU2 - sinU1 * cosU2 * cosLambda
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
if (sinSigma === 0) {
|
||
|
return geolib.distance = 0; // co-incident points
|
||
|
}
|
||
|
|
||
|
cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
|
||
|
sigma = Math.atan2(sinSigma, cosSigma);
|
||
|
sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
|
||
|
cosSqAlpha = 1 - sinAlpha * sinAlpha;
|
||
|
cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
|
||
|
|
||
|
if (isNaN(cos2SigmaM)) {
|
||
|
cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6)
|
||
|
}
|
||
|
var C = (
|
||
|
f / 16 * cosSqAlpha * (
|
||
|
4 + f * (
|
||
|
4 - 3 * cosSqAlpha
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
lambdaP = lambda;
|
||
|
lambda = (
|
||
|
L + (
|
||
|
1 - C
|
||
|
) * f * sinAlpha * (
|
||
|
sigma + C * sinSigma * (
|
||
|
cos2SigmaM + C * cosSigma * (
|
||
|
-1 + 2 * cos2SigmaM * cos2SigmaM
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
} while (Math.abs(lambda-lambdaP) > 1e-12 && --iterLimit>0);
|
||
|
|
||
|
if (iterLimit === 0) {
|
||
|
return NaN; // formula failed to converge
|
||
|
}
|
||
|
|
||
|
var uSq = (
|
||
|
cosSqAlpha * (
|
||
|
a * a - b * b
|
||
|
) / (
|
||
|
b*b
|
||
|
)
|
||
|
);
|
||
|
|
||
|
var A = (
|
||
|
1 + uSq / 16384 * (
|
||
|
4096 + uSq * (
|
||
|
-768 + uSq * (
|
||
|
320 - 175 * uSq
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
var B = (
|
||
|
uSq / 1024 * (
|
||
|
256 + uSq * (
|
||
|
-128 + uSq * (
|
||
|
74-47 * uSq
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
var deltaSigma = (
|
||
|
B * sinSigma * (
|
||
|
cos2SigmaM + B / 4 * (
|
||
|
cosSigma * (
|
||
|
-1 + 2 * cos2SigmaM * cos2SigmaM
|
||
|
) -B / 6 * cos2SigmaM * (
|
||
|
-3 + 4 * sinSigma * sinSigma
|
||
|
) * (
|
||
|
-3 + 4 * cos2SigmaM * cos2SigmaM
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
var distance = b * A * (sigma - deltaSigma);
|
||
|
|
||
|
distance = distance.toFixed(3); // round to 1mm precision
|
||
|
|
||
|
if (start.hasOwnProperty(elevation) && end.hasOwnProperty(elevation)) {
|
||
|
var climb = Math.abs(start[elevation] - end[elevation]);
|
||
|
distance = Math.sqrt(distance*distance + climb*climb);
|
||
|
}
|
||
|
|
||
|
return geolib.distance = Math.floor(Math.round(distance/accuracy)*accuracy);
|
||
|
|
||
|
/*
|
||
|
// note: to return initial/final bearings in addition to distance, use something like:
|
||
|
var fwdAz = Math.atan2(cosU2*sinLambda, cosU1*sinU2-sinU1*cosU2*cosLambda);
|
||
|
var revAz = Math.atan2(cosU1*sinLambda, -sinU1*cosU2+cosU1*sinU2*cosLambda);
|
||
|
|
||
|
return { distance: s, initialBearing: fwdAz.toDeg(), finalBearing: revAz.toDeg() };
|
||
|
*/
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Calculates the distance between two spots.
|
||
|
* This method is more simple but also more inaccurate
|
||
|
*
|
||
|
* @param object Start position {latitude: 123, longitude: 123}
|
||
|
* @param object End position {latitude: 123, longitude: 123}
|
||
|
* @param integer Accuracy (in meters)
|
||
|
* @return integer Distance (in meters)
|
||
|
*/
|
||
|
getDistanceSimple: function(start, end, accuracy) {
|
||
|
|
||
|
var keys = geolib.getKeys(start);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
|
||
|
accuracy = Math.floor(accuracy) || 1;
|
||
|
|
||
|
var coord1 = {}, coord2 = {};
|
||
|
coord1[latitude] = parseFloat(geolib.useDecimal(start[latitude])).toRad();
|
||
|
coord1[longitude] = parseFloat(geolib.useDecimal(start[longitude])).toRad();
|
||
|
|
||
|
coord2[latitude] = parseFloat(geolib.useDecimal(end[latitude])).toRad();
|
||
|
coord2[longitude] = parseFloat(geolib.useDecimal(end[longitude])).toRad();
|
||
|
|
||
|
var distance =
|
||
|
Math.round(
|
||
|
Math.acos(
|
||
|
Math.sin(
|
||
|
coord2[latitude]
|
||
|
) *
|
||
|
Math.sin(
|
||
|
coord1[latitude]
|
||
|
) +
|
||
|
Math.cos(
|
||
|
coord2[latitude]
|
||
|
) *
|
||
|
Math.cos(
|
||
|
coord1[latitude]
|
||
|
) *
|
||
|
Math.cos(
|
||
|
coord1[longitude] - coord2[longitude]
|
||
|
)
|
||
|
) * radius
|
||
|
);
|
||
|
|
||
|
return geolib.distance = Math.floor(Math.round(distance/accuracy)*accuracy);
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Calculates the center of a collection of geo coordinates
|
||
|
*
|
||
|
* @param array Collection of coords [{latitude: 51.510, longitude: 7.1321}, {latitude: 49.1238, longitude: "8° 30' W"}, ...]
|
||
|
* @return object {latitude: centerLat, longitude: centerLng, distance: diagonalDistance}
|
||
|
*/
|
||
|
getCenter: function(coords) {
|
||
|
|
||
|
if (!coords.length) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var keys = geolib.getKeys(coords[0]);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
|
||
|
var max = function( array ){
|
||
|
return Math.max.apply( Math, array );
|
||
|
};
|
||
|
|
||
|
var min = function( array ){
|
||
|
return Math.min.apply( Math, array );
|
||
|
};
|
||
|
|
||
|
var lat, lng, splitCoords = {lat: [], lng: []};
|
||
|
|
||
|
for(var coord in coords) {
|
||
|
splitCoords.lat.push(geolib.useDecimal(coords[coord][latitude]));
|
||
|
splitCoords.lng.push(geolib.useDecimal(coords[coord][longitude]));
|
||
|
}
|
||
|
|
||
|
var minLat = min(splitCoords.lat);
|
||
|
var minLng = min(splitCoords.lng);
|
||
|
var maxLat = max(splitCoords.lat);
|
||
|
var maxLng = max(splitCoords.lng);
|
||
|
|
||
|
lat = ((minLat + maxLat)/2).toFixed(6);
|
||
|
lng = ((minLng + maxLng)/2).toFixed(6);
|
||
|
|
||
|
// distance from the deepest left to the highest right point (diagonal distance)
|
||
|
var distance = geolib.convertUnit('km', geolib.getDistance({lat:minLat, lng:minLng}, {lat:maxLat, lng:maxLng}));
|
||
|
|
||
|
return {"latitude": lat, "longitude": lng, "distance": distance};
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the max and min, latitude, longitude, and elevation (if provided).
|
||
|
* @param array array with coords e.g. [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...]
|
||
|
* @return object {maxLat: maxLat,
|
||
|
* minLat: minLat
|
||
|
* maxLng: maxLng,
|
||
|
* minLng: minLng,
|
||
|
* maxElev: maxElev,
|
||
|
* minElev: minElev}
|
||
|
*/
|
||
|
getBounds: function(coords) {
|
||
|
|
||
|
if (!coords.length) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var keys = geolib.getKeys(coords[0]);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
var elevation = keys.elevation;
|
||
|
|
||
|
var useElevation = coords[0].hasOwnProperty(elevation);
|
||
|
var stats = {
|
||
|
maxLat: -Infinity,
|
||
|
minLat: Infinity,
|
||
|
maxLng: -Infinity,
|
||
|
minLng: Infinity
|
||
|
};
|
||
|
|
||
|
if (useElevation) {
|
||
|
stats.maxElev = 0;
|
||
|
stats.minElev = Infinity;
|
||
|
}
|
||
|
|
||
|
for (var i = 0, l = coords.length; i < l; ++i) {
|
||
|
stats.maxLat = Math.max(coords[i][latitude], stats.maxLat);
|
||
|
stats.minLat = Math.min(coords[i][latitude], stats.minLat);
|
||
|
stats.maxLng = Math.max(coords[i][longitude], stats.maxLng);
|
||
|
stats.minLng = Math.min(coords[i][longitude], stats.minLng);
|
||
|
if (useElevation) {
|
||
|
stats.maxElev = Math.max(coords[i][elevation], stats.maxElev);
|
||
|
stats.minElev = Math.min(coords[i][elevation], stats.minElev);
|
||
|
}
|
||
|
}
|
||
|
return stats;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Computes the bounding coordinates of all points on the surface
|
||
|
* of the earth less than or equal to the specified great circle
|
||
|
* distance.
|
||
|
*
|
||
|
* @param object Point position {latitude: 123, longitude: 123}
|
||
|
* @param number Distance (in meters).
|
||
|
* @return array Collection of two points defining the SW and NE corners.
|
||
|
*/
|
||
|
getBoundsOfDistance: function(point, distance) {
|
||
|
var keys = geolib.getKeys(point);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
var coord = {};
|
||
|
coord[latitude] = geolib.useDecimal(point[latitude]);
|
||
|
coord[longitude] = geolib.useDecimal(point[longitude]);
|
||
|
var radLat = coord[latitude].toRad();
|
||
|
var radLon = coord[longitude].toRad();
|
||
|
var radDist = distance / radius;
|
||
|
var minLat = radLat - radDist;
|
||
|
var maxLat = radLat + radDist;
|
||
|
var MAX_LAT_RAD = MAX_LAT.toRad();
|
||
|
var MIN_LAT_RAD = MIN_LAT.toRad();
|
||
|
var MAX_LON_RAD = MAX_LON.toRad();
|
||
|
var MIN_LON_RAD = MIN_LON.toRad();
|
||
|
var minLon, maxLon;
|
||
|
if (minLat > MIN_LAT_RAD && maxLat < MAX_LAT_RAD) {
|
||
|
var deltaLon = Math.asin(Math.sin(radDist) / Math.cos(radLat));
|
||
|
minLon = radLon - deltaLon;
|
||
|
if (minLon < MIN_LON_RAD) {
|
||
|
minLon += 2 * Math.PI;
|
||
|
}
|
||
|
maxLon = radLon + deltaLon;
|
||
|
if (maxLon > MAX_LON_RAD) {
|
||
|
maxLon -= 2 * Math.PI;
|
||
|
}
|
||
|
} else {
|
||
|
// A pole is within the distance.
|
||
|
minLat = Math.max(minLat, MIN_LAT_RAD);
|
||
|
maxLat = Math.min(maxLat, MAX_LAT_RAD);
|
||
|
minLon = MIN_LON_RAD;
|
||
|
maxLon = MAX_LON_RAD;
|
||
|
}
|
||
|
|
||
|
return [
|
||
|
// Southwest
|
||
|
{"latitude": minLat.toDeg(), "longitude": minLon.toDeg()},
|
||
|
// Northeast
|
||
|
{"latitude": maxLat.toDeg(), "longitude": maxLon.toDeg()}
|
||
|
];
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Checks whether a point is inside of a polygon or not.
|
||
|
* Note that the polygon coords must be in correct order!
|
||
|
*
|
||
|
* @param object coordinate to check e.g. {latitude: 51.5023, longitude: 7.3815}
|
||
|
* @param array array with coords e.g. [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...]
|
||
|
* @return bool true if the coordinate is inside the given polygon
|
||
|
*/
|
||
|
isPointInside: function(latlng, coords) {
|
||
|
|
||
|
var keys = geolib.getKeys(latlng);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
|
||
|
for(var c = false, i = -1, l = coords.length, j = l - 1; ++i < l; j = i) {
|
||
|
|
||
|
if(
|
||
|
(
|
||
|
(coords[i][longitude] <= latlng[longitude] && latlng[longitude] < coords[j][longitude]) ||
|
||
|
(coords[j][longitude] <= latlng[longitude] && latlng[longitude] < coords[i][longitude])
|
||
|
) &&
|
||
|
(
|
||
|
latlng[latitude] < (coords[j][latitude] - coords[i][latitude]) *
|
||
|
(latlng[longitude] - coords[i][longitude]) /
|
||
|
(coords[j][longitude] - coords[i][longitude]) +
|
||
|
coords[i][latitude]
|
||
|
)
|
||
|
) {
|
||
|
c = !c;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
return c;
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Shortcut for geolib.isPointInside()
|
||
|
*/
|
||
|
isInside: function() {
|
||
|
return geolib.isPointInside.apply(geolib, arguments);
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Checks whether a point is inside of a circle or not.
|
||
|
*
|
||
|
* @param object coordinate to check (e.g. {latitude: 51.5023, longitude: 7.3815})
|
||
|
* @param object coordinate of the circle's center (e.g. {latitude: 51.4812, longitude: 7.4025})
|
||
|
* @param integer maximum radius in meters
|
||
|
* @return bool true if the coordinate is inside the given radius
|
||
|
*/
|
||
|
isPointInCircle: function(latlng, center, radius) {
|
||
|
|
||
|
return geolib.getDistance(latlng, center) < radius;
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Shortcut for geolib.isPointInCircle()
|
||
|
*/
|
||
|
withinRadius: function() {
|
||
|
return geolib.isPointInCircle.apply(geolib, arguments);
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Gets rhumb line bearing of two points. Find out about the difference between rhumb line and
|
||
|
* great circle bearing on Wikipedia. It's quite complicated. Rhumb line should be fine in most cases:
|
||
|
*
|
||
|
* http://en.wikipedia.org/wiki/Rhumb_line#General_and_mathematical_description
|
||
|
*
|
||
|
* Function heavily based on Doug Vanderweide's great PHP version (licensed under GPL 3.0)
|
||
|
* http://www.dougv.com/2009/07/13/calculating-the-bearing-and-compass-rose-direction-between-two-latitude-longitude-coordinates-in-php/
|
||
|
*
|
||
|
* @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815})
|
||
|
* @param object destination coordinate
|
||
|
* @return integer calculated bearing
|
||
|
*/
|
||
|
getRhumbLineBearing: function(originLL, destLL) {
|
||
|
|
||
|
var keys = geolib.getKeys(originLL);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
|
||
|
// difference of longitude coords
|
||
|
var diffLon = geolib.useDecimal(destLL[longitude]).toRad() - geolib.useDecimal(originLL[longitude]).toRad();
|
||
|
|
||
|
// difference latitude coords phi
|
||
|
var diffPhi = Math.log(Math.tan(geolib.useDecimal(destLL[latitude]).toRad() / 2 + Math.PI / 4) / Math.tan(geolib.useDecimal(originLL[latitude]).toRad() / 2 + Math.PI / 4));
|
||
|
|
||
|
// recalculate diffLon if it is greater than pi
|
||
|
if(Math.abs(diffLon) > Math.PI) {
|
||
|
if(diffLon > 0) {
|
||
|
diffLon = (2 * Math.PI - diffLon) * -1;
|
||
|
}
|
||
|
else {
|
||
|
diffLon = 2 * Math.PI + diffLon;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//return the angle, normalized
|
||
|
return (Math.atan2(diffLon, diffPhi).toDeg() + 360) % 360;
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Gets great circle bearing of two points. See description of getRhumbLineBearing for more information
|
||
|
*
|
||
|
* @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815})
|
||
|
* @param object destination coordinate
|
||
|
* @return integer calculated bearing
|
||
|
*/
|
||
|
getBearing: function(originLL, destLL) {
|
||
|
|
||
|
var keys = geolib.getKeys(originLL);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
|
||
|
destLL[latitude] = geolib.useDecimal(destLL[latitude]);
|
||
|
destLL[longitude] = geolib.useDecimal(destLL[longitude]);
|
||
|
originLL[latitude] = geolib.useDecimal(originLL[latitude]);
|
||
|
originLL[longitude] = geolib.useDecimal(originLL[longitude]);
|
||
|
|
||
|
var bearing = (
|
||
|
(
|
||
|
Math.atan2(
|
||
|
Math.sin(
|
||
|
destLL[longitude].toRad() -
|
||
|
originLL[longitude].toRad()
|
||
|
) *
|
||
|
Math.cos(
|
||
|
destLL[latitude].toRad()
|
||
|
),
|
||
|
Math.cos(
|
||
|
originLL[latitude].toRad()
|
||
|
) *
|
||
|
Math.sin(
|
||
|
destLL[latitude].toRad()
|
||
|
) -
|
||
|
Math.sin(
|
||
|
originLL[latitude].toRad()
|
||
|
) *
|
||
|
Math.cos(
|
||
|
destLL[latitude].toRad()
|
||
|
) *
|
||
|
Math.cos(
|
||
|
destLL[longitude].toRad() - originLL[longitude].toRad()
|
||
|
)
|
||
|
)
|
||
|
).toDeg() + 360
|
||
|
) % 360;
|
||
|
|
||
|
return bearing;
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Gets the compass direction from an origin coordinate to a destination coordinate.
|
||
|
*
|
||
|
* @param object origin coordinate (e.g. {latitude: 51.5023, longitude: 7.3815})
|
||
|
* @param object destination coordinate
|
||
|
* @param string Bearing mode. Can be either circle or rhumbline
|
||
|
* @return object Returns an object with a rough (NESW) and an exact direction (NNE, NE, ENE, E, ESE, etc).
|
||
|
*/
|
||
|
getCompassDirection: function(originLL, destLL, bearingMode) {
|
||
|
|
||
|
var direction, bearing;
|
||
|
if(bearingMode == 'circle') { // use great circle bearing
|
||
|
bearing = geolib.getBearing(originLL, destLL);
|
||
|
} else { // default is rhumb line bearing
|
||
|
bearing = geolib.getRhumbLineBearing(originLL, destLL);
|
||
|
}
|
||
|
|
||
|
switch(Math.round(bearing/22.5)) {
|
||
|
case 1:
|
||
|
direction = {exact: "NNE", rough: "N"};
|
||
|
break;
|
||
|
case 2:
|
||
|
direction = {exact: "NE", rough: "N"};
|
||
|
break;
|
||
|
case 3:
|
||
|
direction = {exact: "ENE", rough: "E"};
|
||
|
break;
|
||
|
case 4:
|
||
|
direction = {exact: "E", rough: "E"};
|
||
|
break;
|
||
|
case 5:
|
||
|
direction = {exact: "ESE", rough: "E"};
|
||
|
break;
|
||
|
case 6:
|
||
|
direction = {exact: "SE", rough: "E"};
|
||
|
break;
|
||
|
case 7:
|
||
|
direction = {exact: "SSE", rough: "S"};
|
||
|
break;
|
||
|
case 8:
|
||
|
direction = {exact: "S", rough: "S"};
|
||
|
break;
|
||
|
case 9:
|
||
|
direction = {exact: "SSW", rough: "S"};
|
||
|
break;
|
||
|
case 10:
|
||
|
direction = {exact: "SW", rough: "S"};
|
||
|
break;
|
||
|
case 11:
|
||
|
direction = {exact: "WSW", rough: "W"};
|
||
|
break;
|
||
|
case 12:
|
||
|
direction = {exact: "W", rough: "W"};
|
||
|
break;
|
||
|
case 13:
|
||
|
direction = {exact: "WNW", rough: "W"};
|
||
|
break;
|
||
|
case 14:
|
||
|
direction = {exact: "NW", rough: "W"};
|
||
|
break;
|
||
|
case 15:
|
||
|
direction = {exact: "NNW", rough: "N"};
|
||
|
break;
|
||
|
default:
|
||
|
direction = {exact: "N", rough: "N"};
|
||
|
}
|
||
|
|
||
|
direction['bearing'] = bearing;
|
||
|
return direction;
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Shortcut for getCompassDirection
|
||
|
*/
|
||
|
getDirection: function(originLL, destLL, bearingMode) {
|
||
|
return geolib.getCompassDirection.apply(geolib, arguments);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Sorts an array of coords by distance from a reference coordinate
|
||
|
*
|
||
|
* @param object reference coordinate e.g. {latitude: 51.5023, longitude: 7.3815}
|
||
|
* @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...]
|
||
|
* @return array ordered array
|
||
|
*/
|
||
|
orderByDistance: function(latlng, coords) {
|
||
|
|
||
|
var keys = geolib.getKeys(latlng);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
|
||
|
var coordsArray = [];
|
||
|
for(var coord in coords) {
|
||
|
var d = geolib.getDistance(latlng, coords[coord]);
|
||
|
coordsArray.push({
|
||
|
key: coord, latitude: coords[coord][latitude],
|
||
|
longitude: coords[coord][longitude], distance: d
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return coordsArray.sort(function(a, b) { return a.distance - b.distance; });
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Finds the nearest coordinate to a reference coordinate
|
||
|
*
|
||
|
* @param object reference coordinate e.g. {latitude: 51.5023, longitude: 7.3815}
|
||
|
* @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...]
|
||
|
* @return array ordered array
|
||
|
*/
|
||
|
findNearest: function(latlng, coords, offset) {
|
||
|
|
||
|
offset = offset || 0;
|
||
|
var ordered = geolib.orderByDistance(latlng, coords);
|
||
|
return ordered[offset];
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Calculates the length of a given path
|
||
|
*
|
||
|
* @param mixed array or object with coords [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...]
|
||
|
* @return integer length of the path (in meters)
|
||
|
*/
|
||
|
getPathLength: function(coords) {
|
||
|
|
||
|
var dist = 0, last;
|
||
|
for (var i = 0, l = coords.length; i < l; ++i) {
|
||
|
if(last) {
|
||
|
dist += geolib.getDistance(coords[i], last);
|
||
|
}
|
||
|
last = coords[i];
|
||
|
}
|
||
|
|
||
|
return dist;
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Calculates the speed between to points within a given time span.
|
||
|
*
|
||
|
* @param object coords with javascript timestamp {latitude: 51.5143, longitude: 7.4138, time: 1360231200880}
|
||
|
* @param object coords with javascript timestamp {latitude: 51.5502, longitude: 7.4323, time: 1360245600460}
|
||
|
* @param object options (currently "unit" is the only option. Default: km(h));
|
||
|
* @return float speed in *unit* per hour
|
||
|
*/
|
||
|
getSpeed: function(start, end, options) {
|
||
|
|
||
|
var unit = options && options.unit || 'km';
|
||
|
|
||
|
if(unit == 'mph') {
|
||
|
unit = 'mi';
|
||
|
} else if(unit == 'kmh') {
|
||
|
unit = 'km';
|
||
|
}
|
||
|
|
||
|
var distance = geolib.getDistance(start, end);
|
||
|
var time = ((end.time*1)/1000) - ((start.time*1)/1000);
|
||
|
var mPerHr = (distance/time)*3600;
|
||
|
var speed = Math.round(mPerHr * geolib.measures[unit] * 10000)/10000;
|
||
|
return speed;
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Converts a distance from meters to km, mm, cm, mi, ft, in or yd
|
||
|
*
|
||
|
* @param string Format to be converted in
|
||
|
* @param float Distance in meters
|
||
|
* @param float Decimal places for rounding (default: 4)
|
||
|
* @return float Converted distance
|
||
|
*/
|
||
|
convertUnit: function(unit, distance, round) {
|
||
|
|
||
|
if(distance === 0 || typeof distance == 'undefined') {
|
||
|
|
||
|
if(geolib.distance === 0) {
|
||
|
// throw 'No distance given.';
|
||
|
return 0;
|
||
|
} else {
|
||
|
distance = geolib.distance;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
unit = unit || 'm';
|
||
|
round = (null == round ? 4 : round);
|
||
|
|
||
|
if(typeof geolib.measures[unit] !== 'undefined') {
|
||
|
return geolib.round(distance * geolib.measures[unit], round);
|
||
|
} else {
|
||
|
throw new Error('Unknown unit for conversion.');
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Checks if a value is in decimal format or, if neccessary, converts to decimal
|
||
|
*
|
||
|
* @param mixed Value to be checked/converted
|
||
|
* @return float Coordinate in decimal format
|
||
|
*/
|
||
|
useDecimal: function(value) {
|
||
|
|
||
|
value = value.toString().replace(/\s*/, '');
|
||
|
|
||
|
// looks silly but works as expected
|
||
|
// checks if value is in decimal format
|
||
|
if (!isNaN(parseFloat(value)) && parseFloat(value) == value) {
|
||
|
return parseFloat(value);
|
||
|
// checks if it's sexagesimal format (HHH° MM' SS" (NESW))
|
||
|
} else if(geolib.isSexagesimal(value) === true) {
|
||
|
return parseFloat(geolib.sexagesimal2decimal(value));
|
||
|
} else {
|
||
|
throw 'Unknown format.';
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Converts a decimal coordinate value to sexagesimal format
|
||
|
*
|
||
|
* @param float decimal
|
||
|
* @return string Sexagesimal value (XX° YY' ZZ")
|
||
|
*/
|
||
|
decimal2sexagesimal: function(dec) {
|
||
|
|
||
|
if (dec in geolib.sexagesimal) {
|
||
|
return geolib.sexagesimal[dec];
|
||
|
}
|
||
|
|
||
|
var tmp = dec.toString().split('.');
|
||
|
|
||
|
var deg = Math.abs(tmp[0]);
|
||
|
var min = ('0.' + tmp[1])*60;
|
||
|
var sec = min.toString().split('.');
|
||
|
|
||
|
min = Math.floor(min);
|
||
|
sec = (('0.' + sec[1]) * 60).toFixed(2);
|
||
|
|
||
|
geolib.sexagesimal[dec] = (deg + '° ' + min + "' " + sec + '"');
|
||
|
|
||
|
return geolib.sexagesimal[dec];
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Converts a sexagesimal coordinate to decimal format
|
||
|
*
|
||
|
* @param float Sexagesimal coordinate
|
||
|
* @return string Decimal value (XX.XXXXXXXX)
|
||
|
*/
|
||
|
sexagesimal2decimal: function(sexagesimal) {
|
||
|
|
||
|
if (sexagesimal in geolib.decimal) {
|
||
|
return geolib.decimal[sexagesimal];
|
||
|
}
|
||
|
|
||
|
var regEx = new RegExp(sexagesimalPattern);
|
||
|
var data = regEx.exec(sexagesimal);
|
||
|
var min = 0, sec = 0;
|
||
|
|
||
|
if(data) {
|
||
|
min = parseFloat(data[2]/60);
|
||
|
sec = parseFloat(data[4]/3600) || 0;
|
||
|
}
|
||
|
|
||
|
var dec = ((parseFloat(data[1]) + min + sec)).toFixed(8);
|
||
|
// South and West are negative decimals
|
||
|
dec = (data[7] == 'S' || data[7] == 'W') ? dec * -1 : dec;
|
||
|
|
||
|
geolib.decimal[sexagesimal] = dec;
|
||
|
|
||
|
return dec;
|
||
|
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Checks if a value is in sexagesimal format
|
||
|
*
|
||
|
* @param string Value to be checked
|
||
|
* @return bool True if in sexagesimal format
|
||
|
*/
|
||
|
isSexagesimal: function(value) {
|
||
|
|
||
|
return sexagesimalPattern.test(value);
|
||
|
|
||
|
},
|
||
|
|
||
|
round: function(value, n) {
|
||
|
var decPlace = Math.pow(10, n);
|
||
|
return Math.round(value * decPlace)/decPlace;
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
/* Optional elevation addon requires Googlemaps API JS */
|
||
|
|
||
|
/*global google:true geolib:true require:true module:true elevationResult*/
|
||
|
/**
|
||
|
* @param Array Collection of coords [{latitude: 51.510, longitude: 7.1321}, {latitude: 49.1238, longitude: "8° 30' W"}, ...]
|
||
|
*
|
||
|
* @return Array [{lat:#lat, lng:#lng, elev:#elev},....]}
|
||
|
*/
|
||
|
geolib.getElevation = function() {
|
||
|
if (typeof window.navigator !== 'undefined') {
|
||
|
geolib.getElevationClient.apply(this, arguments);
|
||
|
} else {
|
||
|
geolib.getElevationServer.apply(this, arguments);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
geolib.getElevationClient = function(coords, cb) {
|
||
|
|
||
|
if (!window.google) {
|
||
|
throw new Error("Google maps api not loaded");
|
||
|
}
|
||
|
|
||
|
if (coords.length === 0) {
|
||
|
return cb(null, null);
|
||
|
}
|
||
|
|
||
|
if (coords.length === 1) {
|
||
|
return cb(new Error("getElevation requires at least 2 points."));
|
||
|
}
|
||
|
|
||
|
var path = [];
|
||
|
var keys = geolib.getKeys(coords[0]);
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
|
||
|
for(var i = 0; i < coords.length; i++) {
|
||
|
path.push(new google.maps.LatLng(
|
||
|
geolib.useDecimal(coords[i][latitude]),
|
||
|
geolib.useDecimal(coords[i][longitude])
|
||
|
));
|
||
|
}
|
||
|
|
||
|
var positionalRequest = {
|
||
|
'path': path,
|
||
|
'samples': path.length
|
||
|
};
|
||
|
var elevationService = new google.maps.ElevationService();
|
||
|
elevationService.getElevationAlongPath(positionalRequest,function (results, status) {
|
||
|
geolib.elevationHandler(results, status, coords, keys, cb);
|
||
|
});
|
||
|
|
||
|
};
|
||
|
|
||
|
geolib.getElevationServer = function(coords, cb) {
|
||
|
|
||
|
if (coords.length === 0) {
|
||
|
return cb(null, null);
|
||
|
}
|
||
|
|
||
|
if (coords.length === 1) {
|
||
|
return cb(new Error("getElevation requires at least 2 points."));
|
||
|
}
|
||
|
|
||
|
var gm = require('googlemaps');
|
||
|
var path = [];
|
||
|
var keys = geolib.getKeys(coords[0]);
|
||
|
//coords[0]
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
|
||
|
for(var i = 0; i < coords.length; i++) {
|
||
|
path.push(geolib.useDecimal(coords[i][latitude]) + ',' +
|
||
|
geolib.useDecimal(coords[i][longitude]));
|
||
|
}
|
||
|
|
||
|
gm.elevationFromPath(path.join('|'), path.length, function(err, results) {
|
||
|
geolib.elevationHandler(results.results, results.status, coords, keys, cb);
|
||
|
});
|
||
|
|
||
|
},
|
||
|
|
||
|
geolib.elevationHandler = function(results, status, coords, keys, cb){
|
||
|
var latsLngsElevs = [];
|
||
|
var latitude = keys.latitude;
|
||
|
var longitude = keys.longitude;
|
||
|
if (status == "OK" ) {
|
||
|
for (var i = 0; i < results.length; i++) {
|
||
|
latsLngsElevs.push({
|
||
|
"lat":coords[i][latitude],
|
||
|
"lng":coords[i][longitude],
|
||
|
"elev":results[i].elevation
|
||
|
});
|
||
|
}
|
||
|
cb(null, latsLngsElevs);
|
||
|
} else {
|
||
|
cb(new Error("Could not get elevation using Google's API"), elevationResult.status);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @param Array [{lat:#lat, lng:#lng, elev:#elev},....]}
|
||
|
*
|
||
|
* @return Number % grade
|
||
|
*/
|
||
|
geolib.getGrade = function(coords){
|
||
|
var keys = geolib.getKeys(coords[0]);
|
||
|
var elevation = keys.elevation;
|
||
|
var rise = Math.abs(coords[coords.length-1][elevation] - coords[0][elevation]);
|
||
|
var run = geolib.getPathLength(coords);
|
||
|
return Math.floor((rise/run)*100);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @param Array [{lat:#lat, lng:#lng, elev:#elev},....]}
|
||
|
*
|
||
|
* @return Object {gain:#gain, loss:#loss}
|
||
|
*/
|
||
|
geolib.getTotalElevationGainAndLoss = function(coords){
|
||
|
var keys = geolib.getKeys(coords[0]);
|
||
|
var elevation = keys.elevation;
|
||
|
var gain = 0;
|
||
|
var loss = 0;
|
||
|
for(var i = 0; i < coords.length - 1; i++){
|
||
|
var deltaElev = coords[i][elevation] - coords[i + 1][elevation];
|
||
|
if (deltaElev > 0) {
|
||
|
loss += deltaElev;
|
||
|
} else {
|
||
|
gain += Math.abs(deltaElev);
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
"gain": gain,
|
||
|
"loss": loss
|
||
|
};
|
||
|
};
|
||
|
|
||
|
|
||
|
if (typeof(Number.prototype.toRad) === "undefined") {
|
||
|
Number.prototype.toRad = function() {
|
||
|
return this * Math.PI / 180;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
if (typeof(Number.prototype.toDeg) === "undefined") {
|
||
|
Number.prototype.toDeg = function() {
|
||
|
return this * 180 / Math.PI;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
// we're in a browser
|
||
|
window.geolib = geolib;
|
||
|
if (typeof module != 'undefined') {
|
||
|
module.exports = geolib;
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
if (typeof module != 'undefined') {
|
||
|
|
||
|
// Node module
|
||
|
global.geolib = module.exports = geolib;
|
||
|
|
||
|
} else if (typeof define === "function" && define.amd) {
|
||
|
|
||
|
// AMD module
|
||
|
define("geolib", [], function () {
|
||
|
return geolib;
|
||
|
});
|
||
|
|
||
|
// what's the difference to:
|
||
|
//define(function() { return geolib; });
|
||
|
// ?
|
||
|
|
||
|
} else {
|
||
|
|
||
|
// we're in a browser, yay
|
||
|
global.geolib = geolib;
|
||
|
|
||
|
}
|
||
|
|
||
|
}(this));
|