blob: 71124eb3c177246ae0575f5017a8717f388d1f60 [file] [log] [blame]
/*
* 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.euclidean.twod.path;
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.Collectors;
import java.util.stream.Stream;
import org.apache.commons.geometry.core.Sized;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.twod.BoundarySource2D;
import org.apache.commons.geometry.euclidean.twod.Line;
import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
import org.apache.commons.geometry.euclidean.twod.Lines;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
/** Class representing a connected path of {@link LineConvexSubset line convex subsets}.
* The elements in the path are connected end to end, with the end vertex of the previous
* element equivalent to the start vertex of the next element. Elements are not required to
* be finite. However, since path elements are connected, only the first element and/or last
* element may be infinite.
*
* <p>Instances of this class are guaranteed to be immutable.</p>
*/
public class LinePath implements BoundarySource2D, Sized {
/** Line path instance containing no elements. */
private static final LinePath EMPTY = new LinePath(Collections.emptyList());
/** The line convex subsets comprising the path. */
private final List<LineConvexSubset> elements;
/** Simple constructor. Callers are responsible for ensuring that the given list of
* line subsets defines a valid path. No validation is performed.
* @param elements elements defining the path.
*/
LinePath(final List<LineConvexSubset> elements) {
this.elements = Collections.unmodifiableList(elements);
}
/** {@inheritDoc} */
@Override
public Stream<LineConvexSubset> boundaryStream() {
return getElements().stream();
}
/** Get the sequence of line subsets comprising the path.
* @return the sequence of line subsets comprising the path
*/
public List<LineConvexSubset> getElements() {
return elements;
}
/** Get the line subset at the start of the path or null if the path is empty. If the
* path consists of a single line subset, then the returned instance with be the same
* as that returned by {@link #getEnd()}.
* @return the line subset at the start of the path or null if the path is empty
* @see #getEnd()
*/
public LineConvexSubset getStart() {
if (!isEmpty()) {
return elements.get(0);
}
return null;
}
/** Get the line subset at the end of the path or null if the path is empty. If the
* path consists of a single line subset, then the returned instance with be the same
* as that returned by {@link #getStart()}.
* @return the line subset at the end of the path or null if the path is empty
* @see #getStart()
*/
public LineConvexSubset getEnd() {
if (!isEmpty()) {
return elements.get(elements.size() - 1);
}
return null;
}
/** Get the sequence of vertices defined by the path. Vertices appear in the
* list as many times as they are visited in the path. For example, the vertex
* sequence for a closed path contains the start point at the beginning
* of the list as well as the end.
* @return the sequence of vertices defined by the path
*/
public List<Vector2D> getVertexSequence() {
final List<Vector2D> sequence = new ArrayList<>();
Vector2D pt;
// add the start point, if present
pt = getStartVertex();
if (pt != null) {
sequence.add(pt);
}
// add end points
for (final LineConvexSubset sub : elements) {
pt = sub.getEndPoint();
if (pt != null) {
sequence.add(pt);
}
}
return sequence;
}
/** Return true if the path has an element with infinite size.
* @return true if the path is infinite
*/
@Override
public boolean isInfinite() {
return !isEmpty() && (getStartVertex() == null || getEndVertex() == null);
}
/** Return true if the path has a finite size. This will be true if there are
* no elements in the path or if all elements have a finite length.
* @return true if the path is finite
*/
@Override
public boolean isFinite() {
return !isInfinite();
}
/** {@inheritDoc}
*
* <p>The size of the path is defined as the sum of the sizes (lengths) of all path elements.</p>
*/
@Override
public double getSize() {
double sum = 0.0;
for (final LineConvexSubset element : elements) {
sum += element.getSize();
}
return sum;
}
/** Return true if the path does not contain any elements.
* @return true if the path does not contain any elements
*/
public boolean isEmpty() {
return elements.isEmpty();
}
/** Return true if the path is closed, meaning that the end point for the last
* element is equivalent to the start point of the first.
* @return true if the end point for the last element is equivalent to the
* start point for the first
*/
public boolean isClosed() {
final LineConvexSubset endElement = getEnd();
if (endElement != null) {
final Vector2D start = getStartVertex();
final Vector2D end = endElement.getEndPoint();
return start != null && end != null && start.eq(end, endElement.getPrecision());
}
return false;
}
/** Transform this instance with the argument, returning the result in a new instance.
* @param transform the transform to apply
* @return a new instance, transformed by the argument
*/
public LinePath transform(final Transform<Vector2D> transform) {
if (!isEmpty()) {
final List<LineConvexSubset> transformed = elements.stream()
.map(s -> s.transform(transform))
.collect(Collectors.toCollection(ArrayList::new));
return new LinePath(transformed);
}
return this;
}
/** Return a new instance with all line subset directions, and their order,
* reversed. The last line subset in this instance will be the first in the
* returned instance.
* @return a new instance with the path reversed
*/
public LinePath reverse() {
if (!isEmpty()) {
final List<LineConvexSubset> reversed = elements.stream()
.map(LineConvexSubset::reverse)
.collect(Collectors.toCollection(ArrayList::new));
Collections.reverse(reversed);
return new LinePath(reversed);
}
return this;
}
/** Simplify this path, if possible, by combining adjacent elements that lie on the
* same line (as determined by {@link Line#equals(Object)}).
* @return a simplified instance
*/
public LinePath simplify() {
final List<LineConvexSubset> simplified = new ArrayList<>();
final int size = elements.size();
LineConvexSubset current;
Line currentLine;
double end;
int idx = 0;
int testIdx;
while (idx < size) {
current = elements.get(idx);
currentLine = current.getLine();
end = current.getSubspaceEnd();
// try to combine with forward neighbors
testIdx = idx + 1;
while (testIdx < size && currentLine.equals(elements.get(testIdx).getLine())) {
end = Math.max(end, elements.get(testIdx).getSubspaceEnd());
++testIdx;
}
if (testIdx > idx + 1) {
// we found something to merge
simplified.add(Lines.subsetFromInterval(currentLine, current.getSubspaceStart(), end));
} else {
simplified.add(current);
}
idx = testIdx;
}
// combine the first and last items if needed
if (isClosed() && simplified.size() > 2 && simplified.get(0).getLine().equals(
simplified.get(simplified.size() - 1).getLine())) {
final LineConvexSubset startElement = simplified.get(0);
final LineConvexSubset endElement = simplified.remove(simplified.size() - 1);
final LineConvexSubset combined = Lines.subsetFromInterval(
endElement.getLine(), endElement.getSubspaceStart(), startElement.getSubspaceEnd());
simplified.set(0, combined);
}
return new SimplifiedLinePath(simplified);
}
/** Return a string representation of the path.
*
* <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 LinePath[empty= true]}</li>
* </ul>
* </li>
* <li>Single element
* <ul>
* <li>{@code LinePath[single= Segment[startPoint= (0.0, 0.0), endPoint= (1.0, 0.0)]]}</li>
* </ul>
* </li>
* <li>Path with infinite start element
* <ul>
* <li>{@code LinePath[startDirection= (1.0, 0.0), vertices= [(1.0, 0.0), (1.0, 1.0)]]}</li>
* </ul>
* </li>
* <li>Path with infinite end element
* <ul>
* <li>{@code LinePath[vertices= [(0.0, 1.0), (0.0, 0.0)], endDirection= (1.0, 0.0)]}</li>
* </ul>
* </li>
* <li>Path with infinite start and end elements
* <ul>
* <li>{@code LinePath[startDirection= (0.0, 1.0), vertices= [(0.0, 0.0)], endDirection= (1.0, 0.0)]}</li>
* </ul>
* </li>
* <li>Path with no infinite elements
* <ul>
* <li>{@code LinePath[vertices= [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]}</li>
* </ul>
* </li>
* </ul>
*/
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName())
.append('[');
if (elements.isEmpty()) {
sb.append("empty= true");
} else if (elements.size() == 1) {
sb.append("single= ")
.append(elements.get(0));
} else {
final LineConvexSubset startElement = getStart();
if (startElement.getStartPoint() == null) {
sb.append("startDirection= ")
.append(startElement.getLine().getDirection())
.append(", ");
}
sb.append("vertexSequence= ")
.append(getVertexSequence());
final LineConvexSubset endElement = getEnd();
if (endElement.getEndPoint() == null) {
sb.append(", endDirection= ")
.append(endElement.getLine().getDirection());
}
}
sb.append(']');
return sb.toString();
}
/** Get the start vertex for the path or null if the path is empty
* or has an infinite start line subset.
* @return the start vertex for the path or null if the path does
* not start with a vertex
*/
private Vector2D getStartVertex() {
final LineConvexSubset seg = getStart();
return (seg != null) ? seg.getStartPoint() : null;
}
/** Get the end vertex for the path or null if the path is empty
* or has an infinite end line subset.
* @return the end vertex for the path or null if the path does
* not end with a vertex
*/
private Vector2D getEndVertex() {
final LineConvexSubset seg = getEnd();
return (seg != null) ? seg.getEndPoint() : null;
}
/** Build a new path from the given line subsets.
* @param subsets the line subsets to comprise the path
* @return new path containing the given line subsets in order
* @throws IllegalStateException if the line subsets do not form a connected path
*/
public static LinePath from(final LineConvexSubset... subsets) {
return from(Arrays.asList(subsets));
}
/** Build a new path from the given line subsets.
* @param subsets the line subsets to comprise the path
* @return new path containing the given line subsets in order
* @throws IllegalStateException if the subsets do not form a connected path
*/
public static LinePath from(final Collection<? extends LineConvexSubset> subsets) {
final Builder builder = builder(null);
for (final LineConvexSubset subset : subsets) {
builder.append(subset);
}
return builder.build();
}
/** Build a new path from the given vertices. A line segment is created
* from the last vertex to the first one, if the two vertices are not already
* considered equal using the given precision context. This method is equivalent to
* calling {@link #fromVertices(Collection, boolean, DoublePrecisionContext)
* fromVertices(vertices, true, precision)}
* @param vertices the vertices to construct the closed path from
* @param precision precision context used to construct the line segment
* instances for the path
* @return new closed path constructed from the given vertices
* @throws IllegalStateException if {@code vertices} contains only a single unique vertex
* @see #fromVertices(Collection, boolean, DoublePrecisionContext)
*/
public static LinePath fromVertexLoop(final Collection<Vector2D> vertices,
final DoublePrecisionContext precision) {
return fromVertices(vertices, true, precision);
}
/** Build a new path from the given vertices. No additional segment is added
* from the last vertex to the first. This method is equivalent to calling
* {@link #fromVertices(Collection, boolean, DoublePrecisionContext)
* fromVertices(vertices, false, precision)}.
* @param vertices the vertices to construct the path from
* @param precision precision context used to construct the line segment
* instances for the path
* @return new path constructed from the given vertices
* @throws IllegalStateException if {@code vertices} contains only a single unique vertex
* @see #fromVertices(Collection, boolean, DoublePrecisionContext)
*/
public static LinePath fromVertices(final Collection<Vector2D> vertices,
final DoublePrecisionContext precision) {
return fromVertices(vertices, false, precision);
}
/** Build a new path from the given vertices.
* @param vertices the vertices to construct the path from
* @param close if true, a line segment is created from the last vertex
* given to the first one, if the two vertices are not already considered
* equal using the given precision context.
* @param precision precision context used to construct the line segment
* instances for the path
* @return new path constructed from the given vertices
* @throws IllegalStateException if {@code vertices} contains only a single unique vertex
*/
public static LinePath fromVertices(final Collection<Vector2D> vertices,
final boolean close, final DoublePrecisionContext precision) {
return builder(precision)
.appendVertices(vertices)
.build(close);
}
/** Return a path containing no elements.
* @return a path containing no elements
*/
public static LinePath empty() {
return EMPTY;
}
/** Return a {@link Builder} instance configured with the given precision
* context. The precision context is used when building line segments from
* vertices and may be omitted if raw vertices are not used.
* @param precision precision context to use when building line segments from
* raw vertices; may be null if raw vertices are not used.
* @return a new {@link Builder} instance
*/
public static Builder builder(final DoublePrecisionContext precision) {
return new Builder(precision);
}
/** Class used to build line paths.
*/
public static final class Builder {
/** Line subsets appended to the path. */
private List<LineConvexSubset> appended;
/** Line subsets prepended to the path. */
private List<LineConvexSubset> prepended;
/** Precision context used when creating line segments directly from vertices. */
private DoublePrecisionContext precision;
/** The current vertex at the start of the path. */
private Vector2D startVertex;
/** The current vertex at the end of the path. */
private Vector2D endVertex;
/** The precision context used when performing comparisons involving the current
* end vertex.
*/
private DoublePrecisionContext endVertexPrecision;
/** Construct a new instance configured with the given precision context. The
* precision context is used when building line segments from vertices and
* may be omitted if raw vertices are not used.
* @param precision precision context to use when creating line segments
* from vertices
*/
private Builder(final DoublePrecisionContext precision) {
setPrecision(precision);
}
/** Set the precision context. This context is used only when creating line segments
* from appended or prepended vertices. It is not used when adding existing
* {@link LineConvexSubset} instances since those contain their own precision contexts.
* @param builderPrecision precision context to use when creating line segments
* from vertices
* @return this instance
*/
public Builder setPrecision(final DoublePrecisionContext builderPrecision) {
this.precision = builderPrecision;
return this;
}
/** Get the line subset at the start of the path or null if it does not exist.
* @return the line subset at the start of the path
*/
public LineConvexSubset getStart() {
LineConvexSubset start = getLast(prepended);
if (start == null) {
start = getFirst(appended);
}
return start;
}
/** Get the line subset at the end of the path or null if it does not exist.
* @return the line subset at the end of the path
*/
public LineConvexSubset getEnd() {
LineConvexSubset end = getLast(appended);
if (end == null) {
end = getFirst(prepended);
}
return end;
}
/** Append a line subset to the end of the path.
* @param subset line subset to append to the path
* @return the current builder instance
* @throws IllegalStateException if the path contains a previous element
* and the end vertex of the previous element is not equivalent to the
* start vertex of the argument
*/
public Builder append(final LineConvexSubset subset) {
validateConnected(getEnd(), subset);
appendInternal(subset);
return this;
}
/** Add a vertex to the end of this path. If the path already has an end vertex,
* then a line segment 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(DoublePrecisionContext)
*/
public Builder append(final Vector2D vertex) {
final DoublePrecisionContext vertexPrecision = getAddVertexPrecision();
if (endVertex == null) {
// make sure that we're not adding to an infinite element
final LineConvexSubset end = getEnd();
if (end != null) {
throw new IllegalStateException(
MessageFormat.format("Cannot add vertex {0} after infinite line subset: {1}",
vertex, end));
}
// this is the first vertex added
startVertex = vertex;
endVertex = vertex;
endVertexPrecision = vertexPrecision;
} else if (!endVertex.eq(vertex, endVertexPrecision)) {
// only add the vertex if its not equal to the end point
// of the last element
appendInternal(Lines.segmentFromPoints(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(Vector2D)
*/
public Builder appendVertices(final Collection<? extends Vector2D> vertices) {
for (final Vector2D vertex : vertices) {
append(vertex);
}
return this;
}
/** Convenience method for appending multiple vertices to the path at once.
* @param vertices the vertices to append
* @return this instance
* @see #append(Vector2D)
*/
public Builder appendVertices(final Vector2D... vertices) {
return appendVertices(Arrays.asList(vertices));
}
/** Prepend a line subset to the beginning of the path.
* @param subset line subset to prepend to the path
* @return the current builder instance
* @throws IllegalStateException if the path contains a start element
* and the end vertex of the argument is not equivalent to the
* start vertex of the start element.
*/
public Builder prepend(final LineConvexSubset subset) {
validateConnected(subset, getStart());
prependInternal(subset);
return this;
}
/** Add a vertex to the front of this path. If the path already has a start vertex,
* then a line segment 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(DoublePrecisionContext)
*/
public Builder prepend(final Vector2D vertex) {
final DoublePrecisionContext vertexPrecision = getAddVertexPrecision();
if (startVertex == null) {
// make sure that we're not adding to an infinite element
final LineConvexSubset start = getStart();
if (start != null) {
throw new IllegalStateException(
MessageFormat.format("Cannot add vertex {0} before infinite line subset: {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 element
prependInternal(Lines.segmentFromPoints(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(Vector2D)}
* method in reverse order.
* @param vertices the vertices to prepend
* @return this instance
* @see #prepend(Vector2D)
*/
public Builder prependVertices(final Collection<Vector2D> vertices) {
return prependVertices(vertices.toArray(new Vector2D[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(Vector2D)}
* method in reverse order.
* @param vertices the vertices to prepend
* @return this instance
* @see #prepend(Vector2D)
*/
public Builder prependVertices(final Vector2D... vertices) {
for (int i = vertices.length - 1; i >= 0; --i) {
prepend(vertices[i]);
}
return this;
}
/** Close the current path and build a new {@link LinePath} instance. This method is equivalent
* to {@code builder.build(true)}.
* @return new closed path instance
* @throws IllegalStateException if the builder was given only a single unique vertex
*/
public LinePath close() {
return build(true);
}
/** Build a {@link LinePath} instance from the configured path. This method is equivalent
* to {@code builder.build(false)}.
* @return new path instance
* @throws IllegalStateException if the builder was given only a single unique vertex
*/
public LinePath build() {
return build(false);
}
/** Build a {@link LinePath} 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
* @throws IllegalStateException if the builder was given only a single unique vertex
*/
public LinePath build(final boolean close) {
if (close) {
closePath();
}
// combine all of the line subsets
List<LineConvexSubset> result = null;
if (prepended != null) {
result = prepended;
Collections.reverse(result);
}
if (appended != null) {
if (result == null) {
result = appended;
} else {
result.addAll(appended);
}
}
if (result == null) {
result = Collections.emptyList();
}
if (result.isEmpty() && startVertex != null) {
throw new IllegalStateException(
MessageFormat.format("Unable to create line path; only a single unique vertex provided: {0} ",
startVertex));
}
// clear internal state
appended = null;
prepended = null;
// build the final path instance, using the shared empty instance if
// no line subsets are present
return result.isEmpty() ? empty() : new LinePath(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 LineConvexSubset end = getEnd();
if (end != null) {
if (startVertex != null && endVertex != null) {
if (!endVertex.eq(startVertex, endVertexPrecision)) {
appendInternal(Lines.segmentFromPoints(endVertex, startVertex, endVertexPrecision));
}
} else {
throw new IllegalStateException("Unable to close line path: line path is infinite");
}
}
}
/** Validate that the given line subsets are connected, meaning that the end vertex of {@code previous}
* is equivalent to the start vertex of {@code next}. The line subsets are considered valid if either
* line subset is null.
* @param previous previous line subset
* @param next next line subset
* @throws IllegalStateException if previous and next are not null and the end vertex of previous
* is not equivalent the start vertex of next
*/
private void validateConnected(final LineConvexSubset previous, final LineConvexSubset next) {
if (previous != null && next != null) {
final Vector2D nextStartVertex = next.getStartPoint();
final Vector2D previousEndVertex = previous.getEndPoint();
final DoublePrecisionContext previousPrecision = previous.getPrecision();
if (nextStartVertex == null || previousEndVertex == null ||
!(nextStartVertex.eq(previousEndVertex, previousPrecision))) {
throw new IllegalStateException(
MessageFormat.format("Path line subsets are not connected: previous= {0}, next= {1}",
previous, next));
}
}
}
/** Get the precision context used when adding raw vertices to the path. An exception is thrown
* if no precision has been specified.
* @return the precision context used when creating working with raw vertices
* @throws IllegalStateException if no precision context is configured
*/
private DoublePrecisionContext getAddVertexPrecision() {
if (precision == null) {
throw new IllegalStateException("Unable to create line segment: no vertex precision specified");
}
return precision;
}
/** Append the given, validated line subsets to the path.
* @param subset validated line subset to append
*/
private void appendInternal(final LineConvexSubset subset) {
if (appended == null) {
appended = new ArrayList<>();
}
if (appended.isEmpty() &&
(prepended == null || prepended.isEmpty())) {
startVertex = subset.getStartPoint();
}
endVertex = subset.getEndPoint();
endVertexPrecision = subset.getPrecision();
appended.add(subset);
}
/** Prepend the given, validated line subset to the path.
* @param subset validated line subset to prepend
*/
private void prependInternal(final LineConvexSubset subset) {
if (prepended == null) {
prepended = new ArrayList<>();
}
startVertex = subset.getStartPoint();
if (prepended.isEmpty() &&
(appended == null || appended.isEmpty())) {
endVertex = subset.getEndPoint();
endVertexPrecision = subset.getPrecision();
}
prepended.add(subset);
}
/** 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 LineConvexSubset getFirst(final List<? extends LineConvexSubset> 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 LineConvexSubset getLast(final List<? extends LineConvexSubset> list) {
if (list != null && !list.isEmpty()) {
return list.get(list.size() - 1);
}
return null;
}
}
/** Internal class returned when a line path is simplified to remove unnecessary line subset divisions.
* The {@link #simplify()} method on this class simply returns the same instance.
*/
private static final class SimplifiedLinePath extends LinePath {
/** Create a new instance containing the given line subsets. No validation is
* performed on the inputs. Caller must ensure that the given line subsets represent
* a valid, simplified path.
* @param elements line subsets comprising the path
*/
private SimplifiedLinePath(final List<LineConvexSubset> elements) {
super(elements);
}
/** {@inheritDoc} */
@Override
public SimplifiedLinePath simplify() {
// already simplified
return this;
}
}
}