@allmaps/triangulate
Version:
Allmaps Triangulation Library
157 lines (156 loc) • 6.02 kB
JavaScript
import { orient2d } from 'robust-predicates';
import KDBush from 'kdbush';
import { distance, stepDistanceAngle, lineAngle, closeRing, computeBbox } from '@allmaps/stdlib';
// Split lines using points
export function pointOnLine(point, line, exclude = true) {
const [x, y] = point;
const [[x0, y0], [x1, y1]] = line;
if (exclude) {
if (x === x0 && y === y0)
return false;
if (x === x1 && y === y1)
return false;
}
if ((x - x0) * (x - x1) > 0)
return false;
if ((y - y0) * (y - y1) > 0)
return false;
return orient2d(x0, y0, x1, y1, x, y) === 0;
}
export function buildKDBushPointIndex(points) {
const tree = new KDBush(points.length);
for (const [x, y] of points)
tree.add(x, y);
tree.finish();
return { tree, points };
}
export function splitRingLines(ring, { tree, points }) {
const result = [];
for (let i = 0; i < ring.length; i++) {
const p0 = ring[i];
const p1 = ring[(i + 1) % ring.length];
const [x0, y0] = p0;
const [x1, y1] = p1;
result.push(p0);
const ids = tree.range(Math.min(x0, x1), Math.min(y0, y1), Math.max(x0, x1), Math.max(y0, y1));
if (ids.length === 0)
continue;
const hits = [];
for (const id of ids) {
const [x, y] = points[id];
if (pointOnLine([x, y], [p0, p1]))
hits.push(points[id]);
}
if (hits.length === 1) {
result.push(hits[0]);
}
else if (hits.length > 1) {
hits.sort(([ax, ay], [bx, by]) => (ax - x0) ** 2 + (ay - y0) ** 2 - ((bx - x0) ** 2 + (by - y0) ** 2));
result.push(...hits);
}
}
return result;
}
export function splitPolygonLines(polygon, points, index = buildKDBushPointIndex(points)) {
return polygon.map((ring) => splitRingLines(ring, index));
}
// Interpolate line using distance
// Return an array of points containing the first line point,
// and betwen the first and last line point other points every `dist`
function interpolateLine(line, dist) {
const lineDistance = distance(...line);
// Note: ciel - 1 instead of floor, such that for round numbers we don't include the last step
const steps = Math.ceil(lineDistance / dist) - 1;
const angle = lineAngle(line);
let currentPoint = line[0];
const result = [currentPoint];
for (let step = 1; step <= steps; step++) {
currentPoint = stepDistanceAngle(currentPoint, dist, angle);
result.push(currentPoint);
}
// Note: the last nextpoint, which is also line[1], is not pushed
return result;
}
// Return an array of points containing the ring points,
// and between every pair of ring points other points every `dist`
export function interpolateRing(ring, dist) {
ring = closeRing(ring);
let result = [];
for (let i = 0; i < ring.length - 1; i++) {
result = result.concat(interpolateLine([ring[i], ring[i + 1]], dist));
}
return result;
}
export function interpolatePolygon(polygon, dist) {
return polygon.map((ring) => interpolateRing(ring, dist));
}
// Grid
export function bboxToGridPoints(bbox, gridSize) {
const grid = [];
for (let x = bbox[0] + gridSize, i = 0; x <= bbox[2]; i++, x += gridSize) {
for (let y = bbox[1] + gridSize, j = 0; y <= bbox[3]; j++, y += gridSize) {
grid.push([x, y]);
}
}
return grid;
}
// Point inside triangle
export function preprocessPolygonForInsideCheck(polygon) {
const closedRings = polygon.map((ring) => {
const closedRing = closeRing(ring);
const ringLength = closedRing.length - 1;
// Flat typed array: [x0, y0, x1, y1, ...] — avoids nested array pointer chasing
const coords = new Float64Array(closedRing.length * 2);
for (let i = 0; i < closedRing.length; i++) {
coords[i * 2] = closedRing[i][0];
coords[i * 2 + 1] = closedRing[i][1];
}
return { coords, length: ringLength };
});
const bbox = computeBbox(polygon);
return { closedRings, bbox };
}
// Improved from point-in-polygon-hao package
// https://github.com/rowanwins/point-in-polygon-hao/blob/938b2be31d326c52c8f6cffbbb1c59bae4d609bc/src/index.js
// Returns true if point is inside of polygon with holes
//
// Speedups:
// - Reuse preprocessed polygon
// - Avoid point allocation (and it's garbage collection) and use coordinates
export function coordsInPolygonForInsidenessCheck(x, y, polygonForInsidenessCheck) {
if (x < polygonForInsidenessCheck.bbox[0] ||
y < polygonForInsidenessCheck.bbox[1] ||
x > polygonForInsidenessCheck.bbox[2] ||
y > polygonForInsidenessCheck.bbox[3])
return false;
let k = 0;
const closedRings = polygonForInsidenessCheck.closedRings;
for (let i = 0; i < closedRings.length; i++) {
const coords = closedRings[i].coords;
const length = closedRings[i].length;
let u1 = coords[0] - x;
let v1 = coords[1] - y;
for (let ii = 0; ii < length; ii++) {
const base = (ii + 1) * 2;
const u2 = coords[base] - x;
const v2 = coords[base + 1] - y;
if (v1 === 0 && v2 === 0) {
if ((u2 <= 0 && u1 >= 0) || (u1 <= 0 && u2 >= 0))
return 0;
}
else if ((v2 >= 0 && v1 <= 0) || (v2 <= 0 && v1 >= 0)) {
const f = orient2d(u1, u2, v1, v2, 0, 0);
if (f === 0)
return 0;
if ((f > 0 && v2 > 0 && v1 <= 0) || (f < 0 && v2 <= 0 && v1 > 0))
k++;
}
u1 = u2;
v1 = v2;
}
}
return k % 2 !== 0;
}
export function coordsInPolygonsForInsidenessCheck(x, y, polygonsForInsidenessChecks) {
return polygonsForInsidenessChecks.some((polygonForInsidenessChecks) => coordsInPolygonForInsidenessCheck(x, y, polygonForInsidenessChecks));
}