GreatArcPath.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.geometry.spherical.twod;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.List;
- import java.util.stream.Stream;
- import org.apache.commons.numbers.core.Precision;
- /** Class representing a connected sequence of {@link GreatArc} instances.
- */
- public final class GreatArcPath implements BoundarySource2S {
- /** Instance containing no arcs. */
- private static final GreatArcPath EMPTY = new GreatArcPath(Collections.emptyList());
- /** Arcs comprising the instance. */
- private final List<GreatArc> arcs;
- /** Simple constructor. No validation is performed on the input arc.
- * @param arcs arcs for the path, in connection order
- */
- private GreatArcPath(final List<GreatArc> arcs) {
- this.arcs = Collections.unmodifiableList(arcs);
- }
- /** {@inheritDoc} */
- @Override
- public Stream<GreatArc> boundaryStream() {
- return getArcs().stream();
- }
- /** Get the arcs in path.
- * @return the arcs in the path
- */
- public List<GreatArc> getArcs() {
- return arcs;
- }
- /** Get the start arc for the path or null if the path is empty.
- * @return the start arc for the path or null if the path is empty
- */
- public GreatArc getStartArc() {
- if (!isEmpty()) {
- return arcs.get(0);
- }
- return null;
- }
- /** Get the end arc for the path or null if the path is empty.
- * @return the end arc for the path or null if the path is empty
- */
- public GreatArc getEndArc() {
- if (!isEmpty()) {
- return arcs.get(arcs.size() - 1);
- }
- return null;
- }
- /** Get the start vertex for the path or null if the path is empty
- * or consists of a single, full arc.
- * @return the start vertex for the path
- */
- public Point2S getStartVertex() {
- final GreatArc arc = getStartArc();
- return (arc != null) ? arc.getStartPoint() : null;
- }
- /** Get the end vertex for the path or null if the path is empty
- * or consists of a single, full arc.
- * @return the end vertex for the path
- */
- public Point2S getEndVertex() {
- final GreatArc arc = getEndArc();
- return (arc != null) ? arc.getEndPoint() : null;
- }
- /** Get the vertices contained in the path in the order they appear.
- * Closed paths contain the start vertex at the beginning of the list
- * as well as the end.
- * @return the vertices contained in the path in order they appear
- */
- public List<Point2S> getVertices() {
- final List<Point2S> vertices = new ArrayList<>();
- Point2S pt;
- // add the start point, if present
- pt = getStartVertex();
- if (pt != null) {
- vertices.add(pt);
- }
- // add end points
- for (final GreatArc arc : arcs) {
- pt = arc.getEndPoint();
- if (pt != null) {
- vertices.add(pt);
- }
- }
- return vertices;
- }
- /** Return true if the path does not contain any arcs.
- * @return true if the path does not contain any arcs
- */
- public boolean isEmpty() {
- return arcs.isEmpty();
- }
- /** Return true if the path is closed, meaning that the end
- * point for the last arc is equal to the start point
- * for the path.
- * @return true if the end point for the last arc is
- * equal to the start point for the path
- */
- public boolean isClosed() {
- final GreatArc endArc = getEndArc();
- if (endArc != null) {
- final Point2S start = getStartVertex();
- final Point2S end = endArc.getEndPoint();
- return start != null && end != null && start.eq(end, endArc.getPrecision());
- }
- return false;
- }
- /** Return a string representation of this arc path instance.
- *
- * <p>In order to keep the string representation short but useful, the exact format of the return
- * value depends on the properties of the path. See below for examples.
- *
- * <ul>
- * <li>Empty path
- * <ul>
- * <li>{@code GreatArcPath[empty= true]}</li>
- * </ul>
- * </li>
- * <li>Single, full arc
- * <ul>
- * <li>{@code GreatArcPath[full= true, circle= GreatCircle[pole= (0.0, 0.0, 1.0),
- * x= (0.0, 1.0, -0.0), y= (-1.0, 0.0, 0.0)]]}</li>
- * </ul>
- * </li>
- * <li>One or more non-full arcs
- * <ul>
- * <li>{@code GreatArcPath[vertices= [(0.0, 1.5707963267948966),
- * (1.5707963267948966, 1.5707963267948966)]]}</li>
- * </ul>
- * </li>
- * </ul>
- */
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder();
- sb.append(this.getClass().getSimpleName())
- .append('[');
- if (isEmpty()) {
- sb.append("empty= true");
- } else if (arcs.size() == 1 && arcs.get(0).isFull()) {
- sb.append("full= true, circle= ")
- .append(arcs.get(0).getCircle());
- } else {
- sb.append("vertices= ")
- .append(getVertices());
- }
- sb.append(']');
- return sb.toString();
- }
- /** Construct a new path from the given arcs.
- * @param arcs arc instance to use to construct the path
- * @return a new instance constructed from the given arc instances
- */
- public static GreatArcPath fromArcs(final GreatArc... arcs) {
- return fromArcs(Arrays.asList(arcs));
- }
- /** Construct a new path from the given arcs.
- * @param arcs arc instance to use to construct the path
- * @return a new instance constructed from the given arc instances
- */
- public static GreatArcPath fromArcs(final Collection<GreatArc> arcs) {
- final Builder builder = builder(null);
- for (final GreatArc arc : arcs) {
- builder.append(arc);
- }
- return builder.build();
- }
- /** Return a new path formed by connecting the given vertices. An additional arc is added
- * from the last point to the first point to construct a loop, if the two points are not
- * already considered equal by the given precision context. This method is equivalent
- * to calling {@link #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
- * fromPoints(points, true, precision)}.
- * @param vertices the points to construct the path from
- * @param precision precision precision context used to construct the arc instances for the
- * path
- * @return a new path formed by connecting the given vertices
- * @see #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
- */
- public static GreatArcPath fromVertexLoop(final Collection<Point2S> vertices,
- final Precision.DoubleEquivalence precision) {
- return fromVertices(vertices, true, precision);
- }
- /** Return a new path formed by connecting the given vertices. No additional arc
- * is inserted to connect the last point to the first. This method is equivalent
- * to calling {@link #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
- * fromPoint(points, false, precision)}.
- * @param vertices the points to construct the path from
- * @param precision precision context used to construct the arc instances for the
- * path
- * @return a new path formed by connecting the given vertices
- * @see #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
- */
- public static GreatArcPath fromVertices(final Collection<Point2S> vertices,
- final Precision.DoubleEquivalence precision) {
- return fromVertices(vertices, false, precision);
- }
- /** Return a new path formed by connecting the given vertices.
- * @param vertices the points to construct the path from
- * @param close if true, then an additional arc will be added from the last point
- * to the first, if the points are not already considered equal by the given
- * precision context
- * @param precision precision context used to construct the arc instances for the
- * path
- * @return a new path formed by connecting the given points
- */
- public static GreatArcPath fromVertices(final Collection<Point2S> vertices, final boolean close,
- final Precision.DoubleEquivalence precision) {
- return builder(precision)
- .appendVertices(vertices)
- .build(close);
- }
- /** Return a {@link Builder} instance configured with the given precision
- * context. The precision context is used when building arcs from points
- * and may be omitted if raw points are not used.
- * @param precision precision context to use when building arcs from
- * raw points; may be null if raw points are not used.
- * @return a new {@link Builder} instance
- */
- public static Builder builder(final Precision.DoubleEquivalence precision) {
- return new Builder(precision);
- }
- /** Get an instance containing no arcs.
- * @return an instance containing no arcs
- */
- public static GreatArcPath empty() {
- return EMPTY;
- }
- /** Class used to build arc paths.
- */
- public static final class Builder {
- /** Arcs appended to the path. */
- private List<GreatArc> appendedArcs;
- /** Arcs prepended to the path. */
- private List<GreatArc> prependedArcs;
- /** Precision context used when creating arcs directly from points. */
- private Precision.DoubleEquivalence precision;
- /** The current point at the start of the path. */
- private Point2S startVertex;
- /** The current point at the end of the path. */
- private Point2S endVertex;
- /** The precision context used when performing comparisons involving the current
- * end point.
- */
- private Precision.DoubleEquivalence endVertexPrecision;
- /** Construct a new instance configured with the given precision context. The
- * precision context is used when building arcs from points and
- * may be omitted if raw points are not used.
- * @param precision precision context to use when creating arcs
- * from points
- */
- private Builder(final Precision.DoubleEquivalence precision) {
- setPrecision(precision);
- }
- /** Set the precision context. This context is used only when creating arcs
- * from appended or prepended points. It is not used when adding existing
- * {@link GreatArc} instances since those contain their own precision contexts.
- * @param builderPrecision precision context to use when creating arcs from points
- * @return this instance
- */
- public Builder setPrecision(final Precision.DoubleEquivalence builderPrecision) {
- this.precision = builderPrecision;
- return this;
- }
- /** Get the arc at the start of the path or null if it does not exist.
- * @return the arc at the start of the path
- */
- public GreatArc getStartArc() {
- GreatArc start = getLast(prependedArcs);
- if (start == null) {
- start = getFirst(appendedArcs);
- }
- return start;
- }
- /** Get the arc at the end of the path or null if it does not exist.
- * @return the arc at the end of the path
- */
- public GreatArc getEndArc() {
- GreatArc end = getLast(appendedArcs);
- if (end == null) {
- end = getFirst(prependedArcs);
- }
- return end;
- }
- /** Append an arc to the end of the path.
- * @param arc arc to append to the path
- * @return the current builder instance
- * @throws IllegalStateException if the path contains a previous arc
- * and the end point of the previous arc is not equivalent to the
- * start point of the given arc
- */
- public Builder append(final GreatArc arc) {
- validateArcsConnected(getEndArc(), arc);
- appendInternal(arc);
- return this;
- }
- /** Add a vertex to the end of this path. If the path already has an end vertex,
- * then an arc is added between the previous end vertex and this vertex,
- * using the configured precision context.
- * @param vertex the vertex to add
- * @return this instance
- * @see #setPrecision(Precision.DoubleEquivalence)
- */
- public Builder append(final Point2S vertex) {
- final Precision.DoubleEquivalence vertexPrecision = getAddPointPrecision();
- if (endVertex == null) {
- // make sure that we're not adding to a full arc
- final GreatArc end = getEndArc();
- if (end != null) {
- throw new IllegalStateException(
- MessageFormat.format("Cannot add point {0} after full arc: {1}", vertex, end));
- }
- // this is the first vertex added
- startVertex = vertex;
- endVertex = vertex;
- endVertexPrecision = vertexPrecision;
- } else if (!endVertex.eq(vertex, vertexPrecision)) {
- // only add the vertex if its not equal to the end point
- // of the last arc
- appendInternal(GreatCircles.arcFromPoints(endVertex, vertex, endVertexPrecision));
- }
- return this;
- }
- /** Convenience method for appending a collection of vertices to the path in a single
- * method call.
- * @param vertices the vertices to append
- * @return this instance
- * @see #append(Point2S)
- */
- public Builder appendVertices(final Collection<Point2S> vertices) {
- for (final Point2S vertex : vertices) {
- append(vertex);
- }
- return this;
- }
- /** Convenience method for appending multiple vertices to the path at once.
- * @param vertices the points to append
- * @return this instance
- * @see #append(Point2S)
- */
- public Builder appendVertices(final Point2S... vertices) {
- return appendVertices(Arrays.asList(vertices));
- }
- /** Prepend an arc to the beginning of the path.
- * @param arc arc to prepend to the path
- * @return the current builder instance
- * @throws IllegalStateException if the path contains a start arc
- * and the end point of the given arc is not equivalent to the
- * start point of the start arc
- */
- public Builder prepend(final GreatArc arc) {
- validateArcsConnected(arc, getStartArc());
- prependInternal(arc);
- return this;
- }
- /** Add a vertex to the front of this path. If the path already has a start vertex,
- * then an arc is added between this vertex and the previous start vertex,
- * using the configured precision context.
- * @param vertex the vertex to add
- * @return this instance
- * @see #setPrecision(Precision.DoubleEquivalence)
- */
- public Builder prepend(final Point2S vertex) {
- final Precision.DoubleEquivalence vertexPrecision = getAddPointPrecision();
- if (startVertex == null) {
- // make sure that we're not adding to a full arc
- final GreatArc start = getStartArc();
- if (start != null) {
- throw new IllegalStateException(
- MessageFormat.format("Cannot add point {0} before full arc: {1}", vertex, start));
- }
- // this is the first vertex added
- startVertex = vertex;
- endVertex = vertex;
- endVertexPrecision = vertexPrecision;
- } else if (!vertex.eq(startVertex, vertexPrecision)) {
- // only add if the vertex is not equal to the start
- // point of the first arc
- prependInternal(GreatCircles.arcFromPoints(vertex, startVertex, vertexPrecision));
- }
- return this;
- }
- /** Convenience method for prepending a collection of vertices to the path in a single method call.
- * The vertices are logically prepended as a single group, meaning that the first vertex
- * in the given collection appears as the first vertex in the path after this method call.
- * Internally, this means that the vertices are actually passed to the {@link #prepend(Point2S)}
- * method in reverse order.
- * @param vertices the points to prepend
- * @return this instance
- * @see #prepend(Point2S)
- */
- public Builder prependPoints(final Collection<Point2S> vertices) {
- return prependPoints(vertices.toArray(new Point2S[0]));
- }
- /** Convenience method for prepending multiple vertices to the path in a single method call.
- * The vertices are logically prepended as a single group, meaning that the first vertex
- * in the given collection appears as the first vertex in the path after this method call.
- * Internally, this means that the vertices are actually passed to the {@link #prepend(Point2S)}
- * method in reverse order.
- * @param vertices the vertices to prepend
- * @return this instance
- * @see #prepend(Point2S)
- */
- public Builder prependPoints(final Point2S... vertices) {
- for (int i = vertices.length - 1; i >= 0; --i) {
- prepend(vertices[i]);
- }
- return this;
- }
- /** Close the current path and build a new {@link GreatArcPath} instance. This method is equivalent
- * to {@code builder.build(true)}.
- * @return new closed path instance
- */
- public GreatArcPath close() {
- return build(true);
- }
- /** Build a {@link GreatArcPath} instance from the configured path. This method is equivalent
- * to {@code builder.build(false)}.
- * @return new path instance
- */
- public GreatArcPath build() {
- return build(false);
- }
- /** Build a {@link GreatArcPath} instance from the configured path.
- * @param close if true, the path will be closed by adding an end point equivalent to the
- * start point
- * @return new path instance
- */
- public GreatArcPath build(final boolean close) {
- if (close) {
- closePath();
- }
- // combine all of the arcs
- List<GreatArc> result = null;
- if (prependedArcs != null) {
- result = prependedArcs;
- Collections.reverse(result);
- }
- if (appendedArcs != null) {
- if (result == null) {
- result = appendedArcs;
- } else {
- result.addAll(appendedArcs);
- }
- }
- if (result == null) {
- result = Collections.emptyList();
- }
- if (result.isEmpty() && startVertex != null) {
- throw new IllegalStateException(
- MessageFormat.format("Unable to create path; only a single point provided: {0}",
- startVertex));
- }
- // clear internal state
- appendedArcs = null;
- prependedArcs = null;
- // build the final path instance, using the shared empty instance if
- // no arcs are present
- return result.isEmpty() ? empty() : new GreatArcPath(result);
- }
- /** Close the path by adding an end point equivalent to the path start point.
- * @throws IllegalStateException if the path cannot be closed
- */
- private void closePath() {
- final GreatArc end = getEndArc();
- if (end != null) {
- if (startVertex != null && endVertex != null) {
- if (!endVertex.eq(startVertex, endVertexPrecision)) {
- appendInternal(GreatCircles.arcFromPoints(endVertex, startVertex, endVertexPrecision));
- }
- } else {
- throw new IllegalStateException("Unable to close path: path is full");
- }
- }
- }
- /** Validate that the given arcs are connected, meaning that the end point of {@code previous}
- * is equivalent to the start point of {@code next}. The arcs are considered valid if either
- * arc is null.
- * @param previous previous arc
- * @param next next arc
- * @throws IllegalStateException if previous and next are not null and the end point of previous
- * is not equivalent the start point of next
- */
- private void validateArcsConnected(final GreatArc previous, final GreatArc next) {
- if (previous != null && next != null) {
- final Point2S nextStartVertex = next.getStartPoint();
- final Point2S previousEndVertex = previous.getEndPoint();
- final Precision.DoubleEquivalence previousPrecision = previous.getPrecision();
- if (nextStartVertex == null || previousEndVertex == null ||
- !(nextStartVertex.eq(previousEndVertex, previousPrecision))) {
- throw new IllegalStateException(
- MessageFormat.format("Path arcs are not connected: previous= {0}, next= {1}",
- previous, next));
- }
- }
- }
- /** Get the precision context used when adding raw points to the path. An exception is thrown
- * if no precision has been specified.
- * @return the precision context used when working with raw points
- * @throws IllegalStateException if no precision context is configured
- */
- private Precision.DoubleEquivalence getAddPointPrecision() {
- if (precision == null) {
- throw new IllegalStateException("Unable to create arc: no point precision specified");
- }
- return precision;
- }
- /** Append the given, validated arc to the path.
- * @param arc validated arc to append
- */
- private void appendInternal(final GreatArc arc) {
- if (appendedArcs == null) {
- appendedArcs = new ArrayList<>();
- }
- if (appendedArcs.isEmpty() &&
- (prependedArcs == null || prependedArcs.isEmpty())) {
- startVertex = arc.getStartPoint();
- }
- endVertex = arc.getEndPoint();
- endVertexPrecision = arc.getPrecision();
- appendedArcs.add(arc);
- }
- /** Prepend the given, validated arc to the path.
- * @param arc validated arc to prepend
- */
- private void prependInternal(final GreatArc arc) {
- if (prependedArcs == null) {
- prependedArcs = new ArrayList<>();
- }
- startVertex = arc.getStartPoint();
- if (prependedArcs.isEmpty() &&
- (appendedArcs == null || appendedArcs.isEmpty())) {
- endVertex = arc.getEndPoint();
- endVertexPrecision = arc.getPrecision();
- }
- prependedArcs.add(arc);
- }
- /** Get the first element in the list or null if the list is null
- * or empty.
- * @param list the list to return the first item from
- * @return the first item from the given list or null if it does not exist
- */
- private GreatArc getFirst(final List<GreatArc> list) {
- if (list != null && !list.isEmpty()) {
- return list.get(0);
- }
- return null;
- }
- /** Get the last element in the list or null if the list is null
- * or empty.
- * @param list the list to return the last item from
- * @return the last item from the given list or null if it does not exist
- */
- private GreatArc getLast(final List<GreatArc> list) {
- if (list != null && !list.isEmpty()) {
- return list.get(list.size() - 1);
- }
- return null;
- }
- }
- }