h3-js
Version:
Pure-Javascript version of the H3 library, a hexagon-based geographic grid system
1,430 lines (1,345 loc) • 55.9 kB
JavaScript
/*
* Copyright 2018-2019, 2022 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @module h3
*/
import C from '../out/libh3';
import BINDINGS from './bindings';
import {
throwIfError,
H3LibraryError,
JSBindingError,
E_RES_DOMAIN,
E_UNKNOWN_UNIT,
E_ARRAY_LENGTH
} from './errors';
const H3 = {};
// Create the bound functions themselves
BINDINGS.forEach(function bind(def) {
H3[def[0]] = C.cwrap(...def);
});
// Alias the hexidecimal base for legibility
const BASE_16 = 16;
// Alias unused bits for legibility
const UNUSED_UPPER_32_BITS = 0;
// ----------------------------------------------------------------------------
// Byte size imports
const SZ_INT = 4;
const SZ_PTR = 4;
const SZ_DBL = 8;
const SZ_INT64 = 8;
const SZ_H3INDEX = H3.sizeOfH3Index();
const SZ_LATLNG = H3.sizeOfLatLng();
const SZ_CELLBOUNDARY = H3.sizeOfCellBoundary();
const SZ_GEOPOLYGON = H3.sizeOfGeoPolygon();
const SZ_GEOLOOP = H3.sizeOfGeoLoop();
const SZ_LINKED_GEOPOLYGON = H3.sizeOfLinkedGeoPolygon();
const SZ_COORDIJ = H3.sizeOfCoordIJ();
// ----------------------------------------------------------------------------
// Custom types
/**
* 64-bit hexidecimal string representation of an H3 index
* @static
* @typedef {string} H3Index
*/
/**
* 64-bit hexidecimal string representation of an H3 index,
* or two 32-bit integers in little endian order in an array.
* @static
* @typedef {string | number[]} H3IndexInput
*/
/**
* Coordinates as an `{i, j}` pair
* @static
* @typedef CoordIJ
* @type {Object}
* @property {number} i
* @property {number} j
*/
/**
* Custom JS Error with an attached error code. Error codes come from the
* core H3 library and can be found [in the H3 docs](https://h3geo.org/docs/next/library/errors#table-of-error-codes).
* @static
* @typedef H3Error
* @extends Error
* @property {string} message
* @property {number} code
*/
// ----------------------------------------------------------------------------
// Unit constants
/**
* Length/Area units
* @static
* @type {Object}
* @property {string} m
* @property {string} m2
* @property {string} km
* @property {string} km2
* @property {string} rads
* @property {string} rads2
*/
export const UNITS = {
m: 'm',
m2: 'm2',
km: 'km',
km2: 'km2',
rads: 'rads',
rads2: 'rads2'
};
// ----------------------------------------------------------------------------
// Utilities and helpers
/**
* Validate a resolution, throwing an error if invalid
* @private
* @param {unknown} res Value to validate
* @throws {H3Error} If invalid
*/
function validateRes(res) {
if (typeof res !== 'number' || res < 0 || res > 15 || Math.floor(res) !== res) {
throw H3LibraryError(E_RES_DOMAIN, res);
}
}
const MAX_JS_ARRAY_LENGTH = Math.pow(2, 32) - 1;
/**
* Validate an array length. JS will throw its own error if you try
* to create an array larger than 2^32 - 1, but validating beforehand
* allows us to exit early before we try to process large amounts
* of data that won't even fit in an output array
* @private
* @param {number} length Length to validate
* @throws {H3Error} If invalid
*/
function validateArrayLength(length) {
if (length > MAX_JS_ARRAY_LENGTH) {
throw JSBindingError(E_ARRAY_LENGTH, length);
}
}
const INVALID_HEXIDECIMAL_CHAR = /[^0-9a-fA-F]/;
/**
* Convert an H3 index (64-bit hexidecimal string) into a "split long" - a pair of 32-bit ints
* @param {H3IndexInput} h3Index H3 index to check
* @return {number[]} A two-element array with 32 lower bits and 32 upper bits
*/
export function h3IndexToSplitLong(h3Index) {
if (
Array.isArray(h3Index) &&
h3Index.length === 2 &&
Number.isInteger(h3Index[0]) &&
Number.isInteger(h3Index[1])
) {
return h3Index;
}
if (typeof h3Index !== 'string' || INVALID_HEXIDECIMAL_CHAR.test(h3Index)) {
return [0, 0];
}
const upper = parseInt(h3Index.substring(0, h3Index.length - 8), BASE_16);
const lower = parseInt(h3Index.substring(h3Index.length - 8), BASE_16);
return [lower, upper];
}
/**
* Convert a 32-bit int to a hexdecimal string
* @private
* @param {number} num Integer to convert
* @return {H3Index} Hexidecimal string
*/
function hexFrom32Bit(num) {
if (num >= 0) {
return num.toString(BASE_16);
}
// Handle negative numbers
num = num & 0x7fffffff;
let tempStr = zeroPad(8, num.toString(BASE_16));
const topNum = (parseInt(tempStr[0], BASE_16) + 8).toString(BASE_16);
tempStr = topNum + tempStr.substring(1);
return tempStr;
}
/**
* Get a H3 index string from a split long (pair of 32-bit ints)
* @param {number} lower Lower 32 bits
* @param {number} upper Upper 32 bits
* @return {H3Index} H3 index
*/
export function splitLongToH3Index(lower, upper) {
return hexFrom32Bit(upper) + zeroPad(8, hexFrom32Bit(lower));
}
/**
* Zero-pad a string to a given length
* @private
* @param {number} fullLen Target length
* @param {string} numStr String to zero-pad
* @return {string} Zero-padded string
*/
function zeroPad(fullLen, numStr) {
const numZeroes = fullLen - numStr.length;
let outStr = '';
for (let i = 0; i < numZeroes; i++) {
outStr += '0';
}
outStr = outStr + numStr;
return outStr;
}
/**
* Populate a C-appropriate GeoLoop struct from a polygon array
* @private
* @param {Array[]} polygonArray Polygon, as an array of coordinate pairs
* @param {number} geoLoop C pointer to a GeoLoop struct
* @param {boolean} isGeoJson Whether coordinates are in [lng, lat] order per GeoJSON spec
* @return {number} C pointer to populated GeoLoop struct
*/
function polygonArrayToGeoLoop(polygonArray, geoLoop, isGeoJson) {
const numVerts = polygonArray.length;
const geoCoordArray = C._calloc(numVerts, SZ_LATLNG);
// Support [lng, lat] pairs if GeoJSON is specified
const latIndex = isGeoJson ? 1 : 0;
const lngIndex = isGeoJson ? 0 : 1;
for (let i = 0; i < numVerts * 2; i += 2) {
C.HEAPF64.set(
[polygonArray[i / 2][latIndex], polygonArray[i / 2][lngIndex]].map(degsToRads),
geoCoordArray / SZ_DBL + i
);
}
C.HEAPU32.set([numVerts, geoCoordArray], geoLoop / SZ_INT);
return geoLoop;
}
/**
* Create a C-appropriate GeoPolygon struct from an array of polygons
* @private
* @param {Array[]} coordinates Array of polygons, each an array of coordinate pairs
* @param {boolean} isGeoJson Whether coordinates are in [lng, lat] order per GeoJSON spec
* @return {number} C pointer to populated GeoPolygon struct
*/
function coordinatesToGeoPolygon(coordinates, isGeoJson) {
// Any loops beyond the first loop are holes
const numHoles = coordinates.length - 1;
const geoPolygon = C._calloc(SZ_GEOPOLYGON);
// Byte positions within the struct
const geoLoopOffset = 0;
const numHolesOffset = geoLoopOffset + SZ_GEOLOOP;
const holesOffset = numHolesOffset + SZ_INT;
// geoLoop is first part of struct
polygonArrayToGeoLoop(coordinates[0], geoPolygon + geoLoopOffset, isGeoJson);
let holes;
if (numHoles > 0) {
holes = C._calloc(numHoles, SZ_GEOLOOP);
for (let i = 0; i < numHoles; i++) {
polygonArrayToGeoLoop(coordinates[i + 1], holes + SZ_GEOLOOP * i, isGeoJson);
}
}
C.setValue(geoPolygon + numHolesOffset, numHoles, 'i32');
C.setValue(geoPolygon + holesOffset, holes, 'i32');
return geoPolygon;
}
/**
* Free memory allocated for a GeoPolygon struct. It is an error to access the struct
* after passing it to this method.
* @private
* @return {number} geoPolygon C pointer to populated GeoPolygon struct
*/
function destroyGeoPolygon(geoPolygon) {
// Byte positions within the struct
const geoLoopOffset = 0;
const numHolesOffset = geoLoopOffset + SZ_GEOLOOP;
const holesOffset = numHolesOffset + SZ_INT;
// Offset of the geoLoop vertex array pointer within the GeoLoop struct
const geoLoopArrayOffset = SZ_INT;
// Free the outer vertex array
C._free(C.getValue(geoPolygon + geoLoopOffset + geoLoopArrayOffset, 'i8*'));
// Free the vertex array for the holes, if any
const numHoles = C.getValue(geoPolygon + numHolesOffset, 'i32');
if (numHoles > 0) {
const holes = C.getValue(geoPolygon + holesOffset, 'i32');
for (let i = 0; i < numHoles; i++) {
C._free(C.getValue(holes + SZ_GEOLOOP * i + geoLoopArrayOffset, 'i8*'));
}
C._free(holes);
}
C._free(geoPolygon);
}
/**
* Read an H3 index from a pointer to C memory.
* @private
* @param {number} cAddress Pointer to allocated C memory
* @param {number} offset Offset, in number of H3 indexes, in case we're
* reading an array
* @return {H3Index} H3 index, or null if index was invalid
*/
function readH3IndexFromPointer(cAddress, offset = 0) {
const lower = C.getValue(cAddress + SZ_H3INDEX * offset, 'i32');
const upper = C.getValue(cAddress + SZ_H3INDEX * offset + SZ_INT, 'i32');
// The lower bits are allowed to be 0s, but if the upper bits are 0
// this represents an invalid H3 index
return upper ? splitLongToH3Index(lower, upper) : null;
}
/**
* Read a boolean (32 bit) from a pointer to C memory.
* @private
* @param {number} cAddress Pointer to allocated C memory
* @param {number} offset Offset, in number of booleans, in case we're
* reading an array
* @return {Boolean} Boolean value
*/
function readBooleanFromPointer(cAddress, offset = 0) {
const val = C.getValue(cAddress + SZ_INT * offset, 'i32');
return Boolean(val);
}
/**
* Read a double from a pointer to C memory.
* @private
* @param {number} cAddress Pointer to allocated C memory
* @param {number} offset Offset, in number of doubles, in case we're
* reading an array
* @return {number} Double value
*/
function readDoubleFromPointer(cAddress, offset = 0) {
return C.getValue(cAddress + SZ_DBL * offset, 'double');
}
/**
* Read a 64-bit int from a pointer to C memory into a JS 64-bit float.
* Note that this may lose precision if larger than MAX_SAFE_INTEGER
* @private
* @param {number} cAddress Pointer to allocated C memory
* @return {number} Double value
*/
function readInt64AsDoubleFromPointer(cAddress) {
return H3.readInt64AsDoubleFromPointer(cAddress);
}
/**
* Store an H3 index in C memory. Primarily used as an efficient way to
* write sets of hexagons.
* @private
* @param {H3IndexInput} h3Index H3 index to store
* @param {number} cAddress Pointer to allocated C memory
* @param {number} offset Offset, in number of H3 indexes from beginning
* of the current array
*/
function storeH3Index(h3Index, cAddress, offset) {
// HEAPU32 is a typed array projection on the index space
// as unsigned 32-bit integers. This means the index needs
// to be divided by SZ_INT (4) to access correctly. Also,
// the H3 index is 64 bits, so we skip by twos as we're writing
// to 32-bit integers in the proper order.
C.HEAPU32.set(h3IndexToSplitLong(h3Index), cAddress / SZ_INT + 2 * offset);
}
/**
* Read an array of 64-bit H3 indexes from C and convert to a JS array of
* H3 index strings
* @private
* @param {number} cAddress Pointer to C ouput array
* @param {number} maxCount Max number of hexagons in array. Hexagons with
* the value 0 will be skipped, so this isn't
* necessarily the length of the output array.
* @return {H3Index[]} Array of H3 indexes
*/
function readArrayOfH3Indexes(cAddress, maxCount) {
const out = [];
for (let i = 0; i < maxCount; i++) {
const h3Index = readH3IndexFromPointer(cAddress, i);
if (h3Index !== null) {
out.push(h3Index);
}
}
return out;
}
/**
* Store an array of H3 index strings as a C array of 64-bit integers.
* @private
* @param {number} cAddress Pointer to C input array
* @param {H3IndexInput[]} hexagons H3 indexes to pass to the C lib
*/
function storeArrayOfH3Indexes(cAddress, hexagons) {
// Assuming the cAddress points to an already appropriately
// allocated space
const count = hexagons.length;
for (let i = 0; i < count; i++) {
storeH3Index(hexagons[i], cAddress, i);
}
}
/**
* Populate a C-appropriate LatLng struct from a [lat, lng] array
* @private
* @param {number} lat Coordinate latitude
* @param {number} lng Coordinate longitude
* @return {number} C pointer to populated LatLng struct
*/
function storeLatLng(lat, lng) {
const geoCoord = C._calloc(1, SZ_LATLNG);
C.HEAPF64.set([lat, lng].map(degsToRads), geoCoord / SZ_DBL);
return geoCoord;
}
function readSingleCoord(cAddress) {
return radsToDegs(C.getValue(cAddress, 'double'));
}
/**
* Read a LatLng from C and return a [lat, lng] pair.
* @private
* @param {number} cAddress Pointer to C struct
* @return {number[]} [lat, lng] pair
*/
function readLatLng(cAddress) {
return [readSingleCoord(cAddress), readSingleCoord(cAddress + SZ_DBL)];
}
/**
* Read a LatLng from C and return a GeoJSON-style [lng, lat] pair.
* @private
* @param {number} cAddress Pointer to C struct
* @return {number[]} [lng, lat] pair
*/
function readLatLngGeoJson(cAddress) {
return [readSingleCoord(cAddress + SZ_DBL), readSingleCoord(cAddress)];
}
/**
* Read the CellBoundary structure into a list of geo coordinate pairs
* @private
* @param {number} cellBoundary C pointer to CellBoundary struct
* @param {boolean} geoJsonCoords Whether to provide GeoJSON coordinate order: [lng, lat]
* @param {boolean} closedLoop Whether to close the loop
* @return {Array[]} Array of geo coordinate pairs
*/
function readCellBoundary(cellBoundary, geoJsonCoords, closedLoop) {
const numVerts = C.getValue(cellBoundary, 'i32');
// Note that though numVerts is an int, the coordinate doubles have to be
// aligned to 8 bytes, hence the 8-byte offset here
const vertsPos = cellBoundary + SZ_DBL;
const out = [];
// Support [lng, lat] pairs if GeoJSON is specified
const readCoord = geoJsonCoords ? readLatLngGeoJson : readLatLng;
for (let i = 0; i < numVerts * 2; i += 2) {
out.push(readCoord(vertsPos + SZ_DBL * i));
}
if (closedLoop) {
// Close loop if GeoJSON is specified
out.push(out[0]);
}
return out;
}
/**
* Read the LinkedGeoPolygon structure into a nested array of MultiPolygon coordinates
* @private
* @param {number} polygon C pointer to LinkedGeoPolygon struct
* @param {boolean} formatAsGeoJson Whether to provide GeoJSON output: [lng, lat], closed loops
* @return {number[][][][]} MultiPolygon-style output.
*/
function readMultiPolygon(polygon, formatAsGeoJson) {
const output = [];
const readCoord = formatAsGeoJson ? readLatLngGeoJson : readLatLng;
let loops;
let loop;
let coords;
let coord;
// Loop through the linked structure, building the output
while (polygon) {
output.push((loops = []));
// Follow ->first pointer
loop = C.getValue(polygon, 'i8*');
while (loop) {
loops.push((coords = []));
// Follow ->first pointer
coord = C.getValue(loop, 'i8*');
while (coord) {
coords.push(readCoord(coord));
// Follow ->next pointer
coord = C.getValue(coord + SZ_DBL * 2, 'i8*');
}
if (formatAsGeoJson) {
// Close loop if GeoJSON is requested
coords.push(coords[0]);
}
// Follow ->next pointer
loop = C.getValue(loop + SZ_PTR * 2, 'i8*');
}
// Follow ->next pointer
polygon = C.getValue(polygon + SZ_PTR * 2, 'i8*');
}
return output;
}
/**
* Read a CoordIJ from C and return an {i, j} pair.
* @private
* @param {number} cAddress Pointer to C struct
* @return {CoordIJ} {i, j} pair
*/
function readCoordIJ(cAddress) {
return {
i: C.getValue(cAddress, 'i32'),
j: C.getValue(cAddress + SZ_INT, 'i32')
};
}
/**
* Store an {i, j} pair to a C CoordIJ struct.
* @private
* @param {number} cAddress Pointer to C struct
* @return {CoordIJ} {i, j} pair
*/
function storeCoordIJ(cAddress, {i, j}) {
C.setValue(cAddress, i, 'i32');
C.setValue(cAddress + SZ_INT, j, 'i32');
}
/**
* Read an array of positive integers array from C. Negative
* values are considered invalid and ignored in output.
* @private
* @param {number} cAddress Pointer to C array
* @param {number} count Length of C array
* @return {number[]} Javascript integer array
*/
function readArrayOfPositiveIntegers(cAddress, count) {
const out = [];
for (let i = 0; i < count; i++) {
const int = C.getValue(cAddress + SZ_INT * i, 'i32');
if (int >= 0) {
out.push(int);
}
}
return out;
}
// ----------------------------------------------------------------------------
// Public API functions: Core
/**
* Whether a given string represents a valid H3 index
* @static
* @param {H3IndexInput} h3Index H3 index to check
* @return {boolean} Whether the index is valid
*/
export function isValidCell(h3Index) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
return Boolean(H3.isValidCell(lower, upper));
}
/**
* Whether the given H3 index is a pentagon
* @static
* @param {H3IndexInput} h3Index H3 index to check
* @return {boolean} isPentagon
*/
export function isPentagon(h3Index) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
return Boolean(H3.isPentagon(lower, upper));
}
/**
* Whether the given H3 index is in a Class III resolution (rotated versus
* the icosahedron and subject to shape distortion adding extra points on
* icosahedron edges, making them not true hexagons).
* @static
* @param {H3IndexInput} h3Index H3 index to check
* @return {boolean} isResClassIII
*/
export function isResClassIII(h3Index) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
return Boolean(H3.isResClassIII(lower, upper));
}
/**
* Get the number of the base cell for a given H3 index
* @static
* @param {H3IndexInput} h3Index H3 index to get the base cell for
* @return {number} Index of the base cell (0-121)
*/
export function getBaseCellNumber(h3Index) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
return H3.getBaseCellNumber(lower, upper);
}
/**
* Get the indices of all icosahedron faces intersected by a given H3 index
* @static
* @param {H3IndexInput} h3Index H3 index to get faces for
* @return {number[]} Indices (0-19) of all intersected faces
* @throws {H3Error} If input is invalid
*/
export function getIcosahedronFaces(h3Index) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
const countPtr = C._malloc(SZ_INT);
try {
throwIfError(H3.maxFaceCount(lower, upper, countPtr));
const count = C.getValue(countPtr, 'i32');
const faces = C._malloc(SZ_INT * count);
try {
throwIfError(H3.getIcosahedronFaces(lower, upper, faces));
return readArrayOfPositiveIntegers(faces, count);
} finally {
C._free(faces);
}
} finally {
C._free(countPtr);
}
}
/**
* Returns the resolution of an H3 index
* @static
* @param {H3IndexInput} h3Index H3 index to get resolution
* @return {number} The number (0-15) resolution, or -1 if invalid
*/
export function getResolution(h3Index) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
if (!H3.isValidCell(lower, upper)) {
// Compatability with stated API
return -1;
}
return H3.getResolution(lower, upper);
}
/**
* Get the hexagon containing a lat,lon point
* @static
* @param {number} lat Latitude of point
* @param {number} lng Longtitude of point
* @param {number} res Resolution of hexagons to return
* @return {H3Index} H3 index
* @throws {H3Error} If input is invalid
*/
export function latLngToCell(lat, lng, res) {
const latLng = C._malloc(SZ_LATLNG);
// Slightly more efficient way to set the memory
C.HEAPF64.set([lat, lng].map(degsToRads), latLng / SZ_DBL);
// Read value as a split long
const h3Index = C._malloc(SZ_H3INDEX);
try {
throwIfError(H3.latLngToCell(latLng, res, h3Index));
return readH3IndexFromPointer(h3Index);
} finally {
C._free(h3Index);
C._free(latLng);
}
}
/**
* Get the lat,lon center of a given hexagon
* @static
* @param {H3IndexInput} h3Index H3 index
* @return {number[]} Point as a [lat, lng] pair
* @throws {H3Error} If input is invalid
*/
export function cellToLatLng(h3Index) {
const latLng = C._malloc(SZ_LATLNG);
const [lower, upper] = h3IndexToSplitLong(h3Index);
try {
throwIfError(H3.cellToLatLng(lower, upper, latLng));
return readLatLng(latLng);
} finally {
C._free(latLng);
}
}
/**
* Get the vertices of a given hexagon (or pentagon), as an array of [lat, lng]
* points. For pentagons and hexagons on the edge of an icosahedron face, this
* function may return up to 10 vertices.
* @static
* @param {H3Index} h3Index H3 index
* @param {boolean} [formatAsGeoJson] Whether to provide GeoJSON output: [lng, lat], closed loops
* @return {number[][]} Array of [lat, lng] pairs
* @throws {H3Error} If input is invalid
*/
export function cellToBoundary(h3Index, formatAsGeoJson) {
const cellBoundary = C._malloc(SZ_CELLBOUNDARY);
const [lower, upper] = h3IndexToSplitLong(h3Index);
try {
throwIfError(H3.cellToBoundary(lower, upper, cellBoundary));
return readCellBoundary(cellBoundary, formatAsGeoJson, formatAsGeoJson);
} finally {
C._free(cellBoundary);
}
}
// ----------------------------------------------------------------------------
// Public API functions: Algorithms
/**
* Get the parent of the given hexagon at a particular resolution
* @static
* @param {H3IndexInput} h3Index H3 index to get parent for
* @param {number} res Resolution of hexagon to return
* @return {H3Index} H3 index of parent, or null for invalid input
* @throws {H3Error} If input is invalid
*/
export function cellToParent(h3Index, res) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
const parent = C._malloc(SZ_H3INDEX);
try {
throwIfError(H3.cellToParent(lower, upper, res, parent));
return readH3IndexFromPointer(parent);
} finally {
C._free(parent);
}
}
/**
* Get the children/descendents of the given hexagon at a particular resolution
* @static
* @param {H3IndexInput} h3Index H3 index to get children for
* @param {number} res Resolution of hexagons to return
* @return {H3Index[]} H3 indexes of children, or empty array for invalid input
* @throws {H3Error} If resolution is invalid or output is too large for JS
*/
export function cellToChildren(h3Index, res) {
// Bad input in this case can potentially result in high computation volume
// using the current C algorithm. Validate and return an empty array on failure.
if (!isValidCell(h3Index)) {
return [];
}
const [lower, upper] = h3IndexToSplitLong(h3Index);
const countPtr = C._malloc(SZ_INT64);
try {
throwIfError(H3.cellToChildrenSize(lower, upper, res, countPtr));
const count = readInt64AsDoubleFromPointer(countPtr);
validateArrayLength(count);
const hexagons = C._calloc(count, SZ_H3INDEX);
try {
throwIfError(H3.cellToChildren(lower, upper, res, hexagons));
return readArrayOfH3Indexes(hexagons, count);
} finally {
C._free(hexagons);
}
} finally {
C._free(countPtr);
}
}
/**
* Get the center child of the given hexagon at a particular resolution
* @static
* @param {H3IndexInput} h3Index H3 index to get center child for
* @param {number} res Resolution of hexagon to return
* @return {H3Index} H3 index of child, or null for invalid input
* @throws {H3Error} If resolution is invalid
*/
export function cellToCenterChild(h3Index, res) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
const centerChild = C._malloc(SZ_H3INDEX);
try {
throwIfError(H3.cellToCenterChild(lower, upper, res, centerChild));
return readH3IndexFromPointer(centerChild);
} finally {
C._free(centerChild);
}
}
/**
* Get all hexagons in a k-ring around a given center. The order of the hexagons is undefined.
* @static
* @param {H3IndexInput} h3Index H3 index of center hexagon
* @param {number} ringSize Radius of k-ring
* @return {H3Index[]} H3 indexes for all hexagons in ring
* @throws {H3Error} If input is invalid or output is too large for JS
*/
export function gridDisk(h3Index, ringSize) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
const countPtr = C._malloc(SZ_INT64);
try {
throwIfError(H3.maxGridDiskSize(ringSize, countPtr));
const count = readInt64AsDoubleFromPointer(countPtr);
validateArrayLength(count);
const hexagons = C._calloc(count, SZ_H3INDEX);
try {
throwIfError(H3.gridDisk(lower, upper, ringSize, hexagons));
return readArrayOfH3Indexes(hexagons, count);
} finally {
C._free(hexagons);
}
} finally {
C._free(countPtr);
}
}
/**
* Get all hexagons in a k-ring around a given center, in an array of arrays
* ordered by distance from the origin. The order of the hexagons within each ring is undefined.
* @static
* @param {H3IndexInput} h3Index H3 index of center hexagon
* @param {number} ringSize Radius of k-ring
* @return {H3Index[][]} Array of arrays with H3 indexes for all hexagons each ring
* @throws {H3Error} If input is invalid or output is too large for JS
*/
export function gridDiskDistances(h3Index, ringSize) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
const countPtr = C._malloc(SZ_INT64);
try {
throwIfError(H3.maxGridDiskSize(ringSize, countPtr));
const count = readInt64AsDoubleFromPointer(countPtr);
validateArrayLength(count);
const kRings = C._calloc(count, SZ_H3INDEX);
const distances = C._calloc(count, SZ_INT);
try {
throwIfError(H3.gridDiskDistances(lower, upper, ringSize, kRings, distances));
// Create an array of empty arrays to hold the output
const out = [];
for (let i = 0; i < ringSize + 1; i++) {
out.push([]);
}
// Read the array of hexagons, putting them into the appropriate rings
for (let i = 0; i < count; i++) {
const cell = readH3IndexFromPointer(kRings, i);
const index = C.getValue(distances + SZ_INT * i, 'i32');
// eslint-disable-next-line max-depth
if (cell !== null) {
out[index].push(cell);
}
}
return out;
} finally {
C._free(kRings);
C._free(distances);
}
} finally {
C._free(countPtr);
}
}
/**
* Get all hexagons in a hollow hexagonal ring centered at origin with sides of a given length.
* Unlike kRing, this function will throw an error if there is a pentagon anywhere in the ring.
* @static
* @param {H3IndexInput} h3Index H3 index of center hexagon
* @param {number} ringSize Radius of ring
* @return {H3Index[]} H3 indexes for all hexagons in ring
* @throws {Error} If the algorithm could not calculate the ring
* @throws {H3Error} If input is invalid
*/
export function gridRingUnsafe(h3Index, ringSize) {
const maxCount = ringSize === 0 ? 1 : 6 * ringSize;
const hexagons = C._calloc(maxCount, SZ_H3INDEX);
try {
throwIfError(H3.gridRingUnsafe(...h3IndexToSplitLong(h3Index), ringSize, hexagons));
return readArrayOfH3Indexes(hexagons, maxCount);
} finally {
C._free(hexagons);
}
}
/**
* Get all hexagons with centers contained in a given polygon. The polygon
* is specified with GeoJson semantics as an array of loops. Each loop is
* an array of [lat, lng] pairs (or [lng, lat] if isGeoJson is specified).
* The first loop is the perimeter of the polygon, and subsequent loops are
* expected to be holes.
* @static
* @param {number[][] | number[][][]} coordinates
* Array of loops, or a single loop
* @param {number} res Resolution of hexagons to return
* @param {boolean} [isGeoJson] Whether to expect GeoJson-style [lng, lat]
* pairs instead of [lat, lng]
* @return {H3Index[]} H3 indexes for all hexagons in polygon
* @throws {H3Error} If input is invalid or output is too large for JS
*/
export function polygonToCells(coordinates, res, isGeoJson) {
validateRes(res);
isGeoJson = Boolean(isGeoJson);
// Guard against empty input
if (coordinates.length === 0 || coordinates[0].length === 0) {
return [];
}
// Wrap to expected format if a single loop is provided
if (typeof coordinates[0][0] === 'number') {
coordinates = [coordinates];
}
const geoPolygon = coordinatesToGeoPolygon(coordinates, isGeoJson);
const countPtr = C._malloc(SZ_INT64);
try {
throwIfError(H3.maxPolygonToCellsSize(geoPolygon, res, 0, countPtr));
const count = readInt64AsDoubleFromPointer(countPtr);
validateArrayLength(count);
const hexagons = C._calloc(count, SZ_H3INDEX);
try {
throwIfError(H3.polygonToCells(geoPolygon, res, 0, hexagons));
return readArrayOfH3Indexes(hexagons, count);
} finally {
C._free(hexagons);
}
} finally {
C._free(countPtr);
destroyGeoPolygon(geoPolygon);
}
}
/**
* Get the outlines of a set of H3 hexagons, returned in GeoJSON MultiPolygon
* format (an array of polygons, each with an array of loops, each an array of
* coordinates). Coordinates are returned as [lat, lng] pairs unless GeoJSON
* is requested.
*
* It is the responsibility of the caller to ensure that all hexagons in the
* set have the same resolution and that the set contains no duplicates. Behavior
* is undefined if duplicates or multiple resolutions are present, and the
* algorithm may produce unexpected or invalid polygons.
*
* @static
* @param {H3IndexInput[]} h3Indexes H3 indexes to get outlines for
* @param {boolean} [formatAsGeoJson] Whether to provide GeoJSON output:
* [lng, lat], closed loops
* @return {number[][][][]} MultiPolygon-style output.
* @throws {H3Error} If input is invalid
*/
export function cellsToMultiPolygon(h3Indexes, formatAsGeoJson) {
// Early exit on empty input
if (!h3Indexes || !h3Indexes.length) {
return [];
}
// Set up input set
const indexCount = h3Indexes.length;
const set = C._calloc(indexCount, SZ_H3INDEX);
storeArrayOfH3Indexes(set, h3Indexes);
// Allocate memory for output linked polygon
const polygon = C._calloc(SZ_LINKED_GEOPOLYGON);
try {
throwIfError(H3.cellsToLinkedMultiPolygon(set, indexCount, polygon));
return readMultiPolygon(polygon, formatAsGeoJson);
} finally {
// Clean up
H3.destroyLinkedMultiPolygon(polygon);
C._free(polygon);
C._free(set);
}
}
/**
* Compact a set of hexagons of the same resolution into a set of hexagons across
* multiple levels that represents the same area.
* @static
* @param {H3IndexInput[]} h3Set H3 indexes to compact
* @return {H3Index[]} Compacted H3 indexes
* @throws {H3Error} If the input is invalid (e.g. duplicate hexagons)
*/
export function compactCells(h3Set) {
if (!h3Set || !h3Set.length) {
return [];
}
// Set up input set
const count = h3Set.length;
const set = C._calloc(count, SZ_H3INDEX);
storeArrayOfH3Indexes(set, h3Set);
// Allocate memory for compacted hexagons, worst-case is no compaction
const compactedSet = C._calloc(count, SZ_H3INDEX);
try {
throwIfError(H3.compactCells(set, compactedSet, count, UNUSED_UPPER_32_BITS));
return readArrayOfH3Indexes(compactedSet, count);
} finally {
C._free(set);
C._free(compactedSet);
}
}
/**
* Uncompact a compacted set of hexagons to hexagons of the same resolution
* @static
* @param {H3IndexInput[]} compactedSet H3 indexes to uncompact
* @param {number} res The resolution to uncompact to
* @return {H3Index[]} The uncompacted H3 indexes
* @throws {H3Error} If the input is invalid (e.g. invalid resolution)
*/
export function uncompactCells(compactedSet, res) {
validateRes(res);
if (!compactedSet || !compactedSet.length) {
return [];
}
// Set up input set
const count = compactedSet.length;
const set = C._calloc(count, SZ_H3INDEX);
storeArrayOfH3Indexes(set, compactedSet);
// Estimate how many hexagons we need (always overestimates if in error)
const uncompactCellSizePtr = C._malloc(SZ_INT64);
try {
throwIfError(
H3.uncompactCellsSize(set, count, UNUSED_UPPER_32_BITS, res, uncompactCellSizePtr)
);
const uncompactCellSize = readInt64AsDoubleFromPointer(uncompactCellSizePtr);
validateArrayLength(uncompactCellSize);
// Allocate memory for uncompacted hexagons
const uncompactedSet = C._calloc(uncompactCellSize, SZ_H3INDEX);
try {
throwIfError(
H3.uncompactCells(
set,
count,
UNUSED_UPPER_32_BITS,
uncompactedSet,
uncompactCellSize,
UNUSED_UPPER_32_BITS,
res
)
);
return readArrayOfH3Indexes(uncompactedSet, uncompactCellSize);
} finally {
C._free(set);
C._free(uncompactedSet);
}
} finally {
C._free(uncompactCellSizePtr);
}
}
// ----------------------------------------------------------------------------
// Public API functions: Directed edges
/**
* Whether two H3 indexes are neighbors (share an edge)
* @static
* @param {H3IndexInput} origin Origin hexagon index
* @param {H3IndexInput} destination Destination hexagon index
* @return {boolean} Whether the hexagons share an edge
* @throws {H3Error} If the input is invalid
*/
export function areNeighborCells(origin, destination) {
const [oLower, oUpper] = h3IndexToSplitLong(origin);
const [dLower, dUpper] = h3IndexToSplitLong(destination);
const out = C._malloc(SZ_INT);
try {
throwIfError(H3.areNeighborCells(oLower, oUpper, dLower, dUpper, out));
return readBooleanFromPointer(out);
} finally {
C._free(out);
}
}
/**
* Get an H3 index representing a unidirectional edge for a given origin and destination
* @static
* @param {H3IndexInput} origin Origin hexagon index
* @param {H3IndexInput} destination Destination hexagon index
* @return {H3Index} H3 index of the edge, or null if no edge is shared
* @throws {H3Error} If the input is invalid
*/
export function cellsToDirectedEdge(origin, destination) {
const [oLower, oUpper] = h3IndexToSplitLong(origin);
const [dLower, dUpper] = h3IndexToSplitLong(destination);
const h3Index = C._malloc(SZ_H3INDEX);
try {
throwIfError(H3.cellsToDirectedEdge(oLower, oUpper, dLower, dUpper, h3Index));
return readH3IndexFromPointer(h3Index);
} finally {
C._free(h3Index);
}
}
/**
* Get the origin hexagon from an H3 index representing a unidirectional edge
* @static
* @param {H3IndexInput} edgeIndex H3 index of the edge
* @return {H3Index} H3 index of the edge origin
* @throws {H3Error} If the input is invalid
*/
export function getDirectedEdgeOrigin(edgeIndex) {
const [lower, upper] = h3IndexToSplitLong(edgeIndex);
const h3Index = C._malloc(SZ_H3INDEX);
try {
throwIfError(H3.getDirectedEdgeOrigin(lower, upper, h3Index));
return readH3IndexFromPointer(h3Index);
} finally {
C._free(h3Index);
}
}
/**
* Get the destination hexagon from an H3 index representing a unidirectional edge
* @static
* @param {H3IndexInput} edgeIndex H3 index of the edge
* @return {H3Index} H3 index of the edge destination
* @throws {H3Error} If the input is invalid
*/
export function getDirectedEdgeDestination(edgeIndex) {
const [lower, upper] = h3IndexToSplitLong(edgeIndex);
const h3Index = C._malloc(SZ_H3INDEX);
try {
throwIfError(H3.getDirectedEdgeDestination(lower, upper, h3Index));
return readH3IndexFromPointer(h3Index);
} finally {
C._free(h3Index);
}
}
/**
* Whether the input is a valid unidirectional edge
* @static
* @param {H3IndexInput} edgeIndex H3 index of the edge
* @return {boolean} Whether the index is valid
*/
export function isValidDirectedEdge(edgeIndex) {
const [lower, upper] = h3IndexToSplitLong(edgeIndex);
return Boolean(H3.isValidDirectedEdge(lower, upper));
}
/**
* Get the [origin, destination] pair represented by a unidirectional edge
* @static
* @param {H3IndexInput} edgeIndex H3 index of the edge
* @return {H3Index[]} [origin, destination] pair as H3 indexes
* @throws {H3Error} If the input is invalid
*/
export function directedEdgeToCells(edgeIndex) {
const [lower, upper] = h3IndexToSplitLong(edgeIndex);
const count = 2;
const hexagons = C._calloc(count, SZ_H3INDEX);
try {
throwIfError(H3.directedEdgeToCells(lower, upper, hexagons));
return readArrayOfH3Indexes(hexagons, count);
} finally {
C._free(hexagons);
}
}
/**
* Get all of the unidirectional edges with the given H3 index as the origin (i.e. an edge to
* every neighbor)
* @static
* @param {H3IndexInput} h3Index H3 index of the origin hexagon
* @return {H3Index[]} List of unidirectional edges
* @throws {H3Error} If the input is invalid
*/
export function originToDirectedEdges(h3Index) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
const count = 6;
const edges = C._calloc(count, SZ_H3INDEX);
try {
throwIfError(H3.originToDirectedEdges(lower, upper, edges));
return readArrayOfH3Indexes(edges, count);
} finally {
C._free(edges);
}
}
/**
* Get the vertices of a given edge as an array of [lat, lng] points. Note that for edges that
* cross the edge of an icosahedron face, this may return 3 coordinates.
* @static
* @param {H3IndexInput} edgeIndex H3 index of the edge
* @param {boolean} [formatAsGeoJson] Whether to provide GeoJSON output: [lng, lat]
* @return {number[][]} Array of geo coordinate pairs
* @throws {H3Error} If the input is invalid
*/
export function directedEdgeToBoundary(edgeIndex, formatAsGeoJson) {
const cellBoundary = C._malloc(SZ_CELLBOUNDARY);
const [lower, upper] = h3IndexToSplitLong(edgeIndex);
try {
throwIfError(H3.directedEdgeToBoundary(lower, upper, cellBoundary));
return readCellBoundary(cellBoundary, formatAsGeoJson);
} finally {
C._free(cellBoundary);
}
}
/**
* Get the grid distance between two hex indexes. This function may fail
* to find the distance between two indexes if they are very far apart or
* on opposite sides of a pentagon.
* @static
* @param {H3IndexInput} origin Origin hexagon index
* @param {H3IndexInput} destination Destination hexagon index
* @return {number} Distance between hexagons
* @throws {H3Error} If input is invalid or the distance could not be calculated
*/
export function gridDistance(origin, destination) {
const [oLower, oUpper] = h3IndexToSplitLong(origin);
const [dLower, dUpper] = h3IndexToSplitLong(destination);
const countPtr = C._malloc(SZ_INT64);
try {
throwIfError(H3.gridDistance(oLower, oUpper, dLower, dUpper, countPtr));
return readInt64AsDoubleFromPointer(countPtr);
} finally {
C._free(countPtr);
}
}
/**
* Given two H3 indexes, return the line of indexes between them (inclusive).
*
* This function may fail to find the line between two indexes, for
* example if they are very far apart. It may also fail when finding
* distances for indexes on opposite sides of a pentagon.
*
* Notes:
*
* - The specific output of this function should not be considered stable
* across library versions. The only guarantees the library provides are
* that the line length will be `h3Distance(start, end) + 1` and that
* every index in the line will be a neighbor of the preceding index.
* - Lines are drawn in grid space, and may not correspond exactly to either
* Cartesian lines or great arcs.
*
* @static
* @param {H3IndexInput} origin Origin hexagon index
* @param {H3IndexInput} destination Destination hexagon index
* @return {H3Index[]} H3 indexes connecting origin and destination
* @throws {H3Error} If input is invalid or the line cannot be calculated
*/
export function gridPathCells(origin, destination) {
const [oLower, oUpper] = h3IndexToSplitLong(origin);
const [dLower, dUpper] = h3IndexToSplitLong(destination);
const countPtr = C._malloc(SZ_INT64);
try {
throwIfError(H3.gridPathCellsSize(oLower, oUpper, dLower, dUpper, countPtr));
const count = readInt64AsDoubleFromPointer(countPtr);
validateArrayLength(count);
const hexagons = C._calloc(count, SZ_H3INDEX);
try {
H3.gridPathCells(oLower, oUpper, dLower, dUpper, hexagons);
return readArrayOfH3Indexes(hexagons, count);
} finally {
C._free(hexagons);
}
} finally {
C._free(countPtr);
}
}
const LOCAL_IJ_DEFAULT_MODE = 0;
/**
* Produces IJ coordinates for an H3 index anchored by an origin.
*
* - The coordinate space used by this function may have deleted
* regions or warping due to pentagonal distortion.
* - Coordinates are only comparable if they come from the same
* origin index.
* - Failure may occur if the index is too far away from the origin
* or if the index is on the other side of a pentagon.
* - This function is experimental, and its output is not guaranteed
* to be compatible across different versions of H3.
* @static
* @param {H3IndexInput} origin Origin H3 index
* @param {H3IndexInput} destination H3 index for which to find relative coordinates
* @return {CoordIJ} Coordinates as an `{i, j}` pair
* @throws {H3Error} If the IJ coordinates cannot be calculated
*/
export function cellToLocalIj(origin, destination) {
const ij = C._malloc(SZ_COORDIJ);
try {
throwIfError(
H3.cellToLocalIj(
...h3IndexToSplitLong(origin),
...h3IndexToSplitLong(destination),
LOCAL_IJ_DEFAULT_MODE,
ij
)
);
return readCoordIJ(ij);
} finally {
C._free(ij);
}
}
/**
* Produces an H3 index for IJ coordinates anchored by an origin.
*
* - The coordinate space used by this function may have deleted
* regions or warping due to pentagonal distortion.
* - Coordinates are only comparable if they come from the same
* origin index.
* - Failure may occur if the index is too far away from the origin
* or if the index is on the other side of a pentagon.
* - This function is experimental, and its output is not guaranteed
* to be compatible across different versions of H3.
* @static
* @param {H3IndexInput} origin Origin H3 index
* @param {CoordIJ} coords Coordinates as an `{i, j}` pair
* @return {H3Index} H3 index at the relative coordinates
* @throws {H3Error} If the H3 index cannot be calculated
*/
export function localIjToCell(origin, coords) {
// Validate input coords
if (!coords || typeof coords.i !== 'number' || typeof coords.j !== 'number') {
throw new Error('Coordinates must be provided as an {i, j} object');
}
// Allocate memory for the CoordIJ struct and an H3 index to hold the return value
const ij = C._malloc(SZ_COORDIJ);
const out = C._malloc(SZ_H3INDEX);
storeCoordIJ(ij, coords);
try {
throwIfError(
H3.localIjToCell(...h3IndexToSplitLong(origin), ij, LOCAL_IJ_DEFAULT_MODE, out)
);
return readH3IndexFromPointer(out);
} finally {
C._free(ij);
C._free(out);
}
}
// ----------------------------------------------------------------------------
// Public API functions: Distance/area utilities
/**
* Great circle distance between two geo points. This is not specific to H3,
* but is implemented in the library and provided here as a convenience.
* @static
* @param {number[]} latLng1 Origin coordinate as [lat, lng]
* @param {number[]} latLng2 Destination coordinate as [lat, lng]
* @param {string} unit Distance unit (either UNITS.m, UNITS.km, or UNITS.rads)
* @return {number} Great circle distance
* @throws {H3Error} If the unit is invalid
*/
export function greatCircleDistance(latLng1, latLng2, unit) {
const coord1 = storeLatLng(latLng1[0], latLng1[1]);
const coord2 = storeLatLng(latLng2[0], latLng2[1]);
let result;
switch (unit) {
case UNITS.m:
result = H3.greatCircleDistanceM(coord1, coord2);
break;
case UNITS.km:
result = H3.greatCircleDistanceKm(coord1, coord2);
break;
case UNITS.rads:
result = H3.greatCircleDistanceRads(coord1, coord2);
break;
default:
result = null;
}
C._free(coord1);
C._free(coord2);
if (result === null) {
throw JSBindingError(E_UNKNOWN_UNIT, unit);
}
return result;
}
/**
* Exact area of a given cell
* @static
* @param {H3Index} h3Index H3 index of the hexagon to measure
* @param {string} unit Distance unit (either UNITS.m2, UNITS.km2, or UNITS.rads2)
* @return {number} Cell area
* @throws {H3Error} If the input is invalid
*/
export function cellArea(h3Index, unit) {
const [lower, upper] = h3IndexToSplitLong(h3Index);
const out = C._malloc(SZ_DBL);
try {
switch (unit) {
case UNITS.m2:
throwIfError(H3.cellAreaM2(lower, upper, out));
break;
case UNITS.km2:
throwIfError(H3.cellAreaKm2(lower, upper, out));
break;
case UNITS.rads2:
throwIfError(H3.cellAreaRads2(lower, upper, out));
break;
default:
throw JSBindingError(E_UNKNOWN_UNIT, unit);
}
return readDoubleFromPointer(out);
} finally {
C._free(out);
}
}
/**
* Exact length of a given unidirectional edge
* @static
* @param {H3Index} edge H3 index of the edge to measure
* @param {string} unit Distance unit (either UNITS.m, UNITS.km, or UNITS.rads)
* @return {number} Cell area
* @throws {H3Error} If the input is invalid
*/
export function exactEdgeLength(edge, unit) {
const [lower, upper] = h3IndexToSplitLong(edge);
const out = C._malloc(SZ_DBL);
try {
switch (unit) {
case UNITS.m:
throwIfError(H3.exactEdgeLengthM(lower, upper, out));
break;
case UNITS.km:
throwIfError(H3.exactEdgeLengthKm(lower, upper, out));
break;
case UNITS.rads:
throwIfError(H3.exactEdgeLengthRads(lower, upper, out));
break;
default:
throw JSBindingError(E_UNKNOWN_UNIT, unit);
}
return readDoubleFromPointer(out);
} finally {
C._free(out);
}
}
/**
* Average hexagon area at a given resolution
* @static
* @param {number} res Hexagon resolution
* @param {string} unit Area unit (either UNITS.m2, UNITS.km2, or UNITS.rads2)
* @return {number} Average area
* @throws {H3Error} If the input is invalid
*/
export function getHexagonAreaAvg(res, unit) {
validateRes(res);
const out = C._malloc(SZ_DBL);
try {
switch (unit) {