1371 lines
46 KiB
JavaScript
1371 lines
46 KiB
JavaScript
;(function(global, undefined) {
|
|
|
|
"use strict";
|
|
|
|
function Geolib() {}
|
|
|
|
// Constants
|
|
Geolib.TO_RAD = Math.PI / 180;
|
|
Geolib.TO_DEG = 180 / Math.PI;
|
|
Geolib.PI_X2 = Math.PI * 2;
|
|
Geolib.PI_DIV4 = Math.PI / 4;
|
|
|
|
// Setting readonly defaults
|
|
var geolib = Object.create(Geolib.prototype, {
|
|
version: {
|
|
value: "$version$"
|
|
},
|
|
radius: {
|
|
value: 6378137
|
|
},
|
|
minLat: {
|
|
value: -90
|
|
},
|
|
maxLat: {
|
|
value: 90
|
|
},
|
|
minLon: {
|
|
value: -180
|
|
},
|
|
maxLon: {
|
|
value: 180
|
|
},
|
|
sexagesimalPattern: {
|
|
value: /^([0-9]{1,3})°\s*([0-9]{1,3}(?:\.(?:[0-9]{1,2}))?)'\s*(([0-9]{1,3}(\.([0-9]{1,4}))?)"\s*)?([NEOSW]?)$/
|
|
},
|
|
measures: {
|
|
value: Object.create(Object.prototype, {
|
|
"m" : {value: 1},
|
|
"km": {value: 0.001},
|
|
"cm": {value: 100},
|
|
"mm": {value: 1000},
|
|
"mi": {value: (1 / 1609.344)},
|
|
"sm": {value: (1 / 1852.216)},
|
|
"ft": {value: (100 / 30.48)},
|
|
"in": {value: (100 / 2.54)},
|
|
"yd": {value: (1 / 0.9144)}
|
|
})
|
|
},
|
|
prototype: {
|
|
value: Geolib.prototype
|
|
},
|
|
extend: {
|
|
value: function(methods, overwrite) {
|
|
for(var prop in methods) {
|
|
if(typeof geolib.prototype[prop] === 'undefined' || overwrite === true) {
|
|
if(typeof methods[prop] === 'function' && typeof methods[prop].bind === 'function') {
|
|
geolib.prototype[prop] = methods[prop].bind(geolib);
|
|
} else {
|
|
geolib.prototype[prop] = methods[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (typeof(Number.prototype.toRad) === 'undefined') {
|
|
Number.prototype.toRad = function() {
|
|
return this * Geolib.TO_RAD;
|
|
};
|
|
}
|
|
|
|
if (typeof(Number.prototype.toDeg) === 'undefined') {
|
|
Number.prototype.toDeg = function() {
|
|
return this * Geolib.TO_DEG;
|
|
};
|
|
}
|
|
|
|
// Here comes the magic
|
|
geolib.extend({
|
|
|
|
decimal: {},
|
|
|
|
sexagesimal: {},
|
|
|
|
distance: null,
|
|
|
|
getKeys: function(point) {
|
|
|
|
// GeoJSON Array [longitude, latitude(, elevation)]
|
|
if(Object.prototype.toString.call(point) == '[object Array]') {
|
|
|
|
return {
|
|
longitude: point.length >= 1 ? 0 : undefined,
|
|
latitude: point.length >= 2 ? 1 : undefined,
|
|
elevation: point.length >= 3 ? 2 : undefined
|
|
};
|
|
|
|
}
|
|
|
|
var getKey = function(possibleValues) {
|
|
|
|
var key;
|
|
|
|
possibleValues.every(function(val) {
|
|
// TODO: check if point is an object
|
|
if(typeof point != 'object') {
|
|
return true;
|
|
}
|
|
return point.hasOwnProperty(val) ? (function() { key = val; return false; }()) : true;
|
|
});
|
|
|
|
return key;
|
|
|
|
};
|
|
|
|
var longitude = getKey(['lng', 'lon', 'longitude']);
|
|
var latitude = getKey(['lat', 'latitude']);
|
|
var elevation = getKey(['alt', 'altitude', 'elevation', 'elev']);
|
|
|
|
// return undefined if not at least one valid property was found
|
|
if(typeof latitude == 'undefined' &&
|
|
typeof longitude == 'undefined' &&
|
|
typeof elevation == 'undefined') {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
elevation: elevation
|
|
};
|
|
|
|
},
|
|
|
|
// returns latitude of a given point, converted to decimal
|
|
// set raw to true to avoid conversion
|
|
getLat: function(point, raw) {
|
|
return raw === true ? point[this.getKeys(point).latitude] : this.useDecimal(point[this.getKeys(point).latitude]);
|
|
},
|
|
|
|
// Alias for getLat
|
|
latitude: function(point) {
|
|
return this.getLat.call(this, point);
|
|
},
|
|
|
|
// returns longitude of a given point, converted to decimal
|
|
// set raw to true to avoid conversion
|
|
getLon: function(point, raw) {
|
|
return raw === true ? point[this.getKeys(point).longitude] : this.useDecimal(point[this.getKeys(point).longitude]);
|
|
},
|
|
|
|
// Alias for getLon
|
|
longitude: function(point) {
|
|
return this.getLon.call(this, point);
|
|
},
|
|
|
|
getElev: function(point) {
|
|
return point[this.getKeys(point).elevation];
|
|
},
|
|
|
|
// Alias for getElev
|
|
elevation: function(point) {
|
|
return this.getElev.call(this, point);
|
|
},
|
|
|
|
coords: function(point, raw) {
|
|
|
|
var retval = {
|
|
latitude: raw === true ? point[this.getKeys(point).latitude] : this.useDecimal(point[this.getKeys(point).latitude]),
|
|
longitude: raw === true ? point[this.getKeys(point).longitude] : this.useDecimal(point[this.getKeys(point).longitude])
|
|
};
|
|
|
|
var elev = point[this.getKeys(point).elevation];
|
|
|
|
if(typeof elev !== 'undefined') {
|
|
retval['elevation'] = elev;
|
|
}
|
|
|
|
return retval;
|
|
|
|
},
|
|
|
|
// Alias for coords
|
|
ll: function(point, raw) {
|
|
return this.coords.call(this, point, raw);
|
|
},
|
|
|
|
|
|
// checks if a variable contains a valid latlong object
|
|
validate: function(point) {
|
|
|
|
var keys = this.getKeys(point);
|
|
|
|
if(typeof keys === 'undefined' || typeof keys.latitude === 'undefined' || keys.longitude === 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
var lat = point[keys.latitude];
|
|
var lng = point[keys.longitude];
|
|
|
|
if(typeof lat === 'undefined' || !this.isDecimal(lat) && !this.isSexagesimal(lat)) {
|
|
return false;
|
|
}
|
|
|
|
if(typeof lng === 'undefined' || !this.isDecimal(lng) && !this.isSexagesimal(lng)) {
|
|
return false;
|
|
}
|
|
|
|
lat = this.useDecimal(lat);
|
|
lng = this.useDecimal(lng);
|
|
|
|
if(lat < this.minLat || lat > this.maxLat || lng < this.minLon || lng > this.maxLon) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
/**
|
|
* 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)
|
|
* @param integer Precision (in decimal cases)
|
|
* @return integer Distance (in meters)
|
|
*/
|
|
getDistance: function(start, end, accuracy, precision) {
|
|
|
|
accuracy = Math.floor(accuracy) || 1;
|
|
precision = Math.floor(precision) || 0;
|
|
|
|
var s = this.coords(start);
|
|
var e = this.coords(end);
|
|
|
|
var a = 6378137, b = 6356752.314245, f = 1/298.257223563; // WGS-84 ellipsoid params
|
|
var L = (e['longitude']-s['longitude']).toRad();
|
|
|
|
var cosSigma, sigma, sinAlpha, cosSqAlpha, cos2SigmaM, sinSigma;
|
|
|
|
var U1 = Math.atan((1-f) * Math.tan(parseFloat(s['latitude']).toRad()));
|
|
var U2 = Math.atan((1-f) * Math.tan(parseFloat(e['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(precision); // round to 1mm precision
|
|
|
|
//if (start.hasOwnProperty(elevation) && end.hasOwnProperty(elevation)) {
|
|
if (typeof this.elevation(start) !== 'undefined' && typeof this.elevation(end) !== 'undefined') {
|
|
var climb = Math.abs(this.elevation(start) - this.elevation(end));
|
|
distance = Math.sqrt(distance * distance + climb * climb);
|
|
}
|
|
|
|
return this.distance = Math.round(distance * Math.pow(10, precision) / accuracy) * accuracy / Math.pow(10, precision);
|
|
|
|
/*
|
|
// 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 far 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) {
|
|
|
|
accuracy = Math.floor(accuracy) || 1;
|
|
|
|
var distance =
|
|
Math.round(
|
|
Math.acos(
|
|
Math.sin(
|
|
this.latitude(end).toRad()
|
|
) *
|
|
Math.sin(
|
|
this.latitude(start).toRad()
|
|
) +
|
|
Math.cos(
|
|
this.latitude(end).toRad()
|
|
) *
|
|
Math.cos(
|
|
this.latitude(start).toRad()
|
|
) *
|
|
Math.cos(
|
|
this.longitude(start).toRad() - this.longitude(end).toRad()
|
|
)
|
|
) * this.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}
|
|
*/
|
|
getCenter: function(coords) {
|
|
|
|
var coordsArray = coords;
|
|
if(typeof coords === 'object' && !(coords instanceof Array)) {
|
|
|
|
coordsArray = [];
|
|
|
|
for(var key in coords) {
|
|
coordsArray.push(
|
|
this.coords(coords[key])
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
if(!coordsArray.length) {
|
|
return false;
|
|
}
|
|
|
|
var X = 0.0;
|
|
var Y = 0.0;
|
|
var Z = 0.0;
|
|
var lat, lon, hyp;
|
|
|
|
coordsArray.forEach(function(coord) {
|
|
|
|
lat = this.latitude(coord).toRad();
|
|
lon = this.longitude(coord).toRad();
|
|
|
|
X += Math.cos(lat) * Math.cos(lon);
|
|
Y += Math.cos(lat) * Math.sin(lon);
|
|
Z += Math.sin(lat);
|
|
|
|
}, this);
|
|
|
|
var nb_coords = coordsArray.length;
|
|
X = X / nb_coords;
|
|
Y = Y / nb_coords;
|
|
Z = Z / nb_coords;
|
|
|
|
lon = Math.atan2(Y, X);
|
|
hyp = Math.sqrt(X * X + Y * Y);
|
|
lat = Math.atan2(Z, hyp);
|
|
|
|
return {
|
|
latitude: (lat * Geolib.TO_DEG).toFixed(6),
|
|
longitude: (lon * Geolib.TO_DEG).toFixed(6)
|
|
};
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* 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 useElevation = this.elevation(coords[0]);
|
|
|
|
var stats = {
|
|
maxLat: -Infinity,
|
|
minLat: Infinity,
|
|
maxLng: -Infinity,
|
|
minLng: Infinity
|
|
};
|
|
|
|
if (typeof useElevation != 'undefined') {
|
|
stats.maxElev = 0;
|
|
stats.minElev = Infinity;
|
|
}
|
|
|
|
for (var i = 0, l = coords.length; i < l; ++i) {
|
|
|
|
stats.maxLat = Math.max(this.latitude(coords[i]), stats.maxLat);
|
|
stats.minLat = Math.min(this.latitude(coords[i]), stats.minLat);
|
|
stats.maxLng = Math.max(this.longitude(coords[i]), stats.maxLng);
|
|
stats.minLng = Math.min(this.longitude(coords[i]), stats.minLng);
|
|
|
|
if (useElevation) {
|
|
stats.maxElev = Math.max(this.elevation(coords[i]), stats.maxElev);
|
|
stats.minElev = Math.min(this.elevation(coords[i]), stats.minElev);
|
|
}
|
|
|
|
}
|
|
|
|
return stats;
|
|
|
|
},
|
|
|
|
/**
|
|
* Calculates the center of the bounds of geo coordinates.
|
|
*
|
|
* On polygons like political borders (eg. states)
|
|
* this may gives a closer result to human expectation, than `getCenter`,
|
|
* because that function can be disturbed by uneven distribution of
|
|
* point in different sides.
|
|
* Imagine the US state Oklahoma: `getCenter` on that gives a southern
|
|
* point, because the southern border contains a lot more nodes,
|
|
* than the others.
|
|
*
|
|
* @param array Collection of coords [{latitude: 51.510, longitude: 7.1321}, {latitude: 49.1238, longitude: "8° 30' W"}, ...]
|
|
* @return object {latitude: centerLat, longitude: centerLng}
|
|
*/
|
|
getCenterOfBounds: function(coords) {
|
|
var b = this.getBounds(coords);
|
|
var latitude = b.minLat + ((b.maxLat - b.minLat) / 2);
|
|
var longitude = b.minLng + ((b.maxLng - b.minLng) / 2);
|
|
return {
|
|
latitude: parseFloat(latitude.toFixed(6)),
|
|
longitude: parseFloat(longitude.toFixed(6))
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* 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 latitude = this.latitude(point);
|
|
var longitude = this.longitude(point);
|
|
|
|
var radLat = latitude.toRad();
|
|
var radLon = longitude.toRad();
|
|
|
|
var radDist = distance / this.radius;
|
|
var minLat = radLat - radDist;
|
|
var maxLat = radLat + radDist;
|
|
|
|
var MAX_LAT_RAD = this.maxLat.toRad();
|
|
var MIN_LAT_RAD = this.minLat.toRad();
|
|
var MAX_LON_RAD = this.maxLon.toRad();
|
|
var MIN_LON_RAD = this.minLon.toRad();
|
|
|
|
var minLon;
|
|
var 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 += Geolib.PI_X2;
|
|
}
|
|
|
|
maxLon = radLon + deltaLon;
|
|
|
|
if (maxLon > MAX_LON_RAD) {
|
|
maxLon -= Geolib.PI_X2;
|
|
}
|
|
|
|
} 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) {
|
|
|
|
for(var c = false, i = -1, l = coords.length, j = l - 1; ++i < l; j = i) {
|
|
|
|
if(
|
|
(
|
|
(this.longitude(coords[i]) <= this.longitude(latlng) && this.longitude(latlng) < this.longitude(coords[j])) ||
|
|
(this.longitude(coords[j]) <= this.longitude(latlng) && this.longitude(latlng) < this.longitude(coords[i]))
|
|
) &&
|
|
(
|
|
this.latitude(latlng) < (this.latitude(coords[j]) - this.latitude(coords[i])) *
|
|
(this.longitude(latlng) - this.longitude(coords[i])) /
|
|
(this.longitude(coords[j]) - this.longitude(coords[i])) +
|
|
this.latitude(coords[i])
|
|
)
|
|
) {
|
|
c = !c;
|
|
}
|
|
|
|
}
|
|
|
|
return c;
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Pre calculate the polygon coords, to speed up the point inside check.
|
|
* Use this function before calling isPointInsideWithPreparedPolygon()
|
|
* @see Algorythm from http://alienryderflex.com/polygon/
|
|
* @param array array with coords e.g. [{latitude: 51.5143, longitude: 7.4138}, {latitude: 123, longitude: 123}, ...]
|
|
*/
|
|
preparePolygonForIsPointInsideOptimized: function(coords) {
|
|
|
|
for(var i = 0, j = coords.length-1; i < coords.length; i++) {
|
|
|
|
if(this.longitude(coords[j]) === this.longitude(coords[i])) {
|
|
|
|
coords[i].constant = this.latitude(coords[i]);
|
|
coords[i].multiple = 0;
|
|
|
|
} else {
|
|
|
|
coords[i].constant = this.latitude(coords[i]) - (
|
|
this.longitude(coords[i]) * this.latitude(coords[j])
|
|
) / (
|
|
this.longitude(coords[j]) - this.longitude(coords[i])
|
|
) + (
|
|
this.longitude(coords[i])*this.latitude(coords[i])
|
|
) / (
|
|
this.longitude(coords[j])-this.longitude(coords[i])
|
|
);
|
|
|
|
coords[i].multiple = (
|
|
this.latitude(coords[j])-this.latitude(coords[i])
|
|
) / (
|
|
this.longitude(coords[j])-this.longitude(coords[i])
|
|
);
|
|
|
|
}
|
|
|
|
j=i;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* Checks whether a point is inside of a polygon or not.
|
|
* "This is useful if you have many points that need to be tested against the same (static) polygon."
|
|
* Please call the function preparePolygonForIsPointInsideOptimized() with the same coords object before using this function.
|
|
* Note that the polygon coords must be in correct order!
|
|
*
|
|
* @see Algorythm from http://alienryderflex.com/polygon/
|
|
*
|
|
* @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
|
|
*/
|
|
isPointInsideWithPreparedPolygon: function(point, coords) {
|
|
|
|
var flgPointInside = false,
|
|
y = this.longitude(point),
|
|
x = this.latitude(point);
|
|
|
|
for(var i = 0, j = coords.length-1; i < coords.length; i++) {
|
|
|
|
if ((this.longitude(coords[i]) < y && this.longitude(coords[j]) >=y ||
|
|
this.longitude(coords[j]) < y && this.longitude(coords[i]) >= y)) {
|
|
|
|
flgPointInside^=(y*coords[i].multiple+coords[i].constant < x);
|
|
|
|
}
|
|
|
|
j=i;
|
|
|
|
}
|
|
|
|
return flgPointInside;
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Shortcut for geolib.isPointInside()
|
|
*/
|
|
isInside: function() {
|
|
return this.isPointInside.apply(this, 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 within the given radius
|
|
*/
|
|
isPointInCircle: function(latlng, center, radius) {
|
|
return this.getDistance(latlng, center) < radius;
|
|
},
|
|
|
|
|
|
/**
|
|
* Shortcut for geolib.isPointInCircle()
|
|
*/
|
|
withinRadius: function() {
|
|
return this.isPointInCircle.apply(this, 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) {
|
|
|
|
// difference of longitude coords
|
|
var diffLon = this.longitude(destLL).toRad() - this.longitude(originLL).toRad();
|
|
|
|
// difference latitude coords phi
|
|
var diffPhi = Math.log(
|
|
Math.tan(
|
|
this.latitude(destLL).toRad() / 2 + Geolib.PI_DIV4
|
|
) /
|
|
Math.tan(
|
|
this.latitude(originLL).toRad() / 2 + Geolib.PI_DIV4
|
|
)
|
|
);
|
|
|
|
// recalculate diffLon if it is greater than pi
|
|
if(Math.abs(diffLon) > Math.PI) {
|
|
if(diffLon > 0) {
|
|
diffLon = (Geolib.PI_X2 - diffLon) * -1;
|
|
}
|
|
else {
|
|
diffLon = Geolib.PI_X2 + 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) {
|
|
|
|
destLL['latitude'] = this.latitude(destLL);
|
|
destLL['longitude'] = this.longitude(destLL);
|
|
originLL['latitude'] = this.latitude(originLL);
|
|
originLL['longitude'] = this.longitude(originLL);
|
|
|
|
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;
|
|
var bearing;
|
|
|
|
if(bearingMode == 'circle') {
|
|
// use great circle bearing
|
|
bearing = this.getBearing(originLL, destLL);
|
|
} else {
|
|
// default is rhumb line bearing
|
|
bearing = this.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 this.getCompassDirection.apply(this, 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 coordsArray = [];
|
|
|
|
for(var coord in coords) {
|
|
|
|
var distance = this.getDistance(latlng, coords[coord]);
|
|
var augmentedCoord = Object.create(coords[coord]);
|
|
augmentedCoord.distance = distance;
|
|
augmentedCoord.key = coord;
|
|
|
|
coordsArray.push(augmentedCoord);
|
|
|
|
}
|
|
|
|
return coordsArray.sort(function(a, b) {
|
|
return a.distance - b.distance;
|
|
});
|
|
|
|
},
|
|
|
|
/**
|
|
* Check if a point lies in line created by two other points
|
|
*
|
|
* @param object Point to check: {latitude: 123, longitude: 123}
|
|
* @param object Start of line {latitude: 123, longitude: 123}
|
|
* @param object End of line {latitude: 123, longitude: 123}
|
|
* @return boolean
|
|
*/
|
|
isPointInLine: function(point, start, end) {
|
|
|
|
return (this.getDistance(start, point, 1, 3)+this.getDistance(point, end, 1, 3)).toFixed(3)==this.getDistance(start, end, 1, 3);
|
|
},
|
|
|
|
/**
|
|
* Check if a point lies within a given distance from a line created by two other points
|
|
*
|
|
* @param object Point to check: {latitude: 123, longitude: 123}
|
|
* @param object Start of line {latitude: 123, longitude: 123}
|
|
* @param object End of line {latitude: 123, longitude: 123}
|
|
* @pararm float maximum distance from line
|
|
* @return boolean
|
|
*/
|
|
isPointNearLine: function(point, start, end, distance) {
|
|
return this.getDistanceFromLine(point, start, end) < distance;
|
|
},
|
|
|
|
/**
|
|
* return the minimum distance from a point to a line
|
|
*
|
|
* @param object Point away from line
|
|
* @param object Start of line {latitude: 123, longitude: 123}
|
|
* @param object End of line {latitude: 123, longitude: 123}
|
|
* @return float distance from point to line
|
|
*/
|
|
getDistanceFromLine: function(point, start, end) {
|
|
var d1 = this.getDistance(start, point, 1, 3);
|
|
var d2 = this.getDistance(point, end, 1, 3);
|
|
var d3 = this.getDistance(start, end, 1, 3);
|
|
var distance = 0;
|
|
|
|
// alpha is the angle between the line from start to point, and from start to end //
|
|
var alpha = Math.acos((d1*d1 + d3*d3 - d2*d2)/(2*d1*d3));
|
|
// beta is the angle between the line from end to point and from end to start //
|
|
var beta = Math.acos((d2*d2 + d3*d3 - d1*d1)/(2*d2*d3));
|
|
|
|
// if the angle is greater than 90 degrees, then the minimum distance is the
|
|
// line from the start to the point //
|
|
if(alpha>Math.PI/2) {
|
|
distance = d1;
|
|
}
|
|
// same for the beta //
|
|
else if(beta > Math.PI/2) {
|
|
distance = d2;
|
|
}
|
|
// otherwise the minimum distance is achieved through a line perpendular to the start-end line,
|
|
// which goes from the start-end line to the point //
|
|
else {
|
|
distance = Math.sin(alpha) * d1;
|
|
}
|
|
|
|
return 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, limit) {
|
|
|
|
offset = offset || 0;
|
|
limit = limit || 1;
|
|
var ordered = this.orderByDistance(latlng, coords);
|
|
|
|
if(limit === 1) {
|
|
return ordered[offset];
|
|
} else {
|
|
return ordered.splice(offset, limit);
|
|
}
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* 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;
|
|
var last;
|
|
|
|
for (var i = 0, l = coords.length; i < l; ++i) {
|
|
if(last) {
|
|
//console.log(coords[i], last, this.getDistance(coords[i], last));
|
|
dist += this.getDistance(this.coords(coords[i]), last);
|
|
}
|
|
last = this.coords(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 * this.measures[unit] * 10000)/10000;
|
|
return speed;
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Computes the destination point given an initial point, a distance
|
|
* and a bearing
|
|
*
|
|
* see http://www.movable-type.co.uk/scripts/latlong.html for the original code
|
|
*
|
|
* @param object start coordinate (e.g. {latitude: 51.5023, longitude: 7.3815})
|
|
* @param float longitude of the inital point in degree
|
|
* @param float distance to go from the inital point in meter
|
|
* @param float bearing in degree of the direction to go, e.g. 0 = north, 180 = south
|
|
* @param float optional (in meter), defaults to mean radius of the earth
|
|
* @return object {latitude: destLat (in degree), longitude: destLng (in degree)}
|
|
*/
|
|
computeDestinationPoint: function(start, distance, bearing, radius) {
|
|
|
|
var lat = this.latitude(start);
|
|
var lng = this.longitude(start);
|
|
|
|
radius = (typeof radius === 'undefined') ? this.radius : Number(radius);
|
|
|
|
var δ = Number(distance) / radius; // angular distance in radians
|
|
var θ = Number(bearing).toRad();
|
|
|
|
var φ1 = Number(lat).toRad();
|
|
var λ1 = Number(lng).toRad();
|
|
|
|
var φ2 = Math.asin( Math.sin(φ1)*Math.cos(δ) +
|
|
Math.cos(φ1)*Math.sin(δ)*Math.cos(θ) );
|
|
var λ2 = λ1 + Math.atan2(Math.sin(θ)*Math.sin(δ)*Math.cos(φ1),
|
|
Math.cos(δ)-Math.sin(φ1)*Math.sin(φ2));
|
|
λ2 = (λ2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180°
|
|
|
|
return {
|
|
latitude: φ2.toDeg(),
|
|
longitude: λ2.toDeg()
|
|
};
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* 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) {
|
|
|
|
return 0;
|
|
|
|
} else if(typeof distance === 'undefined') {
|
|
|
|
if(this.distance === null) {
|
|
throw new Error('No distance was given');
|
|
} else if(this.distance === 0) {
|
|
return 0;
|
|
} else {
|
|
distance = this.distance;
|
|
}
|
|
|
|
}
|
|
|
|
unit = unit || 'm';
|
|
round = (null == round ? 4 : round);
|
|
|
|
if(typeof this.measures[unit] !== 'undefined') {
|
|
return this.round(distance * this.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(s) to be checked/converted (array of latlng objects, latlng object, sexagesimal string, float)
|
|
* @return float Input data in decimal format
|
|
*/
|
|
useDecimal: function(value) {
|
|
|
|
if(Object.prototype.toString.call(value) === '[object Array]') {
|
|
|
|
var geolib = this;
|
|
|
|
value = value.map(function(val) {
|
|
|
|
//if(!isNaN(parseFloat(val))) {
|
|
if(geolib.isDecimal(val)) {
|
|
|
|
return geolib.useDecimal(val);
|
|
|
|
} else if(typeof val == 'object') {
|
|
|
|
if(geolib.validate(val)) {
|
|
|
|
return geolib.coords(val);
|
|
|
|
} else {
|
|
|
|
for(var prop in val) {
|
|
val[prop] = geolib.useDecimal(val[prop]);
|
|
}
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
} else if(geolib.isSexagesimal(val)) {
|
|
|
|
return geolib.sexagesimal2decimal(val);
|
|
|
|
} else {
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return value;
|
|
|
|
} else if(typeof value === 'object' && this.validate(value)) {
|
|
|
|
return this.coords(value);
|
|
|
|
} else if(typeof value === 'object') {
|
|
|
|
for(var prop in value) {
|
|
value[prop] = this.useDecimal(value[prop]);
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
if (this.isDecimal(value)) {
|
|
|
|
return parseFloat(value);
|
|
|
|
} else if(this.isSexagesimal(value) === true) {
|
|
|
|
return parseFloat(this.sexagesimal2decimal(value));
|
|
|
|
}
|
|
|
|
throw new Error('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 this.sexagesimal) {
|
|
return this.sexagesimal[dec];
|
|
}
|
|
|
|
var tmp = dec.toString().split('.');
|
|
|
|
var deg = Math.abs(tmp[0]);
|
|
var min = ('0.' + (tmp[1] || 0))*60;
|
|
var sec = min.toString().split('.');
|
|
|
|
min = Math.floor(min);
|
|
sec = (('0.' + (sec[1] || 0)) * 60).toFixed(2);
|
|
|
|
this.sexagesimal[dec] = (deg + '° ' + min + "' " + sec + '"');
|
|
|
|
return this.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 this.decimal) {
|
|
return this.decimal[sexagesimal];
|
|
}
|
|
|
|
var regEx = new RegExp(this.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);
|
|
//var dec = ((parseFloat(data[1]) + min + sec));
|
|
|
|
// South and West are negative decimals
|
|
dec = (data[7] == 'S' || data[7] == 'W') ? parseFloat(-dec) : parseFloat(dec);
|
|
//dec = (data[7] == 'S' || data[7] == 'W') ? -dec : dec;
|
|
|
|
this.decimal[sexagesimal] = dec;
|
|
|
|
return dec;
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Checks if a value is in decimal format
|
|
*
|
|
* @param string Value to be checked
|
|
* @return bool True if in sexagesimal format
|
|
*/
|
|
isDecimal: function(value) {
|
|
|
|
value = value.toString().replace(/\s*/, '');
|
|
|
|
// looks silly but works as expected
|
|
// checks if value is in decimal format
|
|
return (!isNaN(parseFloat(value)) && parseFloat(value) == value);
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Checks if a value is in sexagesimal format
|
|
*
|
|
* @param string Value to be checked
|
|
* @return bool True if in sexagesimal format
|
|
*/
|
|
isSexagesimal: function(value) {
|
|
|
|
value = value.toString().replace(/\s*/, '');
|
|
|
|
return this.sexagesimalPattern.test(value);
|
|
|
|
},
|
|
|
|
round: function(value, n) {
|
|
var decPlace = Math.pow(10, n);
|
|
return Math.round(value * decPlace)/decPlace;
|
|
}
|
|
|
|
});
|
|
|
|
// Node module
|
|
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
|
|
|
module.exports = geolib;
|
|
|
|
// react native
|
|
if (typeof global === 'object') {
|
|
global.geolib = geolib;
|
|
}
|
|
|
|
// AMD module
|
|
} else if (typeof define === "function" && define.amd) {
|
|
|
|
define("geolib", [], function () {
|
|
return geolib;
|
|
});
|
|
|
|
// we're in a browser
|
|
} else {
|
|
|
|
global.geolib = geolib;
|
|
|
|
}
|
|
|
|
}(this));
|