// math.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors /* eslint-disable max-statements, max-depth, complexity, no-unused-expressions */ import {bitCode, intersect, BoundingBox} from './lineclip'; import {getPointAtIndex, copy, push} from './utils'; import type {NumericArray} from '@math.gl/core'; export type Polygon = { positions: Readonly; holeIndices?: Readonly; edgeTypes?: Readonly; }; export function cutPolylineByGrid( positions: NumericArray, options?: { size?: number; broken?: boolean; gridResolution?: number; gridOffset?: [number, number]; startIndex?: number; endIndex?: number; } ): number[] | number[][] { const { size = 2, broken = false, gridResolution = 10, gridOffset = [0, 0], startIndex = 0, endIndex = positions.length } = options || {}; const numPoints = (endIndex - startIndex) / size; let part: number[] = []; const result: number[][] = [part]; const a: number[] = getPointAtIndex(positions, 0, size, startIndex); let b: number[]; let codeB: number; const cell: BoundingBox = getGridCell(a, gridResolution, gridOffset, []); const scratchPoint: number[] = []; push(part, a); for (let i = 1; i < numPoints; i++) { b = getPointAtIndex(positions, i, size, startIndex, b); codeB = bitCode(b, cell); while (codeB) { // find the intersection with the current cell intersect(a, b, codeB, cell, scratchPoint); const codeAlt = bitCode(scratchPoint, cell); if (codeAlt) { intersect(a, scratchPoint, codeAlt, cell, scratchPoint); codeB = codeAlt; } push(part, scratchPoint); // move to the next cell copy(a, scratchPoint); moveToNeighborCell(cell, gridResolution, codeB); if (broken && part.length > size) { part = []; result.push(part); push(part, a); } codeB = bitCode(b, cell); } push(part, b); copy(a, b); } return broken ? result : result[0]; } const TYPE_INSIDE = 0; const TYPE_BORDER = 1; /** Internal helper type during cutting, name TBD */ type PolygonCut = { pos: Readonly; types: number[]; holes: Readonly; }; /** Internal helper type during cutting, name TBD */ type MutablePolygonCut = { pos: number[]; types: number[]; holes: number[]; }; /** * Cuts a polygon by a pre-defined grid */ export function cutPolygonByGrid( positions: Readonly, holeIndices: Readonly | null = null, options?: { size?: number; gridResolution?: number; gridOffset?: [number, number]; edgeTypes?: boolean; } ): Polygon[] { if (!positions.length) { // input is empty return []; } const {size = 2, gridResolution = 10, gridOffset = [0, 0], edgeTypes = false} = options || {}; const result: Polygon[] = []; const queue: PolygonCut[] = [ { pos: positions, types: edgeTypes ? (new Array(positions.length / size).fill(TYPE_BORDER) as number[]) : null, holes: holeIndices || [] } ]; const bbox: number[][] = [[], []]; // @ts-ignore let cell: BoundingBox = []; // Recursively bisect polygon until every part fit in a single grid cell while (queue.length) { const {pos, types, holes} = queue.shift(); // Get the bounding box of the outer polygon getBoundingBox(pos, size, holes[0] || pos.length, bbox); cell = getGridCell(bbox[0], gridResolution, gridOffset, cell); const code = bitCode(bbox[1], cell); if (code) { // Split the outer ring at the boundary let parts = bisectPolygon(pos, types, size, 0, holes[0] || pos.length, cell, code); const polygonLow: MutablePolygonCut = {pos: parts[0].pos, types: parts[0].types, holes: []}; const polygonHigh: MutablePolygonCut = {pos: parts[1].pos, types: parts[1].types, holes: []}; queue.push(polygonLow, polygonHigh); // Split each hole at the boundary for (let i = 0; i < holes.length; i++) { parts = bisectPolygon(pos, types, size, holes[i], holes[i + 1] || pos.length, cell, code); if (parts[0]) { polygonLow.holes.push(polygonLow.pos.length); polygonLow.pos = concatInPlace(polygonLow.pos, parts[0].pos); if (edgeTypes) { polygonLow.types = concatInPlace(polygonLow.types, parts[0].types); } } if (parts[1]) { polygonHigh.holes.push(polygonHigh.pos.length); polygonHigh.pos = concatInPlace(polygonHigh.pos, parts[1].pos); if (edgeTypes) { polygonHigh.types = concatInPlace(polygonHigh.types, parts[1].types); } } } } else { // Polygon fits in a single cell, no more processing required const polygon: Polygon = {positions: pos}; if (edgeTypes) { polygon.edgeTypes = types; } if (holes.length) { polygon.holeIndices = holes; } result.push(polygon); } } return result; } // edgeTypes: // TYPE_BORDER - edge from the original polygon // TYPE_INSIDE - inside the original polygon // eslint-disable-next-line max-params function bisectPolygon( positions: Readonly, edgeTypes: number[] | undefined, size: number, startIndex: number, endIndex: number, bbox: BoundingBox, edge: number ): { pos: number[]; types?: number[]; }[] { const numPoints = (endIndex - startIndex) / size; const resultLow: number[] = []; const resultHigh: number[] = []; const typesLow: number[] = []; const typesHigh: number[] = []; const scratchPoint: number[] = []; let p: number[]; let side: number; let type: number; const prev = getPointAtIndex(positions, numPoints - 1, size, startIndex); let prevSide = Math.sign(edge & 8 ? prev[1] - bbox[3] : prev[0] - bbox[2]); let prevType = edgeTypes && edgeTypes[numPoints - 1]; let lowPointCount = 0; let highPointCount = 0; for (let i = 0; i < numPoints; i++) { p = getPointAtIndex(positions, i, size, startIndex, p); side = Math.sign(edge & 8 ? p[1] - bbox[3] : p[0] - bbox[2]); type = edgeTypes && edgeTypes[startIndex / size + i]; // if segment goes through the boundary, add an intersection if (side && prevSide && prevSide !== side) { intersect(prev, p, edge, bbox, scratchPoint); push(resultLow, scratchPoint) && typesLow.push(prevType); push(resultHigh, scratchPoint) && typesHigh.push(prevType); } if (side <= 0) { push(resultLow, p) && typesLow.push(type); lowPointCount -= side; } else if (typesLow.length) { typesLow[typesLow.length - 1] = TYPE_INSIDE; } if (side >= 0) { push(resultHigh, p) && typesHigh.push(type); highPointCount += side; } else if (typesHigh.length) { typesHigh[typesHigh.length - 1] = TYPE_INSIDE; } copy(prev, p); prevSide = side; prevType = type; } return [ lowPointCount ? {pos: resultLow, types: edgeTypes && typesLow} : null, highPointCount ? {pos: resultHigh, types: edgeTypes && typesHigh} : null ]; } function getGridCell( p: number[], gridResolution: number, gridOffset: [number, number], out: number[] ): BoundingBox { const left = Math.floor((p[0] - gridOffset[0]) / gridResolution) * gridResolution + gridOffset[0]; const bottom = Math.floor((p[1] - gridOffset[1]) / gridResolution) * gridResolution + gridOffset[1]; out[0] = left; out[1] = bottom; out[2] = left + gridResolution; out[3] = bottom + gridResolution; return out as BoundingBox; } function moveToNeighborCell(cell: number[], gridResolution: number, edge: number): void { if (edge & 8) { // top cell[1] += gridResolution; cell[3] += gridResolution; } else if (edge & 4) { // bottom cell[1] -= gridResolution; cell[3] -= gridResolution; } else if (edge & 2) { // right cell[0] += gridResolution; cell[2] += gridResolution; } else if (edge & 1) { // left cell[0] -= gridResolution; cell[2] -= gridResolution; } } function getBoundingBox( positions: Readonly, size: number, endIndex: number, out: number[][] ): number[][] { let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; for (let i = 0; i < endIndex; i += size) { const x = positions[i]; const y = positions[i + 1]; minX = x < minX ? x : minX; maxX = x > maxX ? x : maxX; minY = y < minY ? y : minY; maxY = y > maxY ? y : maxY; } out[0][0] = minX; out[0][1] = minY; out[1][0] = maxX; out[1][1] = maxY; return out; } function concatInPlace(arr1: number[], arr2: number[]): number[] { for (let i = 0; i < arr2.length; i++) { arr1.push(arr2[i]); } return arr1; }