UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

281 lines 10.4 kB
import { K_MAX_EDGE } from './metrics'; import { K_MAX_LENGTH_2, chordAngleSin2, fromAngle, fromLength2, fromS2Points, negativeAngle, rightAngle, straightAngle, toAngle, } from '../s1/chordAngle'; import { children, fromFace, getEdgesRaw, getVertices, containsS2Point as idContainsS2Point, level, } from '../id'; import { invert, cross as s2PointCross, dot as s2PointDot, norm2 as s2PointNorm2, } from '../s2/point'; let kMaxEdge; /** * Return an empty cap, i.e. a cap that contains no points. * @param data - the data * @returns - the empty cap */ export function emptyCap(data) { return { center: [1, 0, 0], radius: negativeAngle(), data }; } /** * Return a full cap, i.e. a cap that contains all points. * @param data - the data * @returns - the full cap */ export function fullCap(data) { // return Init(S2Point.Init(1.0, 0.0, 0.0), S1ChordAngle.Straight(), data_); return { center: [1, 0, 0], radius: straightAngle(), data }; } /** * Return the area of the cap. * @param cap - the cap * @returns - the area */ export function getArea(cap) { return 2 * Math.PI * Math.max(0, height(cap)); } /** * Return true if the cap is empty, i.e. it contains no points. * @param cap - the cap * @returns - true if the cap is empty */ export function isEmpty(cap) { return cap.radius < 0; } /** * Return true if the cap is full, i.e. it contains all points. * @param cap - the cap * @returns - true if the cap is full */ export function isFull(cap) { return cap.radius === 4; } /** * Returns the height of the cap, i.e. the distance from the center point to * the cutoff plane. * @param cap - the cap * @returns - the height */ export function height(cap) { return 0.5 * cap.radius; } /** * Constructs a cap with the given center and radius. A negative radius * yields an empty cap; a radius of 180 degrees or more yields a full cap * (containing the entire sphere). "center" should be unit length. * @param center - the center point * @param radius - the radius * @param data - the data * @returns - the cap */ export function fromS1Angle(center, radius, data) { return { center, radius: fromAngle(radius), data }; } /** * Constructs a cap where the angle is expressed as an S1ChordAngle. This * constructor is more efficient than the one above. * @param center - the center * @param radius - the radius * @param data - the data * @returns - the cap */ export function fromS1ChordAngle(center, radius, data) { return { center, radius, data }; } /** * Convenience function that creates a cap containing a single point. This * method is more efficient that the S2Cap(center, radius) constructor. * @param center - the center * @param data - the data * @returns - an empty cap */ export function fromS2Point(center, data) { return { center, radius: 0, data }; } /** * Return the cap radius as an S1Angle. (Note that the cap angle is stored * internally as an S1ChordAngle, so this method requires a trigonometric * operation and may yield a slightly different result than the value passed * to the (S2Point, S1Angle) constructor.) * @param cap - the cap * @returns - the radius as an S1Angle in radians */ export function radius(cap) { return toAngle(cap.radius); } /** * Returns true if the cap contains the given point. * NOTE: The point "p" should be a unit-length vector. * @param cap - the cap * @param p - the point * @returns - true if the cap contains the point */ export function containsS2Point(cap, p) { return fromS2Points(cap.center, p) <= cap.radius; } /** * Return the complement of the interior of the cap. A cap and its * complement have the same boundary but do not share any interior points. * The complement operator is not a bijection because the complement of a * singleton cap (containing a single point) is the same as the complement * of an empty cap. * @param cap - the cap * @returns - the complement */ export function complement(cap) { // The complement of a full cap is an empty cap, not a singleton. // Also make sure that the complement of an empty cap is full. if (isFull(cap)) return emptyCap(cap.data); if (isEmpty(cap)) return fullCap(cap.data); return { center: invert(cap.center), radius: fromLength2(K_MAX_LENGTH_2 - cap.radius), data: cap.data, }; } /** * Return true if the cap contains the given cell. * @param cap - the cap * @param cell - the cell * @returns - true if the cap contains the cell */ export function containsS2CellVertexCount(cap, cell) { // If the cap does not contain all cell vertices, return false. let count = 0; for (const vertex of getVertices(cell)) { if (containsS2Point(cap, vertex)) count++; } return count; } /** * Return true if the cap contains the given cell. * @param cap - the cap * @param cell - the cell * @returns - true if the cap contains the cell */ export function containsS2Cell(cap, cell) { // If the cap does not contain all cell vertices, return false. // We check the vertices before taking the complement() because we can't // accurately represent the complement of a very small cap (a height // of 2-epsilon is rounded off to 2). const vertices = getVertices(cell); for (const vertex of vertices) { if (!containsS2Point(cap, vertex)) return false; } // Otherwise, return true if the complement of the cap does not intersect // the cell. (This test is slightly conservative, because technically we // want complement().InteriorIntersects() here.) return !intersectsS2Cell(complement(cap), cell, vertices); } /** * Return true if the cap intersects "cell", given that the cap does intersect * any of the cell vertices or edges. * @param cap - the cap * @param cell - the cell * @returns - true if the cap intersects the cell */ export function intersectsS2CellFast(cap, cell) { // If the cap contains any cell vertex, return true. const vertices = getVertices(cell); for (const vertex of vertices) { if (containsS2Point(cap, vertex)) return true; } return intersectsS2Cell(cap, cell, vertices); } /** * Return true if the cap intersects "cell", given that the cap does contain * any of the cell vertices (supplied in "vertices", an array of length 4). * Return true if this cap intersects any point of 'cell' excluding its * vertices (which are assumed to already have been checked). * @param cap - the cap * @param cell - the cell * @param vertices - the vertices of the cell * @returns - true if the cap intersects the cell */ export function intersectsS2Cell(cap, cell, vertices) { // If the cap is a hemisphere or larger, the cell and the complement of the // cap are both convex. Therefore since no vertex of the cell is contained, // no other interior point of the cell is contained either. if (cap.radius >= rightAngle()) return false; // We need to check for empty caps due to the center check just below. if (isEmpty(cap)) return false; // Optimization: return true if the cell contains the cap center. (This // allows half of the edge checks below to be skipped.) if (idContainsS2Point(cell, cap.center)) return true; // At this point we know that the cell does not contain the cap center, // and the cap does not contain any cell vertex. The only way that they // can intersect is if the cap intersects the interior of some edge. const sin2Angle = chordAngleSin2(cap.radius); const edges = getEdgesRaw(cell); for (let k = 0; k < 4; k += 1) { const edge = edges[k]; const dot = s2PointDot(cap.center, edge); if (dot > 0) { // The center is in the interior half-space defined by the edge. We don't // need to consider these edges, since if the cap intersects this edge // then it also intersects the edge on the opposite side of the cell // (because we know the center is not contained with the cell). continue; } // The Norm2() factor is necessary because "edge" is not normalized. if (dot * dot > sin2Angle * s2PointNorm2(edge)) { return false; // Entire cap is on the exterior side of this edge. } // Otherwise, the great circle containing this edge intersects // the interior of the cap. We just need to check whether the point // of closest approach occurs between the two edge endpoints. const dir = s2PointCross(edge, cap.center); if (s2PointDot(dir, vertices[k]) < 0 && s2PointDot(dir, vertices[(k + 1) & 3]) > 0) return true; } return false; } /** * @param cap - the cap * @returns - the cells that intersect the cap */ export function getIntersectingCells(cap) { if (kMaxEdge === undefined) kMaxEdge = K_MAX_EDGE(); const res = []; // Find appropriate max depth for radius // while loop: // - if cell corners are all in cap, store in res. // - if cell corners are all outside cap, move on. // - if even one cell corner is outside cap: // - - if reached max depth, store in res // - - if not reached max depth, store children in cache for another pass if (isEmpty(cap)) return res; const queue = [0, 1, 2, 3, 4, 5].map(fromFace); if (isFull(cap)) return queue; const maxDepth = kMaxEdge.getClosestLevel(toAngle(cap.radius)); while (true) { const cell = queue.pop(); if (cell === undefined) break; const vertexCount = containsS2CellVertexCount(cap, cell); const maxLevel = level(cell) >= maxDepth; if (vertexCount === 4 || (vertexCount > 0 && maxLevel)) { res.push(cell); } else if (vertexCount === 0 && !maxLevel) { // if cap center is in the cell, then we check all children because the cell is larger than the cap if (idContainsS2Point(cell, cap.center)) { queue.push(...children(cell)); } else continue; } else { if (maxLevel) continue; queue.push(...children(cell)); } } return res; } //# sourceMappingURL=cap.js.map