View Javadoc
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.spherical.twod;
18  
19  import java.text.MessageFormat;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.stream.Stream;
26  
27  import org.apache.commons.numbers.core.Precision;
28  
29  /** Class representing a connected sequence of {@link GreatArc} instances.
30   */
31  public final class GreatArcPath implements BoundarySource2S {
32      /** Instance containing no arcs. */
33      private static final GreatArcPath EMPTY = new GreatArcPath(Collections.emptyList());
34  
35      /** Arcs comprising the instance. */
36      private final List<GreatArc> arcs;
37  
38      /** Simple constructor. No validation is performed on the input arc.
39       * @param arcs arcs for the path, in connection order
40       */
41      private GreatArcPath(final List<GreatArc> arcs) {
42          this.arcs = Collections.unmodifiableList(arcs);
43      }
44  
45      /** {@inheritDoc} */
46      @Override
47      public Stream<GreatArc> boundaryStream() {
48          return getArcs().stream();
49      }
50  
51      /** Get the arcs in path.
52       * @return the arcs in the path
53       */
54      public List<GreatArc> getArcs() {
55          return arcs;
56      }
57  
58      /** Get the start arc for the path or null if the path is empty.
59       * @return the start arc for the path or null if the path is empty
60       */
61      public GreatArc getStartArc() {
62          if (!isEmpty()) {
63              return arcs.get(0);
64          }
65          return null;
66      }
67  
68      /** Get the end arc for the path or null if the path is empty.
69       * @return the end arc for the path or null if the path is empty
70       */
71      public GreatArc getEndArc() {
72          if (!isEmpty()) {
73              return arcs.get(arcs.size() - 1);
74          }
75          return null;
76      }
77  
78      /** Get the start vertex for the path or null if the path is empty
79       * or consists of a single, full arc.
80       * @return the start vertex for the path
81       */
82      public Point2S getStartVertex() {
83          final GreatArc arc = getStartArc();
84          return (arc != null) ? arc.getStartPoint() : null;
85      }
86  
87      /** Get the end vertex for the path or null if the path is empty
88       * or consists of a single, full arc.
89       * @return the end vertex for the path
90       */
91      public Point2S getEndVertex() {
92          final GreatArc arc = getEndArc();
93          return (arc != null) ? arc.getEndPoint() : null;
94      }
95  
96      /** Get the vertices contained in the path in the order they appear.
97       * Closed paths contain the start vertex at the beginning of the list
98       * as well as the end.
99       * @return the vertices contained in the path in order they appear
100      */
101     public List<Point2S> getVertices() {
102         final List<Point2S> vertices = new ArrayList<>();
103 
104         Point2S pt;
105 
106         // add the start point, if present
107         pt = getStartVertex();
108         if (pt != null) {
109             vertices.add(pt);
110         }
111 
112         // add end points
113         for (final GreatArc arc : arcs) {
114             pt = arc.getEndPoint();
115             if (pt != null) {
116                 vertices.add(pt);
117             }
118         }
119 
120         return vertices;
121     }
122 
123     /** Return true if the path does not contain any arcs.
124      * @return true if the path does not contain any arcs
125      */
126     public boolean isEmpty() {
127         return arcs.isEmpty();
128     }
129 
130     /** Return true if the path is closed, meaning that the end
131      * point for the last arc is equal to the start point
132      * for the path.
133      * @return true if the end point for the last arc is
134      *      equal to the start point for the path
135      */
136     public boolean isClosed() {
137         final GreatArc endArc = getEndArc();
138 
139         if (endArc != null) {
140             final Point2S start = getStartVertex();
141             final Point2S end = endArc.getEndPoint();
142 
143             return start != null && end != null && start.eq(end, endArc.getPrecision());
144         }
145 
146         return false;
147     }
148 
149     /** Return a string representation of this arc path instance.
150     *
151     * <p>In order to keep the string representation short but useful, the exact format of the return
152     * value depends on the properties of the path. See below for examples.
153     *
154     * <ul>
155     *      <li>Empty path
156     *          <ul>
157     *              <li>{@code GreatArcPath[empty= true]}</li>
158     *          </ul>
159     *      </li>
160     *      <li>Single, full arc
161     *          <ul>
162     *              <li>{@code GreatArcPath[full= true, circle= GreatCircle[pole= (0.0, 0.0, 1.0),
163     *              x= (0.0, 1.0, -0.0), y= (-1.0, 0.0, 0.0)]]}</li>
164     *          </ul>
165     *      </li>
166     *      <li>One or more non-full arcs
167     *          <ul>
168     *              <li>{@code GreatArcPath[vertices= [(0.0, 1.5707963267948966),
169     *              (1.5707963267948966, 1.5707963267948966)]]}</li>
170     *          </ul>
171     *      </li>
172     * </ul>
173     */
174     @Override
175     public String toString() {
176         final StringBuilder sb = new StringBuilder();
177         sb.append(this.getClass().getSimpleName())
178             .append('[');
179 
180         if (isEmpty()) {
181             sb.append("empty= true");
182         } else if (arcs.size() == 1 && arcs.get(0).isFull()) {
183             sb.append("full= true, circle= ")
184                 .append(arcs.get(0).getCircle());
185         } else {
186             sb.append("vertices= ")
187                 .append(getVertices());
188         }
189 
190         sb.append(']');
191 
192         return sb.toString();
193     }
194 
195     /** Construct a new path from the given arcs.
196      * @param arcs arc instance to use to construct the path
197      * @return a new instance constructed from the given arc instances
198      */
199     public static GreatArcPath fromArcs(final GreatArc... arcs) {
200         return fromArcs(Arrays.asList(arcs));
201     }
202 
203     /** Construct a new path from the given arcs.
204      * @param arcs arc instance to use to construct the path
205      * @return a new instance constructed from the given arc instances
206      */
207     public static GreatArcPath fromArcs(final Collection<GreatArc> arcs) {
208         final Builder builder = builder(null);
209         for (final GreatArc arc : arcs) {
210             builder.append(arc);
211         }
212 
213         return builder.build();
214     }
215 
216     /** Return a new path formed by connecting the given vertices. An additional arc is added
217      * from the last point to the first point to construct a loop, if the two points are not
218      * already considered equal by the given precision context. This method is equivalent
219      * to calling {@link #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
220      * fromPoints(points, true, precision)}.
221      * @param vertices the points to construct the path from
222      * @param precision precision precision context used to construct the arc instances for the
223      *      path
224      * @return a new path formed by connecting the given vertices
225      * @see #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
226      */
227     public static GreatArcPath fromVertexLoop(final Collection<Point2S> vertices,
228             final Precision.DoubleEquivalence precision) {
229         return fromVertices(vertices, true, precision);
230     }
231 
232     /** Return a new path formed by connecting the given vertices. No additional arc
233      * is inserted to connect the last point to the first. This method is equivalent
234      * to calling {@link #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
235      * fromPoint(points, false, precision)}.
236      * @param vertices the points to construct the path from
237      * @param precision precision context used to construct the arc instances for the
238      *      path
239      * @return a new path formed by connecting the given vertices
240      * @see #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
241      */
242     public static GreatArcPath fromVertices(final Collection<Point2S> vertices,
243             final Precision.DoubleEquivalence precision) {
244         return fromVertices(vertices, false, precision);
245     }
246 
247     /** Return a new path formed by connecting the given vertices.
248      * @param vertices the points to construct the path from
249      * @param close if true, then an additional arc will be added from the last point
250      *      to the first, if the points are not already considered equal by the given
251      *      precision context
252      * @param precision precision context used to construct the arc instances for the
253      *      path
254      * @return a new path formed by connecting the given points
255      */
256     public static GreatArcPath fromVertices(final Collection<Point2S> vertices, final boolean close,
257             final Precision.DoubleEquivalence precision) {
258 
259         return builder(precision)
260                 .appendVertices(vertices)
261                 .build(close);
262     }
263 
264     /** Return a {@link Builder} instance configured with the given precision
265      * context. The precision context is used when building arcs from points
266      * and may be omitted if raw points are not used.
267      * @param precision precision context to use when building arcs from
268      *      raw points; may be null if raw points are not used.
269      * @return a new {@link Builder} instance
270      */
271     public static Builder builder(final Precision.DoubleEquivalence precision) {
272         return new Builder(precision);
273     }
274 
275     /** Get an instance containing no arcs.
276      * @return an instance containing no arcs
277      */
278     public static GreatArcPath empty() {
279         return EMPTY;
280     }
281 
282     /** Class used to build arc paths.
283      */
284     public static final class Builder {
285         /** Arcs appended to the path. */
286         private List<GreatArc> appendedArcs;
287 
288         /** Arcs prepended to the path. */
289         private List<GreatArc> prependedArcs;
290 
291         /** Precision context used when creating arcs directly from points. */
292         private Precision.DoubleEquivalence precision;
293 
294         /** The current point at the start of the path. */
295         private Point2S startVertex;
296 
297         /** The current point at the end of the path. */
298         private Point2S endVertex;
299 
300         /** The precision context used when performing comparisons involving the current
301          * end point.
302          */
303         private Precision.DoubleEquivalence endVertexPrecision;
304 
305         /** Construct a new instance configured with the given precision context. The
306          * precision context is used when building arcs from points and
307          * may be omitted if raw points are not used.
308          * @param precision precision context to use when creating arcs
309          *      from points
310          */
311         private Builder(final Precision.DoubleEquivalence precision) {
312             setPrecision(precision);
313         }
314 
315         /** Set the precision context. This context is used only when creating arcs
316          * from appended or prepended points. It is not used when adding existing
317          * {@link GreatArc} instances since those contain their own precision contexts.
318          * @param builderPrecision precision context to use when creating arcs from points
319          * @return this instance
320          */
321         public Builder setPrecision(final Precision.DoubleEquivalence builderPrecision) {
322             this.precision = builderPrecision;
323 
324             return this;
325         }
326 
327         /** Get the arc at the start of the path or null if it does not exist.
328          * @return the arc at the start of the path
329          */
330         public GreatArc getStartArc() {
331             GreatArc start = getLast(prependedArcs);
332             if (start == null) {
333                 start = getFirst(appendedArcs);
334             }
335             return start;
336         }
337 
338         /** Get the arc at the end of the path or null if it does not exist.
339          * @return the arc at the end of the path
340          */
341         public GreatArc getEndArc() {
342             GreatArc end = getLast(appendedArcs);
343             if (end == null) {
344                 end = getFirst(prependedArcs);
345             }
346             return end;
347         }
348 
349         /** Append an arc to the end of the path.
350          * @param arc arc to append to the path
351          * @return the current builder instance
352          * @throws IllegalStateException if the path contains a previous arc
353          *      and the end point of the previous arc is not equivalent to the
354          *      start point of the given arc
355          */
356         public Builder append(final GreatArc arc) {
357             validateArcsConnected(getEndArc(), arc);
358             appendInternal(arc);
359 
360             return this;
361         }
362 
363         /** Add a vertex to the end of this path. If the path already has an end vertex,
364          * then an arc is added between the previous end vertex and this vertex,
365          * using the configured precision context.
366          * @param vertex the vertex to add
367          * @return this instance
368          * @see #setPrecision(Precision.DoubleEquivalence)
369          */
370         public Builder append(final Point2S vertex) {
371             final Precision.DoubleEquivalence vertexPrecision = getAddPointPrecision();
372 
373             if (endVertex == null) {
374                 // make sure that we're not adding to a full arc
375                 final GreatArc end = getEndArc();
376                 if (end != null) {
377                     throw new IllegalStateException(
378                             MessageFormat.format("Cannot add point {0} after full arc: {1}", vertex, end));
379                 }
380 
381                 // this is the first vertex added
382                 startVertex = vertex;
383                 endVertex = vertex;
384                 endVertexPrecision = vertexPrecision;
385             } else if (!endVertex.eq(vertex, vertexPrecision)) {
386                 // only add the vertex if its not equal to the end point
387                 // of the last arc
388                 appendInternal(GreatCircles.arcFromPoints(endVertex, vertex, endVertexPrecision));
389             }
390 
391             return this;
392         }
393 
394         /** Convenience method for appending a collection of vertices to the path in a single
395          * method call.
396          * @param vertices the vertices to append
397          * @return this instance
398          * @see #append(Point2S)
399          */
400         public Builder appendVertices(final Collection<Point2S> vertices) {
401             for (final Point2S vertex : vertices) {
402                 append(vertex);
403             }
404 
405             return this;
406         }
407 
408         /** Convenience method for appending multiple vertices to the path at once.
409          * @param vertices the points to append
410          * @return this instance
411          * @see #append(Point2S)
412          */
413         public Builder appendVertices(final Point2S... vertices) {
414             return appendVertices(Arrays.asList(vertices));
415         }
416 
417         /** Prepend an arc to the beginning of the path.
418          * @param arc arc to prepend to the path
419          * @return the current builder instance
420          * @throws IllegalStateException if the path contains a start arc
421          *      and the end point of the given arc is not equivalent to the
422          *      start point of the start arc
423          */
424         public Builder prepend(final GreatArc arc) {
425             validateArcsConnected(arc, getStartArc());
426             prependInternal(arc);
427 
428             return this;
429         }
430 
431         /** Add a vertex to the front of this path. If the path already has a start vertex,
432          * then an arc is added between this vertex and the previous start vertex,
433          * using the configured precision context.
434          * @param vertex the vertex to add
435          * @return this instance
436          * @see #setPrecision(Precision.DoubleEquivalence)
437          */
438         public Builder prepend(final Point2S vertex) {
439             final Precision.DoubleEquivalence vertexPrecision = getAddPointPrecision();
440 
441             if (startVertex == null) {
442                 // make sure that we're not adding to a full arc
443                 final GreatArc start = getStartArc();
444                 if (start != null) {
445                     throw new IllegalStateException(
446                             MessageFormat.format("Cannot add point {0} before full arc: {1}", vertex, start));
447                 }
448 
449                 // this is the first vertex added
450                 startVertex = vertex;
451                 endVertex = vertex;
452                 endVertexPrecision = vertexPrecision;
453             } else if (!vertex.eq(startVertex, vertexPrecision)) {
454                 // only add if the vertex is not equal to the start
455                 // point of the first arc
456                 prependInternal(GreatCircles.arcFromPoints(vertex, startVertex, vertexPrecision));
457             }
458 
459             return this;
460         }
461 
462         /** Convenience method for prepending a collection of vertices to the path in a single method call.
463          * The vertices are logically prepended as a single group, meaning that the first vertex
464          * in the given collection appears as the first vertex in the path after this method call.
465          * Internally, this means that the vertices are actually passed to the {@link #prepend(Point2S)}
466          * method in reverse order.
467          * @param vertices the points to prepend
468          * @return this instance
469          * @see #prepend(Point2S)
470          */
471         public Builder prependPoints(final Collection<Point2S> vertices) {
472             return prependPoints(vertices.toArray(new Point2S[0]));
473         }
474 
475         /** Convenience method for prepending multiple vertices to the path in a single method call.
476          * The vertices are logically prepended as a single group, meaning that the first vertex
477          * in the given collection appears as the first vertex in the path after this method call.
478          * Internally, this means that the vertices are actually passed to the {@link #prepend(Point2S)}
479          * method in reverse order.
480          * @param vertices the vertices to prepend
481          * @return this instance
482          * @see #prepend(Point2S)
483          */
484         public Builder prependPoints(final Point2S... vertices) {
485             for (int i = vertices.length - 1; i >= 0; --i) {
486                 prepend(vertices[i]);
487             }
488 
489             return this;
490         }
491 
492         /** Close the current path and build a new {@link GreatArcPath} instance. This method is equivalent
493          * to {@code builder.build(true)}.
494          * @return new closed path instance
495          */
496         public GreatArcPath close() {
497             return build(true);
498         }
499 
500         /** Build a {@link GreatArcPath} instance from the configured path. This method is equivalent
501          * to {@code builder.build(false)}.
502          * @return new path instance
503          */
504         public GreatArcPath build() {
505             return build(false);
506         }
507 
508         /** Build a {@link GreatArcPath} instance from the configured path.
509          * @param close if true, the path will be closed by adding an end point equivalent to the
510          *      start point
511          * @return new path instance
512          */
513         public GreatArcPath build(final boolean close) {
514             if (close) {
515                 closePath();
516             }
517 
518             // combine all of the arcs
519             List<GreatArc> result = null;
520 
521             if (prependedArcs != null) {
522                 result = prependedArcs;
523                 Collections.reverse(result);
524             }
525 
526             if (appendedArcs != null) {
527                 if (result == null) {
528                     result = appendedArcs;
529                 } else {
530                     result.addAll(appendedArcs);
531                 }
532             }
533 
534             if (result == null) {
535                 result = Collections.emptyList();
536             }
537 
538             if (result.isEmpty() && startVertex != null) {
539                 throw new IllegalStateException(
540                         MessageFormat.format("Unable to create path; only a single point provided: {0}",
541                                 startVertex));
542             }
543 
544             // clear internal state
545             appendedArcs = null;
546             prependedArcs = null;
547 
548             // build the final path instance, using the shared empty instance if
549             // no arcs are present
550             return result.isEmpty() ? empty() : new GreatArcPath(result);
551         }
552 
553         /** Close the path by adding an end point equivalent to the path start point.
554          * @throws IllegalStateException if the path cannot be closed
555          */
556         private void closePath() {
557             final GreatArc end = getEndArc();
558 
559             if (end != null) {
560                 if (startVertex != null && endVertex != null) {
561                     if (!endVertex.eq(startVertex, endVertexPrecision)) {
562                         appendInternal(GreatCircles.arcFromPoints(endVertex, startVertex, endVertexPrecision));
563                     }
564                 } else {
565                     throw new IllegalStateException("Unable to close path: path is full");
566                 }
567             }
568         }
569 
570         /** Validate that the given arcs are connected, meaning that the end point of {@code previous}
571          * is equivalent to the start point of {@code next}. The arcs are considered valid if either
572          * arc is null.
573          * @param previous previous arc
574          * @param next next arc
575          * @throws IllegalStateException if previous and next are not null and the end point of previous
576          *      is not equivalent the start point of next
577          */
578         private void validateArcsConnected(final GreatArc previous, final GreatArc next) {
579             if (previous != null && next != null) {
580                 final Point2S nextStartVertex = next.getStartPoint();
581                 final Point2S previousEndVertex = previous.getEndPoint();
582                 final Precision.DoubleEquivalence previousPrecision = previous.getPrecision();
583 
584                 if (nextStartVertex == null || previousEndVertex == null ||
585                         !(nextStartVertex.eq(previousEndVertex, previousPrecision))) {
586 
587                     throw new IllegalStateException(
588                             MessageFormat.format("Path arcs are not connected: previous= {0}, next= {1}",
589                                     previous, next));
590                 }
591             }
592         }
593 
594         /** Get the precision context used when adding raw points to the path. An exception is thrown
595          * if no precision has been specified.
596          * @return the precision context used when working with raw points
597          * @throws IllegalStateException if no precision context is configured
598          */
599         private Precision.DoubleEquivalence getAddPointPrecision() {
600             if (precision == null) {
601                 throw new IllegalStateException("Unable to create arc: no point precision specified");
602             }
603 
604             return precision;
605         }
606 
607         /** Append the given, validated arc to the path.
608          * @param arc validated arc to append
609          */
610         private void appendInternal(final GreatArc arc) {
611             if (appendedArcs == null) {
612                 appendedArcs = new ArrayList<>();
613             }
614 
615             if (appendedArcs.isEmpty() &&
616                     (prependedArcs == null || prependedArcs.isEmpty())) {
617                 startVertex = arc.getStartPoint();
618             }
619 
620             endVertex = arc.getEndPoint();
621             endVertexPrecision = arc.getPrecision();
622 
623             appendedArcs.add(arc);
624         }
625 
626         /** Prepend the given, validated arc to the path.
627          * @param arc validated arc to prepend
628          */
629         private void prependInternal(final GreatArc arc) {
630             if (prependedArcs == null) {
631                 prependedArcs = new ArrayList<>();
632             }
633 
634             startVertex = arc.getStartPoint();
635 
636             if (prependedArcs.isEmpty() &&
637                     (appendedArcs == null || appendedArcs.isEmpty())) {
638                 endVertex = arc.getEndPoint();
639                 endVertexPrecision = arc.getPrecision();
640             }
641 
642             prependedArcs.add(arc);
643         }
644 
645         /** Get the first element in the list or null if the list is null
646          * or empty.
647          * @param list the list to return the first item from
648          * @return the first item from the given list or null if it does not exist
649          */
650         private GreatArc getFirst(final List<GreatArc> list) {
651             if (list != null && !list.isEmpty()) {
652                 return list.get(0);
653             }
654             return null;
655         }
656 
657         /** Get the last element in the list or null if the list is null
658          * or empty.
659          * @param list the list to return the last item from
660          * @return the last item from the given list or null if it does not exist
661          */
662         private GreatArc getLast(final List<GreatArc> list) {
663             if (list != null && !list.isEmpty()) {
664                 return list.get(list.size() - 1);
665             }
666             return null;
667         }
668     }
669 }