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 }