// loaders.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors // Forked from https://github.com/mapbox/geojson-vt under compatible ISC license import type {ProtoFeature} from './proto-feature'; import {createProtoFeature} from './proto-feature'; /* eslint-disable no-continue */ /** * Clip features between two vertical or horizontal axis-parallel lines: * | | * ___|___ | / * / | \____|____/ * | | * * @param k1 and k2 are the line coordinates * @param axis: 0 for x, 1 for y * @param minAll and maxAll: minimum and maximum coordinate value for all features */ // eslint-disable-next-line max-params, complexity, max-statements export function clipFeatures( features: ProtoFeature[], scale: number, k1: number, k2: number, axis, minAll: number, maxAll: number, options: {lineMetrics: boolean} ): ProtoFeature[] | null { k1 /= scale; k2 /= scale; if (minAll >= k1 && maxAll < k2) { return features; } // trivial accept else if (maxAll < k1 || minAll >= k2) { return null; // trivial reject } const clipped: ProtoFeature[] = []; for (const feature of features) { const geometry = feature.geometry; let type = feature.type; const min = axis === 0 ? feature.minX : feature.minY; const max = axis === 0 ? feature.maxX : feature.maxY; if (min >= k1 && max < k2) { // trivial accept clipped.push(feature); continue; } else if (max < k1 || min >= k2) { // trivial reject continue; } let newGeometry: number[][][] | number[][] = []; if (type === 'Point' || type === 'MultiPoint') { clipPoints(geometry, newGeometry, k1, k2, axis); } else if (type === 'LineString') { clipLine(geometry, newGeometry, k1, k2, axis, false, options.lineMetrics); } else if (type === 'MultiLineString') { clipLines(geometry, newGeometry, k1, k2, axis, false); } else if (type === 'Polygon') { clipLines(geometry, newGeometry, k1, k2, axis, true); } else if (type === 'MultiPolygon') { for (const polygon of geometry) { const newPolygon = []; clipLines(polygon, newPolygon, k1, k2, axis, true); if (newPolygon.length) { newGeometry.push(newPolygon); } } } if (newGeometry.length) { if (options.lineMetrics && type === 'LineString') { for (const line of newGeometry) { clipped.push(createProtoFeature(feature.id, type, line, feature.tags)); } continue; } if (type === 'LineString' || type === 'MultiLineString') { if (newGeometry.length === 1) { type = 'LineString'; // @ts-expect-error TODO - use proper GeoJSON geometry types newGeometry = newGeometry[0]; } else { type = 'MultiLineString'; } } if (type === 'Point' || type === 'MultiPoint') { type = newGeometry.length === 3 ? 'Point' : 'MultiPoint'; } clipped.push(createProtoFeature(feature.id, type, newGeometry, feature.tags)); } } return clipped.length ? clipped : null; } function clipPoints(geom, newGeom, k1: number, k2: number, axis): void { for (let i = 0; i < geom.length; i += 3) { const a = geom[i + axis]; if (a >= k1 && a <= k2) { addPoint(newGeom, geom[i], geom[i + 1], geom[i + 2]); } } } // eslint-disable-next-line max-params, complexity, max-statements function clipLine( geom, newGeom, k1: number, k2: number, axis, isPolygon: boolean, trackMetrics: boolean ): void { let slice = newSlice(geom); const intersect = axis === 0 ? intersectX : intersectY; let len = geom.start; let segLen; let t; for (let i = 0; i < geom.length - 3; i += 3) { const ax = geom[i]; const ay = geom[i + 1]; const az = geom[i + 2]; const bx = geom[i + 3]; const by = geom[i + 4]; const a = axis === 0 ? ax : ay; const b = axis === 0 ? bx : by; let exited = false; if (trackMetrics) { segLen = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2)); } if (a < k1) { // ---|--> | (line enters the clip region from the left) if (b > k1) { t = intersect(slice, ax, ay, bx, by, k1); if (trackMetrics) { slice.start = len + segLen * t; } } } else if (a > k2) { // | <--|--- (line enters the clip region from the right) if (b < k2) { t = intersect(slice, ax, ay, bx, by, k2); if (trackMetrics) { slice.start = len + segLen * t; } } } else { addPoint(slice, ax, ay, az); } if (b < k1 && a >= k1) { // <--|--- | or <--|-----|--- (line exits the clip region on the left) t = intersect(slice, ax, ay, bx, by, k1); exited = true; } if (b > k2 && a <= k2) { // | ---|--> or ---|-----|--> (line exits the clip region on the right) t = intersect(slice, ax, ay, bx, by, k2); exited = true; } if (!isPolygon && exited) { if (trackMetrics) { slice.end = len + segLen * t; } newGeom.push(slice); slice = newSlice(geom); } if (trackMetrics) { len += segLen; } } // add the last point let last = geom.length - 3; const ax = geom[last]; const ay = geom[last + 1]; const az = geom[last + 2]; const a = axis === 0 ? ax : ay; if (a >= k1 && a <= k2) addPoint(slice, ax, ay, az); // close the polygon if its endpoints are not the same after clipping last = slice.length - 3; if (isPolygon && last >= 3 && (slice[last] !== slice[0] || slice[last + 1] !== slice[1])) { addPoint(slice, slice[0], slice[1], slice[2]); } // add the final slice if (slice.length) { newGeom.push(slice); } } class Slice extends Array { size?: number; start?: number; end?: number; } function newSlice(line: {size: number; start: number; end: number}): Slice { const slice: Slice = []; slice.size = line.size; slice.start = line.start; slice.end = line.end; return slice; } // eslint-disable-next-line max-params function clipLines(geom, newGeom, k1: number, k2: number, axis, isPolygon: boolean): void { for (const line of geom) { clipLine(line, newGeom, k1, k2, axis, isPolygon, false); } } function addPoint(out: number[], x: number, y: number, z: number): void { out.push(x, y, z); } // eslint-disable-next-line max-params function intersectX(out, ax: number, ay: number, bx: number, by: number, x: number): number { const t = (x - ax) / (bx - ax); addPoint(out, x, ay + (by - ay) * t, 1); return t; } // eslint-disable-next-line max-params function intersectY(out, ax: number, ay: number, bx: number, by: number, y): number { const t = (y - ay) / (by - ay); addPoint(out, ax + (bx - ax) * t, y, 1); return t; }