| /* |
| * 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(); |
| } |
| } |