geokit
Version:
An assortment of geolocation related tools, all packaged in one easy to use kit.
151 lines (144 loc) • 4.79 kB
JavaScript
const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
function base32(value) {
return BASE32.charAt(value);
}
function decimalChunk(value) {
return '0123456789bcdefghjkmnpqrstuvwxyz'.indexOf(value.toLowerCase());
}
function toRad(degrees) {
return (degrees * Math.PI) / 180;
}
function validateHash(geohash, flag = false) {
let error;
if (typeof geohash !== 'string') {
error = 'geohash must be a string';
}
else if (geohash.length === 0) {
error = 'geohash cannot be the empty string';
}
else {
for (const letter of geohash) {
if (BASE32.indexOf(letter) === -1) {
error = "geohash cannot contain '" + letter + "'";
}
}
}
if (typeof error !== 'undefined' && !flag) {
throw new Error("Invalid geohash '" + geohash + "': " + error);
}
else {
return !error;
}
}
function decodeHash(hash) {
validateHash(hash);
let even = true;
const latRng = [-90, 90];
const lngRng = [-180, 180];
const hashChars = hash.split('');
while (hashChars.length) {
const chunk = decimalChunk(hashChars.shift());
for (let i = 0; i < 5; i++) {
const mask = [16, 8, 4, 2, 1][i];
const range = even ? lngRng : latRng;
const middle = (range[0] + range[1]) / 2;
range[chunk & mask ? 0 : 1] = middle;
even = !even;
}
}
return { lat: (latRng[0] + latRng[1]) / 2, lng: (lngRng[0] + lngRng[1]) / 2 };
}
function validateCoordinates(coordinates, flag = false) {
const error = [];
if (!coordinates) {
error.push('Coordinates must exist.');
}
else if (typeof coordinates.lat === 'undefined') {
error.push('Latitude must exist on coordinates');
}
else if (typeof coordinates.lng === 'undefined') {
error.push('Longitude must exist on coordinates');
}
else {
const latitude = coordinates.lat;
const longitude = coordinates.lng;
if (typeof latitude !== 'number' || isNaN(latitude)) {
error.push('Latitude must be a number.');
}
else if (latitude < -90 || latitude > 90) {
error.push('Latitude must be within the range [-90, 90].');
}
else if (typeof longitude !== 'number' || isNaN(longitude)) {
error.push('Longitude must be a number');
}
else if (longitude < -180 || longitude > 180) {
error.push('Longitude must be within the range [-180, 180]');
}
}
if (error.length > 0 && !flag) {
throw new Error('Invalid coordinates: ' + error.join(' '));
}
else {
return !error;
}
}
function distance(start, end, unit = 'km') {
validateCoordinates(start);
validateCoordinates(end);
const radius = unit.toLowerCase() === 'miles' ? 3963 : 6371;
const dLat = toRad(end.lat - start.lat);
const dLon = toRad(end.lng - start.lng);
const lat1 = toRad(start.lat);
const lat2 = toRad(end.lat);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return radius * c;
}
function hash(coordinates, precision = 10) {
validateCoordinates(coordinates);
if (typeof precision === 'number' && !isNaN(precision)) {
if (precision <= 0) {
throw new Error('Precision must be greater than 0');
}
else if (precision > 22) {
throw new Error('Precision cannot be greater than 22');
}
else if (Math.round(precision) !== precision) {
throw new Error('Precision must be an integer');
}
}
else {
throw new Error('Precision must be a number');
}
const latRng = [-90, 90];
const lngRng = [-180, 180];
let hash = '';
let hashVal = 0;
let bits = 0;
let even = 1;
while (hash.length < precision) {
const val = even ? coordinates.lng : coordinates.lat;
const range = even ? lngRng : latRng;
const mid = (range[0] + range[1]) / 2;
if (val > mid) {
hashVal = (hashVal << 1) + 1;
range[0] = mid;
}
else {
hashVal = (hashVal << 1) + 0;
range[1] = mid;
}
even = !even;
if (bits < 4) {
bits++;
}
else {
bits = 0;
hash += base32(hashVal);
hashVal = 0;
}
}
return hash;
}
export { decodeHash, distance, hash, validateCoordinates, validateHash };