@math.gl/polygon
Version:
Polygon/polyline processing utilities
314 lines (280 loc) • 8.82 kB
text/typescript
// 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<NumericArray>;
holeIndices?: Readonly<NumericArray>;
edgeTypes?: Readonly<NumericArray>;
};
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<NumericArray>;
types: number[];
holes: Readonly<NumericArray>;
};
/** 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<NumericArray>,
holeIndices: Readonly<NumericArray> | 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<NumericArray>,
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<NumericArray>,
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;
}