GreatArcPath.java

  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. import java.text.MessageFormat;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collection;
  22. import java.util.Collections;
  23. import java.util.List;
  24. import java.util.stream.Stream;

  25. import org.apache.commons.numbers.core.Precision;

  26. /** Class representing a connected sequence of {@link GreatArc} instances.
  27.  */
  28. public final class GreatArcPath implements BoundarySource2S {
  29.     /** Instance containing no arcs. */
  30.     private static final GreatArcPath EMPTY = new GreatArcPath(Collections.emptyList());

  31.     /** Arcs comprising the instance. */
  32.     private final List<GreatArc> arcs;

  33.     /** Simple constructor. No validation is performed on the input arc.
  34.      * @param arcs arcs for the path, in connection order
  35.      */
  36.     private GreatArcPath(final List<GreatArc> arcs) {
  37.         this.arcs = Collections.unmodifiableList(arcs);
  38.     }

  39.     /** {@inheritDoc} */
  40.     @Override
  41.     public Stream<GreatArc> boundaryStream() {
  42.         return getArcs().stream();
  43.     }

  44.     /** Get the arcs in path.
  45.      * @return the arcs in the path
  46.      */
  47.     public List<GreatArc> getArcs() {
  48.         return arcs;
  49.     }

  50.     /** Get the start arc for the path or null if the path is empty.
  51.      * @return the start arc for the path or null if the path is empty
  52.      */
  53.     public GreatArc getStartArc() {
  54.         if (!isEmpty()) {
  55.             return arcs.get(0);
  56.         }
  57.         return null;
  58.     }

  59.     /** Get the end arc for the path or null if the path is empty.
  60.      * @return the end arc for the path or null if the path is empty
  61.      */
  62.     public GreatArc getEndArc() {
  63.         if (!isEmpty()) {
  64.             return arcs.get(arcs.size() - 1);
  65.         }
  66.         return null;
  67.     }

  68.     /** Get the start vertex for the path or null if the path is empty
  69.      * or consists of a single, full arc.
  70.      * @return the start vertex for the path
  71.      */
  72.     public Point2S getStartVertex() {
  73.         final GreatArc arc = getStartArc();
  74.         return (arc != null) ? arc.getStartPoint() : null;
  75.     }

  76.     /** Get the end vertex for the path or null if the path is empty
  77.      * or consists of a single, full arc.
  78.      * @return the end vertex for the path
  79.      */
  80.     public Point2S getEndVertex() {
  81.         final GreatArc arc = getEndArc();
  82.         return (arc != null) ? arc.getEndPoint() : null;
  83.     }

  84.     /** Get the vertices contained in the path in the order they appear.
  85.      * Closed paths contain the start vertex at the beginning of the list
  86.      * as well as the end.
  87.      * @return the vertices contained in the path in order they appear
  88.      */
  89.     public List<Point2S> getVertices() {
  90.         final List<Point2S> vertices = new ArrayList<>();

  91.         Point2S pt;

  92.         // add the start point, if present
  93.         pt = getStartVertex();
  94.         if (pt != null) {
  95.             vertices.add(pt);
  96.         }

  97.         // add end points
  98.         for (final GreatArc arc : arcs) {
  99.             pt = arc.getEndPoint();
  100.             if (pt != null) {
  101.                 vertices.add(pt);
  102.             }
  103.         }

  104.         return vertices;
  105.     }

  106.     /** Return true if the path does not contain any arcs.
  107.      * @return true if the path does not contain any arcs
  108.      */
  109.     public boolean isEmpty() {
  110.         return arcs.isEmpty();
  111.     }

  112.     /** Return true if the path is closed, meaning that the end
  113.      * point for the last arc is equal to the start point
  114.      * for the path.
  115.      * @return true if the end point for the last arc is
  116.      *      equal to the start point for the path
  117.      */
  118.     public boolean isClosed() {
  119.         final GreatArc endArc = getEndArc();

  120.         if (endArc != null) {
  121.             final Point2S start = getStartVertex();
  122.             final Point2S end = endArc.getEndPoint();

  123.             return start != null && end != null && start.eq(end, endArc.getPrecision());
  124.         }

  125.         return false;
  126.     }

  127.     /** Return a string representation of this arc path instance.
  128.     *
  129.     * <p>In order to keep the string representation short but useful, the exact format of the return
  130.     * value depends on the properties of the path. See below for examples.
  131.     *
  132.     * <ul>
  133.     *      <li>Empty path
  134.     *          <ul>
  135.     *              <li>{@code GreatArcPath[empty= true]}</li>
  136.     *          </ul>
  137.     *      </li>
  138.     *      <li>Single, full arc
  139.     *          <ul>
  140.     *              <li>{@code GreatArcPath[full= true, circle= GreatCircle[pole= (0.0, 0.0, 1.0),
  141.     *              x= (0.0, 1.0, -0.0), y= (-1.0, 0.0, 0.0)]]}</li>
  142.     *          </ul>
  143.     *      </li>
  144.     *      <li>One or more non-full arcs
  145.     *          <ul>
  146.     *              <li>{@code GreatArcPath[vertices= [(0.0, 1.5707963267948966),
  147.     *              (1.5707963267948966, 1.5707963267948966)]]}</li>
  148.     *          </ul>
  149.     *      </li>
  150.     * </ul>
  151.     */
  152.     @Override
  153.     public String toString() {
  154.         final StringBuilder sb = new StringBuilder();
  155.         sb.append(this.getClass().getSimpleName())
  156.             .append('[');

  157.         if (isEmpty()) {
  158.             sb.append("empty= true");
  159.         } else if (arcs.size() == 1 && arcs.get(0).isFull()) {
  160.             sb.append("full= true, circle= ")
  161.                 .append(arcs.get(0).getCircle());
  162.         } else {
  163.             sb.append("vertices= ")
  164.                 .append(getVertices());
  165.         }

  166.         sb.append(']');

  167.         return sb.toString();
  168.     }

  169.     /** Construct a new path from the given arcs.
  170.      * @param arcs arc instance to use to construct the path
  171.      * @return a new instance constructed from the given arc instances
  172.      */
  173.     public static GreatArcPath fromArcs(final GreatArc... arcs) {
  174.         return fromArcs(Arrays.asList(arcs));
  175.     }

  176.     /** Construct a new path from the given arcs.
  177.      * @param arcs arc instance to use to construct the path
  178.      * @return a new instance constructed from the given arc instances
  179.      */
  180.     public static GreatArcPath fromArcs(final Collection<GreatArc> arcs) {
  181.         final Builder builder = builder(null);
  182.         for (final GreatArc arc : arcs) {
  183.             builder.append(arc);
  184.         }

  185.         return builder.build();
  186.     }

  187.     /** Return a new path formed by connecting the given vertices. An additional arc is added
  188.      * from the last point to the first point to construct a loop, if the two points are not
  189.      * already considered equal by the given precision context. This method is equivalent
  190.      * to calling {@link #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
  191.      * fromPoints(points, true, precision)}.
  192.      * @param vertices the points to construct the path from
  193.      * @param precision precision precision context used to construct the arc instances for the
  194.      *      path
  195.      * @return a new path formed by connecting the given vertices
  196.      * @see #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
  197.      */
  198.     public static GreatArcPath fromVertexLoop(final Collection<Point2S> vertices,
  199.             final Precision.DoubleEquivalence precision) {
  200.         return fromVertices(vertices, true, precision);
  201.     }

  202.     /** Return a new path formed by connecting the given vertices. No additional arc
  203.      * is inserted to connect the last point to the first. This method is equivalent
  204.      * to calling {@link #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
  205.      * fromPoint(points, false, precision)}.
  206.      * @param vertices the points to construct the path from
  207.      * @param precision precision context used to construct the arc instances for the
  208.      *      path
  209.      * @return a new path formed by connecting the given vertices
  210.      * @see #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
  211.      */
  212.     public static GreatArcPath fromVertices(final Collection<Point2S> vertices,
  213.             final Precision.DoubleEquivalence precision) {
  214.         return fromVertices(vertices, false, precision);
  215.     }

  216.     /** Return a new path formed by connecting the given vertices.
  217.      * @param vertices the points to construct the path from
  218.      * @param close if true, then an additional arc will be added from the last point
  219.      *      to the first, if the points are not already considered equal by the given
  220.      *      precision context
  221.      * @param precision precision context used to construct the arc instances for the
  222.      *      path
  223.      * @return a new path formed by connecting the given points
  224.      */
  225.     public static GreatArcPath fromVertices(final Collection<Point2S> vertices, final boolean close,
  226.             final Precision.DoubleEquivalence precision) {

  227.         return builder(precision)
  228.                 .appendVertices(vertices)
  229.                 .build(close);
  230.     }

  231.     /** Return a {@link Builder} instance configured with the given precision
  232.      * context. The precision context is used when building arcs from points
  233.      * and may be omitted if raw points are not used.
  234.      * @param precision precision context to use when building arcs from
  235.      *      raw points; may be null if raw points are not used.
  236.      * @return a new {@link Builder} instance
  237.      */
  238.     public static Builder builder(final Precision.DoubleEquivalence precision) {
  239.         return new Builder(precision);
  240.     }

  241.     /** Get an instance containing no arcs.
  242.      * @return an instance containing no arcs
  243.      */
  244.     public static GreatArcPath empty() {
  245.         return EMPTY;
  246.     }

  247.     /** Class used to build arc paths.
  248.      */
  249.     public static final class Builder {
  250.         /** Arcs appended to the path. */
  251.         private List<GreatArc> appendedArcs;

  252.         /** Arcs prepended to the path. */
  253.         private List<GreatArc> prependedArcs;

  254.         /** Precision context used when creating arcs directly from points. */
  255.         private Precision.DoubleEquivalence precision;

  256.         /** The current point at the start of the path. */
  257.         private Point2S startVertex;

  258.         /** The current point at the end of the path. */
  259.         private Point2S endVertex;

  260.         /** The precision context used when performing comparisons involving the current
  261.          * end point.
  262.          */
  263.         private Precision.DoubleEquivalence endVertexPrecision;

  264.         /** Construct a new instance configured with the given precision context. The
  265.          * precision context is used when building arcs from points and
  266.          * may be omitted if raw points are not used.
  267.          * @param precision precision context to use when creating arcs
  268.          *      from points
  269.          */
  270.         private Builder(final Precision.DoubleEquivalence precision) {
  271.             setPrecision(precision);
  272.         }

  273.         /** Set the precision context. This context is used only when creating arcs
  274.          * from appended or prepended points. It is not used when adding existing
  275.          * {@link GreatArc} instances since those contain their own precision contexts.
  276.          * @param builderPrecision precision context to use when creating arcs from points
  277.          * @return this instance
  278.          */
  279.         public Builder setPrecision(final Precision.DoubleEquivalence builderPrecision) {
  280.             this.precision = builderPrecision;

  281.             return this;
  282.         }

  283.         /** Get the arc at the start of the path or null if it does not exist.
  284.          * @return the arc at the start of the path
  285.          */
  286.         public GreatArc getStartArc() {
  287.             GreatArc start = getLast(prependedArcs);
  288.             if (start == null) {
  289.                 start = getFirst(appendedArcs);
  290.             }
  291.             return start;
  292.         }

  293.         /** Get the arc at the end of the path or null if it does not exist.
  294.          * @return the arc at the end of the path
  295.          */
  296.         public GreatArc getEndArc() {
  297.             GreatArc end = getLast(appendedArcs);
  298.             if (end == null) {
  299.                 end = getFirst(prependedArcs);
  300.             }
  301.             return end;
  302.         }

  303.         /** Append an arc to the end of the path.
  304.          * @param arc arc to append to the path
  305.          * @return the current builder instance
  306.          * @throws IllegalStateException if the path contains a previous arc
  307.          *      and the end point of the previous arc is not equivalent to the
  308.          *      start point of the given arc
  309.          */
  310.         public Builder append(final GreatArc arc) {
  311.             validateArcsConnected(getEndArc(), arc);
  312.             appendInternal(arc);

  313.             return this;
  314.         }

  315.         /** Add a vertex to the end of this path. If the path already has an end vertex,
  316.          * then an arc is added between the previous end vertex and this vertex,
  317.          * using the configured precision context.
  318.          * @param vertex the vertex to add
  319.          * @return this instance
  320.          * @see #setPrecision(Precision.DoubleEquivalence)
  321.          */
  322.         public Builder append(final Point2S vertex) {
  323.             final Precision.DoubleEquivalence vertexPrecision = getAddPointPrecision();

  324.             if (endVertex == null) {
  325.                 // make sure that we're not adding to a full arc
  326.                 final GreatArc end = getEndArc();
  327.                 if (end != null) {
  328.                     throw new IllegalStateException(
  329.                             MessageFormat.format("Cannot add point {0} after full arc: {1}", vertex, end));
  330.                 }

  331.                 // this is the first vertex added
  332.                 startVertex = vertex;
  333.                 endVertex = vertex;
  334.                 endVertexPrecision = vertexPrecision;
  335.             } else if (!endVertex.eq(vertex, vertexPrecision)) {
  336.                 // only add the vertex if its not equal to the end point
  337.                 // of the last arc
  338.                 appendInternal(GreatCircles.arcFromPoints(endVertex, vertex, endVertexPrecision));
  339.             }

  340.             return this;
  341.         }

  342.         /** Convenience method for appending a collection of vertices to the path in a single
  343.          * method call.
  344.          * @param vertices the vertices to append
  345.          * @return this instance
  346.          * @see #append(Point2S)
  347.          */
  348.         public Builder appendVertices(final Collection<Point2S> vertices) {
  349.             for (final Point2S vertex : vertices) {
  350.                 append(vertex);
  351.             }

  352.             return this;
  353.         }

  354.         /** Convenience method for appending multiple vertices to the path at once.
  355.          * @param vertices the points to append
  356.          * @return this instance
  357.          * @see #append(Point2S)
  358.          */
  359.         public Builder appendVertices(final Point2S... vertices) {
  360.             return appendVertices(Arrays.asList(vertices));
  361.         }

  362.         /** Prepend an arc to the beginning of the path.
  363.          * @param arc arc to prepend to the path
  364.          * @return the current builder instance
  365.          * @throws IllegalStateException if the path contains a start arc
  366.          *      and the end point of the given arc is not equivalent to the
  367.          *      start point of the start arc
  368.          */
  369.         public Builder prepend(final GreatArc arc) {
  370.             validateArcsConnected(arc, getStartArc());
  371.             prependInternal(arc);

  372.             return this;
  373.         }

  374.         /** Add a vertex to the front of this path. If the path already has a start vertex,
  375.          * then an arc is added between this vertex and the previous start vertex,
  376.          * using the configured precision context.
  377.          * @param vertex the vertex to add
  378.          * @return this instance
  379.          * @see #setPrecision(Precision.DoubleEquivalence)
  380.          */
  381.         public Builder prepend(final Point2S vertex) {
  382.             final Precision.DoubleEquivalence vertexPrecision = getAddPointPrecision();

  383.             if (startVertex == null) {
  384.                 // make sure that we're not adding to a full arc
  385.                 final GreatArc start = getStartArc();
  386.                 if (start != null) {
  387.                     throw new IllegalStateException(
  388.                             MessageFormat.format("Cannot add point {0} before full arc: {1}", vertex, start));
  389.                 }

  390.                 // this is the first vertex added
  391.                 startVertex = vertex;
  392.                 endVertex = vertex;
  393.                 endVertexPrecision = vertexPrecision;
  394.             } else if (!vertex.eq(startVertex, vertexPrecision)) {
  395.                 // only add if the vertex is not equal to the start
  396.                 // point of the first arc
  397.                 prependInternal(GreatCircles.arcFromPoints(vertex, startVertex, vertexPrecision));
  398.             }

  399.             return this;
  400.         }

  401.         /** Convenience method for prepending a collection of vertices to the path in a single method call.
  402.          * The vertices are logically prepended as a single group, meaning that the first vertex
  403.          * in the given collection appears as the first vertex in the path after this method call.
  404.          * Internally, this means that the vertices are actually passed to the {@link #prepend(Point2S)}
  405.          * method in reverse order.
  406.          * @param vertices the points to prepend
  407.          * @return this instance
  408.          * @see #prepend(Point2S)
  409.          */
  410.         public Builder prependPoints(final Collection<Point2S> vertices) {
  411.             return prependPoints(vertices.toArray(new Point2S[0]));
  412.         }

  413.         /** Convenience method for prepending multiple vertices to the path in a single method call.
  414.          * The vertices are logically prepended as a single group, meaning that the first vertex
  415.          * in the given collection appears as the first vertex in the path after this method call.
  416.          * Internally, this means that the vertices are actually passed to the {@link #prepend(Point2S)}
  417.          * method in reverse order.
  418.          * @param vertices the vertices to prepend
  419.          * @return this instance
  420.          * @see #prepend(Point2S)
  421.          */
  422.         public Builder prependPoints(final Point2S... vertices) {
  423.             for (int i = vertices.length - 1; i >= 0; --i) {
  424.                 prepend(vertices[i]);
  425.             }

  426.             return this;
  427.         }

  428.         /** Close the current path and build a new {@link GreatArcPath} instance. This method is equivalent
  429.          * to {@code builder.build(true)}.
  430.          * @return new closed path instance
  431.          */
  432.         public GreatArcPath close() {
  433.             return build(true);
  434.         }

  435.         /** Build a {@link GreatArcPath} instance from the configured path. This method is equivalent
  436.          * to {@code builder.build(false)}.
  437.          * @return new path instance
  438.          */
  439.         public GreatArcPath build() {
  440.             return build(false);
  441.         }

  442.         /** Build a {@link GreatArcPath} instance from the configured path.
  443.          * @param close if true, the path will be closed by adding an end point equivalent to the
  444.          *      start point
  445.          * @return new path instance
  446.          */
  447.         public GreatArcPath build(final boolean close) {
  448.             if (close) {
  449.                 closePath();
  450.             }

  451.             // combine all of the arcs
  452.             List<GreatArc> result = null;

  453.             if (prependedArcs != null) {
  454.                 result = prependedArcs;
  455.                 Collections.reverse(result);
  456.             }

  457.             if (appendedArcs != null) {
  458.                 if (result == null) {
  459.                     result = appendedArcs;
  460.                 } else {
  461.                     result.addAll(appendedArcs);
  462.                 }
  463.             }

  464.             if (result == null) {
  465.                 result = Collections.emptyList();
  466.             }

  467.             if (result.isEmpty() && startVertex != null) {
  468.                 throw new IllegalStateException(
  469.                         MessageFormat.format("Unable to create path; only a single point provided: {0}",
  470.                                 startVertex));
  471.             }

  472.             // clear internal state
  473.             appendedArcs = null;
  474.             prependedArcs = null;

  475.             // build the final path instance, using the shared empty instance if
  476.             // no arcs are present
  477.             return result.isEmpty() ? empty() : new GreatArcPath(result);
  478.         }

  479.         /** Close the path by adding an end point equivalent to the path start point.
  480.          * @throws IllegalStateException if the path cannot be closed
  481.          */
  482.         private void closePath() {
  483.             final GreatArc end = getEndArc();

  484.             if (end != null) {
  485.                 if (startVertex != null && endVertex != null) {
  486.                     if (!endVertex.eq(startVertex, endVertexPrecision)) {
  487.                         appendInternal(GreatCircles.arcFromPoints(endVertex, startVertex, endVertexPrecision));
  488.                     }
  489.                 } else {
  490.                     throw new IllegalStateException("Unable to close path: path is full");
  491.                 }
  492.             }
  493.         }

  494.         /** Validate that the given arcs are connected, meaning that the end point of {@code previous}
  495.          * is equivalent to the start point of {@code next}. The arcs are considered valid if either
  496.          * arc is null.
  497.          * @param previous previous arc
  498.          * @param next next arc
  499.          * @throws IllegalStateException if previous and next are not null and the end point of previous
  500.          *      is not equivalent the start point of next
  501.          */
  502.         private void validateArcsConnected(final GreatArc previous, final GreatArc next) {
  503.             if (previous != null && next != null) {
  504.                 final Point2S nextStartVertex = next.getStartPoint();
  505.                 final Point2S previousEndVertex = previous.getEndPoint();
  506.                 final Precision.DoubleEquivalence previousPrecision = previous.getPrecision();

  507.                 if (nextStartVertex == null || previousEndVertex == null ||
  508.                         !(nextStartVertex.eq(previousEndVertex, previousPrecision))) {

  509.                     throw new IllegalStateException(
  510.                             MessageFormat.format("Path arcs are not connected: previous= {0}, next= {1}",
  511.                                     previous, next));
  512.                 }
  513.             }
  514.         }

  515.         /** Get the precision context used when adding raw points to the path. An exception is thrown
  516.          * if no precision has been specified.
  517.          * @return the precision context used when working with raw points
  518.          * @throws IllegalStateException if no precision context is configured
  519.          */
  520.         private Precision.DoubleEquivalence getAddPointPrecision() {
  521.             if (precision == null) {
  522.                 throw new IllegalStateException("Unable to create arc: no point precision specified");
  523.             }

  524.             return precision;
  525.         }

  526.         /** Append the given, validated arc to the path.
  527.          * @param arc validated arc to append
  528.          */
  529.         private void appendInternal(final GreatArc arc) {
  530.             if (appendedArcs == null) {
  531.                 appendedArcs = new ArrayList<>();
  532.             }

  533.             if (appendedArcs.isEmpty() &&
  534.                     (prependedArcs == null || prependedArcs.isEmpty())) {
  535.                 startVertex = arc.getStartPoint();
  536.             }

  537.             endVertex = arc.getEndPoint();
  538.             endVertexPrecision = arc.getPrecision();

  539.             appendedArcs.add(arc);
  540.         }

  541.         /** Prepend the given, validated arc to the path.
  542.          * @param arc validated arc to prepend
  543.          */
  544.         private void prependInternal(final GreatArc arc) {
  545.             if (prependedArcs == null) {
  546.                 prependedArcs = new ArrayList<>();
  547.             }

  548.             startVertex = arc.getStartPoint();

  549.             if (prependedArcs.isEmpty() &&
  550.                     (appendedArcs == null || appendedArcs.isEmpty())) {
  551.                 endVertex = arc.getEndPoint();
  552.                 endVertexPrecision = arc.getPrecision();
  553.             }

  554.             prependedArcs.add(arc);
  555.         }

  556.         /** Get the first element in the list or null if the list is null
  557.          * or empty.
  558.          * @param list the list to return the first item from
  559.          * @return the first item from the given list or null if it does not exist
  560.          */
  561.         private GreatArc getFirst(final List<GreatArc> list) {
  562.             if (list != null && !list.isEmpty()) {
  563.                 return list.get(0);
  564.             }
  565.             return null;
  566.         }

  567.         /** Get the last element in the list or null if the list is null
  568.          * or empty.
  569.          * @param list the list to return the last item from
  570.          * @return the last item from the given list or null if it does not exist
  571.          */
  572.         private GreatArc getLast(final List<GreatArc> list) {
  573.             if (list != null && !list.isEmpty()) {
  574.                 return list.get(list.size() - 1);
  575.             }
  576.             return null;
  577.         }
  578.     }
  579. }