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.geometry.core.precision.DoublePrecisionContext;
    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, DoublePrecisionContext)
    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, DoublePrecisionContext)
    226      */
    227     public static GreatArcPath fromVertexLoop(final Collection<Point2S> vertices,
    228             final DoublePrecisionContext 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, DoublePrecisionContext)
    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, DoublePrecisionContext)
    241      */
    242     public static GreatArcPath fromVertices(final Collection<Point2S> vertices,
    243             final DoublePrecisionContext 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 DoublePrecisionContext 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 DoublePrecisionContext 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 = null;
    287
    288         /** Arcs prepended to the path. */
    289         private List<GreatArc> prependedArcs = null;
    290
    291         /** Precision context used when creating arcs directly from points. */
    292         private DoublePrecisionContext 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 DoublePrecisionContext 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 DoublePrecisionContext 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 DoublePrecisionContext 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(DoublePrecisionContext)
    369          */
    370         public Builder append(final Point2S vertex) {
    371             final DoublePrecisionContext 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(DoublePrecisionContext)
    437          */
    438         public Builder prepend(final Point2S vertex) {
    439             final DoublePrecisionContext 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 DoublePrecisionContext 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 DoublePrecisionContext 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 }