node-red-contrib-tak-registration
Version:
A Node-RED node to register to TAK and to help wrap files as datapackages to send to TAK
216 lines (191 loc) • 7.03 kB
JavaScript
import rbush from 'geojson-rbush';
import square from '@turf/square';
import bbox from '@turf/bbox';
import truncate from '@turf/truncate';
import lineSegment from '@turf/line-segment';
import lineIntersect from '@turf/line-intersect';
import nearestPointOnLine from '@turf/nearest-point-on-line';
import { getType, getCoords, getCoord } from '@turf/invariant';
import { flattenEach, featureEach, featureReduce } from '@turf/meta';
import { featureCollection, lineString } from '@turf/helpers';
/**
* Split a LineString by another GeoJSON Feature.
*
* @name lineSplit
* @param {Feature<LineString>} line LineString Feature to split
* @param {Feature<any>} splitter Feature used to split line
* @returns {FeatureCollection<LineString>} Split LineStrings
* @example
* var line = turf.lineString([[120, -25], [145, -25]]);
* var splitter = turf.lineString([[130, -15], [130, -35]]);
*
* var split = turf.lineSplit(line, splitter);
*
* //addToMap
* var addToMap = [line, splitter]
*/
function lineSplit(line, splitter) {
if (!line) throw new Error("line is required");
if (!splitter) throw new Error("splitter is required");
var lineType = getType(line);
var splitterType = getType(splitter);
if (lineType !== "LineString") throw new Error("line must be LineString");
if (splitterType === "FeatureCollection")
throw new Error("splitter cannot be a FeatureCollection");
if (splitterType === "GeometryCollection")
throw new Error("splitter cannot be a GeometryCollection");
// remove excessive decimals from splitter
// to avoid possible approximation issues in rbush
var truncatedSplitter = truncate(splitter, { precision: 7 });
switch (splitterType) {
case "Point":
return splitLineWithPoint(line, truncatedSplitter);
case "MultiPoint":
return splitLineWithPoints(line, truncatedSplitter);
case "LineString":
case "MultiLineString":
case "Polygon":
case "MultiPolygon":
return splitLineWithPoints(line, lineIntersect(line, truncatedSplitter));
}
}
/**
* Split LineString with MultiPoint
*
* @private
* @param {Feature<LineString>} line LineString
* @param {FeatureCollection<Point>} splitter Point
* @returns {FeatureCollection<LineString>} split LineStrings
*/
function splitLineWithPoints(line, splitter) {
var results = [];
var tree = rbush();
flattenEach(splitter, function (point) {
// Add index/id to features (needed for filter)
results.forEach(function (feature, index) {
feature.id = index;
});
// First Point - doesn't need to handle any previous line results
if (!results.length) {
results = splitLineWithPoint(line, point).features;
// Add Square BBox to each feature for GeoJSON-RBush
results.forEach(function (feature) {
if (!feature.bbox) feature.bbox = square(bbox(feature));
});
tree.load(featureCollection(results));
// Split with remaining points - lines might needed to be split multiple times
} else {
// Find all lines that are within the splitter's bbox
var search = tree.search(point);
if (search.features.length) {
// RBush might return multiple lines - only process the closest line to splitter
var closestLine = findClosestFeature(point, search);
// Remove closest line from results since this will be split into two lines
// This removes any duplicates inside the results & index
results = results.filter(function (feature) {
return feature.id !== closestLine.id;
});
tree.remove(closestLine);
// Append the two newly split lines into the results
featureEach(splitLineWithPoint(closestLine, point), function (line) {
results.push(line);
tree.insert(line);
});
}
}
});
return featureCollection(results);
}
/**
* Split LineString with Point
*
* @private
* @param {Feature<LineString>} line LineString
* @param {Feature<Point>} splitter Point
* @returns {FeatureCollection<LineString>} split LineStrings
*/
function splitLineWithPoint(line, splitter) {
var results = [];
// handle endpoints
var startPoint = getCoords(line)[0];
var endPoint = getCoords(line)[line.geometry.coordinates.length - 1];
if (
pointsEquals(startPoint, getCoord(splitter)) ||
pointsEquals(endPoint, getCoord(splitter))
)
return featureCollection([line]);
// Create spatial index
var tree = rbush();
var segments = lineSegment(line);
tree.load(segments);
// Find all segments that are within bbox of splitter
var search = tree.search(splitter);
// Return itself if point is not within spatial index
if (!search.features.length) return featureCollection([line]);
// RBush might return multiple lines - only process the closest line to splitter
var closestSegment = findClosestFeature(splitter, search);
// Initial value is the first point of the first segments (beginning of line)
var initialValue = [startPoint];
var lastCoords = featureReduce(
segments,
function (previous, current, index) {
var currentCoords = getCoords(current)[1];
var splitterCoords = getCoord(splitter);
// Location where segment intersects with line
if (index === closestSegment.id) {
previous.push(splitterCoords);
results.push(lineString(previous));
// Don't duplicate splitter coordinate (Issue #688)
if (pointsEquals(splitterCoords, currentCoords))
return [splitterCoords];
return [splitterCoords, currentCoords];
// Keep iterating over coords until finished or intersection is found
} else {
previous.push(currentCoords);
return previous;
}
},
initialValue
);
// Append last line to final split results
if (lastCoords.length > 1) {
results.push(lineString(lastCoords));
}
return featureCollection(results);
}
/**
* Find Closest Feature
*
* @private
* @param {Feature<Point>} point Feature must be closest to this point
* @param {FeatureCollection<LineString>} lines Collection of Features
* @returns {Feature<LineString>} closest LineString
*/
function findClosestFeature(point, lines) {
if (!lines.features.length) throw new Error("lines must contain features");
// Filter to one segment that is the closest to the line
if (lines.features.length === 1) return lines.features[0];
var closestFeature;
var closestDistance = Infinity;
featureEach(lines, function (segment) {
var pt = nearestPointOnLine(segment, point);
var dist = pt.properties.dist;
if (dist < closestDistance) {
closestFeature = segment;
closestDistance = dist;
}
});
return closestFeature;
}
/**
* Compares two points and returns if they are equals
*
* @private
* @param {Array<number>} pt1 point
* @param {Array<number>} pt2 point
* @returns {boolean} true if they are equals
*/
function pointsEquals(pt1, pt2) {
return pt1[0] === pt2[0] && pt1[1] === pt2[1];
}
export default lineSplit;