isect/sweep.js
2018-10-15 00:07:54 -07:00

331 lines
8.4 KiB
JavaScript

import createEventQueue from './src/createEventQueue';
import createSweepStatus from './src/sweepStatus';
import SweepEvent from './src/SweepEvent';
import {intersectSegments, EPS, angle, samePoint} from './src/geom';
/**
* A point on a line
*
* @typedef {Object} Point
* @property {number} x coordinate
* @property {number} y coordinate
*/
/**
* @typedef {Object} Segment
* @property {Point} from start of the segment
* @property {Point} to end of the segment
*/
/**
* @typedef {function(point : Point, interior : Segment[], lower : Segment[], upper : Segment[])} ReportIntersectionCallback
*/
/**
* @typedef {Object} ISectOptions
* @property {ReportIntersectionCallback} onFound
*/
/**
* @typedef {Object} ISectResult
*/
// We use EMPTY array to avoid pressure on garbage collector. Need to be
// very cautious to not mutate this array.
var EMPTY = [];
/**
* Finds all intersections among given segments.
*
* The algorithm follows "Computation Geometry, Algorithms and Applications" book
* by Mark de Berg, Otfried Cheong, Marc van Kreveld, and Mark Overmars.
*
* Line is swept top-down
*
* @param {Segment[]} segments
* @param {ISectOptions=} options
* @returns {ISectResult}
*/
export default function isect(segments, options) {
var results = [];
var reportIntersection = (options && options.onFound) || defaultIntersectionReporter;
var onError = (options && options.onError) || defaultErrorReporter;
var eventQueue = createEventQueue(byY);
var sweepStatus = createSweepStatus(onError, EPS);
var lower, interior, lastPoint;
segments.forEach(addSegment);
return {
/**
* Find all intersections synchronously.
*
* @returns array of found intersections.
*/
run,
/**
* Performs a single step in the sweep line algorithm
*
* @returns true if there was something to process; False if no more work to do
*/
step,
// Methods below are low level API for fine-grained control.
// Don't use it unless you understand this code thoroughly
/**
* Add segment into the
*/
addSegment,
/**
* Direct access to event queue. Queue contains segment endpoints and
* pending detected intersections.
*/
eventQueue,
/**
* Direct access to sweep line status. "Status" holds information about
* all intersected segments.
*/
sweepStatus,
/**
* Access to results array. Works only when you use default onFound() handler
*/
results
}
function run() {
while (!eventQueue.isEmpty()) {
var eventPoint = eventQueue.pop();
if (handleEventPoint(eventPoint)) {
// they decided to stop.
return;
};
}
return results;
}
function step() {
if (!eventQueue.isEmpty()) {
var eventPoint = eventQueue.pop();
handleEventPoint(eventPoint);
// Note: we don't check results of `handleEventPoint()`
// assumption is that client controls `step()` and thus they
// know better if they want to stop.
return true;
}
return false;
}
function handleEventPoint(p) {
lastPoint = p.point;
var upper = p.from || EMPTY;
lower = interior = undefined;
// TODO: move lower/interior into sweep status method?
sweepStatus.findSegmentsWithPoint(lastPoint, addLowerOrInterior);
// if (segmentsWithPoint) {
// segmentsWithPoint.forEach()
// }
if (!lower) lower = EMPTY;
if (!interior) interior = EMPTY;
var uLength = upper.length;
var iLength = interior.length;
var lLength = lower.length;
var hasIntersection = uLength + iLength + lLength > 1;
var hasPointIntersection = !hasIntersection && (uLength === 0 && lLength === 0 && iLength > 0);
if (hasIntersection || hasPointIntersection) {
p.isReported = true;
if (reportIntersection(lastPoint, union(interior, union(lower, upper)))) {
return true;
}
}
sweepStatus.deleteSegments(lower, interior, lastPoint);
sweepStatus.insertSegments(interior, upper, lastPoint);
var sLeft, sRight;
var hasNoCrossing = (uLength + iLength === 0);
if (hasNoCrossing) {
var leftRight = sweepStatus.getLeftRightPoint(lastPoint);
sLeft = leftRight.left;
if (!sLeft) return;
sRight = leftRight.right;
if (!sRight) return;
findNewEvent(sLeft, sRight, p);
} else {
var boundarySegments = sweepStatus.getBoundarySegments(upper, interior);
findNewEvent(boundarySegments.beforeLeft, boundarySegments.left, p);
findNewEvent(boundarySegments.right, boundarySegments.afterRight, p);
}
return false;
}
function addLowerOrInterior(s) {
if (samePoint(s.to, lastPoint)) {
if (!lower) lower = [s];
else lower.push(s);
} else if (!samePoint(s.from, lastPoint)) {
if (!interior) interior = [s];
else interior.push(s);
}
}
function findNewEvent(left, right, p) {
if (!left || !right) return;
var intersection = intersectSegments(left, right);
if (!intersection) {
return;
}
var dy = p.point.y - intersection.y
// TODO: should I add dy to intersection.y?
if (dy < -EPS) {
// this means intersection happened after the sweep line.
// We already processed it.
return;
}
if (Math.abs(dy) < EPS && intersection.x <= p.point.x) {
return;
}
// Need to adjust floating point for this special case,
// since otherwise it gives rounding errors:
roundNearZero(intersection);
var current = eventQueue.find(intersection);
if (current && current.isReported) {
// We already reported this event. No need to add it one more time
// TODO: Is this case even possible?
onError('We already reported this event.');
return;
}
if (!current) {
var event = new SweepEvent(intersection)
eventQueue.insert(event);
}
}
function defaultIntersectionReporter(p, segments) {
results.push({
point: p,
segments: segments
});
}
function addSegment(segment) {
var from = segment.from;
var to = segment.to;
// Small numbers give more precision errors. Rounding them to 0.
roundNearZero(from);
roundNearZero(to);
var dy = from.y - to.y;
// Note: dy is much smaller then EPS on purpose. I found that higher
// precision here does less good - getting way more rounding errors.
if (Math.abs(dy) < 1e-5) {
from.y = to.y;
segment.dy = 0;
}
if ((from.y < to.y) || (
(from.y === to.y) && (from.x > to.x))
) {
var temp = from;
from = segment.from = to;
to = segment.to = temp;
}
// We pre-compute some immutable properties of the segment
// They are used quite often in the tree traversal, and pre-computation
// gives significant boost:
segment.dy = from.y - to.y;
segment.dx = from.x - to.x;
segment.angle = angle(segment.dy, segment.dx);
var isPoint = segment.dy === segment.dx && segment.dy === 0;
var prev = eventQueue.find(from)
if (prev && !isPoint) {
// this detects identical segments early. Without this check
// the algorithm would break since sweep line has no means to
// detect identical segments.
var prevFrom = prev.data.from;
if (prevFrom) {
for (var i = 0; i < prevFrom.length; ++i) {
var s = prevFrom[i];
if (samePoint(s.to, to)) {
reportIntersection(s.from, [s.from, s.to]);
reportIntersection(s.to, [s.from, s.to]);
return;
}
}
}
}
if (!isPoint) {
if (prev) {
if (prev.data.from) prev.data.from.push(segment);
else prev.data.from = [segment];
} else {
var e = new SweepEvent(from, segment)
eventQueue.insert(e);
}
var event = new SweepEvent(to)
eventQueue.insert(event)
} else {
var event = new SweepEvent(to)
eventQueue.insert(event)
}
}
}
function roundNearZero(point) {
if (Math.abs(point.x) < EPS) point.x = 0;
if (Math.abs(point.y) < EPS) point.y = 0;
}
function defaultErrorReporter(errorMessage) {
throw new Error(errorMessage);
}
function union(a, b) {
if (!a) return b;
if (!b) return a;
return a.concat(b);
}
function byY(a, b) {
// decreasing Y
var res = b.y - a.y;
// TODO: This might mess up the status tree.
if (Math.abs(res) < EPS) {
// increasing x.
res = a.x - b.x;
if (Math.abs(res) < EPS) res = 0;
}
return res;
}