sprotty
Version:
A next-gen framework for graphical views
181 lines (167 loc) • 7.75 kB
text/typescript
/********************************************************************************
* Copyright (c) 2019 Rowan Winsemius and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
// Based on the sweepline implementation at https://github.com/rowanwins/sweepline-intersections
// which is published under the terms of MIT, but has been adapted to the use case of sprotty.
import TinyQueue from "tinyqueue";
import { Point } from "sprotty-protocol/lib/utils/geometry";
import { PointToPointLine } from "../../utils/geometry";
import { Intersection } from "./intersection-finder";
import { RoutedPoint } from "../routing/routing";
/*
* The algorithm implemented in this module is loosely based on the Bentley-Ottmann algorithm for
* finding intersection among line segments in `O((n+k) log n)`, whereas `n` is the number of line
* segments and `k` is the number of intersections.
* The underlying idea is to use a imaginary sweep line that moves over the x/y plane and testing
* only the line segments for intersection that the sweepline currently crosses, instead of
* testing all segment with each other, which would be `O(n^2)`.
* It does so by generating a prioritized event queue for start and end events of the line segments
* and working its way through the queue (i.e., sweeping).
* More information can be found at https://en.wikipedia.org/wiki/Bentley%E2%80%93Ottmann_algorithm
* In contrast to the original Bently-Ottmann algorithm, the implementation below does not use a tree
* data structure to store the segments in order to simplify the implementation.
* See also https://github.com/rowanwins/sweepline-intersections#algorithm-notes
*/
/**
* Add the specified `route` to the event `queue` from left to right.
* @param routeId id of the route.
* @param route the route as array of points.
* @param queue the queue to add the route to.
*/
export function addRoute(routeId: string, route: RoutedPoint[], queue: TinyQueue<SweepEvent>) {
if (route.length < 1) return;
let currentPoint = route[0];
let nextPoint = undefined;
for (let i = 0; i < route.length - 1; i++) {
nextPoint = route[i + 1];
const e1 = new SweepEvent(routeId, currentPoint, i);
const e2 = new SweepEvent(routeId, nextPoint, i + 1);
e1.otherEvent = e2;
e2.otherEvent = e1;
if (checkWhichEventIsLeft(e1, e2) > 0) {
e2.isLeftEndpoint = true;
e1.isLeftEndpoint = false;
} else {
e1.isLeftEndpoint = true;
e2.isLeftEndpoint = false;
}
queue.push(e1);
queue.push(e2);
currentPoint = nextPoint;
}
}
/**
* Returns which of the two events is left.
* This is used to classify the endpoints of a segment when generating the
* event queue.
*/
export function checkWhichEventIsLeft(e1: SweepEvent, e2: SweepEvent): 1 | -1 {
if (e1.point.x > e2.point.x) return 1;
if (e1.point.x < e2.point.x) return -1;
if (e1.point.y !== e2.point.y) return e1.point.y > e2.point.y ? 1 : -1;
return 1;
}
/**
* An event -- or with other words a start or end point of a segment -- in the context
* of the event queue for the sweep.
*
* Stores the original Sprotty `edgeId` and the segment index of this segment in the edge
* to keep track of which edge and segment this event originated from.
*/
export class SweepEvent {
otherEvent: SweepEvent;
isLeftEndpoint: boolean;
constructor(readonly edgeId: string, readonly point: Point, readonly segmentIndex: number) { }
}
/**
* A line segment consists of a start and a stop event.
*/
export class Segment {
readonly leftSweepEvent: SweepEvent;
readonly rightSweepEvent: SweepEvent;
constructor(event: SweepEvent) {
this.leftSweepEvent = event;
this.rightSweepEvent = event.otherEvent;
}
}
/**
* Performs the main sweep algorithm on the specified event queue.
*
* An empty priority queue is created to store segments encountered.
* An item is removed from the priority queue if the vertex is the left endpoint
* of a segment, we test it against every other segment in the segment queue for
* intersections with any intersection recorded. We then add the vertex (and it's
* associated right endpoint) to the segment queue.
* If we encounter a right endpoint we remove the first item from the segment queue.
*
* Each pair of segments are only tested once. And only segments that overlap on the
* x plane are tested against each other.
*
* @param eventQueue the event queue.
* @returns the identified intersections.
*/
export function runSweep(eventQueue: TinyQueue<SweepEvent>): Intersection[] {
const intersectionPoints: Intersection[] = [];
const outQueue = new TinyQueue<Segment>([], checkWhichSegmentHasRightEndpointFirst);
while (eventQueue.length) {
const event = eventQueue.pop();
if (event?.isLeftEndpoint) {
const segment = new Segment(event);
for (let i = 0; i < outQueue.data.length; i++) {
const otherSegment = outQueue.data[i];
const intersection = intersectionOfSegments(segment, otherSegment);
if (intersection) {
intersectionPoints.push(
<Intersection>{
routable1: event.edgeId,
routable2: otherSegment.leftSweepEvent.edgeId,
segmentIndex1: getSegmentIndex(segment),
segmentIndex2: getSegmentIndex(otherSegment),
intersectionPoint: intersection
}
);
}
}
outQueue.push(segment);
} else if (event?.isLeftEndpoint === false) {
outQueue.pop();
}
}
return intersectionPoints;
}
/**
* Specifies which of the two specified segments has a right endpoint first.
* Used as a comparator to sort the event queue.
*/
export function checkWhichSegmentHasRightEndpointFirst(seg1: Segment, seg2: Segment): 1 | -1 {
if (seg1.rightSweepEvent.point.x > seg2.rightSweepEvent.point.x) return 1;
if (seg1.rightSweepEvent.point.x < seg2.rightSweepEvent.point.x) return -1;
if (seg1.rightSweepEvent.point.y !== seg2.rightSweepEvent.point.y) return seg1.rightSweepEvent.point.y < seg2.rightSweepEvent.point.y ? 1 : -1;
return 1;
}
export function getSegmentIndex(segment: Segment): number {
return Math.min(segment.leftSweepEvent.segmentIndex, segment.rightSweepEvent.segmentIndex);
}
/**
* Tests whether two segments intersect and returns the intersection point if existing.
*/
export function intersectionOfSegments(seg1: Segment, seg2: Segment): Point | undefined {
if (seg1.leftSweepEvent.edgeId === seg2.leftSweepEvent.edgeId) {
return undefined;
}
const seg1Line = new PointToPointLine(seg1.leftSweepEvent.point, seg1.rightSweepEvent.point);
const seg2Line = new PointToPointLine(seg2.leftSweepEvent.point, seg2.rightSweepEvent.point);
return seg1Line.intersection(seg2Line);
}