UNPKG

@kylebarron/snap-features-to-tin

Version:

Snap vector features to the faces of a triangulated irregular network (TIN).

267 lines (227 loc) 8.55 kB
import { polygon as Polygon, lineString as LineString } from "@turf/helpers"; import { getType } from "@turf/invariant"; import bbox from "@turf/bbox"; import bboxClip from "@turf/bbox-clip"; import isPointInPolygon from "@turf/boolean-point-in-polygon"; import barycentric from "barycentric"; import lineIntersect from "@turf/line-intersect"; import uniqBy from "lodash.uniqby"; import orderBy from "lodash.orderby"; import equals from "fast-deep-equal/es6"; import Flatbush from "flatbush"; // var equals = require('fast-deep-equal/es6'); // const Flatbush = require('flatbush'); // var Polygon = require('@turf/helpers').polygon // var LineString = require('@turf/helpers').lineString // var Feature = require('@turf/helpers').feature // var Point = require('@turf/helpers').point // var {getType} = require('@turf/invariant') // var bbox = require('@turf/bbox').default // var bboxClip = require("@turf/bbox-clip").default; // // var isPointInPolygon = require('@turf/boolean-point-in-polygon').default // var barycentric = require("barycentric") // var lineIntersect = require('@turf/line-intersect').default // var _ = require('lodash') // // var terrain = require('./data/terrain.json') // var indices = new Uint32Array(Object.values(terrain.indices.value)) // var positions = new Float32Array(Object.values(terrain.attributes.POSITION.value)) // var features = require('./data/features.json') export function snapFeatures(options = {}) { const { indices, positions, features, bounds = null } = options; const [index, triangles] = constructRTree(indices, positions); const newFeatures = []; for (const feature of features) { const geometryType = getType(feature); if (geometryType === "Point") { const coord = feature.geometry.coordinates; if (bounds && bounds.length === 4) { // Make sure coordinate is within bounds if ( coord[0] < bounds[0] || coord[0] > bounds[2] || coord[1] < bounds[1] || coord[1] > bounds[3] ) { continue; } } feature.geometry.coordinates = handlePoint(coord, index, triangles); newFeatures.push(feature); } else if (geometryType === "LineString") { // Instantiate clippedFeature in case bounds is null let clippedFeature = feature; // Clip to box if (bounds && bounds.length === 4) { clippedFeature = bboxClip(feature, bounds); } const coords = clippedFeature.geometry.coordinates; // If empty, continue if (coords.length === 0) { continue; } // TODO: support multilinestrings // Note that the clipped Feature can now be a MultiLineString if (getType(clippedFeature) === "MultiLineString") { continue; } clippedFeature.geometry.coordinates = handleLineString( coords, index, triangles ); newFeatures.push(clippedFeature); } else { console.error("invalid type"); } } return newFeatures; } // Get triangles from terrain function constructRTree(indices, positions) { // Create list of objects for insertion into RTree const triangles = []; for (let i = 0; i < indices.length; i += 3) { // The indices within `positions` of the three vertices of the triangle const aIndex = indices[i]; const bIndex = indices[i + 1]; const cIndex = indices[i + 2]; // The three vertices of the triangle, where each vertex is an array of [x, y, z] const a = positions.subarray(aIndex * 3, (aIndex + 1) * 3); const b = positions.subarray(bIndex * 3, (bIndex + 1) * 3); const c = positions.subarray(cIndex * 3, (cIndex + 1) * 3); // Create polygon from these coords const geom = Polygon([[a, b, c, a]]); triangles.push(geom); } // initialize Flatbush for # of items const index = new Flatbush(triangles.length); // fill it with 1000 rectangles for (const triangle of triangles) { // Get bounding box of triangle for insertion into rtree const [minX, minY, maxX, maxY] = bbox(triangle); index.add(minX, minY, maxX, maxY); } // perform the indexing index.finish(); return [index, triangles]; } // Find elevation of point function handlePoint(point, index, triangles) { // Search index for point const [x, y] = point.slice(0, 2); const results = index.search(x, y, x, y).map(i => triangles[i]); // Check each result // Since I'm working with triangles and not square boxes, it's possible that a point could be // inside the triangle's bounding box but outside the triangle itself. const filteredResults = results.filter(result => { if (isPointInPolygon(point, result)) return result; }); // if (filteredResults.length > 1) { // console.log(`${filteredResults.length} results from point in polygon search`) // } // // if (filteredResults.length === 0) { // console.log('no results') // console.log(point) // } // Now linearly interpolate elevation within this triangle const triangle = filteredResults[0].geometry.coordinates[0]; const interpolatedPoint = interpolateTriangle( triangle[0], triangle[1], triangle[2], point ); return interpolatedPoint; } // a, b, c must be arrays of three elements // point must be an array of two elements // TODO: add tests where you assert that the interpolated z is above the min vertex height and below // the max vertex height function interpolateTriangle(a, b, c, point) { const [ax, ay, az] = a; const [bx, by, bz] = b; const [cx, cy, cz] = c; // Find the mix of a, b, and c to use const mix = barycentric( [ [ax, ay], [bx, by], [cx, cy] ], point.slice(0, 2) ); // Find the correct z based on that mix const interpolatedZ = mix[0] * az + mix[1] * bz + mix[2] * cz; return [point[0], point[1], interpolatedZ]; } // Add coordinates for LineString function handleLineString(line, index, triangles) { let coordsWithZ = []; // Loop over each line segment for (let i = 0; i < line.length - 1; i++) { const start = line[i]; const end = line[i + 1]; const lineSegment = LineString([start, end]); // Sometimes the start and end points can be the same, usually from clipping if (equals(start, end)) { continue; } // Find edges that this line segment crosses // First search in rtree. This is fast but has false-positives const minX = Math.min(start[0], end[0]); const minY = Math.min(start[1], end[1]); const maxX = Math.max(start[0], end[0]); const maxY = Math.max(start[1], end[1]); const results = index.search(minX, minY, maxX, maxY).map(i => triangles[i]); // Find points where line crosses edges // intersectionPoints is [Feature[Point]] // Note that intersectionPoints has 2x duplicates! // This is because every edge crossed is part of two triangles! const intersectionPoints = results.flatMap(result => { const intersection = lineIntersect(lineSegment, result.geometry); if (intersection.features.length === 0) return []; // Otherwise, has an intersection point(s) const newPoints = []; const triangle = result.geometry.coordinates[0]; for (const int of intersection.features) { const point = int.geometry.coordinates; const newPoint = interpolateTriangle( triangle[0], triangle[1], triangle[2], point ); newPoints.push(newPoint); } return newPoints; }); // Quick and dirty deduplication // Since interpolateTriangle appears to be working now, this just deduplicates on the first // element. const uniqCoords = uniqBy(intersectionPoints, x => x[0]); // sort points in order from start to end const deltaX = end[0] - start[0]; const deltaY = end[1] - start[1]; let sorted; if (deltaX > 0) { sorted = orderBy(uniqCoords, c => c[0], "asc"); } else if (deltaX < 0) { sorted = orderBy(uniqCoords, c => c[0], "desc"); } else if (deltaY > 0) { sorted = orderBy(uniqCoords, c => c[1], "asc"); } else if (deltaY < 0) { sorted = orderBy(uniqCoords, c => c[1], "desc"); } else { throw new Error("start and end point same???"); } const newStart = handlePoint(start, index, triangles); coordsWithZ.push(newStart); coordsWithZ = coordsWithZ.concat(sorted); } const endPoint = line.slice(-1)[0]; coordsWithZ.push(handlePoint(endPoint, index, triangles)); return coordsWithZ; }