gis-tools-ts
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
255 lines • 10.2 kB
JavaScript
import { equalLines, equalPoints, fromLineString, mergeBBoxes } from '../../../index.js';
/** Reconstructing a poly line that interacts with intersections */
export class PolyPath {
id = 0; // helps down the road to spot duplicate pulls of this Path
outer;
oldOuters = [];
holes = [];
polysConsumed = new Set(); // indexes of the polygons in the multipolygon. So we can quickly consume holes.
bbox;
// eslint-disable-next-line jsdoc/require-jsdoc
constructor(ring, polysConsumed, outer, bbox) {
if (outer)
this.outer = ring;
else
this.holes.push(ring);
this.polysConsumed = polysConsumed;
this.bbox = bbox ?? fromLineString(ring);
}
// eslint-disable-next-line jsdoc/require-jsdoc
getPath() {
if (this.outer === undefined)
return undefined;
if (this.outer.length < 4)
return undefined;
const res = [this.outer];
for (const hole of this.holes) {
if (hole.length < 4)
continue;
res.push(hole);
}
return res;
}
}
/** A path/piece/chunk from a polygon */
export class RingChunk {
polyIndex;
ringIndex;
bbox;
mid;
from;
to;
fromAngle;
toAngle;
visted = false;
next; // used in final step, to link all chunks together.
// eslint-disable-next-line jsdoc/require-jsdoc
constructor(polyIndex, ringIndex, bbox, mid, // Always starts with either the beginning of the poly ring OR an intersection point.
from, to, fromAngle, toAngle) {
this.polyIndex = polyIndex;
this.ringIndex = ringIndex;
this.bbox = bbox;
this.mid = mid;
this.from = from;
this.to = to;
this.fromAngle = fromAngle;
this.toAngle = toAngle;
}
// eslint-disable-next-line jsdoc/require-jsdoc
equalChunk(other) {
return (this.ringIndex > 0 === other.ringIndex > 0 &&
equalPoints(this.from, other.from) &&
equalPoints(this.to, other.to) &&
equalLines(this.mid, other.mid));
}
}
/** Intersection Lookup for chunks */
export class InterPointLookup {
lookup = {};
// eslint-disable-next-line jsdoc/require-jsdoc
get(point) {
return ((this.lookup[point.x] ??= {})[point.y] ??= { point, from: [], to: [] });
}
// eslint-disable-next-line jsdoc/require-jsdoc
linkInts(polyIndex, ringIndex, from, to, mid, fromAngle, toAngle) {
// first build a chunk
const bbox = mergeBBoxes(fromLineString(mid), fromLineString([from, to]));
fromAngle = fromAngle ?? angle(mid.at(-1) ?? from, to);
toAngle = toAngle ?? angle(mid.at(0) ?? to, from);
const chunk = new RingChunk(polyIndex, ringIndex, bbox, mid, from, to, fromAngle, toAngle);
this.get(from).to.push(chunk);
this.get(to).from.push(chunk);
return chunk;
}
}
/**
* Build the PolyPaths and RingChunks
* @param vectorPolygons - the collection of polygons
* @param ringIntersectLookup - the ring intersection lookup for all rings in the multipolygon collection
* @returns - the PolyPaths, their lookups, and RingChunks
*/
export function buildPathsAndChunks(vectorPolygons, ringIntersectLookup) {
// Setup result. Paths are the final structure of joined polygons.
const paths = [];
// Lookup is a helper for quickly finding paths in the future
const pathLookup = new Map();
// Track all bboxes for all outer-rings
const outerRingBBoxes = new Array(vectorPolygons.length);
// 2) Build Poly Pieces
// If no intersections for the polyIndex+RingIndex -> push as completed ring (into paths)
const chunks = [];
const intLookup = new InterPointLookup();
for (let pI = 0; pI < vectorPolygons.length; pI++) {
const poly = vectorPolygons[pI];
for (let rI = 0; rI < poly.length; rI++) {
const ring = poly[rI].map((point) => ({ ...point }));
let intersections = ringIntersectLookup.get(pI, rI);
// Case 1: Insert into paths because it's already completed or expand existing path
if (intersections.length === 0) {
const existingPath = pathLookup.get(pI);
if (existingPath === undefined) {
const path = new PolyPath(ring, new Set([pI]), rI === 0);
if (rI === 0)
outerRingBBoxes[pI] = path.bbox;
pathLookup.set(pI, path);
paths.push(path);
}
else {
if (rI === 0) {
existingPath.outer = ring;
existingPath.bbox = mergeBBoxes(existingPath.bbox, fromLineString(ring));
outerRingBBoxes[pI] = existingPath.bbox;
}
else
existingPath.holes.push(ring);
}
continue;
}
// Case 2: Handle the intersections and build RingChunks
if (rI === 0)
outerRingBBoxes[pI] = fromLineString(ring);
intersections = intersections.filter((i) => i.t !== 0);
let currIndex = 0;
let intIndex = 0;
let curInt = intersections.at(intIndex);
while (currIndex < ring.length - 1) {
// if we are still working with intersections, build points with them
if (curInt !== undefined) {
// until we get to the next intersection, we link the points
if (currIndex !== curInt.from) {
const start = currIndex;
while (currIndex !== curInt.from)
currIndex++;
const mid = ring.slice(start + 1, currIndex);
chunks.push(intLookup.linkInts(pI, rI, ring[start], ring[currIndex], mid));
}
// now build links with the intersections until we get to the next intersection that isn't the same index
let from = ring[currIndex];
while (curInt !== undefined && curInt.from === currIndex) {
if (!equalPoints(from, curInt.point)) {
// NOTE: For robustness, we have to store the angles we found when studying the intersections.
// We make decisions about the polygons during the analysis of the intersections using
// robust predicates. otherwise we would actually compute slightly different angles
// that could percieve the intersection lines as swapped (non-existent).
const ang = curInt.tAngle;
chunks.push(intLookup.linkInts(pI, rI, from, curInt.point, [], invertAngle(ang), ang));
}
intIndex++;
from = curInt.point;
curInt = intersections.at(intIndex);
}
// if the intersection t is not 1, then we need to link the point to the end of the currIndex
const next = ring[currIndex + 1];
if (!equalPoints(from, next)) {
const { tAngle } = intersections[intIndex - 1];
chunks.push(intLookup.linkInts(pI, rI, from, next, [], invertAngle(tAngle), tAngle));
}
}
else {
// no intersection, just build the point
chunks.push(intLookup.linkInts(pI, rI, ring[currIndex], ring[currIndex + 1], []));
}
currIndex++;
}
}
}
// sort chunks by left then bottom for the eventual final run through
chunks.sort((a, b) => {
let diff = a.bbox[0] - b.bbox[0];
if (diff === 0)
diff = a.bbox[1] - b.bbox[1];
return diff;
});
return [paths, pathLookup, chunks, intLookup, outerRingBBoxes];
}
/**
* Given an of intersection, find the best way to connect the from->to chunks
* @param intersection - the intersection to analyze
*/
export function mergeIntersectionPairs(intersection) {
const { from, to, point: intPoint } = intersection;
if (from.length === 0 || to.length === 0)
return;
if (from.length === 1 && to.length === 1) {
// connect the two chunks and move on
from[0].next = { chunk: to[0], intPoint };
return;
}
// remove "duplicate"/"same" chunks
const froms = [];
for (const c of from) {
if (c.visted)
continue;
if (!froms.some((r) => r.equalChunk(c)))
froms.push(c);
else
c.visted = true;
}
const tos = [];
for (const c of to) {
if (c.visted)
continue;
if (!tos.some((r) => r.equalChunk(c)))
tos.push(c);
else
c.visted = true;
}
const pairs = [];
for (const f of froms) {
for (const t of tos) {
const angle = t.toAngle - f.fromAngle;
pairs.push({ from: f, to: t, angle: angle < 0 ? angle + Math.PI * 2 : angle });
}
}
pairs.sort((a, b) => a.angle - b.angle);
for (const { from, to } of pairs) {
if (from.visted || to.visted)
continue;
from.next = { chunk: to, intPoint };
from.visted = true;
to.visted = true;
}
// cleanup visited
for (const f of froms)
f.visted = false;
for (const t of tos)
t.visted = false;
}
/**
* Returns the absolute angle between points A->B->C
* @param a - First point
* @param b - Second Point
* @returns Angle in degrees [-PI, PI]
*/
function angle(a, b) {
return Math.atan2(a.y - b.y, a.x - b.x);
}
/**
* Returns the absolute angle between points A->B->C
* @param angle - Angle in degrees [-PI, PI]
* @returns Angle in degrees [-PI, PI]
*/
function invertAngle(angle) {
return angle >= 0 ? angle - Math.PI : angle + Math.PI;
}
//# sourceMappingURL=pathBuilder.js.map