s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
706 lines • 22.9 kB
JavaScript
/** COMPONENTS */
import { IJtoST, STtoIJ, quadraticSTtoUV as STtoUV, SiTiToST, quadraticUVtoST as UVtoST, XYZtoFaceUV, faceUVtoXYZ, getUNorm, getVNorm, lonLatToXYZ, xyzToLonLat, } from './s2/coords';
import { toIJ as S2PointToIJ, invert, normalize, fromUV as s2PointFromUV } from './s2/point';
/** CONSTANTS */
let LOOKUP_POS;
let LOOKUP_IJ;
export const K_FACE_BITS = 3;
export const FACE_BITS = 3n;
export const K_NUM_FACES = 6;
export const NUM_FACES = 6n;
export const K_MAX_LEVEL = 30;
export const MAX_LEVEL = 30n;
export const POS_BITS = 61n;
export const K_WRAP_OFFSET = 13835058055282163712n;
export const K_MAX_SIZE = 1073741824;
/**
* Initialize the lookup table for the Hilbert curve
* @param level - zoom level of the cell
* @param i - x coord
* @param j - y coord
* @param origOrientation - original orientation
* @param pos - position
* @param orientation - orientation
*/
function initLookupCell(level, i, j, origOrientation, pos, orientation) {
if (LOOKUP_POS === undefined)
LOOKUP_POS = [];
if (LOOKUP_IJ === undefined)
LOOKUP_IJ = [];
const kPosToOriengation = [1, 0, 0, 3];
const kPosToIJ = [
[0, 1, 3, 2],
[0, 2, 3, 1],
[3, 2, 0, 1],
[3, 1, 0, 2],
];
if (level === 4) {
const ij = (i << 4) + j;
LOOKUP_POS[(ij << 2) + origOrientation] = BigInt((pos << 2) + orientation);
LOOKUP_IJ[(pos << 2) + origOrientation] = (ij << 2) + orientation;
}
else {
level++;
i <<= 1;
j <<= 1;
pos <<= 2;
const r = kPosToIJ[orientation];
initLookupCell(level, i + (r[0] >> 1), j + (r[0] & 1), origOrientation, pos, orientation ^ kPosToOriengation[0]);
initLookupCell(level, i + (r[1] >> 1), j + (r[1] & 1), origOrientation, pos + 1, orientation ^ kPosToOriengation[1]);
initLookupCell(level, i + (r[2] >> 1), j + (r[2] & 1), origOrientation, pos + 2, orientation ^ kPosToOriengation[2]);
initLookupCell(level, i + (r[3] >> 1), j + (r[3] & 1), origOrientation, pos + 3, orientation ^ kPosToOriengation[3]);
}
}
/**
* Create a default S2CellID given a face on the sphere [0-6)
* @param face - the face
* @returns the S2CellID
*/
export function fromFace(face) {
return (BigInt(face) << POS_BITS) + (1n << 60n);
}
/**
* Return a cell given its face (range 0..5), Hilbert curve position within
* that face (an unsigned integer with S2CellId::kPosBits bits), and level
* (range 0..kMaxLevel). The given position will be modified to correspond
* to the Hilbert curve position at the center of the returned cell. This
* is a static function rather than a constructor in order to indicate what
* the arguments represent.
* @param face - the face
* @param pos - the Hilbert curve position
* @param level - the level
* @returns the S2CellID
*/
export function fromFacePosLevel(face, pos, level) {
const cell = (BigInt(face) << POS_BITS) + (pos | 1n);
return parentLevel(cell, BigInt(level));
}
/**
* Create an S2CellID from a lon-lat coordinate
* @param lon - longitude
* @param lat - latitude
* @returns the S2CellID
*/
export function fromLonLat(lon, lat) {
const xyz = lonLatToXYZ(lon, lat);
return fromS2Point(xyz);
}
/**
* Create an S2CellID from an XYZ Point
* @param xyz - 3D input vector
* @returns the S2CellID
*/
export function fromS2Point(xyz) {
// convert to face-i-j
const [face, i, j] = S2PointToIJ(xyz);
// now convert from ij
return fromIJ(face, i, j);
}
/**
* Create an S2CellID from an Face-U-V coordinate
* @param face - the face
* @param u - u coordinate
* @param v - v coordinate
* @returns the S2CellID
*/
export function fromUV(face, u, v) {
// now convert from st
return fromST(face, UVtoST(u), UVtoST(v));
}
/**
* Create an S2CellID from an Face-S-T coordinate
* @param face - the face
* @param s - s coordinate
* @param t - t coordinate
* @returns the S2CellID
*/
export function fromST(face, s, t) {
// now convert from ij
return fromIJ(face, STtoIJ(s), STtoIJ(t));
}
/**
* Create an S2CellID given a distance and level (zoom). Default level is 30n
* @param distance - distance
* @param level - level
* @returns the S2CellID
*/
export function fromDistance(distance, level = MAX_LEVEL) {
level = 2n * (MAX_LEVEL - level);
return (distance << (level + 1n)) + (1n << level);
}
/**
* @param id - the S2CellID
* @returns [face, zoom, i, j]
*/
export function toFaceIJ(id) {
const zoom = level(id);
const [face, i, j] = toIJ(id, zoom);
return [face, zoom, i, j];
}
/**
* Create an S2CellID from an Face-I-J coordinate and map it to a zoom if desired.
* @param face - the face
* @param i - i coordinate
* @param j - j coordinate
* @param level - zoom level
* @returns the S2CellID
*/
export function fromIJ(face, i, j, level) {
if (LOOKUP_POS === undefined) {
LOOKUP_POS = [];
for (let i = 0; i < 4; i++)
initLookupCell(0, 0, 0, i, 0, i);
}
const bigFace = BigInt(face);
let bigI = BigInt(i);
let bigJ = BigInt(j);
if (level !== undefined) {
const levelB = BigInt(level);
bigI = bigI << (MAX_LEVEL - levelB);
bigJ = bigJ << (MAX_LEVEL - levelB);
}
let n = bigFace << 60n;
// Alternating faces have opposite Hilbert curve orientations; this
// is necessary in order for all faces to have a right-handed
// coordinate system.
let bits = bigFace & 1n;
// Each iteration maps 4 bits of "i" and "j" into 8 bits of the Hilbert
// curve position. The lookup table transforms a 10-bit key of the form
// "iiiijjjjoo" to a 10-bit value of the form "ppppppppoo", where the
// letters [ijpo] denote bits of "i", "j", Hilbert curve position, and
// Hilbert curve orientation respectively.
for (let k = 7n; k >= 0n; k--) {
const kk = k * 4n;
bits += ((bigI >> kk) & 15n) << NUM_FACES;
bits += ((bigJ >> kk) & 15n) << 2n;
bits = LOOKUP_POS[Number(bits)];
n |= (bits >> 2n) << (k * 8n);
bits &= FACE_BITS;
}
const id = n * 2n + 1n;
if (level !== undefined)
return parent(id, level);
return id;
}
/**
* Convert an S2CellID to a Face-I-J coordinate and provide its orientation.
* If a level is provided, the I-J coordinates will be shifted to that level.
* @param id - the S2CellID
* @param level - zoom level
* @returns face-i-j with orientation
*/
export function toIJ(id, level) {
if (LOOKUP_IJ === undefined) {
LOOKUP_IJ = [];
for (let i = 0; i < 4; i++)
initLookupCell(0, 0, 0, i, 0, i);
}
let i = 0;
let j = 0;
const face = Number(id >> POS_BITS);
let bits = face & 1;
// Each iteration maps 8 bits of the Hilbert curve position into
// 4 bits of "i" and "j". The lookup table transforms a key of the
// form "ppppppppoo" to a value of the form "iiiijjjjoo", where the
// letters [ijpo] represents bits of "i", "j", the Hilbert curve
// position, and the Hilbert curve orientation respectively.
//
// On the first iteration we need to be careful to clear out the bits
// representing the cube face.
for (let k = 7; k >= 0; k--) {
const nbits = k === 7 ? 2 : 4;
bits += (Number(id >> (BigInt(k) * 8n + 1n)) & ((1 << (2 * nbits)) - 1)) << 2;
bits = LOOKUP_IJ[bits];
i += (bits >> K_NUM_FACES) << (k * 4);
j += ((bits >> 2) & 15) << (k * 4);
bits &= K_FACE_BITS;
}
// adjust bits to the orientation
const lsb = id & (~id + 1n);
if ((lsb & 1229782938247303424n) !== 0n)
bits ^= 1;
if (level !== undefined) {
i >>= K_MAX_LEVEL - level;
j >>= K_MAX_LEVEL - level;
}
return [face, i, j, Number(bits)];
}
/**
* Convert an S2CellID to an Face-S-T coordinate
* @param id - the S2CellID
* @returns face-s-t coordinate associated with the S2CellID
*/
export function toST(id) {
const [face, i, j] = toIJ(id);
const s = IJtoST(i);
const t = IJtoST(j);
return [face, s, t];
}
/**
* Convert an S2CellID to an Face-U-V coordinate
* @param id - the S2CellID
* @returns face-u-v coordinate associated with the S2CellID
*/
export function toUV(id) {
const [face, s, t] = toST(id);
const u = STtoUV(s);
const v = STtoUV(t);
return [face, u, v];
}
/**
* Convert an S2CellID to an lon-lat coordinate
* @param id - the S2CellID
* @returns lon-lat coordinates
*/
export function toLonLat(id) {
const xyz = toS2Point(id);
return xyzToLonLat(xyz);
}
/**
* Convert an S2CellID to an XYZ Point
* @param id - the S2CellID
* @returns a 3D vector
*/
export function toS2Point(id) {
// Decompose the S2CellID into its constituent parts: face, u, and v.
const [face, u, v] = toUV(id);
// Use the decomposed parts to construct an XYZ Point.
return s2PointFromUV(face, u, v);
}
/**
* Given an S2CellID, get the face it's located in
* @param id - the S2CellID
* @returns face of the cell
*/
export function face(id) {
const face = Number(id >> POS_BITS);
return face;
}
/**
* Given an S2CellID, check if it is a Face Cell.
* @param id - the S2CellID
* @returns true if the cell is a face (lowest zoom level)
*/
export function isFace(id) {
return (id & ((1n << 60n) - 1n)) === 0n;
}
/**
* Given an S2CellID, find the quad tree position [0-4) it's located in
* @param id - the S2CellID
* @returns quad tree position
*/
export function pos(id) {
return id & 2305843009213693951n;
}
/**
* Given an S2CellID, find the level (zoom) its located in
* @param id - the S2CellID
* @returns zoom level
*/
export function level(id) {
let count = 0;
let i = 0n;
while ((id & (1n << i)) === 0n && i < 60n) {
i += 2n;
count++;
}
return 30 - count;
}
/**
* Given an S2CellID, get the distance it spans (or length it covers)
* @param id - the S2CellID
* @param lev - optional zoom level
* @returns distance
*/
export function distance(id, lev) {
if (lev === undefined)
lev = level(id);
return id >> BigInt(2 * (30 - lev) + 1);
}
/**
* Given an S2CellID, get the quad child tile of your choice [0, 4)
* @param id - the S2CellID
* @param pos - quad position 0, 1, 2, or 3
* @returns the child tile at that position
*/
export function child(id, pos) {
const newLSB = (id & (~id + 1n)) >> 2n;
return id + (2n * pos - FACE_BITS) * newLSB;
}
/**
* Given an S2CellID, get all the quad children tiles
* @param id - the S2CellID
* @param orientation - orientation of the child (0 or 1)
* @returns the child tile at that position
*/
export function children(id, orientation = 0) {
const childs = [
child(id, 0n),
child(id, 3n),
child(id, 2n),
child(id, 1n),
];
if (orientation === 0) {
const tmp = childs[1];
childs[1] = childs[3];
childs[3] = tmp;
}
return childs;
}
/**
* Given a Face-level-i-j coordinate, get all its quad children tiles
* @param face - the Face
* @param level - zoom level
* @param i - i coordinate
* @param j - j coordinate
* @returns the child tile at that position
*/
export function childrenIJ(face, level, i, j) {
i = i << 1;
j = j << 1;
return [
fromIJ(face, i, j, level + 1),
fromIJ(face, i + 1, j, level + 1),
fromIJ(face, i, j + 1, level + 1),
fromIJ(face, i + 1, j + 1, level + 1),
];
}
/**
* Given an S2CellID, get the quad position relative to its parent
* @param id - the S2CellID
* @param level - zoom level
* @returns the child tile at that position
*/
export function childPosition(id, level) {
return Number((id >> (2n * (MAX_LEVEL - BigInt(level)) + 1n)) & FACE_BITS);
}
/**
* Given an S2CellID, get the parent quad tile
* @param id - the S2CellID
* @param level - zoom level
* @returns the parent of the input S2CellID
*/
export function parent(id, level) {
const newLSB = level !== undefined ? 1n << (2n * (MAX_LEVEL - BigInt(level))) : (id & (~id + 1n)) << 2n;
return (id & (~newLSB + 1n)) | newLSB;
}
/**
* given an id and level, return the id of the parent level
* @param id - the S2CellID
* @param level - zoom level
* @returns - the parent of the input S2CellID
*/
export function parentLevel(id, level) {
const newLsb = 1n << (2n * (MAX_LEVEL - level));
return (id & (~newLsb + 1n)) | newLsb;
}
/**
* Given an S2CellID, get the hilbert range it spans
* @param id - the S2CellID
* @returns [min, max]
*/
export function range(id) {
const lsb = id & (~id + 1n);
return [id - (lsb - 1n), id + (lsb - 1n)];
}
/**
* Check if the first S2CellID contains the second.
* @param a - the first S2CellID
* @param b - the second S2CellID
* @returns true if a contains b
*/
export function contains(a, b) {
const [min, max] = range(a);
return b >= min && b <= max;
}
/**
* @param a - the first S2CellID
* @param p - the second Point3D
* @returns true if a contains p
*/
export function containsS2Point(a, p) {
const b = fromS2Point(p);
return contains(a, b);
}
/**
* Check if an S2CellID intersects another. This includes edges touching.
* @param a - the first S2CellID
* @param b - the second S2CellID
* @returns true if a intersects b
*/
export function intersects(a, b) {
const [aMin, aMax] = range(a);
const [bMin, bMax] = range(b);
return bMin <= aMax && bMax >= aMin;
}
/**
* Get the next S2CellID in the hilbert space
* @param id - input S2CellID
* @returns the next S2CellID in the hilbert space
*/
export function next(id) {
const n = id + ((id & (~id + 1n)) << 1n);
if (n < K_WRAP_OFFSET)
return n;
return n - K_WRAP_OFFSET;
}
/**
* Get the previous S2CellID in the hilbert space
* @param id - input S2CellID
* @returns the previous S2CellID in the hilbert space
*/
export function prev(id) {
const p = id - ((id & (~id + 1n)) << 1n);
if (p < K_WRAP_OFFSET)
return p;
return p + K_WRAP_OFFSET;
}
/**
* Check if the S2CellID is a leaf value. This means it's the smallest possible cell
* @param id - input S2CellID
* @returns true if the S2CellID is a leaf
*/
export function isLeaf(id) {
return (id & 1n) === 1n;
}
/**
* Given an S2CellID and level (zoom), get the center point of that cell in S-T space
* @param id - the S2CellID
* @returns [face, s, t]
*/
export function centerST(id) {
const [face, i, j] = toIJ(id);
const delta = (id & 1n) !== 0n ? 1 : ((BigInt(i) ^ (id >> 2n)) & 1n) !== 0n ? 2 : 0;
// Note that (2 * {i,j} + delta) will never overflow a 32-bit integer.
const si = 2 * i + delta;
const ti = 2 * j + delta;
return [face, SiTiToST(Number(si)), SiTiToST(Number(ti))];
}
/**
* Given an S2CellID and level (zoom), get the S-T bounding range of that cell
* @param id - the S2CellID
* @param lev - zoom level
* @returns [sMin, tMin, sMax, tMax]
*/
export function boundsST(id, lev) {
if (lev === undefined)
lev = level(id);
const [, s, t] = centerST(id);
const halfSize = sizeST(lev) * 0.5;
return [s - halfSize, t - halfSize, s + halfSize, t + halfSize];
}
/**
* Return the range maximum of a level (zoom) in S-T space
* @param level - zoom level
* @returns sMax or tMax
*/
export function sizeST(level) {
return IJtoST(sizeIJ(level));
}
/**
* Return the range maximum of a level (zoom) in I-J space
* @param level - zoom level
* @returns iMax or jMax
*/
export function sizeIJ(level) {
return 1 << (30 - level);
}
/**
* Given an S2CellID, find the neighboring S2CellIDs
* @param id - the S2CellID
* @returns [up, right, down, left]
*/
export function neighbors(id) {
const lev = level(id);
const size = sizeIJ(lev);
const [face, i, j] = toIJ(id);
return [
parent(fromIJSame(face, i, j - size, j - size >= 0), lev),
parent(fromIJSame(face, i + size, j, i + size < K_MAX_SIZE), lev),
parent(fromIJSame(face, i, j + size, j + size < K_MAX_SIZE), lev),
parent(fromIJSame(face, i - size, j, i - size >= 0), lev),
];
}
/**
* Given a Face-I-J and a desired level (zoom), find the neighboring S2CellIDs
* @param face - the Face
* @param i - the I coordinate
* @param j - the J coordinate
* @param level - the zoom level (desired)
* @returns neighbors: [down, right, up, left]
*/
export function neighborsIJ(face, i, j, level) {
const size = sizeIJ(level);
return [
parent(fromIJSame(face, i, j - size, j - size >= 0), level),
parent(fromIJSame(face, i + size, j, i + size < K_MAX_SIZE), level),
parent(fromIJSame(face, i, j + size, j + size < K_MAX_SIZE), level),
parent(fromIJSame(face, i - size, j, i - size >= 0), level),
];
}
/**
* Build an S2CellID given a Face-I-J, but ensure the face is the same if desired
* @param face - the Face
* @param i - the I coordinate
* @param j - the J coordinate
* @param sameFace - if the face should be the same
* @returns the S2CellID
*/
export function fromIJSame(face, i, j, sameFace) {
if (sameFace)
return fromIJ(face, i, j);
else
return fromIJWrap(face, i, j);
}
/**
* Build an S2CellID given a Face-I-J, but ensure it's a legal value, otherwise wrap before creation
* @param face - the Face
* @param i - the I coordinate
* @param j - the J coordinate
* @returns the S2CellID
*/
export function fromIJWrap(face, i, j) {
const { max, min } = Math;
// Convert i and j to the coordinates of a leaf cell just beyond the
// boundary of this face. This prevents 32-bit overflow in the case
// of finding the neighbors of a face cell.
i = max(-1, min(K_MAX_SIZE, i));
j = max(-1, min(K_MAX_SIZE, j));
// We want to wrap these coordinates onto the appropriate adjacent face.
// The easiest way to do this is to convert the (i,j) coordinates to (x,y,z)
// (which yields a point outside the normal face boundary), and then call
// S2::XYZtoFaceUV() to project back onto the correct face.
//
// The code below converts (i,j) to (si,ti), and then (si,ti) to (u,v) using
// the linear projection (u=2*s-1 and v=2*t-1). (The code further below
// converts back using the inverse projection, s=0.5*(u+1) and t=0.5*(v+1).
// Any projection would work here, so we use the simplest.) We also clamp
// the (u,v) coordinates so that the point is barely outside the
// [-1,1]x[-1,1] face rectangle, since otherwise the reprojection step
// (which divides by the new z coordinate) might change the other
// coordinates enough so that we end up in the wrong leaf cell.
const kScale = 1 / K_MAX_SIZE;
const kLimit = 1 + 2.2204460492503131e-16;
const u = max(-kLimit, min(kLimit, kScale * (2 * (i - K_MAX_SIZE / 2) + 1)));
const v = max(-kLimit, min(kLimit, kScale * (2 * (j - K_MAX_SIZE / 2) + 1)));
// Find the leaf cell coordinates on the adjacent face, and convert
// them to a cell id at the appropriate level.
const [nFace, nU, nV] = XYZtoFaceUV(faceUVtoXYZ(face, u, v));
return fromIJ(nFace, STtoIJ(0.5 * (nU + 1)), STtoIJ(0.5 * (nV + 1)));
}
/**
* Given an S2CellID, find it's nearest neighbors associated with it
* @param id - the S2CellID
* @param lev - the zoom level (if not provided, defaults to current level of id)
* @returns neighbors
*/
export function vertexNeighbors(id, lev) {
if (lev === undefined)
lev = level(id);
const res = [];
const [face, i, j] = toIJ(id);
// Determine the i- and j-offsets to the closest neighboring cell in each
// direction. This involves looking at the next bit of "i" and "j" to
// determine which quadrant of this->parent(level) this cell lies in.
const halfsize = sizeIJ(lev + 1);
const size = halfsize << 1;
let isame, jsame, ioffset, joffset;
if ((i & halfsize) !== 0) {
ioffset = size;
isame = i + size < K_MAX_SIZE;
}
else {
ioffset = -size;
isame = i - size >= 0;
}
if ((j & halfsize) !== 0) {
joffset = size;
jsame = j + size < K_MAX_SIZE;
}
else {
joffset = -size;
jsame = j - size >= 0;
}
res.push(parent(id, lev));
res.push(parent(fromIJSame(face, i + ioffset, j, isame), lev));
res.push(parent(fromIJSame(face, i, j + joffset, jsame), lev));
if (isame || jsame)
res.push(parent(fromIJSame(face, i + ioffset, j + joffset, isame && jsame), lev));
return res;
}
/**
* Returns the four vertices of the cell. Vertices are returned
* in CCW order (lower left, lower right, upper right, upper left in the UV
* plane). The points returned by getVertices are normalized.
* @param id - the S2CellID
* @returns the k-th vertex of the cell
*/
export function getVertices(id) {
return getVerticesRaw(id).map(normalize);
}
/**
* Returns the k-th vertex of the cell (k = 0,1,2,3). Vertices are returned
* in CCW order (lower left, lower right, upper right, upper left in the UV
* plane). The points returned by getVerticesRaw are not normalized.
* @param id - the S2CellID
* @returns the k-th vertex of the cell
*/
export function getVerticesRaw(id) {
const f = face(id);
const [uLow, uHigh, vLow, vHigh] = getBoundUV(id);
return [
faceUVtoXYZ(f, uLow, vLow),
faceUVtoXYZ(f, uHigh, vLow),
faceUVtoXYZ(f, uHigh, vHigh),
faceUVtoXYZ(f, uLow, vHigh),
];
}
/**
* Returns the inward-facing normal of the great circle passing through the
* edge from vertex k to vertex k+1 (mod 4). The normals returned by
* getEdges will be unit length.
* @param id - the S2CellID
* @returns the 4 edges of the cell normalized
*/
export function getEdges(id) {
return getEdgesRaw(id).map(normalize);
}
/**
* Returns the inward-facing normal of the great circle passing through the
* edge from vertex k to vertex k+1 (mod 4). The normals returned by
* getEdgesRaw are not necessarily unit length.
* @param id - the S2CellID
* @returns the 4 edges of the cell
*/
export function getEdgesRaw(id) {
const f = face(id);
const [uLow, uHigh, vLow, vHigh] = getBoundUV(id);
return [
getVNorm(f, vLow),
getUNorm(f, uHigh),
invert(getVNorm(f, vHigh)),
invert(getUNorm(f, uLow)),
];
}
/**
* Return the bounds of this cell in (u,v)-space.
* @param id - the S2CellID
* @returns the bounds [uLow, uHigh, vLow, vHigh]
*/
export function getBoundUV(id) {
const [, i, j] = toIJ(id);
const cellSize = getSizeIJ(id);
const iLow = i & -cellSize;
const jLow = j & -cellSize;
const ijBounds = [iLow, iLow + cellSize, jLow, jLow + cellSize];
return ijBounds.map((n) => STtoUV(IJtoST(n)));
}
/**
* Return the edge length of this cell in (i,j)-space.
* @param id - the S2CellID
* @returns the edge length
*/
export function getSizeIJ(id) {
return 1 << (K_MAX_LEVEL - level(id));
}
//# sourceMappingURL=id.js.map