@nebula.gl/layers
Version:
A suite of 3D-enabled data editing layers, suitable for deck.gl
179 lines (158 loc) • 5.29 kB
text/typescript
import destination from '@turf/destination';
import bearing from '@turf/bearing';
import pointToLineDistance from '@turf/point-to-line-distance';
import { point } from '@turf/helpers';
import {
Position,
Point,
LineString,
FeatureOf,
FeatureWithProps,
Viewport,
} from '@nebula.gl/edit-modes';
import WebMercatorViewport from 'viewport-mercator-project';
// TODO edit-modes: delete and use edit-modes/utils instead
export type NearestPointType = FeatureWithProps<Point, { dist: number; index: number }>;
export function toDeckColor(
color?: [number, number, number, number] | number,
defaultColor: [number, number, number, number] = [255, 0, 0, 255]
): [number, number, number, number] {
if (!Array.isArray(color)) {
return defaultColor;
}
return [color[0] * 255, color[1] * 255, color[2] * 255, color[3] * 255];
}
//
// a GeoJSON helper function that calls the provided function with
// an argument that is the most deeply-nested array having elements
// that are arrays of primitives as an argument, e.g.
//
// {
// "type": "MultiPolygon",
// "coordinates": [
// [
// [[30, 20], [45, 40], [10, 40], [30, 20]]
// ],
// [
// [[15, 5], [40, 10], [10, 20], [5, 10], [15, 5]]
// ]
// ]
// }
//
// the function would be called on:
//
// [[30, 20], [45, 40], [10, 40], [30, 20]]
//
// and
//
// [[15, 5], [40, 10], [10, 20], [5, 10], [15, 5]]
//
export function recursivelyTraverseNestedArrays(
array: Array<any>,
prefix: Array<number>,
fn: Function
) {
if (!Array.isArray(array[0])) {
return true;
}
for (let i = 0; i < array.length; i++) {
if (recursivelyTraverseNestedArrays(array[i], [...prefix, i], fn)) {
fn(array, prefix);
break;
}
}
return false;
}
export function generatePointsParallelToLinePoints(
p1: Position,
p2: Position,
groundCoords: Position
): Position[] {
const lineString: LineString = {
type: 'LineString',
coordinates: [p1, p2],
};
const pt = point(groundCoords);
const ddistance = pointToLineDistance(pt, lineString);
const lineBearing = bearing(p1, p2);
// Check if current point is to the left or right of line
// Line from A=(x1,y1) to B=(x2,y2) a point P=(x,y)
// then (x−x1)(y2−y1)−(y−y1)(x2−x1)
const isPointToLeftOfLine =
(groundCoords[0] - p1[0]) * (p2[1] - p1[1]) - (groundCoords[1] - p1[1]) * (p2[0] - p1[0]);
// Bearing to draw perpendicular to the line string
const orthogonalBearing = isPointToLeftOfLine < 0 ? lineBearing - 90 : lineBearing - 270;
// Get coordinates for the point p3 and p4 which are perpendicular to the lineString
// Add the distance as the current position moves away from the lineString
const p3 = destination(p2, ddistance, orthogonalBearing);
const p4 = destination(p1, ddistance, orthogonalBearing);
//@ts-ignore
return [p3.geometry.coordinates, p4.geometry.coordinates];
}
export function distance2d(x1: number, y1: number, x2: number, y2: number): number {
const dx = x1 - x2;
const dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
export function mix(a: number, b: number, ratio: number): number {
return b * ratio + a * (1 - ratio);
}
export function nearestPointOnProjectedLine(
line: FeatureOf<LineString>,
inPoint: FeatureOf<Point>,
viewport: Viewport
): NearestPointType {
const wmViewport = new WebMercatorViewport(viewport);
// Project the line to viewport, then find the nearest point
const coordinates: Array<Array<number>> = line.geometry.coordinates as any;
const projectedCoords = coordinates.map(([x, y, z = 0]) => wmViewport.project([x, y, z]));
//@ts-ignore
const [x, y] = wmViewport.project(inPoint.geometry.coordinates);
// console.log('projectedCoords', JSON.stringify(projectedCoords));
let minDistance = Infinity;
let minPointInfo = {};
projectedCoords.forEach(([x2, y2], index) => {
if (index === 0) {
return;
}
const [x1, y1] = projectedCoords[index - 1];
// line from projectedCoords[index - 1] to projectedCoords[index]
// convert to Ax + By + C = 0
const A = y1 - y2;
const B = x2 - x1;
const C = x1 * y2 - x2 * y1;
// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
const div = A * A + B * B;
const distance = Math.abs(A * x + B * y + C) / Math.sqrt(div);
// TODO: Check if inside bounds
if (distance < minDistance) {
minDistance = distance;
minPointInfo = {
index,
x0: (B * (B * x - A * y) - A * C) / div,
y0: (A * (-B * x + A * y) - B * C) / div,
};
}
});
//@ts-ignore
const { index, x0, y0 } = minPointInfo;
const [x1, y1, z1 = 0] = projectedCoords[index - 1];
const [x2, y2, z2 = 0] = projectedCoords[index];
// calculate what ratio of the line we are on to find the proper z
const lineLength = distance2d(x1, y1, x2, y2);
const startToPointLength = distance2d(x1, y1, x0, y0);
const ratio = startToPointLength / lineLength;
const z0 = mix(z1, z2, ratio);
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: wmViewport.unproject([x0, y0, z0]),
},
properties: {
// TODO: calculate the distance in proper units
dist: minDistance,
index: index - 1,
},
};
}