maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
220 lines (195 loc) • 7.71 kB
text/typescript
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);
}