@math.gl/polygon
Version:
Polygon/polyline processing utilities
221 lines • 8.12 kB
JavaScript
// math.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
/* eslint-disable max-statements, max-depth, complexity, no-unused-expressions */
import { bitCode, intersect } from "./lineclip.js";
import { getPointAtIndex, copy, push } from "./utils.js";
export function cutPolylineByGrid(positions, options) {
const { size = 2, broken = false, gridResolution = 10, gridOffset = [0, 0], startIndex = 0, endIndex = positions.length } = options || {};
const numPoints = (endIndex - startIndex) / size;
let part = [];
const result = [part];
const a = getPointAtIndex(positions, 0, size, startIndex);
let b;
let codeB;
const cell = getGridCell(a, gridResolution, gridOffset, []);
const scratchPoint = [];
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;
/**
* Cuts a polygon by a pre-defined grid
*/
export function cutPolygonByGrid(positions, holeIndices = null, options) {
if (!positions.length) {
// input is empty
return [];
}
const { size = 2, gridResolution = 10, gridOffset = [0, 0], edgeTypes = false } = options || {};
const result = [];
const queue = [
{
pos: positions,
types: edgeTypes ? new Array(positions.length / size).fill(TYPE_BORDER) : null,
holes: holeIndices || []
}
];
const bbox = [[], []];
// @ts-ignore
let cell = [];
// 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 = { pos: parts[0].pos, types: parts[0].types, holes: [] };
const polygonHigh = { 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 = { 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, edgeTypes, size, startIndex, endIndex, bbox, edge) {
const numPoints = (endIndex - startIndex) / size;
const resultLow = [];
const resultHigh = [];
const typesLow = [];
const typesHigh = [];
const scratchPoint = [];
let p;
let side;
let type;
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, gridResolution, gridOffset, out) {
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;
}
function moveToNeighborCell(cell, gridResolution, edge) {
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, size, endIndex, out) {
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, arr2) {
for (let i = 0; i < arr2.length; i++) {
arr1.push(arr2[i]);
}
return arr1;
}
//# sourceMappingURL=cut-by-grid.js.map