geojson-polygon-self-intersections
Version:
Find self-intersections in geojson polygon (possibly with interior rings)
212 lines (211 loc) • 7.33 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import RBush from 'rbush';
const defaultOptions = {
useSpatialIndex: true,
epsilon: 0,
reportVertexOnVertex: false,
reportVertexOnEdge: false,
callbackFunction: ({ isect }) => isect
};
/**
* Return a polygon's self-intersections
*
* @export
* @param {Feature<Polygon>} polygon GeoJSON Polygon
* @param {?(Partial<Options>)} [partialOptions] Option
* @returns {any[]} Array of self-intersection points (or any type produced by the callback function)
*/
export default function gpsi(polygon, partialOptions) {
const options = mergeOptions(defaultOptions, partialOptions);
const coords = polygon.geometry.coordinates;
const output = [];
const seen = {};
let tree;
if (options.useSpatialIndex) {
const allEdgesAsRbushTreeItems = [];
for (let ring0 = 0; ring0 < coords.length; ring0++) {
for (let edge0 = 0; edge0 < coords[ring0].length - 1; edge0++) {
allEdgesAsRbushTreeItems.push(rbushTreeItem(coords, ring0, edge0));
}
}
tree = new RBush();
tree.load(allEdgesAsRbushTreeItems);
}
for (let ring0 = 0; ring0 < coords.length; ring0++) {
for (let edge0 = 0; edge0 < coords[ring0].length - 1; edge0++) {
if (options.useSpatialIndex) {
const bboxOverlaps = tree.search(rbushTreeItem(coords, ring0, edge0));
bboxOverlaps.forEach(function (bboxIsect) {
const ring1 = bboxIsect.ring;
const edge1 = bboxIsect.edge;
ifIsectAddToOutput(ring0, edge0, ring1, edge1);
});
}
else {
for (let ring1 = 0; ring1 < coords.length; ring1++) {
for (let edge1 = 0; edge1 < coords[ring1].length - 1; edge1++) {
// TODO: speedup possible if only interested in unique: start last two loops at ring0 and edge0+1
ifIsectAddToOutput(ring0, edge0, ring1, edge1);
}
}
}
}
}
// Check if two edges intersect and add the intersection to the output
function ifIsectAddToOutput(ring0, edge0, ring1, edge1) {
const start0 = coords[ring0][edge0];
const end0 = coords[ring0][edge0 + 1];
const start1 = coords[ring1][edge1];
const end1 = coords[ring1][edge1 + 1];
const isect = intersect(start0, end0, start1, end1);
if (isect == undefined)
return; // discard parallels and coincidence
let frac0;
let frac1;
if (end0[0] != start0[0]) {
frac0 = (isect[0] - start0[0]) / (end0[0] - start0[0]);
}
else {
frac0 = (isect[1] - start0[1]) / (end0[1] - start0[1]);
}
if (end1[0] != start1[0]) {
frac1 = (isect[0] - start1[0]) / (end1[0] - start1[0]);
}
else {
frac1 = (isect[1] - start1[1]) / (end1[1] - start1[1]);
}
// There are roughly three cases we need to deal with.
// 1. If at least one of the fracs lies outside [0,1], there is no intersection.
if (isOutside(frac0, options.epsilon) ||
isOutside(frac1, options.epsilon)) {
return; // require segment intersection
}
// 2. If both are either exactly 0 or exactly 1, this is not an intersection but just
// two edge segments sharing a common vertex.
if (isBoundaryCase(frac0, options.epsilon) &&
isBoundaryCase(frac1, options.epsilon)) {
if (!options.reportVertexOnVertex)
return;
}
// 3. If only one of the fractions is exactly 0 or 1, this is
// a vertex-on-edge situation.
if (isBoundaryCase(frac0, options.epsilon) ||
isBoundaryCase(frac1, options.epsilon)) {
if (!options.reportVertexOnEdge)
return;
}
const key = isect;
const unique = !seen[String(key)];
if (unique) {
seen[String(key)] = true;
}
output.push(options.callbackFunction({
isect,
ring0,
edge0,
start0,
end0,
frac0,
ring1,
edge1,
start1,
end1,
frac1,
unique
}));
}
return output;
}
/**
* Compute where two lines intersect.
*
* From https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
*
* @param {Position} start0
* @param {Position} end0
* @param {Position} start1
* @param {Position} end1
* @returns {(Position | undefined)} The point of intersection, or undefined if no intersection
*/
export function intersect(start0, end0, start1, end1) {
if (equalArrays(start0, start1) ||
equalArrays(start0, end1) ||
equalArrays(end0, start1) ||
equalArrays(end1, start1)) {
return undefined;
}
const x0 = start0[0], y0 = start0[1], x1 = end0[0], y1 = end0[1], x2 = start1[0], y2 = start1[1], x3 = end1[0], y3 = end1[1];
const denom = (x0 - x1) * (y2 - y3) - (y0 - y1) * (x2 - x3);
if (denom == 0)
return undefined;
const x4 = ((x0 * y1 - y0 * x1) * (x2 - x3) - (x0 - x1) * (x2 * y3 - y2 * x3)) / denom;
const y4 = ((x0 * y1 - y0 * x1) * (y2 - y3) - (y0 - y1) * (x2 * y3 - y2 * x3)) / denom;
return [x4, y4];
}
// Check if arrays are equal.
export function equalArrays(array1, array2) {
// If the other array is a falsy value, return
if (!array1 || !array2)
return false;
// Compare lengths - can save a lot of time
if (array1.length != array2.length)
return false;
for (let i = 0, l = array1.length; i < l; i++) {
// Check if we have nested arrays
if (array1[i] instanceof Array && array2[i] instanceof Array) {
// Recurse into the nested arrays
if (!equalArrays(array1[i], array2[i]))
return false;
}
else if (array1[i] != array2[i]) {
// Warning - two different object instances will never be equal: {x:20} != {x:20}
return false;
}
}
return true;
}
// Check if boundary case: true if frac is (almost) 1.0 or 0.0
function isBoundaryCase(frac, epsilon) {
const e2 = epsilon * epsilon;
return e2 >= (frac - 1) * (frac - 1) || e2 >= frac * frac;
}
// Check if outside
function isOutside(frac, epsilon) {
return frac < 0 - epsilon || frac > 1 + epsilon;
}
// Return a rbush tree item given an ring and edge number
function rbushTreeItem(polygon, ring, edge) {
const start = polygon[ring][edge];
const end = polygon[ring][edge + 1];
let minX;
let maxX;
let minY;
let maxY;
if (start[0] < end[0]) {
minX = start[0];
maxX = end[0];
}
else {
minX = end[0];
maxX = start[0];
}
if (start[1] < end[1]) {
minY = start[1];
maxY = end[1];
}
else {
minY = end[1];
maxY = start[1];
}
return {
minX: minX,
minY: minY,
maxX: maxX,
maxY: maxY,
ring: ring,
edge: edge
};
}
function mergeOptions(option0, options1) {
return { ...option0, ...options1 };
}