001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.spherical.twod;
018
019import java.text.MessageFormat;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.List;
025import java.util.stream.Stream;
026
027import org.apache.commons.numbers.core.Precision;
028
029/** Class representing a connected sequence of {@link GreatArc} instances.
030 */
031public final class GreatArcPath implements BoundarySource2S {
032    /** Instance containing no arcs. */
033    private static final GreatArcPath EMPTY = new GreatArcPath(Collections.emptyList());
034
035    /** Arcs comprising the instance. */
036    private final List<GreatArc> arcs;
037
038    /** Simple constructor. No validation is performed on the input arc.
039     * @param arcs arcs for the path, in connection order
040     */
041    private GreatArcPath(final List<GreatArc> arcs) {
042        this.arcs = Collections.unmodifiableList(arcs);
043    }
044
045    /** {@inheritDoc} */
046    @Override
047    public Stream<GreatArc> boundaryStream() {
048        return getArcs().stream();
049    }
050
051    /** Get the arcs in path.
052     * @return the arcs in the path
053     */
054    public List<GreatArc> getArcs() {
055        return arcs;
056    }
057
058    /** Get the start arc for the path or null if the path is empty.
059     * @return the start arc for the path or null if the path is empty
060     */
061    public GreatArc getStartArc() {
062        if (!isEmpty()) {
063            return arcs.get(0);
064        }
065        return null;
066    }
067
068    /** Get the end arc for the path or null if the path is empty.
069     * @return the end arc for the path or null if the path is empty
070     */
071    public GreatArc getEndArc() {
072        if (!isEmpty()) {
073            return arcs.get(arcs.size() - 1);
074        }
075        return null;
076    }
077
078    /** Get the start vertex for the path or null if the path is empty
079     * or consists of a single, full arc.
080     * @return the start vertex for the path
081     */
082    public Point2S getStartVertex() {
083        final GreatArc arc = getStartArc();
084        return (arc != null) ? arc.getStartPoint() : null;
085    }
086
087    /** Get the end vertex for the path or null if the path is empty
088     * or consists of a single, full arc.
089     * @return the end vertex for the path
090     */
091    public Point2S getEndVertex() {
092        final GreatArc arc = getEndArc();
093        return (arc != null) ? arc.getEndPoint() : null;
094    }
095
096    /** Get the vertices contained in the path in the order they appear.
097     * Closed paths contain the start vertex at the beginning of the list
098     * as well as the end.
099     * @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}