1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.geometry.euclidean.internal;
18
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.NavigableSet;
22 import java.util.TreeSet;
23
24 /** Abstract base class for joining unconnected path elements into connected, directional
25 * paths. The connection algorithm is exposed as a set of protected methods, allowing subclasses
26 * to define their own public API. Implementations must supply their own subclass of {@link ConnectableElement}
27 * specific for the objects being connected.
28 *
29 * <p>The connection algorithm proceeds as follows:
30 * <ul>
31 * <li>Create a sorted list of {@link ConnectableElement}s.</li>
32 * <li>For each element, attempt to find other elements with start points next the
33 * first instance's end point by calling {@link ConnectableElement#getConnectionSearchKey()} and
34 * using the returned instance to locate a search start location in the sorted element list.</li>
35 * <li>Search up through the sorted list from the start location, testing each element for possible connectivity
36 * with {@link ConnectableElement#canConnectTo(AbstractPathConnector.ConnectableElement)}. Collect possible
37 * connections in a list. Terminate the search when
38 * {@link ConnectableElement#shouldContinueConnectionSearch(AbstractPathConnector.ConnectableElement, boolean)}
39 * returns false.
40 * <li>Repeat the previous step searching downward through the list from the start location.</li>
41 * <li>Select the best connection option from the list of possible connections, using
42 * {@link #selectPointConnection(AbstractPathConnector.ConnectableElement, List)}
43 * and/or {@link #selectConnection(AbstractPathConnector.ConnectableElement, List)} when multiple possibilities
44 * are found.</li>
45 * <li>Repeat the above steps for each element. When done, the elements represent a linked list
46 * of connected paths.</li>
47 * </ul>
48 *
49 * <p>This class is not thread-safe.</p>
50 *
51 * @param <E> Element type
52 * @see ConnectableElement
53 */
54 public abstract class AbstractPathConnector<E extends AbstractPathConnector.ConnectableElement<E>> {
55 /** List of path elements. */
56 private final NavigableSet<E> pathElements = new TreeSet<>();
57
58 /** View of the path element set in descending order. */
59 private final NavigableSet<E> pathElementsDescending = pathElements.descendingSet();
60
61 /** List used to store possible connections for the current element. */
62 private final List<E> possibleConnections = new ArrayList<>();
63
64 /** List used to store possible point-like (zero-length) connections for the current element. */
65 private final List<E> possiblePointConnections = new ArrayList<>();
66
67 /** Add a collection of path elements to the connector and attempt to connect each new element
68 * with previously added ones.
69 * @param elements path elements to connect
70 */
71 protected void connectPathElements(final Iterable<E> elements) {
72 elements.forEach(this::addPathElement);
73
74 for (final E element : elements) {
75 makeForwardConnection(element);
76 }
77 }
78
79 /** Add a single path element to the connector, leaving it unconnected until a later call to
80 * to {@link #connectPathElements(Iterable)} or {@link #computePathRoots()}.
81 * @param element value to add to the connector
82 * @see #connectPathElements(Iterable)
83 * @see #computePathRoots()
84 */
85 protected void addPathElement(final E element) {
86 pathElements.add(element);
87 }
88
89 /** Compute all connected paths and return a list of path elements representing
90 * the roots (start locations) of each. Each returned element is the head of a
91 * (possibly circular) linked list that follows a connected path.
92 *
93 * <p>The connector is reset after this call. Further calls to add elements
94 * will result in new paths being generated.</p>
95 * @return a list of root elements for the computed connected paths
96 */
97 protected List<E> computePathRoots() {
98 for (final E element : pathElements) {
99 followForwardConnections(element);
100 }
101
102 final List<E> rootEntries = new ArrayList<>();
103 E root;
104 for (final E element : pathElements) {
105 root = element.exportPath();
106 if (root != null) {
107 rootEntries.add(root);
108 }
109 }
110
111 pathElements.clear();
112 possibleConnections.clear();
113 possiblePointConnections.clear();
114
115 return rootEntries;
116 }
117
118 /** Find and follow forward connections from the given start element.
119 * @param start element to begin the connection operation with
120 */
121 private void followForwardConnections(final E start) {
122 E current = start;
123
124 while (current != null && current.hasEnd() && !current.hasNext()) {
125 current = makeForwardConnection(current);
126 }
127 }
128
129 /** Connect the end point of the given element to the start point of another element. Returns
130 * the newly connected element or null if no forward connection was made.
131 * @param element element to connect
132 * @return the next element in the path or null if no connection was made
133 */
134 private E makeForwardConnection(final E element) {
135 findPossibleConnections(element);
136
137 E next = null;
138
139 // select from all available connections, handling point-like segments first
140 if (!possiblePointConnections.isEmpty()) {
141 next = (possiblePointConnections.size() == 1) ?
142 possiblePointConnections.get(0) :
143 selectPointConnection(element, possiblePointConnections);
144 } else if (!possibleConnections.isEmpty()) {
145
146 next = (possibleConnections.size() == 1) ?
147 possibleConnections.get(0) :
148 selectConnection(element, possibleConnections);
149 }
150
151 if (next != null) {
152 element.connectTo(next);
153 }
154
155 return next;
156 }
157
158 /** Find possible connections for the given element and place them in the
159 * {@link #possibleConnections} and {@link #possiblePointConnections} lists.
160 * @param element the element to find connections for
161 */
162 private void findPossibleConnections(final E element) {
163 possibleConnections.clear();
164 possiblePointConnections.clear();
165
166 if (element.hasEnd()) {
167 final E searchKey = element.getConnectionSearchKey();
168
169 // search up
170 for (final E candidate : pathElements.tailSet(searchKey)) {
171 if (!addPossibleConnection(element, candidate) &&
172 !element.shouldContinueConnectionSearch(candidate, true)) {
173 break;
174 }
175 }
176
177 // search down
178 for (final E candidate : pathElementsDescending.tailSet(searchKey, false)) {
179 if (!addPossibleConnection(element, candidate) &&
180 !element.shouldContinueConnectionSearch(candidate, false)) {
181 break;
182 }
183 }
184 }
185 }
186
187 /** Add the candidate to one of the connection lists if it represents a possible connection. Returns
188 * true if the candidate was added, otherwise false.
189 * @param element element to check for connections with
190 * @param candidate candidate connection element
191 * @return true if the candidate is a possible connection
192 */
193 private boolean addPossibleConnection(final E element, final E candidate) {
194 if (element != candidate &&
195 !candidate.hasPrevious() &&
196 candidate.hasStart() &&
197 element.canConnectTo(candidate)) {
198
199 if (element.endPointsEq(candidate)) {
200 possiblePointConnections.add(candidate);
201 } else {
202 possibleConnections.add(candidate);
203 }
204
205 return true;
206 }
207
208 return false;
209 }
210
211 /** Method called to select a connection to use for a given element when multiple zero-length connections are
212 * available. The algorithm here attempts to choose the point most likely to produce a logical path by selecting
213 * the outgoing element with the smallest relative angle with the incoming element, with unconnected element
214 * preferred over ones that are already connected (thereby allowing other connections to occur in the path).
215 * @param incoming the incoming element
216 * @param outgoingList list of available outgoing point-like connections
217 * @return the connection to use
218 */
219 protected E selectPointConnection(final E incoming, final List<E> outgoingList) {
220
221 double angle;
222 boolean isUnconnected;
223
224 double smallestAngle = 0.0;
225 E bestElement = null;
226 boolean bestIsUnconnected = false;
227
228 for (final E outgoing : outgoingList) {
229 angle = Math.abs(incoming.getRelativeAngle(outgoing));
230 isUnconnected = !outgoing.hasNext();
231
232 if (bestElement == null || (!bestIsUnconnected && isUnconnected) ||
233 (bestIsUnconnected == isUnconnected && angle < smallestAngle)) {
234
235 smallestAngle = angle;
236 bestElement = outgoing;
237 bestIsUnconnected = isUnconnected;
238 }
239 }
240
241 return bestElement;
242 }
243
244 /** Method called to select a connection to use for a given segment when multiple non-length-zero
245 * connections are available. In this case, the selection of the outgoing connection depends only
246 * on the desired characteristics of the connected path.
247 * @param incoming the incoming segment
248 * @param outgoing list of available outgoing connections; will always contain at least
249 * two elements
250 * @return the connection to use
251 */
252 protected abstract E selectConnection(E incoming, List<E> outgoing);
253
254 /** Class used to represent connectable path elements for use with {@link AbstractPathConnector}.
255 * Subclasses must fulfill the following requirements in order for path connection operations
256 * to work correctly:
257 * <ul>
258 * <li>Implement {@link #compareTo(Object)} such that elements are sorted by their start
259 * point locations. Other criteria may be used as well but elements with start points in close
260 * proximity must be grouped together.</li>
261 * <li>Implement {@link #getConnectionSearchKey()} such that it returns an instance that will be placed
262 * next to elements with start points close to the current instance's end point when sorted with
263 * {@link #compareTo(Object)}.</li>
264 * <li>Implement {@link #shouldContinueConnectionSearch(AbstractPathConnector.ConnectableElement, boolean)}
265 * such that it returns false when the search for possible connections through a sorted list of elements
266 * may terminate.</li>
267 * </ul>
268 *
269 * @param <E> Element type
270 * @see AbstractPathConnector
271 */
272 public abstract static class ConnectableElement<E extends ConnectableElement<E>>
273 implements Comparable<E> {
274 /** Next connected element. */
275 private E next;
276
277 /** Previous connected element. */
278 private E previous;
279
280 /** Flag set to true when this element has exported its value to a path. */
281 private boolean exported;
282
283 /** Return true if the instance is connected to another element's start point.
284 * @return true if the instance has a next element
285 */
286 public boolean hasNext() {
287 return next != null;
288 }
289
290 /** Get the next connected element in the path, if any.
291 * @return the next connected segment in the path; may be null
292 */
293 public E getNext() {
294 return next;
295 }
296
297 /** Set the next connected element for this path. This is intended for
298 * internal use only. Callers should use the {@link #connectTo(AbstractPathConnector.ConnectableElement)}
299 * method instead.
300 * @param next next path element
301 */
302 protected void setNext(final E next) {
303 this.next = next;
304 }
305
306 /** Return true if another element is connected to this instance's start point.
307 * @return true if the instance has a previous element
308 */
309 public boolean hasPrevious() {
310 return previous != null;
311 }
312
313 /** Get the previous connected element in the path, if any.
314 * @return the previous connected element in the path; may be null
315 */
316 public E getPrevious() {
317 return previous;
318 }
319
320 /** Set the previous connected element for this path. This is intended for
321 * internal use only. Callers should use the {@link #connectTo(AbstractPathConnector.ConnectableElement)}
322 * method instead.
323 * @param previous previous path element
324 */
325 protected void setPrevious(final E previous) {
326 this.previous = previous;
327 }
328
329 /** Connect this instance's end point to the given element's start point. No validation
330 * is performed in this method. The {@link #canConnectTo(AbstractPathConnector.ConnectableElement)}
331 * method must have been called previously.
332 * @param nextElement the next element in the path
333 */
334 public void connectTo(final E nextElement) {
335 setNext(nextElement);
336 nextElement.setPrevious(getSelf());
337 }
338
339 /** Export the path that this element belongs to, returning the root
340 * segment. This method traverses all connected element, sets their
341 * exported flags to true, and returns the root element of the path
342 * (or this element in the case of a loop). Each path can only be
343 * exported once. Later calls to this method on this instance or any of its
344 * connected elements will return null.
345 * @return the root of the path or null if the path that this element
346 * belongs to has already been exported
347 */
348 public E exportPath() {
349 if (markExported()) {
350
351 // export the connected portions of the path, moving both
352 // forward and backward
353 E current;
354 E root = getSelf();
355
356 // forward
357 current = next;
358 while (current != null && current.markExported()) {
359 current = current.getNext();
360 }
361
362 // backward
363 current = previous;
364 while (current != null && current.markExported()) {
365 root = current;
366 current = current.getPrevious();
367 }
368
369 return root;
370 }
371
372 return null;
373 }
374
375 /** Set the export flag for this instance to true. Returns true
376 * if the flag was changed and false otherwise.
377 * @return true if the flag was changed and false if it was
378 * already set to true
379 */
380 protected boolean markExported() {
381 if (!exported) {
382 exported = true;
383 return true;
384 }
385 return false;
386 }
387
388 /** Return true if this instance has a start point that can be
389 * connected to another element's end point.
390 * @return true if this instance has a start point that can be
391 * connected to another element's end point
392 */
393 public abstract boolean hasStart();
394
395 /** Return true if this instance has an end point that can be
396 * connected to another element's start point.
397 * @return true if this instance has an end point that can be
398 * connected to another element's start point
399 */
400 public abstract boolean hasEnd();
401
402 /** Return true if the end point of this instance should be considered
403 * equivalent to the end point of the argument.
404 * @param other element to compare end points with
405 * @return true if this instance has an end point equivalent to that
406 * of the argument
407 */
408 public abstract boolean endPointsEq(E other);
409
410 /** Return true if this instance's end point can be connected to
411 * the argument's start point.
412 * @param nextElement candidate for the next element in the path; this value
413 * is guaranteed to not be null and to contain a start point
414 * @return true if this instance's end point can be connected to
415 * the argument's start point
416 */
417 public abstract boolean canConnectTo(E nextElement);
418
419 /** Return the relative angle between this element and the argument.
420 * @param other element to compute the angle with
421 * @return the relative angle between this element and the argument
422 */
423 public abstract double getRelativeAngle(E other);
424
425 /** Get a new instance used as a search key to help locate other elements
426 * with start points matching this instance's end point. The only restriction
427 * on the returned instance is that it be compatible with the implementation
428 * class' {@link #compareTo(Object)} method.
429 * @return a new instance used to help locate other path elements with start
430 * points equivalent to this instance's end point
431 */
432 public abstract E getConnectionSearchKey();
433
434 /** Return true if the search for possible connections should continue through
435 * the sorted set of possible path elements given the current candidate element
436 * and search direction. The search operation stops for the given direction
437 * when this method returns false.
438 * @param candidate last tested candidate connection element
439 * @param ascending true if the search is proceeding in an ascending direction;
440 * false otherwise
441 * @return true if the connection search should continue
442 */
443 public abstract boolean shouldContinueConnectionSearch(E candidate, boolean ascending);
444
445 /** Return the current instance as the generic type.
446 * @return the current instance as the generic type.
447 */
448 protected abstract E getSelf();
449 }
450 }