UNPKG

@zenlocator/geo

Version:
811 lines (605 loc) 17.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.Geo = factory()); }(this, (function () { 'use strict'; var convertDistance = function (distance, fromUnits, toUnits) { var meters = 0; /* basic formatting & error checking */ distance = parseInt(distance, 10); fromUnits = fromUnits.toUpperCase(); toUnits = toUnits.toUpperCase(); /* convert distance to meters first */ switch (fromUnits) { case this.distanceUnits.KILOMETERS: case this.distanceUnits.KM: meters = distance * 1000; break; case this.distanceUnits.METERS: meters = distance; break; case this.distanceUnits.LEAGUES: meters = distance * 5556; break; case this.distanceUnits.MILES: meters = distance * 1609.344; break; case this.distanceUnits.YARDS: meters = distance / 1.0936; break; case this.distanceUnits.FEET: meters = distance * 0.3048; break; case this.distanceUnits.INCHES: meters = distance * 0.0254; break; case this.distanceUnits.NAUTICAL_MILES: meters = distance * 1852; break; case this.distanceUnits.SHEPPEYS: meters = distance / 0.00071428571; break; } /* then return result in `toUnits` */ return this.formatDistance(meters, toUnits); }; var detectCoordFormat = function (coord) { /* really rough check for the number of floats present in a string */ if (typeof coord === 'number') { return this.coordFormats.D; } else if (typeof coord === 'string') { var floats = coord.match(/[+-]?\d+(\.\d+)?/g); if (floats) { if (floats.length === 3) { return this.coordFormats.DMS; } else if (floats.length === 2) { return this.coordFormats.DM; } else if (floats.length === 1) { return coord.indexOf('°') === -1 ? this.coordFormats.D : this.coordFormats.DD; } } } }; var formatBounds = function (bounds) { var ret = { sw: 0, nw: 0, ne: 0, se: 0 }; var offsetCenter = { lat: -90, lng: 1 }; var sorted = []; /* `bounds` has to be either an array, or an object */ if (this.isArray(bounds) && bounds.length >= 2) { sorted = this.orderByDistance(offsetCenter, bounds); } else if (this.isPlainObject(bounds) && Object.keys(bounds).length >= 2) { sorted = this.orderByDistance(offsetCenter, this.values(bounds)); } else { return; } /* assign two coordinates for bounds (first and last) */ if (sorted.length) { ret.sw = this.formatCoords({ lat: sorted[0].lat, lng: sorted[0].lng }); var lastIndex = sorted.length - 1; ret.ne = this.formatCoords({ lat: sorted[lastIndex].lat, lng: sorted[lastIndex].lng }); } else { return; } /* swap them if they're backwards */ if (this.parseCoord(ret.ne.lat) < this.parseCoord(ret.sw.lat) || this.parseCoord(ret.ne.lng) < this.parseCoord(ret.sw.lng)) { var _ref = [ret.ne, ret.sw]; ret.sw = _ref[0]; ret.ne = _ref[1]; } /* generate other missing coords */ ret.nw = this.formatCoords({ lat: ret.ne.lat, lng: ret.sw.lng }); ret.se = this.formatCoords({ lat: ret.sw.lat, lng: ret.ne.lng }); return ret; }; var formatCoord = function (coord) { var isLat = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; if (typeof coord === 'undefined') { return coord; } var ret = []; var iterations = void 0; var remainder = void 0; var direction = void 0; coord = this.parseCoord(coord, false); if (this.settings.coordsFormat === this.coordFormats.D) { return parseFloat(coord.toFixed(this.settings.coordsPrecision)); } /* split coord into digits (degrees, mins, secs) */ if (this.settings.coordsFormat === this.coordFormats.DM) { iterations = 1; } else if (this.settings.coordsFormat === this.coordFormats.DMS) { iterations = 2; } if (iterations) { for (var i = 0; i !== iterations; i++) { ret.push(parseInt(coord, 10)); remainder = coord % 1; coord = remainder * 60; } } ret.push(parseFloat(coord.toFixed(this.settings.coordsPrecision))); /* adjust direction */ if (isLat) { direction = ret[0] > 0 ? 'N' : 'S'; } else { direction = ret[0] > 0 ? 'E' : 'W'; } ret = ret.map(function (d) { return Math.abs(d); }); /* format each digit in `ret` */ if (ret.length === 3) { ret = ret[0] + '\xB0 ' + ret[1] + '\' ' + ret[2] + '" ' + direction; } else if (ret.length === 2) { ret = ret[0] + '\xB0 ' + ret[1] + '\' ' + direction; } else if (ret.length === 1) { ret = ret[0] + '\xB0 ' + direction; } else { ret = undefined; } return ret; }; var formatCoords = function (coords) { coords = { lat: this.parseCoord(coords.lat), lng: this.parseCoord(coords.lng) }; /* treat [0,-180] and [0,180] as [0,0] */ if (coords.lat === 0 && coords.lng === -180 || coords.lat === 0 && coords.lng === 180) { coords.lng = 0; } return { lat: this.formatCoord(coords.lat, true), lng: this.formatCoord(coords.lng, false) }; }; var formatDistance = function (meters) { var distanceUnits = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.settings.distanceUnits; var ret = parseFloat(meters); var distancePrecision = void 0; /* convert to proper units first */ switch (distanceUnits) { case this.distanceUnits.KILOMETERS: case this.distanceUnits.KM: ret *= 0.001; distancePrecision = ret < 1 ? 2 : 1; break; /* case this.distanceUnits.METERS: ret *= 1; distancePrecision = 0; break; */ case this.distanceUnits.LEAGUES: ret *= 0.000179986; distancePrecision = ret < 1 ? 1 : 1; break; case this.distanceUnits.MILES: ret *= 0.000621371; distancePrecision = ret < 1 ? 2 : 1; break; case this.distanceUnits.YARDS: ret *= 1.09361; distancePrecision = 0; break; case this.distanceUnits.FEET: ret *= 3.28084; distancePrecision = 0; break; case this.distanceUnits.INCHES: ret *= 39.3701; distancePrecision = 0; break; case this.distanceUnits.NAUTICAL_MILES: ret *= 0.000539957; distancePrecision = ret < 1 ? 2 : 1; break; case this.distanceUnits.SHEPPEYS: ret *= 0.00071428571; distancePrecision = 2; break; } /* set the right precision */ if (this.settings.distancePrecision !== this.FLEXIBLE_DISTANCE_PRECISION) { distancePrecision = this.settings.distancePrecision; } return parseFloat(ret.toFixed(distancePrecision)); }; var getBounds = function (center, meters) { var lat = this.parseCoord(center.lat, true); var lng = this.parseCoord(center.lng, false); center = { lat: lat, lng: lng }; var sw = this.getDestinationPoint(center, meters, 225); var ne = this.getDestinationPoint(center, meters, 45); return this.formatBounds({ sw: sw, ne: ne }); }; var getCenter = function () { var coordsArray = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; /* if no maths required */ if (!coordsArray.length) { return; } if (coordsArray.length === 1) { return this.formatCoords({ lat: coordsArray[0].lat, lng: coordsArray[0].lng }); } /* otherwise, find a centroid */ var x = 0.0; var y = 0.0; var z = 0.0; var lat = void 0; var lng = void 0; for (var i = 0; i !== coordsArray.length; i++) { var coords = coordsArray[i]; lat = this.toRad(this.parseCoord(coords.lat)); lng = this.toRad(this.parseCoord(coords.lng)); x += Math.cos(lat) * Math.cos(lng); y += Math.cos(lat) * Math.sin(lng); z += Math.sin(lat); } var total = coordsArray.length; x /= total; y /= total; z /= total; lng = Math.atan2(y, x); var hyp = Math.sqrt(x * x + y * y); lat = Math.atan2(z, hyp); return this.formatCoords({ lat: lat * 180 / Math.PI, lng: lng * 180 / Math.PI }); }; var getClosest = function (center) { var coords = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var ret = void 0; var orderedCoords = this.orderByDistance(center, coords); if (orderedCoords.length) { ret = orderedCoords[0]; } return ret; }; /* source: `http://www.movable-type.co.uk/scripts/latlong.html` */ var getDestinationPoint = function (coords, distance, bearing) { var radius = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : this.RADIUS_EQUILATERAL; var lat = this.parseCoord(coords.lat, true); var lng = this.parseCoord(coords.lng, false); var δ = Number(distance) / radius; var θ = this.toRad(Number(bearing)); var φ1 = this.toRad(Number(lat)); var λ1 = this.toRad(Number(lng)); 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; return this.formatCoords({ lat: this.toDeg(φ2), lng: this.toDeg(λ2) }); }; var getDistance = function (coords1, coords2) { /* haversine formula */ var degreesToRadians = function degreesToRadians(deg) { return deg * (Math.PI / 180); }; var lat1 = this.parseCoord(coords1.lat, true); var lng1 = this.parseCoord(coords1.lng, false); var lat2 = this.parseCoord(coords2.lat, true); var lng2 = this.parseCoord(coords2.lng, false); var earthRadiusMeters = 6371000; var dLat = degreesToRadians(lat2 - lat1); var dLng = degreesToRadians(lng2 - lng1); var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(degreesToRadians(lat1)) * Math.cos(degreesToRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); return this.formatDistance(earthRadiusMeters * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))); }; var isPointInBounds = function (coords, bounds) { coords = { lat: this.parseCoord(coords.lat), lng: this.parseCoord(coords.lng) }; bounds = this.parseBounds(bounds); return coords.lat >= bounds.sw.lat && coords.lat <= bounds.ne.lat && coords.lng >= bounds.nw.lng && coords.lng <= bounds.se.lng; }; var isPointInCircle = function (coords, center, meters) { var originaldistanceUnits = this.settings.distanceUnits; this.settings.distanceUnits = this.distanceUnits.METER; var ret = this.getDistance(coords, center) <= meters; this.settings.distanceUnits = originaldistanceUnits; return ret; }; var isValidLat = function (lat) { lat = this.parseCoord(lat); return lat >= -90 && lat <= 90; }; var isValidLng = function (lng) { lng = this.parseCoord(lng); return lng >= -180 && lng <= 180; }; var orderByDistance = function (center, coords) { var _this = this; if (!this.isPlainObject(coords) && !this.isArray(coords)) { return; } var ret = this.isArray(coords) ? [] : {}; var distances = []; var orderedDistances = []; var getDistance = function getDistance(c, k) { var item = {}; if (k) { item.key = k; } item.value = c; item.distance = _this.getDistance(center, c); distances.push(item); orderedDistances.push(item.distance); }; /* build `distances` */ if (this.isArray(coords)) { var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = coords[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var c = _step.value; getDistance(c); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } else { for (var k in coords) { getDistance(coords[k], k); } } /* sort */ orderedDistances.sort(function (a, b) { return a - b; }); /* re-assemble */ var _loop = function _loop(i) { var chunk = distances.filter(function (c) { if (c.distance === orderedDistances[i]) { return c; } }); for (var j in chunk) { var _c = chunk[j]; var v = _this.formatCoords(_c.value); if (_c.hasOwnProperty('key')) { ret[_c.key] = v; } else { ret.push(v); } } }; for (var i in orderedDistances) { _loop(i); } return ret; }; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var parseBounds = function (bounds) { bounds = _extends({}, this.formatBounds(bounds)); if (bounds) { bounds = { sw: { lat: this.parseCoord(bounds.sw.lat), lng: this.parseCoord(bounds.sw.lng) }, nw: { lat: this.parseCoord(bounds.nw.lat), lng: this.parseCoord(bounds.nw.lng) }, ne: { lat: this.parseCoord(bounds.ne.lat), lng: this.parseCoord(bounds.ne.lng) }, se: { lat: this.parseCoord(bounds.se.lat), lng: this.parseCoord(bounds.se.lng) } }; } return bounds; }; var parseCoord = function (coord) { var usePrecision = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var coordFormat = this.detectCoordFormat(coord); var digits = []; var ret = void 0; if (coordFormat) { if (coordFormat === this.coordFormats.D) { ret = parseFloat(coord); } else { /* extract floats first */ if (typeof coord === 'string') { coord = coord.toLowerCase(); digits = coord.match(/[+-]?\d+(\.\d+)?/g).map(function (d) { return parseFloat(d); }); } /* test against format */ if (digits.length === 1 && coordFormat !== this.coordFormats.DD || digits.length === 2 && coordFormat !== this.coordFormats.DM || digits.length === 3 && coordFormat !== this.coordFormats.DMS || digits.length < 1 || digits.length > 3) { digits = []; } /* convert to internal decimal format */ if (digits.length) { var remainder = 0; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = digits.reverse()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { ret = _step.value; ret += remainder; remainder = ret / 60; } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } /* assign proper sign */ if (ret && typeof coord === 'string' && (coord.indexOf('s') !== -1 || coord.indexOf('w') !== -1)) { ret *= -1; } } } if (ret && usePrecision) { ret = parseFloat(ret.toFixed(this.settings.coordsPrecision)); } return ret; }; var Geo = function Geo() { var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; classCallCheck(this, Geo); /* cleanup settings */ for (var k in settings) { if (settings.hasOwnProperty(k)) { if (typeof settings[k] === 'string') { settings[k] = settings[k].toUpperCase(); } if (['distancePrecision', 'coordsPrecision'].indexOf(k) !== -1) { settings[k] = parseInt(settings[k], 10); } } } /* constants */ this.distanceUnits = { KILOMETERS: 'KILOMETERS', KM: 'KM', METERS: 'METERS', LEAGUES: 'LEAGUES', MILES: 'MILES', YARDS: 'YARDS', FEET: 'FEET', INCHES: 'INCHES', NAUTICAL_MILES: 'NAUTICAL_MILES', SHEPPEYS: 'SHEPPEYS' }; this.FLEXIBLE_DISTANCE_PRECISION = -1; this.coordFormats = { DMS: 'DMS', /* degrees, minutes and seconds: (string)`DDD° MM' SS.S"` */ DM: 'DM', /* degrees and decimal minutes: (string)`DDD° MM.MMM'` */ DD: 'DD', /* decimal degrees: (string)`DDD.DDDDD°` */ D: 'D' /* decimal: (float)`DDD.DDDDD` */ }; /* functions */ this.toRad = function (degrees) { return degrees * Math.PI / 180; }; this.toDeg = function (radians) { return radians * 180 / Math.PI; }; this.isPlainObject = function (o) { return (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === 'object' && o.constructor === Object; }; this.isArray = function (a) { return Object.prototype.toString.call(a) === '[object Array]'; }; this.values = function (o) { return Object.keys(o).map(function (k) { return o[k]; }); }; /* radius(es?) :P */ this.RADIUS_MEAN = 6371000; this.RADIUS_EQUILATERAL = 6378100; this.RADIUS_POLAR = 6356800; /* assign settings */ this.settings = _extends({ distanceUnits: this.distanceUnits.METER, distancePrecision: this.FLEXIBLE_DISTANCE_PRECISION, coordsFormat: this.coordFormats.D, coordsPrecision: 6 }, settings); }; Geo.prototype = _extends({}, Geo.prototype, { convertDistance: convertDistance, detectCoordFormat: detectCoordFormat, formatBounds: formatBounds, formatCoord: formatCoord, formatCoords: formatCoords, formatDistance: formatDistance, getBounds: getBounds, getCenter: getCenter, getClosest: getClosest, getDestinationPoint: getDestinationPoint, getDistance: getDistance, isPointInBounds: isPointInBounds, isPointInCircle: isPointInCircle, isValidLat: isValidLat, isValidLng: isValidLng, orderByDistance: orderByDistance, parseBounds: parseBounds, parseCoord: parseCoord }); return Geo; })));