UNPKG

gis-tools-ts

Version:

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

206 lines 9.31 kB
import { InterPointLookup, PolyPath, RingChunk, bboxArea, bboxInside, buildPathsAndChunks, equalPoints, mergeBBoxes, mergeIntersectionPairs, polygonRingArea, polygonsIntersectionsLookup, polylineInPolyline, } from '../../../index.js'; /** * Given a collection of polygons, if any of the polygons are kinked, dekink them * * NOTE: This algorithm assumes the ring order is correct. Outer rings must be counter-clockwise and * inner rings must be clockwise * @param polygons - the polygons are from either a VectorFeature, VectorPolygonGeometry, or raw VectorPolygon * @returns - the dekinked polygons */ export function dekinkPolygons(polygons) { const vectorPolygons = 'geometry' in polygons ? polygons.geometry.coordinates : 'coordinates' in polygons ? polygons.coordinates : polygons; const res = { type: 'MultiPolygon', coordinates: [], is3D: false }; for (const vectorPoly in vectorPolygons) { const dekinked = dekinkPolygon(vectorPolygons[vectorPoly]); if (dekinked !== undefined) { if (dekinked.bbox !== undefined) res.bbox = mergeBBoxes(res.bbox, dekinked.bbox); res.coordinates.push(...dekinked.coordinates); } } return res; } /** * Given a polygon, if the polygon is kinked, dekink it * * NOTE: This algorithm assumes the ring order is correct. Outer rings must be counter-clockwise and * inner rings must be clockwise * @param polygon - the polygon as either a VectorFeature, VectorPolygonGeometry, or raw VectorPolygon * @returns - the dekinked polygon */ export function dekinkPolygon(polygon) { const vectorPolygon = 'geometry' in polygon ? polygon.geometry.coordinates : 'coordinates' in polygon ? polygon.coordinates : polygon; // not enough data, just return undefined if (vectorPolygon.length === 0) return undefined; const vectorPolygons = [vectorPolygon]; // 1) build intersections `[polyIndex][ringIndex] -> Intersections`. Store where on the ring other rings intersect const ringIntLookup = polygonsIntersectionsLookup(vectorPolygons, (seg1) => { return (seg2) => // if same id ignore seg2.id !== seg1.id && // only pass forward not backward seg2.id > seg1.id && // TODO: At some point intersections of inner rings against the outer ring should be considered. // For now the ringIndex must be the same, polygonsIntersectionsLookup should return // two problem sets down the road, one for cleaning individual rings, and one for fixing holes that go out of bounds seg2.ringIndex === seg1.ringIndex; }); // 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, _, chunks, intLookup] = buildPathsAndChunks(vectorPolygons, ringIntLookup); // 3) Consume chunks into PolyPaths // If no intersections for the polyIndex+RingIndex -> push as completed ring buildPathsFromChunks(paths, intLookup, chunks); // 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 intersections - all intersections * @param chunks - a set of chunks */ function buildPathsFromChunks(paths, intersections, chunks) { const ringStore = {}; // store existing paths and reset. for (const { outer, polysConsumed, bbox, holes } of paths) { const polyIndex = [...polysConsumed][0]; const polyStore = (ringStore[polyIndex] ??= []); if (outer !== undefined) polyStore.push({ lineString: outer, isCCW: true, isHole: false, bbox, area: bboxArea(bbox) }); for (const hole of holes) polyStore.push({ lineString: hole, isCCW: false, isHole: true, bbox, area: bboxArea(bbox) }); } paths.splice(0, paths.length); // 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; let currChunk = chunk; const lineString = [{ ...start }]; let bbox = currChunk.bbox; while (true) { if (currChunk.visted) break; currChunk.visted = true; lineString.push(...currChunk.mid); bbox = mergeBBoxes(bbox, currChunk.bbox); if (currChunk.next === undefined) break; const { chunk: nextChunk, intPoint } = currChunk.next; lineString.push({ ...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; const isHole = chunk.ringIndex !== 0; // store in correct location const polyStore = (ringStore[chunk.polyIndex] ??= []); polyStore.push({ lineString, isCCW, isHole, bbox, area: bboxArea(bbox) }); } // For each ringStore, build out polys and store them in paths for (const rings of Object.values(ringStore)) ringSetToPaths(paths, rings); } /** * Convert a set of rings into a set of paths * @param paths - the collection of paths to store the results in * @param ringSet - the current set of rings re-built from a polygon */ function ringSetToPaths(paths, ringSet) { if (ringSet.length === 0) return; // sort by bbox area desc ringSet.sort((a, b) => b.area - a.area); // filter by case type const outers = ringSet.filter((r) => !r.isHole && r.isCCW); const outersMaybeHole = ringSet.filter((r) => !r.isHole && !r.isCCW); const holes = ringSet.filter((r) => r.isHole && !r.isCCW); // const holesMaybeHole = ringSet.filter((r) => r.isHole && r.isCCW); const actualOuters = []; // store all true outers. We know the first outer is the largest original outer ring. // The future outers are holes either kink inside or outside the original. Only store kinks that are outside the original if (outers.length > 0) { // store the first one const { lineString: firstLink, bbox: firstBBox } = outers[0]; actualOuters.push(new PolyPath(firstLink, new Set(), true, firstBBox)); // check all the others for (const outer of outers.slice(1)) { if (bboxInside(outer.bbox, firstBBox) && polylineInPolyline(outer.lineString, firstLink)) continue; actualOuters.push(new PolyPath(outer.lineString, new Set(), true, outer.bbox)); } } // If outer in `outersMaybeHole` is inside an actual outer, it's a hole; Otherwise it's another outer for (const { lineString, bbox } of outersMaybeHole) { let found = false; for (const actualOuter of actualOuters) { if (bboxInside(bbox, actualOuter.bbox) && polylineInPolyline(lineString, actualOuter.outer)) { // store the hole in this outer actualOuter.holes.push(lineString); found = true; break; } } if (found) continue; // otherwise, it's a new outer actualOuters.push(new PolyPath(lineString.reverse(), new Set(), true, bbox)); } // now organize holes if (actualOuters.length !== 0) { for (const { lineString, bbox } of holes) { if (actualOuters.length === 1) { actualOuters[0].holes.push(lineString); } else { // find the outer this hole belongs to for (const actualOuter of actualOuters) { if (bboxInside(bbox, actualOuter.bbox) && polylineInPolyline(lineString, actualOuter.outer)) { // store the hole in this outer actualOuter.holes.push(lineString); break; } } } } } // Now store all actualOuters we built paths.push(...actualOuters); } //# sourceMappingURL=dekink.js.map