blob: 1ab732d4418387ca546895dfe3f9e85845d1061a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.euclidean.internal;
import java.util.ArrayList;
import java.util.List;
import java.util.NavigableSet;
import java.util.TreeSet;
/** Abstract base class for joining unconnected path elements into connected, directional
* paths. The connection algorithm is exposed as a set of protected methods, allowing subclasses
* to define their own public API. Implementations must supply their own subclass of {@link ConnectableElement}
* specific for the objects being connected.
*
* <p>The connection algorithm proceeds as follows:
* <ul>
* <li>Create a sorted list of {@link ConnectableElement}s.</li>
* <li>For each element, attempt to find other elements with start points next the
* first instance's end point by calling {@link ConnectableElement#getConnectionSearchKey()} and
* using the returned instance to locate a search start location in the sorted element list.</li>
* <li>Search up through the sorted list from the start location, testing each element for possible connectivity
* with {@link ConnectableElement#canConnectTo(AbstractPathConnector.ConnectableElement)}. Collect possible
* connections in a list. Terminate the search when
* {@link ConnectableElement#shouldContinueConnectionSearch(AbstractPathConnector.ConnectableElement, boolean)}
* returns false.
* <li>Repeat the previous step searching downward through the list from the start location.</li>
* <li>Select the best connection option from the list of possible connections, using
* {@link #selectPointConnection(AbstractPathConnector.ConnectableElement, List)}
* and/or {@link #selectConnection(AbstractPathConnector.ConnectableElement, List)} when multiple possibilities
* are found.</li>
* <li>Repeat the above steps for each element. When done, the elements represent a linked list
* of connected paths.</li>
* </ul>
*
* <p>This class is not thread-safe.</p>
*
* @param <E> Element type
* @see ConnectableElement
*/
public abstract class AbstractPathConnector<E extends AbstractPathConnector.ConnectableElement<E>> {
/** List of path elements. */
private final NavigableSet<E> pathElements = new TreeSet<>();
/** View of the path element set in descending order. */
private final NavigableSet<E> pathElementsDescending = pathElements.descendingSet();
/** List used to store possible connections for the current element. */
private final List<E> possibleConnections = new ArrayList<>();
/** List used to store possible point-like (zero-length) connections for the current element. */
private final List<E> possiblePointConnections = new ArrayList<>();
/** Add a collection of path elements to the connector and attempt to connect each new element
* with previously added ones.
* @param elements path elements to connect
*/
protected void connectPathElements(final Iterable<E> elements) {
elements.forEach(this::addPathElement);
for (final E element : elements) {
makeForwardConnection(element);
}
}
/** Add a single path element to the connector, leaving it unconnected until a later call to
* to {@link #connectPathElements(Iterable)} or {@link #computePathRoots()}.
* @param element value to add to the connector
* @see #connectPathElements(Iterable)
* @see #computePathRoots()
*/
protected void addPathElement(final E element) {
pathElements.add(element);
}
/** Compute all connected paths and return a list of path elements representing
* the roots (start locations) of each. Each returned element is the head of a
* (possibly circular) linked list that follows a connected path.
*
* <p>The connector is reset after this call. Further calls to add elements
* will result in new paths being generated.</p>
* @return a list of root elements for the computed connected paths
*/
protected List<E> computePathRoots() {
for (final E segment : pathElements) {
followForwardConnections(segment);
}
List<E> rootEntries = new ArrayList<>();
E root;
for (final E segment : pathElements) {
root = segment.exportPath();
if (root != null) {
rootEntries.add(root);
}
}
pathElements.clear();
possibleConnections.clear();
possiblePointConnections.clear();
return rootEntries;
}
/** Find and follow forward connections from the given start element.
* @param start element to begin the connection operation with
*/
private void followForwardConnections(final E start) {
E current = start;
while (current != null && current.hasEnd() && !current.hasNext()) {
current = makeForwardConnection(current);
}
}
/** Connect the end point of the given element to the start point of another element. Returns
* the newly connected element or null if no forward connection was made.
* @param element element to connect
* @return the next element in the path or null if no connection was made
*/
private E makeForwardConnection(final E element) {
findPossibleConnections(element);
E next = null;
// select from all available connections, handling point-like segments first
if (!possiblePointConnections.isEmpty()) {
next = (possiblePointConnections.size() == 1) ?
possiblePointConnections.get(0) :
selectPointConnection(element, possiblePointConnections);
} else if (!possibleConnections.isEmpty()) {
next = (possibleConnections.size() == 1) ?
possibleConnections.get(0) :
selectConnection(element, possibleConnections);
}
if (next != null) {
element.connectTo(next);
}
return next;
}
/** Find possible connections for the given element and place them in the
* {@link #possibleConnections} and {@link #possiblePointConnections} lists.
* @param element the element to find connections for
*/
private void findPossibleConnections(final E element) {
possibleConnections.clear();
possiblePointConnections.clear();
if (element.hasEnd()) {
final E searchKey = element.getConnectionSearchKey();
// search up
for (E candidate : pathElements.tailSet(searchKey)) {
if (!addPossibleConnection(element, candidate) &&
!element.shouldContinueConnectionSearch(candidate, true)) {
break;
}
}
// search down
for (E candidate : pathElementsDescending.tailSet(searchKey, false)) {
if (!addPossibleConnection(element, candidate) &&
!element.shouldContinueConnectionSearch(candidate, false)) {
break;
}
}
}
}
/** Add the candidate to one of the connection lists if it represents a possible connection. Returns
* true if the candidate was added, otherwise false.
* @param element element to check for connections with
* @param candidate candidate connection element
* @return true if the candidate is a possible connection
*/
private boolean addPossibleConnection(final E element, final E candidate) {
if (element != candidate &&
!candidate.hasPrevious() &&
candidate.hasStart() &&
element.canConnectTo(candidate)) {
if (element.endPointsEq(candidate)) {
possiblePointConnections.add(candidate);
} else {
possibleConnections.add(candidate);
}
return true;
}
return false;
}
/** Method called to select a connection to use for a given element when multiple zero-length connections are
* available. The algorithm here attempts to choose the point most likely to produce a logical path by selecting
* the outgoing element with the smallest relative angle with the incoming element, with unconnected element
* preferred over ones that are already connected (thereby allowing other connections to occur in the path).
* @param incoming the incoming element
* @param outgoingList list of available outgoing point-like connections
* @return the connection to use
*/
protected E selectPointConnection(final E incoming, final List<E> outgoingList) {
double angle;
boolean isUnconnected;
double smallestAngle = 0.0;
E bestElement = null;
boolean bestIsUnconnected = false;
for (final E outgoing : outgoingList) {
angle = Math.abs(incoming.getRelativeAngle(outgoing));
isUnconnected = !outgoing.hasNext();
if (bestElement == null || (!bestIsUnconnected && isUnconnected) ||
(bestIsUnconnected == isUnconnected && angle < smallestAngle)) {
smallestAngle = angle;
bestElement = outgoing;
bestIsUnconnected = isUnconnected;
}
}
return bestElement;
}
/** Method called to select a connection to use for a given segment when multiple non-length-zero
* connections are available. In this case, the selection of the outgoing connection depends only
* on the desired characteristics of the connected path.
* @param incoming the incoming segment
* @param outgoing list of available outgoing connections; will always contain at least
* two elements
* @return the connection to use
*/
protected abstract E selectConnection(E incoming, List<E> outgoing);
/** Class used to represent connectable path elements for use with {@link AbstractPathConnector}.
* Subclasses must fulfill the following requirements in order for path connection operations
* to work correctly:
* <ul>
* <li>Implement {@link #compareTo(Object)} such that elements are sorted by their start
* point locations. Other criteria may be used as well but elements with start points in close
* proximity must be grouped together.</li>
* <li>Implement {@link #getConnectionSearchKey()} such that it returns an instance that will be placed
* next to elements with start points close to the current instance's end point when sorted with
* {@link #compareTo(Object)}.</li>
* <li>Implement {@link #shouldContinueConnectionSearch(AbstractPathConnector.ConnectableElement, boolean)}
* such that it returns false when the search for possible connections through a sorted list of elements
* may terminate.</li>
* </ul>
*
* @param <E> Element type
* @see AbstractPathConnector
*/
public abstract static class ConnectableElement<E extends ConnectableElement<E>>
implements Comparable<E> {
/** Next connected element. */
private E next;
/** Previous connected element. */
private E previous;
/** Flag set to true when this element has exported its value to a path. */
private boolean exported = false;
/** Return true if the instance is connected to another element's start point.
* @return true if the instance has a next element
*/
public boolean hasNext() {
return next != null;
}
/** Get the next connected element in the path, if any.
* @return the next connected segment in the path; may be null
*/
public E getNext() {
return next;
}
/** Set the next connected element for this path. This is intended for
* internal use only. Callers should use the {@link #connectTo(AbstractPathConnector.ConnectableElement)}
* method instead.
* @param next next path element
*/
protected void setNext(final E next) {
this.next = next;
}
/** Return true if another element is connected to this instance's start point.
* @return true if the instance has a previous element
*/
public boolean hasPrevious() {
return previous != null;
}
/** Get the previous connected element in the path, if any.
* @return the previous connected element in the path; may be null
*/
public E getPrevious() {
return previous;
}
/** Set the previous connected element for this path. This is intended for
* internal use only. Callers should use the {@link #connectTo(AbstractPathConnector.ConnectableElement)}
* method instead.
* @param previous previous path element
*/
protected void setPrevious(final E previous) {
this.previous = previous;
}
/** Connect this instance's end point to the given element's start point. No validation
* is performed in this method. The {@link #canConnectTo(AbstractPathConnector.ConnectableElement)}
* method must have been called previously.
* @param nextElement the next element in the path
*/
public void connectTo(final E nextElement) {
setNext(nextElement);
nextElement.setPrevious(getSelf());
}
/** Export the path that this element belongs to, returning the root
* segment. This method traverses all connected element, sets their
* exported flags to true, and returns the root element of the path
* (or this element in the case of a loop). Each path can only be
* exported once. Later calls to this method on this instance or any of its
* connected elements will return null.
* @return the root of the path or null if the path that this element
* belongs to has already been exported
*/
public E exportPath() {
if (markExported()) {
// export the connected portions of the path, moving both
// forward and backward
E current;
E root = getSelf();
// forward
current = next;
while (current != null && current.markExported()) {
current = current.getNext();
}
// backward
current = previous;
while (current != null && current.markExported()) {
root = current;
current = current.getPrevious();
}
return root;
}
return null;
}
/** Set the export flag for this instance to true. Returns true
* if the flag was changed and false otherwise.
* @return true if the flag was changed and false if it was
* already set to true
*/
protected boolean markExported() {
if (!exported) {
exported = true;
return true;
}
return false;
}
/** Return true if this instance has a start point that can be
* connected to another element's end point.
* @return true if this instance has a start point that can be
* connected to another element's end point
*/
public abstract boolean hasStart();
/** Return true if this instance has an end point that can be
* connected to another element's start point.
* @return true if this instance has an end point that can be
* connected to another element's start point
*/
public abstract boolean hasEnd();
/** Return true if the end point of this instance should be considered
* equivalent to the end point of the argument.
* @param other element to compare end points with
* @return true if this instance has an end point equivalent to that
* of the argument
*/
public abstract boolean endPointsEq(E other);
/** Return true if this instance's end point can be connected to
* the argument's start point.
* @param nextElement candidate for the next element in the path; this value
* is guaranteed to not be null and to contain a start point
* @return true if this instance's end point can be connected to
* the argument's start point
*/
public abstract boolean canConnectTo(E nextElement);
/** Return the relative angle between this element and the argument.
* @param other element to compute the angle with
* @return the relative angle between this element and the argument
*/
public abstract double getRelativeAngle(E other);
/** Get a new instance used as a search key to help locate other elements
* with start points matching this instance's end point. The only restriction
* on the returned instance is that it be compatible with the implementation
* class' {@link #compareTo(Object)} method.
* @return a new instance used to help locate other path elements with start
* points equivalent to this instance's end point
*/
public abstract E getConnectionSearchKey();
/** Return true if the search for possible connections should continue through
* the sorted set of possible path elements given the current candidate element
* and search direction. The search operation stops for the given direction
* when this method returns false.
* @param candidate last tested candidate connection element
* @param ascending true if the search is proceeding in an ascending direction;
* false otherwise
* @return true if the connection search should continue
*/
public abstract boolean shouldContinueConnectionSearch(E candidate, boolean ascending);
/** Return the current instance as the generic type.
* @return the current instance as the generic type.
*/
protected abstract E getSelf();
}
}