UNPKG

@thetsf/geofirex

Version:

Realtime Firestore GeoQueries with RxJS

378 lines (377 loc) 11.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.neighbors = exports.neighbor_int = exports.neighbor = exports.decode_int = exports.decode = exports.decode_bbox_int = exports.decode_bbox = exports.encode_int = exports.encode = void 0; exports.distance = distance; exports.bearing = bearing; exports.toGeoJSONFeature = toGeoJSONFeature; exports.flip = flip; exports.setPrecision = setPrecision; const distance_1 = __importDefault(require("@turf/distance")); const bearing_1 = __importDefault(require("@turf/bearing")); function distance(from, to) { return (0, distance_1.default)(toGeoJSONFeature(from), toGeoJSONFeature(to)); } function bearing(from, to) { return (0, bearing_1.default)(toGeoJSONFeature(from), toGeoJSONFeature(to)); } function toGeoJSONFeature(coordinates, props) { coordinates = flip(coordinates); return { type: 'Feature', geometry: { type: 'Point', coordinates }, properties: props }; } function flip(arr) { return [arr[1], arr[0]]; } function setPrecision(km) { switch (true) { case km <= 0.00477: return 9; case km <= 0.0382: return 8; case km <= 0.153: return 7; case km <= 1.22: return 6; case km <= 4.89: return 5; case km <= 39.1: return 4; case km <= 156: return 3; case km <= 1250: return 2; default: return 1; } // 1 ≤ 5,000km × 5,000km // 2 ≤ 1,250km × 625km // 3 ≤ 156km × 156km // 4 ≤ 39.1km × 19.5km // 5 ≤ 4.89km × 4.89km // 6 ≤ 1.22km × 0.61km // 7 ≤ 153m × 153m // 8 ≤ 38.2m × 19.1m // 9 ≤ 4.77m × 4.77m } /////// NGEOHASH //////// var BASE32_CODES = '0123456789bcdefghjkmnpqrstuvwxyz'; var BASE32_CODES_DICT = {}; for (var i = 0; i < BASE32_CODES.length; i++) { BASE32_CODES_DICT[BASE32_CODES.charAt(i)] = i; } var ENCODE_AUTO = 'auto'; /** * Significant Figure Hash Length * * This is a quick and dirty lookup to figure out how long our hash * should be in order to guarantee a certain amount of trailing * significant figures. This was calculated by determining the error: * 45/2^(n-1) where n is the number of bits for a latitude or * longitude. Key is # of desired sig figs, value is minimum length of * the geohash. * @type Array */ // Desired sig figs: 0 1 2 3 4 5 6 7 8 9 10 var SIGFIG_HASH_LENGTH = [0, 5, 7, 8, 11, 12, 13, 15, 16, 17, 18]; /** * Encode * * Create a Geohash out of a latitude and longitude that is * `numberOfChars` long. * * @param {Number|String} latitude * @param {Number|String} longitude * @param {Number} numberOfChars * @returns {String} */ const encode = function (latitude, longitude, numberOfChars) { if (numberOfChars === ENCODE_AUTO) { if (typeof latitude === 'number' || typeof longitude === 'number') { throw new Error('string notation required for auto precision.'); } var decSigFigsLat = latitude.split('.')[1].length; var decSigFigsLong = longitude.split('.')[1].length; var numberOfSigFigs = Math.max(decSigFigsLat, decSigFigsLong); numberOfChars = SIGFIG_HASH_LENGTH[numberOfSigFigs]; } else if (numberOfChars === undefined) { numberOfChars = 9; } let chars = [], bits = 0, bitsTotal = 0, hash_value = 0, maxLat = 90, minLat = -90, maxLon = 180, minLon = -180, mid; while (chars.length < numberOfChars) { if (bitsTotal % 2 === 0) { mid = (maxLon + minLon) / 2; if (longitude > mid) { hash_value = (hash_value << 1) + 1; minLon = mid; } else { hash_value = (hash_value << 1) + 0; maxLon = mid; } } else { mid = (maxLat + minLat) / 2; if (latitude > mid) { hash_value = (hash_value << 1) + 1; minLat = mid; } else { hash_value = (hash_value << 1) + 0; maxLat = mid; } } bits++; bitsTotal++; if (bits === 5) { const code = BASE32_CODES[hash_value]; chars.push(code); bits = 0; hash_value = 0; } } return chars.join(''); }; exports.encode = encode; /** * Encode Integer * * Create a Geohash out of a latitude and longitude that is of 'bitDepth'. * * @param {Number} latitude * @param {Number} longitude * @param {Number} bitDepth * @returns {Number} */ const encode_int = function (latitude, longitude, bitDepth) { bitDepth = bitDepth || 52; var bitsTotal = 0, maxLat = 90, minLat = -90, maxLon = 180, minLon = -180, mid, combinedBits = 0; while (bitsTotal < bitDepth) { combinedBits *= 2; if (bitsTotal % 2 === 0) { mid = (maxLon + minLon) / 2; if (longitude > mid) { combinedBits += 1; minLon = mid; } else { maxLon = mid; } } else { mid = (maxLat + minLat) / 2; if (latitude > mid) { combinedBits += 1; minLat = mid; } else { maxLat = mid; } } bitsTotal++; } return combinedBits; }; exports.encode_int = encode_int; /** * Decode Bounding Box * * Decode hashString into a bound box matches it. Data returned in a four-element array: [minlat, minlon, maxlat, * maxlon] * @param {String} hash_string * @returns {Array} */ const decode_bbox = function (hash_string) { var isLon = true, maxLat = 90, minLat = -90, maxLon = 180, minLon = -180, mid; var hashValue = 0; for (var i = 0, l = hash_string.length; i < l; i++) { var code = hash_string[i].toLowerCase(); hashValue = BASE32_CODES_DICT[code]; for (var bits = 4; bits >= 0; bits--) { var bit = (hashValue >> bits) & 1; if (isLon) { mid = (maxLon + minLon) / 2; if (bit === 1) { minLon = mid; } else { maxLon = mid; } } else { mid = (maxLat + minLat) / 2; if (bit === 1) { minLat = mid; } else { maxLat = mid; } } isLon = !isLon; } } return [minLat, minLon, maxLat, maxLon]; }; exports.decode_bbox = decode_bbox; /** * Decode Bounding Box Integer * * Decode hash number into a bound box matches it. Data returned in a four-element array: [minlat, minlon, maxlat, * maxlon] * @param {Number} hashInt * @param {Number} bitDepth * @returns {Array} */ const decode_bbox_int = function (hashInt, bitDepth) { bitDepth = bitDepth || 52; var maxLat = 90, minLat = -90, maxLon = 180, minLon = -180; var latBit = 0, lonBit = 0; var step = bitDepth / 2; for (var i = 0; i < step; i++) { lonBit = get_bit(hashInt, (step - i) * 2 - 1); latBit = get_bit(hashInt, (step - i) * 2 - 2); if (latBit === 0) { maxLat = (maxLat + minLat) / 2; } else { minLat = (maxLat + minLat) / 2; } if (lonBit === 0) { maxLon = (maxLon + minLon) / 2; } else { minLon = (maxLon + minLon) / 2; } } return [minLat, minLon, maxLat, maxLon]; }; exports.decode_bbox_int = decode_bbox_int; function get_bit(bits, position) { return (bits / Math.pow(2, position)) & 0x01; } /** * Decode * * Decode a hash string into pair of latitude and longitude. A javascript object is returned with keys `latitude`, * `longitude` and `error`. * @param {String} hashString * @returns {Object} */ const decode = function (hashString) { var bbox = (0, exports.decode_bbox)(hashString); var lat = (bbox[0] + bbox[2]) / 2; var lon = (bbox[1] + bbox[3]) / 2; var latErr = bbox[2] - lat; var lonErr = bbox[3] - lon; return { latitude: lat, longitude: lon, error: { latitude: latErr, longitude: lonErr } }; }; exports.decode = decode; /** * Decode Integer * * Decode a hash number into pair of latitude and longitude. A javascript object is returned with keys `latitude`, * `longitude` and `error`. * @param {Number} hash_int * @param {Number} bitDepth * @returns {Object} */ const decode_int = function (hash_int, bitDepth) { var bbox = (0, exports.decode_bbox_int)(hash_int, bitDepth); var lat = (bbox[0] + bbox[2]) / 2; var lon = (bbox[1] + bbox[3]) / 2; var latErr = bbox[2] - lat; var lonErr = bbox[3] - lon; return { latitude: lat, longitude: lon, error: { latitude: latErr, longitude: lonErr } }; }; exports.decode_int = decode_int; /** * Neighbor * * Find neighbor of a geohash string in certain direction. Direction is a two-element array, i.e. [1,0] means north, * [-1,-1] means southwest. direction [lat, lon], i.e. * [1,0] - north * [1,1] - northeast * ... * @param {String} hashString * @param {Array} Direction as a 2D normalized vector. * @returns {String} */ const neighbor = function (hashString, direction) { var lonLat = (0, exports.decode)(hashString); var neighborLat = lonLat.latitude + direction[0] * lonLat.error.latitude * 2; var neighborLon = lonLat.longitude + direction[1] * lonLat.error.longitude * 2; return (0, exports.encode)(neighborLat, neighborLon, hashString.length); }; exports.neighbor = neighbor; /** * Neighbor Integer * * Find neighbor of a geohash integer in certain direction. Direction is a two-element array, i.e. [1,0] means north, * [-1,-1] means southwest. direction [lat, lon], i.e. * [1,0] - north * [1,1] - northeast * ... * @param {String} hash_string * @returns {Array} */ const neighbor_int = function (hash_int, direction, bitDepth) { bitDepth = bitDepth || 52; var lonlat = (0, exports.decode_int)(hash_int, bitDepth); var neighbor_lat = lonlat.latitude + direction[0] * lonlat.error.latitude * 2; var neighbor_lon = lonlat.longitude + direction[1] * lonlat.error.longitude * 2; return (0, exports.encode_int)(neighbor_lat, neighbor_lon, bitDepth); }; exports.neighbor_int = neighbor_int; /** * Neighbors * * Returns all neighbors' hashstrings clockwise from north around to northwest * 7 0 1 * 6 x 2 * 5 4 3 * @param {String} hash_string * @returns {encoded neighborHashList|Array} */ const neighbors = function (hash_string) { var hashstringLength = hash_string.length; var lonlat = (0, exports.decode)(hash_string); var lat = lonlat.latitude; var lon = lonlat.longitude; var latErr = lonlat.error.latitude * 2; var lonErr = lonlat.error.longitude * 2; var neighbor_lat, neighbor_lon; var neighborHashList = [ encodeNeighbor(1, 0), encodeNeighbor(1, 1), encodeNeighbor(0, 1), encodeNeighbor(-1, 1), encodeNeighbor(-1, 0), encodeNeighbor(-1, -1), encodeNeighbor(0, -1), encodeNeighbor(1, -1) ]; function encodeNeighbor(neighborLatDir, neighborLonDir) { neighbor_lat = lat + neighborLatDir * latErr; neighbor_lon = lon + neighborLonDir * lonErr; return (0, exports.encode)(neighbor_lat, neighbor_lon, hashstringLength); } return neighborHashList; }; exports.neighbors = neighbors;