UNPKG

@allmaps/triangulate

Version:

Allmaps Triangulation Library

142 lines (141 loc) 6.44 kB
import { getGridPointsInBbox, interpolateRing, interpolatePolygon, pointInPolygon } from './shared.js'; import { computeBbox, conformPolygon, mergeOptions, midPoint, triangleAngles } from '@allmaps/stdlib'; import Delaunator from 'delaunator'; import Constrainautor from '@kninnug/constrainautor'; const MINIMUM_TRIANGLE_ANGLE = 0.0001; const defaultTriangulationOptions = { steinerPoints: [], minimumTriangleAngle: MINIMUM_TRIANGLE_ANGLE }; export { interpolateRing, interpolatePolygon }; /** * Triangulate a polygon to triangles smaller then a distance * * Grid points are placed inside the polygon to obtain small, well conditioned triangles. * * @param polygon - Polygon * @param distance - Distance that conditions the triangles * @param triangulationOptions - Triangulation Options. * @returns Array of triangles partitioning the polygon */ export function triangulate(polygon, distance, triangulationOptions) { { const { triangles } = triangulateToUnique(polygon, distance, triangulationOptions); return triangles; } } /** * Triangulate a polygon to triangles smaller then a distance, and return them via unique points. * * Grid points are placed inside the polygon to obtain small, well conditioned triangles. * * This function returns the triangulation as an array of unique points, and triangles of indices refering to those unique points. * * @param polygon - Polygon * @param distance - Distance that conditions the triangles * @param triangulationOptions - Triangulation Options. * @returns Triangulation Object with uniquePointIndexTriangles and uniquePoints */ export function triangulateToUnique(polygon, distance, triangulationOptions) { const mergedTriangulationOptions = mergeOptions(defaultTriangulationOptions, triangulationOptions); const steinerPoints = mergedTriangulationOptions.steinerPoints; const minimumTriangleAngle = mergedTriangulationOptions.minimumTriangleAngle; // Conform polygon (this also checks if there are at least 3 points) polygon = conformPolygon(polygon); let interpolatedPolygon = []; let interpolatedPolygonPoints = []; let gridPoints = []; let gridPointsInPolygon = []; if (distance) { // Interpolate polygon interpolatedPolygon = interpolatePolygon(polygon, distance); interpolatedPolygonPoints = interpolatedPolygon.flat(); // Add grid points inside the polygon gridPoints = getGridPointsInBbox(computeBbox(polygon), distance); gridPointsInPolygon = gridPoints.filter((point) => pointInPolygon(point, polygon)); } else { interpolatedPolygon = polygon; interpolatedPolygonPoints = polygon.flat(); } const steinerPointsInPolygon = steinerPoints.filter((point) => pointInPolygon(point, polygon)); const uniquePoints = [ ...interpolatedPolygonPoints, ...gridPointsInPolygon, ...steinerPointsInPolygon ]; // Initialize Delaunay triangulation from polygon + grid points const delautator = new Delaunator(uniquePoints.flat()); // Collect indices of (interpolated) polygon edges let ringOffset = 0; const uniquePointIndexInterpolatedPolygon = interpolatedPolygon.map((ring) => { const uniqueIndexRing = ring.map((_point, index) => ringOffset + index); ringOffset += ring.length; return uniqueIndexRing; }); const uniquePointIndexEdges = uniquePointIndexInterpolatedPolygon .map((ring) => ring.map((uniqueIndex) => [uniqueIndex, (uniqueIndex + 1) % ring.length])) .flat(); // Constrain triangulation // Note: instead of // const constrainautor = new Constrainautor(delautator, uniquePointIndexEdges) // which gives an error "Constraining edge intersects point" for small distances, e.g. maxDepth 7 on https://gist.githubusercontent.com/sammeltassen/fa3dbfaf4dfa800e00824478c4bd1928/raw/f182beac911e38b0a1d1eb420fbd54b4e6d2f2eb/nl-railway-map.json // we perform a delaunify check first as proposed by // https://github.com/kninnug/Constrainautor/issues/11#issuecomment-2571296247 // Keep an eye on proposed solutions, sinche the delaunify check is expensive const constrainautor = new Constrainautor(delautator); constrainautor.delaunify(true); constrainautor.constrainAll(uniquePointIndexEdges); let uniquePointIndexTriangles = []; let triangles = []; const shouldClassifyTriangles = []; for (let i = 0; i < constrainautor.del.triangles.length; i += 3) { uniquePointIndexTriangles.push([ constrainautor.del.triangles[i], constrainautor.del.triangles[i + 1], constrainautor.del.triangles[i + 2] ]); triangles.push([ uniquePoints[constrainautor.del.triangles[i]], uniquePoints[constrainautor.del.triangles[i + 1]], uniquePoints[constrainautor.del.triangles[i + 2]] ]); // Only classify triangles if they are along the border shouldClassifyTriangles.push(constrainautor.del.triangles[i] < interpolatedPolygonPoints.length || constrainautor.del.triangles[i + 1] < interpolatedPolygonPoints.length || constrainautor.del.triangles[i + 2] < interpolatedPolygonPoints.length); } // Check if triangles inside const shouldKeep = triangles.map((triangle, index) => { // Only keep if inside if (shouldClassifyTriangles[index]) { return (pointInPolygon(midPoint(...triangle), polygon) && triangleAngles(triangle).every((angle) => angle >= minimumTriangleAngle)); } else { return true; } }); uniquePointIndexTriangles = uniquePointIndexTriangles.filter((_triangle, index) => shouldKeep[index]); triangles = triangles.filter((_triangle, index) => shouldKeep[index]); // Fill in edges using unique const edges = []; for (let i = 0; i < uniquePointIndexEdges.length; i += 1) { edges.push([ uniquePoints[uniquePointIndexEdges[i][0]], uniquePoints[uniquePointIndexEdges[i][1]] ]); } return { interpolatedPolygon, interpolatedPolygonPoints, gridPoints, gridPointsInPolygon, uniquePoints, triangles, uniquePointIndexTriangles, uniquePointIndexInterpolatedPolygon, uniquePointIndexEdges }; }