UNPKG

gis-tools-ts

Version:

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

260 lines 11.8 kB
import { BoxIndex, equalPoints, intersectionOfSegmentsRobust, nextDown, nextUp, } from '../../../index.js'; /** Intersection Lookup for mapped by polyIndex and ringIndex */ export class RingIntersectionLookup { /** [polyIndex][ringIndex] -> Intersections */ store = new Map(); // eslint-disable-next-line jsdoc/require-jsdoc get(polyIndex, ringIndex) { return this.store.get(polyIndex)?.get(ringIndex) ?? []; } // eslint-disable-next-line jsdoc/require-jsdoc set(polyIndex, ringIndex, int) { let poly = this.store.get(polyIndex); if (poly === undefined) this.store.set(polyIndex, (poly = new Map())); let ring = poly.get(ringIndex); if (ring === undefined) poly.set(ringIndex, (ring = [])); ring.push(int); } } /** * Find the intersection of a collection of polygons * @param polygons - the collection of polygons * @param includeSelfIntersections - if true, include self intersections * @returns - found intersections */ export function polygonsIntersections(polygons, includeSelfIntersections = false) { const res = []; // setup accessing data const vectorPolygons = 'geometry' in polygons ? polygons.geometry.coordinates : 'coordinates' in polygons ? polygons.coordinates : polygons; // build all segments const segments = buildPolygonSegments(vectorPolygons); /** * Setup a function for accessing the minX, minY, maxX, and maxY properties of the items. * @param segment - the segment * @returns - the minX, minY, maxX, and maxY */ const getBounds = (segment) => { const { min, max } = Math; const { polyIndex, ringIndex, from, to } = segment; const { x: fromX, y: fromY } = vectorPolygons[polyIndex][ringIndex][from]; const { x: toX, y: toY } = vectorPolygons[polyIndex][ringIndex][to]; return [min(fromX, toX), min(fromY, toY), max(fromX, toX), max(fromY, toY)]; }; // setup a 2D box index const boxIndex = new BoxIndex(segments, getBounds); // iterate each segment and check for intersections with other segments for (const segment1 of segments) { const potentialIntersections = boxIndex.search(...getBounds(segment1), (seg) => seg.id !== segment1.id && // if self-intersections are not included skip all segments from the same polyIndex // otherwise skip all segments from the same ringIndex whose end points interact (!includeSelfIntersections ? seg.polyIndex !== segment1.polyIndex : seg.ringIndex !== segment1.ringIndex || (seg.from !== segment1.from && seg.to !== segment1.to && seg.to !== segment1.from && seg.from !== segment1.to)) && seg.id > segment1.id); for (const segment2 of potentialIntersections) { const intP = findPolygonIntersections(vectorPolygons, segment1, segment2); if (intP !== undefined) res.push({ segment1, segment2, point: intP.point, u: intP.u, t: intP.t }); } } return res; } /** * Run through the vectorPolygons and Builds the ring intersection lookup * @param vectorPolygons - the collection of polygons * @param segmentFilter - the function to filter the segments, default ignores self intersections * @returns - the ring intersection lookup for all rings in the multipolygon collection */ export function polygonsIntersectionsLookup(vectorPolygons, segmentFilter) { const segments = buildPolygonSegments(vectorPolygons); const ringIntersectLookup = new RingIntersectionLookup(); if (segmentFilter === undefined) { /** * Default segment filter * @param seg1 - the first segment * @returns - filter on the second segment */ segmentFilter = (seg1) => { return (seg2) => // if same id ignore seg2.id !== seg1.id && // only pass forward not backward seg2.id > seg1.id && // if same polyIndex ignore seg2.polyIndex !== seg1.polyIndex; }; } /** * Setup a function for accessing the minX, minY, maxX, and maxY properties of the items. * @param segment - the segment * @returns - the minX, minY, maxX, and maxY */ const getBounds = (segment) => { const { min, max } = Math; const { polyIndex, ringIndex, from, to } = segment; const { x: fromX, y: fromY } = vectorPolygons[polyIndex][ringIndex][from]; const { x: toX, y: toY } = vectorPolygons[polyIndex][ringIndex][to]; return [min(fromX, toX), min(fromY, toY), max(fromX, toX), max(fromY, toY)]; }; // setup a 2D box index const boxIndex = new BoxIndex(segments, getBounds); // iterate each segment and check for intersections with other segments for (const segment1 of segments) { const { from: s1f, to: s1t, polyIndex: s1pi, ringIndex: s1ri } = segment1; const potentialIntersections = boxIndex.search(...getBounds(segment1), segmentFilter(segment1)); for (const segment2 of potentialIntersections) { const { from: s2f, to: s2t, polyIndex: s2pi, ringIndex: s2ri } = segment2; const pInt = findPolygonIntersections(vectorPolygons, segment1, segment2); // ignore points that interact at their edges if both segments leaving or coming if (pInt !== undefined) { // NOTE: It's important both segments share the same point as it may be updated const { point, u, t, uVec, tVec, uAngle, tAngle } = pInt; // skip if u and t are equal if (u === t && (u === 0 || u === 1)) continue; // first segment intersection const uInt = { from: s1f, to: s1t, point, t: u, tVec: uVec, tAngle: uAngle }; ringIntersectLookup.set(s1pi, s1ri, uInt); // second segment intersection const tInt = { from: s2f, to: s2t, point, t, tVec, tAngle }; ringIntersectLookup.set(s2pi, s2ri, tInt); } } } // finally clean the intersections before return for (const [_, polys] of ringIntersectLookup.store) { for (const [ringKey, intersections] of polys) { polys.set(ringKey, cleanIntersections(intersections)); } } return ringIntersectLookup; } /** * Build all segments * @param vectorPolygons - the collection of polygons * @returns - the collection of segments */ export function buildPolygonSegments(vectorPolygons) { const segments = []; for (let p = 0; p < vectorPolygons.length; p++) { const polygon = vectorPolygons[p]; for (let r = 0; r < polygon.length; r++) { const ring = polygon[r]; for (let s = 0; s < ring.length - 1; s++) { segments.push({ id: segments.length, polyIndex: p, ringIndex: r, from: s, to: s + 1 }); } } } return segments; } /** * Find the intersection of two segments if it exists * @param vectorPolygons - the collection of polygons * @param segment1 - the first segment * @param segment2 - the second segment * @returns - the intersection if it exists. Undefined otherwise. */ export function findPolygonIntersections(vectorPolygons, segment1, segment2) { const p1 = vectorPolygons[segment1.polyIndex][segment1.ringIndex][segment1.from]; const p2 = vectorPolygons[segment1.polyIndex][segment1.ringIndex][segment1.to]; const q1 = vectorPolygons[segment2.polyIndex][segment2.ringIndex][segment2.from]; const q2 = vectorPolygons[segment2.polyIndex][segment2.ringIndex][segment2.to]; return intersectionOfSegmentsRobust([p1, p2], [q1, q2], segment1.polyIndex === segment2.polyIndex && segment1.ringIndex === segment2.ringIndex); } /** * Given a ring's of intersections, clean them up * @param intersections - a collection of intersections to clean up * @returns - the cleaned up intersections */ export function cleanIntersections(intersections) { if (intersections.length === 0) return []; intersections.sort((a, b) => { let diff = a.from - b.from; if (diff === 0) diff = a.t - b.t; return diff; }); // 1) Remove duplicates const dedupInts = []; for (const int of intersections) { if (dedupInts.some((c) => c.from === int.from && c.t === int.t && equalPoints(c.point, int.point))) continue; dedupInts.push(int); } // 2) Cancel out any intersections with other rings we only touch once with a single point if (dedupInts.length === 2) { const [first, second] = dedupInts; if ((first.t === 0 || first.t === 1) && (second.t === 0 || second.t === 1) && equalPoints(first.point, second.point)) { return []; } } // 3) Intersections whose t values are not 0 or 1 but are equal to the start, end, or other // intersections with different t values need to be shifted by the smallest float possible to ensure // it doesn't conflict but on the line segment. updateIntersectionPoints(dedupInts); return dedupInts; } /** * Update all intersection points to ensure they are not equal to the start or end points if their t * values are not 0 or 1. * * When there is an intersection that the resultant point is equal to one of the segment edges, * then we shift the point by the smallest float possible. * * NOTE, If we have two or more points that are equal to one of the segment edges BUT the t values * are barely different, we need to keep shifting forward as needed. * * NOTE, What if we have TWO points that are equal to one of the segment edges BUT the t values * are different? We need to shift again as needed. There are also cases where two different lines * intersect another line and the resultant intersection is the same point but the t value along * the line is different. * * TODO: There are some corner cases I definitely haven't covered. Like a shift pushes an intersection into another intersection * @param intersections - the collection of intersections */ function updateIntersectionPoints(intersections) { const starts = []; const ends = []; for (let i = 1; i < intersections.length; i++) { const int = intersections[i]; const prev = intersections[i - 1]; if (int.from !== prev.from || int.t === 0 || int.t === 1 || prev.t === 0 || prev.t === 1) continue; if (i !== 0 && equalPoints(int.point, prev.point) && int.t !== prev.t) { // because they are sorted by t, starts we want to inc "forward" the NEXT one; ends we want to dec "back" the PREVIOUS one if (int.t <= 0.5) starts.push(int); else ends.push(prev); } } // Choose direction as further away from the end point it's closer to. for (let i = 0; i < starts.length; i++) { const { point, tVec } = starts[i]; if (tVec.x !== 0) point.x = tVec.x > 0 ? nextUp(point.x, i + 1) : nextDown(point.x, i + 1); if (tVec.y !== 0) point.y = tVec.y > 0 ? nextUp(point.y, i + 1) : nextDown(point.y, i + 1); } for (let i = 0; i < ends.length; i++) { const { point, tVec } = ends[i]; if (tVec.x !== 0) point.x = tVec.x < 0 ? nextUp(point.x, i + 1) : nextDown(point.x, i + 1); if (tVec.y !== 0) point.y = tVec.y < 0 ? nextUp(point.y, i + 1) : nextDown(point.y, i + 1); } } //# sourceMappingURL=intersections.js.map