import Point from '@mapbox/point-geometry'; /** * Returns the part of a multiline that intersects with the provided rectangular box. * * @param lines - the lines to check * @param x1 - the left edge of the box * @param y1 - the top edge of the box * @param x2 - the right edge of the box * @param y2 - the bottom edge of the box * @returns lines */ export function clipLine(lines: Point[][], x1: number, y1: number, x2: number, y2: number): Point[][] { const clippedLines: Point[][] = []; for (const line of lines) { let clippedLine: Point[] | undefined; for (let i = 0; i < line.length - 1; i++) { let p0 = line[i]; let p1 = line[i + 1]; if (p0.x < x1 && p1.x < x1) { continue; } else if (p0.x < x1) { p0 = new Point(x1, p0.y + (p1.y - p0.y) * ((x1 - p0.x) / (p1.x - p0.x)))._round(); } else if (p1.x < x1) { p1 = new Point(x1, p0.y + (p1.y - p0.y) * ((x1 - p0.x) / (p1.x - p0.x)))._round(); } if (p0.y < y1 && p1.y < y1) { continue; } else if (p0.y < y1) { p0 = new Point(p0.x + (p1.x - p0.x) * ((y1 - p0.y) / (p1.y - p0.y)), y1)._round(); } else if (p1.y < y1) { p1 = new Point(p0.x + (p1.x - p0.x) * ((y1 - p0.y) / (p1.y - p0.y)), y1)._round(); } if (p0.x >= x2 && p1.x >= x2) { continue; } else if (p0.x >= x2) { p0 = new Point(x2, p0.y + (p1.y - p0.y) * ((x2 - p0.x) / (p1.x - p0.x)))._round(); } else if (p1.x >= x2) { p1 = new Point(x2, p0.y + (p1.y - p0.y) * ((x2 - p0.x) / (p1.x - p0.x)))._round(); } if (p0.y >= y2 && p1.y >= y2) { continue; } else if (p0.y >= y2) { p0 = new Point(p0.x + (p1.x - p0.x) * ((y2 - p0.y) / (p1.y - p0.y)), y2)._round(); } else if (p1.y >= y2) { p1 = new Point(p0.x + (p1.x - p0.x) * ((y2 - p0.y) / (p1.y - p0.y)), y2)._round(); } if (!clippedLine || !p0.equals(clippedLine[clippedLine.length - 1])) { clippedLine = [p0]; clippedLines.push(clippedLine); } clippedLine.push(p1); } } return clippedLines; } /** * Clips the geometry to the given bounds. * @param geometry - the geometry to clip * @param type - the geometry type (1=POINT, 2=LINESTRING, 3=POLYGON) * @param x1 - the left edge of the clipping box * @param y1 - the top edge of the clipping box * @param x2 - the right edge of the clipping box * @param y2 - the bottom edge of the clipping box * @returns the clipped geometry */ export function clipGeometry(geometry: Point[][], type: 0 | 1 | 2 | 3, x1: number, y1: number, x2: number, y2: number): Point[][] { let clippedGeometry = clipGeometryOnAxis(geometry, type, x1, x2, AxisType.X); clippedGeometry = clipGeometryOnAxis(clippedGeometry, type, y1, y2, AxisType.Y); return clippedGeometry; } /** * On which axis to clip */ const enum AxisType { X = 0, Y = 1 } /** * Clip features between two vertical or horizontal axis-parallel lines: * ``` * | | * ___|___ | / * / | \____|____/ * | | *``` * @param geometry - the geometry to clip * @param type - the geometry type (1=POINT, 2=LINESTRING, 3=POLYGON) * @param start - the start line coordinate (x or y) to clip against * @param end - the end line coordinate (x or y) to clip against * @param axis - the axis to clip on (X or Y) * @returns the clipped geometry */ function clipGeometryOnAxis(geometry: Point[][], type: 0 | 1 | 2 | 3, start: number, end: number, axis: AxisType): Point[][] { switch (type) { case 1: // POINT return clipPoints(geometry, start, end, axis); case 2: // LINESTRING return clipLines(geometry, start, end, axis, false); case 3: // POLYGON return clipLines(geometry, start, end, axis, true); } return []; } function clipPoints(geometry: Point[][], start: number, end: number, axis: AxisType): Point[][] { const newGeometry: Point[][] = []; for (const ring of geometry) { for (const point of ring) { const a = axis === AxisType.X ? point.x : point.y; if (a >= start && a <= end) { newGeometry.push([point]); } } } return newGeometry; } /** * Clips a line to the given start and end coordinates. * @param line - the line to clip * @param start - the start line coordinate (x or y) to clip against * @param end - the end line coordinate (x or y) to clip against * @param axis - the axis to clip on (X or Y) * @param isPolygon - whether the line is part of a polygon * @returns the clipped line(s) */ function clipLineInternal(line: Point[], start: number, end: number, axis: AxisType, isPolygon: boolean): Point[][] { const intersectionPoint = axis === AxisType.X ? intersectionPointX : intersectionPointY; let slice: Point[] = []; const newLine: Point[][] = []; for (let i = 0; i < line.length - 1; i++) { const p1 = line[i]; const p2 = line[i + 1]; const pos1 = axis === AxisType.X ? p1.x : p1.y; const pos2 = axis === AxisType.X ? p2.x : p2.y; let exited = false; if (pos1 < start) { // ---|--> | (line enters the clip region from the left) if (pos2 > start) { slice.push(intersectionPoint(p1, p2, start)); } } else if (pos1 > end) { // | <--|--- (line enters the clip region from the right) if (pos2 < end) { slice.push(intersectionPoint(p1, p2, end)); } } else { slice.push(p1); } if (pos2 < start && pos1 >= start) { // <--|--- | or <--|-----|--- (line exits the clip region on the left) slice.push(intersectionPoint(p1, p2, start)); exited = true; } if (pos2 > end && pos1 <= end) { // | ---|--> or ---|-----|--> (line exits the clip region on the right) slice.push(intersectionPoint(p1, p2, end)); exited = true; } if (!isPolygon && exited) { newLine.push(slice); slice = []; } } // add the last point const last = line.length - 1; const lastPos = axis === AxisType.X ? line[last].x : line[last].y; if (lastPos >= start && lastPos <= end) { slice.push(line[last]); } // close the polygon if its endpoints are not the same after clipping if (isPolygon && slice.length > 0 && !slice[0].equals(slice[slice.length - 1])) { slice.push(new Point(slice[0].x, slice[0].y)); } if (slice.length > 0) { newLine.push(slice); } return newLine; } function clipLines(geometry: Point[][], start: number, end: number, axis: AxisType, isPolygon: boolean): Point[][] { const newGeometry: Point[][] = []; for (const line of geometry) { const clippedLines = clipLineInternal(line, start, end, axis, isPolygon); if (clippedLines.length > 0) { newGeometry.push(...clippedLines); } } return newGeometry; } function intersectionPointX(p1: Point, p2: Point, x: number): Point { const t = (x - p1.x) / (p2.x - p1.x); return new Point(x, p1.y + (p2.y - p1.y) * t); } function intersectionPointY(p1: Point, p2: Point, y: number): Point { const t = (y - p1.y) / (p2.y - p1.y); return new Point(p1.x + (p2.x - p1.x) * t, y); }