UNPKG

gis-tools-ts

Version:

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

281 lines 10.5 kB
import { K_MAX_EDGE } from './metrics'; import { K_MAX_LENGTH_2, chordAngFromAngle, chordAngFromLength2, chordAngFromS2Points, chordAngNegativeAngle, chordAngRightAngle, chordAngSin2, chordAngStraightAngle, chordAngToAngle, } from '../s1/chordAngle'; import { idChildren, idContainsS2Point, idFromFace, idGetEdgesRaw, idGetVertices, idLevel, } from '../id'; import { pointCross, pointDot, pointInvert, pointNorm2 } 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 capEmpty(data) { return { center: { x: 1, y: 0, z: 0 }, radius: chordAngNegativeAngle(), data }; } /** * Return a full cap, i.e. a cap that contains all points. * @param data - the data * @returns - the full cap */ export function capFull(data) { return { center: { x: 1, y: 0, z: 0 }, radius: chordAngStraightAngle(), data }; } /** * Return the area of the cap. * @param cap - the cap * @returns - the area */ export function capArea(cap) { return 2 * Math.PI * Math.max(0, capHeight(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 capIsEmpty(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 capIsFull(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 capHeight(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 capFromS1Angle(center, radius, data) { return { center, radius: chordAngFromAngle(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 capFromS1ChordAngle(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 capFromS2Point(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 capRadius(cap) { return chordAngToAngle(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 capContainsS2Point(cap, p) { return chordAngFromS2Points(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 capComplement(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 (capIsFull(cap)) return capEmpty(cap.data); if (capIsEmpty(cap)) return capFull(cap.data); return { center: pointInvert(cap.center), radius: chordAngFromLength2(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 capContainsS2CellVertexCount(cap, cell) { // If the cap does not contain all cell vertices, return false. let count = 0; for (const vertex of idGetVertices(cell)) { if (capContainsS2Point(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 capContainsS2Cell(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 = idGetVertices(cell); for (const vertex of vertices) { if (!capContainsS2Point(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(capComplement(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 capIntersectsS2CellFast(cap, cell) { // If the cap contains any cell vertex, return true. const vertices = idGetVertices(cell); for (const vertex of vertices) { if (capContainsS2Point(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 >= chordAngRightAngle()) return false; // We need to check for empty caps due to the center check just below. if (capIsEmpty(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 = chordAngSin2(cap.radius); const edges = idGetEdgesRaw(cell); for (let k = 0; k < 4; k += 1) { const edge = edges[k]; const dot = pointDot(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 * pointNorm2(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 = pointCross(edge, cap.center); if (pointDot(dir, vertices[k]) < 0 && pointDot(dir, vertices[(k + 1) & 3]) > 0) return true; } return false; } /** * Return the cells that intersect the cap. * @param cap - the cap * @returns - the cells that intersect the cap */ export function capGetIntersectingCells(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 (capIsEmpty(cap)) return res; const queue = [0, 1, 2, 3, 4, 5].map(idFromFace); if (capIsFull(cap)) return queue; const maxDepth = kMaxEdge.getClosestLevel(chordAngToAngle(cap.radius)); while (true) { const cell = queue.pop(); if (cell === undefined) break; const vertexCount = capContainsS2CellVertexCount(cap, cell); const maxLevel = idLevel(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(...idChildren(cell)); } else continue; } else { if (maxLevel) continue; queue.push(...idChildren(cell)); } } return res; } //# sourceMappingURL=cap.js.map