hilbert-geohash
Version:
Node.js implementation of Hilbert-curve based geohashing.
182 lines (181 loc) • 6.05 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.rectangle = exports.hilbert_curve = exports.neighbours = exports.decode_exactly = exports.decode = exports.encode = void 0;
const { encode_int, decode_int } = require("./conversion");
const assert = require("assert");
const xy2hash = (x, y, dim) => {
let d = 0n;
let lvl = BigInt(dim >> 1);
while (lvl > 0) {
let rx = (x & lvl) > 0 ? 1n : 0n;
let ry = (y & lvl) > 0 ? 1n : 0n;
d += lvl * lvl * ((3n * rx) ^ ry);
const r = rotate(lvl, x, y, rx, ry);
x = r.x;
y = r.y;
lvl >>= 1n;
}
return d;
};
const hash2xy = (hashcode, dim) => {
let x = 0n;
let y = 0n;
let lvl = 1n;
let big = BigInt(hashcode);
while (lvl < dim) {
let rx = 1n & (big >> 1n);
let ry = 1n & (big ^ rx);
const r = rotate(lvl, x, y, rx, ry);
x = r.x;
y = r.y;
x += lvl * rx;
y += lvl * ry;
big >>= 2n;
lvl <<= 1n;
}
return { x: Number(x), y: Number(y) };
};
const rotate = (n, x, y, rx, ry) => {
if (ry === 0n) {
if (rx === 1n) {
x = n - 1n - x;
y = n - 1n - y;
}
return { x: y, y: x };
}
return { x, y };
};
const LAT_INTERVAL = [-90.0, 90.0];
const LNG_INTERVAL = [-180.0, 180.0];
const lvl_error = (level) => {
const err = 1 / (1 << level);
return { lng: 180 * err, lat: 90 * err };
};
const decode_exactly = (code, bits_per_char = 6) => {
isCorrectBpc(bits_per_char);
let bits = code.length * bits_per_char;
isOverflowing(bits);
let level = BigInt(bits) >> 1n;
let dim = 1n << level;
let code_int = decode_int(code, bits_per_char);
const { x, y } = hash2xy(code_int, dim);
const { lng, lat } = int2coord(x, y, Number(dim));
const err = lvl_error(Number(level));
return {
lng: lng + err.lng,
lat: lat + err.lat,
lng_err: err.lng,
lat_err: err.lat,
};
};
exports.decode_exactly = decode_exactly;
const decode = (code, bits_per_char = 6) => {
isCorrectBpc(bits_per_char);
const { lng, lat } = decode_exactly(code, bits_per_char);
return { lng, lat };
};
exports.decode = decode;
const int2coord = (x, y, dim) => {
const lng = (x / dim) * 360 - 180;
const lat = (y / dim) * 180 - 90;
return { lng, lat };
};
const coord2int = (lng, lat, dim) => {
const lat_y = ((lat + LAT_INTERVAL[1]) / 180.0) * dim;
const lng_x = ((lng + LNG_INTERVAL[1]) / 360.0) * dim;
return {
x: Math.min(dim - 1, Math.floor(lng_x)),
y: Math.min(dim - 1, Math.floor(lat_y)),
};
};
const neighbours = (code, bits_per_char = 6) => {
isCorrectBpc(bits_per_char);
const { lng, lat, lng_err, lat_err } = decode_exactly(code, bits_per_char);
const precision = code.length;
let north = lat + 2 * lat_err;
let south = lat - 2 * lat_err;
let east = lng + 2 * lng_err;
if (east > 180)
east -= 360;
let west = lng - 2 * lng_err;
if (west < -180)
west += 360;
const neighbours = {
east: encode(east, lat, precision, bits_per_char),
west: encode(west, lat, precision, bits_per_char),
};
if (north <= 90) {
neighbours["north"] = encode(lng, north, precision, bits_per_char);
neighbours["north-east"] = encode(east, north, precision, bits_per_char);
neighbours["north-west"] = encode(west, north, precision, bits_per_char);
}
if (south >= -90) {
neighbours["south"] = encode(lng, south, precision, bits_per_char);
neighbours["south-east"] = encode(east, south, precision, bits_per_char);
neighbours["south-west"] = encode(west, south, precision, bits_per_char);
}
return neighbours;
};
exports.neighbours = neighbours;
const rectangle = (code, bits_per_char = 6) => {
isCorrectBpc(bits_per_char);
const { lng, lat, lng_err, lat_err } = decode_exactly(code, bits_per_char);
return {
type: "Feature",
properties: {
code,
lng,
lat,
lng_err,
lat_err,
bits_per_char,
},
bbox: [lng - lng_err, lat - lat_err, lng + lng_err, lat + lat_err],
geometry: {
type: "Polygon",
coordinates: [
[
[lng - lng_err, lat - lat_err],
[lng + lng_err, lat - lat_err],
[lng + lng_err, lat + lat_err],
[lng - lng_err, lat + lat_err],
[lng - lng_err, lat - lat_err],
],
],
},
};
};
exports.rectangle = rectangle;
const hilbert_curve = (precision, bits_per_char = 6) => {
isCorrectBpc(bits_per_char);
assert(precision < 4, "Only precision less than 4 supported right now");
const bits = precision * bits_per_char;
const coordinates = [];
for (let i = 0; i < 1 << bits; i++) {
const code = encode_int(i, bits_per_char).padStart(precision, "0");
const { lng, lat } = decode_exactly(code, bits_per_char);
coordinates.push([lng, lat]);
}
return {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates,
},
};
};
exports.hilbert_curve = hilbert_curve;
const isOverflowing = (bits) => assert(bits < 64, "Over 64 bits not supported. Reduce 'precision' or 'bits_per_char' so their product is <= 64");
const isCorrectBpc = (bpc) => assert([2, 4, 6].includes(bpc), "bits_per_char must be 2, 4 or 6");
const encode = (lng, lat, precision = 10, bits_per_char = 6) => {
isCorrectBpc(bits_per_char);
const bits = precision * bits_per_char;
isOverflowing(bits);
const level = bits >> 1;
const dim = 1 << level;
const { x, y } = coord2int(lng, lat, dim);
const code = xy2hash(BigInt(x), BigInt(y), dim);
return encode_int(code, bits_per_char).padStart(precision, "0");
};
exports.encode = encode;