UNPKG

geokit

Version:

An assortment of geolocation related tools, all packaged in one easy to use kit.

151 lines (144 loc) 4.79 kB
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 };