gis-tools-ts
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
218 lines • 9.62 kB
JavaScript
import { PolyPath, bboxInside, buildPathsAndChunks, equalPoints, mergeBBoxes, mergeIntersectionPairs, polygonRingArea, polygonsIntersectionsLookup, } from '../../../index.js';
/**
* Given a collection of polygons, if any of the polygons interact, merge them as a union
* @param polygons - the polygons are from either a VectorFeature, VectorPolygonGeometry, or raw VectorPolygon
* @returns - a union of polygons should a union exist.
*/
export function polygonsUnion(polygons) {
const vectorPolygons = 'geometry' in polygons
? polygons.geometry.coordinates
: 'coordinates' in polygons
? polygons.coordinates
: polygons;
// not enough data, just clone
if (vectorPolygons.length === 0)
return undefined;
// 1) build intersections `[polyIndex][ringIndex] -> Intersections`. Store where on the ring other rings intersect
const ringIntersectLookup = polygonsIntersectionsLookup(vectorPolygons);
// 2) Build Poly Pieces
// Setup result paths with chunks that are the final structure of joined polygons.
// Lookup is a helper for quickly finding the right path in the future as paths can consume multiple polygons
// If no intersections for the polyIndex+RingIndex -> it's immediately consumed into paths. Otherwise it's a chunk
const [paths, pathLookup, chnks, ints, bboxes] = buildPathsAndChunks(vectorPolygons, ringIntersectLookup);
// 3) Consume chunks into PolyPaths
// If no intersections for the polyIndex+RingIndex -> push as completed ring
buildPathsFromChunks(paths, pathLookup, ints, chnks, bboxes);
// 4) Convert PolyPaths into the resultant MultiPolygon
let bbox;
for (const { outer, bbox: pathBBox } of paths) {
if (outer === undefined)
continue;
bbox = mergeBBoxes(bbox, pathBBox);
}
const coordinates = paths
.map((p) => p.getPath())
.filter((p) => p !== undefined);
if (coordinates.length === 0)
return undefined;
return { type: 'MultiPolygon', coordinates, is3D: false, bbox };
}
/**
* Given a set of chunks, build a set of paths
* @param paths - a set of paths to add to
* @param pathLookup - a lookup of existing paths
* @param intersections - all intersections
* @param chunks - a set of chunks
* @param bboxes - the bboxes of all polygons outer rings
*/
function buildPathsFromChunks(paths, pathLookup, intersections, chunks, bboxes) {
// merge in all potential "dead" outer-rings (do we need this?)
for (const path of paths)
storeInnerOldOuters(path, bboxes);
// for each intersections, connect all the from and to, smallest angle between from->to first slowly work your way through
for (const xs of Object.values(intersections.lookup)) {
for (const ys of Object.values(xs))
mergeIntersectionPairs(ys);
}
// run through all chunks, if unvisited, add to paths
for (const chunk of chunks) {
if (chunk.visted)
continue;
const start = chunk.from;
// console.log(`START: `, start);
let currChunk = chunk;
const foundPolygons = new Set();
const lineString = [{ ...start }];
let bbox = currChunk.bbox;
while (true) {
if (currChunk.visted)
break;
currChunk.visted = true;
if (currChunk.mid.length !== 0) {
// console.log(`ADD MID:`, currChunk.mid);
}
lineString.push(...currChunk.mid);
foundPolygons.add(currChunk.polyIndex);
bbox = mergeBBoxes(bbox, currChunk.bbox);
if (currChunk.next === undefined)
break;
const { chunk: nextChunk, intPoint } = currChunk.next;
lineString.push({ ...intPoint });
// console.log(`ADD INT:`, intPoint);
currChunk = nextChunk;
if (equalPoints(intPoint, start))
break;
}
const area = polygonRingArea(lineString, 1);
if (area === 0 || lineString.length < 4 || !equalPoints(lineString.at(0), lineString.at(-1)))
continue;
// now build the path or add to an existing path
const isCCW = area > 0;
// Find the correct PolyPath to insert into, otherwise create a new one, update the lookup to
// include all new polygon indexes used in the path
let foundPaths = [];
// Pull in all the old paths to merge with this one (may expand upon multiple paths, consume the holes)
let currID = 0;
for (const polyIndex of foundPolygons) {
const path = pathLookup.get(polyIndex);
if (path !== undefined) {
path.id = currID++;
foundPaths.push(path);
}
}
// filter foundPaths if they have the same id as the previous
foundPaths = foundPaths
.sort((p1, p2) => p1.id - p2.id)
.filter((p, i) => i === 0 || p.id !== foundPaths[i - 1]?.id);
let path;
if (foundPaths.length === 0) {
path = new PolyPath(lineString, foundPolygons, isCCW, bbox);
paths.push(path);
}
else {
// TODO: `chunk.ringIndex !== 0` may not be enough as may contain a hole chunk but started as an outer chunk
// if only one found, update that one, otherwise create a new merged path and store the new result
path = foundPaths.length === 1 ? foundPaths[0] : mergePaths(foundPaths);
addChunkToPath(path, lineString, foundPolygons, bbox, isCCW, chunk.ringIndex !== 0);
}
// Store all inner outer rings that have not yet been consumed by the new outer but are inside the new outer
storeInnerOldOuters(path, bboxes);
// All found polyIndex references now point to the new path
for (const polyIndex of foundPolygons)
pathLookup.set(polyIndex, path);
}
// TODO: Poly's may still be able to consume eachother
}
/**
* Add a chunks built into a line+bbox to a path
* @param path - the path to add to
* @param ring - the linestring to add
* @param polyIndexes - all polygon indexes touched
* @param bbox - the bounding box of the collection of chunks (ring)
* @param isCCW - whether the ring is CCW
* @param wasHole - whether the ring is a hole
*/
function addChunkToPath(path, ring, polyIndexes, bbox, isCCW, wasHole) {
path.polysConsumed = path.polysConsumed.union(polyIndexes);
// If one poly outer ring is entirely in another poly AND its CCW, it gets "consumed" (deleted. path is
// because of the ordering, the first chunk to be an outer will be the one creating the path,
// so we know all future CCW chunks that share a path will be "outers" that are inside the existing
// path outer)
// If one poly outer ring is entirely in another poly AND its CW, it converts to a hole
// If one poly inner ring is CW, it gets consumed by an associated outer
// If one poly inner ring is CCW, remove it
// If a hole is found, it didn't come from a hole, and it's inside one of the old outer's bboxes, delete the pair.
if (isCCW) {
if (wasHole)
return;
if (path.outer === undefined) {
path.outer = ring;
path.bbox = mergeBBoxes(path.bbox, bbox);
}
else {
// If the ring's bbox is smaller than the existing outer, store. Otherwise replace
if (bboxInside(bbox, path.bbox)) {
path.oldOuters.push(bbox);
}
else {
path.oldOuters.push(path.bbox);
path.outer = ring;
path.bbox = mergeBBoxes(path.bbox, bbox);
}
}
}
else {
if (!wasHole) {
// Store discarded smaller outer rings, if hole is inside inner outer-ring, it cancels out the hole
for (const oldOuter of path.oldOuters) {
if (bboxInside(bbox, oldOuter))
return;
}
}
path.holes.push(ring);
}
}
/**
* Store all inner old outers that have not yet been consumed by the new outer
* @param path - the path
* @param bboxes - the bboxes of all outer rings we are merging
*/
function storeInnerOldOuters(path, bboxes) {
if (path.outer === undefined)
return;
const { oldOuters, polysConsumed } = path;
for (let i = 0; i < bboxes.length; i++) {
if (polysConsumed.has(i))
continue;
const bbox = bboxes[i];
if (bboxInside(bbox, path.bbox))
oldOuters.push(bbox);
}
}
/**
* Merge in a collection of paths
* @param pathsToMerge - the collection of paths
* @returns - the result of all paths merged into one
*/
function mergePaths(pathsToMerge) {
const res = pathsToMerge[0];
for (let i = 1; i < pathsToMerge.length; i++) {
const other = pathsToMerge[i];
// If this bbox is smaller than the existing outer, replace
if (other.outer !== undefined && bboxInside(res.bbox, other.bbox)) {
if (res.outer !== undefined)
res.oldOuters.push(res.bbox);
res.outer = other.outer;
}
res.holes.push(...other.holes);
res.oldOuters.push(...other.oldOuters);
res.polysConsumed = res.polysConsumed.union(other.polysConsumed);
res.bbox = mergeBBoxes(res.bbox, other.bbox);
// clear the path now that we comsumed it
other.outer = undefined;
other.holes = [];
other.oldOuters = [];
}
return res;
}
//# sourceMappingURL=union.js.map