GEOMETRY-32: refactoring BSP and related classes; also addresses issues GEOMETRY-32 (simplify Transform interface), GEOMETRY-33 (Region API), and GEOMETRY-34 (SubHyperplane optimized implementations)
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Embedding.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Embedding.java
new file mode 100644
index 0000000..cf258c8
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Embedding.java
@@ -0,0 +1,71 @@
+/*
+ * 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.core;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** This interface defines mappings between a space and one of its subspaces.
+
+ * <p>Subspaces are the lower-dimension subsets of a space. For example,
+ * in an n-dimension space, the subspaces are the (n-1) dimension space,
+ * the (n-2) dimension space, and so on. This interface can be used regardless
+ * of the difference in number of dimensions between the space and the target
+ * subspace. For example, a line in 3D Euclidean space can use this interface
+ * to map directly from 3D Euclidean space to 1D Euclidean space (ie, the location
+ * along the line).</p>
+ *
+ * @param <P> Point type defining the embedding space.
+ * @param <S> Point type defining the embedded subspace.
+ */
+public interface Embedding<P extends Point<P>, S extends Point<S>> {
+
+ /** Transform a space point into a subspace point.
+ * @param point n-dimension point of the space
+ * @return lower-dimension point of the subspace corresponding to
+ * the specified space point
+ * @see #toSpace
+ */
+ S toSubspace(P point);
+
+ /** Transform a collection of space points into subspace points.
+ * @param points collection of n-dimension points to transform
+ * @return collection of transformed lower-dimension points.
+ * @see #toSubspace(Point)
+ */
+ default List<S> toSubspace(final Collection<P> points) {
+ return points.stream().map(this::toSubspace).collect(Collectors.toList());
+ }
+
+ /** Transform a subspace point into a space point.
+ * @param point lower-dimension point of the subspace
+ * @return n-dimension point of the space corresponding to the
+ * specified subspace point
+ * @see #toSubspace(Point)
+ */
+ P toSpace(S point);
+
+ /** Transform a collection of subspace points into space points.
+ * @param points collection of lower-dimension points to transform
+ * @return collection of transformed n-dimension points.
+ * @see #toSpace(Point)
+ */
+ default List<P> toSpace(final Collection<S> points) {
+ return points.stream().map(this::toSpace).collect(Collectors.toList());
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
index 819a9e5..37c40aa 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
@@ -23,8 +23,8 @@
/** Alias for {@link Math#PI}, placed here for completeness. */
public static final double PI = Math.PI;
- /** Constant value for {@code -pi} */
- public static final double MINUS_PI = - Math.PI;
+ /** Constant value for {@code -pi}. */
+ public static final double MINUS_PI = -Math.PI;
/** Constant value for {@code 2*pi}. */
public static final double TWO_PI = 2.0 * Math.PI;
@@ -36,7 +36,7 @@
public static final double HALF_PI = 0.5 * Math.PI;
/** Constant value for {@code - pi/2}. */
- public static final double MINUS_HALF_PI = - 0.5 * Math.PI;
+ public static final double MINUS_HALF_PI = -0.5 * Math.PI;
/** Constant value for {@code 3*pi/2}. */
public static final double THREE_HALVES_PI = 1.5 * Math.PI;
@@ -46,6 +46,6 @@
*/
public static final double ZERO_PI = 0.0;
- /** Private constructor */
+ /** Private constructor. */
private Geometry() {}
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
new file mode 100644
index 0000000..233cd4b
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
@@ -0,0 +1,82 @@
+/*
+ * 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.core;
+
+/** Interface representing a region in a space. A region partitions a space
+ * into sets of points lying on the inside, outside, and boundary.
+ * @param <P> Point implementation type
+ */
+public interface Region<P extends Point<P>> {
+
+ /** Return true if the region spans the entire space. In other words,
+ * a region is full if no points in the space are classified as
+ * {@link RegionLocation#OUTSIDE outside}.
+ * @return true if the region spans the entire space
+ */
+ boolean isFull();
+
+ /** Return true if the region is completely empty, ie all points in
+ * the space are classified as {@link RegionLocation#OUTSIDE outside}.
+ * @return true if the region is empty
+ */
+ boolean isEmpty();
+
+ /** Get the size of the region. The meaning of this will vary depending on
+ * the space and dimension of the region. For example, in Euclidean space,
+ * this will be a length in 1D, an area in 2D, and a volume in 3D.
+ * @return the size of the region
+ */
+ double getSize();
+
+ /** Get the size of the boundary of the region. The size is a value in
+ * the {@code d-1} dimension space. For example, in Euclidean space,
+ * this will be a length in 2D and an area in 3D.
+ * @return the size of the boundary of the region
+ */
+ double getBoundarySize();
+
+ /** Get the barycenter of the region or null if none exists. A barycenter
+ * will not exist for empty or infinite regions.
+ * @return the barycenter of the region or null if none exists
+ */
+ P getBarycenter();
+
+ /** Classify the given point with respect to the region.
+ * @param pt the point to classify
+ * @return the location of the point with respect to the region
+ */
+ RegionLocation classify(P pt);
+
+ /** Return true if the given point is on the inside or boundary
+ * of the region.
+ * @param pt the point to test
+ * @return true if the point is on the inside or boundary of the region
+ */
+ default boolean contains(P pt) {
+ final RegionLocation location = classify(pt);
+ return location != null && location != RegionLocation.OUTSIDE;
+ }
+
+ /** Project a point onto the boundary of the region. Null is returned if
+ * the region contains no boundaries (ie, is either {@link #isFull() full}
+ * or {@link #isEmpty() empty}).
+ * @param pt pt to project
+ * @return projection of the point on the boundary of the region or null
+ * if the region does not contain any boundaries
+ */
+ P project(P pt);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionLocation.java
similarity index 62%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionLocation.java
index 046defe..19f5068 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionLocation.java
@@ -14,23 +14,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partitioning;
+package org.apache.commons.geometry.core;
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+/** Enumeration containing the possible locations of a point with
+ * respect to a region.
+ * @see Region
*/
-public enum Side {
+public enum RegionLocation {
- /** Code for the plus side of the hyperplane. */
- PLUS,
+ /** Value indicating that a point lies on the inside of
+ * a region.
+ */
+ INSIDE,
- /** Code for the minus side of the hyperplane. */
- MINUS,
+ /** Value indicating that a point lies on the outside of
+ * a region.
+ */
+ OUTSIDE,
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
-
- /** Code for the hyperplane itself. */
- HYPER;
-
+ /** Value indicating that a point lies on the boundary of
+ * a region.
+ */
+ BOUNDARY
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Spatial.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Spatial.java
index c6f76c1..ae5e277 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Spatial.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Spatial.java
@@ -38,4 +38,10 @@
* are NaN
*/
boolean isInfinite();
+
+ /** Returns true if all values in this element are finite, meaning
+ * they are not NaN or infinite.
+ * @return true if all values in this element are finite
+ */
+ boolean isFinite();
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Transform.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Transform.java
new file mode 100644
index 0000000..7eedd67
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Transform.java
@@ -0,0 +1,56 @@
+/*
+ * 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.core;
+
+import java.util.function.Function;
+
+/** This interface represents an <em>inversible affine transform</em> in a space.
+ * Common examples of this type of transform in Euclidean space include
+ * scalings, translations, and rotations.
+ *
+ * <h2>Implementation Note</h2>
+ * <p>Implementations are responsible for ensuring that they meet the geometric
+ * requirements outlined above. These are:
+ * <ol>
+ * <li>The transform must be <a href="https://en.wikipedia.org/wiki/Affine_transformation">affine</a>.
+ * This means that points and parallel lines must be preserved by the transformation. For example,
+ * a translation or rotation in Euclidean 3D space meets this requirement because a mapping exists for
+ * all points and lines that are parallel before the transform remain parallel afterwards.
+ * However, a projective transform that causes parallel lines to meet at a point in infinity does not.
+ * </li>
+ * <li>The transform must be <em>inversible</em>. An inverse transform must exist that will return
+ * the original point if given the transformed point. In other words, for a transform {@code t}, there
+ * must exist an inverse {@code inv} such that {@code inv.apply(t.apply(pt))} returns a point equal to
+ * the input point {@code pt}.
+ * </li>
+ * </ol>
+ * Implementations that do not meet these requirements cannot be expected to produce correct results in
+ * algorithms that use this interface.
+ *
+ * @param <P> Point implementation type
+ * @see <a href="https://en.wikipedia.org/wiki/Affine_transformation">Affine Space</a>
+ */
+public interface Transform<P extends Point<P>> extends Function<P, P> {
+
+ /** Return true if the transform preserves the orientation of the space.
+ * For example, in Euclidean 2D space, this will be true for translations,
+ * rotations, and scalings but will be false for reflections.
+ * @return true if the transform preserves the orientation of the space
+ * @see <a href="https://en.wikipedia.org/wiki/Orientation_(vector_space)">Orientation</a>
+ */
+ boolean preservesOrientation();
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
index 873e24e..c62abe7 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
@@ -16,8 +16,6 @@
*/
package org.apache.commons.geometry.core;
-import org.apache.commons.geometry.core.exception.IllegalNormException;
-
/** Interface representing a vector in a vector space or displacement vectors
* in an affine space.
*
@@ -96,7 +94,8 @@
/** Get a normalized vector aligned with the instance. The returned
* vector has a magnitude of 1.
* @return a new normalized vector
- * @exception IllegalNormException if the norm is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm is
+ * zero, NaN, or infinite
*/
V normalize();
@@ -131,7 +130,8 @@
/** Compute the angular separation between two vectors in radians.
* @param v other vector
* @return angular separation between this instance and v in radians
- * @exception IllegalNormException if either vector has a zero, NaN, or infinite norm
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if either
+ * vector has a zero, NaN, or infinite norm
*/
double angle(V v);
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryException.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryException.java
index 57462be..d37bef8 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryException.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryException.java
@@ -22,7 +22,7 @@
*/
public class GeometryException extends RuntimeException {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180909L;
/** Simple constructor with error message.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java
index a5f7d3f..592045b 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java
@@ -21,7 +21,7 @@
*/
public class GeometryValueException extends GeometryException {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20190210L;
/** Simple constructor with error message.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java
index 6993704..ad65322 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java
@@ -21,7 +21,7 @@
*/
public class IllegalNormException extends GeometryValueException {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180909L;
/** Simple constructor accepting the illegal norm value.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java
new file mode 100644
index 0000000..cd52a73
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java
@@ -0,0 +1,33 @@
+/*
+ * 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.core.internal;
+
+/** Interface for determining equivalency, not exact equality, between
+ * two objects. This is performs a function similar to {@link Object#equals(Object)}
+ * but allows fuzzy comparisons to occur instead of strict equality. This is
+ * especially useful when comparing objects with floating point values that
+ * may not be exact but are operationally equivalent.
+ * @param <T> The type being compared
+ */
+public interface Equivalency<T> {
+
+ /** Determine if this object is equivalent (effectively equal) to the argument.
+ * @param other the object to compare for equivalency
+ * @return true if this object is equivalent to the argument; false otherwise
+ */
+ boolean eq(T other);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalError.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalError.java
index 219ccdb..bc52f62 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalError.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalError.java
@@ -27,7 +27,7 @@
"error in the algorithm implementation than in the calling code or data. Please file a bug report " +
"with the developers.";
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180913L;
/** Simple constructor with a default error message.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/IteratorTransform.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/IteratorTransform.java
new file mode 100644
index 0000000..b60d40d
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/IteratorTransform.java
@@ -0,0 +1,92 @@
+/*
+ * 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.core.internal;
+
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+
+/** Class that wraps another iterator, converting each input iterator value into
+ * one or more output iterator values.
+ * @param <I> Input iterator type
+ * @param <T> Output iterator type
+ */
+public abstract class IteratorTransform<I, T> implements Iterator<T> {
+
+ /** Input iterator instance that supplies the input values for this instance. */
+ private final Iterator<I> inputIterator;
+
+ /** Output value queue. */
+ private final Deque<T> outputQueue = new LinkedList<>();
+
+ /** Create a new instance that uses the given iterator as the input source.
+ * @param inputIterator iterator supplying input values
+ */
+ public IteratorTransform(final Iterator<I> inputIterator) {
+ this.inputIterator = inputIterator;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasNext() {
+ return loadNextOutput();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public T next() {
+ if (outputQueue.isEmpty()) {
+ throw new NoSuchElementException();
+ }
+
+ return outputQueue.removeFirst();
+ }
+
+ /** Load the next output values into the output queue. Returns true if the output queue
+ * contains more entries.
+ * @return true if more output values are available
+ */
+ private boolean loadNextOutput() {
+ while (outputQueue.isEmpty() && inputIterator.hasNext()) {
+ acceptInput(inputIterator.next());
+ }
+
+ return !outputQueue.isEmpty();
+ }
+
+ /** Add a value to the output queue.
+ * @param value value to add to the output queue
+ */
+ protected void addOutput(final T value) {
+ outputQueue.add(value);
+ }
+
+ /** Add multiple values to the output queue.
+ * @param values values to add to the output queue
+ */
+ protected void addAllOutput(final Collection<T> values) {
+ outputQueue.addAll(values);
+ }
+
+ /** Accept a value from the input iterator. This method should take
+ * the input value and add one or more values to the output queue.
+ * @param input value from the input iterator
+ */
+ protected abstract void acceptInput(I input);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/SimpleTupleFormat.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/SimpleTupleFormat.java
index 15637e0..ce0c9ac 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/SimpleTupleFormat.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/SimpleTupleFormat.java
@@ -25,7 +25,7 @@
/** Default value separator string. */
private static final String DEFAULT_SEPARATOR = ",";
- /** Space character */
+ /** Space character. */
private static final String SPACE = " ";
/** Static instance configured with default values. Tuples in this format
@@ -34,13 +34,13 @@
private static final SimpleTupleFormat DEFAULT_INSTANCE =
new SimpleTupleFormat(",", "(", ")");
- /** String separating tuple values */
+ /** String separating tuple values. */
private final String separator;
- /** String used to signal the start of a tuple; may be null */
+ /** String used to signal the start of a tuple; may be null. */
private final String prefix;
- /** String used to signal the end of a tuple; may be null */
+ /** String used to signal the end of a tuple; may be null. */
private final String suffix;
/** Constructs a new instance with the default string separator (a comma)
@@ -298,8 +298,7 @@
matchSequence(str, separator, pos);
return value;
- }
- catch (NumberFormatException exc) {
+ } catch (NumberFormatException exc) {
fail(String.format("unable to parse number from string \"%s\"", substr), str, pos, exc);
return 0.0; // for the compiler
}
@@ -342,7 +341,7 @@
int idx = pos.getIndex();
final int len = str.length();
- for (; idx<len; ++idx) {
+ for (; idx < len; ++idx) {
if (!Character.isWhitespace(str.codePointAt(idx))) {
break;
}
@@ -369,7 +368,7 @@
int i = idx;
int s = 0;
- for (; i<inputLength && s<seqLength; ++i, ++s) {
+ for (; i < inputLength && s < seqLength; ++i, ++s) {
if (str.codePointAt(i) != seq.codePointAt(s)) {
break;
}
@@ -444,7 +443,7 @@
*/
private static class TupleParseException extends IllegalArgumentException {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180629;
/** Simple constructor.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegion.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegion.java
new file mode 100644
index 0000000..d920335
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegion.java
@@ -0,0 +1,379 @@
+/*
+ * 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.core.partitioning;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+
+/** Base class for convex hyperplane-bounded regions. This class provides generic implementations of many
+ * algorithms related to convex regions.
+ * @param <P> Point implementation type
+ * @param <S> Convex subhyperplane implementation type
+ */
+public abstract class AbstractConvexHyperplaneBoundedRegion<P extends Point<P>, S extends ConvexSubHyperplane<P>>
+ implements HyperplaneBoundedRegion<P>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190812L;
+
+ /** List of boundaries for the region. */
+ private final List<S> boundaries;
+
+ /** Simple constructor. Callers are responsible for ensuring that the given list of subhyperplanes
+ * represent a valid convex region boundary. No validation is performed.
+ * @param boundaries the boundaries of the convex region
+ */
+ protected AbstractConvexHyperplaneBoundedRegion(final List<S> boundaries) {
+ this.boundaries = Collections.unmodifiableList(boundaries);
+ }
+
+ /** Get the boundaries of the convex region. The exact ordering of the boundaries
+ * is not guaranteed.
+ * @return the boundaries of the convex region
+ */
+ public List<S> getBoundaries() {
+ return boundaries;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFull() {
+ // no boundaries => no outside
+ return boundaries.isEmpty();
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns false.</p>
+ */
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getBoundarySize() {
+ double sum = 0.0;
+ for (final S boundary : boundaries) {
+ sum += boundary.getSize();
+ }
+
+ return sum;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionLocation classify(P pt) {
+ boolean isOn = false;
+
+ HyperplaneLocation loc;
+ for (final S boundary : boundaries) {
+ loc = boundary.getHyperplane().classify(pt);
+
+ if (loc == HyperplaneLocation.PLUS) {
+ return RegionLocation.OUTSIDE;
+ } else if (loc == HyperplaneLocation.ON) {
+ isOn = true;
+ }
+ }
+
+ return isOn ? RegionLocation.BOUNDARY : RegionLocation.INSIDE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public P project(P pt) {
+
+ P projected;
+ double dist;
+
+ P closestPt = null;
+ double closestDist = Double.POSITIVE_INFINITY;
+
+ for (final S boundary : boundaries) {
+ projected = boundary.closest(pt);
+ dist = pt.distance(projected);
+
+ if (projected != null && (closestPt == null || dist < closestDist)) {
+ closestPt = projected;
+ closestDist = dist;
+ }
+ }
+
+ return closestPt;
+ }
+
+ /** Trim the given convex subhyperplane to the portion contained inside this instance.
+ * @param convexSubHyperplane convex subhyperplane to trim. Null is returned if the subhyperplane
+ * does not intersect the instance.
+ * @return portion of the argument that lies entirely inside the region represented by
+ * this instance, or null if it does not intersect.
+ */
+ public ConvexSubHyperplane<P> trim(final ConvexSubHyperplane<P> convexSubHyperplane) {
+ ConvexSubHyperplane<P> remaining = convexSubHyperplane;
+ for (final S boundary : boundaries) {
+ remaining = remaining.split(boundary.getHyperplane()).getMinus();
+ if (remaining == null) {
+ break;
+ }
+ }
+
+ return remaining;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[boundaries= ")
+ .append(boundaries);
+
+ return sb.toString();
+ }
+
+ /** Generic, internal transform method. Subclasses should use this to implement their own transform methods.
+ * @param transform the transform to apply to the instance
+ * @param thisInstance a reference to the current instance; this is passed as
+ * an argument in order to allow it to be a generic type
+ * @param subhpType the type used for the boundary subhyperplanes
+ * @param factory function used to create new convex region instances
+ * @param <R> Region implementation type
+ * @return the result of the transform operation
+ */
+ protected <R extends AbstractConvexHyperplaneBoundedRegion<P, S>> R transformInternal(
+ final Transform<P> transform, final R thisInstance, final Class<S> subhpType,
+ final Function<List<S>, R> factory) {
+
+ if (isFull()) {
+ return thisInstance;
+ }
+
+ final List<S> origBoundaries = getBoundaries();
+
+ final int size = origBoundaries.size();
+ final List<S> tBoundaries = new ArrayList<>(size);
+
+ // determine if the hyperplanes should be reversed
+ final S boundary = origBoundaries.get(0);
+ ConvexSubHyperplane<P> tBoundary = boundary.transform(transform);
+
+ final boolean reverseDirection = swapsInsideOutside(transform);
+
+ // transform all of the segments
+ if (reverseDirection) {
+ tBoundary = tBoundary.reverse();
+ }
+ tBoundaries.add(subhpType.cast(tBoundary));
+
+ for (int i = 1; i < origBoundaries.size(); ++i) {
+ tBoundary = origBoundaries.get(i).transform(transform);
+
+ if (reverseDirection) {
+ tBoundary = tBoundary.reverse();
+ }
+
+ tBoundaries.add(subhpType.cast(tBoundary));
+ }
+
+ return factory.apply(tBoundaries);
+ }
+
+ /** Return true if the given transform swaps the inside and outside of
+ * the region.
+ *
+ * <p>The default behavior of this method is to return true if the transform
+ * does not preserve spatial orientation (ie, {@link Transform#preservesOrientation()}
+ * is false). Subclasses may need to override this method to implement the correct
+ * behavior for their space and dimension.</p>
+ * @param transform transform to check
+ * @return true if the given transform swaps the interior and exterior of
+ * the region
+ */
+ protected boolean swapsInsideOutside(final Transform<P> transform) {
+ return !transform.preservesOrientation();
+ }
+
+ /** Generic, internal split method. Subclasses should call this from their {@link #split(Hyperplane)} methods.
+ * @param splitter splitting hyperplane
+ * @param thisInstance a reference to the current instance; this is passed as
+ * an argument in order to allow it to be a generic type
+ * @param subhpType the type used for the boundary subhyperplanes
+ * @param factory function used to create new convex region instances
+ * @param <R> Region implementation type
+ * @return the result of the split operation
+ */
+ protected <R extends AbstractConvexHyperplaneBoundedRegion<P, S>> Split<R> splitInternal(
+ final Hyperplane<P> splitter, final R thisInstance, final Class<S> subhpType,
+ final Function<List<S>, R> factory) {
+
+ if (isFull()) {
+ final R minus = factory.apply(Arrays.asList(subhpType.cast(splitter.span())));
+ final R plus = factory.apply(Arrays.asList(subhpType.cast(splitter.reverse().span())));
+
+ return new Split<>(minus, plus);
+ } else {
+ final ConvexSubHyperplane<P> trimmedSplitter = trim(splitter.span());
+
+ if (trimmedSplitter == null) {
+ // The splitter lies entirely outside of the region; we need
+ // to determine whether we lie on the plus or minus side of the splitter.
+ // We can use the first boundary to determine this. If the boundary is entirely
+ // on the minus side of the splitter or lies directly on the splitter and has
+ // the same orientation, then the area lies on the minus side of the splitter.
+ // Otherwise, it lies on the plus side.
+ final ConvexSubHyperplane<P> testSegment = boundaries.get(0);
+ final SplitLocation testLocation = testSegment.split(splitter).getLocation();
+
+ if (SplitLocation.MINUS == testLocation ||
+ (SplitLocation.NEITHER == testLocation &&
+ splitter.similarOrientation(testSegment.getHyperplane()))) {
+ return new Split<>(thisInstance, null);
+ }
+
+ return new Split<>(null, thisInstance);
+ }
+
+ final List<S> minusBoundaries = new ArrayList<>();
+ final List<S> plusBoundaries = new ArrayList<>();
+
+ Split<? extends ConvexSubHyperplane<P>> split;
+ ConvexSubHyperplane<P> minusBoundary;
+ ConvexSubHyperplane<P> plusBoundary;
+
+ for (final S boundary : boundaries) {
+ split = boundary.split(splitter);
+
+ minusBoundary = split.getMinus();
+ plusBoundary = split.getPlus();
+
+ if (minusBoundary != null) {
+ minusBoundaries.add(subhpType.cast(minusBoundary));
+ }
+
+ if (plusBoundary != null) {
+ plusBoundaries.add(subhpType.cast(plusBoundary));
+ }
+ }
+
+ minusBoundaries.add(subhpType.cast(trimmedSplitter));
+ plusBoundaries.add(subhpType.cast(trimmedSplitter.reverse()));
+
+ return new Split<>(factory.apply(minusBoundaries), factory.apply(plusBoundaries));
+ }
+ }
+
+ /** Internal class encapsulating the logic for building convex region boundaries from collections of
+ * hyperplanes.
+ * @param <P> Point implementation type
+ * @param <S> ConvexSubHyperplane implementation type
+ */
+ protected static class ConvexRegionBoundaryBuilder<P extends Point<P>, S extends ConvexSubHyperplane<P>> {
+
+ /** Convex subhyperplane implementation type. */
+ private final Class<S> subhyperplaneType;
+
+ /** Construct a new instance for building convex region boundaries with the given convex subhyperplane
+ * implementation type.
+ * @param subhyperplaneType Convex subhyperplane implementation type
+ */
+ public ConvexRegionBoundaryBuilder(final Class<S> subhyperplaneType) {
+ this.subhyperplaneType = subhyperplaneType;
+ }
+
+ /** Compute a list of convex subhyperplanes representing the boundaries of the convex region
+ * bounded by the given collection of hyperplanes.
+ * @param bounds hyperplanes defining the convex region
+ * @return a list of convex subhyperplanes representing the boundaries of the convex region
+ * @throws GeometryException if the given hyperplanes do not form a convex region
+ */
+ public List<S> build(final Iterable<? extends Hyperplane<P>> bounds) {
+
+ final List<S> boundaries = new ArrayList<>();
+
+ // cut each hyperplane by every other hyperplane in order to get the subplane boundaries
+ boolean notConvex = false;
+ int outerIdx = 0;
+ ConvexSubHyperplane<P> subhp;
+
+ for (final Hyperplane<P> hp : bounds) {
+ ++outerIdx;
+ subhp = hp.span();
+
+ int innerIdx = 0;
+ for (final Hyperplane<P> splitter : bounds) {
+ ++innerIdx;
+
+ if (hp != splitter) {
+ final Split<? extends ConvexSubHyperplane<P>> split = subhp.split(splitter);
+
+ if (split.getLocation() == SplitLocation.NEITHER) {
+ if (hp.similarOrientation(splitter)) {
+ // two or more splitters are the equivalent; only
+ // use the segment from the first one
+ if (outerIdx > innerIdx) {
+ subhp = null;
+ }
+ } else {
+ // two or more splitters are coincident and have opposite
+ // orientations, meaning that no area is on the minus side
+ // of both
+ notConvex = true;
+ break;
+ }
+ } else {
+ subhp = subhp.split(splitter).getMinus();
+ }
+
+ if (subhp == null) {
+ break;
+ }
+ } else if (outerIdx > innerIdx) {
+ // this hyperplane is duplicated in the list; skip all but the
+ // first insertion of its subhyperplane
+ subhp = null;
+ break;
+ }
+ }
+
+ if (notConvex) {
+ break;
+ }
+
+ if (subhp != null) {
+ boundaries.add(subhyperplaneType.cast(subhp));
+ }
+ }
+
+ if (notConvex || (outerIdx > 0 && boundaries.isEmpty())) {
+ throw new GeometryException("Bounding hyperplanes do not produce a convex region: " + bounds);
+ }
+
+ return boundaries;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplane.java
new file mode 100644
index 0000000..6bfb2b8
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplane.java
@@ -0,0 +1,110 @@
+/*
+ * 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.core.partitioning;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+
+/** Abstract base class for subhyperplane implementations that embed a lower-dimension region through
+ * an embedding hyperplane.
+ * @param <P> Point implementation type
+ * @param <S> Subspace point implementation type
+ * @param <H> Hyperplane containing the embedded subspace
+ */
+public abstract class AbstractEmbeddingSubHyperplane<
+ P extends Point<P>,
+ S extends Point<S>,
+ H extends EmbeddingHyperplane<P, S>> implements SubHyperplane<P>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190729L;
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFull() {
+ return getSubspaceRegion().isFull();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ return getSubspaceRegion().isEmpty();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return Double.isInfinite(getSize());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return !isInfinite();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return getSubspaceRegion().getSize();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionLocation classify(P point) {
+ final H hyperplane = getHyperplane();
+
+ if (hyperplane.contains(point)) {
+ final S subPoint = hyperplane.toSubspace(point);
+
+ return getSubspaceRegion().classify(subPoint);
+ }
+ return RegionLocation.OUTSIDE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public P closest(P point) {
+ final H hyperplane = getHyperplane();
+
+ final P projected = hyperplane.project(point);
+ final S subProjected = hyperplane.toSubspace(projected);
+
+ final Region<S> region = getSubspaceRegion();
+ if (region.contains(subProjected)) {
+ return projected;
+ }
+
+ final S subRegionBoundary = region.project(subProjected);
+ if (subRegionBoundary != null) {
+ return hyperplane.toSpace(subRegionBoundary);
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public abstract H getHyperplane();
+
+ /** Return the embedded subspace region for this instance.
+ * @return the embedded subspace region for this instance
+ */
+ public abstract HyperplaneBoundedRegion<S> getSubspaceRegion();
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplane.java
new file mode 100644
index 0000000..03d1723
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplane.java
@@ -0,0 +1,70 @@
+/*
+ * 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.core.partitioning;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Base class for hyperplane implementations.
+ * @param <P> Point implementation type
+ */
+public abstract class AbstractHyperplane<P extends Point<P>> implements Hyperplane<P>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 1L;
+
+ /** Precision object used to perform floating point comparisons. */
+ private final DoublePrecisionContext precision;
+
+ /** Construct an instance using the given precision context.
+ * @param precision object used to perform floating point comparisons
+ */
+ protected AbstractHyperplane(final DoublePrecisionContext precision) {
+ this.precision = precision;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public HyperplaneLocation classify(final P point) {
+ final double offsetValue = offset(point);
+
+ final int cmp = precision.sign(offsetValue);
+ if (cmp > 0) {
+ return HyperplaneLocation.PLUS;
+ } else if (cmp < 0) {
+ return HyperplaneLocation.MINUS;
+ }
+ return HyperplaneLocation.ON;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(final P point) {
+ final HyperplaneLocation loc = classify(point);
+ return loc == HyperplaneLocation.ON;
+ }
+
+ /** Get the precision object used to perform floating point
+ * comparisons for this instance.
+ * @return the precision object for this instance
+ */
+ public DoublePrecisionContext getPrecision() {
+ return precision;
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java
deleted file mode 100644
index d3047c8..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java
+++ /dev/null
@@ -1,516 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.TreeSet;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-
-/** Abstract class for all regions, independent of geometry type or dimension.
-
- * @param <P> Point type defining the space
- * @param <S> Point type defining the sub-space
- */
-public abstract class AbstractRegion<P extends Point<P>, S extends Point<S>> implements Region<P> {
-
- /** Inside/Outside BSP tree. */
- private BSPTree<P> tree;
-
- /** Precision context used to determine floating point equality. */
- private final DoublePrecisionContext precision;
-
- /** Size of the instance. */
- private double size;
-
- /** Barycenter. */
- private P barycenter;
-
- /** Build a region representing the whole space.
- * @param precision precision context used to compare floating point numbers
- */
- protected AbstractRegion(final DoublePrecisionContext precision) {
- this.tree = new BSPTree<>(Boolean.TRUE);
- this.precision = precision;
- }
-
- /** Build a region from an inside/outside BSP tree.
- * <p>The leaf nodes of the BSP tree <em>must</em> have a
- * {@code Boolean} attribute representing the inside status of
- * the corresponding cell (true for inside cells, false for outside
- * cells). In order to avoid building too many small objects, it is
- * recommended to use the predefined constants
- * {@code Boolean.TRUE} and {@code Boolean.FALSE}. The
- * tree also <em>must</em> have either null internal nodes or
- * internal nodes representing the boundary as specified in the
- * {@link #getTree getTree} method).</p>
- * @param tree inside/outside BSP tree representing the region
- * @param precision precision context used to compare floating point values
- */
- protected AbstractRegion(final BSPTree<P> tree, final DoublePrecisionContext precision) {
- this.tree = tree;
- this.precision = precision;
- }
-
- /** Build a Region from a Boundary REPresentation (B-rep).
- * <p>The boundary is provided as a collection of {@link
- * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
- * interior part of the region on its minus side and the exterior on
- * its plus side.</p>
- * <p>The boundary elements can be in any order, and can form
- * several non-connected sets (like for example polygons with holes
- * or a set of disjoints polyhedrons considered as a whole). In
- * fact, the elements do not even need to be connected together
- * (their topological connections are not used here). However, if the
- * boundary does not really separate an inside open from an outside
- * open (open having here its topological meaning), then subsequent
- * calls to the {@link #checkPoint(Point) checkPoint} method will not be
- * meaningful anymore.</p>
- * <p>If the boundary is empty, the region will represent the whole
- * space.</p>
- * @param boundary collection of boundary elements, as a
- * collection of {@link SubHyperplane SubHyperplane} objects
- * @param precision precision context used to compare floating point values
- */
- protected AbstractRegion(final Collection<SubHyperplane<P>> boundary, final DoublePrecisionContext precision) {
-
- this.precision = precision;
-
- if (boundary.size() == 0) {
-
- // the tree represents the whole space
- tree = new BSPTree<>(Boolean.TRUE);
-
- } else {
-
- // sort the boundary elements in decreasing size order
- // (we don't want equal size elements to be removed, so
- // we use a trick to fool the TreeSet)
- final TreeSet<SubHyperplane<P>> ordered = new TreeSet<>(new Comparator<SubHyperplane<P>>() {
- /** {@inheritDoc} */
- @Override
- public int compare(final SubHyperplane<P> o1, final SubHyperplane<P> o2) {
- final double size1 = o1.getSize();
- final double size2 = o2.getSize();
- return (size2 < size1) ? -1 : ((o1 == o2) ? 0 : +1);
- }
- });
- ordered.addAll(boundary);
-
- // build the tree top-down
- tree = new BSPTree<>();
- insertCuts(tree, ordered);
-
- // set up the inside/outside flags
- tree.visit(new BSPTreeVisitor<P>() {
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<P> node) {
- return Order.PLUS_SUB_MINUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<P> node) {
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<P> node) {
- if (node.getParent() == null || node == node.getParent().getMinus()) {
- node.setAttribute(Boolean.TRUE);
- } else {
- node.setAttribute(Boolean.FALSE);
- }
- }
- });
-
- }
-
- }
-
- /** Build a convex region from an array of bounding hyperplanes.
- * @param hyperplanes array of bounding hyperplanes (if null, an
- * empty region will be built)
- * @param precision precision context used to compare floating point values
- */
- public AbstractRegion(final Hyperplane<P>[] hyperplanes, final DoublePrecisionContext precision) {
- this.precision = precision;
- if ((hyperplanes == null) || (hyperplanes.length == 0)) {
- tree = new BSPTree<>(Boolean.FALSE);
- } else {
-
- // use the first hyperplane to build the right class
- tree = hyperplanes[0].wholeSpace().getTree(false);
-
- // chop off parts of the space
- BSPTree<P> node = tree;
- node.setAttribute(Boolean.TRUE);
- for (final Hyperplane<P> hyperplane : hyperplanes) {
- if (node.insertCut(hyperplane)) {
- node.setAttribute(null);
- node.getPlus().setAttribute(Boolean.FALSE);
- node = node.getMinus();
- node.setAttribute(Boolean.TRUE);
- }
- }
-
- }
-
- }
-
- /** {@inheritDoc} */
- @Override
- public abstract AbstractRegion<P, S> buildNew(BSPTree<P> newTree);
-
- /** Get the object used to determine floating point equality for this region.
- * @return the floating point precision context for the instance
- */
- public DoublePrecisionContext getPrecision() {
- return precision;
- }
-
- /** Recursively build a tree by inserting cut sub-hyperplanes.
- * @param node current tree node (it is a leaf node at the beginning
- * of the call)
- * @param boundary collection of edges belonging to the cell defined
- * by the node
- */
- private void insertCuts(final BSPTree<P> node, final Collection<SubHyperplane<P>> boundary) {
-
- final Iterator<SubHyperplane<P>> iterator = boundary.iterator();
-
- // build the current level
- Hyperplane<P> inserted = null;
- while ((inserted == null) && iterator.hasNext()) {
- inserted = iterator.next().getHyperplane();
- if (!node.insertCut(inserted.copySelf())) {
- inserted = null;
- }
- }
-
- if (!iterator.hasNext()) {
- return;
- }
-
- // distribute the remaining edges in the two sub-trees
- final ArrayList<SubHyperplane<P>> plusList = new ArrayList<>();
- final ArrayList<SubHyperplane<P>> minusList = new ArrayList<>();
- while (iterator.hasNext()) {
- final SubHyperplane<P> other = iterator.next();
- final SubHyperplane.SplitSubHyperplane<P> split = other.split(inserted);
- switch (split.getSide()) {
- case PLUS:
- plusList.add(other);
- break;
- case MINUS:
- minusList.add(other);
- break;
- case BOTH:
- plusList.add(split.getPlus());
- minusList.add(split.getMinus());
- break;
- default:
- // ignore the sub-hyperplanes belonging to the cut hyperplane
- }
- }
-
- // recurse through lower levels
- insertCuts(node.getPlus(), plusList);
- insertCuts(node.getMinus(), minusList);
-
- }
-
- /** {@inheritDoc} */
- @Override
- public AbstractRegion<P, S> copySelf() {
- return buildNew(tree.copySelf());
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isEmpty() {
- return isEmpty(tree);
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isEmpty(final BSPTree<P> node) {
-
- // we use a recursive function rather than the BSPTreeVisitor
- // interface because we can stop visiting the tree as soon as we
- // have found an inside cell
-
- if (node.getCut() == null) {
- // if we find an inside node, the region is not empty
- return !((Boolean) node.getAttribute());
- }
-
- // check both sides of the sub-tree
- return isEmpty(node.getMinus()) && isEmpty(node.getPlus());
-
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isFull() {
- return isFull(tree);
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isFull(final BSPTree<P> node) {
-
- // we use a recursive function rather than the BSPTreeVisitor
- // interface because we can stop visiting the tree as soon as we
- // have found an outside cell
-
- if (node.getCut() == null) {
- // if we find an outside node, the region does not cover full space
- return (Boolean) node.getAttribute();
- }
-
- // check both sides of the sub-tree
- return isFull(node.getMinus()) && isFull(node.getPlus());
-
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean contains(final Region<P> region) {
- return new RegionFactory<P>().difference(region, this).isEmpty();
- }
-
- /** {@inheritDoc}
- */
- @Override
- public BoundaryProjection<P> projectToBoundary(final P point) {
- final BoundaryProjector<P, S> projector = new BoundaryProjector<>(point);
- getTree(true).visit(projector);
- return projector.getProjection();
- }
-
- /** {@inheritDoc} */
- @Override
- public Location checkPoint(final P point) {
- return checkPoint(tree, point);
- }
-
- /** Check a point with respect to the region starting at a given node.
- * @param node root node of the region
- * @param point point to check
- * @return a code representing the point status: either {@link
- * Region.Location#INSIDE INSIDE}, {@link Region.Location#OUTSIDE
- * OUTSIDE} or {@link Region.Location#BOUNDARY BOUNDARY}
- */
- protected Location checkPoint(final BSPTree<P> node, final P point) {
- final BSPTree<P> cell = node.getCell(point, precision);
- if (cell.getCut() == null) {
- // the point is in the interior of a cell, just check the attribute
- return ((Boolean) cell.getAttribute()) ? Location.INSIDE : Location.OUTSIDE;
- }
-
- // the point is on a cut-sub-hyperplane, is it on a boundary ?
- final Location minusCode = checkPoint(cell.getMinus(), point);
- final Location plusCode = checkPoint(cell.getPlus(), point);
- return (minusCode == plusCode) ? minusCode : Location.BOUNDARY;
-
- }
-
- /** {@inheritDoc} */
- @Override
- public BSPTree<P> getTree(final boolean includeBoundaryAttributes) {
- if (includeBoundaryAttributes && (tree.getCut() != null) && (tree.getAttribute() == null)) {
- // compute the boundary attributes
- tree.visit(new BoundaryBuilder<P>());
- }
- return tree;
- }
-
- /** {@inheritDoc} */
- @Override
- public double getBoundarySize() {
- final BoundarySizeVisitor<P> visitor = new BoundarySizeVisitor<>();
- getTree(true).visit(visitor);
- return visitor.getSize();
- }
-
- /** {@inheritDoc} */
- @Override
- public double getSize() {
- if (barycenter == null) {
- computeGeometricalProperties();
- }
- return size;
- }
-
- /** Set the size of the instance.
- * @param size size of the instance
- */
- protected void setSize(final double size) {
- this.size = size;
- }
-
- /** {@inheritDoc} */
- @Override
- public P getBarycenter() {
- if (barycenter == null) {
- computeGeometricalProperties();
- }
- return barycenter;
- }
-
- /** Set the barycenter of the instance.
- * @param barycenter barycenter of the instance
- */
- protected void setBarycenter(final P barycenter) {
- this.barycenter = barycenter;
- }
-
- /** Compute some geometrical properties.
- * <p>The properties to compute are the barycenter and the size.</p>
- */
- protected abstract void computeGeometricalProperties();
-
- /** {@inheritDoc} */
- @Override
- public SubHyperplane<P> intersection(final SubHyperplane<P> sub) {
- return recurseIntersection(tree, sub);
- }
-
- /** Recursively compute the parts of a sub-hyperplane that are
- * contained in the region.
- * @param node current BSP tree node
- * @param sub sub-hyperplane traversing the region
- * @return filtered sub-hyperplane
- */
- private SubHyperplane<P> recurseIntersection(final BSPTree<P> node, final SubHyperplane<P> sub) {
-
- if (node.getCut() == null) {
- return (Boolean) node.getAttribute() ? sub.copySelf() : null;
- }
-
- final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
- final SubHyperplane.SplitSubHyperplane<P> split = sub.split(hyperplane);
- if (split.getPlus() != null) {
- if (split.getMinus() != null) {
- // both sides
- final SubHyperplane<P> plus = recurseIntersection(node.getPlus(), split.getPlus());
- final SubHyperplane<P> minus = recurseIntersection(node.getMinus(), split.getMinus());
- if (plus == null) {
- return minus;
- } else if (minus == null) {
- return plus;
- } else {
- return plus.reunite(minus);
- }
- } else {
- // only on plus side
- return recurseIntersection(node.getPlus(), sub);
- }
- } else if (split.getMinus() != null) {
- // only on minus side
- return recurseIntersection(node.getMinus(), sub);
- } else {
- // on hyperplane
- return recurseIntersection(node.getPlus(),
- recurseIntersection(node.getMinus(), sub));
- }
-
- }
-
- /** Transform a region.
- * <p>Applying a transform to a region consist in applying the
- * transform to all the hyperplanes of the underlying BSP tree and
- * of the boundary (and also to the sub-hyperplanes embedded in
- * these hyperplanes) and to the barycenter. The instance is not
- * modified, a new instance is built.</p>
- * @param transform transform to apply
- * @return a new region, resulting from the application of the
- * transform to the instance
- */
- public AbstractRegion<P, S> applyTransform(final Transform<P, S> transform) {
-
- // transform the tree, except for boundary attribute splitters
- final Map<BSPTree<P>, BSPTree<P>> map = new HashMap<>();
- final BSPTree<P> transformedTree = recurseTransform(getTree(false), transform, map);
-
- // set up the boundary attributes splitters
- for (final Map.Entry<BSPTree<P>, BSPTree<P>> entry : map.entrySet()) {
- if (entry.getKey().getCut() != null) {
- @SuppressWarnings("unchecked")
- BoundaryAttribute<P> original = (BoundaryAttribute<P>) entry.getKey().getAttribute();
- if (original != null) {
- @SuppressWarnings("unchecked")
- BoundaryAttribute<P> transformed = (BoundaryAttribute<P>) entry.getValue().getAttribute();
- for (final BSPTree<P> splitter : original.getSplitters()) {
- transformed.getSplitters().add(map.get(splitter));
- }
- }
- }
- }
-
- return buildNew(transformedTree);
-
- }
-
- /** Recursively transform an inside/outside BSP-tree.
- * @param node current BSP tree node
- * @param transform transform to apply
- * @param map transformed nodes map
- * @return a new tree
- */
- @SuppressWarnings("unchecked")
- private BSPTree<P> recurseTransform(final BSPTree<P> node, final Transform<P, S> transform,
- final Map<BSPTree<P>, BSPTree<P>> map) {
-
- final BSPTree<P> transformedNode;
- if (node.getCut() == null) {
- transformedNode = new BSPTree<>(node.getAttribute());
- } else {
-
- final SubHyperplane<P> sub = node.getCut();
- final SubHyperplane<P> tSub = ((AbstractSubHyperplane<P, S>) sub).applyTransform(transform);
- BoundaryAttribute<P> attribute = (BoundaryAttribute<P>) node.getAttribute();
- if (attribute != null) {
- final SubHyperplane<P> tPO = (attribute.getPlusOutside() == null) ?
- null : ((AbstractSubHyperplane<P, S>) attribute.getPlusOutside()).applyTransform(transform);
- final SubHyperplane<P> tPI = (attribute.getPlusInside() == null) ?
- null : ((AbstractSubHyperplane<P, S>) attribute.getPlusInside()).applyTransform(transform);
- // we start with an empty list of splitters, it will be filled in out of recursion
- attribute = new BoundaryAttribute<>(tPO, tPI, new NodesSet<P>());
- }
-
- transformedNode = new BSPTree<>(tSub,
- recurseTransform(node.getPlus(), transform, map),
- recurseTransform(node.getMinus(), transform, map),
- attribute);
- }
-
- map.put(node, transformedNode);
- return transformedNode;
-
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java
deleted file mode 100644
index c8dfad1..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.commons.geometry.core.Point;
-
-/** This class implements the dimension-independent parts of {@link SubHyperplane}.
-
- * <p>sub-hyperplanes are obtained when parts of an {@link
- * Hyperplane hyperplane} are chopped off by other hyperplanes that
- * intersect it. The remaining part is a convex region. Such objects
- * appear in {@link BSPTree BSP trees} as the intersection of a cut
- * hyperplane with the convex region which it splits, the chopping
- * hyperplanes are the cut hyperplanes closer to the tree root.</p>
-
- * @param <P> Point type defining the space
- * @param <S> Point type defining the sub-space
- */
-public abstract class AbstractSubHyperplane<P extends Point<P>, S extends Point<S>>
- implements SubHyperplane<P> {
-
- /** Underlying hyperplane. */
- private final Hyperplane<P> hyperplane;
-
- /** Remaining region of the hyperplane. */
- private final Region<S> remainingRegion;
-
- /** Build a sub-hyperplane from an hyperplane and a region.
- * @param hyperplane underlying hyperplane
- * @param remainingRegion remaining region of the hyperplane
- */
- protected AbstractSubHyperplane(final Hyperplane<P> hyperplane,
- final Region<S> remainingRegion) {
- this.hyperplane = hyperplane;
- this.remainingRegion = remainingRegion;
- }
-
- /** Build a sub-hyperplane from an hyperplane and a region.
- * @param hyper underlying hyperplane
- * @param remaining remaining region of the hyperplane
- * @return a new sub-hyperplane
- */
- protected abstract AbstractSubHyperplane<P, S> buildNew(final Hyperplane<P> hyper,
- final Region<S> remaining);
-
- /** {@inheritDoc} */
- @Override
- public AbstractSubHyperplane<P, S> copySelf() {
- return buildNew(hyperplane.copySelf(), remainingRegion);
- }
-
- /** Get the underlying hyperplane.
- * @return underlying hyperplane
- */
- @Override
- public Hyperplane<P> getHyperplane() {
- return hyperplane;
- }
-
- /** Get the remaining region of the hyperplane.
- * <p>The returned region is expressed in the canonical hyperplane
- * frame and has the hyperplane dimension. For example a chopped
- * hyperplane in the 3D Euclidean is a 2D plane and the
- * corresponding region is a convex 2D polygon.</p>
- * @return remaining region of the hyperplane
- */
- public Region<S> getRemainingRegion() {
- return remainingRegion;
- }
-
- /** {@inheritDoc} */
- @Override
- public double getSize() {
- return remainingRegion.getSize();
- }
-
- /** {@inheritDoc} */
- @Override
- public AbstractSubHyperplane<P, S> reunite(final SubHyperplane<P> other) {
- @SuppressWarnings("unchecked")
- AbstractSubHyperplane<P, S> o = (AbstractSubHyperplane<P, S>) other;
- return buildNew(hyperplane,
- new RegionFactory<S>().union(remainingRegion, o.remainingRegion));
- }
-
- /** Apply a transform to the instance.
- * <p>The instance must be a (D-1)-dimension sub-hyperplane with
- * respect to the transform <em>not</em> a (D-2)-dimension
- * sub-hyperplane the transform knows how to transform by
- * itself. The transform will consist in transforming first the
- * hyperplane and then the all region using the various methods
- * provided by the transform.</p>
- * @param transform D-dimension transform to apply
- * @return the transformed instance
- */
- public AbstractSubHyperplane<P, S> applyTransform(final Transform<P, S> transform) {
- final Hyperplane<P> tHyperplane = transform.apply(hyperplane);
-
- // transform the tree, except for boundary attribute splitters
- final Map<BSPTree<S>, BSPTree<S>> map = new HashMap<>();
- final BSPTree<S> tTree =
- recurseTransform(remainingRegion.getTree(false), tHyperplane, transform, map);
-
- // set up the boundary attributes splitters
- for (final Map.Entry<BSPTree<S>, BSPTree<S>> entry : map.entrySet()) {
- if (entry.getKey().getCut() != null) {
- @SuppressWarnings("unchecked")
- BoundaryAttribute<S> original = (BoundaryAttribute<S>) entry.getKey().getAttribute();
- if (original != null) {
- @SuppressWarnings("unchecked")
- BoundaryAttribute<S> transformed = (BoundaryAttribute<S>) entry.getValue().getAttribute();
- for (final BSPTree<S> splitter : original.getSplitters()) {
- transformed.getSplitters().add(map.get(splitter));
- }
- }
- }
- }
-
- return buildNew(tHyperplane, remainingRegion.buildNew(tTree));
-
- }
-
- /** Recursively transform a BSP-tree from a sub-hyperplane.
- * @param node current BSP tree node
- * @param transformed image of the instance hyperplane by the transform
- * @param transform transform to apply
- * @param map transformed nodes map
- * @return a new tree
- */
- private BSPTree<S> recurseTransform(final BSPTree<S> node,
- final Hyperplane<P> transformed,
- final Transform<P, S> transform,
- final Map<BSPTree<S>, BSPTree<S>> map) {
-
- final BSPTree<S> transformedNode;
- if (node.getCut() == null) {
- transformedNode = new BSPTree<>(node.getAttribute());
- } else {
-
- @SuppressWarnings("unchecked")
- BoundaryAttribute<S> attribute = (BoundaryAttribute<S>) node.getAttribute();
- if (attribute != null) {
- final SubHyperplane<S> tPO = (attribute.getPlusOutside() == null) ?
- null : transform.apply(attribute.getPlusOutside(), hyperplane, transformed);
- final SubHyperplane<S> tPI = (attribute.getPlusInside() == null) ?
- null : transform.apply(attribute.getPlusInside(), hyperplane, transformed);
- // we start with an empty list of splitters, it will be filled in out of recursion
- attribute = new BoundaryAttribute<>(tPO, tPI, new NodesSet<S>());
- }
-
- transformedNode = new BSPTree<>(transform.apply(node.getCut(), hyperplane, transformed),
- recurseTransform(node.getPlus(), transformed, transform, map),
- recurseTransform(node.getMinus(), transformed, transform, map),
- attribute);
- }
-
- map.put(node, transformedNode);
- return transformedNode;
-
- }
-
- /** {@inheritDoc} */
- @Override
- public abstract SplitSubHyperplane<P> split(Hyperplane<P> hyper);
-
- /** {@inheritDoc} */
- @Override
- public boolean isEmpty() {
- return remainingRegion.isEmpty();
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java
deleted file mode 100644
index 9a00a89..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java
+++ /dev/null
@@ -1,781 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor.Order;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-
-/** This class represent a Binary Space Partition tree.
-
- * <p>BSP trees are an efficient way to represent space partitions and
- * to associate attributes with each cell. Each node in a BSP tree
- * represents a convex region which is partitioned in two convex
- * sub-regions at each side of a cut hyperplane. The root tree
- * contains the complete space.</p>
-
- * <p>The main use of such partitions is to use a boolean attribute to
- * define an inside/outside property, hence representing arbitrary
- * polytopes (line segments in 1D, polygons in 2D and polyhedrons in
- * 3D) and to operate on them.</p>
-
- * <p>Another example would be to represent Voronoi tesselations, the
- * attribute of each cell holding the defining point of the cell.</p>
-
- * <p>The application-defined attributes are shared among copied
- * instances and propagated to split parts. These attributes are not
- * used by the BSP-tree algorithms themselves, so the application can
- * use them for any purpose. Since the tree visiting method holds
- * internal and leaf nodes differently, it is possible to use
- * different classes for internal nodes attributes and leaf nodes
- * attributes. This should be used with care, though, because if the
- * tree is modified in any way after attributes have been set, some
- * internal nodes may become leaf nodes and some leaf nodes may become
- * internal nodes.</p>
-
- * <p>One of the main sources for the development of this package was
- * Bruce Naylor, John Amanatides and William Thibault paper <a
- * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf">Merging
- * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph '90,
- * Computer Graphics 24(4), August 1990, pp 115-124, published by the
- * Association for Computing Machinery (ACM).</p>
-
- * @param <P> Point type defining the space
- */
-public class BSPTree<P extends Point<P>> {
-
- /** Cut sub-hyperplane. */
- private SubHyperplane<P> cut;
-
- /** Tree at the plus side of the cut hyperplane. */
- private BSPTree<P> plus;
-
- /** Tree at the minus side of the cut hyperplane. */
- private BSPTree<P> minus;
-
- /** Parent tree. */
- private BSPTree<P> parent;
-
- /** Application-defined attribute. */
- private Object attribute;
-
- /** Build a tree having only one root cell representing the whole space.
- */
- public BSPTree() {
- cut = null;
- plus = null;
- minus = null;
- parent = null;
- attribute = null;
- }
-
- /** Build a tree having only one root cell representing the whole space.
- * @param attribute attribute of the tree (may be null)
- */
- public BSPTree(final Object attribute) {
- cut = null;
- plus = null;
- minus = null;
- parent = null;
- this.attribute = attribute;
- }
-
- /** Build a BSPTree from its underlying elements.
- * <p>This method does <em>not</em> perform any verification on
- * consistency of its arguments, it should therefore be used only
- * when then caller knows what it is doing.</p>
- * <p>This method is mainly useful to build trees
- * bottom-up. Building trees top-down is realized with the help of
- * method {@link #insertCut insertCut}.</p>
- * @param cut cut sub-hyperplane for the tree
- * @param plus plus side sub-tree
- * @param minus minus side sub-tree
- * @param attribute attribute associated with the node (may be null)
- * @see #insertCut
- */
- public BSPTree(final SubHyperplane<P> cut, final BSPTree<P> plus, final BSPTree<P> minus,
- final Object attribute) {
- this.cut = cut;
- this.plus = plus;
- this.minus = minus;
- this.parent = null;
- this.attribute = attribute;
- plus.parent = this;
- minus.parent = this;
- }
-
- /** Insert a cut sub-hyperplane in a node.
- * <p>The sub-tree starting at this node will be completely
- * overwritten. The new cut sub-hyperplane will be built from the
- * intersection of the provided hyperplane with the cell. If the
- * hyperplane does intersect the cell, the cell will have two
- * children cells with {@code null} attributes on each side of
- * the inserted cut sub-hyperplane. If the hyperplane does not
- * intersect the cell then <em>no</em> cut hyperplane will be
- * inserted and the cell will be changed to a leaf cell. The
- * attribute of the node is never changed.</p>
- * <p>This method is mainly useful when called on leaf nodes
- * (i.e. nodes for which {@link #getCut getCut} returns
- * {@code null}), in this case it provides a way to build a
- * tree top-down (whereas the {@link #BSPTree(SubHyperplane,
- * BSPTree, BSPTree, Object) 4 arguments constructor} is devoted to
- * build trees bottom-up).</p>
- * @param hyperplane hyperplane to insert, it will be chopped in
- * order to fit in the cell defined by the parent nodes of the
- * instance
- * @return true if a cut sub-hyperplane has been inserted (i.e. if
- * the cell now has two leaf child nodes)
- * @see #BSPTree(SubHyperplane, BSPTree, BSPTree, Object)
- */
- public boolean insertCut(final Hyperplane<P> hyperplane) {
-
- if (cut != null) {
- plus.parent = null;
- minus.parent = null;
- }
-
- final SubHyperplane<P> chopped = fitToCell(hyperplane.wholeHyperplane());
- if (chopped == null || chopped.isEmpty()) {
- cut = null;
- plus = null;
- minus = null;
- return false;
- }
-
- cut = chopped;
- plus = new BSPTree<>();
- plus.parent = this;
- minus = new BSPTree<>();
- minus.parent = this;
- return true;
-
- }
-
- /** Copy the instance.
- * <p>The instance created is completely independent of the original
- * one. A deep copy is used, none of the underlying objects are
- * shared (except for the nodes attributes and immutable
- * objects).</p>
- * @return a new tree, copy of the instance
- */
- public BSPTree<P> copySelf() {
-
- if (cut == null) {
- return new BSPTree<>(attribute);
- }
-
- return new BSPTree<>(cut.copySelf(), plus.copySelf(), minus.copySelf(),
- attribute);
-
- }
-
- /** Get the cut sub-hyperplane.
- * @return cut sub-hyperplane, null if this is a leaf tree
- */
- public SubHyperplane<P> getCut() {
- return cut;
- }
-
- /** Get the tree on the plus side of the cut hyperplane.
- * @return tree on the plus side of the cut hyperplane, null if this
- * is a leaf tree
- */
- public BSPTree<P> getPlus() {
- return plus;
- }
-
- /** Get the tree on the minus side of the cut hyperplane.
- * @return tree on the minus side of the cut hyperplane, null if this
- * is a leaf tree
- */
- public BSPTree<P> getMinus() {
- return minus;
- }
-
- /** Get the parent node.
- * @return parent node, null if the node has no parents
- */
- public BSPTree<P> getParent() {
- return parent;
- }
-
- /** Associate an attribute with the instance.
- * @param attribute attribute to associate with the node
- * @see #getAttribute
- */
- public void setAttribute(final Object attribute) {
- this.attribute = attribute;
- }
-
- /** Get the attribute associated with the instance.
- * @return attribute associated with the node or null if no
- * attribute has been explicitly set using the {@link #setAttribute
- * setAttribute} method
- * @see #setAttribute
- */
- public Object getAttribute() {
- return attribute;
- }
-
- /** Visit the BSP tree nodes.
- * @param visitor object visiting the tree nodes
- */
- public void visit(final BSPTreeVisitor<P> visitor) {
- if (cut == null) {
- visitor.visitLeafNode(this);
- } else {
- Order order = visitor.visitOrder(this);
- switch (order) {
- case PLUS_MINUS_SUB:
- plus.visit(visitor);
- minus.visit(visitor);
- visitor.visitInternalNode(this);
- break;
- case PLUS_SUB_MINUS:
- plus.visit(visitor);
- visitor.visitInternalNode(this);
- minus.visit(visitor);
- break;
- case MINUS_PLUS_SUB:
- minus.visit(visitor);
- plus.visit(visitor);
- visitor.visitInternalNode(this);
- break;
- case MINUS_SUB_PLUS:
- minus.visit(visitor);
- visitor.visitInternalNode(this);
- plus.visit(visitor);
- break;
- case SUB_PLUS_MINUS:
- visitor.visitInternalNode(this);
- plus.visit(visitor);
- minus.visit(visitor);
- break;
- case SUB_MINUS_PLUS:
- visitor.visitInternalNode(this);
- minus.visit(visitor);
- plus.visit(visitor);
- break;
- default:
- // we shouldn't end up here since all possibilities are
- // covered above
- throw new IllegalStateException("Invalid node visit order: " + order);
- }
- }
- }
-
- /** Fit a sub-hyperplane inside the cell defined by the instance.
- * <p>Fitting is done by chopping off the parts of the
- * sub-hyperplane that lie outside of the cell using the
- * cut-hyperplanes of the parent nodes of the instance.</p>
- * @param sub sub-hyperplane to fit
- * @return a new sub-hyperplane, guaranteed to have no part outside
- * of the instance cell
- */
- private SubHyperplane<P> fitToCell(final SubHyperplane<P> sub) {
- SubHyperplane<P> s = sub;
- for (BSPTree<P> tree = this; tree.parent != null && s != null; tree = tree.parent) {
- if (tree == tree.parent.plus) {
- s = s.split(tree.parent.cut.getHyperplane()).getPlus();
- } else {
- s = s.split(tree.parent.cut.getHyperplane()).getMinus();
- }
- }
- return s;
- }
-
- /** Get the cell to which a point belongs.
- * <p>If the returned cell is a leaf node the points belongs to the
- * interior of the node, if the cell is an internal node the points
- * belongs to the node cut sub-hyperplane.</p>
- * @param point point to check
- * @param precision precision context used to determine which points
- * close to a cut hyperplane are considered to belong to the hyperplane itself
- * @return the tree cell to which the point belongs
- */
- public BSPTree<P> getCell(final P point, final DoublePrecisionContext precision) {
-
- if (cut == null) {
- return this;
- }
-
- // position of the point with respect to the cut hyperplane
- final double offset = cut.getHyperplane().getOffset(point);
-
- final int comparison = precision.compare(offset, 0.0);
- if (comparison == 0) {
- return this;
- } else if (comparison < 0) {
- // point is on the minus side of the cut hyperplane
- return minus.getCell(point, precision);
- } else {
- // point is on the plus side of the cut hyperplane
- return plus.getCell(point, precision);
- }
-
- }
-
- /** Get the cells whose cut sub-hyperplanes are close to the point.
- * @param point point to check
- * @param maxOffset offset below which a cut sub-hyperplane is considered
- * close to the point (in absolute value)
- * @return close cells (may be empty if all cut sub-hyperplanes are farther
- * than maxOffset from the point)
- */
- public List<BSPTree<P>> getCloseCuts(final P point, final double maxOffset) {
- final List<BSPTree<P>> close = new ArrayList<>();
- recurseCloseCuts(point, maxOffset, close);
- return close;
- }
-
- /** Get the cells whose cut sub-hyperplanes are close to the point.
- * @param point point to check
- * @param maxOffset offset below which a cut sub-hyperplane is considered
- * close to the point (in absolute value)
- * @param close list to fill
- */
- private void recurseCloseCuts(final P point, final double maxOffset,
- final List<BSPTree<P>> close) {
- if (cut != null) {
-
- // position of the point with respect to the cut hyperplane
- final double offset = cut.getHyperplane().getOffset(point);
-
- if (offset < -maxOffset) {
- // point is on the minus side of the cut hyperplane
- minus.recurseCloseCuts(point, maxOffset, close);
- } else if (offset > maxOffset) {
- // point is on the plus side of the cut hyperplane
- plus.recurseCloseCuts(point, maxOffset, close);
- } else {
- // point is close to the cut hyperplane
- close.add(this);
- minus.recurseCloseCuts(point, maxOffset, close);
- plus.recurseCloseCuts(point, maxOffset, close);
- }
-
- }
- }
-
- /** Perform condensation on a tree.
- * <p>The condensation operation is not recursive, it must be called
- * explicitly from leaves to root.</p>
- */
- private void condense() {
- if ((cut != null) && (plus.cut == null) && (minus.cut == null) &&
- (((plus.attribute == null) && (minus.attribute == null)) ||
- ((plus.attribute != null) && plus.attribute.equals(minus.attribute)))) {
- attribute = (plus.attribute == null) ? minus.attribute : plus.attribute;
- cut = null;
- plus = null;
- minus = null;
- }
- }
-
- /** Merge a BSP tree with the instance.
- * <p>All trees are modified (parts of them are reused in the new
- * tree), it is the responsibility of the caller to ensure a copy
- * has been done before if any of the former tree should be
- * preserved, <em>no</em> such copy is done here!</p>
- * <p>The algorithm used here is directly derived from the one
- * described in the Naylor, Amanatides and Thibault paper (section
- * III, Binary Partitioning of a BSP Tree).</p>
- * @param tree other tree to merge with the instance (will be
- * <em>unusable</em> after the operation, as well as the
- * instance itself)
- * @param leafMerger object implementing the final merging phase
- * (this is where the semantic of the operation occurs, generally
- * depending on the attribute of the leaf node)
- * @return a new tree, result of <code>instance <op>
- * tree</code>, this value can be ignored if parentTree is not null
- * since all connections have already been established
- */
- public BSPTree<P> merge(final BSPTree<P> tree, final LeafMerger<P> leafMerger) {
- return merge(tree, leafMerger, null, false);
- }
-
- /** Merge a BSP tree with the instance.
- * @param tree other tree to merge with the instance (will be
- * <em>unusable</em> after the operation, as well as the
- * instance itself)
- * @param leafMerger object implementing the final merging phase
- * (this is where the semantic of the operation occurs, generally
- * depending on the attribute of the leaf node)
- * @param parentTree parent tree to connect to (may be null)
- * @param isPlusChild if true and if parentTree is not null, the
- * resulting tree should be the plus child of its parent, ignored if
- * parentTree is null
- * @return a new tree, result of <code>instance <op>
- * tree</code>, this value can be ignored if parentTree is not null
- * since all connections have already been established
- */
- private BSPTree<P> merge(final BSPTree<P> tree, final LeafMerger<P> leafMerger,
- final BSPTree<P> parentTree, final boolean isPlusChild) {
- if (cut == null) {
- // cell/tree operation
- return leafMerger.merge(this, tree, parentTree, isPlusChild, true);
- } else if (tree.cut == null) {
- // tree/cell operation
- return leafMerger.merge(tree, this, parentTree, isPlusChild, false);
- } else {
- // tree/tree operation
- final BSPTree<P> merged = tree.split(cut);
- if (parentTree != null) {
- merged.parent = parentTree;
- if (isPlusChild) {
- parentTree.plus = merged;
- } else {
- parentTree.minus = merged;
- }
- }
-
- // merging phase
- plus.merge(merged.plus, leafMerger, merged, true);
- minus.merge(merged.minus, leafMerger, merged, false);
- merged.condense();
- if (merged.cut != null) {
- merged.cut = merged.fitToCell(merged.cut.getHyperplane().wholeHyperplane());
- }
-
- return merged;
-
- }
- }
-
- /** This interface gather the merging operations between a BSP tree
- * leaf and another BSP tree.
- * <p>As explained in Bruce Naylor, John Amanatides and William
- * Thibault paper <a
- * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf">Merging
- * BSP Trees Yields Polyhedral Set Operations</a>,
- * the operations on {@link BSPTree BSP trees} can be expressed as a
- * generic recursive merging operation where only the final part,
- * when one of the operand is a leaf, is specific to the real
- * operation semantics. For example, a tree representing a region
- * using a boolean attribute to identify inside cells and outside
- * cells would use four different objects to implement the final
- * merging phase of the four set operations union, intersection,
- * difference and symmetric difference (exclusive or).</p>
- * @param <S> Type of the space.
- */
- public interface LeafMerger<S extends Point<S>> {
-
- /** Merge a leaf node and a tree node.
- * <p>This method is called at the end of a recursive merging
- * resulting from a {@code tree1.merge(tree2, leafMerger)}
- * call, when one of the sub-trees involved is a leaf (i.e. when
- * its cut-hyperplane is null). This is the only place where the
- * precise semantics of the operation are required. For all upper
- * level nodes in the tree, the merging operation is only a
- * generic partitioning algorithm.</p>
- * <p>Since the final operation may be non-commutative, it is
- * important to know if the leaf node comes from the instance tree
- * ({@code tree1}) or the argument tree
- * ({@code tree2}). The third argument of the method is
- * devoted to this. It can be ignored for commutative
- * operations.</p>
- * <p>The {@link BSPTree#insertInTree BSPTree.insertInTree} method
- * may be useful to implement this method.</p>
- * @param leaf leaf node (its cut hyperplane is guaranteed to be
- * null)
- * @param tree tree node (its cut hyperplane may be null or not)
- * @param parentTree parent tree to connect to (may be null)
- * @param isPlusChild if true and if parentTree is not null, the
- * resulting tree should be the plus child of its parent, ignored if
- * parentTree is null
- * @param leafFromInstance if true, the leaf node comes from the
- * instance tree ({@code tree1}) and the tree node comes from
- * the argument tree ({@code tree2})
- * @return the BSP tree resulting from the merging (may be one of
- * the arguments)
- */
- BSPTree<S> merge(BSPTree<S> leaf, BSPTree<S> tree, BSPTree<S> parentTree,
- boolean isPlusChild, boolean leafFromInstance);
-
- }
-
- /** This interface handles the corner cases when an internal node cut sub-hyperplane vanishes.
- * <p>
- * Such cases happens for example when a cut sub-hyperplane is inserted into
- * another tree (during a merge operation), and is split in several parts,
- * some of which becomes smaller than the tolerance. The corresponding node
- * as then no cut sub-hyperplane anymore, but does have children. This interface
- * specifies how to handle this situation.
- * setting
- * </p>
- * @param <S> Type of the space.
- */
- public interface VanishingCutHandler<S extends Point<S>> {
-
- /** Fix a node with both vanished cut and children.
- * @param node node to fix
- * @return fixed node
- */
- BSPTree<S> fixNode(BSPTree<S> node);
-
- }
-
- /** Split a BSP tree by an external sub-hyperplane.
- * <p>Split a tree in two halves, on each side of the
- * sub-hyperplane. The instance is not modified.</p>
- * <p>The tree returned is not upward-consistent: despite all of its
- * sub-trees cut sub-hyperplanes (including its own cut
- * sub-hyperplane) are bounded to the current cell, it is <em>not</em>
- * attached to any parent tree yet. This tree is intended to be
- * later inserted into an higher level tree.</p>
- * <p>The algorithm used here is the one given in Naylor, Amanatides
- * and Thibault paper (section III, Binary Partitioning of a BSP
- * Tree).</p>
- * @param sub partitioning sub-hyperplane, must be already clipped
- * to the convex region represented by the instance, will be used as
- * the cut sub-hyperplane of the returned tree
- * @return a tree having the specified sub-hyperplane as its cut
- * sub-hyperplane, the two parts of the split instance as its two
- * sub-trees and a null parent
- */
- public BSPTree<P> split(final SubHyperplane<P> sub) {
-
- if (cut == null) {
- return new BSPTree<>(sub, copySelf(), new BSPTree<P>(attribute), null);
- }
-
- final Hyperplane<P> cHyperplane = cut.getHyperplane();
- final Hyperplane<P> sHyperplane = sub.getHyperplane();
- final SubHyperplane.SplitSubHyperplane<P> subParts = sub.split(cHyperplane);
- switch (subParts.getSide()) {
- case PLUS :
- { // the partitioning sub-hyperplane is entirely in the plus sub-tree
- final BSPTree<P> split = plus.split(sub);
- if (cut.split(sHyperplane).getSide() == Side.PLUS) {
- split.plus =
- new BSPTree<>(cut.copySelf(), split.plus, minus.copySelf(), attribute);
- split.plus.condense();
- split.plus.parent = split;
- } else {
- split.minus =
- new BSPTree<>(cut.copySelf(), split.minus, minus.copySelf(), attribute);
- split.minus.condense();
- split.minus.parent = split;
- }
- return split;
- }
- case MINUS :
- { // the partitioning sub-hyperplane is entirely in the minus sub-tree
- final BSPTree<P> split = minus.split(sub);
- if (cut.split(sHyperplane).getSide() == Side.PLUS) {
- split.plus =
- new BSPTree<>(cut.copySelf(), plus.copySelf(), split.plus, attribute);
- split.plus.condense();
- split.plus.parent = split;
- } else {
- split.minus =
- new BSPTree<>(cut.copySelf(), plus.copySelf(), split.minus, attribute);
- split.minus.condense();
- split.minus.parent = split;
- }
- return split;
- }
- case BOTH :
- {
- final SubHyperplane.SplitSubHyperplane<P> cutParts = cut.split(sHyperplane);
- final BSPTree<P> split =
- new BSPTree<>(sub, plus.split(subParts.getPlus()), minus.split(subParts.getMinus()),
- null);
- split.plus.cut = cutParts.getPlus();
- split.minus.cut = cutParts.getMinus();
- final BSPTree<P> tmp = split.plus.minus;
- split.plus.minus = split.minus.plus;
- split.plus.minus.parent = split.plus;
- split.minus.plus = tmp;
- split.minus.plus.parent = split.minus;
- split.plus.condense();
- split.minus.condense();
- return split;
- }
- default :
- return cHyperplane.sameOrientationAs(sHyperplane) ?
- new BSPTree<>(sub, plus.copySelf(), minus.copySelf(), attribute) :
- new BSPTree<>(sub, minus.copySelf(), plus.copySelf(), attribute);
- }
-
- }
-
- /** Insert the instance into another tree.
- * <p>The instance itself is modified so its former parent should
- * not be used anymore.</p>
- * @param parentTree parent tree to connect to (may be null)
- * @param isPlusChild if true and if parentTree is not null, the
- * resulting tree should be the plus child of its parent, ignored if
- * parentTree is null
- * @param vanishingHandler handler to use for handling very rare corner
- * cases of vanishing cut sub-hyperplanes in internal nodes during merging
- * @see LeafMerger
- */
- public void insertInTree(final BSPTree<P> parentTree, final boolean isPlusChild,
- final VanishingCutHandler<P> vanishingHandler) {
-
- // set up parent/child links
- parent = parentTree;
- if (parentTree != null) {
- if (isPlusChild) {
- parentTree.plus = this;
- } else {
- parentTree.minus = this;
- }
- }
-
- // make sure the inserted tree lies in the cell defined by its parent nodes
- if (cut != null) {
-
- // explore the parent nodes from here towards tree root
- for (BSPTree<P> tree = this; tree.parent != null; tree = tree.parent) {
-
- // this is an hyperplane of some parent node
- final Hyperplane<P> hyperplane = tree.parent.cut.getHyperplane();
-
- // chop off the parts of the inserted tree that extend
- // on the wrong side of this parent hyperplane
- if (tree == tree.parent.plus) {
- cut = cut.split(hyperplane).getPlus();
- plus.chopOffMinus(hyperplane, vanishingHandler);
- minus.chopOffMinus(hyperplane, vanishingHandler);
- } else {
- cut = cut.split(hyperplane).getMinus();
- plus.chopOffPlus(hyperplane, vanishingHandler);
- minus.chopOffPlus(hyperplane, vanishingHandler);
- }
-
- if (cut == null) {
- // the cut sub-hyperplane has vanished
- final BSPTree<P> fixed = vanishingHandler.fixNode(this);
- cut = fixed.cut;
- plus = fixed.plus;
- minus = fixed.minus;
- attribute = fixed.attribute;
- if (cut == null) {
- break;
- }
- }
-
- }
-
- // since we may have drop some parts of the inserted tree,
- // perform a condensation pass to keep the tree structure simple
- condense();
-
- }
-
- }
-
- /** Prune a tree around a cell.
- * <p>
- * This method can be used to extract a convex cell from a tree.
- * The original cell may either be a leaf node or an internal node.
- * If it is an internal node, it's subtree will be ignored (i.e. the
- * extracted cell will be a leaf node in all cases). The original
- * tree to which the original cell belongs is not touched at all,
- * a new independent tree will be built.
- * </p>
- * @param cellAttribute attribute to set for the leaf node
- * corresponding to the initial instance cell
- * @param otherLeafsAttributes attribute to set for the other leaf
- * nodes
- * @param internalAttributes attribute to set for the internal nodes
- * @return a new tree (the original tree is left untouched) containing
- * a single branch with the cell as a leaf node, and other leaf nodes
- * as the remnants of the pruned branches
- */
- public BSPTree<P> pruneAroundConvexCell(final Object cellAttribute,
- final Object otherLeafsAttributes,
- final Object internalAttributes) {
-
- // build the current cell leaf
- BSPTree<P> tree = new BSPTree<>(cellAttribute);
-
- // build the pruned tree bottom-up
- for (BSPTree<P> current = this; current.parent != null; current = current.parent) {
- final SubHyperplane<P> parentCut = current.parent.cut.copySelf();
- final BSPTree<P> sibling = new BSPTree<>(otherLeafsAttributes);
- if (current == current.parent.plus) {
- tree = new BSPTree<>(parentCut, tree, sibling, internalAttributes);
- } else {
- tree = new BSPTree<>(parentCut, sibling, tree, internalAttributes);
- }
- }
-
- return tree;
-
- }
-
- /** Chop off parts of the tree.
- * <p>The instance is modified in place, all the parts that are on
- * the minus side of the chopping hyperplane are discarded, only the
- * parts on the plus side remain.</p>
- * @param hyperplane chopping hyperplane
- * @param vanishingHandler handler to use for handling very rare corner
- * cases of vanishing cut sub-hyperplanes in internal nodes during merging
- */
- private void chopOffMinus(final Hyperplane<P> hyperplane, final VanishingCutHandler<P> vanishingHandler) {
- if (cut != null) {
-
- cut = cut.split(hyperplane).getPlus();
- plus.chopOffMinus(hyperplane, vanishingHandler);
- minus.chopOffMinus(hyperplane, vanishingHandler);
-
- if (cut == null) {
- // the cut sub-hyperplane has vanished
- final BSPTree<P> fixed = vanishingHandler.fixNode(this);
- cut = fixed.cut;
- plus = fixed.plus;
- minus = fixed.minus;
- attribute = fixed.attribute;
- }
-
- }
- }
-
- /** Chop off parts of the tree.
- * <p>The instance is modified in place, all the parts that are on
- * the plus side of the chopping hyperplane are discarded, only the
- * parts on the minus side remain.</p>
- * @param hyperplane chopping hyperplane
- * @param vanishingHandler handler to use for handling very rare corner
- * cases of vanishing cut sub-hyperplanes in internal nodes during merging
- */
- private void chopOffPlus(final Hyperplane<P> hyperplane, final VanishingCutHandler<P> vanishingHandler) {
- if (cut != null) {
-
- cut = cut.split(hyperplane).getMinus();
- plus.chopOffPlus(hyperplane, vanishingHandler);
- minus.chopOffPlus(hyperplane, vanishingHandler);
-
- if (cut == null) {
- // the cut sub-hyperplane has vanished
- final BSPTree<P> fixed = vanishingHandler.fixNode(this);
- cut = fixed.cut;
- plus = fixed.plus;
- minus = fixed.minus;
- attribute = fixed.attribute;
- }
-
- }
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java
deleted file mode 100644
index 52d0eee..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-/** This interface is used to visit {@link BSPTree BSP tree} nodes.
-
- * <p>Navigation through {@link BSPTree BSP trees} can be done using
- * two different point of views:</p>
- * <ul>
- * <li>
- * the first one is in a node-oriented way using the {@link
- * BSPTree#getPlus}, {@link BSPTree#getMinus} and {@link
- * BSPTree#getParent} methods. Terminal nodes without associated
- * {@link SubHyperplane sub-hyperplanes} can be visited this way,
- * there is no constraint in the visit order, and it is possible
- * to visit either all nodes or only a subset of the nodes
- * </li>
- * <li>
- * the second one is in a sub-hyperplane-oriented way using
- * classes implementing this interface which obeys the visitor
- * design pattern. The visit order is provided by the visitor as
- * each node is first encountered. Each node is visited exactly
- * once.
- * </li>
- * </ul>
-
- * @param <P> Point type defining the space
-
- * @see BSPTree
- * @see SubHyperplane
- */
-public interface BSPTreeVisitor<P extends Point<P>> {
-
- /** Enumerate for visit order with respect to plus sub-tree, minus sub-tree and cut sub-hyperplane. */
- enum Order {
- /** Indicator for visit order plus sub-tree, then minus sub-tree,
- * and last cut sub-hyperplane.
- */
- PLUS_MINUS_SUB,
-
- /** Indicator for visit order plus sub-tree, then cut sub-hyperplane,
- * and last minus sub-tree.
- */
- PLUS_SUB_MINUS,
-
- /** Indicator for visit order minus sub-tree, then plus sub-tree,
- * and last cut sub-hyperplane.
- */
- MINUS_PLUS_SUB,
-
- /** Indicator for visit order minus sub-tree, then cut sub-hyperplane,
- * and last plus sub-tree.
- */
- MINUS_SUB_PLUS,
-
- /** Indicator for visit order cut sub-hyperplane, then plus sub-tree,
- * and last minus sub-tree.
- */
- SUB_PLUS_MINUS,
-
- /** Indicator for visit order cut sub-hyperplane, then minus sub-tree,
- * and last plus sub-tree.
- */
- SUB_MINUS_PLUS;
- }
-
- /** Determine the visit order for this node.
- * <p>Before attempting to visit an internal node, this method is
- * called to determine the desired ordering of the visit. It is
- * guaranteed that this method will be called before {@link
- * #visitInternalNode visitInternalNode} for a given node, it will be
- * called exactly once for each internal node.</p>
- * @param node BSP node guaranteed to have a non null cut sub-hyperplane
- * @return desired visit order, must be one of
- * {@link Order#PLUS_MINUS_SUB}, {@link Order#PLUS_SUB_MINUS},
- * {@link Order#MINUS_PLUS_SUB}, {@link Order#MINUS_SUB_PLUS},
- * {@link Order#SUB_PLUS_MINUS}, {@link Order#SUB_MINUS_PLUS}
- */
- Order visitOrder(BSPTree<P> node);
-
- /** Visit a BSP tree node node having a non-null sub-hyperplane.
- * <p>It is guaranteed that this method will be called after {@link
- * #visitOrder visitOrder} has been called for a given node,
- * it wil be called exactly once for each internal node.</p>
- * @param node BSP node guaranteed to have a non null cut sub-hyperplane
- * @see #visitLeafNode
- */
- void visitInternalNode(BSPTree<P> node);
-
- /** Visit a leaf BSP tree node node having a null sub-hyperplane.
- * @param node leaf BSP node having a null sub-hyperplane
- * @see #visitInternalNode
- */
- void visitLeafNode(BSPTree<P> node);
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java
deleted file mode 100644
index 0476c34..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Class holding boundary attributes.
- *
- * <p>This class is used for the attributes associated with the
- * nodes of region boundary shell trees returned by the {@link
- * Region#getTree(boolean) Region.getTree(includeBoundaryAttributes)}
- * when the boolean {@code includeBoundaryAttributes} parameter is
- * set to {@code true}. It contains the parts of the node cut
- * sub-hyperplane that belong to the boundary.</p>
- *
- * <p>This class is a simple placeholder, it does not provide any
- * processing methods.</p>
- *
- * @param <P> Point type defining the space
- * @see Region#getTree
- */
-public class BoundaryAttribute<P extends Point<P>> {
-
- /** Part of the node cut sub-hyperplane that belongs to the
- * boundary and has the outside of the region on the plus side of
- * its underlying hyperplane (may be null).
- */
- private final SubHyperplane<P> plusOutside;
-
- /** Part of the node cut sub-hyperplane that belongs to the
- * boundary and has the inside of the region on the plus side of
- * its underlying hyperplane (may be null).
- */
- private final SubHyperplane<P> plusInside;
-
- /** Sub-hyperplanes that were used to split the boundary part. */
- private final NodesSet<P> splitters;
-
- /** Simple constructor.
- * @param plusOutside part of the node cut sub-hyperplane that
- * belongs to the boundary and has the outside of the region on
- * the plus side of its underlying hyperplane (may be null)
- * @param plusInside part of the node cut sub-hyperplane that
- * belongs to the boundary and has the inside of the region on the
- * plus side of its underlying hyperplane (may be null)
- * @param splitters sub-hyperplanes that were used to
- * split the boundary part (may be null)
- */
- BoundaryAttribute(final SubHyperplane<P> plusOutside,
- final SubHyperplane<P> plusInside,
- final NodesSet<P> splitters) {
- this.plusOutside = plusOutside;
- this.plusInside = plusInside;
- this.splitters = splitters;
- }
-
- /** Get the part of the node cut sub-hyperplane that belongs to the
- * boundary and has the outside of the region on the plus side of
- * its underlying hyperplane.
- * @return part of the node cut sub-hyperplane that belongs to the
- * boundary and has the outside of the region on the plus side of
- * its underlying hyperplane
- */
- public SubHyperplane<P> getPlusOutside() {
- return plusOutside;
- }
-
- /** Get the part of the node cut sub-hyperplane that belongs to the
- * boundary and has the inside of the region on the plus side of
- * its underlying hyperplane.
- * @return part of the node cut sub-hyperplane that belongs to the
- * boundary and has the inside of the region on the plus side of
- * its underlying hyperplane
- */
- public SubHyperplane<P> getPlusInside() {
- return plusInside;
- }
-
- /** Get the sub-hyperplanes that were used to split the boundary part.
- * @return sub-hyperplanes that were used to split the boundary part
- */
- public NodesSet<P> getSplitters() {
- return splitters;
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.java
deleted file mode 100644
index 63d19b8..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Visitor building boundary shell tree.
- *
- * <p>
- * The boundary shell is represented as {@link BoundaryAttribute boundary attributes}
- * at each internal node.
- * </p>
- *
- * @param <P> Point type defining the space.
- */
-class BoundaryBuilder<P extends Point<P>> implements BSPTreeVisitor<P> {
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(BSPTree<P> node) {
- return Order.PLUS_MINUS_SUB;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(BSPTree<P> node) {
-
- SubHyperplane<P> plusOutside = null;
- SubHyperplane<P> plusInside = null;
- NodesSet<P> splitters = null;
-
- // characterize the cut sub-hyperplane,
- // first with respect to the plus sub-tree
- final Characterization<P> plusChar = new Characterization<>(node.getPlus(), node.getCut().copySelf());
-
- if (plusChar.touchOutside()) {
- // plusChar.outsideTouching() corresponds to a subset of the cut sub-hyperplane
- // known to have outside cells on its plus side, we want to check if parts
- // of this subset do have inside cells on their minus side
- final Characterization<P> minusChar = new Characterization<>(node.getMinus(), plusChar.outsideTouching());
- if (minusChar.touchInside()) {
- // this part belongs to the boundary,
- // it has the outside on its plus side and the inside on its minus side
- plusOutside = minusChar.insideTouching();
- splitters = new NodesSet<>();
- splitters.addAll(minusChar.getInsideSplitters());
- splitters.addAll(plusChar.getOutsideSplitters());
- }
- }
-
- if (plusChar.touchInside()) {
- // plusChar.insideTouching() corresponds to a subset of the cut sub-hyperplane
- // known to have inside cells on its plus side, we want to check if parts
- // of this subset do have outside cells on their minus side
- final Characterization<P> minusChar = new Characterization<>(node.getMinus(), plusChar.insideTouching());
- if (minusChar.touchOutside()) {
- // this part belongs to the boundary,
- // it has the inside on its plus side and the outside on its minus side
- plusInside = minusChar.outsideTouching();
- if (splitters == null) {
- splitters = new NodesSet<>();
- }
- splitters.addAll(minusChar.getOutsideSplitters());
- splitters.addAll(plusChar.getInsideSplitters());
- }
- }
-
- if (splitters != null) {
- // the parent nodes are natural splitters for boundary sub-hyperplanes
- for (BSPTree<P> up = node.getParent(); up != null; up = up.getParent()) {
- splitters.add(up);
- }
- }
-
- // set the boundary attribute at non-leaf nodes
- node.setAttribute(new BoundaryAttribute<>(plusOutside, plusInside, splitters));
-
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(BSPTree<P> node) {
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java
deleted file mode 100644
index 27709c2..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Class holding the result of point projection on region boundary.
- *
- * <p>This class is a simple placeholder, it does not provide any
- * processing methods.</p>
- *
- * <p>Instances of this class are guaranteed to be immutable</p>
- *
- * @param <P> Point type defining the space
- * @see AbstractRegion#projectToBoundary(Point)
- */
-public class BoundaryProjection<P extends Point<P>> {
-
- /** Original point. */
- private final P original;
-
- /** Projected point. */
- private final P projected;
-
- /** Offset of the point with respect to the boundary it is projected on. */
- private final double offset;
-
- /** Constructor from raw elements.
- * @param original original point
- * @param projected projected point
- * @param offset offset of the point with respect to the boundary it is projected on
- */
- public BoundaryProjection(final P original, final P projected, final double offset) {
- this.original = original;
- this.projected = projected;
- this.offset = offset;
- }
-
- /** Get the original point.
- * @return original point
- */
- public P getOriginal() {
- return original;
- }
-
- /** Projected point.
- * @return projected point, or null if there are no boundary
- */
- public P getProjected() {
- return projected;
- }
-
- /** Offset of the point with respect to the boundary it is projected on.
- * <p>
- * The offset with respect to the boundary is negative if the {@link
- * #getOriginal() original point} is inside the region, and positive otherwise.
- * </p>
- * <p>
- * If there are no boundary, the value is set to either {@code
- * Double.POSITIVE_INFINITY} if the region is empty (i.e. all points are
- * outside of the region) or {@code Double.NEGATIVE_INFINITY} if the region
- * covers the whole space (i.e. all points are inside of the region).
- * </p>
- * @return offset of the point with respect to the boundary it is projected on
- */
- public double getOffset() {
- return offset;
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java
deleted file mode 100644
index f694a89..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-
-/** Local tree visitor to compute projection on boundary.
- * @param <P> Point type defining the space
- * @param <S> Point type defining the sub-space
- */
-class BoundaryProjector<P extends Point<P>, S extends Point<S>> implements BSPTreeVisitor<P> {
-
- /** Original point. */
- private final P original;
-
- /** Current best projected point. */
- private P projected;
-
- /** Leaf node closest to the test point. */
- private BSPTree<P> leaf;
-
- /** Current offset. */
- private double offset;
-
- /** Simple constructor.
- * @param original original point
- */
- BoundaryProjector(final P original) {
- this.original = original;
- this.projected = null;
- this.leaf = null;
- this.offset = Double.POSITIVE_INFINITY;
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<P> node) {
- // we want to visit the tree so that the first encountered
- // leaf is the one closest to the test point
- if (node.getCut().getHyperplane().getOffset(original) <= 0) {
- return Order.MINUS_SUB_PLUS;
- } else {
- return Order.PLUS_SUB_MINUS;
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<P> node) {
-
- // project the point on the cut sub-hyperplane
- final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
- final double signedOffset = hyperplane.getOffset(original);
- if (Math.abs(signedOffset) < offset) {
-
- // project point
- final P regular = hyperplane.project(original);
-
- // get boundary parts
- final List<Region<S>> boundaryParts = boundaryRegions(node);
-
- // check if regular projection really belongs to the boundary
- boolean regularFound = false;
- for (final Region<S> part : boundaryParts) {
- if (!regularFound && belongsToPart(regular, hyperplane, part)) {
- // the projected point lies in the boundary
- projected = regular;
- offset = Math.abs(signedOffset);
- regularFound = true;
- }
- }
-
- if (!regularFound) {
- // the regular projected point is not on boundary,
- // so we have to check further if a singular point
- // (i.e. a vertex in 2D case) is a possible projection
- for (final Region<S> part : boundaryParts) {
- final P spI = singularProjection(regular, hyperplane, part);
- if (spI != null) {
- final double distance = original.distance(spI);
- if (distance < offset) {
- projected = spI;
- offset = distance;
- }
- }
- }
-
- }
-
- }
-
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<P> node) {
- if (leaf == null) {
- // this is the first leaf we visit,
- // it is the closest one to the original point
- leaf = node;
- }
- }
-
- /** Get the projection.
- * @return projection
- */
- public BoundaryProjection<P> getProjection() {
-
- // fix offset sign
- offset = Math.copySign(offset, (Boolean) leaf.getAttribute() ? -1 : +1);
-
- return new BoundaryProjection<>(original, projected, offset);
-
- }
-
- /** Extract the regions of the boundary on an internal node.
- * @param node internal node
- * @return regions in the node sub-hyperplane
- */
- private List<Region<S>> boundaryRegions(final BSPTree<P> node) {
-
- final List<Region<S>> regions = new ArrayList<>(2);
-
- @SuppressWarnings("unchecked")
- final BoundaryAttribute<P> ba = (BoundaryAttribute<P>) node.getAttribute();
- addRegion(ba.getPlusInside(), regions);
- addRegion(ba.getPlusOutside(), regions);
-
- return regions;
-
- }
-
- /** Add a boundary region to a list.
- * @param sub sub-hyperplane defining the region
- * @param list to fill up
- */
- private void addRegion(final SubHyperplane<P> sub, final List<Region<S>> list) {
- if (sub != null) {
- @SuppressWarnings("unchecked")
- final Region<S> region = ((AbstractSubHyperplane<P, S>) sub).getRemainingRegion();
- if (region != null) {
- list.add(region);
- }
- }
- }
-
- /** Check if a projected point lies on a boundary part.
- * @param point projected point to check
- * @param hyperplane hyperplane into which the point was projected
- * @param part boundary part
- * @return true if point lies on the boundary part
- */
- private boolean belongsToPart(final P point, final Hyperplane<P> hyperplane,
- final Region<S> part) {
-
- // there is a non-null sub-space, we can dive into smaller dimensions
- @SuppressWarnings("unchecked")
- final Embedding<P, S> embedding = (Embedding<P, S>) hyperplane;
- return part.checkPoint(embedding.toSubSpace(point)) != Location.OUTSIDE;
-
- }
-
- /** Get the projection to the closest boundary singular point.
- * @param point projected point to check
- * @param hyperplane hyperplane into which the point was projected
- * @param part boundary part
- * @return projection to a singular point of boundary part (may be null)
- */
- private P singularProjection(final P point, final Hyperplane<P> hyperplane,
- final Region<S> part) {
-
- // there is a non-null sub-space, we can dive into smaller dimensions
- @SuppressWarnings("unchecked")
- final Embedding<P, S> embedding = (Embedding<P, S>) hyperplane;
- final BoundaryProjection<S> bp = part.projectToBoundary(embedding.toSubSpace(point));
-
- // back to initial dimension
- return (bp.getProjected() == null) ? null : embedding.toSpace(bp.getProjected());
-
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java
deleted file mode 100644
index e84c70a..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Visitor computing the boundary size.
- * @param <P> Point type defining the space
- */
-class BoundarySizeVisitor<P extends Point<P>> implements BSPTreeVisitor<P> {
-
- /** Size of the boundary. */
- private double boundarySize;
-
- /** Simple constructor.
- */
- BoundarySizeVisitor() {
- boundarySize = 0;
- }
-
- /** {@inheritDoc}*/
- @Override
- public Order visitOrder(final BSPTree<P> node) {
- return Order.MINUS_SUB_PLUS;
- }
-
- /** {@inheritDoc}*/
- @Override
- public void visitInternalNode(final BSPTree<P> node) {
- @SuppressWarnings("unchecked")
- final BoundaryAttribute<P> attribute =
- (BoundaryAttribute<P>) node.getAttribute();
- if (attribute.getPlusOutside() != null) {
- boundarySize += attribute.getPlusOutside().getSize();
- }
- if (attribute.getPlusInside() != null) {
- boundarySize += attribute.getPlusInside().getSize();
- }
- }
-
- /** {@inheritDoc}*/
- @Override
- public void visitLeafNode(final BSPTree<P> node) {
- }
-
- /** Get the size of the boundary.
- * @return size of the boundary
- */
- public double getSize() {
- return boundarySize;
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java
deleted file mode 100644
index d9ec6e7..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Cut sub-hyperplanes characterization with respect to inside/outside cells.
- * @see BoundaryBuilder
- * @param <P> Point type defining the space
- */
-class Characterization<P extends Point<P>> {
-
- /** Part of the cut sub-hyperplane that touch outside cells. */
- private SubHyperplane<P> outsideTouching;
-
- /** Part of the cut sub-hyperplane that touch inside cells. */
- private SubHyperplane<P> insideTouching;
-
- /** Nodes that were used to split the outside touching part. */
- private final NodesSet<P> outsideSplitters;
-
- /** Nodes that were used to split the outside touching part. */
- private final NodesSet<P> insideSplitters;
-
- /** Simple constructor.
- * <p>Characterization consists in splitting the specified
- * sub-hyperplane into several parts lying in inside and outside
- * cells of the tree. The principle is to compute characterization
- * twice for each cut sub-hyperplane in the tree, once on the plus
- * node and once on the minus node. The parts that have the same flag
- * (inside/inside or outside/outside) do not belong to the boundary
- * while parts that have different flags (inside/outside or
- * outside/inside) do belong to the boundary.</p>
- * @param node current BSP tree node
- * @param sub sub-hyperplane to characterize
- */
- Characterization(final BSPTree<P> node, final SubHyperplane<P> sub) {
- outsideTouching = null;
- insideTouching = null;
- outsideSplitters = new NodesSet<>();
- insideSplitters = new NodesSet<>();
- characterize(node, sub, new ArrayList<BSPTree<P>>());
- }
-
- /** Filter the parts of an hyperplane belonging to the boundary.
- * <p>The filtering consist in splitting the specified
- * sub-hyperplane into several parts lying in inside and outside
- * cells of the tree. The principle is to call this method twice for
- * each cut sub-hyperplane in the tree, once on the plus node and
- * once on the minus node. The parts that have the same flag
- * (inside/inside or outside/outside) do not belong to the boundary
- * while parts that have different flags (inside/outside or
- * outside/inside) do belong to the boundary.</p>
- * @param node current BSP tree node
- * @param sub sub-hyperplane to characterize
- * @param splitters nodes that did split the current one
- */
- private void characterize(final BSPTree<P> node, final SubHyperplane<P> sub,
- final List<BSPTree<P>> splitters) {
- if (node.getCut() == null) {
- // we have reached a leaf node
- final boolean inside = (Boolean) node.getAttribute();
- if (inside) {
- addInsideTouching(sub, splitters);
- } else {
- addOutsideTouching(sub, splitters);
- }
- } else {
- final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
- final SubHyperplane.SplitSubHyperplane<P> split = sub.split(hyperplane);
- switch (split.getSide()) {
- case PLUS:
- characterize(node.getPlus(), sub, splitters);
- break;
- case MINUS:
- characterize(node.getMinus(), sub, splitters);
- break;
- case BOTH:
- splitters.add(node);
- characterize(node.getPlus(), split.getPlus(), splitters);
- characterize(node.getMinus(), split.getMinus(), splitters);
- splitters.remove(splitters.size() - 1);
- break;
- default:
- // If we reach this point, then the sub-hyperplane we're
- // testing lies directly on this node's hyperplane. In theory,
- // this shouldn't ever happen with correctly-formed trees. However,
- // this does actually occur in practice, especially with manually
- // built trees or very complex models. Rather than throwing an
- // exception, we'll attempt to handle this situation gracefully
- // by treating these sub-hyperplanes as if they lie on the minus
- // side of the cut hyperplane.
- characterize(node.getMinus(), sub, splitters);
- break;
- }
- }
- }
-
- /** Add a part of the cut sub-hyperplane known to touch an outside cell.
- * @param sub part of the cut sub-hyperplane known to touch an outside cell
- * @param splitters sub-hyperplanes that did split the current one
- */
- private void addOutsideTouching(final SubHyperplane<P> sub,
- final List<BSPTree<P>> splitters) {
- if (outsideTouching == null) {
- outsideTouching = sub;
- } else {
- outsideTouching = outsideTouching.reunite(sub);
- }
- outsideSplitters.addAll(splitters);
- }
-
- /** Add a part of the cut sub-hyperplane known to touch an inside cell.
- * @param sub part of the cut sub-hyperplane known to touch an inside cell
- * @param splitters sub-hyperplanes that did split the current one
- */
- private void addInsideTouching(final SubHyperplane<P> sub,
- final List<BSPTree<P>> splitters) {
- if (insideTouching == null) {
- insideTouching = sub;
- } else {
- insideTouching = insideTouching.reunite(sub);
- }
- insideSplitters.addAll(splitters);
- }
-
- /** Check if the cut sub-hyperplane touches outside cells.
- * @return true if the cut sub-hyperplane touches outside cells
- */
- public boolean touchOutside() {
- return outsideTouching != null && !outsideTouching.isEmpty();
- }
-
- /** Get all the parts of the cut sub-hyperplane known to touch outside cells.
- * @return parts of the cut sub-hyperplane known to touch outside cells
- * (may be null or empty)
- */
- public SubHyperplane<P> outsideTouching() {
- return outsideTouching;
- }
-
- /** Get the nodes that were used to split the outside touching part.
- * <p>
- * Splitting nodes are internal nodes (i.e. they have a non-null
- * cut sub-hyperplane).
- * </p>
- * @return nodes that were used to split the outside touching part
- */
- public NodesSet<P> getOutsideSplitters() {
- return outsideSplitters;
- }
-
- /** Check if the cut sub-hyperplane touches inside cells.
- * @return true if the cut sub-hyperplane touches inside cells
- */
- public boolean touchInside() {
- return insideTouching != null && !insideTouching.isEmpty();
- }
-
- /** Get all the parts of the cut sub-hyperplane known to touch inside cells.
- * @return parts of the cut sub-hyperplane known to touch inside cells
- * (may be null or empty)
- */
- public SubHyperplane<P> insideTouching() {
- return insideTouching;
- }
-
- /** Get the nodes that were used to split the inside touching part.
- * <p>
- * Splitting nodes are internal nodes (i.e. they have a non-null
- * cut sub-hyperplane).
- * </p>
- * @return nodes that were used to split the inside touching part
- */
- public NodesSet<P> getInsideSplitters() {
- return insideSplitters;
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/ConvexSubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/ConvexSubHyperplane.java
new file mode 100644
index 0000000..606e233
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/ConvexSubHyperplane.java
@@ -0,0 +1,50 @@
+/*
+ * 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.core.partitioning;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Transform;
+
+/** Extension of the {@link SubHyperplane} interface with the additional restriction
+ * that instances represent convex regions of space.
+ * @param <P> Point implementation type
+ */
+public interface ConvexSubHyperplane<P extends Point<P>> extends SubHyperplane<P> {
+
+ /** Reverse the orientation of the hyperplane for this instance. The subhyperplane
+ * occupies the same locations in space but with a reversed orientation.
+ * @return a convex subhyperplane representing the same region but with the
+ * opposite orientation.
+ */
+ ConvexSubHyperplane<P> reverse();
+
+ /** {@inheritDoc}
+ *
+ * <p>The parts resulting from a split operation with a convex subhyperplane
+ * are guaranteed to also be convex.</p>
+ */
+ @Override
+ Split<? extends ConvexSubHyperplane<P>> split(Hyperplane<P> splitter);
+
+ /** {@inheritDoc}
+ *
+ * <p>Convex subhyperplanes subjected to affine transformations remain
+ * convex.</p>
+ */
+ @Override
+ ConvexSubHyperplane<P> transform(Transform<P> transform);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java
deleted file mode 100644
index dfc40bf..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-/** This interface defines mappers between a space and one of its sub-spaces.
-
- * <p>Sub-spaces are the lower dimensions subsets of a n-dimensions
- * space. The (n-1)-dimension sub-spaces are specific sub-spaces known
- * as {@link Hyperplane hyperplanes}. This interface can be used regardless
- * of the dimensions differences. For example, a line in 3D Euclidean space
- * can map directly from 3 dimensions to 1.</p>
-
- * <p>In the 3D Euclidean space, hyperplanes are 2D planes, and the 1D
- * sub-spaces are lines.</p>
-
- * <p>
- * Note that this interface is <em>not</em> intended to be implemented
- * by Apache Commons Geometry users, it is only intended to be implemented
- * within the library itself. New methods may be added even for minor
- * versions, which breaks compatibility for external implementations.
- * </p>
-
- * @param <P> Point type defining the embedding space.
- * @param <S> Point type defining the embedded sub-space.
-
- * @see Hyperplane
- */
-public interface Embedding<P extends Point<P>, S extends Point<S>> {
-
- /** Transform a space point into a sub-space point.
- * @param point n-dimension point of the space
- * @return (n-1)-dimension point of the sub-space corresponding to
- * the specified space point
- * @see #toSpace
- */
- S toSubSpace(P point);
-
- /** Transform a sub-space point into a space point.
- * @param point (n-1)-dimension point of the sub-space
- * @return n-dimension point of the space corresponding to the
- * specified sub-space point
- * @see #toSubSpace
- */
- P toSpace(S point);
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/EmbeddingHyperplane.java
similarity index 67%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/EmbeddingHyperplane.java
index 046defe..71b1297 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/EmbeddingHyperplane.java
@@ -16,21 +16,15 @@
*/
package org.apache.commons.geometry.core.partitioning;
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+import org.apache.commons.geometry.core.Embedding;
+import org.apache.commons.geometry.core.Point;
+
+/** Hyperplane that also embeds a subspace.
+ * @param <P> Point implementation type
+ * @param <S> Subspace point implementation type
+ * @see Hyperplane
+ * @see Embedding
*/
-public enum Side {
-
- /** Code for the plus side of the hyperplane. */
- PLUS,
-
- /** Code for the minus side of the hyperplane. */
- MINUS,
-
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
-
- /** Code for the hyperplane itself. */
- HYPER;
-
+public interface EmbeddingHyperplane<P extends Point<P>, S extends Point<S>>
+ extends Hyperplane<P>, Embedding<P, S> {
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
index 28d9d12..d7bd09f 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
@@ -17,80 +17,69 @@
package org.apache.commons.geometry.core.partitioning;
import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.Transform;
-/** This interface represents an hyperplane of a space.
-
- * <p>The most prominent place where hyperplane appears in space
- * partitioning is as cutters. Each partitioning node in a {@link
- * BSPTree BSP tree} has a cut {@link SubHyperplane sub-hyperplane}
- * which is either an hyperplane or a part of an hyperplane. In an
- * n-dimensions Euclidean space, an hyperplane is an (n-1)-dimensions
- * hyperplane (for example a traditional plane in the 3D Euclidean
- * space). They can be more exotic objects in specific fields, for
- * example a circle on the surface of the unit sphere.</p>
-
- * <p>
- * Note that this interface is <em>not</em> intended to be implemented
- * by Apache Commons Geometry users, it is only intended to be implemented
- * within the library itself. New methods may be added even for minor
- * versions, which breaks compatibility for external implementations.
- * </p>
-
- * @param <P> Point type defining the space
+/** Interface representing a hyperplane, which is a subspace of degree
+ * one less than the space it is embedded in.
+ * @param <P> Point implementation type
*/
public interface Hyperplane<P extends Point<P>> {
- /** Copy the instance.
- * <p>The instance created is completely independant of the original
- * one. A deep copy is used, none of the underlying objects are
- * shared (except for immutable objects).</p>
- * @return a new hyperplane, copy of the instance
+ /** Get the offset (oriented distance) of a point with respect
+ * to this instance. Points with an offset of zero lie on the
+ * hyperplane itself.
+ * @param point the point to compute the offset for
+ * @return the offset of the point
*/
- Hyperplane<P> copySelf();
+ double offset(P point);
- /** Get the offset (oriented distance) of a point.
- * <p>The offset is 0 if the point is on the underlying hyperplane,
- * it is positive if the point is on one particular side of the
- * hyperplane, and it is negative if the point is on the other side,
- * according to the hyperplane natural orientation.</p>
- * @param point point to check
- * @return offset of the point
+ /** Classify a point with respect to this hyperplane.
+ * @param point the point to classify
+ * @return the relative location of the point with
+ * respect to this instance
*/
- double getOffset(P point);
+ HyperplaneLocation classify(P point);
- /** Project a point to the hyperplane.
- * @param point point to project
- * @return projected point
+ /** Return true if the given point lies on the hyperplane.
+ * @param point the point to test
+ * @return true if the point lies on the hyperplane
+ */
+ boolean contains(P point);
+
+ /** Project a point onto this instance.
+ * @param point the point to project
+ * @return the projection of the point onto this instance. The returned
+ * point lies on the hyperplane.
*/
P project(P point);
- /** Get the object used to determine floating point equality for this hyperplane.
- * This determines which points belong to the hyperplane and which do not, or pictured
- * another way, the "thickness" of the hyperplane.
- * @return the floating point precision context for the instance
+ /** Return a hyperplane that has the opposite orientation as this instance.
+ * That is, the plus side of this instance is the minus side of the returned
+ * instance and vice versa.
+ * @return a hyperplane with the opposite orientation
*/
- DoublePrecisionContext getPrecision();
+ Hyperplane<P> reverse();
- /** Check if the instance has the same orientation as another hyperplane.
- * <p>This method is expected to be called on parallel hyperplanes. The
- * method should <em>not</em> re-check for parallelism, only for
- * orientation, typically by testing something like the sign of the
- * dot-products of normals.</p>
- * @param other other hyperplane to check against the instance
- * @return true if the instance and the other hyperplane have
- * the same orientation
+ /** Transform this instance using the given {@link Transform}.
+ * @param transform object to transform this instance with
+ * @return a new, transformed hyperplane
*/
- boolean sameOrientationAs(Hyperplane<P> other);
+ Hyperplane<P> transform(Transform<P> transform);
- /** Build a sub-hyperplane covering the whole hyperplane.
- * @return a sub-hyperplane covering the whole hyperplane
+ /** Return true if this instance has a similar orientation to the given hyperplane,
+ * meaning that they point in generally the same direction. This method is not
+ * used to determine exact equality of hyperplanes, but rather to determine whether
+ * two hyperplanes that contain the same points are parallel (point in the same direction)
+ * or anti-parallel (point in opposite directions).
+ * @param other the hyperplane to compare with
+ * @return true if the hyperplanes point in generally the same direction and could
+ * possibly be parallel
*/
- SubHyperplane<P> wholeHyperplane();
+ boolean similarOrientation(Hyperplane<P> other);
- /** Build a region covering the whole space.
- * @return a region containing the instance
+ /** Return a {@link ConvexSubHyperplane} spanning this entire hyperplane. The returned
+ * subhyperplane contains all points lying in this hyperplane and no more.
+ * @return a {@link ConvexSubHyperplane} containing all points lying in this hyperplane
*/
- Region<P> wholeSpace();
-
+ ConvexSubHyperplane<P> span();
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneBoundedRegion.java
similarity index 60%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneBoundedRegion.java
index 046defe..0718d09 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneBoundedRegion.java
@@ -16,21 +16,15 @@
*/
package org.apache.commons.geometry.core.partitioning;
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Region;
+
+/** Interface representing regions with boundaries defined by hyperplanes or
+ * portions of hyperplanes. This interface is intended to represent closed regions
+ * with finite sizes as well as infinite and empty spaces. Regions of this type
+ * can be recursively split by hyperplanes into similar regions.
+ * @param <P> Point implementation type
*/
-public enum Side {
-
- /** Code for the plus side of the hyperplane. */
- PLUS,
-
- /** Code for the minus side of the hyperplane. */
- MINUS,
-
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
-
- /** Code for the hyperplane itself. */
- HYPER;
-
+public interface HyperplaneBoundedRegion<P extends Point<P>>
+ extends Region<P>, Splittable<P, HyperplaneBoundedRegion<P>> {
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneLocation.java
similarity index 68%
rename from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
rename to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneLocation.java
index 046defe..ce45f0f 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneLocation.java
@@ -16,21 +16,23 @@
*/
package org.apache.commons.geometry.core.partitioning;
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+/** Enumeration containing possible locations of a point with respect to
+ * a hyperplane.
+ * @see Hyperplane
*/
-public enum Side {
+public enum HyperplaneLocation {
- /** Code for the plus side of the hyperplane. */
- PLUS,
-
- /** Code for the minus side of the hyperplane. */
+ /** Value indicating that a point lies on the minus side of
+ * a hyperplane.
+ */
MINUS,
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
+ /** Value indicating that a point lies on the plus side of
+ * a hyperplane.
+ */
+ PLUS,
- /** Code for the hyperplane itself. */
- HYPER;
-
+ /** Value indicating that a point lies directly on a hyperplane.
+ */
+ ON
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java
deleted file mode 100644
index cb384d9..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Utility class checking if inside nodes can be found
- * on the plus and minus sides of an hyperplane.
- * @param <P> Point type defining the space
- */
-class InsideFinder<P extends Point<P>> {
-
- /** Region on which to operate. */
- private final Region<P> region;
-
- /** Indicator of inside leaf nodes found on the plus side. */
- private boolean plusFound;
-
- /** Indicator of inside leaf nodes found on the plus side. */
- private boolean minusFound;
-
- /** Simple constructor.
- * @param region region on which to operate
- */
- InsideFinder(final Region<P> region) {
- this.region = region;
- plusFound = false;
- minusFound = false;
- }
-
- /** Search recursively for inside leaf nodes on each side of the given hyperplane.
-
- * <p>The algorithm used here is directly derived from the one
- * described in section III (<i>Binary Partitioning of a BSP
- * Tree</i>) of the Bruce Naylor, John Amanatides and William
- * Thibault paper <a
- * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf">Merging
- * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph
- * '90, Computer Graphics 24(4), August 1990, pp 115-124, published
- * by the Association for Computing Machinery (ACM)..</p>
-
- * @param node current BSP tree node
- * @param sub sub-hyperplane
- */
- public void recurseSides(final BSPTree<P> node, final SubHyperplane<P> sub) {
-
- if (node.getCut() == null) {
- if ((Boolean) node.getAttribute()) {
- // this is an inside cell expanding across the hyperplane
- plusFound = true;
- minusFound = true;
- }
- return;
- }
-
- final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
- final SubHyperplane.SplitSubHyperplane<P> split = sub.split(hyperplane);
- switch (split.getSide()) {
- case PLUS :
- // the sub-hyperplane is entirely in the plus sub-tree
- if (node.getCut().split(sub.getHyperplane()).getSide() == Side.PLUS) {
- if (!region.isEmpty(node.getMinus())) {
- plusFound = true;
- }
- } else {
- if (!region.isEmpty(node.getMinus())) {
- minusFound = true;
- }
- }
- if (!(plusFound && minusFound)) {
- recurseSides(node.getPlus(), sub);
- }
- break;
- case MINUS :
- // the sub-hyperplane is entirely in the minus sub-tree
- if (node.getCut().split(sub.getHyperplane()).getSide() == Side.PLUS) {
- if (!region.isEmpty(node.getPlus())) {
- plusFound = true;
- }
- } else {
- if (!region.isEmpty(node.getPlus())) {
- minusFound = true;
- }
- }
- if (!(plusFound && minusFound)) {
- recurseSides(node.getMinus(), sub);
- }
- break;
- case BOTH :
- // the sub-hyperplane extends in both sub-trees
-
- // explore first the plus sub-tree
- recurseSides(node.getPlus(), split.getPlus());
-
- // if needed, explore the minus sub-tree
- if (!(plusFound && minusFound)) {
- recurseSides(node.getMinus(), split.getMinus());
- }
- break;
- default :
- // the sub-hyperplane and the cut sub-hyperplane share the same hyperplane
- if (node.getCut().getHyperplane().sameOrientationAs(sub.getHyperplane())) {
- if ((node.getPlus().getCut() != null) || ((Boolean) node.getPlus().getAttribute())) {
- plusFound = true;
- }
- if ((node.getMinus().getCut() != null) || ((Boolean) node.getMinus().getAttribute())) {
- minusFound = true;
- }
- } else {
- if ((node.getPlus().getCut() != null) || ((Boolean) node.getPlus().getAttribute())) {
- minusFound = true;
- }
- if ((node.getMinus().getCut() != null) || ((Boolean) node.getMinus().getAttribute())) {
- plusFound = true;
- }
- }
- }
-
- }
-
- /** Check if inside leaf nodes have been found on the plus side.
- * @return true if inside leaf nodes have been found on the plus side
- */
- public boolean plusFound() {
- return plusFound;
- }
-
- /** Check if inside leaf nodes have been found on the minus side.
- * @return true if inside leaf nodes have been found on the minus side
- */
- public boolean minusFound() {
- return minusFound;
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java
deleted file mode 100644
index 54e0c3d..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Set of {@link BSPTree BSP tree} nodes.
- * @see BoundaryAttribute
- * @param <P> Point type defining the space
- */
-public class NodesSet<P extends Point<P>> implements Iterable<BSPTree<P>> {
-
- /** List of sub-hyperplanes. */
- private final List<BSPTree<P>> list;
-
- /** Simple constructor.
- */
- public NodesSet() {
- list = new ArrayList<>();
- }
-
- /** Add a node if not already known.
- * @param node node to add
- */
- public void add(final BSPTree<P> node) {
-
- for (final BSPTree<P> existing : list) {
- if (node == existing) {
- // the node is already known, don't add it
- return;
- }
- }
-
- // the node was not known, add it
- list.add(node);
-
- }
-
- /** Add nodes if they are not already known.
- * @param iterator nodes iterator
- */
- public void addAll(final Iterable<BSPTree<P>> iterator) {
- for (final BSPTree<P> node : iterator) {
- add(node);
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public Iterator<BSPTree<P>> iterator() {
- return list.iterator();
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java
deleted file mode 100644
index 4fef7e3..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-/** This interface represents a region of a space as a partition.
-
- * <p>Region are subsets of a space, they can be infinite (whole
- * space, half space, infinite stripe ...) or finite (polygons in 2D,
- * polyhedrons in 3D ...). Their main characteristic is to separate
- * points that are considered to be <em>inside</em> the region from
- * points considered to be <em>outside</em> of it. In between, there
- * may be points on the <em>boundary</em> of the region.</p>
-
- * <p>This implementation is limited to regions for which the boundary
- * is composed of several {@link SubHyperplane sub-hyperplanes},
- * including regions with no boundary at all: the whole space and the
- * empty region. They are not necessarily finite and not necessarily
- * path-connected. They can contain holes.</p>
-
- * <p>Regions can be combined using the traditional sets operations :
- * union, intersection, difference and symetric difference (exclusive
- * or) for the binary operations, complement for the unary
- * operation.</p>
-
- * <p>
- * Note that this interface is <em>not</em> intended to be implemented
- * by Apache Commons Math users, it is only intended to be implemented
- * within the library itself. New methods may be added even for minor
- * versions, which breaks compatibility for external implementations.
- * </p>
-
- * @param <P> Point type defining the space
- */
-public interface Region<P extends Point<P>> {
-
- /** Enumerate for the location of a point with respect to the region. */
- enum Location {
- /** Code for points inside the partition. */
- INSIDE,
-
- /** Code for points outside of the partition. */
- OUTSIDE,
-
- /** Code for points on the partition boundary. */
- BOUNDARY;
- }
-
- /** Build a region using the instance as a prototype.
- * <p>This method allow to create new instances without knowing
- * exactly the type of the region. It is an application of the
- * prototype design pattern.</p>
- * <p>The leaf nodes of the BSP tree <em>must</em> have a
- * {@code Boolean} attribute representing the inside status of
- * the corresponding cell (true for inside cells, false for outside
- * cells). In order to avoid building too many small objects, it is
- * recommended to use the predefined constants
- * {@code Boolean.TRUE} and {@code Boolean.FALSE}. The
- * tree also <em>must</em> have either null internal nodes or
- * internal nodes representing the boundary as specified in the
- * {@link #getTree getTree} method).</p>
- * @param newTree inside/outside BSP tree representing the new region
- * @return the built region
- */
- Region<P> buildNew(BSPTree<P> newTree);
-
- /** Copy the instance.
- * <p>The instance created is completely independant of the original
- * one. A deep copy is used, none of the underlying objects are
- * shared (except for the underlying tree {@code Boolean}
- * attributes and immutable objects).</p>
- * @return a new region, copy of the instance
- */
- Region<P> copySelf();
-
- /** Check if the instance is empty.
- * @return true if the instance is empty
- */
- boolean isEmpty();
-
- /** Check if the sub-tree starting at a given node is empty.
- * @param node root node of the sub-tree (<em>must</em> have {@link
- * Region Region} tree semantics, i.e. the leaf nodes must have
- * {@code Boolean} attributes representing an inside/outside
- * property)
- * @return true if the sub-tree starting at the given node is empty
- */
- boolean isEmpty(final BSPTree<P> node);
-
- /** Check if the instance covers the full space.
- * @return true if the instance covers the full space
- */
- boolean isFull();
-
- /** Check if the sub-tree starting at a given node covers the full space.
- * @param node root node of the sub-tree (<em>must</em> have {@link
- * Region Region} tree semantics, i.e. the leaf nodes must have
- * {@code Boolean} attributes representing an inside/outside
- * property)
- * @return true if the sub-tree starting at the given node covers the full space
- */
- boolean isFull(final BSPTree<P> node);
-
- /** Check if the instance entirely contains another region.
- * @param region region to check against the instance
- * @return true if the instance contains the specified tree
- */
- boolean contains(final Region<P> region);
-
- /** Check a point with respect to the region.
- * @param point point to check
- * @return a code representing the point status: either {@link
- * Location#INSIDE}, {@link Location#OUTSIDE} or {@link Location#BOUNDARY}
- */
- Location checkPoint(final P point);
-
- /** Project a point on the boundary of the region.
- * @param point point to check
- * @return projection of the point on the boundary
- */
- BoundaryProjection<P> projectToBoundary(final P point);
-
- /** Get the underlying BSP tree.
-
- * <p>Regions are represented by an underlying inside/outside BSP
- * tree whose leaf attributes are {@code Boolean} instances
- * representing inside leaf cells if the attribute value is
- * {@code true} and outside leaf cells if the attribute is
- * {@code false}. These leaf attributes are always present and
- * guaranteed to be non null.</p>
-
- * <p>In addition to the leaf attributes, the internal nodes which
- * correspond to cells split by cut sub-hyperplanes may contain
- * {@link BoundaryAttribute BoundaryAttribute} objects representing
- * the parts of the corresponding cut sub-hyperplane that belong to
- * the boundary. When the boundary attributes have been computed,
- * all internal nodes are guaranteed to have non-null
- * attributes, however some {@link BoundaryAttribute
- * BoundaryAttribute} instances may have their {@link
- * BoundaryAttribute#getPlusInside() getPlusInside} and {@link
- * BoundaryAttribute#getPlusOutside() getPlusOutside} methods both
- * returning null if the corresponding cut sub-hyperplane does not
- * have any parts belonging to the boundary.</p>
-
- * <p>Since computing the boundary is not always required and can be
- * time-consuming for large trees, these internal nodes attributes
- * are computed using lazy evaluation only when required by setting
- * the {@code includeBoundaryAttributes} argument to
- * {@code true}. Once computed, these attributes remain in the
- * tree, which implies that in this case, further calls to the
- * method for the same region will always include these attributes
- * regardless of the value of the
- * {@code includeBoundaryAttributes} argument.</p>
-
- * @param includeBoundaryAttributes if true, the boundary attributes
- * at internal nodes are guaranteed to be included (they may be
- * included even if the argument is false, if they have already been
- * computed due to a previous call)
- * @return underlying BSP tree
- * @see BoundaryAttribute
- */
- BSPTree<P> getTree(final boolean includeBoundaryAttributes);
-
- /** Get the size of the boundary.
- * @return the size of the boundary (this is 0 in 1D, a length in
- * 2D, an area in 3D ...)
- */
- double getBoundarySize();
-
- /** Get the size of the instance.
- * @return the size of the instance (this is a length in 1D, an area
- * in 2D, a volume in 3D ...)
- */
- double getSize();
-
- /** Get the barycenter of the instance.
- * @return an object representing the barycenter
- */
- P getBarycenter();
-
- /** Get the parts of a sub-hyperplane that are contained in the region.
- * <p>The parts of the sub-hyperplane that belong to the boundary are
- * <em>not</em> included in the resulting parts.</p>
- * @param sub sub-hyperplane traversing the region
- * @return filtered sub-hyperplane
- */
- SubHyperplane<P> intersection(final SubHyperplane<P> sub);
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java
deleted file mode 100644
index c15676c..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java
+++ /dev/null
@@ -1,383 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.partitioning.BSPTree.VanishingCutHandler;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane.SplitSubHyperplane;
-
-/** This class is a factory for {@link Region}.
-
- * @param <P> Point type defining the space
- */
-public class RegionFactory<P extends Point<P>> {
-
- /** Visitor removing internal nodes attributes. */
- private final NodesCleaner nodeCleaner;
-
- /** Simple constructor.
- */
- public RegionFactory() {
- nodeCleaner = new NodesCleaner();
- }
-
- /** Build a convex region from a collection of bounding hyperplanes.
- * @param hyperplanes collection of bounding hyperplanes
- * @return a new convex region, or null if the collection is empty
- */
- @SafeVarargs
- public final Region<P> buildConvex(final Hyperplane<P> ... hyperplanes) {
- if ((hyperplanes == null) || (hyperplanes.length == 0)) {
- return null;
- }
-
- // use the first hyperplane to build the right class
- final Region<P> region = hyperplanes[0].wholeSpace();
-
- // chop off parts of the space
- BSPTree<P> node = region.getTree(false);
- node.setAttribute(Boolean.TRUE);
- for (final Hyperplane<P> hyperplane : hyperplanes) {
- if (node.insertCut(hyperplane)) {
- node.setAttribute(null);
- node.getPlus().setAttribute(Boolean.FALSE);
- node = node.getMinus();
- node.setAttribute(Boolean.TRUE);
- } else {
- // the hyperplane could not be inserted in the current leaf node
- // either it is completely outside (which means the input hyperplanes
- // are wrong), or it is parallel to a previous hyperplane
- SubHyperplane<P> s = hyperplane.wholeHyperplane();
- for (BSPTree<P> tree = node; tree.getParent() != null && s != null; tree = tree.getParent()) {
- final Hyperplane<P> other = tree.getParent().getCut().getHyperplane();
- final SplitSubHyperplane<P> split = s.split(other);
- switch (split.getSide()) {
- case HYPER :
- // the hyperplane is parallel to a previous hyperplane
- if (!hyperplane.sameOrientationAs(other)) {
- // this hyperplane is opposite to the other one,
- // the region is thinner than the tolerance, we consider it empty
- return getComplement(hyperplanes[0].wholeSpace());
- }
- // the hyperplane is an extension of an already known hyperplane, we just ignore it
- break;
- case PLUS :
- // the hyperplane is outside of the current convex zone,
- // the input hyperplanes are inconsistent
- throw new IllegalArgumentException("Hyperplanes do not define a convex region");
- default :
- s = split.getMinus();
- }
- }
- }
- }
-
- return region;
-
- }
-
- /** Compute the union of two regions.
- * @param region1 first region (will be unusable after the operation as
- * parts of it will be reused in the new region)
- * @param region2 second region (will be unusable after the operation as
- * parts of it will be reused in the new region)
- * @return a new region, result of {@code region1 union region2}
- */
- public Region<P> union(final Region<P> region1, final Region<P> region2) {
- final BSPTree<P> tree =
- region1.getTree(false).merge(region2.getTree(false), new UnionMerger());
- tree.visit(nodeCleaner);
- return region1.buildNew(tree);
- }
-
- /** Compute the intersection of two regions.
- * @param region1 first region (will be unusable after the operation as
- * parts of it will be reused in the new region)
- * @param region2 second region (will be unusable after the operation as
- * parts of it will be reused in the new region)
- * @return a new region, result of {@code region1 intersection region2}
- */
- public Region<P> intersection(final Region<P> region1, final Region<P> region2) {
- final BSPTree<P> tree =
- region1.getTree(false).merge(region2.getTree(false), new IntersectionMerger());
- tree.visit(nodeCleaner);
- return region1.buildNew(tree);
- }
-
- /** Compute the symmetric difference (exclusive or) of two regions.
- * @param region1 first region (will be unusable after the operation as
- * parts of it will be reused in the new region)
- * @param region2 second region (will be unusable after the operation as
- * parts of it will be reused in the new region)
- * @return a new region, result of {@code region1 xor region2}
- */
- public Region<P> xor(final Region<P> region1, final Region<P> region2) {
- final BSPTree<P> tree =
- region1.getTree(false).merge(region2.getTree(false), new XorMerger());
- tree.visit(nodeCleaner);
- return region1.buildNew(tree);
- }
-
- /** Compute the difference of two regions.
- * @param region1 first region (will be unusable after the operation as
- * parts of it will be reused in the new region)
- * @param region2 second region (will be unusable after the operation as
- * parts of it will be reused in the new region)
- * @return a new region, result of {@code region1 minus region2}
- */
- public Region<P> difference(final Region<P> region1, final Region<P> region2) {
- final BSPTree<P> tree =
- region1.getTree(false).merge(region2.getTree(false), new DifferenceMerger(region1, region2));
- tree.visit(nodeCleaner);
- return region1.buildNew(tree);
- }
-
- /** Get the complement of the region (exchanged interior/exterior).
- * @param region region to complement, it will not modified, a new
- * region independent region will be built
- * @return a new region, complement of the specified one
- */
- /** Get the complement of the region (exchanged interior/exterior).
- * @param region region to complement, it will not modified, a new
- * region independent region will be built
- * @return a new region, complement of the specified one
- */
- public Region<P> getComplement(final Region<P> region) {
- return region.buildNew(recurseComplement(region.getTree(false)));
- }
-
- /** Recursively build the complement of a BSP tree.
- * @param node current node of the original tree
- * @return new tree, complement of the node
- */
- private BSPTree<P> recurseComplement(final BSPTree<P> node) {
-
- // transform the tree, except for boundary attribute splitters
- final Map<BSPTree<P>, BSPTree<P>> map = new HashMap<>();
- final BSPTree<P> transformedTree = recurseComplement(node, map);
-
- // set up the boundary attributes splitters
- for (final Map.Entry<BSPTree<P>, BSPTree<P>> entry : map.entrySet()) {
- if (entry.getKey().getCut() != null) {
- @SuppressWarnings("unchecked")
- BoundaryAttribute<P> original = (BoundaryAttribute<P>) entry.getKey().getAttribute();
- if (original != null) {
- @SuppressWarnings("unchecked")
- BoundaryAttribute<P> transformed = (BoundaryAttribute<P>) entry.getValue().getAttribute();
- for (final BSPTree<P> splitter : original.getSplitters()) {
- transformed.getSplitters().add(map.get(splitter));
- }
- }
- }
- }
-
- return transformedTree;
-
- }
-
- /** Recursively build the complement of a BSP tree.
- * @param node current node of the original tree
- * @param map transformed nodes map
- * @return new tree, complement of the node
- */
- private BSPTree<P> recurseComplement(final BSPTree<P> node,
- final Map<BSPTree<P>, BSPTree<P>> map) {
-
- final BSPTree<P> transformedNode;
- if (node.getCut() == null) {
- transformedNode = new BSPTree<>(((Boolean) node.getAttribute()) ? Boolean.FALSE : Boolean.TRUE);
- } else {
-
- @SuppressWarnings("unchecked")
- BoundaryAttribute<P> attribute = (BoundaryAttribute<P>) node.getAttribute();
- if (attribute != null) {
- final SubHyperplane<P> plusOutside =
- (attribute.getPlusInside() == null) ? null : attribute.getPlusInside().copySelf();
- final SubHyperplane<P> plusInside =
- (attribute.getPlusOutside() == null) ? null : attribute.getPlusOutside().copySelf();
- // we start with an empty list of splitters, it will be filled in out of recursion
- attribute = new BoundaryAttribute<>(plusOutside, plusInside, new NodesSet<P>());
- }
-
- transformedNode = new BSPTree<>(node.getCut().copySelf(),
- recurseComplement(node.getPlus(), map),
- recurseComplement(node.getMinus(), map),
- attribute);
- }
-
- map.put(node, transformedNode);
- return transformedNode;
-
- }
-
- /** BSP tree leaf merger computing union of two regions. */
- private class UnionMerger implements BSPTree.LeafMerger<P> {
- /** {@inheritDoc} */
- @Override
- public BSPTree<P> merge(final BSPTree<P> leaf, final BSPTree<P> tree,
- final BSPTree<P> parentTree,
- final boolean isPlusChild, final boolean leafFromInstance) {
- if ((Boolean) leaf.getAttribute()) {
- // the leaf node represents an inside cell
- leaf.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
- return leaf;
- }
- // the leaf node represents an outside cell
- tree.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(false));
- return tree;
- }
- }
-
- /** BSP tree leaf merger computing intersection of two regions. */
- private class IntersectionMerger implements BSPTree.LeafMerger<P> {
- /** {@inheritDoc} */
- @Override
- public BSPTree<P> merge(final BSPTree<P> leaf, final BSPTree<P> tree,
- final BSPTree<P> parentTree,
- final boolean isPlusChild, final boolean leafFromInstance) {
- if ((Boolean) leaf.getAttribute()) {
- // the leaf node represents an inside cell
- tree.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
- return tree;
- }
- // the leaf node represents an outside cell
- leaf.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(false));
- return leaf;
- }
- }
-
- /** BSP tree leaf merger computing symmetric difference (exclusive or) of two regions. */
- private class XorMerger implements BSPTree.LeafMerger<P> {
- /** {@inheritDoc} */
- @Override
- public BSPTree<P> merge(final BSPTree<P> leaf, final BSPTree<P> tree,
- final BSPTree<P> parentTree, final boolean isPlusChild,
- final boolean leafFromInstance) {
- BSPTree<P> t = tree;
- if ((Boolean) leaf.getAttribute()) {
- // the leaf node represents an inside cell
- t = recurseComplement(t);
- }
- t.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
- return t;
- }
- }
-
- /** BSP tree leaf merger computing difference of two regions. */
- private class DifferenceMerger implements BSPTree.LeafMerger<P>, VanishingCutHandler<P> {
-
- /** Region to subtract from. */
- private final Region<P> region1;
-
- /** Region to subtract. */
- private final Region<P> region2;
-
- /** Simple constructor.
- * @param region1 region to subtract from
- * @param region2 region to subtract
- */
- DifferenceMerger(final Region<P> region1, final Region<P> region2) {
- this.region1 = region1.copySelf();
- this.region2 = region2.copySelf();
- }
-
- /** {@inheritDoc} */
- @Override
- public BSPTree<P> merge(final BSPTree<P> leaf, final BSPTree<P> tree,
- final BSPTree<P> parentTree, final boolean isPlusChild,
- final boolean leafFromInstance) {
- if ((Boolean) leaf.getAttribute()) {
- // the leaf node represents an inside cell
- final BSPTree<P> argTree =
- recurseComplement(leafFromInstance ? tree : leaf);
- argTree.insertInTree(parentTree, isPlusChild, this);
- return argTree;
- }
- // the leaf node represents an outside cell
- final BSPTree<P> instanceTree =
- leafFromInstance ? leaf : tree;
- instanceTree.insertInTree(parentTree, isPlusChild, this);
- return instanceTree;
- }
-
- /** {@inheritDoc} */
- @Override
- public BSPTree<P> fixNode(final BSPTree<P> node) {
- // get a representative point in the degenerate cell
- final BSPTree<P> cell = node.pruneAroundConvexCell(Boolean.TRUE, Boolean.FALSE, null);
- final Region<P> r = region1.buildNew(cell);
- final P p = r.getBarycenter();
- return new BSPTree<>(region1.checkPoint(p) == Location.INSIDE &&
- region2.checkPoint(p) == Location.OUTSIDE);
- }
-
- }
-
- /** Visitor removing internal nodes attributes. */
- private class NodesCleaner implements BSPTreeVisitor<P> {
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<P> node) {
- return Order.PLUS_SUB_MINUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<P> node) {
- node.setAttribute(null);
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<P> node) {
- }
-
- }
-
- /** Handler replacing nodes with vanishing cuts with leaf nodes. */
- private class VanishingToLeaf implements VanishingCutHandler<P> {
-
- /** Inside/outside indocator to use for ambiguous nodes. */
- private final boolean inside;
-
- /** Simple constructor.
- * @param inside inside/outside indicator to use for ambiguous nodes
- */
- VanishingToLeaf(final boolean inside) {
- this.inside = inside;
- }
-
- /** {@inheritDoc} */
- @Override
- public BSPTree<P> fixNode(final BSPTree<P> node) {
- if (node.getPlus().getAttribute().equals(node.getMinus().getAttribute())) {
- // no ambiguity
- return new BSPTree<>(node.getPlus().getAttribute());
- } else {
- // ambiguous node
- return new BSPTree<>(inside);
- }
- }
-
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Split.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Split.java
new file mode 100644
index 0000000..20307e3
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Split.java
@@ -0,0 +1,97 @@
+/*
+ * 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.core.partitioning;
+
+/** Class containing the result of splitting an object with a hyperplane.
+ * @param <T> Split type
+ */
+public final class Split<T> {
+
+ /** Part of the object lying on the minus side of the splitting hyperplane.
+ */
+ private final T minus;
+
+ /** Part of the object lying on the plus side of the splitting hyperplane.
+ */
+ private final T plus;
+
+ /** Build a new instance from its parts.
+ * @param minus part of the object lying on the minus side of the
+ * splitting hyperplane or null if no such part exists
+ * @param plus part of the object lying on the plus side of the
+ * splitting hyperplane or null if no such part exists.
+ */
+ public Split(final T minus, final T plus) {
+ this.minus = minus;
+ this.plus = plus;
+ }
+
+ /** Get the part of the object lying on the minus side of the splitting
+ * hyperplane or null if no such part exists.
+ * @return part of the object lying on the minus side of the splitting
+ * hyperplane
+ */
+ public T getMinus() {
+ return minus;
+ }
+
+ /** Get the part of the object lying on the plus side of the splitting
+ * hyperplane or null if no such part exists.
+ * @return part of the object lying on the plus side of the splitting
+ * hyperplane
+ */
+ public T getPlus() {
+ return plus;
+ }
+
+ /** Get the location of the object with respect to its splitting
+ * hyperplane.
+ * @return
+ * <ul>
+ * <li>{@link SplitLocation#PLUS} - if only {@link #getPlus()} is not null</li>
+ * <li>{@link SplitLocation#MINUS} - if only {@link #getMinus()} is not null</li>
+ * <li>{@link SplitLocation#BOTH} - if both {@link #getPlus()} and {@link #getMinus()}
+ * are not null</li>
+ * <li>{@link SplitLocation#NEITHER} - if both {@link #getPlus()} and {@link #getMinus()}
+ * are null</li>
+ * </ul>
+ */
+ public SplitLocation getLocation() {
+ if (minus != null) {
+ return plus != null ? SplitLocation.BOTH : SplitLocation.MINUS;
+ } else if (plus != null) {
+ return SplitLocation.PLUS;
+ }
+ return SplitLocation.NEITHER;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[location= ")
+ .append(getLocation())
+ .append(", minus= ")
+ .append(minus)
+ .append(", plus= ")
+ .append(plus)
+ .append(']');
+
+ return sb.toString();
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SplitLocation.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SplitLocation.java
new file mode 100644
index 0000000..cbf2445
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SplitLocation.java
@@ -0,0 +1,45 @@
+/*
+ * 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.core.partitioning;
+
+/** Enumeration representing the location of a split object with respect
+ * to its splitting {@link Hyperplane hyperplane}.
+ */
+public enum SplitLocation {
+
+ /** Value indicating that the split object lies entirely on the
+ * plus side of the splitting hyperplane.
+ */
+ PLUS,
+
+ /** Value indicating that the split object lies entirely on the
+ * minus side of the splitting hyperplane.
+ */
+ MINUS,
+
+ /** Value indicating that the split object lies in both the plus
+ * and minus sides of the splitting hyperplane.
+ */
+ BOTH,
+
+ /** Value indicating that the split object lies neither on the plus
+ * or minus sides of the splitting hyperplane. This is the case when
+ * the object lies entirely on the hyperplane or is empty (and
+ * therefore "lies" nowhere).
+ */
+ NEITHER;
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Splittable.java
similarity index 64%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Splittable.java
index 046defe..60cef06 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Splittable.java
@@ -16,21 +16,17 @@
*/
package org.apache.commons.geometry.core.partitioning;
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+import org.apache.commons.geometry.core.Point;
+
+/** Interface representing objects that can be split by hyperplanes.
+ * @param <P> Point implementation type
+ * @param <S> Split type
*/
-public enum Side {
+public interface Splittable<P extends Point<P>, S extends Splittable<P, S>> {
- /** Code for the plus side of the hyperplane. */
- PLUS,
-
- /** Code for the minus side of the hyperplane. */
- MINUS,
-
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
-
- /** Code for the hyperplane itself. */
- HYPER;
-
+ /** Split this instance with the given hyperplane.
+ * @param splitter the hyperplane to split this object with.
+ * @return result of the split operation
+ */
+ Split<? extends S> split(Hyperplane<P> splitter);
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java
index 7237bb7..887f2a8 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java
@@ -16,127 +16,129 @@
*/
package org.apache.commons.geometry.core.partitioning;
+import java.util.List;
+
import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
-/** This interface represents the remaining parts of an hyperplane after
- * other parts have been chopped off.
+/** Interface representing subhyperplanes, which are regions
+ * embedded in a hyperplane.
- * <p>sub-hyperplanes are obtained when parts of an {@link
- * Hyperplane hyperplane} are chopped off by other hyperplanes that
- * intersect it. The remaining part is a convex region. Such objects
- * appear in {@link BSPTree BSP trees} as the intersection of a cut
- * hyperplane with the convex region which it splits, the chopping
- * hyperplanes are the cut hyperplanes closer to the tree root.</p>
-
- * <p>
- * Note that this interface is <em>not</em> intended to be implemented
- * by Apache Commons Math users, it is only intended to be implemented
- * within the library itself. New methods may be added even for minor
- * versions, which breaks compatibility for external implementations.
- * </p>
-
- * @param <P> Point type defining the embedding space.
+ * @param <P> Point implementation type
*/
-public interface SubHyperplane<P extends Point<P>> {
+public interface SubHyperplane<P extends Point<P>> extends Splittable<P, SubHyperplane<P>> {
- /** Copy the instance.
- * <p>The instance created is completely independent of the original
- * one. A deep copy is used, none of the underlying objects are
- * shared (except for the nodes attributes and immutable
- * objects).</p>
- * @return a new sub-hyperplane, copy of the instance
- */
- SubHyperplane<P> copySelf();
-
- /** Get the underlying hyperplane.
- * @return underlying hyperplane
+ /** Get the hyperplane that this instance is embedded in.
+ * @return the hyperplane that this instance is embedded in.
*/
Hyperplane<P> getHyperplane();
- /** Check if the instance is empty.
- * @return true if the instance is empty
+ /** Return true if this instance contains all points in the
+ * hyperplane.
+ * @return true if this instance contains all points in the
+ * hyperplane
+ */
+ boolean isFull();
+
+ /** Return true if this instance does not contain any points.
+ * @return true if this instance does not contain any points
*/
boolean isEmpty();
- /** Get the size of the instance.
- * @return the size of the instance (this is a length in 1D, an area
- * in 2D, a volume in 3D ...)
+ /** Return true if this instance has infinite size.
+ * @return true if this instance has infinite size
+ */
+ boolean isInfinite();
+
+ /** Return true if this instance has finite size.
+ * @return true if this instance has finite size
+ */
+ boolean isFinite();
+
+ /** Return the size of this instance. This will have different
+ * meanings in different spaces and dimensions. For example, in
+ * Euclidean space, this will be length in 2D and area in 3D.
+ * @return the size of this instance
*/
double getSize();
- /** Split the instance in two parts by an hyperplane.
- * @param hyperplane splitting hyperplane
- * @return an object containing both the part of the instance
- * on the plus side of the hyperplane and the part of the
- * instance on the minus side of the hyperplane
+ /** Classify a point with respect to the subhyperplane's region. The point is
+ * classified as follows:
+ * <ul>
+ * <li>{@link RegionLocation#INSIDE INSIDE} - The point lies on the hyperplane
+ * and inside of the subhyperplane's region.</li>
+ * <li>{@link RegionLocation#BOUNDARY BOUNDARY} - The point lies on the hyperplane
+ * and is on the boundary of the subhyperplane's region.</li>
+ * <li>{@link RegionLocation#OUTSIDE OUTSIDE} - The point does not lie on
+ * the hyperplane or it does lie on the hyperplane but is outside of the
+ * subhyperplane's region.</li>
+ * </ul>
+ * @param point the point to classify
+ * @return classification of the point with respect to the subhyperplane's hyperplane
+ * and region
*/
- SplitSubHyperplane<P> split(Hyperplane<P> hyperplane);
+ RegionLocation classify(P point);
- /** Compute the union of the instance and another sub-hyperplane.
- * @param other other sub-hyperplane to union (<em>must</em> be in the
- * same hyperplane as the instance)
- * @return a new sub-hyperplane, union of the instance and other
+ /** Return true if the subhyperplane contains the given point, meaning that the point
+ * lies on the hyperplane and is not on the outside of the subhyperplane's region.
+ * @param point the point to check
+ * @return true if the point is contained in the subhyperplane
*/
- SubHyperplane<P> reunite(SubHyperplane<P> other);
-
- /** Class holding the results of the {@link #split split} method.
- * @param <U> Type of the embedding space.
- */
- class SplitSubHyperplane<U extends Point<U>> {
-
- /** Part of the sub-hyperplane on the plus side of the splitting hyperplane. */
- private final SubHyperplane<U> plus;
-
- /** Part of the sub-hyperplane on the minus side of the splitting hyperplane. */
- private final SubHyperplane<U> minus;
-
- /** Build a SplitSubHyperplane from its parts.
- * @param plus part of the sub-hyperplane on the plus side of the
- * splitting hyperplane
- * @param minus part of the sub-hyperplane on the minus side of the
- * splitting hyperplane
- */
- public SplitSubHyperplane(final SubHyperplane<U> plus,
- final SubHyperplane<U> minus) {
- this.plus = plus;
- this.minus = minus;
- }
-
- /** Get the part of the sub-hyperplane on the plus side of the splitting hyperplane.
- * @return part of the sub-hyperplane on the plus side of the splitting hyperplane
- */
- public SubHyperplane<U> getPlus() {
- return plus;
- }
-
- /** Get the part of the sub-hyperplane on the minus side of the splitting hyperplane.
- * @return part of the sub-hyperplane on the minus side of the splitting hyperplane
- */
- public SubHyperplane<U> getMinus() {
- return minus;
- }
-
- /** Get the side of the split sub-hyperplane with respect to its splitter.
- * @return {@link Side#PLUS} if only {@link #getPlus()} is neither null nor empty,
- * {@link Side#MINUS} if only {@link #getMinus()} is neither null nor empty,
- * {@link Side#BOTH} if both {@link #getPlus()} and {@link #getMinus()}
- * are neither null nor empty or {@link Side#HYPER} if both {@link #getPlus()} and
- * {@link #getMinus()} are either null or empty
- */
- public Side getSide() {
- if (plus != null && !plus.isEmpty()) {
- if (minus != null && !minus.isEmpty()) {
- return Side.BOTH;
- } else {
- return Side.PLUS;
- }
- } else if (minus != null && !minus.isEmpty()) {
- return Side.MINUS;
- } else {
- return Side.HYPER;
- }
- }
-
+ default boolean contains(P point) {
+ final RegionLocation loc = classify(point);
+ return loc != null && loc != RegionLocation.OUTSIDE;
}
+ /** Return the closest point to the argument that is contained in the subhyperplane
+ * (ie, not classified as {@link RegionLocation#OUTSIDE outside}), or null if no
+ * such point exists.
+ * @param point the reference point
+ * @return the closest point to the reference point that is contained in the subhyperplane,
+ * or null if no such point exists
+ */
+ P closest(P point);
+
+ /** Return a {@link Builder} instance for joining multiple
+ * subhyperplanes together.
+ * @return a new builder instance
+ */
+ Builder<P> builder();
+
+ /** Return a new subhyperplane instance resulting from the application
+ * of the given transform. The current instance is not modified.
+ * @param transform the transform instance to apply
+ * @return new transformed subhyperplane instance
+ */
+ SubHyperplane<P> transform(Transform<P> transform);
+
+ /** Convert this instance into a list of convex child subhyperplanes.
+ * @return a list of convex subhyperplanes representing the same subspace
+ * region as this instance
+ */
+ List<? extends ConvexSubHyperplane<P>> toConvex();
+
+ /** Interface for joining multiple {@link SubHyperplane}s into a single
+ * instance.
+ * @param <P> Point implementation type
+ */
+ interface Builder<P extends Point<P>> {
+
+ /** Add a {@link SubHyperplane} instance to the builder.
+ * @param sub subhyperplane to add to this instance
+ */
+ void add(SubHyperplane<P> sub);
+
+ /** Add a {@link ConvexSubHyperplane} instance to the builder.
+ * @param sub convex subhyperplane to add to this instance
+ */
+ void add(ConvexSubHyperplane<P> sub);
+
+ /** Get a {@link SubHyperplane} representing the union
+ * of all input subhyperplanes.
+ * @return subhyperplane representing the union of all input
+ * subhyperplanes
+ */
+ SubHyperplane<P> build();
+ }
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java
deleted file mode 100644
index 53cb056..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-
-
-/** This interface represents an inversible affine transform in a space.
- * <p>Inversible affine transform include for example scalings,
- * translations, rotations.</p>
-
- * <p>Transforms are dimension-specific. The consistency rules between
- * the three {@code apply} methods are the following ones for a
- * transformed defined for dimension D:</p>
- * <ul>
- * <li>
- * the transform can be applied to a point in the
- * D-dimension space using its {@link #apply(Point)}
- * method
- * </li>
- * <li>
- * the transform can be applied to a (D-1)-dimension
- * hyperplane in the D-dimension space using its
- * {@link #apply(Hyperplane)} method
- * </li>
- * <li>
- * the transform can be applied to a (D-2)-dimension
- * sub-hyperplane in a (D-1)-dimension hyperplane using
- * its {@link #apply(SubHyperplane, Hyperplane, Hyperplane)}
- * method
- * </li>
- * </ul>
-
- * @param <P> Point type defining the embedding space.
- * @param <S> Point type defining the embedded sub-space.
- */
-public interface Transform<P extends Point<P>, S extends Point<S>> {
-
- /** Transform a point of a space.
- * @param point point to transform
- * @return a new object representing the transformed point
- */
- P apply(P point);
-
- /** Transform an hyperplane of a space.
- * @param hyperplane hyperplane to transform
- * @return a new object representing the transformed hyperplane
- */
- Hyperplane<P> apply(Hyperplane<P> hyperplane);
-
- /** Transform a sub-hyperplane embedded in an hyperplane.
- * @param sub sub-hyperplane to transform
- * @param original hyperplane in which the sub-hyperplane is
- * defined (this is the original hyperplane, the transform has
- * <em>not</em> been applied to it)
- * @param transformed hyperplane in which the sub-hyperplane is
- * defined (this is the transformed hyperplane, the transform
- * <em>has</em> been applied to it)
- * @return a new object representing the transformed sub-hyperplane
- */
- SubHyperplane<S> apply(SubHyperplane<S> sub, Hyperplane<P> original, Hyperplane<P> transformed);
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTree.java
new file mode 100644
index 0000000..157eb16
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTree.java
@@ -0,0 +1,1108 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import java.io.Serializable;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.Order;
+
+/** Abstract class for Binary Space Partitioning (BSP) tree implementations.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+public abstract class AbstractBSPTree<P extends Point<P>, N extends AbstractBSPTree.AbstractNode<P, N>>
+ implements BSPTree<P, N>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190330L;
+
+ /** The default number of levels to print when creating a string representation of the tree. */
+ private static final int DEFAULT_TREE_STRING_MAX_DEPTH = 8;
+
+ /** Integer value set on various node fields when a value is unknown. */
+ private static final int UNKNOWN_VALUE = -1;
+
+ /** The root node for the tree. */
+ private N root;
+
+ /** The current modification version for the tree structure. This is incremented each time
+ * a structural change occurs in the tree and is used to determine when cached values
+ * must be recomputed.
+ */
+ private int version = 0;
+
+ /** {@inheritDoc} */
+ @Override
+ public N getRoot() {
+ if (root == null) {
+ setRoot(createNode());
+ }
+ return root;
+ }
+
+ /** Set the root node for the tree. Cached tree properties are invalidated
+ * with {@link #invalidate()}.
+ * @param root new root node for the tree
+ */
+ protected void setRoot(final N root) {
+ this.root = root;
+
+ this.root.makeRoot();
+
+ invalidate();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int count() {
+ return getRoot().count();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int height() {
+ return getRoot().height();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void accept(final BSPTreeVisitor<P, N> visitor) {
+ acceptVisitor(getRoot(), visitor);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public N findNode(final P pt, final NodeCutRule cutBehavior) {
+ return findNode(getRoot(), pt, cutBehavior);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void insert(final SubHyperplane<P> sub) {
+ insert(sub.toConvex());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void insert(final ConvexSubHyperplane<P> convexSub) {
+ insertRecursive(getRoot(), convexSub,
+ convexSub.getHyperplane().span());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void insert(final Iterable<? extends ConvexSubHyperplane<P>> convexSubs) {
+ for (final ConvexSubHyperplane<P> convexSub : convexSubs) {
+ insert(convexSub);
+ }
+ }
+
+ /** Return an iterator over the nodes in the tree. */
+ @Override
+ public Iterator<N> iterator() {
+ return new NodeIterator<>(getRoot());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void copy(final BSPTree<P, N> src) {
+ copySubtree(src.getRoot(), getRoot());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void extract(final N node) {
+ // copy downward
+ final N extracted = importSubtree(node);
+
+ // extract upward
+ final N newRoot = extractParentPath(node, extracted);
+
+ // set the root of this tree
+ setRoot(newRoot);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void transform(final Transform<P> transform) {
+ final boolean swapChildren = swapsInsideOutside(transform);
+ transformRecursive(getRoot(), transform, swapChildren);
+
+ invalidate();
+ }
+
+ /** Get a simple string representation of the tree structure. The returned string contains
+ * the tree structure down to the default max depth of {@value #DEFAULT_TREE_STRING_MAX_DEPTH}.
+ * @return a string representation of the tree
+ */
+ public String treeString() {
+ return treeString(DEFAULT_TREE_STRING_MAX_DEPTH);
+ }
+
+ /** Get a simple string representation of the tree structure. The returned string contains
+ * the tree structure down to {@code maxDepth}.
+ * @param maxDepth the maximum depth in the tree to print; nodes below this depth are skipped
+ * @return a string representation of the tree
+ */
+ public String treeString(final int maxDepth) {
+ BSPTreePrinter<P, N> printer = new BSPTreePrinter<>(maxDepth);
+ accept(printer);
+
+ return printer.toString();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append(getClass().getSimpleName())
+ .append("[count= ")
+ .append(count())
+ .append(", height= ")
+ .append(height())
+ .append("]")
+ .toString();
+ }
+
+ /** Create a new node for this tree.
+ * @return a new node for this tree
+ */
+ protected abstract N createNode();
+
+ /** Copy non-structural node properties from {@code src} to {@code dst}.
+ * Non-structural properties are those properties not directly related
+ * to the structure of the BSP tree, i.e. properties other than parent/child
+ * connections and cut subhyperplanes. Subclasses should override this method
+ * when additional properties are stored on nodes.
+ * @param src source node
+ * @param dst destination node
+ */
+ protected void copyNodeProperties(final N src, final N dst) {
+ // no-op
+ }
+
+ /** Method called to initialize a new child node. Subclasses can use this method to
+ * set initial attributes on the node.
+ * @param parent the parent node
+ * @param child the new child node
+ * @param isPlus true if the child will be assigned as the parent's plus child;
+ * false if it will be the parent's minus child
+ */
+ protected void initChildNode(final N parent, final N child, final boolean isPlus) {
+ // no-op
+ }
+
+ /** Create a non-structural copy of the given node. Properties such as parent/child
+ * connections and cut subhyperplanes are <em>not</em> copied.
+ * @param src the node to copy; does not need to belong to the current tree
+ * @return the copied node
+ * @see AbstractBSPTree#copyNodeProperties(AbstractNode, AbstractNode)
+ */
+ protected N copyNode(final N src) {
+ final N copy = createNode();
+ copyNodeProperties(src, copy);
+
+ return copy;
+ }
+
+ /** Recursively copy a subtree. The returned node is not attached to the current tree.
+ * Structural <em>and</em> non-structural properties are copied from the source subtree
+ * to the destination subtree. This method does nothing if {@code src} and {@code dst}
+ * reference the same node.
+ * @param src the node representing the source subtree; does not need to belong to the
+ * current tree
+ * @param dst the node representing the destination subtree
+ * @return the copied node, ie {@code dst}
+ */
+ protected N copySubtree(final N src, final N dst) {
+ // only copy if we're actually switching nodes
+ if (src != dst) {
+ // copy non-structural properties
+ copyNodeProperties(src, dst);
+
+ // copy the subtree structure
+ ConvexSubHyperplane<P> cut = null;
+ N minus = null;
+ N plus = null;
+
+ if (!src.isLeaf()) {
+ final AbstractBSPTree<P, N> dstTree = dst.getTree();
+
+ cut = src.getCut();
+ minus = copySubtree(src.getMinus(), dstTree.createNode());
+ plus = copySubtree(src.getPlus(), dstTree.createNode());
+ }
+
+ dst.setSubtree(cut, minus, plus);
+ }
+
+ return dst;
+ }
+
+ /** Import the subtree represented by the given node into this tree. If the given node
+ * already belongs to this tree, then the node is returned directly without modification.
+ * If the node does <em>not</em> belong to this tree, a new node is created and the src node
+ * subtree is copied into it.
+ *
+ * <p>This method does not modify the current structure of the tree.</p>
+ * @param src node to import
+ * @return the given node if it belongs to this tree, otherwise a new node containing
+ * a copy of the given node's subtree
+ * @see #copySubtree(AbstractNode, AbstractNode)
+ */
+ protected N importSubtree(final N src) {
+ // create a copy of the node if it's not already in this tree
+ if (src.getTree() != this) {
+ return copySubtree(src, createNode());
+ }
+
+ return src;
+ }
+
+ /** Extract the path from {@code src} to the root of its tree and
+ * set it as the parent path of {@code dst}. Leaf nodes created during
+ * the extraction are given the same node properties as their counterparts
+ * in the source tree but without the cuts and child nodes. The properties
+ * of {@code dst} are not modified, with the exception of its parent node
+ * reference.
+ * @param src the source node to copy the parent path from
+ * @param dst the destination node to place under the extracted path
+ * @return the root node of the extracted path
+ */
+ protected N extractParentPath(final N src, final N dst) {
+ N dstParent = dst;
+ N dstChild;
+
+ N srcChild = src;
+ N srcParent = srcChild.getParent();
+
+ while (srcParent != null) {
+ dstChild = dstParent;
+ dstParent = copyNode(srcParent);
+
+ if (srcChild.isMinus()) {
+ dstParent.setSubtree(
+ srcParent.getCut(),
+ dstChild,
+ copyNode(srcParent.getPlus()));
+ } else {
+ dstParent.setSubtree(
+ srcParent.getCut(),
+ copyNode(srcParent.getMinus()),
+ dstChild);
+ }
+
+ srcChild = srcParent;
+ srcParent = srcChild.getParent();
+ }
+
+ return dstParent;
+ }
+
+ /** Find the smallest node in the tree containing the point, starting
+ * at the given node.
+ * @param start the node to begin the search with
+ * @param pt the point to check
+ * @param cutBehavior value determining the search behavior when the test point
+ * lies directly on the cut subhyperplane of an internal node
+ * @return the smallest node in the tree containing the point
+ */
+ protected N findNode(final N start, final P pt, final NodeCutRule cutBehavior) {
+ final Hyperplane<P> cutHyper = start.getCutHyperplane();
+ if (cutHyper != null) {
+ final HyperplaneLocation cutLoc = cutHyper.classify(pt);
+
+ final boolean onPlusSide = cutLoc == HyperplaneLocation.PLUS;
+ final boolean onMinusSide = cutLoc == HyperplaneLocation.MINUS;
+ final boolean onCut = !onPlusSide && !onMinusSide;
+
+ if (onMinusSide || (onCut && cutBehavior == NodeCutRule.MINUS)) {
+ return findNode(start.getMinus(), pt, cutBehavior);
+ } else if (onPlusSide || (onCut && cutBehavior == NodeCutRule.PLUS)) {
+ return findNode(start.getPlus(), pt, cutBehavior);
+ }
+ }
+ return start;
+ }
+
+ /** Visit the nodes in a subtree.
+ * @param node the node to begin the visit process
+ * @param visitor the visitor to pass nodes to
+ */
+ protected void acceptVisitor(final N node, BSPTreeVisitor<P, N> visitor) {
+ if (node.isLeaf()) {
+ visitor.visit(node);
+ } else {
+ final Order order = visitor.visitOrder(node);
+
+ if (order != null) {
+
+ switch (order) {
+ case PLUS_MINUS_NODE:
+ acceptVisitor(node.getPlus(), visitor);
+ acceptVisitor(node.getMinus(), visitor);
+ visitor.visit(node);
+ break;
+ case PLUS_NODE_MINUS:
+ acceptVisitor(node.getPlus(), visitor);
+ visitor.visit(node);
+ acceptVisitor(node.getMinus(), visitor);
+ break;
+ case MINUS_PLUS_NODE:
+ acceptVisitor(node.getMinus(), visitor);
+ acceptVisitor(node.getPlus(), visitor);
+ visitor.visit(node);
+ break;
+ case MINUS_NODE_PLUS:
+ acceptVisitor(node.getMinus(), visitor);
+ visitor.visit(node);
+ acceptVisitor(node.getPlus(), visitor);
+ break;
+ case NODE_PLUS_MINUS:
+ visitor.visit(node);
+ acceptVisitor(node.getPlus(), visitor);
+ acceptVisitor(node.getMinus(), visitor);
+ break;
+ default: // NODE_MINUS_PLUS:
+ visitor.visit(node);
+ acceptVisitor(node.getMinus(), visitor);
+ acceptVisitor(node.getPlus(), visitor);
+ break;
+ }
+ }
+ }
+ }
+
+ /** Cut a node with a hyperplane. The algorithm proceeds are follows:
+ * <ol>
+ * <li>The hyperplane is trimmed by splitting it with each cut hyperplane on the
+ * path from the given node to the root of the tree.</li>
+ * <li>If the remaining portion of the hyperplane is <em>not</em> empty, then
+ * <ul>
+ * <li>the remaining portion becomes the cut subhyperplane for the node,</li>
+ * <li>two new child nodes are created and initialized with
+ * {@link #initChildNode(AbstractNode, AbstractNode, boolean)}, and</li>
+ * <li>true is returned.</li>
+ * </ul>
+ * </li>
+ * <li>If the remaining portion of the hyperplane <em>is</em> empty (ie, the
+ * cutting hyperplane does not intersect the node's region), then
+ * <ul>
+ * <li>the node is converted to a leaf node (meaning that previous
+ * child nodes are lost), and</li>
+ * <li>false is returned.</li>
+ * </ul>
+ * </li>
+ * </ol>
+ *
+ * <p>It is important to note that since this method uses the path from given node
+ * to the tree root, it must only be used on nodes that are already inserted into
+ * the tree.</p>
+ *
+ * <p>This method always calls {@link #invalidate()} to invalidate cached tree properties.</p>
+ *
+ * @param node the node to cut
+ * @param cutter the hyperplane to cut the node with
+ * @return true if the node was cut and two new child nodes were created;
+ * otherwise false
+ * @see #trimToNode(AbstractNode, ConvexSubHyperplane)
+ * @see #cutNode(AbstractNode, ConvexSubHyperplane)
+ * @see #invalidate()
+ */
+ protected boolean insertNodeCut(final N node, final Hyperplane<P> cutter) {
+ // cut the hyperplane using all hyperplanes from this node up
+ // to the root
+ final ConvexSubHyperplane<P> cut = trimToNode(node, cutter.span());
+ if (cut == null || cut.isEmpty()) {
+ // insertion failed; the node was not cut
+ cutNode(node, null);
+ return false;
+ }
+
+ cutNode(node, cut);
+ return true;
+ }
+
+ /** Trim the given subhyperplane to the region defined by the given node. This method cuts the
+ * subhyperplane with the cut hyperplanes (binary partitioners) of all parent nodes up to
+ * the root and returns the trimmed subhyperplane or {@code null} if the subhyperplane lies
+ * outside of the region defined by the node.
+ *
+ * <p>If the subhyperplane is directly coincident with a binary partitioner of a parent node,
+ * then the relative orientations of the associated hyperplanes are used to determine the behavior,
+ * as described below.
+ * <ul>
+ * <li>If the orientations are <strong>similar</strong>, then the subhyperplane is determined to
+ * lie <em>outside</em> of the node's region and {@code null} is returned.</li>
+ * <li>If the orientations are <strong>different</strong> (ie, opposite), then the subhyperplane
+ * is determined to lie <em>inside</em> of the node's region and the fit operation continues
+ * with the remaining parent nodes.</li>
+ * </ul>
+ * These rules are designed to allow the creation of trees with node regions that are the thickness
+ * of a single hyperplane. For example, in two dimensions, a tree could be constructed with an internal
+ * node containing a cut along the x-axis in the positive direction and with a child node containing a
+ * cut along the x-axis in the opposite direction. If the nodes in the tree are given inside and outside
+ * attributes, then this tree could be used to represent a region consisting of a single line or a region
+ * consisting of the entire space except for the single line. This would not be possible if nodes were not
+ * able to have cut hyperplanes that were coincident with parent cuts but in opposite directions.
+ *
+ * <p>
+ * Another way of looking at the rules above is that inserting a hyperplane into the tree that exactly
+ * matches the hyperplane of a parent node does not add any information to the tree. However, adding a
+ * hyperplane to the tree that is coincident with a parent node but with the opposite orientation,
+ * <em>does</em> add information to the tree.
+ *
+ * @param node the node representing the region to fit the subhyperplane to
+ * @param sub the subhyperplane to trim to the node's region
+ * @return the trimmed subhyperplane or null if the given subhyperplane does not intersect
+ * the node's region
+ */
+ protected ConvexSubHyperplane<P> trimToNode(final N node, final ConvexSubHyperplane<P> sub) {
+
+ ConvexSubHyperplane<P> result = sub;
+
+ N parentNode = node.getParent();
+ N currentNode = node;
+
+ while (parentNode != null && result != null) {
+ final Split<? extends ConvexSubHyperplane<P>> split = result.split(parentNode.getCutHyperplane());
+
+ if (split.getLocation() == SplitLocation.NEITHER) {
+ // if we're directly on the splitter and have the same orientation, then
+ // we say the subhyperplane does not lie in the node's region (no new information
+ // is added to the tree in this case)
+ if (result.getHyperplane().similarOrientation(parentNode.getCutHyperplane())) {
+ result = null;
+ }
+ } else {
+ result = currentNode.isPlus() ? split.getPlus() : split.getMinus();
+ }
+
+ currentNode = parentNode;
+ parentNode = parentNode.getParent();
+ }
+
+ return result;
+ }
+
+ /** Remove the cut from the given node. Returns true if the node had a cut before
+ * the call to this method. Any previous child nodes are lost.
+ * @param node the node to remove the cut from
+ * @return true if the node previously had a cut
+ */
+ protected boolean removeNodeCut(final N node) {
+ boolean hadCut = node.getCut() != null;
+ cutNode(node, null);
+
+ return hadCut;
+ }
+
+ /** Set the cut subhyperplane for the given node. If {@code cut} is {@code null} then any
+ * existing child nodes are removed. If {@code cut} is not {@code null}, two new child
+ * nodes are created and initialized with
+ * {@link AbstractBSPTree#initChildNode(AbstractNode, AbstractNode, boolean)}.
+ *
+ * <p>This method performs absolutely <em>no</em> validation on the given cut
+ * subhyperplane. It is the responsibility of the caller to ensure that the
+ * subhyperplane fits the region represented by the node.</p>
+ *
+ * <p>This method always calls {@link #invalidate()} to invalidate cached tree properties.</p>
+ * @param node the node to cut
+ * @param cut the convex subhyperplane to set as the node cut
+ */
+ protected void cutNode(final N node, final ConvexSubHyperplane<P> cut) {
+ N plus = null;
+ N minus = null;
+
+ if (cut != null) {
+ minus = createNode();
+ initChildNode(node, minus, false);
+
+ plus = createNode();
+ initChildNode(node, plus, true);
+ }
+
+ node.setSubtree(cut, minus, plus);
+
+ invalidate();
+ }
+
+ /** Return true if the given transform swaps the inside and outside of
+ * the region.
+ *
+ * <p>The default behavior of this method is to return true if the transform
+ * does not preserve spatial orientation (ie, {@link Transform#preservesOrientation()}
+ * is false). Subclasses may need to override this method to implement the correct
+ * behavior for their space and dimension.</p>
+ * @param transform transform to check
+ * @return true if the given transform swaps the interior and exterior of
+ * the region
+ */
+ protected boolean swapsInsideOutside(final Transform<P> transform) {
+ return !transform.preservesOrientation();
+ }
+
+ /** Recursively insert a subhyperplane into the tree at the given node.
+ * @param node the node to begin insertion with
+ * @param insert the subhyperplane to insert
+ * @param trimmed subhyperplane containing the result of splitting the entire
+ * space with each hyperplane from this node to the root
+ */
+ private void insertRecursive(final N node, final ConvexSubHyperplane<P> insert,
+ final ConvexSubHyperplane<P> trimmed) {
+ if (node.isLeaf()) {
+ cutNode(node, trimmed);
+ } else {
+ final Split<? extends ConvexSubHyperplane<P>> insertSplit = insert.split(node.getCutHyperplane());
+
+ final ConvexSubHyperplane<P> minus = insertSplit.getMinus();
+ final ConvexSubHyperplane<P> plus = insertSplit.getPlus();
+
+ if (minus != null || plus != null) {
+ final Split<? extends ConvexSubHyperplane<P>> trimmedSplit = trimmed.split(node.getCutHyperplane());
+
+ if (minus != null) {
+ insertRecursive(node.getMinus(), minus, trimmedSplit.getMinus());
+ }
+ if (plus != null) {
+ insertRecursive(node.getPlus(), plus, trimmedSplit.getPlus());
+ }
+ }
+ }
+ }
+
+ /** Transform the subtree rooted as {@code node} recursively.
+ * @param node the root node of the subtree to transform
+ * @param t the transform to apply
+ * @param swapChildren if true, the plus and minus child nodes of each internal node
+ * will be swapped; this should be the case when the transform is a reflection
+ */
+ private void transformRecursive(final N node, final Transform<P> t, final boolean swapChildren) {
+ if (node.isInternal()) {
+ // transform our cut
+ final ConvexSubHyperplane<P> transformedCut = node.getCut().transform(t);
+
+ // transform our children
+ transformRecursive(node.getMinus(), t, swapChildren);
+ transformRecursive(node.getPlus(), t, swapChildren);
+
+ final N transformedMinus = swapChildren ? node.getPlus() : node.getMinus();
+ final N transformedPlus = swapChildren ? node.getMinus() : node.getPlus();
+
+ // set our new state
+ node.setSubtree(transformedCut, transformedMinus, transformedPlus);
+ }
+ }
+
+ /** Split this tree with the given hyperplane, placing the split contents into the given
+ * target trees. One of the given trees may be null, in which case that portion of the split
+ * will not be exported. The current tree is not modified.
+ * @param splitter splitting hyperplane
+ * @param minus tree that will contain the portion of the tree on the minus side of the splitter
+ * @param plus tree that will contain the portion of the tree on the plus side of the splitter
+ */
+ protected void splitIntoTrees(final Hyperplane<P> splitter,
+ final AbstractBSPTree<P, N> minus, final AbstractBSPTree<P, N> plus) {
+
+ AbstractBSPTree<P, N> temp = (minus != null) ? minus : plus;
+
+ N splitRoot = temp.splitSubtree(this.getRoot(), splitter.span());
+
+ if (minus != null) {
+ if (plus != null) {
+ plus.extract(splitRoot.getPlus());
+ }
+ minus.extract(splitRoot.getMinus());
+ } else {
+ plus.extract(splitRoot.getPlus());
+ }
+ }
+
+ /** Split the subtree rooted at the given node by a partitioning convex subhyperplane defined
+ * on the same region as the node. The subtree rooted at {@code node} is imported into
+ * this tree, meaning that if it comes from a different tree, the other tree is not
+ * modified.
+ * @param node the root node of the subtree to split; may come from a different tree,
+ * in which case the other tree is not modified
+ * @param partitioner partitioning convex subhyperplane
+ * @return node containing the split subtree
+ */
+ protected N splitSubtree(final N node, final ConvexSubHyperplane<P> partitioner) {
+ if (node.isLeaf()) {
+ return splitLeafNode(node, partitioner);
+ }
+ return splitInternalNode(node, partitioner);
+ }
+
+ /** Split the given leaf node by a partitioning convex subhyperplane defined on the
+ * same region and import it into this tree.
+ * @param node the leaf node to split
+ * @param partitioner partitioning convex subhyperplane
+ * @return node containing the split subtree
+ */
+ private N splitLeafNode(final N node, final ConvexSubHyperplane<P> partitioner) {
+ // in this case, we just create a new parent node with the partitioner as its
+ // cut and two copies of the original node as children
+ final N parent = createNode();
+ parent.setSubtree(partitioner, copyNode(node), copyNode(node));
+
+ return parent;
+ }
+
+ /** Split the given internal node by a partitioning convex subhyperplane defined on the same region
+ * as the node and import it into this tree.
+ * @param node the internal node to split
+ * @param partitioner partitioning convex subhyperplane
+ * @return node containing the split subtree
+ */
+ private N splitInternalNode(final N node, final ConvexSubHyperplane<P> partitioner) {
+ // split the partitioner and node cut with each other's hyperplanes to determine their relative positions
+ final Split<? extends ConvexSubHyperplane<P>> partitionerSplit = partitioner.split(node.getCutHyperplane());
+ final Split<? extends ConvexSubHyperplane<P>> nodeCutSplit = node.getCut().split(partitioner.getHyperplane());
+
+ final SplitLocation partitionerSplitSide = partitionerSplit.getLocation();
+ final SplitLocation nodeCutSplitSide = nodeCutSplit.getLocation();
+
+ final N result = createNode();
+
+ N resultMinus;
+ N resultPlus;
+
+ if (partitionerSplitSide == SplitLocation.PLUS) {
+ if (nodeCutSplitSide == SplitLocation.PLUS) {
+ // partitioner is on node cut plus side, node cut is on partitioner plus side
+ final N nodePlusSplit = splitSubtree(node.getPlus(), partitioner);
+
+ resultMinus = nodePlusSplit.getMinus();
+
+ resultPlus = copyNode(node);
+ resultPlus.setSubtree(node.getCut(), importSubtree(node.getMinus()), nodePlusSplit.getPlus());
+ } else {
+ // partitioner is on node cut plus side, node cut is on partitioner minus side
+ final N nodePlusSplit = splitSubtree(node.getPlus(), partitioner);
+
+ resultMinus = copyNode(node);
+ resultMinus.setSubtree(node.getCut(), importSubtree(node.getMinus()), nodePlusSplit.getMinus());
+
+ resultPlus = nodePlusSplit.getPlus();
+ }
+ } else if (partitionerSplitSide == SplitLocation.MINUS) {
+ if (nodeCutSplitSide == SplitLocation.MINUS) {
+ // partitioner is on node cut minus side, node cut is on partitioner minus side
+ final N nodeMinusSplit = splitSubtree(node.getMinus(), partitioner);
+
+ resultMinus = copyNode(node);
+ resultMinus.setSubtree(node.getCut(), nodeMinusSplit.getMinus(), importSubtree(node.getPlus()));
+
+ resultPlus = nodeMinusSplit.getPlus();
+ } else {
+ // partitioner is on node cut minus side, node cut is on partitioner plus side
+ final N nodeMinusSplit = splitSubtree(node.getMinus(), partitioner);
+
+ resultMinus = nodeMinusSplit.getMinus();
+
+ resultPlus = copyNode(node);
+ resultPlus.setSubtree(node.getCut(), nodeMinusSplit.getPlus(), importSubtree(node.getPlus()));
+ }
+ } else if (partitionerSplitSide == SplitLocation.BOTH) {
+ // partitioner and node cut split each other
+ final N nodeMinusSplit = splitSubtree(node.getMinus(), partitionerSplit.getMinus());
+ final N nodePlusSplit = splitSubtree(node.getPlus(), partitionerSplit.getPlus());
+
+ resultMinus = copyNode(node);
+ resultMinus.setSubtree(nodeCutSplit.getMinus(), nodeMinusSplit.getMinus(), nodePlusSplit.getMinus());
+
+ resultPlus = copyNode(node);
+ resultPlus.setSubtree(nodeCutSplit.getPlus(), nodeMinusSplit.getPlus(), nodePlusSplit.getPlus());
+ } else {
+ // partitioner and node cut are parallel or anti-parallel
+ final boolean sameOrientation = partitioner.getHyperplane().similarOrientation(node.getCutHyperplane());
+
+ resultMinus = importSubtree(sameOrientation ? node.getMinus() : node.getPlus());
+ resultPlus = importSubtree(sameOrientation ? node.getPlus() : node.getMinus());
+ }
+
+ result.setSubtree(partitioner, resultMinus, resultPlus);
+
+ return result;
+ }
+
+ /** Invalidate any previously computed properties that rely on the internal structure of the tree.
+ * This method must be called any time the tree's internal structure changes in order to force cacheable
+ * tree and node properties to be recomputed the next time they are requested.
+ *
+ * <p>This method increments the tree's {@link #version} property.</p>
+ * @see #getVersion()
+ */
+ protected void invalidate() {
+ version = Math.max(0, version + 1); // positive values only
+ }
+
+ /** Get the current structural version of the tree. This is incremented each time the
+ * tree structure is changes and can be used by nodes to allow caching of computed values.
+ * @return the current version of the tree structure
+ * @see #invalidate()
+ */
+ protected int getVersion() {
+ return version;
+ }
+
+ /** Abstract implementation of {@link BSPTree.Node}. This class is intended for use with
+ * {@link AbstractBSPTree} and delegates tree mutation methods back to the parent tree object.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ public abstract static class AbstractNode<P extends Point<P>, N extends AbstractNode<P, N>>
+ implements BSPTree.Node<P, N>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190225L;
+
+ /** The owning tree instance. */
+ private final AbstractBSPTree<P, N> tree;
+
+ /** The parent node; this will be null for the tree root node. */
+ private N parent;
+
+ /** The subhyperplane cutting the node's region; this will be null for leaf nodes. */
+ private ConvexSubHyperplane<P> cut;
+
+ /** The node lying on the minus side of the cut subhyperplane; this will be null
+ * for leaf nodes.
+ */
+ private N minus;
+
+ /** The node lying on the plus side of the cut subhyperplane; this will be null
+ * for leaf nodes.
+ */
+ private N plus;
+
+ /** The current version of the node. This is set to track the tree's version
+ * and is used to detect when certain values need to be recomputed due to
+ * structural changes in the tree.
+ */
+ private int nodeVersion = -1;
+
+ /** The depth of this node in the tree. This will be zero for the root node and
+ * {@link AbstractBSPTree#UNKNOWN_VALUE} when the value needs to be computed.
+ */
+ private int depth = UNKNOWN_VALUE;
+
+ /** The total number of nodes in the subtree rooted at this node. This will be
+ * set to {@link AbstractBSPTree#UNKNOWN_VALUE} when the value needs
+ * to be computed.
+ */
+ private int count = UNKNOWN_VALUE;
+
+ /** The height of the subtree rooted at this node. This will
+ * be set to {@link AbstractBSPTree#UNKNOWN_VALUE} when the value needs
+ * to be computed.
+ */
+ private int height = UNKNOWN_VALUE;
+
+ /** Simple constructor.
+ * @param tree the tree instance that owns this node
+ */
+ protected AbstractNode(final AbstractBSPTree<P, N> tree) {
+ this.tree = tree;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AbstractBSPTree<P, N> getTree() {
+ return tree;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int depth() {
+ if (depth == UNKNOWN_VALUE) {
+ // calculate our depth based on our parent's depth, if
+ // possible
+ if (parent != null) {
+ final int parentDepth = parent.depth();
+ if (parentDepth != UNKNOWN_VALUE) {
+ depth = parentDepth + 1;
+ }
+ }
+ }
+ return depth;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int height() {
+ checkValid();
+
+ if (height == UNKNOWN_VALUE) {
+ if (isLeaf()) {
+ height = 0;
+ } else {
+ height = Math.max(getMinus().height(), getPlus().height()) + 1;
+ }
+ }
+
+ return height;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int count() {
+ checkValid();
+
+ if (count == UNKNOWN_VALUE) {
+ count = 1;
+
+ if (!isLeaf()) {
+ count += minus.count() + plus.count();
+ }
+ }
+
+ return count;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterator<N> iterator() {
+ return new NodeIterator<P, N>(getSelf());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void accept(final BSPTreeVisitor<P, N> visitor) {
+ tree.acceptVisitor(getSelf(), visitor);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public N getParent() {
+ return parent;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isLeaf() {
+ return cut == null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInternal() {
+ return cut != null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isPlus() {
+ return parent != null && parent.getPlus() == this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isMinus() {
+ return parent != null && parent.getMinus() == this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConvexSubHyperplane<P> getCut() {
+ return cut;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Hyperplane<P> getCutHyperplane() {
+ return (cut != null) ? cut.getHyperplane() : null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public N getPlus() {
+ return plus;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public N getMinus() {
+ return minus;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean insertCut(final Hyperplane<P> cutter) {
+ return tree.insertNodeCut(getSelf(), cutter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean clearCut() {
+ return tree.removeNodeCut(getSelf());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public N cut(final Hyperplane<P> cutter) {
+ this.insertCut(cutter);
+
+ return getSelf();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[cut= ")
+ .append(getCut())
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Set the parameters for the subtree rooted at this node. The arguments should either be
+ * all null (representing a leaf node) or all non-null (representing an internal node).
+ *
+ * <p>Absolutely no validation is performed on the arguments. Callers are responsible for
+ * ensuring that any given subhyperplane fits the region defined by the node and that
+ * any child nodes belong to this tree and are correctly initialized.</p>
+ *
+ * @param newCut the new cut subhyperplane for the node
+ * @param newMinus the new minus child for the node
+ * @param newPlus the new plus child for the node
+ */
+ protected void setSubtree(final ConvexSubHyperplane<P> newCut, final N newMinus, final N newPlus) {
+ this.cut = newCut;
+
+ final N self = getSelf();
+
+ // cast for access to private member
+ AbstractNode<P, N> minusNode = newMinus;
+ AbstractNode<P, N> plusNode = newPlus;
+
+ // get the child depth now if we know it offhand, otherwise set it to the unknown value
+ // and have the child pull it when needed
+ final int childDepth = (depth != UNKNOWN_VALUE) ? depth + 1 : UNKNOWN_VALUE;
+
+ if (newMinus != null) {
+ minusNode.parent = self;
+ minusNode.depth = childDepth;
+ }
+ this.minus = newMinus;
+
+ if (newPlus != null) {
+ plusNode.parent = self;
+ plusNode.depth = childDepth;
+ }
+ this.plus = newPlus;
+ }
+
+ /**
+ * Make this node a root node, detaching it from its parent and settings its depth to zero.
+ * Any previous parent node will be left in an invalid state since one of its children now
+ * does not have a reference back to it.
+ */
+ protected void makeRoot() {
+ parent = null;
+ depth = 0;
+ }
+
+ /** Check if cached node properties are valid, meaning that no structural updates have
+ * occurred in the tree since the last call to this method. If updates have occurred, the
+ * {@link #nodeInvalidated()} method is called to clear the cached properties. This method
+ * should be called at the beginning of any method that fetches cacheable properties
+ * to ensure that no stale values are returned.
+ */
+ protected void checkValid() {
+ final int treeVersion = tree.getVersion();
+
+ if (nodeVersion != treeVersion) {
+ // the tree structure changed somewhere
+ nodeInvalidated();
+
+ // store the current version
+ nodeVersion = treeVersion;
+ }
+ }
+
+ /** Method called from {@link #checkValid()} when updates
+ * are detected in the tree. This method should clear out any
+ * computed properties that rely on the structure of the tree
+ * and prepare them for recalculation.
+ */
+ protected void nodeInvalidated() {
+ count = UNKNOWN_VALUE;
+ height = UNKNOWN_VALUE;
+ }
+
+ /** Get a reference to the current instance, cast to type N.
+ * @return a reference to the current instance, as type N.
+ */
+ protected abstract N getSelf();
+ }
+
+ /** Class for iterating through the nodes in a BSP subtree.
+ * @param <P> Point implementation type
+ * @param <N> Node implementation type
+ */
+ public static class NodeIterator<P extends Point<P>, N extends AbstractNode<P, N>> implements Iterator<N> {
+
+ /** The current node stack. */
+ private final Deque<N> stack = new LinkedList<>();
+
+ /** Create a new instance for iterating over the nodes in the given subtree.
+ * @param subtreeRoot the root node of the subtree to iterate
+ */
+ public NodeIterator(final N subtreeRoot) {
+ stack.push(subtreeRoot);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasNext() {
+ return !stack.isEmpty();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public N next() {
+ if (stack.isEmpty()) {
+ throw new NoSuchElementException();
+ }
+
+ final N result = stack.pop();
+
+ if (result != null && !result.isLeaf()) {
+ stack.push(result.getPlus());
+ stack.push(result.getMinus());
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperator.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperator.java
new file mode 100644
index 0000000..8cfaa46
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperator.java
@@ -0,0 +1,147 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree.AbstractNode;
+
+/** Class containing the basic algorithm for merging two {@link AbstractBSPTree}
+ * instances. Subclasses must override the
+ * {@link #mergeLeaf(AbstractBSPTree.AbstractNode, AbstractBSPTree.AbstractNode)} method
+ * to implement the merging logic for their particular use case. The remainder of the
+ * algorithm is independent of the use case.
+ *
+ * <p>This class does not expose any public methods so that subclasses can present their own
+ * public API, tailored to the specific types being worked with. In particular, most subclasses
+ * will want to restrict the tree types used with the algorithm, which is difficult to implement
+ * cleanly at this level.</p>
+ *
+ * <p>This class maintains state during the merging process and is therefore
+ * <em>not</em> thread-safe.</p>
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+public abstract class AbstractBSPTreeMergeOperator<P extends Point<P>, N extends AbstractNode<P, N>> {
+
+ /** The tree that the merge operation output will be written to. All existing content
+ * in this tree is overwritten.
+ */
+ private AbstractBSPTree<P, N> outputTree;
+
+ /** Set the tree used as output for this instance.
+ * @param outputTree the tree used as output for this instance
+ */
+ protected void setOutputTree(final AbstractBSPTree<P, N> outputTree) {
+ this.outputTree = outputTree;
+ }
+
+ /** Get the tree used as output for this instance.
+ * @return the tree used as output for this instance
+ */
+ protected AbstractBSPTree<P, N> getOutputTree() {
+ return outputTree;
+ }
+
+ /** Perform a merge operation with the two input trees and store the result in the output tree. The
+ * output tree may be one of the input trees, in which case, the tree is modified in place.
+ * @param input1 first input tree
+ * @param input2 second input tree
+ * @param output output tree all previous content in this tree is overwritten
+ */
+ protected void performMerge(final AbstractBSPTree<P, N> input1, final AbstractBSPTree<P, N> input2,
+ final AbstractBSPTree<P, N> output) {
+
+ setOutputTree(output);
+
+ final N root1 = input1.getRoot();
+ final N root2 = input2.getRoot();
+
+ final N outputRoot = performMergeRecursive(root1, root2);
+
+ getOutputTree().setRoot(outputRoot);
+ }
+
+ /** Recursively merge two nodes.
+ * @param node1 node from the first input tree
+ * @param node2 node from the second input tree
+ * @return a merged node
+ */
+ private N performMergeRecursive(final N node1, final N node2) {
+
+ if (node1.isLeaf() || node2.isLeaf()) {
+ // delegate to the mergeLeaf method if we can no longer continue
+ // merging recursively
+ final N merged = mergeLeaf(node1, node2);
+
+ // copy the merged node to the output if needed (in case mergeLeaf
+ // returned one of the input nodes directly)
+ return outputTree.importSubtree(merged);
+ } else {
+ final N partitioned = outputTree.splitSubtree(node2, node1.getCut());
+
+ final N minus = performMergeRecursive(node1.getMinus(), partitioned.getMinus());
+
+ final N plus = performMergeRecursive(node1.getPlus(), partitioned.getPlus());
+
+ final N outputNode = outputTree.copyNode(node1);
+ outputNode.setSubtree(node1.getCut(), minus, plus);
+
+ return outputNode;
+ }
+ }
+
+ /** Create a new node in the output tree. The node is associated with the output tree but
+ * is not attached to a parent node.
+ * @return a new node associated with the output tree but not yet attached to a parent
+ */
+ protected N outputNode() {
+ return outputTree.createNode();
+ }
+
+ /** Create a new node in the output tree with the same non-structural properties as the given
+ * node. Non-structural properties are properties other than parent, children, or cut. The
+ * returned node is associated with the output tree but is not attached to a parent node.
+ * Note that this method only copies the given node and <strong>not</strong> any of its children.
+ * @param node the input node to copy properties from
+ * @return a new node in the output tree
+ */
+ protected N outputNode(final N node) {
+ return outputTree.copyNode(node);
+ }
+
+ /** Place the subtree rooted at the given input node into the output tree. The subtree
+ * is copied if needed.
+ * @param node the root of the subtree to copy
+ * @return a subtree in the output tree
+ */
+ protected N outputSubtree(final N node) {
+ return outputTree.importSubtree(node);
+ }
+
+ /** Merge a leaf node from one input with a subtree from another.
+ * <p>When this method is called, one or both of the given nodes will be a leaf node.
+ * This method is expected to return a node representing the merger of the two given
+ * nodes. The way that the returned node is determined defines the overall behavior of
+ * the merge operation.
+ * </p>
+ * <p>The return value can be one of the two input nodes or a completely different one.</p>
+ * @param node1 node from the first input tree
+ * @param node2 node from the second input tree
+ * @return node representing the merger of the two input nodes
+ */
+ protected abstract N mergeLeaf(N node1, N node2);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
new file mode 100644
index 0000000..9a867a8
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
@@ -0,0 +1,966 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.internal.IteratorTransform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.ClosestFirstVisitor;
+
+/** {@link BSPTree} specialized for representing regions of space. For example, this
+ * class can be used to represent polygons in Euclidean 2D space and polyhedrons
+ * in Euclidean 3D space.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+public abstract class AbstractRegionBSPTree<
+ P extends Point<P>,
+ N extends AbstractRegionBSPTree.AbstractRegionNode<P, N>>
+ extends AbstractBSPTree<P, N> implements HyperplaneBoundedRegion<P> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 1L;
+
+ /** Value used to indicate an unknown size. */
+ private static final double UNKNOWN_SIZE = -1.0;
+
+ /** The region boundary size; this is computed when requested and then cached. */
+ private double boundarySize = UNKNOWN_SIZE;
+
+ /** The current size properties for the region. */
+ private RegionSizeProperties<P> regionSizeProperties;
+
+ /** Construct a new region will the given boolean determining whether or not the
+ * region will be full (including the entire space) or empty (excluding the entire
+ * space).
+ * @param full if true, the region will cover the entire space, otherwise it will
+ * be empty
+ */
+ protected AbstractRegionBSPTree(final boolean full) {
+ getRoot().setLocation(full ? RegionLocation.INSIDE : RegionLocation.OUTSIDE);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ return !hasNodeWithLocationRecursive(getRoot(), RegionLocation.INSIDE);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFull() {
+ return !hasNodeWithLocationRecursive(getRoot(), RegionLocation.OUTSIDE);
+ }
+
+ /** Return true if any node in the subtree rooted at the given node has a location with the
+ * given value.
+ * @param node the node at the root of the subtree to search
+ * @param location the location to find
+ * @return true if any node in the subtree has the given location
+ */
+ private boolean hasNodeWithLocationRecursive(final AbstractRegionNode<P, N> node, final RegionLocation location) {
+ if (node == null) {
+ return false;
+ }
+
+ return node.getLocation() == location ||
+ hasNodeWithLocationRecursive(node.getMinus(), location) ||
+ hasNodeWithLocationRecursive(node.getPlus(), location);
+ }
+
+ /** Modify this instance so that it contains the entire space.
+ * @see #isFull()
+ */
+ public void setFull() {
+ final N root = getRoot();
+
+ root.clearCut();
+ root.setLocation(RegionLocation.INSIDE);
+ }
+
+ /** Modify this instance so that is is completely empty.
+ * @see #isEmpty()
+ */
+ public void setEmpty() {
+ final N root = getRoot();
+
+ root.clearCut();
+ root.setLocation(RegionLocation.OUTSIDE);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return getRegionSizeProperties().getSize();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getBoundarySize() {
+ if (boundarySize < 0.0) {
+ double sum = 0.0;
+
+ RegionCutBoundary<P> boundary;
+ for (final AbstractRegionNode<P, N> node : this) {
+ boundary = node.getCutBoundary();
+ if (boundary != null) {
+ sum += boundary.getInsideFacing().getSize();
+ sum += boundary.getOutsideFacing().getSize();
+ }
+ }
+
+ boundarySize = sum;
+ }
+
+ return boundarySize;
+ }
+
+ /** Return an {@link Iterable} for iterating over the boundaries of the region.
+ * Each boundary is oriented such that its plus side points to the outside of the
+ * region. The exact ordering of the boundaries is determined by the internal structure
+ * of the tree.
+ * @return an {@link Iterable} for iterating over the boundaries of the region
+ * @see #getBoundaries()
+ */
+ public Iterable<? extends ConvexSubHyperplane<P>> boundaries() {
+ return createBoundaryIterable(Function.identity());
+ }
+
+ /** Internal method for creating the iterable instances used to iterate the region boundaries.
+ * @param typeConverter function to convert the generic convex subhyperplane type into
+ * the type specific for this tree
+ * @param <C> ConvexSubhyperplane implementation type
+ * @return an iterable to iterating the region boundaries
+ */
+ protected <C extends ConvexSubHyperplane<P>> Iterable<C> createBoundaryIterable(
+ final Function<ConvexSubHyperplane<P>, C> typeConverter) {
+
+ return new Iterable<C>() {
+
+ @Override
+ public Iterator<C> iterator() {
+ final NodeIterator<P, N> nodeIterator = new NodeIterator<>(getRoot());
+ return new RegionBoundaryIterator<>(nodeIterator, typeConverter);
+ }
+ };
+ }
+
+ /** Return a list containing the boundaries of the region. Each boundary is oriented such
+ * that its plus side points to the outside of the region. The exact ordering of
+ * the boundaries is determined by the internal structure of the tree.
+ * @return a list of the boundaries of the region
+ */
+ public List<? extends ConvexSubHyperplane<P>> getBoundaries() {
+ return createBoundaryList(Function.identity());
+ }
+
+ /** Iternal method for creating a list of the region boundaries.
+ * @param typeConverter function to convert the generic convex subhyperplane type into
+ * the type specific for this tree
+ * @param <C> ConvexSubhyperplane implementation type
+ * @return a list of the region boundaries
+ */
+ protected <C extends ConvexSubHyperplane<P>> List<C> createBoundaryList(
+ final Function<ConvexSubHyperplane<P>, C> typeConverter) {
+
+ final List<C> result = new ArrayList<>();
+
+ final RegionBoundaryIterator<P, C, N> it = new RegionBoundaryIterator<>(iterator(), typeConverter);
+ it.forEachRemaining(result::add);
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public P project(P pt) {
+ final BoundaryProjector<P, N> projector = new BoundaryProjector<>(pt);
+ accept(projector);
+
+ return projector.getProjected();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public P getBarycenter() {
+ return getRegionSizeProperties().getBarycenter();
+ }
+
+ /** Helper method implementing the algorithm for splitting a tree by a hyperplane. Subclasses
+ * should call this method with two instantiated trees of the correct type.
+ * @param splitter splitting hyperplane
+ * @param minus tree that will contain the minus side of the split result
+ * @param plus tree that will contain the plus side of the split result
+ * @param <T> Tree implementation type
+ * @return result of splitting this tree with the given hyperplane
+ */
+ protected <T extends AbstractRegionBSPTree<P, N>> Split<T> split(final Hyperplane<P> splitter,
+ final T minus, final T plus) {
+
+ splitIntoTrees(splitter, minus, plus);
+
+ T splitMinus = null;
+ T splitPlus = null;
+
+ if (minus != null) {
+ minus.getRoot().getPlus().setLocation(RegionLocation.OUTSIDE);
+ minus.condense();
+
+ splitMinus = minus.isEmpty() ? null : minus;
+ }
+ if (plus != null) {
+ plus.getRoot().getMinus().setLocation(RegionLocation.OUTSIDE);
+ plus.condense();
+
+ splitPlus = plus.isEmpty() ? null : plus;
+ }
+
+ return new Split<T>(splitMinus, splitPlus);
+ }
+
+ /** Get the size-related properties for the region. The value is computed
+ * lazily and cached.
+ * @return the size-related properties for the region
+ */
+ protected RegionSizeProperties<P> getRegionSizeProperties() {
+ if (regionSizeProperties == null) {
+ regionSizeProperties = computeRegionSizeProperties();
+ }
+
+ return regionSizeProperties;
+ }
+
+ /** Compute the size-related properties of the region.
+ * @return object containing size properties for the region
+ */
+ protected abstract RegionSizeProperties<P> computeRegionSizeProperties();
+
+ /** {@inheritDoc}
+ *
+ * <p>If the point is {@link org.apache.commons.geometry.core.Spatial#isNaN() NaN}, then
+ * {@link RegionLocation#OUTSIDE} is returned.</p>
+ */
+ @Override
+ public RegionLocation classify(final P point) {
+ if (point.isNaN()) {
+ return RegionLocation.OUTSIDE;
+ }
+
+ return classifyRecursive(getRoot(), point);
+ }
+
+ /** Recursively classify a point with respect to the region.
+ * @param node the node to classify against
+ * @param point the point to classify
+ * @return the classification of the point with respect to the region rooted
+ * at the given node
+ */
+ private RegionLocation classifyRecursive(final AbstractRegionNode<P, N> node, final P point) {
+ if (node.isLeaf()) {
+ // the point is in a leaf, so the classification is just the leaf location
+ return node.getLocation();
+ } else {
+ final HyperplaneLocation cutLoc = node.getCutHyperplane().classify(point);
+
+ if (cutLoc == HyperplaneLocation.MINUS) {
+ return classifyRecursive(node.getMinus(), point);
+ } else if (cutLoc == HyperplaneLocation.PLUS) {
+ return classifyRecursive(node.getPlus(), point);
+ } else {
+ // the point is on the cut boundary; classify against both child
+ // subtrees and see if we end up with the same result or not
+ RegionLocation minusLoc = classifyRecursive(node.getMinus(), point);
+ RegionLocation plusLoc = classifyRecursive(node.getPlus(), point);
+
+ if (minusLoc == plusLoc) {
+ return minusLoc;
+ }
+ return RegionLocation.BOUNDARY;
+ }
+ }
+ }
+
+ /** Change this region into its complement. All inside nodes become outside
+ * nodes and vice versa. The orientation of the cut subhyperplanes is not modified.
+ */
+ public void complement() {
+ complementRecursive(getRoot());
+ }
+
+ /** Set this instance to be the complement of the given tree. The argument
+ * is not modified.
+ * @param tree the tree to become the complement of
+ */
+ public void complement(final AbstractRegionBSPTree<P, N> tree) {
+ copySubtree(tree.getRoot(), getRoot());
+ complementRecursive(getRoot());
+ }
+
+ /** Recursively switch all inside nodes to outside nodes and vice versa.
+ * @param node the node at the root of the subtree to switch
+ */
+ private void complementRecursive(final AbstractRegionNode<P, N> node) {
+ if (node != null) {
+ final RegionLocation newLoc = (node.getLocationValue() == RegionLocation.INSIDE) ?
+ RegionLocation.OUTSIDE :
+ RegionLocation.INSIDE;
+
+ node.setLocation(newLoc);
+
+ complementRecursive(node.getMinus());
+ complementRecursive(node.getPlus());
+ }
+ }
+
+ /** Compute the union of this instance and the given region, storing the result back in
+ * this instance. The argument is not modified.
+ * @param other the tree to compute the union with
+ */
+ public void union(final AbstractRegionBSPTree<P, N> other) {
+ new UnionOperator<P, N>().apply(this, other, this);
+ }
+
+ /** Compute the union of the two regions passed as arguments and store the result in
+ * this instance. Any nodes currently existing in this instance are removed.
+ * @param a first argument to the union operation
+ * @param b second argument to the union operation
+ */
+ public void union(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
+ new UnionOperator<P, N>().apply(a, b, this);
+ }
+
+ /** Compute the intersection of this instance and the given region, storing the result back in
+ * this instance. The argument is not modified.
+ * @param other the tree to compute the intersection with
+ */
+ public void intersection(final AbstractRegionBSPTree<P, N> other) {
+ new IntersectionOperator<P, N>().apply(this, other, this);
+ }
+
+ /** Compute the intersection of the two regions passed as arguments and store the result in
+ * this instance. Any nodes currently existing in this instance are removed.
+ * @param a first argument to the intersection operation
+ * @param b second argument to the intersection operation
+ */
+ public void intersection(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
+ new IntersectionOperator<P, N>().apply(a, b, this);
+ }
+
+ /** Compute the difference of this instance and the given region, storing the result back in
+ * this instance. The argument is not modified.
+ * @param other the tree to compute the difference with
+ */
+ public void difference(final AbstractRegionBSPTree<P, N> other) {
+ new DifferenceOperator<P, N>().apply(this, other, this);
+ }
+
+ /** Compute the difference of the two regions passed as arguments and store the result in
+ * this instance. Any nodes currently existing in this instance are removed.
+ * @param a first argument to the difference operation
+ * @param b second argument to the difference operation
+ */
+ public void difference(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
+ new DifferenceOperator<P, N>().apply(a, b, this);
+ }
+
+ /** Compute the symmetric difference (xor) of this instance and the given region, storing the result back in
+ * this instance. The argument is not modified.
+ * @param other the tree to compute the symmetric difference with
+ */
+ public void xor(final AbstractRegionBSPTree<P, N> other) {
+ new XorOperator<P, N>().apply(this, other, this);
+ }
+
+ /** Compute the symmetric difference (xor) of the two regions passed as arguments and store the result in
+ * this instance. Any nodes currently existing in this instance are removed.
+ * @param a first argument to the symmetric difference operation
+ * @param b second argument to the symmetric difference operation
+ */
+ public void xor(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
+ new XorOperator<P, N>().apply(a, b, this);
+ }
+
+ /** Condense this tree by removing redundant subtrees.
+ *
+ * <p>This operation can be used to reduce the total number of nodes in the
+ * tree after performing node manipulations. For example, if two sibling leaf
+ * nodes both represent the same {@link RegionLocation}, then there is no reason
+ * from the perspective of the geometric region to retain both nodes. They are
+ * therefore both merged into their parent node. This method performs this
+ * simplification process.
+ * </p>
+ */
+ protected void condense() {
+ condenseRecursive(getRoot());
+ }
+
+ /** Recursively condense nodes that have children with homogenous location attributes
+ * (eg, both inside, both outside) into single nodes.
+ * @param node the root of the subtree to condense
+ * @return the location of the successfully condensed subtree or null if no condensing was
+ * able to be performed
+ */
+ private RegionLocation condenseRecursive(final N node) {
+ if (node.isLeaf()) {
+ return node.getLocation();
+ }
+
+ final RegionLocation minusLocation = condenseRecursive(node.getMinus());
+ final RegionLocation plusLocation = condenseRecursive(node.getPlus());
+
+ if (minusLocation != null && plusLocation != null && minusLocation == plusLocation) {
+ node.setLocation(minusLocation);
+ node.clearCut();
+
+ return minusLocation;
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void copyNodeProperties(final N src, final N dst) {
+ dst.setLocation(src.getLocationValue());
+ }
+
+ /** Compute the portion of the node's cut subhyperplane that lies on the boundary of
+ * the region.
+ * @param node the node to compute the cut subhyperplane boundary of
+ * @return object representing the portions of the node's cut subhyperplane that lie
+ * on the region's boundary
+ */
+ private RegionCutBoundary<P> computeBoundary(final N node) {
+ if (node.isLeaf()) {
+ // no boundary for leaf nodes; they are either entirely in or
+ // entirely out
+ return null;
+ }
+
+ ConvexSubHyperplane<P> sub = node.getCut();
+
+ // find the portions of the node cut sub-hyperplane that touch inside and
+ // outside cells in the minus sub-tree
+ SubHyperplane.Builder<P> minusInBuilder = sub.builder();
+ SubHyperplane.Builder<P> minusOutBuilder = sub.builder();
+
+ characterizeSubHyperplane(sub, node.getMinus(), minusInBuilder, minusOutBuilder);
+
+ List<? extends ConvexSubHyperplane<P>> minusIn = minusInBuilder.build().toConvex();
+ List<? extends ConvexSubHyperplane<P>> minusOut = minusOutBuilder.build().toConvex();
+
+ // create the result boundary builders
+ SubHyperplane.Builder<P> insideFacing = sub.builder();
+ SubHyperplane.Builder<P> outsideFacing = sub.builder();
+
+ if (!minusIn.isEmpty()) {
+ // Add to the boundary anything that touches an inside cell in the minus sub-tree
+ // and an outside cell in the plus sub-tree. These portions are oriented with their
+ // plus side pointing to the outside of the region.
+ for (ConvexSubHyperplane<P> minusInFragment : minusIn) {
+ characterizeSubHyperplane(minusInFragment, node.getPlus(), null, outsideFacing);
+ }
+ }
+
+ if (!minusOut.isEmpty()) {
+ // Add to the boundary anything that touches an outside cell in the minus sub-tree
+ // and an inside cell in the plus sub-tree. These portions are oriented with their
+ // plus side pointing to the inside of the region.
+ for (ConvexSubHyperplane<P> minusOutFragment : minusOut) {
+ characterizeSubHyperplane(minusOutFragment, node.getPlus(), insideFacing, null);
+ }
+ }
+
+ return new RegionCutBoundary<P>(insideFacing.build(), outsideFacing.build());
+ }
+
+ /** Recursive method to characterize a convex subhyperplane with respect to the region's
+ * boundaries.
+ * @param sub the subhyperplane to characterize
+ * @param node the node to characterize the subhyperplane against
+ * @param in the builder that will receive the portions of the subhyperplane that lie in the inside
+ * of the region; may be null
+ * @param out the builder that will receive the portions of the subhyperplane that lie on the outside
+ * of the region; may be null
+ */
+ private void characterizeSubHyperplane(final ConvexSubHyperplane<P> sub, final AbstractRegionNode<P, N> node,
+ final SubHyperplane.Builder<P> in, final SubHyperplane.Builder<P> out) {
+
+ if (sub != null) {
+ if (node.isLeaf()) {
+ if (node.isInside() && in != null) {
+ in.add(sub);
+ } else if (node.isOutside() && out != null) {
+ out.add(sub);
+ }
+ } else {
+ final Split<? extends ConvexSubHyperplane<P>> split = sub.split(node.getCutHyperplane());
+
+ // Continue further on down the subtree with the same subhyperplane if the
+ // subhyperplane lies directly on the current node's cut
+ if (split.getLocation() == SplitLocation.NEITHER) {
+ characterizeSubHyperplane(sub, node.getPlus(), in, out);
+ characterizeSubHyperplane(sub, node.getMinus(), in, out);
+ } else {
+ characterizeSubHyperplane(split.getPlus(), node.getPlus(), in, out);
+ characterizeSubHyperplane(split.getMinus(), node.getMinus(), in, out);
+ }
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void initChildNode(final N parent, final N child, final boolean isPlus) {
+ super.initChildNode(parent, child, isPlus);
+
+ child.setLocation(isPlus ? RegionLocation.OUTSIDE : RegionLocation.INSIDE);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void invalidate() {
+ super.invalidate();
+
+ // clear cached region properties
+ boundarySize = UNKNOWN_SIZE;
+ regionSizeProperties = null;
+ }
+
+ /** {@link BSPTree.Node} implementation for use with {@link AbstractRegionBSPTree}s.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ public abstract static class AbstractRegionNode<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+ extends AbstractBSPTree.AbstractNode<P, N> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 1L;
+
+ /** The location for the node. This will only be set on leaf nodes. */
+ private RegionLocation location;
+
+ /** Object representing the part of the node cut subhyperplane that lies on the
+ * region boundary. This is calculated lazily and is only present on internal nodes.
+ */
+ private RegionCutBoundary<P> cutBoundary;
+
+ /** Simple constructor.
+ * @param tree owning tree instance
+ */
+ protected AbstractRegionNode(AbstractBSPTree<P, N> tree) {
+ super(tree);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AbstractRegionBSPTree<P, N> getTree() {
+ // cast to our parent tree type
+ return (AbstractRegionBSPTree<P, N>) super.getTree();
+ }
+
+ /** Get the location of the node. This value will only be non-null for
+ * leaf nodes.
+ * @return the location of the node; will be null for internal nodes
+ */
+ public RegionLocation getLocation() {
+ return isLeaf() ? location : null;
+ }
+
+ /** True if the node is a leaf node and has a location of {@link RegionLocation#INSIDE}.
+ * @return true if the node is a leaf node and has a location of
+ * {@link RegionLocation#INSIDE}
+ */
+ public boolean isInside() {
+ return getLocation() == RegionLocation.INSIDE;
+ }
+
+ /** True if the node is a leaf node and has a location of {@link RegionLocation#OUTSIDE}.
+ * @return true if the node is a leaf node and has a location of
+ * {@link RegionLocation#OUTSIDE}
+ */
+ public boolean isOutside() {
+ return getLocation() == RegionLocation.OUTSIDE;
+ }
+
+ /** Get the portion of the node's cut subhyperplane that lies on the boundary of the
+ * region.
+ * @return the portion of the node's cut subhyperplane that lies on the boundary of
+ * the region
+ */
+ public RegionCutBoundary<P> getCutBoundary() {
+ if (!isLeaf()) {
+ checkValid();
+
+ if (cutBoundary == null) {
+ cutBoundary = getTree().computeBoundary(getSelf());
+ }
+ }
+
+ return cutBoundary;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[cut= ")
+ .append(getCut())
+ .append(", location= ")
+ .append(getLocation())
+ .append("]");
+
+ return sb.toString();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void nodeInvalidated() {
+ super.nodeInvalidated();
+
+ // null any computed boundary value since it is no longer valid
+ cutBoundary = null;
+ }
+
+ /** Set the location attribute for the node.
+ * @param location the location attribute for the node
+ */
+ protected void setLocation(final RegionLocation location) {
+ this.location = location;
+ }
+
+ /** Get the value of the location property, unmodified based on the
+ * node's leaf state.
+ * @return the value of the location property
+ */
+ protected RegionLocation getLocationValue() {
+ return location;
+ }
+ }
+
+ /** Class containing the basic algorithm for merging region BSP trees.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ public abstract static class RegionMergeOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+ extends AbstractBSPTreeMergeOperator<P, N> {
+
+ /** Merge two input trees, storing the output in the third. The output tree can be one of the
+ * input trees. The output tree is condensed before the method returns.
+ * @param inputTree1 first input tree
+ * @param inputTree2 second input tree
+ * @param outputTree the tree that will contain the result of the merge; may be one
+ * of the input trees
+ */
+ public void apply(final AbstractRegionBSPTree<P, N> inputTree1, final AbstractRegionBSPTree<P, N> inputTree2,
+ final AbstractRegionBSPTree<P, N> outputTree) {
+
+ this.performMerge(inputTree1, inputTree2, outputTree);
+
+ outputTree.condense();
+ }
+ }
+
+ /** Class for performing boolean union operations on region trees.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ public static class UnionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+ extends RegionMergeOperator<P, N> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected N mergeLeaf(final N node1, final N node2) {
+ if (node1.isLeaf()) {
+ return node1.isInside() ? node1 : node2;
+ }
+
+ // call again with flipped arguments
+ return mergeLeaf(node2, node1);
+ }
+ }
+
+ /** Class for performing boolean intersection operations on region trees.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ public static class IntersectionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+ extends RegionMergeOperator<P, N> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected N mergeLeaf(final N node1, final N node2) {
+ if (node1.isLeaf()) {
+ return node1.isInside() ? node2 : node1;
+ }
+
+ // call again with flipped arguments
+ return mergeLeaf(node2, node1);
+ }
+ }
+
+ /** Class for performing boolean difference operations on region trees.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ public static class DifferenceOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+ extends RegionMergeOperator<P, N> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected N mergeLeaf(final N node1, final N node2) {
+ // a region is included if it belongs in tree1 and is not in tree2
+
+ if (node1.isInside()) {
+ // this region is inside of tree1, so only include subregions that are
+ // not in tree2, ie include everything in node2's complement
+ final N output = outputSubtree(node2);
+ output.getTree().complementRecursive(output);
+
+ return output;
+ } else if (node2.isInside()) {
+ // this region is inside of tree2 and so cannot be in the result region
+ final N output = outputNode();
+ output.setLocation(RegionLocation.OUTSIDE);
+
+ return output;
+ }
+
+ // this region is not in tree2, so we can include everything in tree1
+ return node1;
+ }
+ }
+
+ /** Class for performing boolean symmetric difference (xor) operations on region trees.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ public static class XorOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+ extends RegionMergeOperator<P, N> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected N mergeLeaf(final N node1, final N node2) {
+ // a region is included if it belongs in tree1 and is not in tree2 OR
+ // it belongs in tree2 and is not in tree1
+
+ if (node1.isLeaf()) {
+ if (node1.isInside()) {
+ // this region is inside node1, so only include subregions that are
+ // not in node2, ie include everything in node2's complement
+ final N output = outputSubtree(node2);
+ output.getTree().complementRecursive(output);
+
+ return output;
+ } else {
+ // this region is not in node1, so only include subregions that
+ // in node2
+ return node2;
+ }
+ }
+
+ // the operation is symmetric, so perform the same operation but with the
+ // nodes flipped
+ return mergeLeaf(node2, node1);
+ }
+ }
+
+ /** Class used to compute the point on the region's boundary that is closest to a target point.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ protected static class BoundaryProjector<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+ extends ClosestFirstVisitor<P, N> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190504L;
+
+ /** The projected point. */
+ private P projected;
+
+ /** The current closest distance to the boundary found. */
+ private double minDist = -1.0;
+
+ /** Simple constructor.
+ * @param point the point to project onto the region's boundary
+ */
+ public BoundaryProjector(final P point) {
+ super(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visit(final N node) {
+ final P point = getTarget();
+
+ if (node.isInternal() && (minDist < 0.0 || isPossibleClosestCut(node.getCut(), point, minDist))) {
+ final RegionCutBoundary<P> boundary = node.getCutBoundary();
+ final P boundaryPt = boundary.closest(point);
+
+ final double dist = boundaryPt.distance(point);
+ final int cmp = Double.compare(dist, minDist);
+
+ if (minDist < 0.0 || cmp < 0) {
+ projected = boundaryPt;
+ minDist = dist;
+ } else if (cmp == 0) {
+ // the two points are the _exact_ same distance from the reference point, so use
+ // a separate method to disambiguate them
+ projected = disambiguateClosestPoint(point, projected, boundaryPt);
+ }
+ }
+ }
+
+ /** Return true if the given node cut subhyperplane is a possible candidate for containing the
+ * closest region boundary point to the target.
+ * @param cut the node cut subhyperplane to test
+ * @param target the target point being projected
+ * @param currentMinDist the smallest distance found so far to a region boundary; this value is guaranteed
+ * to be non-negative
+ * @return true if the subhyperplane is a possible candidate for containing the closest region
+ * boundary point to the target
+ */
+ protected boolean isPossibleClosestCut(final SubHyperplane<P> cut, final P target,
+ final double currentMinDist) {
+ return Math.abs(cut.getHyperplane().offset(target)) <= currentMinDist;
+ }
+
+ /** Method used to determine which of points {@code a} and {@code b} should be considered
+ * as the "closest" point to {@code target} when the points are exactly equidistant.
+ * @param target the target point
+ * @param a first point to consider
+ * @param b second point to consider
+ * @return which of {@code a} or {@code b} should be considered as the one closest to
+ * {@code target}
+ */
+ protected P disambiguateClosestPoint(final P target, final P a, final P b) {
+ return a;
+ }
+
+ /** Get the projected point on the region's boundary, or null if no point could be found.
+ * @return the projected point on the region's boundary
+ */
+ public P getProjected() {
+ return projected;
+ }
+ }
+
+ /** Class containing the primary size-related properties of a region. These properties
+ * are typically computed at the same time, so this class serves to encapsulate the result
+ * of the combined computation.
+ * @param <P> Point implementation type
+ */
+ protected static class RegionSizeProperties<P extends Point<P>> implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190428L;
+
+ /** The size of the region. */
+ private final double size;
+
+ /** The barycenter of the region. */
+ private final P barycenter;
+
+ /** Simple constructor.
+ * @param size the region size
+ * @param barycenter the region barycenter
+ */
+ public RegionSizeProperties(final double size, final P barycenter) {
+ this.size = size;
+ this.barycenter = barycenter;
+ }
+
+ /** Get the size of the region.
+ * @return the size of the region
+ */
+ public double getSize() {
+ return size;
+ }
+
+ /** Get the barycenter of the region.
+ * @return the barycenter of the region
+ */
+ public P getBarycenter() {
+ return barycenter;
+ }
+ }
+
+ /** Class that iterates over the boundary convex subhyperplanes from a set of region nodes.
+ * @param <P> Point implementation type
+ * @param <C> Boundary convex subhyperplane implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ protected static final class RegionBoundaryIterator<
+ P extends Point<P>,
+ C extends ConvexSubHyperplane<P>,
+ N extends AbstractRegionNode<P, N>>
+ extends IteratorTransform<N, C> {
+
+ /** Function that converts from the convex subhyperplane type to the output type. */
+ private final Function<ConvexSubHyperplane<P>, C> typeConverter;
+
+ /** Simple constructor.
+ * @param inputIterator iterator that will provide all nodes in the tree
+ * @param typeConverter function that converts from the convex subhyperplane type to the output type
+ */
+ private RegionBoundaryIterator(final Iterator<N> inputIterator,
+ final Function<ConvexSubHyperplane<P>, C> typeConverter) {
+ super(inputIterator);
+
+ this.typeConverter = typeConverter;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void acceptInput(final N input) {
+ if (input.isInternal()) {
+ final RegionCutBoundary<P> cutBoundary = input.getCutBoundary();
+
+ final SubHyperplane<P> outsideFacing = cutBoundary.getOutsideFacing();
+ final SubHyperplane<P> insideFacing = cutBoundary.getInsideFacing();
+
+ if (outsideFacing != null && !outsideFacing.isEmpty()) {
+ for (ConvexSubHyperplane<P> boundary : outsideFacing.toConvex()) {
+
+ addOutput(typeConverter.apply(boundary));
+ }
+ }
+ if (insideFacing != null && !insideFacing.isEmpty()) {
+ for (ConvexSubHyperplane<P> boundary : insideFacing.toConvex()) {
+ ConvexSubHyperplane<P> reversed = boundary.reverse();
+
+ addOutput(typeConverter.apply(reversed));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTree.java
new file mode 100644
index 0000000..d7fecd6
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTree.java
@@ -0,0 +1,144 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Simple {@link BSPTree} implementation allowing arbitrary values to be
+ * associated with each node.
+ * @param <P> Point implementation type
+ * @param <T> Tree node attribute type
+ */
+public class AttributeBSPTree<P extends Point<P>, T>
+ extends AbstractBSPTree<P, AttributeBSPTree.AttributeNode<P, T>> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190306L;
+
+ /** The initial attribute value to use for newly created nodes. */
+ private final T initialNodeAttribute;
+
+ /** Create a new tree instance. New nodes in the tree are given an attribute
+ * of null.
+ */
+ public AttributeBSPTree() {
+ this(null);
+ }
+
+ /** Create a new tree instance. New nodes in the tree are assigned the given
+ * initial attribute value.
+ * @param initialNodeAttribute The attribute value to assign to newly created nodes.
+ */
+ public AttributeBSPTree(T initialNodeAttribute) {
+ this.initialNodeAttribute = initialNodeAttribute;
+
+ this.getRoot().setAttribute(initialNodeAttribute);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected AttributeNode<P, T> createNode() {
+ return new AttributeNode<P, T>(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void initChildNode(final AttributeNode<P, T> parent, final AttributeNode<P, T> child,
+ final boolean isPlus) {
+ super.initChildNode(parent, child, isPlus);
+
+ child.setAttribute(initialNodeAttribute);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void copyNodeProperties(final AttributeNode<P, T> src, final AttributeNode<P, T> dst) {
+ dst.setAttribute(src.getAttribute());
+ }
+
+ /** {@link BSPTree.Node} implementation for use with {@link AttributeBSPTree}s.
+ * @param <P> Point implementation type
+ * @param <T> Tree node attribute type
+ */
+ public static class AttributeNode<P extends Point<P>, T>
+ extends AbstractBSPTree.AbstractNode<P, AttributeNode<P, T>> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 1L;
+
+ /** The node attribute. */
+ private T attribute;
+
+ /** Simple constructor.
+ * @param tree the owning tree; this must be an instance of {@link AttributeBSPTree}
+ */
+ protected AttributeNode(AbstractBSPTree<P, AttributeNode<P, T>> tree) {
+ super(tree);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AttributeBSPTree<P, T> getTree() {
+ // cast to our parent tree type
+ return (AttributeBSPTree<P, T>) super.getTree();
+ }
+
+ /** Get the attribute associated with this node.
+ * @return the attribute associated with this node
+ */
+ public T getAttribute() {
+ return attribute;
+ }
+
+ /** Set the attribute associated with this node.
+ * @param attribute the attribute to associate with this node
+ */
+ public void setAttribute(T attribute) {
+ this.attribute = attribute;
+ }
+
+ /** Set the attribute for this node. The node is returned.
+ * @param attributeValue attribute to set for the node
+ * @return the node instance
+ */
+ public AttributeNode<P, T> attr(final T attributeValue) {
+ setAttribute(attributeValue);
+
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[cut= ")
+ .append(getCut())
+ .append(", attribute= ")
+ .append(attribute)
+ .append("]");
+
+ return sb.toString();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected AttributeNode<P, T> getSelf() {
+ return this;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPSubtree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPSubtree.java
new file mode 100644
index 0000000..5943765
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPSubtree.java
@@ -0,0 +1,53 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Interface for types that form the root of BSP subtrees. This includes trees
+ * themselves as well as each node in a tree.
+ * @param <P> Point implementation type
+ * @param <N> Node implementation type
+ */
+public interface BSPSubtree<P extends Point<P>, N extends BSPTree.Node<P, N>> extends Iterable<N> {
+
+ /** Return the total number of nodes in the subtree.
+ * @return the total number of nodes in the subtree.
+ */
+ int count();
+
+ /** The height of the subtree, ie the length of the longest downward path from
+ * the subtree root to a leaf node. A leaf node has a height of 0.
+ * @return the height of the subtree.
+ */
+ int height();
+
+ /** Accept a visitor instance, calling it with each node from the subtree.
+ * @param visitor visitor called with each subtree node
+ */
+ void accept(BSPTreeVisitor<P, N> visitor);
+
+ /** Create a stream over the nodes in this subtree.
+ * @return a stream for accessing the nodes in this subtree
+ */
+ default Stream<N> stream() {
+ return StreamSupport.stream(spliterator(), false);
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTree.java
new file mode 100644
index 0000000..758ca90
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTree.java
@@ -0,0 +1,225 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** Interface for Binary Space Partitioning (BSP) trees.
+ * @param <P> Point implementation type
+ * @param <N> Node implementation type
+ */
+public interface BSPTree<P extends Point<P>, N extends BSPTree.Node<P, N>>
+ extends BSPSubtree<P, N> {
+
+ /** Enum specifying possible behaviors when a point used to locate a node
+ * falls directly on the cut subhyperplane of an internal node.
+ */
+ enum NodeCutRule {
+
+ /** Choose the minus child of the internal node and continue searching.
+ * This behavior will result in a leaf node always being returned by the
+ * node search.
+ */
+ MINUS,
+
+ /** Choose the plus child of the internal node and continue searching.
+ * This behavior will result in a leaf node always being returned by the
+ * node search.
+ */
+ PLUS,
+
+ /** Choose the internal node and stop searching. This behavior may result
+ * in non-leaf nodes being returned by the node search.
+ */
+ NODE
+ }
+
+ /** Get the root node of the tree.
+ * @return the root node of the tree
+ */
+ N getRoot();
+
+ /** Find a node in this subtree containing the given point or its interior or boundary.
+ * When a point lies directly on the cut of an internal node, the minus child of the
+ * cut is chosen. This is equivalent to {@code subtree.findNode(pt, NodeCutRule.MINUS)}
+ * and always returns a leaf node.
+ * @param pt test point used to locate a node in the tree
+ * @return leaf node containing the point on its interior or boundary
+ * @see #findNode(Point, NodeCutRule)
+ */
+ default N findNode(P pt) {
+ return findNode(pt, NodeCutRule.MINUS);
+ }
+
+ /** Find a node in this subtree containing the given point on it interior or boundary. The
+ * search should always return a leaf node except in the cases where the given point lies
+ * exactly on the cut subhyperplane of an internal node. In this case, it is unclear whether
+ * the search should continue with the minus child, the plus child, or end with the internal
+ * node. The {@code cutRule} argument specifies what should happen in this case.
+ * <ul>
+ * <li>{@link NodeCutRule#MINUS} - continue the search in the minus subtree</li>
+ * <li>{@link NodeCutRule#PLUS} - continue the search in the plus subtree</li>
+ * <li>{@link NodeCutRule#NODE} - stop the search and return the internal node</li>
+ * </ul>
+ * @param pt test point used to locate a node in the tree
+ * @param cutRule value used to determine the search behavior when the test point lies
+ * exactly on the cut subhyperplane of an internal node
+ * @return node containing the point on its interior or boundary
+ * @see #findNode(Point)
+ */
+ N findNode(P pt, NodeCutRule cutRule);
+
+ /** Insert a subhyperplane into the tree.
+ * @param sub the subhyperplane to insert into the tree
+ */
+ void insert(SubHyperplane<P> sub);
+
+ /** Insert a convex subhyperplane into the tree.
+ * @param convexSub the convex subhyperplane to insert into the tree
+ */
+ void insert(ConvexSubHyperplane<P> convexSub);
+
+ /** Insert a set of convex subhyperplanes into the tree.
+ * @param convexSubs iterable containing a collection of subhyperplanes
+ * to insert into the tree
+ */
+ void insert(Iterable<? extends ConvexSubHyperplane<P>> convexSubs);
+
+ /** Make the current instance a deep copy of the argument.
+ * @param src the tree to copy
+ */
+ void copy(BSPTree<P, N> src);
+
+ /** Set this instance to the region represented by the given node. The
+ * given node could have come from this tree or a different tree.
+ * @param node the node to extract
+ */
+ void extract(N node);
+
+ /** Transform this tree. Each cut subhyperplane in the tree is transformed in place using
+ * the given {@link Transform}.
+ * @param transform the transform to apply
+ */
+ void transform(Transform<P> transform);
+
+ /** Interface for Binary Space Partitioning (BSP) tree nodes.
+ * @param <P> Point type
+ * @param <N> BSP tree node implementation type
+ */
+ interface Node<P extends Point<P>, N extends Node<P, N>> extends BSPSubtree<P, N> {
+
+ /** Get the {@link BSPTree} that owns the node.
+ * @return the owning tree
+ */
+ BSPTree<P, N> getTree();
+
+ /** Get the depth of the node in the tree. The root node of the tree
+ * has a depth of 0.
+ * @return the depth of the node in the tree
+ */
+ int depth();
+
+ /** Get the parent of the node. This will be null if the node is the
+ * root of the tree.
+ * @return the parent node for this instance
+ */
+ N getParent();
+
+ /** Get the cut for the node. This is a convex subhyperplane that splits
+ * the region for the cell into two disjoint regions, namely the plus and
+ * minus regions. This will be null for leaf nodes.
+ * @see #getPlus()
+ * @see #getMinus()
+ * @return the cut subhyperplane for the cell
+ */
+ ConvexSubHyperplane<P> getCut();
+
+ /** Get the hyperplane belonging to the node cut, if it exists.
+ * @return the hyperplane belonging to the node cut, or null if
+ * the node does not have a cut
+ * @see #getCut()
+ */
+ Hyperplane<P> getCutHyperplane();
+
+ /** Get the node for the minus region of the cell. This will be null if the
+ * node has not been cut, ie if it is a leaf node.
+ * @return the node for the minus region of the cell
+ */
+ N getMinus();
+
+ /** Get the node for the plus region of the cell. This will be null if the
+ * node has not been cut, ie if it is a leaf node.
+ * @return the node for the plus region of the cell
+ */
+ N getPlus();
+
+ /** Return true if the node is a leaf node, meaning that it has no
+ * binary partitioner (aka "cut") and therefore no child nodes.
+ * @return true if the node is a leaf node
+ */
+ boolean isLeaf();
+
+ /** Return true if the node is an internal node, meaning that is
+ * has a binary partitioner (aka "cut") and therefore two child nodes.
+ * @return true if the node is an internal node
+ */
+ boolean isInternal();
+
+ /** Return true if the node has a parent and is the parent's minus
+ * child.
+ * @return true if the node is the minus child of its parent
+ */
+ boolean isMinus();
+
+ /** Return true if the node has a parent and is the parent's plus
+ * child.
+ * @return true if the node is the plus child of its parent
+ */
+ boolean isPlus();
+
+ /** Insert a cut into this node. If the given hyperplane intersects
+ * this node's region, then the node's cut is set to the {@link ConvexSubHyperplane}
+ * representing the intersection, new plus and minus child leaf nodes
+ * are assigned, and true is returned. If the hyperplane does not intersect
+ * the node's region, then the node's cut and plus and minus child references
+ * are all set to null (ie, it becomes a leaf node) and false is returned. In
+ * either case, any existing cut and/or child nodes are removed by this method.
+ * @param cutter the hyperplane to cut the node's region with
+ * @return true if the cutting hyperplane intersected the node's region, resulting
+ * in the creation of new child nodes
+ */
+ boolean insertCut(Hyperplane<P> cutter);
+
+ /** Remove the cut from this node. Returns true if the node previously had a cut.
+ * @return true if the node had a cut before the call to this method
+ */
+ boolean clearCut();
+
+ /** Cut this node with the given hyperplane. The same node is returned, regardless of
+ * the outcome of the cut operation. If the operation succeeded, then the node will
+ * have plus and minus child nodes.
+ * @param cutter the hyperplane to cut the node's region with
+ * @return this node
+ * @see #insertCut(Hyperplane)
+ */
+ N cut(Hyperplane<P> cutter);
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java
new file mode 100644
index 0000000..b5df252
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java
@@ -0,0 +1,118 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTree.Node;
+
+/** Internal class for creating simple string representations of BSP trees.
+ * @param <P> Point implementation type
+ * @param <N> Node implementation type
+ */
+final class BSPTreePrinter<P extends Point<P>, N extends Node<P, N>>
+ implements BSPTreeVisitor<P, N> {
+
+ /** Line indent string. */
+ private static final String INDENT = " ";
+
+ /** New line character. */
+ private static final String NEW_LINE = "\n";
+
+ /** Entry prefix for nodes on the minus side of their parent. */
+ private static final String MINUS_CHILD = "[-] ";
+
+ /** Entry prefix for nodes on the plus side of their parent. */
+ private static final String PLUS_CHILD = "[+] ";
+
+ /** Ellipsis for truncated representations. */
+ private static final String ELLIPSIS = "...";
+
+ /** Maximum depth of nodes that will be printed. */
+ private final int maxDepth;
+
+ /** Contains the string output. */
+ private final StringBuilder output = new StringBuilder();
+
+ /** Simple constructor.
+ * @param maxDepth maximum depth of nodes to be printed
+ */
+ BSPTreePrinter(final int maxDepth) {
+ this.maxDepth = maxDepth;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visit(final N node) {
+ final int depth = node.depth();
+
+ if (depth <= maxDepth) {
+ startLine(node);
+ writeNode(node);
+ } else if (depth == maxDepth + 1 && node.isPlus()) {
+ startLine(node);
+ write(ELLIPSIS);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(final N node) {
+ return Order.NODE_MINUS_PLUS;
+ }
+
+ /** Start a line for the given node.
+ * @param node the node to begin a line for
+ */
+ private void startLine(final N node) {
+ if (node.getParent() != null) {
+ write(NEW_LINE);
+ }
+
+ final int depth = node.depth();
+ for (int i = 0; i < depth; ++i) {
+ write(INDENT);
+ }
+ }
+
+ /** Writes the given node to the output.
+ * @param node the node to write
+ */
+ private void writeNode(final N node) {
+ if (node.getParent() != null) {
+ if (node.isMinus()) {
+ write(MINUS_CHILD);
+ } else {
+ write(PLUS_CHILD);
+ }
+ }
+
+ write(node.toString());
+ }
+
+ /** Add the given string to the output.
+ * @param str the string to add
+ */
+ private void write(String str) {
+ output.append(str);
+ }
+
+ /** Return the string representation of the visited tree. */
+ @Override
+ public String toString() {
+ return output.toString();
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java
new file mode 100644
index 0000000..8fc0ffd
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java
@@ -0,0 +1,173 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Interface for visiting the nodes in a {@link BSPTree} or {@link BSPSubtree}.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+@FunctionalInterface
+public interface BSPTreeVisitor<P extends Point<P>, N extends BSPTree.Node<P, N>> {
+
+ /** Enum used to specify the order in which visitors should visit the nodes
+ * in the tree.
+ */
+ enum Order {
+
+ /** Indicates that the visitor should first visit the plus sub-tree, then
+ * the minus sub-tree and then the current node.
+ */
+ PLUS_MINUS_NODE,
+
+ /** Indicates that the visitor should first visit the plus sub-tree, then
+ * the current node, and then the minus sub-tree.
+ */
+ PLUS_NODE_MINUS,
+
+ /** Indicates that the visitor should first visit the minus sub-tree, then
+ * the plus sub-tree, and then the current node.
+ */
+ MINUS_PLUS_NODE,
+
+ /** Indicates that the visitor should first visit the minus sub-tree, then the
+ * current node, and then the plus sub-tree.
+ */
+ MINUS_NODE_PLUS,
+
+ /** Indicates that the visitor should first visit the current node, then the
+ * plus sub-tree, and then the minus sub-tree.
+ */
+ NODE_PLUS_MINUS,
+
+ /** Indicates that the visitor should first visit the current node, then the
+ * minus sub-tree, and then the plus sub-tree.
+ */
+ NODE_MINUS_PLUS;
+ }
+
+ /** Visit a node in a BSP tree. This method is called for both internal nodes and
+ * leaf nodes.
+ * @param node the node being visited
+ */
+ void visit(N node);
+
+ /** Determine the visit order for the given internal node. This is called for each
+ * internal node before {@link #visit(BSPTree.Node)} is called. Returning null from
+ * this method skips the subtree rooted at the given node. This method is not called
+ * on leaf nodes.
+ * @param internalNode the internal node to determine the visit order for
+ * @return the order that the subtree rooted at the given node should be visited
+ */
+ default Order visitOrder(final N internalNode) {
+ return Order.NODE_MINUS_PLUS;
+ }
+
+ /** Abstract class for {@link BSPTreeVisitor} implementations that base their visit
+ * ordering on a target point.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ abstract class TargetPointVisitor<P extends Point<P>, N extends BSPTree.Node<P, N>>
+ implements BSPTreeVisitor<P, N>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190504L;
+
+ /** Point serving as the target of the traversal. */
+ private final P target;
+
+ /** Simple constructor.
+ * @param target the point serving as the target for the tree traversal
+ */
+ public TargetPointVisitor(final P target) {
+ this.target = target;
+ }
+
+ /** Get the target point for the tree traversal.
+ * @return the target point for the tree traversal
+ */
+ public P getTarget() {
+ return target;
+ }
+ }
+
+ /** {@link BSPTreeVisitor} base class that orders tree nodes so that nodes closest to the target point are
+ * visited first. This is done by choosing {@link Order#MINUS_NODE_PLUS}
+ * when the target point lies on the minus side of the node's cut hyperplane and {@link Order#PLUS_NODE_MINUS}
+ * when it lies on the plus side. The order {@link Order#MINUS_NODE_PLUS} order is used when
+ * the target point lies directly on the node's cut hyerplane and no child node is closer than the other.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ abstract class ClosestFirstVisitor<P extends Point<P>, N extends BSPTree.Node<P, N>>
+ extends TargetPointVisitor<P, N> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190504L;
+
+ /** Simple constructor.
+ * @param target the point serving as the target for the traversal
+ */
+ public ClosestFirstVisitor(final P target) {
+ super(target);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(N node) {
+ if (node.getCutHyperplane().offset(getTarget()) > 0.0) {
+ return Order.PLUS_NODE_MINUS;
+ }
+ return Order.MINUS_NODE_PLUS;
+ }
+ }
+
+ /** {@link BSPTreeVisitor} base class that orders tree nodes so that nodes farthest from the target point
+ * are traversed first. This is done by choosing {@link Order#PLUS_NODE_MINUS}
+ * when the target point lies on the minus side of the node's cut hyperplane and {@link Order#MINUS_NODE_PLUS}
+ * when it lies on the plus side. The order {@link Order#MINUS_NODE_PLUS} order is used when
+ * the target point lies directly on the node's cut hyerplane and no child node is closer than the other.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+ abstract class FarthestFirstVisitor<P extends Point<P>, N extends BSPTree.Node<P, N>>
+ extends TargetPointVisitor<P, N> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190504L;
+
+ /** Simple constructor.
+ * @param target the point serving as the target for the traversal
+ */
+ public FarthestFirstVisitor(final P target) {
+ super(target);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(N node) {
+ if (node.getCutHyperplane().offset(getTarget()) < 0.0) {
+ return Order.PLUS_NODE_MINUS;
+ }
+ return Order.MINUS_NODE_PLUS;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundary.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundary.java
new file mode 100644
index 0000000..a165381
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundary.java
@@ -0,0 +1,109 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** Class representing the portion of an
+ * {@link AbstractRegionBSPTree.AbstractRegionNode AbstractRegionNode}'s cut subhyperplane that
+ * lies on the boundary of the region. Portions of this subhyperplane
+ * may be oriented so that the plus side of the subhyperplane points toward
+ * the outside of the region ({@link #getOutsideFacing()}) and other portions
+ * of the same subhyperplane may be oriented so that the plus side points
+ * toward the inside of the region ({@link #getInsideFacing()}).
+ *
+ * @param <P> Point implementation type
+ */
+public final class RegionCutBoundary<P extends Point<P>> implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190310L;
+
+ /** Portion of the region cut subhyperplane with its plus side facing the
+ * inside of the region.
+ */
+ private final SubHyperplane<P> insideFacing;
+
+ /** Portion of the region cut subhyperplane with its plus side facing the
+ * outside of the region.
+ */
+ private final SubHyperplane<P> outsideFacing;
+
+ /** Simple constructor.
+ * @param insideFacing portion of the region cut subhyperplane with its plus side facing the
+ * inside of the region
+ * @param outsideFacing portion of the region cut subhyperplane with its plus side facing the
+ * outside of the region
+ */
+ public RegionCutBoundary(final SubHyperplane<P> insideFacing, final SubHyperplane<P> outsideFacing) {
+ this.insideFacing = insideFacing;
+ this.outsideFacing = outsideFacing;
+ }
+
+ /** Get the portion of the region cut subhyperplane with its plus side facing the
+ * inside of the region.
+ * @return the portion of the region cut subhyperplane with its plus side facing the
+ * inside of the region
+ */
+ public SubHyperplane<P> getInsideFacing() {
+ return insideFacing;
+ }
+
+ /** Get the portion of the region cut subhyperplane with its plus side facing the
+ * outside of the region.
+ * @return the portion of the region cut subhyperplane with its plus side facing the
+ * outside of the region
+ */
+ public SubHyperplane<P> getOutsideFacing() {
+ return outsideFacing;
+ }
+
+ /** Return the closest point to the argument in the inside and outside facing
+ * portions of the cut boundary.
+ * @param pt the reference point
+ * @return the point in the cut boundary closest to the reference point
+ * @see SubHyperplane#closest(Point)
+ */
+ public P closest(final P pt) {
+ final P insideFacingPt = (insideFacing != null) ? insideFacing.closest(pt) : null;
+ final P outsideFacingPt = (outsideFacing != null) ? outsideFacing.closest(pt) : null;
+
+ if (insideFacingPt != null && outsideFacingPt != null) {
+ if (pt.distance(insideFacingPt) < pt.distance(outsideFacingPt)) {
+ return insideFacingPt;
+ }
+ return outsideFacingPt;
+ } else if (insideFacingPt != null) {
+ return insideFacingPt;
+ }
+ return outsideFacingPt;
+ }
+
+ /** Return true if the given point is contained in the boundary, in either the
+ * inside facing portion or the outside facing portion.
+ * @param pt point to test
+ * @return true if the point is contained in the boundary
+ * @see SubHyperplane#contains(Point)
+ */
+ public boolean contains(final P pt) {
+ return (insideFacing != null && insideFacing.contains(pt)) ||
+ (outsideFacing != null && outsideFacing.contains(pt));
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/package-info.java
similarity index 62%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/package-info.java
index 046defe..b94442a 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/package-info.java
@@ -14,23 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partitioning;
-
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+/**
+ *
+ * <p>
+ * This package contains classes related to Binary Space Partitioning (BSP) trees. BSP
+ * tree are data structures that allow arbitrary partitioning of spaces using hyperplanes.
+ * </p>
*/
-public enum Side {
-
- /** Code for the plus side of the hyperplane. */
- PLUS,
-
- /** Code for the minus side of the hyperplane. */
- MINUS,
-
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
-
- /** Code for the hyperplane itself. */
- HYPER;
-
-}
+package org.apache.commons.geometry.core.partitioning.bsp;
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
index cfb55c0..b0e4967 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
@@ -16,99 +16,8 @@
*/
/**
*
- * This package provides classes to implement Binary Space Partition trees.
- *
* <p>
- * {@link org.apache.commons.geometry.core.partitioning.BSPTree BSP trees}
- * are an efficient way to represent parts of space and in particular
- * polytopes (line segments in 1D, polygons in 2D and polyhedrons in 3D)
- * and to operate on them. The main principle is to recursively subdivide
- * the space using simple hyperplanes (points in 1D, lines in 2D, planes
- * in 3D).
+ * This package contains code related to partitioning of spaces by hyperplanes.
* </p>
- *
- * <p>
- * We start with a tree composed of a single node without any cut
- * hyperplane: it represents the complete space, which is a convex
- * part. If we add a cut hyperplane to this node, this represents a
- * partition with the hyperplane at the node level and two half spaces at
- * each side of the cut hyperplane. These half-spaces are represented by
- * two child nodes without any cut hyperplanes associated, the plus child
- * which represents the half space on the plus side of the cut hyperplane
- * and the minus child on the other side. Continuing the subdivisions, we
- * end up with a tree having internal nodes that are associated with a
- * cut hyperplane and leaf nodes without any hyperplane which correspond
- * to convex parts.
- * </p>
- *
- * <p>
- * When BSP trees are used to represent polytopes, the convex parts are
- * known to be completely inside or outside the polytope as long as there
- * is no facet in the part (which is obviously the case if the cut
- * hyperplanes have been chosen as the underlying hyperplanes of the
- * facets (this is called an autopartition) and if the subdivision
- * process has been continued until all facets have been processed. It is
- * important to note that the polytope is <em>not</em> defined by a
- * single part, but by several convex ones. This is the property that
- * allows BSP-trees to represent non-convex polytopes despites all parts
- * are convex. The {@link
- * org.apache.commons.geometry.core.partitioning.Region Region} class is
- * devoted to this representation, it is build on top of the {@link
- * org.apache.commons.geometry.core.partitioning.BSPTree BSPTree} class using
- * boolean objects as the leaf nodes attributes to represent the
- * inside/outside property of each leaf part, and also adds various
- * methods dealing with boundaries (i.e. the separation between the
- * inside and the outside parts).
- * </p>
- *
- * <p>
- * Rather than simply associating the internal nodes with an hyperplane,
- * we consider <em>sub-hyperplanes</em> which correspond to the part of
- * the hyperplane that is inside the convex part defined by all the
- * parent nodes (this implies that the sub-hyperplane at root node is in
- * fact a complete hyperplane, because there is no parent to bound
- * it). Since the parts are convex, the sub-hyperplanes are convex, in
- * 3D the convex parts are convex polyhedrons, and the sub-hyperplanes
- * are convex polygons that cut these polyhedrons in two
- * sub-polyhedrons. Using this definition, a BSP tree completely
- * partitions the space. Each point either belongs to one of the
- * sub-hyperplanes in an internal node or belongs to one of the leaf
- * convex parts.
- * </p>
- *
- * <p>
- * In order to determine where a point is, it is sufficient to check its
- * position with respect to the root cut hyperplane, to select the
- * corresponding child tree and to repeat the procedure recursively,
- * until either the point appears to be exactly on one of the hyperplanes
- * in the middle of the tree or to be in one of the leaf parts. For
- * this operation, it is sufficient to consider the complete hyperplanes,
- * there is no need to check the points with the boundary of the
- * sub-hyperplanes, because this check has in fact already been realized
- * by the recursive descent in the tree. This is very easy to do and very
- * efficient, especially if the tree is well balanced (the cost is
- * <code>O(log(n))</code> where <code>n</code> is the number of facets)
- * or if the first tree levels close to the root discriminate large parts
- * of the total space.
- * </p>
- *
- * <p>
- * One of the main sources for the development of this package was Bruce
- * Naylor, John Amanatides and William Thibault paper <a
- * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf">Merging
- * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph '90,
- * Computer Graphics 24(4), August 1990, pp 115-124, published by the
- * Association for Computing Machinery (ACM). The same paper can also be
- * found <a
- * href="http://www.cs.utexas.edu/users/fussell/courses/cs384g/bsp_treemerge.pdf">here</a>.
- * </p>
- *
- * <p>
- * Note that the interfaces defined in this package are <em>not</em> intended to
- * be implemented by Apache Commons Math users, they are only intended to be
- * implemented within the library itself. New methods may be added even for
- * minor versions, which breaks compatibility for external implementations.
- * </p>
- *
*/
package org.apache.commons.geometry.core.partitioning;
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/DoublePrecisionContext.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/DoublePrecisionContext.java
index 6773837..7ca33ab 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/DoublePrecisionContext.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/DoublePrecisionContext.java
@@ -23,7 +23,7 @@
*/
public abstract class DoublePrecisionContext implements Comparator<Double>, Serializable {
- /** Serializable identifier */
+ /** Serializable identifier. */
private static final long serialVersionUID = 20190121L;
/** Return true if the given values are considered equal to each other.
@@ -85,6 +85,22 @@
return compare(a, b) >= 0;
}
+ /** Return the sign of the argument: 0 if the value is considered equal to
+ * zero, -1 if less than 0, and +1 if greater than 0.
+ * @param a number to determine the sign of
+ * @return 0 if the number is considered equal to 0, -1 if less than
+ * 0, and +1 if greater than 0
+ */
+ public int sign(final double a) {
+ final int cmp = compare(a, 0.0);
+ if (cmp < 0) {
+ return -1;
+ } else if (cmp > 0) {
+ return 1;
+ }
+ return 0;
+ }
+
/** {@inheritDoc} */
@Override
public int compare(final Double a, final Double b) {
@@ -110,7 +126,7 @@
* first is smaller than the second, {@code 1} is the first is larger
* than the second or either value is NaN.
*/
- public abstract int compare(final double a, final double b);
+ public abstract int compare(double a, double b);
/** Get the largest positive double value that is still considered equal
* to zero by this instance.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContext.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContext.java
index c26f502..0d69c8b 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContext.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContext.java
@@ -30,7 +30,7 @@
*/
public class EpsilonDoublePrecisionContext extends DoublePrecisionContext {
- /** Serializable identifier */
+ /** Serializable identifier. */
private static final long serialVersionUID = 20190119L;
/** Epsilon value. */
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
new file mode 100644
index 0000000..f15fcc8
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.core;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestPoint1D;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.junit.Test;
+
+import org.junit.Assert;
+
+public class EmbeddingTest {
+
+ @Test
+ public void testToSubspace_collection_emptyInput() {
+ // arrange
+ TestLine line = TestLine.Y_AXIS;
+
+ // act
+ List<TestPoint1D> result = line.toSubspace(new ArrayList<>());
+
+ // assert
+ Assert.assertEquals(0, result.size());
+ }
+
+ @Test
+ public void testToSubspace_collection() {
+ // arrange
+ List<TestPoint2D> pts = Arrays.asList(
+ new TestPoint2D(0, 0),
+ new TestPoint2D(1, 0.25),
+ new TestPoint2D(0.5, 1)
+ );
+
+ TestLine line = TestLine.Y_AXIS;
+
+ // act
+ List<TestPoint1D> result = line.toSubspace(pts);
+
+ // assert
+ Assert.assertEquals(3, result.size());
+ Assert.assertEquals(0, result.get(0).getX(), PartitionTestUtils.EPS);
+ Assert.assertEquals(0.25, result.get(1).getX(), PartitionTestUtils.EPS);
+ Assert.assertEquals(1, result.get(2).getX(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testToSpace_collection_emptyInput() {
+ // arrange
+ TestLine line = TestLine.Y_AXIS;
+
+ // act
+ List<TestPoint2D> result = line.toSpace(new ArrayList<>());
+
+ // assert
+ Assert.assertEquals(0, result.size());
+ }
+
+ @Test
+ public void testToSpace_collection() {
+ // arrange
+ List<TestPoint1D> pts = Arrays.asList(
+ new TestPoint1D(0),
+ new TestPoint1D(1),
+ new TestPoint1D(0.5)
+ );
+
+ TestLine line = TestLine.Y_AXIS;
+
+ // act
+ List<TestPoint2D> result = line.toSpace(pts);
+
+ // assert
+ Assert.assertEquals(3, result.size());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), result.get(0));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 1), result.get(1));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0.5), result.get(2));
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
index 60bdeea..242e2d9 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
@@ -20,6 +20,7 @@
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.util.regex.Pattern;
import org.junit.Assert;
@@ -50,7 +51,7 @@
* @param exceptionType the expected exception type
*/
public static void assertThrows(Runnable r, Class<?> exceptionType) {
- assertThrows(r, exceptionType, null);
+ assertThrows(r, exceptionType, (String) null);
}
/** Asserts that the given Runnable throws an exception of the given type. If
@@ -77,6 +78,42 @@
}
}
+ /** Asserts that the given Runnable throws an exception of the given type. If
+ * {@code pattern} is not null, the exception message is asserted to match the
+ * given regex.
+ * @param r the Runnable instance
+ * @param exceptionType the expected exception type
+ * @param pattern regex pattern to match; ignored if null
+ */
+ public static void assertThrows(Runnable r, Class<?> exceptionType, Pattern pattern) {
+ try {
+ r.run();
+ Assert.fail("Operation should have thrown an exception");
+ }
+ catch (Exception exc) {
+ Class<?> actualType = exc.getClass();
+
+ Assert.assertTrue("Expected exception of type " + exceptionType.getName() + " but was " + actualType.getName(),
+ exceptionType.isAssignableFrom(actualType));
+
+ if (pattern != null) {
+ String message = exc.getMessage();
+
+ String err = "Expected exception message to match /" + pattern + "/ but was [" + message + "]";
+ Assert.assertTrue(err, pattern.matcher(message).matches());
+ }
+ }
+ }
+
+ /** Assert that a string contains a given substring value.
+ * @param substr
+ * @param actual
+ */
+ public static void assertContains(String substr, String actual) {
+ String msg = "Expected string to contain [" + substr + "] but was [" + actual + "]";
+ Assert.assertTrue(msg, actual.contains(substr));
+ }
+
/**
* Serializes and then recovers an object from a byte array. Returns the deserialized object.
*
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/IteratorTransformTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/IteratorTransformTest.java
new file mode 100644
index 0000000..8eaab17
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/IteratorTransformTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.core.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class IteratorTransformTest {
+
+ @Test
+ public void testIteration() {
+ // arrange
+ List<Integer> input = Arrays.asList(1, 2, 3, 4, 12, 13);
+
+ // act
+ List<String> result = toList(new EvenCharIterator(input.iterator()));
+
+ // assert
+ Assert.assertEquals(Arrays.asList("2", "4", "1", "2"), result);
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testThrowsNoSuchElement() {
+ // arrange
+ List<Integer> input = Arrays.asList();
+ EvenCharIterator it = new EvenCharIterator(input.iterator());
+
+ // act/assert
+ Assert.assertFalse(it.hasNext());
+ it.next();
+ }
+
+ private static <T> List<T> toList(Iterator<T> it) {
+ List<T> result = new ArrayList<>();
+ while (it.hasNext()) {
+ result.add(it.next());
+ }
+
+ return result;
+ }
+
+ private static class EvenCharIterator extends IteratorTransform<Integer, String>{
+
+ public EvenCharIterator(final Iterator<Integer> inputIterator) {
+ super(inputIterator);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void acceptInput(Integer input) {
+ // filter out odd integers
+ int value = input.intValue();
+ if (value % 2 == 0) {
+ char[] chars = (value + "").toCharArray();
+
+ if (chars.length > 1) {
+ List<String> strs = new ArrayList<>();
+ for (char ch : chars) {
+ strs.add(ch + "");
+ }
+
+ addAllOutput(strs);
+ }
+ else if (chars.length == 1) {
+ addOutput(chars[0] + "");
+ }
+ }
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java
new file mode 100644
index 0000000..107e5a7
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java
@@ -0,0 +1,115 @@
+/*
+ * 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.core.partition.test;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTree.Node;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+
+/** Class containing utility methods for tests related to the
+ * partition package.
+ */
+public class PartitionTestUtils {
+
+ public static final double EPS = 1e-6;
+
+ public static final DoublePrecisionContext PRECISION =
+ new EpsilonDoublePrecisionContext(EPS);
+
+ /**
+ * Asserts that corresponding values in the given points are equal.
+ * @param expected
+ * @param actual
+ */
+ public static void assertPointsEqual(TestPoint2D expected, TestPoint2D actual) {
+ String msg = "Expected points to equal " + expected + " but was " + actual + ";";
+ Assert.assertEquals(msg, expected.getX(), actual.getX(), EPS);
+ Assert.assertEquals(msg, expected.getY(), actual.getY(), EPS);
+ }
+
+ public static void assertSegmentsEqual(TestLineSegment expected, TestLineSegment actual) {
+ String msg = "Expected line segment to equal " + expected + " but was " + actual;
+
+ Assert.assertEquals(msg, expected.getStartPoint().getX(),
+ actual.getStartPoint().getX(), EPS);
+ Assert.assertEquals(msg, expected.getStartPoint().getY(),
+ actual.getStartPoint().getY(), EPS);
+
+ Assert.assertEquals(msg, expected.getEndPoint().getX(),
+ actual.getEndPoint().getX(), EPS);
+ Assert.assertEquals(msg, expected.getEndPoint().getY(),
+ actual.getEndPoint().getY(), EPS);
+ }
+
+ public static void assertIsInternalNode(Node<?, ?> node) {
+ Assert.assertNotNull(node.getCut());
+ Assert.assertNotNull(node.getMinus());
+ Assert.assertNotNull(node.getPlus());
+
+ Assert.assertTrue(node.isInternal());
+ Assert.assertFalse(node.isLeaf());
+ }
+
+ public static void assertIsLeafNode(Node<?, ?> node) {
+ Assert.assertNull(node.getCut());
+ Assert.assertNull(node.getMinus());
+ Assert.assertNull(node.getPlus());
+
+ Assert.assertFalse(node.isInternal());
+ Assert.assertTrue(node.isLeaf());
+ }
+
+ /** Assert that the given tree for has a valid, consistent internal structure. This checks that all nodes
+ * in the tree are owned by the tree, that the node depth values are correct, and the cut nodes have children
+ * and non-cut nodes do not.
+ * @param tree tree to check
+ */
+ public static <P extends Point<P>, N extends BSPTree.Node<P, N>> void assertTreeStructure(final BSPTree<P, N> tree) {
+ assertTreeStructureRecursive(tree, tree.getRoot(), 0);
+ }
+
+ /** Recursive method to assert that a tree has a valid internal structure.
+ * @param tree tree to check
+ * @param node node to check
+ * @param expectedDepth the expected depth of the node in the tree
+ */
+ private static <P extends Point<P>, N extends BSPTree.Node<P, N>> void assertTreeStructureRecursive(
+ final BSPTree<P, N> tree, final BSPTree.Node<P, N> node, final int expectedDepth) {
+
+ Assert.assertSame("Node has an incorrect owning tree", tree, node.getTree());
+ Assert.assertEquals("Node has an incorrect depth property", node.depth(), expectedDepth);
+
+ if (node.getCut() == null) {
+ String msg = "Node without cut cannot have children";
+
+ Assert.assertNull(msg, node.getMinus());
+ Assert.assertNull(msg, node.getPlus());
+ }
+ else {
+ String msg = "Node with cut must have children";
+
+ Assert.assertNotNull(msg, node.getMinus());
+ Assert.assertNotNull(msg, node.getPlus());
+
+ assertTreeStructureRecursive(tree, node.getPlus(), expectedDepth + 1);
+ assertTreeStructureRecursive(tree, node.getMinus(), expectedDepth + 1);
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java
new file mode 100644
index 0000000..b04ca59
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java
@@ -0,0 +1,64 @@
+/*
+ * 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.core.partition.test;
+
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
+
+/** BSP Tree implementation class for testing purposes.
+ */
+public class TestBSPTree extends AbstractBSPTree<TestPoint2D, TestBSPTree.TestNode> {
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 20190225L;
+
+ /** {@inheritDoc} */
+ @Override
+ protected TestNode createNode() {
+ return new TestNode(this);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>Exposed as public for testing.</p>
+ */
+ @Override
+ public void splitIntoTrees(Hyperplane<TestPoint2D> splitter,
+ final AbstractBSPTree<TestPoint2D, TestBSPTree.TestNode> minus,
+ final AbstractBSPTree<TestPoint2D, TestBSPTree.TestNode> plus) {
+
+ super.splitIntoTrees(splitter, minus, plus);
+ }
+
+ /** BSP Tree node class for {@link TestBSPTree}.
+ */
+ public static class TestNode extends AbstractBSPTree.AbstractNode<TestPoint2D,TestNode> {
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 20190225L;
+
+ public TestNode(AbstractBSPTree<TestPoint2D, TestNode> tree) {
+ super(tree);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected TestNode getSelf() {
+ return this;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java
new file mode 100644
index 0000000..8a19ed5
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java
@@ -0,0 +1,280 @@
+/*
+ * 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.core.partition.test;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+
+/** Class representing a line in two dimensional Euclidean space. This
+ * class should only be used for testing purposes.
+ */
+public class TestLine implements EmbeddingHyperplane<TestPoint2D, TestPoint1D>, Serializable {
+
+ /** Line pointing along the positive x-axis. */
+ public static final TestLine X_AXIS = new TestLine(0, 0, 1, 0);
+
+ /** Line pointing along the positive y-axis. */
+ public static final TestLine Y_AXIS = new TestLine(0, 0, 0, 1);
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 20190224L;
+
+ /** X value of the normalized line direction. */
+ private final double directionX;
+
+ /** Y value of the normalized line direction. */
+ private final double directionY;
+
+ /** The distance between the origin and the line. */
+ private final double originOffset;
+
+ /** Construct a line from two points. The line points in the direction from
+ * {@code p1} to {@code p2}.
+ * @param p1 first point
+ * @param p2 second point
+ */
+ public TestLine(final TestPoint2D p1, final TestPoint2D p2) {
+ this(p1.getX(), p1.getY(), p2.getX(), p2.getY());
+ }
+
+ /** Construct a line from two point, given by their components.
+ * @param x1 x coordinate of first point
+ * @param y1 y coordinate of first point
+ * @param x2 x coordinate of second point
+ * @param y2 y coordinate of second point
+ */
+ public TestLine(final double x1, final double y1, final double x2, final double y2) {
+ double vecX = x2 - x1;
+ double vecY = y2 - y1;
+
+ double norm = norm(vecX, vecY);
+
+ vecX /= norm;
+ vecY /= norm;
+
+ if (!Double.isFinite(vecX) || !Double.isFinite(vecY)) {
+ throw new IllegalStateException("Unable to create line between points: (" +
+ x1 + ", " + y1 + "), (" + x2 + ", " + y2 + ")");
+ }
+
+ this.directionX = vecX;
+ this.directionY = vecY;
+
+ this.originOffset = signedArea(vecX, vecY, x1, y1);
+ }
+
+ /** Get the line origin, meaning the projection of the 2D origin onto the line.
+ * @return line origin
+ */
+ public TestPoint2D getOrigin() {
+ return toSpace(0);
+ }
+
+ /** Get the x component of the line direction.
+ * @return x component of the line direction.
+ */
+ public double getDirectionX() {
+ return directionX;
+ }
+
+ /** Get the y component of the line direction.
+ * @return y component of the line direction.
+ */
+ public double getDirectionY() {
+ return directionY;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double offset(TestPoint2D point) {
+ return originOffset - signedArea(directionX, directionY, point.getX(), point.getY());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public HyperplaneLocation classify(TestPoint2D point) {
+ final double offset = offset(point);
+ final double cmp = PartitionTestUtils.PRECISION.compare(offset, 0.0);
+ if (cmp == 0) {
+ return HyperplaneLocation.ON;
+ }
+ return cmp < 0 ? HyperplaneLocation.MINUS : HyperplaneLocation.PLUS;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(TestPoint2D point) {
+ return classify(point) == HyperplaneLocation.ON;
+ }
+
+ /** Get the location of the given 2D point in the 1D space of the line.
+ * @param point point to project into the line's 1D space
+ * @return location of the point in the line's 1D space
+ */
+ public double toSubspaceValue(TestPoint2D point) {
+ return (directionX * point.getX()) + (directionY * point.getY());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestPoint1D toSubspace(TestPoint2D point) {
+ return new TestPoint1D(toSubspaceValue(point));
+ }
+
+ /** Get the 2D location of the given 1D location in the line's 1D space.
+ * @param abscissa location in the line's 1D space.
+ * @return the location of the given 1D point in 2D space
+ */
+ public TestPoint2D toSpace(final double abscissa) {
+ if (Double.isInfinite(abscissa)) {
+ int dirXCmp = PartitionTestUtils.PRECISION.sign(directionX);
+ int dirYCmp = PartitionTestUtils.PRECISION.sign(directionY);
+
+ double x;
+ if (dirXCmp == 0) {
+ // vertical line
+ x = getOrigin().getX();
+ }
+ else {
+ x = (dirXCmp < 0 ^ abscissa < 0) ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
+ }
+
+ double y;
+ if (dirYCmp == 0) {
+ // horizontal line
+ y = getOrigin().getY();
+ }
+ else {
+ y = (dirYCmp < 0 ^ abscissa < 0) ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
+ }
+
+ return new TestPoint2D(x, y);
+ }
+
+ final double ptX = (abscissa * directionX) + (-originOffset * directionY);
+ final double ptY = (abscissa * directionY) + (originOffset * directionX);
+
+ return new TestPoint2D(ptX, ptY);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestPoint2D toSpace(TestPoint1D point) {
+ return toSpace(point.getX());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestPoint2D project(final TestPoint2D point) {
+ return toSpace(toSubspaceValue(point));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestLine reverse() {
+ TestPoint2D pt = getOrigin();
+ return new TestLine(pt.getX(), pt.getY(), pt.getX() - directionX, pt.getY() - directionY);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestLine transform(Transform<TestPoint2D> transform) {
+ TestPoint2D p1 = transform.apply(toSpace(0));
+ TestPoint2D p2 = transform.apply(toSpace(1));
+
+ return new TestLine(p1, p2);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean similarOrientation(Hyperplane<TestPoint2D> other) {
+ final TestLine otherLine = (TestLine) other;
+ final double dot = (directionX * otherLine.directionX) + (directionY * otherLine.directionY);
+ return dot >= 0.0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestLineSegment span() {
+ return new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, this);
+ }
+
+ /** Get the intersection point of the instance and another line.
+ * @param other other line
+ * @return intersection point of the instance and the other line
+ * or null if there is no unique intersection point (ie, the lines
+ * are parallel or coincident)
+ */
+ public TestPoint2D intersection(final TestLine other) {
+ final double area = signedArea(directionX, directionY, other.directionX, other.directionY);
+ if (PartitionTestUtils.PRECISION.eqZero(area)) {
+ // lines are parallel
+ return null;
+ }
+
+ final double x = ((other.directionX * originOffset) +
+ (-directionX * other.originOffset)) / area;
+
+ final double y = ((other.directionY * originOffset) +
+ (-directionY * other.originOffset)) / area;
+
+ return new TestPoint2D(x, y);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[origin= ")
+ .append(getOrigin())
+ .append(", direction= (")
+ .append(directionX)
+ .append(", ")
+ .append(directionY)
+ .append(")]");
+
+ return sb.toString();
+ }
+
+ /** Compute the signed area of the parallelogram with sides defined by the given
+ * vectors.
+ * @param x1 x coordinate of first vector
+ * @param y1 y coordinate of first vector
+ * @param x2 x coordinate of second vector
+ * @param y2 y coordinate of second vector
+ * @return the signed are of the parallelogram with side defined by the given
+ * vectors
+ */
+ private static double signedArea(final double x1, final double y1,
+ final double x2, final double y2) {
+ return (x1 * y2) + (-y1 * x2);
+ }
+
+ /** Compute the Euclidean norm.
+ * @param x x coordinate value
+ * @param y y coordinate value
+ * @return Euclidean norm
+ */
+ public static double norm(final double x, final double y) {
+ return Math.sqrt((x * x) + (y * y));
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java
new file mode 100644
index 0000000..aa20dd7
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java
@@ -0,0 +1,340 @@
+/*
+ * 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.core.partition.test;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** Class representing a line segment in two dimensional Euclidean space. This
+ * class should only be used for testing purposes.
+ */
+public class TestLineSegment implements ConvexSubHyperplane<TestPoint2D>, Serializable {
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 20190224L;
+
+ /** Abscissa of the line segment start point. */
+ private final double start;
+
+ /** Abscissa of the line segment end point. */
+ private final double end;
+
+ /** The underlying line for the line segment. */
+ private final TestLine line;
+
+ /** Construct a line segment between two points.
+ * @param start start point
+ * @param end end point
+ */
+ public TestLineSegment(final TestPoint2D start, final TestPoint2D end) {
+ this.line = new TestLine(start, end);
+
+ final double startValue = line.toSubspaceValue(start);
+ final double endValue = line.toSubspaceValue(end);
+
+ this.start = Math.min(startValue, endValue);
+ this.end = Math.max(startValue, endValue);
+ }
+
+ /** Construct a line segment between two points.
+ * @param x1 x coordinate of first point
+ * @param y1 y coordinate of first point
+ * @param x2 x coordinate of second point
+ * @param y2 y coordinate of second point
+ */
+ public TestLineSegment(final double x1, final double y1, final double x2, final double y2) {
+ this(new TestPoint2D(x1, y1), new TestPoint2D(x2, y2));
+ }
+
+ /** Construct a line segment based on an existing line.
+ * @param start abscissa of the line segment start point
+ * @param end abscissa of the line segment end point
+ * @param line the underyling line
+ */
+ public TestLineSegment(final double start, final double end, final TestLine line) {
+ this.start = Math.min(start, end);
+ this.end = Math.max(start, end);
+ this.line = line;
+ }
+
+ /** Get the start abscissa value.
+ * @return
+ */
+ public double getStart() {
+ return start;
+ }
+
+ /** Get the end abscissa value.
+ * @return
+ */
+ public double getEnd() {
+ return end;
+ }
+
+ /** Get the start point of the line segment.
+ * @return the start point of the line segment
+ */
+ public TestPoint2D getStartPoint() {
+ return line.toSpace(start);
+ }
+
+ /** Get the end point of the line segment.
+ * @return the end point of the line segment
+ */
+ public TestPoint2D getEndPoint() {
+ return line.toSpace(end);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestLine getHyperplane() {
+ return line;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFull() {
+ return start < end && Double.isInfinite(start) && Double.isInfinite(end);
+
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ return PartitionTestUtils.PRECISION.eqZero(getSize());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return Double.isInfinite(getSize());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return !isInfinite();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return Math.abs(start - end);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionLocation classify(TestPoint2D point) {
+ if (line.contains(point)) {
+ final double value = line.toSubspaceValue(point);
+
+ final int startCmp = PartitionTestUtils.PRECISION.compare(value, start);
+ final int endCmp = PartitionTestUtils.PRECISION.compare(value, end);
+
+ if (startCmp == 0 || endCmp == 0) {
+ return RegionLocation.BOUNDARY;
+ }
+ else if (startCmp > 0 && endCmp < 0) {
+ return RegionLocation.INSIDE;
+ }
+ }
+
+ return RegionLocation.OUTSIDE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestPoint2D closest(TestPoint2D point) {
+ double value = line.toSubspaceValue(point);
+ value = Math.max(Math.min(value, end), start);
+
+ return line.toSpace(value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<ConvexSubHyperplane<TestPoint2D>> toConvex() {
+ return Arrays.asList(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestLineSegment reverse() {
+ TestLine rLine = line.reverse();
+ return new TestLineSegment(-end, -start, rLine);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<TestLineSegment> split(Hyperplane<TestPoint2D> splitter) {
+ final TestLine splitterLine = (TestLine) splitter;
+
+ if (isInfinite()) {
+ return splitInfinite(splitterLine);
+ }
+ return splitFinite(splitterLine);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubHyperplane.Builder<TestPoint2D> builder() {
+ return new TestLineSegmentCollectionBuilder(line);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConvexSubHyperplane<TestPoint2D> transform(Transform<TestPoint2D> transform) {
+ if (!isInfinite()) {
+ // simple case; just transform the points directly
+ TestPoint2D p1 = transform.apply(getStartPoint());
+ TestPoint2D p2 = transform.apply(getEndPoint());
+
+ return new TestLineSegment(p1, p2);
+ }
+
+ // determine how the line has transformed
+ TestPoint2D p0 = transform.apply(line.toSpace(0));
+ TestPoint2D p1 = transform.apply(line.toSpace(1));
+
+ TestLine tLine = new TestLine(p0, p1);
+ double translation = tLine.toSubspaceValue(p0);
+ double scale = tLine.toSubspaceValue(p1);
+
+ double tStart = (start * scale) + translation;
+ double tEnd = (end * scale) + translation;
+
+ return new TestLineSegment(tStart, tEnd, tLine);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[start= ")
+ .append(getStartPoint())
+ .append(", end= ")
+ .append(getEndPoint())
+ .append("]");
+
+ return sb.toString();
+ }
+
+ /** Method used to split the instance with the given line when the instance has
+ * infinite size.
+ * @param splitter the splitter line
+ * @return the split convex subhyperplane
+ */
+ private Split<TestLineSegment> splitInfinite(TestLine splitter) {
+ final TestPoint2D intersection = splitter.intersection(line);
+
+ if (intersection == null) {
+ // the lines are parallel
+ final double originOffset = splitter.offset(line.getOrigin());
+
+ final int sign = PartitionTestUtils.PRECISION.sign(originOffset);
+ if (sign < 0) {
+ return new Split<TestLineSegment>(this, null);
+ }
+ else if (sign > 0) {
+ return new Split<TestLineSegment>(null, this);
+ }
+ return new Split<TestLineSegment>(null, null);
+ }
+ else {
+ // the lines intersect
+ final double intersectionAbscissa = line.toSubspaceValue(intersection);
+
+ TestLineSegment startSegment = null;
+ TestLineSegment endSegment = null;
+
+ if (start < intersectionAbscissa) {
+ startSegment = new TestLineSegment(start, intersectionAbscissa, line);
+ }
+ if (intersectionAbscissa < end) {
+ endSegment = new TestLineSegment(intersectionAbscissa, end, line);
+ }
+
+ final double startOffset = splitter.offset(line.toSpace(intersectionAbscissa - 1));
+ final double startCmp = PartitionTestUtils.PRECISION.sign(startOffset);
+
+ final TestLineSegment minus = (startCmp > 0) ? endSegment: startSegment;
+ final TestLineSegment plus = (startCmp > 0) ? startSegment : endSegment;
+
+ return new Split<TestLineSegment>(minus, plus);
+ }
+ }
+
+ /** Method used to split the instance with the given line when the instance has
+ * finite size.
+ * @param splitter the splitter line
+ * @return the split convex subhyperplane
+ */
+ private Split<TestLineSegment> splitFinite(TestLine splitter) {
+
+ final double startOffset = splitter.offset(line.toSpace(start));
+ final double endOffset = splitter.offset(line.toSpace(end));
+
+ final int startCmp = PartitionTestUtils.PRECISION.sign(startOffset);
+ final int endCmp = PartitionTestUtils.PRECISION.sign(endOffset);
+
+ // startCmp | endCmp | result
+ // --------------------------------
+ // 0 | 0 | hyper
+ // 0 | < 0 | minus
+ // 0 | > 0 | plus
+ // < 0 | 0 | minus
+ // < 0 | < 0 | minus
+ // < 0 | > 0 | SPLIT
+ // > 0 | 0 | plus
+ // > 0 | < 0 | SPLIT
+ // > 0 | > 0 | plus
+
+ if (startCmp == 0 && endCmp == 0) {
+ // the entire line segment is directly on the splitter line
+ return new Split<TestLineSegment>(null, null);
+ }
+ else if (startCmp < 1 && endCmp < 1) {
+ // the entire line segment is on the minus side
+ return new Split<TestLineSegment>(this, null);
+ }
+ else if (startCmp > -1 && endCmp > -1) {
+ // the entire line segment is on the plus side
+ return new Split<TestLineSegment>(null, this);
+ }
+
+ // we need to split the line
+ final TestPoint2D intersection = splitter.intersection(line);
+ final double intersectionAbscissa = line.toSubspaceValue(intersection);
+
+ final TestLineSegment startSegment = new TestLineSegment(start, intersectionAbscissa, line);
+ final TestLineSegment endSegment = new TestLineSegment(intersectionAbscissa, end, line);
+
+ final TestLineSegment minus = (startCmp > 0) ? endSegment: startSegment;
+ final TestLineSegment plus = (startCmp > 0) ? startSegment : endSegment;
+
+ return new Split<TestLineSegment>(minus, plus);
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java
new file mode 100644
index 0000000..a541a5d
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java
@@ -0,0 +1,197 @@
+/*
+ * 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.core.partition.test;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** Class containing a collection line segments. This class should only be used for
+ * testing purposes.
+ */
+public class TestLineSegmentCollection implements SubHyperplane<TestPoint2D>, Serializable {
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 20190303L;
+
+ /** The collection of line-segments making up the subhyperplane.
+ */
+ private final List<TestLineSegment> segments;
+
+ /** Create a new instance with the given line segments. The segments
+ * are all assumed to lie on the same hyperplane.
+ * @param segments the segments to use in the collection
+ * @throws IllegalArgumentException if the collection is null or empty
+ */
+ public TestLineSegmentCollection(List<TestLineSegment> segments) {
+ this.segments = Collections.unmodifiableList(new ArrayList<>(segments));
+ }
+
+ /** Get the list of line segments comprising the collection.
+ * @return the list of line segments in the collection
+ */
+ public List<TestLineSegment> getLineSegments() {
+ return segments;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Hyperplane<TestPoint2D> getHyperplane() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFull() {
+ for (TestLineSegment seg : segments) {
+ if (seg.isFull()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ for (TestLineSegment seg : segments) {
+ if (!seg.isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ for (TestLineSegment seg : segments) {
+ if (seg.isInfinite()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return !isInfinite();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ double size = 0.0;
+
+ for (TestLineSegment seg : segments) {
+ size += seg.getSize();
+ }
+
+ return size;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<TestLineSegmentCollection> split(Hyperplane<TestPoint2D> splitter) {
+ final List<TestLineSegment> minusList = new ArrayList<>();
+ final List<TestLineSegment> plusList = new ArrayList<>();
+
+ for (TestLineSegment segment : segments) {
+ Split<TestLineSegment> split = segment.split(splitter);
+
+ if (split.getMinus() != null) {
+ minusList.add(split.getMinus());
+ }
+
+ if (split.getPlus() != null) {
+ plusList.add(split.getPlus());
+ }
+ }
+
+ final TestLineSegmentCollection minus = minusList.isEmpty() ?
+ null :
+ new TestLineSegmentCollection(minusList);
+
+ final TestLineSegmentCollection plus = plusList.isEmpty() ?
+ null :
+ new TestLineSegmentCollection(plusList);
+
+ return new Split<TestLineSegmentCollection>(minus, plus);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionLocation classify(TestPoint2D point) {
+
+ // simply return the first value that is not outside;
+ // this is decidedly not robust but should work for testing purposes
+ for (TestLineSegment seg : segments) {
+ final RegionLocation loc = seg.classify(point);
+ if (loc != RegionLocation.OUTSIDE) {
+ return loc;
+ }
+ }
+
+ return RegionLocation.OUTSIDE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestPoint2D closest(TestPoint2D point) {
+ TestPoint2D closest = null;
+ double minDist = -1;
+
+ for (TestLineSegment seg : segments) {
+ TestPoint2D pt = seg.closest(point);
+ double dist = pt.distance(point);
+ if (minDist < 0 || dist < minDist) {
+ minDist = dist;
+ closest = pt;
+ }
+ }
+
+ return closest;
+
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<ConvexSubHyperplane<TestPoint2D>> toConvex() {
+ return new ArrayList<>(segments);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubHyperplane<TestPoint2D> transform(Transform<TestPoint2D> transform) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubHyperplane.Builder<TestPoint2D> builder() {
+ return new TestLineSegmentCollectionBuilder(segments.get(0).getHyperplane());
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java
new file mode 100644
index 0000000..8ab0d73
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java
@@ -0,0 +1,101 @@
+/*
+ * 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.core.partition.test;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+public class TestLineSegmentCollectionBuilder implements SubHyperplane.Builder<TestPoint2D> {
+
+ private final TestLine line;
+
+ private final List<SegmentInterval> intervals = new LinkedList<>();
+
+ public TestLineSegmentCollectionBuilder(final TestLine line) {
+ this.line = line;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(SubHyperplane<TestPoint2D> sub) {
+ for (ConvexSubHyperplane<TestPoint2D> convex : sub.toConvex()) {
+ add(convex);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(ConvexSubHyperplane<TestPoint2D> convex) {
+ TestLineSegment seg = (TestLineSegment) convex;
+ addSegment(seg.getStart(), seg.getEnd());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubHyperplane<TestPoint2D> build() {
+ List<TestLineSegment> segments = new ArrayList<>();
+
+ for (SegmentInterval interval : intervals) {
+ segments.add(new TestLineSegment(interval.start, interval.end, line));
+ }
+
+ return new TestLineSegmentCollection(segments);
+ }
+
+ private void addSegment(final double start, final double end) {
+ if (intervals.isEmpty()) {
+ intervals.add(new SegmentInterval(start, end));
+ }
+ else {
+ boolean added = false;
+ SegmentInterval current;
+ for (int i=0; i<intervals.size() && !added; ++i) {
+ current = intervals.get(i);
+
+ if (end < current.start) {
+ intervals.add(i, new SegmentInterval(start, end));
+
+ added = true;
+ }
+ else if (start <= current.end) {
+ current.start = Math.min(current.start, start);
+ current.end = Math.max(current.end, end);
+
+ added = true;
+ }
+ }
+
+ if (!added) {
+ intervals.add(new SegmentInterval(start, end));
+ }
+ }
+ }
+
+ private static class SegmentInterval {
+ public double start;
+ public double end;
+
+ SegmentInterval(final double start, final double end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java
new file mode 100644
index 0000000..7d353f0
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java
@@ -0,0 +1,83 @@
+/*
+ * 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.core.partition.test;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Class representing a point in one dimensional Euclidean space. This
+ * class should only be used for testing purposes.
+ */
+public class TestPoint1D implements Point<TestPoint1D>, Serializable {
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 1L;
+
+ /** X coordinate */
+ private final double x;
+
+ /** Simple constructor.
+ * @param x x coordinate
+ */
+ public TestPoint1D(final double x) {
+ this.x = x;
+ }
+
+ /** Get the x coordinate of the point.
+ * @return the x coordinate of the point
+ */
+ public double getX() {
+ return x;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getDimension() {
+ return 1;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isNaN() {
+ return Double.isNaN(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return Double.isInfinite(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return Double.isFinite(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance(final TestPoint1D p) {
+ return Math.abs(this.x - p.x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "(" + x + ")";
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java
new file mode 100644
index 0000000..e5e9b58
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java
@@ -0,0 +1,107 @@
+/*
+ * 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.core.partition.test;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Class representing a point in two dimensional Euclidean space. This
+ * class should only be used for testing purposes.
+ */
+public class TestPoint2D implements Point<TestPoint2D>, Serializable {
+
+ /** Instance representing the coordinates {@code (0, 0)} */
+ public static final TestPoint2D ZERO = new TestPoint2D(0, 0);
+
+ /** Instance representing the coordinates {@code (1, 0)} */
+ public static final TestPoint2D PLUS_X = new TestPoint2D(1, 0);
+
+ /** Instance representing the coordinates {@code (0, 1)} */
+ public static final TestPoint2D PLUS_Y = new TestPoint2D(0, 1);
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 20190224L;
+
+ /** X coordinate */
+ private final double x;
+
+ /** Y coordinate */
+ private final double y;
+
+ /** Simple constructor.
+ * @param x x coordinate
+ * @param y y coordinate
+ */
+ public TestPoint2D(final double x, final double y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ /** Get the x coordinate value.
+ * @return x coordinate value
+ */
+ public double getX() {
+ return x;
+ }
+
+ /** Get the y coordinate value.
+ * @return y coordinate value
+ */
+ public double getY() {
+ return y;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getDimension() {
+ return 2;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isNaN() {
+ return Double.isNaN(x) || Double.isNaN(y);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return Double.isInfinite(x) || Double.isInfinite(y);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return Double.isFinite(x) && Double.isFinite(y);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance(TestPoint2D p) {
+ final double dx = x - p.x;
+ final double dy = y - p.y;
+
+ return Math.sqrt((dx * dx) + (dy * dy));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "(" + x + ", " + y + ")";
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java
new file mode 100644
index 0000000..2caa2be
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java
@@ -0,0 +1,60 @@
+/*
+ * 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.core.partition.test;
+
+import java.util.function.Function;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Implementation class for 2D {@link Transform}s. This
+ * class should only be used for testing purposes.
+ */
+public class TestTransform2D implements Transform<TestPoint2D> {
+
+ /** Underlying transform function. */
+ private final Function<TestPoint2D, TestPoint2D> fn;
+
+ /** True if the transform preserves the handedness of the space. */
+ private final boolean preservesHandedness;
+
+ /** Create a new instance using the given transform function.
+ * @param fn transform function
+ */
+ public TestTransform2D(final Function<TestPoint2D, TestPoint2D> fn) {
+ this.fn = fn;
+
+ final TestPoint2D tx = fn.apply(TestPoint2D.PLUS_X);
+ final TestPoint2D ty = fn.apply(TestPoint2D.PLUS_Y);
+
+ final double signedArea = (tx.getX() * ty.getY()) -
+ (tx.getY() * ty.getX());
+
+ this.preservesHandedness = signedArea > 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public TestPoint2D apply(final TestPoint2D pt) {
+ return fn.apply(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean preservesOrientation() {
+ return preservesHandedness;
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
new file mode 100644
index 0000000..f957acb
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
@@ -0,0 +1,546 @@
+/*
+ * 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.core.partitioning;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestLineSegment;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partition.test.TestTransform2D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AbstractConvexHyperplaneBoundedRegionTest {
+
+ @Test
+ public void testBoundaries_areUnmodifiable() {
+ // arrange
+ StubRegion region = new StubRegion(new ArrayList<>());
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ region.getBoundaries().add(TestLine.X_AXIS.span());
+ }, UnsupportedOperationException.class);
+ }
+
+ @Test
+ public void testFull() {
+ // act
+ StubRegion region = new StubRegion(Collections.emptyList());
+
+ // assert
+ Assert.assertTrue(region.isFull());
+ Assert.assertFalse(region.isEmpty());
+ }
+
+ @Test
+ public void testGetBoundarySize() {
+ // arrange
+ TestPoint2D p1 = new TestPoint2D(1, 0);
+ TestPoint2D p2 = new TestPoint2D(2, 0);
+ TestPoint2D p3 = new TestPoint2D(1, 1);
+
+ // act/assert
+ Assert.assertEquals(0, new StubRegion(Collections.emptyList()).getBoundarySize(), PartitionTestUtils.EPS);
+ GeometryTestUtils.assertPositiveInfinity(new StubRegion(Arrays.asList(TestLine.X_AXIS.span())).getBoundarySize());
+ Assert.assertEquals(2 + Math.sqrt(2), new StubRegion(Arrays.asList(
+ new TestLineSegment(p1, p2),
+ new TestLineSegment(p2, p3),
+ new TestLineSegment(p3, p1)
+ )).getBoundarySize(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ TestPoint2D p1 = new TestPoint2D(1, 0);
+ TestPoint2D p2 = new TestPoint2D(2, 0);
+ TestPoint2D p3 = new TestPoint2D(1, 1);
+
+ StubRegion full = new StubRegion(Collections.emptyList());
+ StubRegion halfSpace = new StubRegion(Arrays.asList(TestLine.X_AXIS.span()));
+ StubRegion triangle = new StubRegion(Arrays.asList(
+ new TestLineSegment(p1, p2),
+ new TestLineSegment(p2, p3),
+ new TestLineSegment(p3, p1)
+ ));
+
+ // act/assert
+ checkClassify(full, RegionLocation.INSIDE, TestPoint2D.ZERO, p1, p2, p3);
+
+ checkClassify(halfSpace, RegionLocation.INSIDE, new TestPoint2D(0, 1));
+ checkClassify(halfSpace, RegionLocation.OUTSIDE, new TestPoint2D(0, -1));
+ checkClassify(halfSpace, RegionLocation.BOUNDARY,
+ new TestPoint2D(-1, 0), new TestPoint2D(0, 0), new TestPoint2D(1, 0));
+
+ checkClassify(triangle, RegionLocation.INSIDE, new TestPoint2D(1.25, 0.25));
+ checkClassify(triangle, RegionLocation.OUTSIDE, new TestPoint2D(-1, 0), new TestPoint2D(0, 0), new TestPoint2D(3, 0));
+ checkClassify(triangle, RegionLocation.BOUNDARY, p1, p2, p3);
+ }
+
+ @Test
+ public void testProject() {
+ // arrange
+ TestPoint2D p1 = new TestPoint2D(1, 0);
+ TestPoint2D p2 = new TestPoint2D(2, 0);
+ TestPoint2D p3 = new TestPoint2D(1, 1);
+
+ StubRegion full = new StubRegion(Collections.emptyList());
+ StubRegion halfSpace = new StubRegion(Arrays.asList(TestLine.X_AXIS.span()));
+ StubRegion triangle = new StubRegion(Arrays.asList(
+ new TestLineSegment(p1, p2),
+ new TestLineSegment(p2, p3),
+ new TestLineSegment(p3, p1)
+ ));
+
+ // act/assert
+ Assert.assertNull(full.project(TestPoint2D.ZERO));
+ Assert.assertNull(full.project(new TestPoint2D(1, 1)));
+
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, halfSpace.project(new TestPoint2D(0, 1)));
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, halfSpace.project(new TestPoint2D(0, 0)));
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, halfSpace.project(new TestPoint2D(0, -1)));
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(1.25, 0), triangle.project(new TestPoint2D(1.25, 0.1)));
+ PartitionTestUtils.assertPointsEqual(p1, triangle.project(TestPoint2D.ZERO));
+ PartitionTestUtils.assertPointsEqual(p3, triangle.project(new TestPoint2D(0, 10)));
+ }
+
+ @Test
+ public void testTrim() {
+ // arrange
+ TestPoint2D p1 = new TestPoint2D(1, 0);
+ TestPoint2D p2 = new TestPoint2D(2, 0);
+ TestPoint2D p3 = new TestPoint2D(2, 1);
+ TestPoint2D p4 = new TestPoint2D(1, 1);
+
+ StubRegion full = new StubRegion(Collections.emptyList());
+ StubRegion halfSpace = new StubRegion(Arrays.asList(TestLine.Y_AXIS.span()));
+ StubRegion square = new StubRegion(Arrays.asList(
+ new TestLineSegment(p1, p2),
+ new TestLineSegment(p2, p3),
+ new TestLineSegment(p3, p4),
+ new TestLineSegment(p4, p1)
+ ));
+
+ TestLineSegment segment = new TestLineSegment(new TestPoint2D(-1, 0.5), new TestPoint2D(4, 0.5));
+
+ // act/assert
+ Assert.assertSame(segment, full.trim(segment));
+
+ TestLineSegment trimmedA = halfSpace.trim(segment);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0.5), trimmedA.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0.5), trimmedA.getEndPoint());
+
+ TestLineSegment trimmedB = square.trim(segment);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(1, 0.5), trimmedB.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 0.5), trimmedB.getEndPoint());
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ StubRegion region = new StubRegion(Collections.emptyList());
+
+ TestLine splitter = TestLine.X_AXIS;
+
+ // act
+ Split<StubRegion> split = region.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ StubRegion minus = split.getMinus();
+ Assert.assertEquals(1, minus.getBoundaries().size());
+ checkClassify(minus, RegionLocation.INSIDE, new TestPoint2D(0, 1));
+ checkClassify(minus, RegionLocation.BOUNDARY, new TestPoint2D(0, 0));
+ checkClassify(minus, RegionLocation.OUTSIDE, new TestPoint2D(0, -1));
+
+ StubRegion plus = split.getPlus();
+ Assert.assertEquals(1, plus.getBoundaries().size());
+ checkClassify(plus, RegionLocation.OUTSIDE, new TestPoint2D(0, 1));
+ checkClassify(plus, RegionLocation.BOUNDARY, new TestPoint2D(0, 0));
+ checkClassify(plus, RegionLocation.INSIDE, new TestPoint2D(0, -1));
+ }
+
+ @Test
+ public void testSplit_parallel_plusOnly() {
+ // arrange
+ StubRegion region = new StubRegion(
+ Arrays.asList(new TestLineSegment(new TestPoint2D(0, 1), new TestPoint2D(1, 1))));
+
+ TestLine splitter = TestLine.X_AXIS.reverse();
+
+ // act
+ Split<StubRegion> split = region.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(region, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_parallel_minusOnly() {
+ // arrange
+ StubRegion region = new StubRegion(
+ Arrays.asList(new TestLineSegment(new TestPoint2D(0, 1), new TestPoint2D(1, 1))));
+
+ TestLine splitter = TestLine.X_AXIS;
+
+ // act
+ Split<StubRegion> split = region.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(region, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_coincident_sameOrientation() {
+ // arrange
+ StubRegion region = new StubRegion(Arrays.asList(TestLine.X_AXIS.span()));
+
+ TestLine splitter = TestLine.X_AXIS;
+
+ // act
+ Split<StubRegion> split = region.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(region, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_coincident_oppositeOrientation() {
+ // arrange
+ StubRegion region = new StubRegion(Arrays.asList(TestLine.X_AXIS.span()));
+
+ TestLine splitter = TestLine.X_AXIS.reverse();
+
+ // act
+ Split<StubRegion> split = region.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(region, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_finite_both() {
+ // arrange
+ TestPoint2D p1 = new TestPoint2D(1, -0.5);
+ TestPoint2D p2 = new TestPoint2D(2, -0.5);
+ TestPoint2D p3 = new TestPoint2D(2, 0.5);
+ TestPoint2D p4 = new TestPoint2D(1, 0.5);
+
+ StubRegion region = new StubRegion(Arrays.asList(
+ new TestLineSegment(p1, p2),
+ new TestLineSegment(p2, p3),
+ new TestLineSegment(p3, p4),
+ new TestLineSegment(p4, p1)
+ ));
+
+ TestLine splitter = TestLine.X_AXIS;
+
+ // act
+ Split<StubRegion> split = region.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ StubRegion minus = split.getMinus();
+ Assert.assertEquals(4, minus.getBoundaries().size());
+ checkClassify(minus, RegionLocation.INSIDE, new TestPoint2D(1.5, 0.25));
+ checkClassify(minus, RegionLocation.BOUNDARY, new TestPoint2D(1.5, 0));
+ checkClassify(minus, RegionLocation.OUTSIDE, new TestPoint2D(1.5, -0.25));
+
+ StubRegion plus = split.getPlus();
+ Assert.assertEquals(4, plus.getBoundaries().size());
+ checkClassify(plus, RegionLocation.OUTSIDE, new TestPoint2D(1.5, 0.25));
+ checkClassify(plus, RegionLocation.BOUNDARY, new TestPoint2D(1.5, 0));
+ checkClassify(plus, RegionLocation.INSIDE, new TestPoint2D(1.5, -0.25));
+ }
+
+ @Test
+ public void testTransform_full() {
+ // arrange
+ StubRegion region = new StubRegion(Collections.emptyList());
+
+ Transform<TestPoint2D> transform = new TestTransform2D(p -> new TestPoint2D(p.getX() + 1, p.getY() + 2));
+
+ // act
+ StubRegion transformed = region.transform(transform);
+
+ // assert
+ Assert.assertTrue(transformed.isFull());
+ Assert.assertFalse(transformed.isEmpty());
+ }
+
+ @Test
+ public void testTransform_infinite() {
+ // arrange
+ TestLine line = TestLine.Y_AXIS;
+
+ StubRegion region = new StubRegion(Arrays.asList(
+ line.span()
+ ));
+
+ Transform<TestPoint2D> transform = new TestTransform2D(p -> new TestPoint2D(p.getX() + 1, p.getY() + 2));
+
+ // act
+ StubRegion transformed = region.transform(transform);
+
+ // assert
+ List<TestLineSegment> boundaries = transformed.getBoundaries();
+
+ Assert.assertEquals(1, boundaries.size());
+
+ TestLineSegment a = boundaries.get(0);
+ TestLine aLine = a.getHyperplane();
+ PartitionTestUtils.assertPointsEqual(aLine.getOrigin(), new TestPoint2D(1, 0));
+ Assert.assertEquals(0.0, aLine.getDirectionX(), PartitionTestUtils.EPS);
+ Assert.assertEquals(1.0, aLine.getDirectionY(), PartitionTestUtils.EPS);
+
+ GeometryTestUtils.assertNegativeInfinity(a.getStart());
+ GeometryTestUtils.assertPositiveInfinity(a.getEnd());
+ }
+
+ @Test
+ public void testTransform_finite() {
+ // arrange
+ TestPoint2D p1 = new TestPoint2D(1, 0);
+ TestPoint2D p2 = new TestPoint2D(2, 0);
+ TestPoint2D p3 = new TestPoint2D(1, 1);
+
+ StubRegion region = new StubRegion(Arrays.asList(
+ new TestLineSegment(p1, p2),
+ new TestLineSegment(p2, p3),
+ new TestLineSegment(p3, p1)
+ ));
+
+ Transform<TestPoint2D> transform = new TestTransform2D(p -> new TestPoint2D(p.getX() + 1, p.getY() + 2));
+
+ // act
+ StubRegion transformed = region.transform(transform);
+
+ // assert
+ List<TestLineSegment> boundaries = transformed.getBoundaries();
+
+ Assert.assertEquals(3, boundaries.size());
+
+ TestLineSegment a = boundaries.get(0);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 2), a.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(3, 2), a.getEndPoint());
+
+ TestLineSegment b = boundaries.get(1);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(3, 2), b.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 3), b.getEndPoint());
+
+ TestLineSegment c = boundaries.get(2);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 3), c.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 2), c.getEndPoint());
+ }
+
+ @Test
+ public void testTransform_reflection() {
+ // arrange
+ TestPoint2D p1 = new TestPoint2D(1, 0);
+ TestPoint2D p2 = new TestPoint2D(2, 0);
+ TestPoint2D p3 = new TestPoint2D(1, 1);
+
+ StubRegion region = new StubRegion(Arrays.asList(
+ new TestLineSegment(p1, p2),
+ new TestLineSegment(p2, p3),
+ new TestLineSegment(p3, p1)
+ ));
+
+ Transform<TestPoint2D> transform = new TestTransform2D(p -> new TestPoint2D(-p.getX(), p.getY()));
+
+ // act
+ StubRegion transformed = region.transform(transform);
+
+ // assert
+ List<TestLineSegment> boundaries = transformed.getBoundaries();
+
+ Assert.assertEquals(3, boundaries.size());
+
+ TestLineSegment a = boundaries.get(0);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-2, 0), a.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), a.getEndPoint());
+
+ TestLineSegment b = boundaries.get(1);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 1), b.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-2, 0), b.getEndPoint());
+
+ TestLineSegment c = boundaries.get(2);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), c.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 1), c.getEndPoint());
+ }
+
+ @Test
+ public void testConvexRegionBoundaryBuilder_full() {
+ // act
+ StubRegion region = StubRegion.fromBounds(Collections.emptyList());
+
+ // assert
+ Assert.assertSame(StubRegion.FULL, region);
+ }
+
+ @Test
+ public void testConvexRegionBoundaryBuilder_singleLine() {
+ // act
+ StubRegion region = StubRegion.fromBounds(Arrays.asList(TestLine.Y_AXIS));
+
+ // assert
+ Assert.assertEquals(1, region.getBoundaries().size());
+
+ checkClassify(region, RegionLocation.INSIDE, new TestPoint2D(-1, 0));
+ checkClassify(region, RegionLocation.BOUNDARY, new TestPoint2D(0, 0));
+ checkClassify(region, RegionLocation.OUTSIDE, new TestPoint2D(1, 0));
+ }
+
+ @Test
+ public void testConvexRegionBoundaryBuilder_multipleLines() {
+ // act
+ StubRegion region = StubRegion.fromBounds(Arrays.asList(
+ TestLine.X_AXIS,
+ new TestLine(new TestPoint2D(1, 0), new TestPoint2D(0, 1)),
+ TestLine.Y_AXIS.reverse()
+ ));
+
+ // assert
+ Assert.assertEquals(3, region.getBoundaries().size());
+
+ checkClassify(region, RegionLocation.INSIDE, new TestPoint2D(0.25, 0.25));
+
+ checkClassify(region, RegionLocation.BOUNDARY,
+ TestPoint2D.ZERO, new TestPoint2D(1, 0), new TestPoint2D(1, 0), new TestPoint2D(0.5, 0.5));
+
+ checkClassify(region, RegionLocation.OUTSIDE,
+ new TestPoint2D(-1, 0.5), new TestPoint2D(1, 0.5),
+ new TestPoint2D(0.5, 1), new TestPoint2D(0.5, -1));
+ }
+
+ @Test
+ public void testConvexRegionBoundaryBuilder_duplicateLines() {
+ // act
+ StubRegion region = StubRegion.fromBounds(Arrays.asList(
+ TestLine.Y_AXIS,
+ TestLine.Y_AXIS,
+ new TestLine(new TestPoint2D(0, 0), new TestPoint2D(0, 1)),
+ TestLine.Y_AXIS));
+
+ // assert
+ Assert.assertEquals(1, region.getBoundaries().size());
+
+ checkClassify(region, RegionLocation.INSIDE, new TestPoint2D(-1, 0));
+ checkClassify(region, RegionLocation.BOUNDARY, new TestPoint2D(0, 0));
+ checkClassify(region, RegionLocation.OUTSIDE, new TestPoint2D(1, 0));
+ }
+
+ @Test
+ public void testConvexRegionBoundaryBuilder() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ StubRegion.fromBounds(Arrays.asList(TestLine.X_AXIS, TestLine.X_AXIS.reverse()));
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ StubRegion.fromBounds(Arrays.asList(
+ TestLine.X_AXIS,
+ TestLine.Y_AXIS,
+ new TestLine(new TestPoint2D(1, 0), new TestPoint2D(0, -1))));
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ StubRegion region = new StubRegion(Collections.emptyList());
+
+ // act
+ String str = region.toString();
+
+ // assert
+ Assert.assertTrue(str.contains("StubRegion"));
+ Assert.assertTrue(str.contains("boundaries= "));
+ }
+
+ private static void checkClassify(Region<TestPoint2D> region, RegionLocation loc, TestPoint2D ... pts) {
+ for (TestPoint2D pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, loc, region.classify(pt));
+ }
+ }
+
+ private static final class StubRegion extends AbstractConvexHyperplaneBoundedRegion<TestPoint2D, TestLineSegment>{
+
+ private static final long serialVersionUID = 1L;
+
+ private static final StubRegion FULL = new StubRegion(Collections.emptyList());
+
+ StubRegion(List<TestLineSegment> boundaries) {
+ super(boundaries);
+ }
+
+ public StubRegion transform(Transform<TestPoint2D> transform) {
+ return transformInternal(transform, this, TestLineSegment.class, StubRegion::new);
+ }
+
+ @Override
+ public Split<StubRegion> split(Hyperplane<TestPoint2D> splitter) {
+ return splitInternal(splitter, this, TestLineSegment.class, StubRegion::new);
+ }
+
+ @Override
+ public TestLineSegment trim(ConvexSubHyperplane<TestPoint2D> convexSubHyperplane) {
+ return (TestLineSegment) super.trim(convexSubHyperplane);
+ }
+
+ @Override
+ public double getSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public TestPoint2D getBarycenter() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static StubRegion fromBounds(Iterable<TestLine> boundingLines) {
+ final List<TestLineSegment> segments = new ConvexRegionBoundaryBuilder<>(TestLineSegment.class).build(boundingLines);
+ return segments.isEmpty() ? FULL : new StubRegion(segments);
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplaneTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplaneTest.java
new file mode 100644
index 0000000..eda55fa
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplaneTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.core.partitioning;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestPoint1D;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AbstractEmbeddingSubHyperplaneTest {
+
+ @Test
+ public void testSimpleProperties() {
+ // arrange
+ StubSubHyperplane sub = new StubSubHyperplane();
+
+ // act/assert
+ Assert.assertTrue(sub.isFull());
+ Assert.assertTrue(sub.isEmpty());
+ Assert.assertEquals(1.0, sub.getSize(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ StubSubHyperplane sub = new StubSubHyperplane();
+
+ // act/assert
+ Assert.assertEquals(RegionLocation.INSIDE, sub.classify(new TestPoint2D(-1, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(new TestPoint2D(0, 0)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(0, 1)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(-1, 1)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(-1, -1)));
+ }
+
+ @Test
+ public void testClosest() {
+ // arrange
+ StubSubHyperplane sub = new StubSubHyperplane();
+
+ // act/assert
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, 0)));
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(0, 0)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, 0)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, 1)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, -1)));
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, 1)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, -1)));
+ }
+
+ @Test
+ public void testClosest_nullSubspaceRegionProjection() {
+ // arrange
+ StubSubHyperplane sub = new StubSubHyperplane();
+ sub.region.projected = null;
+
+ // act/assert
+ Assert.assertNull(sub.closest(new TestPoint2D(1, 1)));
+ }
+
+ private static class StubSubHyperplane extends AbstractEmbeddingSubHyperplane<TestPoint2D, TestPoint1D, TestLine> {
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 20190729L;
+
+ private StubRegion1D region = new StubRegion1D();
+
+ @Override
+ public boolean isInfinite() {
+ return false;
+ }
+
+ @Override
+ public boolean isFinite() {
+ return true;
+ }
+
+ @Override
+ public Builder<TestPoint2D> builder() {
+ return null;
+ }
+
+ @Override
+ public List<? extends ConvexSubHyperplane<TestPoint2D>> toConvex() {
+ return null;
+ }
+
+ @Override
+ public TestLine getHyperplane() {
+ return TestLine.X_AXIS;
+ }
+
+ @Override
+ public HyperplaneBoundedRegion<TestPoint1D> getSubspaceRegion() {
+ return region;
+ }
+
+ @Override
+ public Split<StubSubHyperplane> split(Hyperplane<TestPoint2D> splitter) {
+ return null;
+ }
+
+ @Override
+ public SubHyperplane<TestPoint2D> transform(Transform<TestPoint2D> transform) {
+ return null;
+ }
+ }
+
+ /** Stub region implementation with some hard-coded values. Negative numbers are
+ * on the inside of the region.
+ */
+ private static class StubRegion1D implements HyperplaneBoundedRegion<TestPoint1D> {
+
+ private TestPoint1D projected = new TestPoint1D(0);
+
+ @Override
+ public boolean isFull() {
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return true;
+ }
+
+ @Override
+ public double getSize() {
+ return 1;
+ }
+
+ @Override
+ public double getBoundarySize() {
+ return 0;
+ }
+
+ @Override
+ public TestPoint1D getBarycenter() {
+ return null;
+ }
+
+ @Override
+ public RegionLocation classify(TestPoint1D pt) {
+ int sign = PartitionTestUtils.PRECISION.sign(pt.getX());
+
+ if (sign < 0) {
+ return RegionLocation.INSIDE;
+ }
+ else if (sign == 0) {
+ return RegionLocation.BOUNDARY;
+ }
+ return RegionLocation.OUTSIDE;
+ }
+
+ @Override
+ public TestPoint1D project(TestPoint1D pt) {
+ return projected;
+ }
+
+ @Override
+ public Split<? extends HyperplaneBoundedRegion<TestPoint1D>> split(Hyperplane<TestPoint1D> splitter) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java
new file mode 100644
index 0000000..5855448
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.core.partitioning;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AbstractHyperplaneTest {
+
+ @Test
+ public void testGetPrecision() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ StubHyperplane hyper = new StubHyperplane(precision);
+
+ // act/assert
+ Assert.assertSame(precision, hyper.getPrecision());
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ StubHyperplane hyper = new StubHyperplane(precision);
+
+ // act/assert
+ Assert.assertEquals(HyperplaneLocation.MINUS, hyper.classify(new TestPoint2D(1, 1)));
+
+ Assert.assertEquals(HyperplaneLocation.ON, hyper.classify(new TestPoint2D(1, 0.09)));
+ Assert.assertEquals(HyperplaneLocation.ON, hyper.classify(new TestPoint2D(1, 0)));
+ Assert.assertEquals(HyperplaneLocation.ON, hyper.classify(new TestPoint2D(1, -0.09)));
+
+ Assert.assertEquals(HyperplaneLocation.PLUS, hyper.classify(new TestPoint2D(1, -1)));
+ }
+
+ @Test
+ public void testContains() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ StubHyperplane hyper = new StubHyperplane(precision);
+
+ // act/assert
+ Assert.assertFalse(hyper.contains(new TestPoint2D(1, 1)));
+
+ Assert.assertTrue(hyper.contains(new TestPoint2D(1, 0.09)));
+ Assert.assertTrue(hyper.contains(new TestPoint2D(1, 0)));
+ Assert.assertTrue(hyper.contains(new TestPoint2D(1, -0.09)));
+
+ Assert.assertFalse(hyper.contains(new TestPoint2D(1, -1)));
+ }
+
+ public static class StubHyperplane extends AbstractHyperplane<TestPoint2D> {
+
+ private static final long serialVersionUID = 1L;
+
+ public StubHyperplane(DoublePrecisionContext precision) {
+ super(precision);
+ }
+
+ @Override
+ public double offset(TestPoint2D point) {
+ return TestLine.X_AXIS.offset(point);
+ }
+
+ @Override
+ public TestPoint2D project(TestPoint2D point) {
+ return null;
+ }
+
+ @Override
+ public Hyperplane<TestPoint2D> reverse() {
+ return null;
+ }
+
+ @Override
+ public Hyperplane<TestPoint2D> transform(Transform<TestPoint2D> transform) {
+ return null;
+ }
+
+ @Override
+ public boolean similarOrientation(Hyperplane<TestPoint2D> other) {
+ return false;
+ }
+
+ @Override
+ public ConvexSubHyperplane<TestPoint2D> span() {
+ return null;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/SplitTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/SplitTest.java
new file mode 100644
index 0000000..5b4ec57
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/SplitTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.core.partitioning;
+
+import org.junit.Test;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.junit.Assert;
+
+public class SplitTest {
+
+ @Test
+ public void testProperties() {
+ // arrange
+ Object a = new Object();
+ Object b = new Object();
+
+ // act
+ Split<Object> split = new Split<>(a, b);
+
+ // assert
+ Assert.assertSame(a, split.getMinus());
+ Assert.assertSame(b, split.getPlus());
+ }
+
+ @Test
+ public void testGetLocation() {
+ // arrange
+ Object a = new Object();
+ Object b = new Object();
+
+ // act/assert
+ Assert.assertEquals(SplitLocation.NEITHER, new Split<Object>(null, null).getLocation());
+ Assert.assertEquals(SplitLocation.MINUS, new Split<Object>(a, null).getLocation());
+ Assert.assertEquals(SplitLocation.PLUS, new Split<Object>(null, b).getLocation());
+ Assert.assertEquals(SplitLocation.BOTH, new Split<Object>(a, b).getLocation());
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Split<String> split = new Split<>("a", "b");
+
+ // act
+ String str = split.toString();
+
+ // assert
+ Assert.assertEquals("Split[location= BOTH, minus= a, plus= b]", str);
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeBuilder.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeBuilder.java
deleted file mode 100644
index 81d03af..0000000
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeBuilder.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.text.ParseException;
-import java.util.StringTokenizer;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-
-/** Local class for building an {@link AbstractRegion} tree.
- * @param <P> Point type defining the space
- */
-public abstract class TreeBuilder<P extends Point<P>> {
-
- /** Default epsilon value for use when no value is specified
- * in the constructor.
- */
- private static final double DEFAULT_EPS = 1e-10;
-
- /** Keyword for internal nodes. */
- private static final String INTERNAL = "internal";
-
- /** Keyword for leaf nodes. */
- private static final String LEAF = "leaf";
-
- /** Keyword for plus children trees. */
- private static final String PLUS = "plus";
-
- /** Keyword for minus children trees. */
- private static final String MINUS = "minus";
-
- /** Keyword for true flags. */
- private static final String TRUE = "true";
-
- /** Keyword for false flags. */
- private static final String FALSE = "false";
-
- /** Tree root. */
- private BSPTree<P> root;
-
- /** Precision. */
- private final DoublePrecisionContext precision;
-
- /** Tokenizer parsing string representation. */
- private final StringTokenizer tokenizer;
-
- /** Constructor using a default precision context.
- * @param type type of the expected representation
- * @param str the tree string representation
- * @exception ParseException if the string cannot be parsed
- */
- public TreeBuilder(final String type, final String str) throws ParseException {
- this(type, str, new EpsilonDoublePrecisionContext(DEFAULT_EPS));
- }
-
- /** Simple constructor.
- * @param type type of the expected representation
- * @param str the tree string representation
- * @param precision precision context for determining floating point equality
- * @exception ParseException if the string cannot be parsed
- */
- public TreeBuilder(final String type, final String str, final DoublePrecisionContext precision)
- throws ParseException {
- this.precision = precision;
-
- root = null;
- tokenizer = new StringTokenizer(str);
- getWord(type);
- getWord(PLUS);
- root = new BSPTree<>();
- parseTree(root);
- if (tokenizer.hasMoreTokens()) {
- throw new ParseException("unexpected " + tokenizer.nextToken(), 0);
- }
- }
-
- /** Parse a tree.
- * @param node start node
- * @exception ParseException if the string cannot be parsed
- */
- private void parseTree(final BSPTree<P> node)
- throws ParseException {
- if (INTERNAL.equals(getWord(INTERNAL, LEAF))) {
- // this is an internal node, it has a cut sub-hyperplane (stored as a whole hyperplane)
- // then a minus tree, then a plus tree
- node.insertCut(parseHyperplane());
- getWord(MINUS);
- parseTree(node.getMinus());
- getWord(PLUS);
- parseTree(node.getPlus());
- } else {
- // this is a leaf node, it has only an inside/outside flag
- node.setAttribute(getBoolean());
- }
- }
-
- /** Get next word.
- * @param allowed allowed values
- * @return parsed word
- * @exception ParseException if the string cannot be parsed
- */
- protected String getWord(final String ... allowed)
- throws ParseException {
- final String token = tokenizer.nextToken();
- for (final String a : allowed) {
- if (a.equals(token)) {
- return token;
- }
- }
- throw new ParseException(token + " != " + allowed[0], 0);
- }
-
- /** Get next number.
- * @return parsed number
- * @exception NumberFormatException if the string cannot be parsed
- */
- protected double getNumber()
- throws NumberFormatException {
- return Double.parseDouble(tokenizer.nextToken());
- }
-
- /** Get next boolean.
- * @return parsed boolean
- * @exception ParseException if the string cannot be parsed
- */
- protected boolean getBoolean()
- throws ParseException {
- return getWord(TRUE, FALSE).equals(TRUE);
- }
-
- /** Get the built tree.
- * @return built tree
- */
- public BSPTree<P> getTree() {
- return root;
- }
-
- /** Get the precision.
- * @return precision
- */
- public DoublePrecisionContext getPrecision() {
- return precision;
- }
-
- /** Parse an hyperplane.
- * @return next hyperplane from the stream
- * @exception ParseException if the string cannot be parsed
- */
- protected abstract Hyperplane<P> parseHyperplane()
- throws ParseException;
-
-}
\ No newline at end of file
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeDumper.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeDumper.java
deleted file mode 100644
index db94a9c..0000000
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeDumper.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.Formatter;
-import java.util.Locale;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Dumping visitor.
- * @param <P> Point type defining the space
- */
-public abstract class TreeDumper<P extends Point<P>> implements BSPTreeVisitor<P> {
- /** Builder for the string representation of the dumped tree. */
- private final StringBuilder dump;
-
- /** Formatter for strings. */
- private final Formatter formatter;
-
- /** Current indentation prefix. */
- private String prefix;
-
- /** Simple constructor.
- * @param type type of the region to dump
- */
- public TreeDumper(final String type) {
- this.dump = new StringBuilder();
- this.formatter = new Formatter(dump, Locale.US);
- this.prefix = "";
- formatter.format("%s%n", type);
- }
-
- /** Get the string representation of the tree.
- * @return string representation of the tree.
- */
- public String getDump() {
- return dump.toString();
- }
-
- /** Get the formatter to use.
- * @return formatter to use
- */
- protected Formatter getFormatter() {
- return formatter;
- }
-
- /** Format a string representation of the hyperplane underlying a cut sub-hyperplane.
- * @param hyperplane hyperplane to format
- */
- protected abstract void formatHyperplane(Hyperplane<P> hyperplane);
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<P> node) {
- return Order.SUB_MINUS_PLUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<P> node) {
- formatter.format("%s %s internal ", prefix, type(node));
- formatHyperplane(node.getCut().getHyperplane());
- formatter.format("%n");
- prefix = prefix + " ";
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<P> node) {
- formatter.format("%s %s leaf %s%n",
- prefix, type(node), node.getAttribute());
- for (BSPTree<P> n = node;
- n.getParent() != null && n == n.getParent().getPlus();
- n = n.getParent()) {
- prefix = prefix.substring(0, prefix.length() - 2);
- }
- }
-
- /** Get the type of the node.
- * @param node node to check
- * @return "plus " or "minus" depending on the node being the plus or minus
- * child of its parent ("plus " is arbitrarily returned for the root node)
- */
- private String type(final BSPTree<P> node) {
- return (node.getParent() != null && node == node.getParent().getMinus()) ? "minus" : "plus ";
- }
-}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreePrinter.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreePrinter.java
deleted file mode 100644
index 75acd92..0000000
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreePrinter.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.Objects;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Base for classes that create string representations of {@link BSPTree}s.
- * @param <P> Point type defining the space
- */
-public abstract class TreePrinter<P extends Point<P>> implements BSPTreeVisitor<P> {
-
- /** Indent per tree level */
- protected static final String INDENT = " ";
-
- /** Current depth in the tree */
- protected int depth;
-
- /** Contains the string output */
- protected StringBuilder output = new StringBuilder();
-
- /** Returns a string representation of the given {@link BSPTree}.
- * @param tree
- * @return
- */
- public String writeAsString(BSPTree<P> tree) {
- output.delete(0, output.length());
-
- tree.visit(this);
-
- return output.toString();
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(BSPTree<P> node) {
- return Order.SUB_MINUS_PLUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(BSPTree<P> node) {
- writeLinePrefix(node);
- writeInternalNode(node);
-
- write("\n");
-
- ++depth;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(BSPTree<P> node) {
- writeLinePrefix(node);
- writeLeafNode(node);
-
- write("\n");
-
- BSPTree<P> cur = node;
- while (cur.getParent() != null && cur.getParent().getPlus() == cur) {
- --depth;
- cur = cur.getParent();
- }
- }
-
- /** Writes the prefix for the current line in the output. This includes
- * the line indent, the plus/minus node indicator, and a string identifier
- * for the node itself.
- * @param node
- */
- protected void writeLinePrefix(BSPTree<P> node) {
- for (int i=0; i<depth; ++i) {
- write(INDENT);
- }
-
- if (node.getParent() != null) {
- if (node.getParent().getMinus() == node) {
- write("[-] ");
- }
- else {
- write("[+] ");
- }
- }
-
- write(nodeIdString(node) + " | ");
- }
-
- /** Returns a short string identifier for the given node.
- * @param node
- * @return
- */
- protected String nodeIdString(BSPTree<P> node) {
- String str = Objects.toString(node);
- int idx = str.lastIndexOf('.');
- if (idx > -1) {
- return str.substring(idx + 1, str.length());
- }
- return str;
- }
-
- /** Adds the given string to the output.
- * @param str
- */
- protected void write(String str) {
- output.append(str);
- }
-
- /** Method for subclasses to provide their own string representation
- * of the given internal node.
- */
- protected abstract void writeInternalNode(BSPTree<P> node);
-
- /** Writes a leaf node. The default implementation here simply writes
- * the node attribute as a string.
- * @param node
- */
- protected void writeLeafNode(BSPTree<P> node) {
- write(String.valueOf(node.getAttribute()));
- }
-}
\ No newline at end of file
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
new file mode 100644
index 0000000..4f1a5fc
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
@@ -0,0 +1,561 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTreeMergeOperator;
+import org.apache.commons.geometry.core.partitioning.bsp.AttributeBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.AttributeBSPTree.AttributeNode;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AbstractBSPTreeMergeOperatorTest {
+
+ @Test
+ public void testMerge_singleNodeTreeWithSingleNodeTree() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().setAttribute("A");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().setAttribute("B");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(1, a.count());
+ Assert.assertEquals(1, b.count());
+ Assert.assertEquals(1, c.count());
+
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_singleNodeTreeWithMultiNodeTree() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().setAttribute("B");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(1, b.count());
+ Assert.assertEquals(3, c.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("Ba", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("Ba", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("BA", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("BA", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_multiNodeTreeWithSingleNodeTree() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().setAttribute("A");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(1, a.count());
+ Assert.assertEquals(3, b.count());
+ Assert.assertEquals(3, c.count());
+
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_cutsIntersect() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(TestLine.Y_AXIS)
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(3, b.count());
+ Assert.assertEquals(7, c.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(1, 0)).getAttribute());
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(-1, 0)).getAttribute());
+
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_cutsParallel() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(3, b.count());
+ Assert.assertEquals(3, c.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_cutsAntiParallel() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(new TestLine(new TestPoint2D(1, 0), TestPoint2D.ZERO))
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(3, b.count());
+ Assert.assertEquals(3, c.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_cutOnPlusSide_parallel() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(new TestLine(new TestPoint2D(0, -2), new TestPoint2D(1, -2)))
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(3, b.count());
+ Assert.assertEquals(5, c.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(0, -1)).getAttribute());
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, -3)).getAttribute());
+
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(-1, -3)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(1, -3)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_cutOnPlusSide_antiParallel() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(new TestLine(new TestPoint2D(1, -2), new TestPoint2D(0, -2)))
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(3, b.count());
+ Assert.assertEquals(5, c.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, -1)).getAttribute());
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(0, -3)).getAttribute());
+
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(-1, -3)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(1, -3)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_cutOnMinusSide_parallel() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(new TestLine(new TestPoint2D(0, 2), new TestPoint2D(1, 2)))
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(3, b.count());
+ Assert.assertEquals(5, c.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(0, 3)).getAttribute());
+
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("AB", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(-1, 3)).getAttribute());
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(1, 3)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_cutOnMinusSide_antiParallel() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(new TestLine(new TestPoint2D(1, 2), new TestPoint2D(0, 2)))
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ AttributeBSPTree<TestPoint2D, String> c = new AttributeBSPTree<>();
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, c);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(3, b.count());
+ Assert.assertEquals(5, c.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(0, 3)).getAttribute());
+
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("ab", c.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("Ab", c.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(-1, 3)).getAttribute());
+ Assert.assertEquals("aB", c.findNode(new TestPoint2D(1, 3)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ PartitionTestUtils.assertTreeStructure(c);
+ }
+
+ @Test
+ public void testMerge_outputIsFirstInput() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(TestLine.Y_AXIS)
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, a);
+
+ // assert
+ Assert.assertEquals(7, a.count());
+ Assert.assertEquals(3, b.count());
+
+ Assert.assertEquals("B", b.findNode(new TestPoint2D(1, 0)).getAttribute());
+ Assert.assertEquals("b", b.findNode(new TestPoint2D(-1, 0)).getAttribute());
+
+ Assert.assertEquals("aB", a.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("ab", a.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("Ab", a.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("AB", a.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ }
+
+ @Test
+ public void testMerge_outputIsSecondInput() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> a = new AttributeBSPTree<>();
+ a.getRoot().cut(TestLine.X_AXIS)
+ .getPlus().attr("A")
+ .getParent()
+ .getMinus().attr("a");
+
+ AttributeBSPTree<TestPoint2D, String> b = new AttributeBSPTree<>();
+ b.getRoot().cut(TestLine.Y_AXIS)
+ .getPlus().attr("B")
+ .getParent()
+ .getMinus().attr("b");
+
+ TestMergeOperator mergeOp = new TestMergeOperator();
+
+ // act
+ mergeOp.apply(a, b, b);
+
+ // assert
+ Assert.assertEquals(3, a.count());
+ Assert.assertEquals(7, b.count());
+
+ Assert.assertEquals("a", a.findNode(new TestPoint2D(0, 1)).getAttribute());
+ Assert.assertEquals("A", a.findNode(new TestPoint2D(0, -1)).getAttribute());
+
+ Assert.assertEquals("aB", b.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("ab", b.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("Ab", b.findNode(new TestPoint2D(-1, -1)).getAttribute());
+ Assert.assertEquals("AB", b.findNode(new TestPoint2D(1, -1)).getAttribute());
+
+ PartitionTestUtils.assertTreeStructure(a);
+ PartitionTestUtils.assertTreeStructure(b);
+ }
+
+ private static class TestMergeOperator extends AbstractBSPTreeMergeOperator<TestPoint2D, AttributeNode<TestPoint2D, String>> {
+
+ /** Perform the test merge operation with the given arguments.
+ * @param input1
+ * @param input2
+ * @param output
+ */
+ public void apply(AttributeBSPTree<TestPoint2D, String> input1, AttributeBSPTree<TestPoint2D, String> input2,
+ AttributeBSPTree<TestPoint2D, String> output) {
+ performMerge(input1, input2, output);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected AttributeNode<TestPoint2D, String> mergeLeaf(AttributeNode<TestPoint2D, String> node1,
+ AttributeNode<TestPoint2D, String> node2) {
+
+ final AttributeNode<TestPoint2D, String> leaf = node1.isLeaf() ? node1 : node2;
+ final AttributeNode<TestPoint2D, String> subtree = node1.isInternal() ? node1 : node2;
+
+ String attr = leaf.getAttribute();
+
+ AttributeNode<TestPoint2D, String> output = outputSubtree(subtree);
+ output.stream().filter(BSPTree.Node::isLeaf).forEach(n -> n.setAttribute(attr + n.getAttribute()));
+
+ return output;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
new file mode 100644
index 0000000..46a88da
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
@@ -0,0 +1,1806 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partition.test.TestBSPTree;
+import org.apache.commons.geometry.core.partition.test.TestBSPTree.TestNode;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestLineSegment;
+import org.apache.commons.geometry.core.partition.test.TestLineSegmentCollection;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partition.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTree.NodeCutRule;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.Order;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AbstractBSPTreeTest {
+
+ @Test
+ public void testInitialization() {
+ // act
+ TestBSPTree tree = new TestBSPTree();
+
+ // assert
+ TestNode root = tree.getRoot();
+
+ Assert.assertNotNull(root);
+ Assert.assertNull(root.getParent());
+
+ PartitionTestUtils.assertIsLeafNode(root);
+ Assert.assertFalse(root.isPlus());
+ Assert.assertFalse(root.isMinus());
+
+ Assert.assertSame(tree, root.getTree());
+ }
+
+ @Test
+ public void testNodeStateGetters() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode root = tree.getRoot();
+ root.cut(TestLine.X_AXIS);
+
+ TestNode plus = root.getPlus();
+ TestNode minus = root.getMinus();
+
+ // act/assert
+ Assert.assertFalse(root.isLeaf());
+ Assert.assertTrue(root.isInternal());
+ Assert.assertFalse(root.isPlus());
+ Assert.assertFalse(root.isMinus());
+
+ Assert.assertTrue(plus.isLeaf());
+ Assert.assertFalse(plus.isInternal());
+ Assert.assertTrue(plus.isPlus());
+ Assert.assertFalse(plus.isMinus());
+
+ Assert.assertTrue(minus.isLeaf());
+ Assert.assertFalse(minus.isInternal());
+ Assert.assertFalse(minus.isPlus());
+ Assert.assertTrue(minus.isMinus());
+ }
+
+ @Test
+ public void testInsertCut() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestLine line = TestLine.X_AXIS;
+
+ // act
+ boolean result = tree.getRoot().insertCut(line);
+
+ // assert
+ Assert.assertTrue(result);
+
+ TestNode root = tree.getRoot();
+ PartitionTestUtils.assertIsInternalNode(root);
+
+ Assert.assertSame(line, root.getCut().getHyperplane());
+
+ PartitionTestUtils.assertIsLeafNode(root.getMinus());
+ PartitionTestUtils.assertIsLeafNode(root.getPlus());
+ }
+
+ @Test
+ public void testInsertCut_fitsCutterToCell() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus();
+
+ // act
+ boolean result = node.insertCut(new TestLine(0.5, 1.5, 1.5, 0.5));
+
+ // assert
+ Assert.assertTrue(result);
+ PartitionTestUtils.assertIsInternalNode(node);
+
+ TestLineSegment segment = (TestLineSegment) node.getCut();
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 2), segment.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 0), segment.getEndPoint());
+ }
+
+ @Test
+ public void testInsertCut_doesNotPassThroughCell_intersects() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus();
+
+ // act
+ boolean result = node.insertCut(new TestLine(-2, 0, 0, -2));
+
+ // assert
+ Assert.assertFalse(result);
+ PartitionTestUtils.assertIsLeafNode(node);
+ }
+
+ @Test
+ public void testInsertCut_doesNotPassThroughCell_parallel() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus();
+
+ // act
+ boolean result = node.insertCut(new TestLine(0, -1, 1, -1));
+
+ // assert
+ Assert.assertFalse(result);
+ PartitionTestUtils.assertIsLeafNode(node);
+ }
+
+ @Test
+ public void testInsertCut_doesNotPassThroughCell_removesExistingChildren() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus()
+ .cut(new TestLine(0, 2, 2, 0));
+
+ // act
+ boolean result = node.insertCut(new TestLine(-2, 0, 0, -2));
+
+ // assert
+ Assert.assertFalse(result);
+ PartitionTestUtils.assertIsLeafNode(node);
+ }
+
+ @Test
+ public void testInsertCut_cutExistsInTree_sameOrientation() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus()
+ .cut(new TestLine(0, 2, 2, 0));
+
+ // act
+ boolean result = node.insertCut(new TestLine(0, 2, 0, 3));
+
+ // assert
+ Assert.assertFalse(result);
+ PartitionTestUtils.assertIsLeafNode(node);
+ }
+
+ @Test
+ public void testInsertCut_cutExistsInTree_oppositeOrientation() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus()
+ .cut(new TestLine(0, 2, 2, 0));
+
+ // act
+ boolean result = node.insertCut(new TestLine(0, 3, 0, 2));
+
+ // assert
+ Assert.assertTrue(result);
+ PartitionTestUtils.assertIsInternalNode(node);
+ }
+
+ @Test
+ public void testInsertCut_createRegionWithThicknessOfHyperplane() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus();
+
+ // act
+ boolean result = node.insertCut(new TestLine(0, 0, -1, 0));
+
+ // assert
+ Assert.assertTrue(result);
+
+ Assert.assertSame(tree.getRoot().getPlus(), tree.findNode(new TestPoint2D(0, -1e-2)));
+ Assert.assertSame(node.getMinus(), tree.findNode(new TestPoint2D(0, 0)));
+ Assert.assertSame(node.getPlus(), tree.findNode(new TestPoint2D(0, 1e-2)));
+ }
+
+ @Test
+ public void testClearCut_cutExists() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ // act
+ boolean result = node.clearCut();
+
+ // assert
+ Assert.assertTrue(result);
+ Assert.assertTrue(node.isLeaf());
+ Assert.assertNull(node.getPlus());
+ Assert.assertNull(node.getMinus());
+ }
+
+ @Test
+ public void testClearCut_cutDoesNotExist() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getMinus();
+
+ // act
+ boolean result = node.clearCut();
+
+ // assert
+ Assert.assertFalse(result);
+ Assert.assertTrue(node.isLeaf());
+ Assert.assertNull(node.getPlus());
+ Assert.assertNull(node.getMinus());
+ }
+
+ @Test
+ public void testClearCut_root_fullTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode node = tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getMinus();
+
+ // act
+ boolean result = tree.getRoot().clearCut();
+
+ // assert
+ Assert.assertTrue(result);
+ Assert.assertTrue(node.isLeaf());
+ Assert.assertNull(node.getPlus());
+ Assert.assertNull(node.getMinus());
+
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testClearCut_root_emptyTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestNode node = tree.getRoot();
+
+ // act
+ boolean result = node.clearCut();
+
+ // assert
+ Assert.assertFalse(result);
+ Assert.assertTrue(node.isLeaf());
+ Assert.assertNull(node.getPlus());
+ Assert.assertNull(node.getMinus());
+
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testFindNode_emptyTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestNode root = tree.getRoot();
+
+ List<TestPoint2D> testPoints = Arrays.asList(
+ new TestPoint2D(0, 0),
+ new TestPoint2D(1, 0),
+ new TestPoint2D(1, 1),
+ new TestPoint2D(0, 1),
+ new TestPoint2D(-1, 1),
+ new TestPoint2D(-1, 0),
+ new TestPoint2D(-1, -1),
+ new TestPoint2D(0, -1),
+ new TestPoint2D(1, -1)
+ );
+
+ // act/assert
+ for (TestPoint2D pt : testPoints) {
+ Assert.assertSame(root, tree.findNode(pt));
+ }
+
+ for (TestPoint2D pt : testPoints) {
+ Assert.assertSame(root, tree.findNode(pt, NodeCutRule.NODE));
+ }
+
+ for (TestPoint2D pt : testPoints) {
+ Assert.assertSame(root, tree.findNode(pt, NodeCutRule.MINUS));
+ }
+
+ for (TestPoint2D pt : testPoints) {
+ Assert.assertSame(root, tree.findNode(pt, NodeCutRule.PLUS));
+ }
+ }
+
+ @Test
+ public void testFindNode_singleArg() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus()
+ .cut(new TestLine(0, 2, 2, 0));
+
+ TestNode root = tree.getRoot();
+ TestNode minusY = root.getPlus();
+
+ TestNode yCut = root.getMinus();
+ TestNode minusXPlusY = yCut.getMinus();
+
+ TestNode diagonalCut = yCut.getPlus();
+ TestNode underDiagonal = diagonalCut.getPlus();
+ TestNode aboveDiagonal = diagonalCut.getMinus();
+
+ // act/assert
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(0, 0)));
+
+ Assert.assertSame(underDiagonal, tree.findNode(new TestPoint2D(1, 0)));
+ Assert.assertSame(aboveDiagonal, tree.findNode(new TestPoint2D(1, 1)));
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(0, 1)));
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(-1, 1)));
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(-1, 0)));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(-1, -1)));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(0, -1)));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(1, -1)));
+
+ Assert.assertSame(underDiagonal, tree.findNode(new TestPoint2D(0.5, 0.5)));
+ Assert.assertSame(aboveDiagonal, tree.findNode(new TestPoint2D(3, 3)));
+ }
+
+ @Test
+ public void testFindNode_nodeCutBehavior() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus()
+ .cut(new TestLine(0, 2, 2, 0));
+
+ TestNode root = tree.getRoot();
+ TestNode minusY = root.getPlus();
+
+ TestNode yCut = root.getMinus();
+ TestNode minusXPlusY = yCut.getMinus();
+
+ TestNode diagonalCut = yCut.getPlus();
+ TestNode underDiagonal = diagonalCut.getPlus();
+ TestNode aboveDiagonal = diagonalCut.getMinus();
+
+ NodeCutRule cutBehavior = NodeCutRule.NODE;
+
+ // act/assert
+ Assert.assertSame(root, tree.findNode(new TestPoint2D(0, 0), cutBehavior));
+
+ Assert.assertSame(root, tree.findNode(new TestPoint2D(1, 0), cutBehavior));
+ Assert.assertSame(diagonalCut, tree.findNode(new TestPoint2D(1, 1), cutBehavior));
+ Assert.assertSame(yCut, tree.findNode(new TestPoint2D(0, 1), cutBehavior));
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(-1, 1), cutBehavior));
+ Assert.assertSame(root, tree.findNode(new TestPoint2D(-1, 0), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(-1, -1), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(0, -1), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(1, -1), cutBehavior));
+
+ Assert.assertSame(underDiagonal, tree.findNode(new TestPoint2D(0.5, 0.5), cutBehavior));
+ Assert.assertSame(aboveDiagonal, tree.findNode(new TestPoint2D(3, 3), cutBehavior));
+ }
+
+ @Test
+ public void testFindNode_minusCutBehavior() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus()
+ .cut(new TestLine(0, 2, 2, 0));
+
+ TestNode root = tree.getRoot();
+ TestNode minusY = root.getPlus();
+
+ TestNode yCut = root.getMinus();
+ TestNode minusXPlusY = yCut.getMinus();
+
+ TestNode diagonalCut = yCut.getPlus();
+ TestNode underDiagonal = diagonalCut.getPlus();
+ TestNode aboveDiagonal = diagonalCut.getMinus();
+
+ NodeCutRule cutBehavior = NodeCutRule.MINUS;
+
+ // act/assert
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(0, 0), cutBehavior));
+
+ Assert.assertSame(underDiagonal, tree.findNode(new TestPoint2D(1, 0), cutBehavior));
+ Assert.assertSame(aboveDiagonal, tree.findNode(new TestPoint2D(1, 1), cutBehavior));
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(0, 1), cutBehavior));
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(-1, 1), cutBehavior));
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(-1, 0), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(-1, -1), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(0, -1), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(1, -1), cutBehavior));
+
+ Assert.assertSame(underDiagonal, tree.findNode(new TestPoint2D(0.5, 0.5), cutBehavior));
+ Assert.assertSame(aboveDiagonal, tree.findNode(new TestPoint2D(3, 3), cutBehavior));
+ }
+
+ @Test
+ public void testFindNode_plusCutBehavior() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getPlus()
+ .cut(new TestLine(0, 2, 2, 0));
+
+ TestNode root = tree.getRoot();
+ TestNode minusY = root.getPlus();
+
+ TestNode yCut = root.getMinus();
+ TestNode minusXPlusY = yCut.getMinus();
+
+ TestNode diagonalCut = yCut.getPlus();
+ TestNode underDiagonal = diagonalCut.getPlus();
+ TestNode aboveDiagonal = diagonalCut.getMinus();
+
+ NodeCutRule cutBehavior = NodeCutRule.PLUS;
+
+ // act/assert
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(0, 0), cutBehavior));
+
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(1, 0), cutBehavior));
+ Assert.assertSame(underDiagonal, tree.findNode(new TestPoint2D(1, 1), cutBehavior));
+ Assert.assertSame(underDiagonal, tree.findNode(new TestPoint2D(0, 1), cutBehavior));
+ Assert.assertSame(minusXPlusY, tree.findNode(new TestPoint2D(-1, 1), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(-1, 0), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(-1, -1), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(0, -1), cutBehavior));
+ Assert.assertSame(minusY, tree.findNode(new TestPoint2D(1, -1), cutBehavior));
+
+ Assert.assertSame(underDiagonal, tree.findNode(new TestPoint2D(0.5, 0.5), cutBehavior));
+ Assert.assertSame(aboveDiagonal, tree.findNode(new TestPoint2D(3, 3), cutBehavior));
+ }
+
+ @Test
+ public void testInsert_convex_emptyTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ // act
+ tree.insert(new TestLineSegment(1, 0, 1, 1));
+
+ // assert
+ TestNode root = tree.getRoot();
+ Assert.assertFalse(root.isLeaf());
+ Assert.assertTrue(root.getMinus().isLeaf());
+ Assert.assertTrue(root.getPlus().isLeaf());
+
+ TestLineSegment seg = (TestLineSegment) root.getCut();
+ PartitionTestUtils.assertPointsEqual(
+ new TestPoint2D(1, Double.NEGATIVE_INFINITY),
+ seg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(
+ new TestPoint2D(1, Double.POSITIVE_INFINITY),
+ seg.getEndPoint());
+ }
+
+ @Test
+ public void testInsert_convex_noSplit() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ // act
+ tree.insert(new TestLineSegment(0.5, 1.5, 1.5, 0.5));
+
+ // assert
+ TestNode root = tree.getRoot();
+ Assert.assertFalse(root.isLeaf());
+
+ TestNode node = tree.findNode(new TestPoint2D(0.5, 0.5));
+ TestLineSegment seg = (TestLineSegment) node.getParent().getCut();
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 2), seg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 0), seg.getEndPoint());
+
+ Assert.assertTrue(tree.getRoot().getPlus().isLeaf());
+ Assert.assertTrue(tree.getRoot().getMinus().getMinus().isLeaf());
+ }
+
+ @Test
+ public void testInsert_convex_split() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ // act
+ tree.insert(new TestLineSegment(-0.5, 2.5, 2.5, -0.5));
+
+ // assert
+ TestNode root = tree.getRoot();
+ Assert.assertFalse(root.isLeaf());
+
+ TestNode plusXPlusY = tree.getRoot().getMinus().getPlus();
+ TestLineSegment plusXPlusYSeg = (TestLineSegment) plusXPlusY.getCut();
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 2), plusXPlusYSeg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 0), plusXPlusYSeg.getEndPoint());
+
+ TestNode minusY = tree.getRoot().getPlus();
+ TestLineSegment minusYSeg = (TestLineSegment) minusY.getCut();
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(2, 0), minusYSeg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(
+ new TestPoint2D(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY), minusYSeg.getEndPoint());
+
+ TestNode minusXPlusY = tree.getRoot().getMinus().getMinus();
+ TestLineSegment minusXPlusYSeg = (TestLineSegment) minusXPlusY.getCut();
+
+ PartitionTestUtils.assertPointsEqual(
+ new TestPoint2D(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY), minusXPlusYSeg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 2), minusXPlusYSeg.getEndPoint());
+ }
+
+ @Test
+ public void testInsert_convexList_convexRegion() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestLineSegment a = new TestLineSegment(0, 0, 1, 0);
+ TestLineSegment b = new TestLineSegment(1, 0, 0, 1);
+ TestLineSegment c = new TestLineSegment(0, 1, 0, 0);
+
+ // act
+ tree.insert(Arrays.asList(a, b, c));
+
+ // assert
+ List<TestLineSegment> segments = getLineSegments(tree);
+
+ Assert.assertEquals(3, segments.size());
+
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TestLine.X_AXIS),
+ segments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(-Math.sqrt(0.5), Double.POSITIVE_INFINITY, new TestLine(1, 0, 0, 1)),
+ segments.get(1));
+ PartitionTestUtils.assertSegmentsEqual(c, segments.get(2));
+ }
+
+ @Test
+ public void testInsert_convexList_concaveRegion() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestLineSegment a = new TestLineSegment(-1, -1, 1, -1);
+ TestLineSegment b = new TestLineSegment(1, -1, 0, 0);
+ TestLineSegment c = new TestLineSegment(0, 0, 1, 1);
+ TestLineSegment d = new TestLineSegment(1, 1, -1, 1);
+ TestLineSegment e = new TestLineSegment(-1, 1, -1, -1);
+
+ // act
+ tree.insert(Arrays.asList(a, b, c, d, e));
+
+ // assert
+ List<TestLineSegment> segments = getLineSegments(tree);
+
+ Assert.assertEquals(5, segments.size());
+
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, new TestLine(-1, -1, 1, -1)),
+ segments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(-Math.sqrt(2), Double.POSITIVE_INFINITY, new TestLine(1, -1, 0, 0)),
+ segments.get(1));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(-1, 1, -1, -1),
+ segments.get(2));
+
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(0, Double.POSITIVE_INFINITY, new TestLine(0, 0, 1, 1)),
+ segments.get(3));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(1, 1, -1, 1),
+ segments.get(4));
+ }
+
+ @Test
+ public void testInsert_subhyperplane_concaveRegion() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestLineSegment a = new TestLineSegment(-1, -1, 1, -1);
+ TestLineSegment b = new TestLineSegment(1, -1, 0, 0);
+ TestLineSegment c = new TestLineSegment(0, 0, 1, 1);
+ TestLineSegment d = new TestLineSegment(1, 1, -1, 1);
+ TestLineSegment e = new TestLineSegment(-1, 1, -1, -1);
+
+ TestLineSegmentCollection coll = new TestLineSegmentCollection(
+ Arrays.asList(a, b, c, d, e));
+
+ // act
+ tree.insert(coll);
+
+ // assert
+ List<TestLineSegment> segments = getLineSegments(tree);
+
+ Assert.assertEquals(5, segments.size());
+
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, new TestLine(-1, -1, 1, -1)),
+ segments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(-Math.sqrt(2), Double.POSITIVE_INFINITY, new TestLine(1, -1, 0, 0)),
+ segments.get(1));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(-1, 1, -1, -1),
+ segments.get(2));
+
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(0, Double.POSITIVE_INFINITY, new TestLine(0, 0, 1, 1)),
+ segments.get(3));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(1, 1, -1, 1),
+ segments.get(4));
+ }
+
+ @Test
+ public void testCount() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ // act/assert
+ Assert.assertEquals(1, tree.count());
+ Assert.assertEquals(1, tree.getRoot().count());
+
+ tree.getRoot().insertCut(TestLine.X_AXIS);
+ Assert.assertEquals(1, tree.getRoot().getMinus().count());
+ Assert.assertEquals(1, tree.getRoot().getPlus().count());
+ Assert.assertEquals(3, tree.count());
+
+ tree.getRoot().getPlus().insertCut(TestLine.Y_AXIS);
+ Assert.assertEquals(1, tree.getRoot().getMinus().count());
+ Assert.assertEquals(3, tree.getRoot().getPlus().count());
+ Assert.assertEquals(5, tree.count());
+
+ tree.getRoot().getMinus().insertCut(TestLine.Y_AXIS);
+ Assert.assertEquals(3, tree.getRoot().getMinus().count());
+ Assert.assertEquals(3, tree.getRoot().getPlus().count());
+ Assert.assertEquals(7, tree.count());
+
+ tree.getRoot().getMinus().insertCut(new TestLine(new TestPoint2D(-1, -1), new TestPoint2D(1, -1)));
+ Assert.assertEquals(1, tree.getRoot().getMinus().count());
+ Assert.assertEquals(3, tree.getRoot().getPlus().count());
+ Assert.assertEquals(5, tree.count());
+ }
+
+ @Test
+ public void testHeight() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ // act/assert
+ Assert.assertEquals(0, tree.height());
+ Assert.assertEquals(0, tree.getRoot().height());
+
+ tree.getRoot().insertCut(TestLine.X_AXIS);
+ Assert.assertEquals(0, tree.getRoot().getMinus().height());
+ Assert.assertEquals(0, tree.getRoot().getPlus().height());
+ Assert.assertEquals(1, tree.height());
+
+ tree.getRoot().getPlus().insertCut(TestLine.Y_AXIS);
+ Assert.assertEquals(0, tree.getRoot().getMinus().height());
+ Assert.assertEquals(1, tree.getRoot().getPlus().height());
+ Assert.assertEquals(2, tree.height());
+
+ tree.getRoot().getMinus().insertCut(TestLine.Y_AXIS);
+ Assert.assertEquals(1, tree.getRoot().getMinus().height());
+ Assert.assertEquals(1, tree.getRoot().getPlus().height());
+ Assert.assertEquals(2, tree.height());
+
+ tree.getRoot().getMinus().clearCut();
+ Assert.assertEquals(0, tree.getRoot().getMinus().height());
+ Assert.assertEquals(1, tree.getRoot().getPlus().height());
+ Assert.assertEquals(2, tree.height());
+
+ tree.getRoot().getPlus().getPlus()
+ .insertCut(new TestLine(new TestPoint2D(0, -1), new TestPoint2D(1, -1)));
+
+ Assert.assertEquals(0, tree.getRoot().getMinus().height());
+ Assert.assertEquals(2, tree.getRoot().getPlus().height());
+ Assert.assertEquals(3, tree.height());
+ }
+
+ @Test
+ public void testDepth() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode root = tree.getRoot();
+ root.cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ // act/assert
+ Assert.assertEquals(0, root.depth());
+
+ Assert.assertEquals(1, root.getPlus().depth());
+
+ Assert.assertEquals(1, root.getMinus().depth());
+ Assert.assertEquals(2, root.getMinus().getPlus().depth());
+ Assert.assertEquals(2, root.getMinus().getMinus().depth());
+ }
+
+ @Test
+ public void testVisit_defaultOrder() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS);
+
+ TestNode root = tree.getRoot();
+ TestNode minus = root.getMinus();
+ TestNode plus = root.getPlus();
+ TestNode minusMinus = minus.getMinus();
+ TestNode minusPlus = minus.getPlus();
+
+ List<TestNode> nodes = new ArrayList<>();
+
+ // act
+ tree.accept(node -> nodes.add(node));
+
+ // assert
+ Assert.assertEquals(
+ Arrays.asList(root, minus, minusMinus, minusPlus, plus),
+ nodes);
+ }
+
+ @Test
+ public void testVisit_specifiedOrder() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS);
+
+ TestNode root = tree.getRoot();
+ TestNode minus = root.getMinus();
+ TestNode plus = root.getPlus();
+ TestNode minusMinus = minus.getMinus();
+ TestNode minusPlus = minus.getPlus();
+
+ // act/assert
+ TestVisitor plusMinusNode = new TestVisitor(Order.PLUS_MINUS_NODE);
+ tree.accept(plusMinusNode);
+ Assert.assertEquals(
+ Arrays.asList(plus, minusPlus, minusMinus, minus, root),
+ plusMinusNode.getVisited());
+
+ TestVisitor plusNodeMinus = new TestVisitor(Order.PLUS_NODE_MINUS);
+ tree.accept(plusNodeMinus);
+ Assert.assertEquals(
+ Arrays.asList(plus, root, minusPlus, minus, minusMinus),
+ plusNodeMinus.getVisited());
+
+ TestVisitor minusPlusNode = new TestVisitor(Order.MINUS_PLUS_NODE);
+ tree.accept(minusPlusNode);
+ Assert.assertEquals(
+ Arrays.asList(minusMinus, minusPlus, minus, plus, root),
+ minusPlusNode.getVisited());
+
+ TestVisitor minusNodePlus = new TestVisitor(Order.MINUS_NODE_PLUS);
+ tree.accept(minusNodePlus);
+ Assert.assertEquals(
+ Arrays.asList(minusMinus, minus, minusPlus, root, plus),
+ minusNodePlus.getVisited());
+
+ TestVisitor nodeMinusPlus = new TestVisitor(Order.NODE_MINUS_PLUS);
+ tree.accept(nodeMinusPlus);
+ Assert.assertEquals(
+ Arrays.asList(root, minus, minusMinus, minusPlus, plus),
+ nodeMinusPlus.getVisited());
+
+ TestVisitor nodePlusMinus = new TestVisitor(Order.NODE_PLUS_MINUS);
+ tree.accept(nodePlusMinus);
+ Assert.assertEquals(
+ Arrays.asList(root, plus, minus, minusPlus, minusMinus),
+ nodePlusMinus.getVisited());
+ }
+
+ @Test
+ public void testVisit_nullVisitOrderSkipsSubtree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS);
+
+ TestNode root = tree.getRoot();
+ TestNode minus = root.getMinus();
+ TestNode minusMinus = minus.getMinus();
+ TestNode minusPlus = minus.getPlus();
+
+ TestVisitor visitor = new TestVisitor(Order.NODE_MINUS_PLUS);
+
+ // act
+ minus.accept(visitor);
+
+ // assert
+ Assert.assertEquals(
+ Arrays.asList(minus, minusMinus, minusPlus),
+ visitor.getVisited());
+ }
+
+ @Test
+ public void testVisit_visitNode() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS);
+
+ TestNode root = tree.getRoot();
+ TestNode minus = root.getMinus();
+ TestNode plus = root.getPlus();
+ TestNode minusMinus = minus.getMinus();
+ TestNode minusPlus = minus.getPlus();
+
+ List<TestNode> nodes = new ArrayList<>();
+
+ // act
+ tree.accept(node -> nodes.add(node));
+
+ // assert
+ Assert.assertEquals(
+ Arrays.asList(root, minus, minusMinus, minusPlus, plus),
+ nodes);
+ }
+
+ @Test
+ public void testIterable_emptyTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ List<TestNode> nodes = new ArrayList<>();
+
+ // act
+ for (TestNode node : tree)
+ {
+ nodes.add(node);
+ }
+
+ // assert
+ Assert.assertEquals(1, nodes.size());
+ Assert.assertSame(tree.getRoot(), nodes.get(0));
+ }
+
+ @Test
+ public void testIterable_multipleNodes() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode root = tree.getRoot();
+ root.cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getParent()
+ .getPlus()
+ .cut(TestLine.Y_AXIS);
+
+ List<TestNode> nodes = new ArrayList<>();
+
+ // act
+ for (TestNode node : tree)
+ {
+ nodes.add(node);
+ }
+
+ // assert
+ Assert.assertEquals(7, nodes.size());
+ Assert.assertSame(root, nodes.get(0));
+
+ Assert.assertSame(root.getMinus(), nodes.get(1));
+ Assert.assertSame(root.getMinus().getMinus(), nodes.get(2));
+ Assert.assertSame(root.getMinus().getPlus(), nodes.get(3));
+
+ Assert.assertSame(root.getPlus(), nodes.get(4));
+ Assert.assertSame(root.getPlus().getMinus(), nodes.get(5));
+ Assert.assertSame(root.getPlus().getPlus(), nodes.get(6));
+ }
+
+ @Test
+ public void testStream_emptyTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ // act
+ List<TestNode> nodes = tree.stream().collect(Collectors.toList());
+
+ // assert
+ Assert.assertEquals(1, nodes.size());
+ Assert.assertSame(tree.getRoot(), nodes.get(0));
+ }
+
+ @Test
+ public void testStream_multipleNodes() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestNode root = tree.getRoot();
+ root.cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getParent()
+ .getPlus()
+ .cut(TestLine.Y_AXIS);
+
+ // act
+ List<TestNode> nodes = tree.stream().collect(Collectors.toList());
+
+ // assert
+ Assert.assertEquals(7, nodes.size());
+ Assert.assertSame(root, nodes.get(0));
+
+ Assert.assertSame(root.getMinus(), nodes.get(1));
+ Assert.assertSame(root.getMinus().getMinus(), nodes.get(2));
+ Assert.assertSame(root.getMinus().getPlus(), nodes.get(3));
+
+ Assert.assertSame(root.getPlus(), nodes.get(4));
+ Assert.assertSame(root.getPlus().getMinus(), nodes.get(5));
+ Assert.assertSame(root.getPlus().getPlus(), nodes.get(6));
+ }
+
+ @Test
+ public void testNodeIterable_singleNodeSubtree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestNode node = tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getMinus();
+
+ List<TestNode> nodes = new ArrayList<>();
+ // act
+ for (TestNode n : node)
+ {
+ nodes.add(n);
+ }
+
+ // assert
+ Assert.assertEquals(1, nodes.size());
+ Assert.assertSame(node, nodes.get(0));
+ }
+
+ @Test
+ public void testNodeIterable_multipleNodeSubtree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestNode node = tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ List<TestNode> nodes = new ArrayList<>();
+ // act
+ for (TestNode n : node)
+ {
+ nodes.add(n);
+ }
+
+ // assert
+ Assert.assertEquals(3, nodes.size());
+ Assert.assertSame(node, nodes.get(0));
+ Assert.assertSame(node.getMinus(), nodes.get(1));
+ Assert.assertSame(node.getPlus(), nodes.get(2));
+ }
+
+ @Test
+ public void testNodeStream_singleNodeSubtree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestNode node = tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS)
+ .getMinus();
+
+ // act
+ List<TestNode> nodes = node.stream().collect(Collectors.toList());
+
+ // assert
+ Assert.assertEquals(1, nodes.size());
+ Assert.assertSame(node, nodes.get(0));
+ }
+
+ @Test
+ public void testNodeStream_multipleNodeSubtree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestNode node = tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ // act
+ List<TestNode> nodes = node.stream().collect(Collectors.toList());
+
+ // assert
+ Assert.assertEquals(3, nodes.size());
+ Assert.assertSame(node, nodes.get(0));
+ Assert.assertSame(node.getMinus(), nodes.get(1));
+ Assert.assertSame(node.getPlus(), nodes.get(2));
+ }
+
+ @Test
+ public void testCopy_rootOnly() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ // act
+ TestBSPTree copy = new TestBSPTree();
+ copy.copy(tree);
+
+ // assert
+ Assert.assertNotSame(tree, copy);
+ Assert.assertNotSame(tree.getRoot(), copy.getRoot());
+
+ Assert.assertEquals(tree.count(), copy.count());
+ }
+
+ @Test
+ public void testCopy_withCuts() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ // act
+ TestBSPTree copy = new TestBSPTree();
+ copy.copy(tree);
+
+ // assert
+ Assert.assertNotSame(tree, copy);
+
+ assertNodesCopiedRecursive(tree.getRoot(), copy.getRoot());
+
+ Assert.assertEquals(tree.count(), copy.count());
+ }
+
+ @Test
+ public void testCopy_changesToOneTreeDoNotAffectCopy() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ // act
+ TestBSPTree copy = new TestBSPTree();
+ copy.copy(tree);
+ tree.getRoot().clearCut();
+
+ // assert
+ Assert.assertEquals(1, tree.count());
+ Assert.assertEquals(5, copy.count());
+ }
+
+ @Test
+ public void testCopy_instancePassedAsArgument() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ // act
+ tree.copy(tree);
+
+ // assert
+ Assert.assertEquals(5, tree.count());
+ }
+
+ @Test
+ public void testExtract_singleNodeTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestBSPTree result = new TestBSPTree();
+ result.getRoot().insertCut(TestLine.X_AXIS);
+
+ // act
+ result.extract(tree.getRoot());
+
+ // assert
+ Assert.assertNotSame(tree.getRoot(), result.getRoot());
+ Assert.assertEquals(1, tree.count());
+ Assert.assertEquals(1, result.count());
+
+ PartitionTestUtils.assertTreeStructure(tree);
+ PartitionTestUtils.assertTreeStructure(result);
+ }
+
+ @Test
+ public void testExtract_clearsExistingNodesInCallingTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ TestBSPTree result = new TestBSPTree();
+ result.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS);
+
+ // act
+ result.extract(tree.getRoot());
+
+ // assert
+ Assert.assertNotSame(tree.getRoot(), result.getRoot());
+ Assert.assertEquals(1, tree.count());
+ Assert.assertEquals(1, result.count());
+
+ PartitionTestUtils.assertTreeStructure(tree);
+ PartitionTestUtils.assertTreeStructure(result);
+ }
+
+ @Test
+ public void testExtract_internalNode() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(new TestPoint2D(0, -1), new TestPoint2D(0, 1)),
+ new TestLineSegment(new TestPoint2D(1, 2), new TestPoint2D(2, 1)),
+ new TestLineSegment(new TestPoint2D(-1, 2), new TestPoint2D(-2, 1)),
+ new TestLineSegment(new TestPoint2D(0, -2), new TestPoint2D(1, -2))
+ ));
+
+ TestBSPTree result = new TestBSPTree();
+
+ // act
+ result.extract(tree.getRoot().getPlus());
+
+ // assert
+ Assert.assertEquals(7, result.count());
+
+ List<TestLineSegment> resultSegments = getLineSegments(result);
+ Assert.assertEquals(3, resultSegments.size());
+
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TestLine.X_AXIS),
+ resultSegments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(Double.NEGATIVE_INFINITY, 0, TestLine.Y_AXIS),
+ resultSegments.get(1));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(0, Double.POSITIVE_INFINITY, new TestLine(new TestPoint2D(0, -2), new TestPoint2D(1, -2))),
+ resultSegments.get(2));
+
+ Assert.assertEquals(13, tree.count());
+
+ List<TestLineSegment> inputSegment = getLineSegments(tree);
+ Assert.assertEquals(6, inputSegment.size());
+
+ PartitionTestUtils.assertTreeStructure(tree);
+ PartitionTestUtils.assertTreeStructure(result);
+ }
+
+ @Test
+ public void testExtract_leafNode() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(new TestPoint2D(0, -1), new TestPoint2D(0, 1)),
+ new TestLineSegment(new TestPoint2D(1, 2), new TestPoint2D(2, 1)),
+ new TestLineSegment(new TestPoint2D(-1, 2), new TestPoint2D(-2, 1)),
+ new TestLineSegment(new TestPoint2D(0, -2), new TestPoint2D(1, -2))
+ ));
+
+ TestPoint2D pt = new TestPoint2D(1, 1);
+
+ TestNode node = tree.findNode(pt);
+ TestBSPTree result = new TestBSPTree();
+
+ // act
+ result.extract(node);
+
+ // assert
+ TestNode resultNode = result.findNode(pt);
+ Assert.assertNotNull(resultNode);
+ Assert.assertNotSame(node, resultNode);
+
+ Assert.assertEquals(7, result.count());
+
+ List<TestLineSegment> resultSegments = getLineSegments(result);
+ Assert.assertEquals(3, resultSegments.size());
+
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TestLine.X_AXIS),
+ resultSegments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(0, Double.POSITIVE_INFINITY, TestLine.Y_AXIS),
+ resultSegments.get(1));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(new TestPoint2D(0, 3), new TestPoint2D(3, 0)),
+ resultSegments.get(2));
+
+ Assert.assertEquals(13, tree.count());
+
+ List<TestLineSegment> inputSegment = getLineSegments(tree);
+ Assert.assertEquals(6, inputSegment.size());
+
+ PartitionTestUtils.assertTreeStructure(tree);
+ PartitionTestUtils.assertTreeStructure(result);
+ }
+
+ @Test
+ public void testExtract_extractFromSameTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(new TestPoint2D(0, -1), new TestPoint2D(0, 1)),
+ new TestLineSegment(new TestPoint2D(1, 2), new TestPoint2D(2, 1)),
+ new TestLineSegment(new TestPoint2D(-1, 2), new TestPoint2D(-2, 1)),
+ new TestLineSegment(new TestPoint2D(0, -2), new TestPoint2D(1, -2))
+ ));
+
+ TestPoint2D pt = new TestPoint2D(1, 1);
+
+ TestNode node = tree.findNode(pt);
+
+ // act
+ tree.extract(node);
+
+ // assert
+ TestNode resultNode = tree.findNode(pt);
+ Assert.assertNotNull(resultNode);
+ Assert.assertSame(node, resultNode);
+
+ Assert.assertEquals(7, tree.count());
+
+ List<TestLineSegment> resultSegments = getLineSegments(tree);
+ Assert.assertEquals(3, resultSegments.size());
+
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TestLine.X_AXIS),
+ resultSegments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(0, Double.POSITIVE_INFINITY, TestLine.Y_AXIS),
+ resultSegments.get(1));
+ PartitionTestUtils.assertSegmentsEqual(
+ new TestLineSegment(new TestPoint2D(0, 3), new TestPoint2D(3, 0)),
+ resultSegments.get(2));
+
+ PartitionTestUtils.assertTreeStructure(tree);
+ }
+
+ @Test
+ public void testTransform_singleNodeTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ Transform<TestPoint2D> t = new TestTransform2D((p) -> new TestPoint2D(p.getX(), p.getY() + 2));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertEquals(1, tree.count());
+ Assert.assertTrue(tree.getRoot().isLeaf());
+ }
+
+ @Test
+ public void testTransform_singleCut() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().insertCut(TestLine.X_AXIS);
+
+ Transform<TestPoint2D> t = new TestTransform2D((p) -> new TestPoint2D(p.getX(), p.getY() + 2));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertEquals(3, tree.count());
+
+ List<TestLineSegment> segments = getLineSegments(tree);
+ Assert.assertEquals(1, segments.size());
+
+ TestLineSegment seg = segments.get(0);
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(Double.NEGATIVE_INFINITY, 2), seg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(Double.POSITIVE_INFINITY, 2), seg.getEndPoint());
+ }
+
+ @Test
+ public void testTransform_multipleCuts() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(new TestPoint2D(-1, 0), new TestPoint2D(1, 0)),
+ new TestLineSegment(new TestPoint2D(-1, -1), new TestPoint2D(1, 1)),
+ new TestLineSegment(new TestPoint2D(3, 1), new TestPoint2D(3, 2))
+ ));
+
+ Transform<TestPoint2D> t = new TestTransform2D((p) -> new TestPoint2D(0.5 * p.getX(), p.getY() + 2));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertEquals(9, tree.count());
+
+ List<TestLineSegment> segments = getLineSegments(tree);
+ Assert.assertEquals(4, segments.size());
+
+ TestLineSegment segment1 = segments.get(0);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(Double.NEGATIVE_INFINITY, 2), segment1.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(Double.POSITIVE_INFINITY, 2), segment1.getEndPoint());
+
+ TestLineSegment segment2 = segments.get(1);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 2), segment2.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), segment2.getEndPoint());
+
+ TestLineSegment segment3 = segments.get(2);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(1.5, 2), segment3.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(1.5, 5), segment3.getEndPoint());
+
+ TestLineSegment segment4 = segments.get(3);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY), segment4.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 2), segment4.getEndPoint());
+ }
+
+ @Test
+ public void testTransform_xAxisReflection() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(new TestPoint2D(-1, 0), new TestPoint2D(1, 0)),
+ new TestLineSegment(new TestPoint2D(0, -1), new TestPoint2D(0, 1)),
+ new TestLineSegment(new TestPoint2D(0, 3), new TestPoint2D(3, 0))
+ ));
+
+ Transform<TestPoint2D> t = new TestTransform2D((p) -> new TestPoint2D(-p.getX(), p.getY()));
+
+ Map<TestPoint2D, TestNode> pointNodeMap = createPointNodeMap(tree, -5, 5);
+
+ // act
+ tree.transform(t);
+
+ // assert
+ checkTransformedPointNodeMap(tree, t, pointNodeMap);
+
+ List<TestLineSegment> segments = getLineSegments(tree);
+ Assert.assertEquals(4, segments.size());
+ }
+
+ @Test
+ public void testTransform_yAxisReflection() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(new TestPoint2D(-1, 0), new TestPoint2D(1, 0)),
+ new TestLineSegment(new TestPoint2D(0, -1), new TestPoint2D(0, 1)),
+ new TestLineSegment(new TestPoint2D(0, 3), new TestPoint2D(3, 0))
+ ));
+
+ Transform<TestPoint2D> t = new TestTransform2D((p) -> new TestPoint2D(p.getX(), -p.getY()));
+
+ Map<TestPoint2D, TestNode> pointNodeMap = createPointNodeMap(tree, -5, 5);
+
+ // act
+ tree.transform(t);
+
+ // assert
+ checkTransformedPointNodeMap(tree, t, pointNodeMap);
+
+ List<TestLineSegment> segments = getLineSegments(tree);
+ Assert.assertEquals(4, segments.size());
+ }
+
+ @Test
+ public void testTransform_xAndYAxisReflection() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(new TestPoint2D(-1, 0), new TestPoint2D(1, 0)),
+ new TestLineSegment(new TestPoint2D(0, -1), new TestPoint2D(0, 1)),
+ new TestLineSegment(new TestPoint2D(0, 3), new TestPoint2D(3, 0))
+ ));
+
+ Transform<TestPoint2D> t = new TestTransform2D((p) -> new TestPoint2D(-p.getX(), -p.getY()));
+
+ Map<TestPoint2D, TestNode> pointNodeMap = createPointNodeMap(tree, -5, 5);
+
+ // act
+ tree.transform(t);
+
+ // assert
+ checkTransformedPointNodeMap(tree, t, pointNodeMap);
+
+ List<TestLineSegment> segments = getLineSegments(tree);
+ Assert.assertEquals(4, segments.size());
+ }
+
+ @Test
+ public void testTreeString() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS);
+
+ // act
+ String str = tree.treeString();
+
+ // assert
+ String[] lines = str.split("\n");
+ Assert.assertEquals(5, lines.length);
+ Assert.assertTrue(lines[0].startsWith("TestNode[cut= TestLineSegment"));
+ Assert.assertTrue(lines[1].startsWith(" [-] TestNode[cut= TestLineSegment"));
+ Assert.assertEquals(" [-] TestNode[cut= null]", lines[2]);
+ Assert.assertEquals(" [+] TestNode[cut= null]", lines[3]);
+ Assert.assertEquals(" [+] TestNode[cut= null]", lines[4]);
+ }
+
+ @Test
+ public void testTreeString_emptyTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+
+ // act
+ String str = tree.treeString();
+
+ // assert
+ Assert.assertEquals("TestNode[cut= null]", str);
+ }
+
+ @Test
+ public void testTreeString_reachesMaxDepth() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS)
+ .getMinus().cut(new TestLine(new TestPoint2D(-2, 1), new TestPoint2D(0, 1)));
+
+ // act
+ String str = tree.treeString(1);
+
+ // assert
+ String[] lines = str.split("\n");
+ Assert.assertEquals(4, lines.length);
+ Assert.assertTrue(lines[0].startsWith("TestNode[cut= TestLineSegment"));
+ Assert.assertTrue(lines[1].startsWith(" [-] TestNode[cut= TestLineSegment"));
+ Assert.assertEquals(" ...", lines[2]);
+ Assert.assertEquals(" [+] TestNode[cut= null]", lines[3]);
+ }
+
+ @Test
+ public void testTreeString_zeroMaxDepth() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS)
+ .getMinus().cut(new TestLine(new TestPoint2D(-2, 1), new TestPoint2D(0, 1)));
+
+ // act
+ String str = tree.treeString(0);
+
+ // assert
+ String[] lines = str.split("\n");
+ Assert.assertEquals(2, lines.length);
+ Assert.assertTrue(lines[0].startsWith("TestNode[cut= TestLineSegment"));
+ Assert.assertTrue(lines[1].startsWith(" ..."));
+ }
+
+ @Test
+ public void testTreeString_negativeMaxDepth() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS)
+ .getMinus().cut(TestLine.Y_AXIS)
+ .getMinus().cut(new TestLine(new TestPoint2D(-2, 1), new TestPoint2D(0, 1)));
+
+ // act
+ String str = tree.treeString(-1);
+
+ // assert
+ Assert.assertEquals("", str);
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().insertCut(TestLine.Y_AXIS);
+
+ // act
+ String str = tree.toString();
+
+ // assert
+ String msg = "Unexpected toString() representation: " + str;
+
+ Assert.assertTrue(msg, str.contains("TestBSPTree"));
+ Assert.assertTrue(msg, str.contains("count= 3"));
+ Assert.assertTrue(msg, str.contains("height= 1"));
+ }
+
+ @Test
+ public void testNodeToString() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot().cut(TestLine.X_AXIS);
+
+ // act
+ String str = tree.getRoot().toString();
+
+ // assert
+ Assert.assertTrue(str.contains("TestNode"));
+ Assert.assertTrue(str.contains("cut= TestLineSegment"));
+ }
+
+ @Test
+ public void testSplitIntoTree() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ TestBSPTree minus = new TestBSPTree();
+ TestBSPTree plus = new TestBSPTree();
+
+ TestLine splitter = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(-1, 1));
+
+ // act
+ tree.splitIntoTrees(splitter, minus, plus);
+
+ // assert
+ TestLineSegment splitSegment = new TestLineSegment(Double.NEGATIVE_INFINITY,
+ Double.POSITIVE_INFINITY, splitter);
+
+ Assert.assertEquals(5, tree.count());
+ Assert.assertEquals(2, tree.height());
+
+ Assert.assertEquals(5, minus.count());
+ Assert.assertEquals(2, minus.height());
+
+ List<TestLineSegment> minusSegments = getLineSegments(minus);
+ Assert.assertEquals(2, minusSegments.size());
+ PartitionTestUtils.assertSegmentsEqual(splitSegment, minusSegments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(new TestLineSegment(Double.NEGATIVE_INFINITY, 0, TestLine.X_AXIS),
+ minusSegments.get(1));
+
+ Assert.assertEquals(7, plus.count());
+ Assert.assertEquals(3, plus.height());
+
+ List<TestLineSegment> plusSegments = getLineSegments(plus);
+ Assert.assertEquals(3, plusSegments.size());
+ PartitionTestUtils.assertSegmentsEqual(splitSegment, plusSegments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(new TestLineSegment(0, Double.POSITIVE_INFINITY, TestLine.X_AXIS),
+ plusSegments.get(1));
+ PartitionTestUtils.assertSegmentsEqual(new TestLineSegment(0, Double.POSITIVE_INFINITY, TestLine.Y_AXIS),
+ plusSegments.get(2));
+ }
+
+ @Test
+ public void testSplitIntoTree_minusOnly() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ TestBSPTree minus = new TestBSPTree();
+
+ TestLine splitter = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(-1, 1));
+
+ // act
+ tree.splitIntoTrees(splitter, minus, null);
+
+ // assert
+ TestLineSegment splitSegment = new TestLineSegment(Double.NEGATIVE_INFINITY,
+ Double.POSITIVE_INFINITY, splitter);
+
+ Assert.assertEquals(5, tree.count());
+ Assert.assertEquals(2, tree.height());
+
+ Assert.assertEquals(5, minus.count());
+ Assert.assertEquals(2, minus.height());
+
+ List<TestLineSegment> minusSegments = getLineSegments(minus);
+ Assert.assertEquals(2, minusSegments.size());
+ PartitionTestUtils.assertSegmentsEqual(splitSegment, minusSegments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(new TestLineSegment(Double.NEGATIVE_INFINITY, 0, TestLine.X_AXIS),
+ minusSegments.get(1));
+ }
+
+ @Test
+ public void testSplitIntoTree_plusOnly() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ tree.getRoot()
+ .cut(TestLine.X_AXIS)
+ .getMinus()
+ .cut(TestLine.Y_AXIS);
+
+ TestBSPTree plus = new TestBSPTree();
+
+ TestLine splitter = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(-1, 1));
+
+ // act
+ tree.splitIntoTrees(splitter, null, plus);
+
+ // assert
+ TestLineSegment splitSegment = new TestLineSegment(Double.NEGATIVE_INFINITY,
+ Double.POSITIVE_INFINITY, splitter);
+
+ Assert.assertEquals(5, tree.count());
+ Assert.assertEquals(2, tree.height());
+
+ Assert.assertEquals(7, plus.count());
+ Assert.assertEquals(3, plus.height());
+
+ List<TestLineSegment> plusSegments = getLineSegments(plus);
+ Assert.assertEquals(3, plusSegments.size());
+ PartitionTestUtils.assertSegmentsEqual(splitSegment, plusSegments.get(0));
+ PartitionTestUtils.assertSegmentsEqual(new TestLineSegment(0, Double.POSITIVE_INFINITY, TestLine.X_AXIS),
+ plusSegments.get(1));
+ PartitionTestUtils.assertSegmentsEqual(new TestLineSegment(0, Double.POSITIVE_INFINITY, TestLine.Y_AXIS),
+ plusSegments.get(2));
+ }
+
+ private void assertNodesCopiedRecursive(final TestNode orig, final TestNode copy) {
+ Assert.assertNotSame(orig, copy);
+
+ Assert.assertEquals(orig.getCut(), copy.getCut());
+
+ if (!orig.isLeaf())
+ {
+ Assert.assertNotSame(orig.getMinus(), copy.getMinus());
+ Assert.assertNotSame(orig.getPlus(), copy.getPlus());
+
+ assertNodesCopiedRecursive(orig.getMinus(), copy.getMinus());
+ assertNodesCopiedRecursive(orig.getPlus(), copy.getPlus());
+ }
+ else {
+ Assert.assertNull(copy.getMinus());
+ Assert.assertNull(copy.getPlus());
+ }
+
+ Assert.assertEquals(orig.depth(), copy.depth());
+ Assert.assertEquals(orig.count(), copy.count());
+ }
+
+ private static List<TestLineSegment> getLineSegments(TestBSPTree tree) {
+ return tree.stream()
+ .filter(BSPTree.Node::isInternal)
+ .map(n -> (TestLineSegment) n.getCut())
+ .collect(Collectors.toList());
+ }
+
+ /** Create a map of points to the nodes that they resolve to in the
+ * given tree.
+ */
+ private static Map<TestPoint2D, TestNode> createPointNodeMap(TestBSPTree tree, int min, int max) {
+ Map<TestPoint2D, TestNode> map = new HashMap<>();
+
+ for (int x = min; x <= max; ++x) {
+ for (int y = min; y <= max; ++y) {
+ TestPoint2D pt = new TestPoint2D(x, y);
+ TestNode node = tree.findNode(pt, NodeCutRule.NODE);
+
+ map.put(pt, node);
+ }
+ }
+
+ return map;
+ }
+
+ /** Check that transformed points resolve to the same tree nodes that were found when the original
+ * points were resolved in the untransformed tree.
+ * @param transformed
+ * @param transform
+ * @param pointNodeMap
+ */
+ private static void checkTransformedPointNodeMap(TestBSPTree transformedTree, Transform<TestPoint2D> transform,
+ Map<TestPoint2D, TestNode> pointNodeMap) {
+
+ for (TestPoint2D pt : pointNodeMap.keySet()) {
+ TestNode expectedNode = pointNodeMap.get(pt);
+ TestPoint2D transformedPt = transform.apply(pt);
+
+ String msg = "Expected transformed point " + transformedPt + " to resolve to node " + expectedNode;
+ Assert.assertSame(msg, expectedNode, transformedTree.findNode(transformedPt, NodeCutRule.NODE));
+ }
+ }
+
+ private static class TestVisitor implements BSPTreeVisitor<TestPoint2D, TestNode> {
+
+ private final Order order;
+
+ private final List<TestNode> visited = new ArrayList<>();
+
+ public TestVisitor(Order order) {
+ this.order = order;
+ }
+
+ @Override
+ public void visit(TestNode node) {
+ visited.add(node);
+ }
+
+ @Override
+ public Order visitOrder(TestNode node) {
+ return order;
+ }
+
+ public List<TestNode> getVisited() {
+ return visited;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
new file mode 100644
index 0000000..32471b7
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
@@ -0,0 +1,2257 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestLineSegment;
+import org.apache.commons.geometry.core.partition.test.TestLineSegmentCollection;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partition.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree.AbstractRegionNode;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree.RegionSizeProperties;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AbstractRegionBSPTreeTest {
+
+ private TestRegionBSPTree tree;
+
+ private TestRegionNode root;
+
+ @Before
+ public void setup() {
+ tree = new TestRegionBSPTree();
+ root = tree.getRoot();
+ }
+
+ @Test
+ public void testDefaultConstructor() {
+ // assert
+ Assert.assertNotNull(root);
+ Assert.assertNull(root.getParent());
+
+ PartitionTestUtils.assertIsLeafNode(root);
+ Assert.assertFalse(root.isPlus());
+ Assert.assertFalse(root.isMinus());
+
+ Assert.assertSame(tree, root.getTree());
+
+ Assert.assertEquals(RegionLocation.INSIDE, root.getLocation());
+ }
+
+ @Test
+ public void testParameterizedConstructor_true() {
+ // act
+ tree = new TestRegionBSPTree(true);
+ root = tree.getRoot();
+
+ // assert
+ Assert.assertNotNull(root);
+ Assert.assertNull(root.getParent());
+
+ PartitionTestUtils.assertIsLeafNode(root);
+ Assert.assertFalse(root.isPlus());
+ Assert.assertFalse(root.isMinus());
+
+ Assert.assertSame(tree, root.getTree());
+
+ Assert.assertEquals(RegionLocation.INSIDE, root.getLocation());
+ }
+
+ @Test
+ public void testParameterizedConstructor_false() {
+ // act
+ tree = new TestRegionBSPTree(false);
+ root = tree.getRoot();
+
+ // assert
+ Assert.assertNotNull(root);
+ Assert.assertNull(root.getParent());
+
+ PartitionTestUtils.assertIsLeafNode(root);
+ Assert.assertFalse(root.isPlus());
+ Assert.assertFalse(root.isMinus());
+
+ Assert.assertSame(tree, root.getTree());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, root.getLocation());
+ }
+
+ @Test
+ public void testGetLocation_emptyRoot() {
+ // act/assert
+ Assert.assertEquals(RegionLocation.INSIDE, root.getLocation());
+ }
+
+ @Test
+ public void testGetLocation_singleCut() {
+ // arrange
+ root.insertCut(TestLine.X_AXIS);
+
+ // act/assert
+ Assert.assertNull(root.getLocation());
+ Assert.assertEquals(RegionLocation.INSIDE, root.getMinus().getLocation());
+ Assert.assertEquals(RegionLocation.OUTSIDE, root.getPlus().getLocation());
+ }
+
+ @Test
+ public void testGetLocation_multipleCuts() {
+ // arrange
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, -1))));
+
+ // act/assert
+ Assert.assertNull(root.getLocation());
+
+ TestRegionNode plus = root.getPlus();
+ Assert.assertNull(plus.getLocation());
+
+ TestRegionNode plusPlus = plus.getPlus();
+ Assert.assertEquals(RegionLocation.OUTSIDE, plusPlus.getLocation());
+
+ TestRegionNode plusMinus = plus.getMinus();
+ Assert.assertEquals(RegionLocation.INSIDE, plusMinus.getLocation());
+
+ TestRegionNode minus = root.getMinus();
+ Assert.assertNull(minus.getLocation());
+
+ TestRegionNode minusPlus = minus.getPlus();
+ Assert.assertEquals(RegionLocation.OUTSIDE, minusPlus.getLocation());
+
+ TestRegionNode minusMinus = minus.getMinus();
+ Assert.assertEquals(RegionLocation.INSIDE, minusMinus.getLocation());
+ }
+
+ @Test
+ public void testGetLocation_resetsLocationWhenNodeCleared() {
+ // arrange
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, -1))));
+
+ // act
+ root.getPlus().clearCut();
+ root.getMinus().clearCut();
+
+ // assert
+ Assert.assertNull(root.getLocation());
+
+ Assert.assertEquals(RegionLocation.INSIDE, root.getMinus().getLocation());
+ Assert.assertEquals(RegionLocation.OUTSIDE, root.getPlus().getLocation());
+ }
+
+ @Test
+ public void testGetLocation_resetRoot() {
+ // arrange
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, -1))));
+
+ TestRegionNode root = tree.getRoot();
+
+ // act
+ root.clearCut();
+
+ // assert
+ Assert.assertEquals(RegionLocation.INSIDE, root.getLocation());
+ }
+
+ @Test
+ public void testBoundaries_fullAndEmpty() {
+ // act/assert
+ tree.setFull();
+ Assert.assertFalse(tree.boundaries().iterator().hasNext());
+
+ tree.setEmpty();
+ Assert.assertFalse(tree.boundaries().iterator().hasNext());
+ }
+
+ @Test
+ public void testBoundaries_finite() {
+ // arrange
+ insertBox(tree, new TestPoint2D(0, 1), new TestPoint2D(1, 0));
+
+ // act
+ List<TestLineSegment> segments = new ArrayList<>();
+ for (ConvexSubHyperplane<TestPoint2D> sub : tree.boundaries()) {
+ segments.add((TestLineSegment) sub);
+ }
+
+ // assert
+ Assert.assertEquals(4, segments.size());
+
+ assertContainsSegment(segments, new TestPoint2D(0, 0), new TestPoint2D(1, 0));
+ assertContainsSegment(segments, new TestPoint2D(1, 0), new TestPoint2D(1, 1));
+ assertContainsSegment(segments, new TestPoint2D(1, 1), new TestPoint2D(0, 1));
+ assertContainsSegment(segments, new TestPoint2D(0, 1), new TestPoint2D(0, 0));
+ }
+
+ @Test
+ public void testBoundaries_finite_inverted() {
+ // arrange
+ insertBox(tree, new TestPoint2D(0, 1), new TestPoint2D(1, 0));
+ tree.complement();
+
+ // act
+ List<TestLineSegment> segments = new ArrayList<>();
+ for (ConvexSubHyperplane<TestPoint2D> sub : tree.boundaries()) {
+ segments.add((TestLineSegment) sub);
+ }
+
+ // assert
+ Assert.assertEquals(4, segments.size());
+
+ assertContainsSegment(segments, new TestPoint2D(0, 0), new TestPoint2D(0, 1));
+ assertContainsSegment(segments, new TestPoint2D(0, 1), new TestPoint2D(1, 1));
+ assertContainsSegment(segments, new TestPoint2D(1, 1), new TestPoint2D(1, 0));
+ assertContainsSegment(segments, new TestPoint2D(1, 0), new TestPoint2D(0, 0));
+ }
+
+ @Test
+ public void testGetBoundaries_fullAndEmpty() {
+ // act/assert
+ tree.setFull();
+ Assert.assertEquals(0, tree.getBoundaries().size());
+
+ tree.setEmpty();
+ Assert.assertEquals(0, tree.getBoundaries().size());
+ }
+
+ @Test
+ public void testGetBoundaries_finite() {
+ // arrange
+ insertBox(tree, new TestPoint2D(0, 1), new TestPoint2D(1, 0));
+
+ // act
+ List<TestLineSegment> segments = new ArrayList<>();
+ for (ConvexSubHyperplane<TestPoint2D> sub : tree.getBoundaries()) {
+ segments.add((TestLineSegment) sub);
+ }
+
+ // assert
+ Assert.assertEquals(4, segments.size());
+
+ assertContainsSegment(segments, new TestPoint2D(0, 0), new TestPoint2D(1, 0));
+ assertContainsSegment(segments, new TestPoint2D(1, 0), new TestPoint2D(1, 1));
+ assertContainsSegment(segments, new TestPoint2D(1, 1), new TestPoint2D(0, 1));
+ assertContainsSegment(segments, new TestPoint2D(0, 1), new TestPoint2D(0, 0));
+ }
+
+ @Test
+ public void testGetBoundaries_finite_inverted() {
+ // arrange
+ insertBox(tree, new TestPoint2D(0, 1), new TestPoint2D(1, 0));
+ tree.complement();
+
+ // act
+ List<TestLineSegment> segments = new ArrayList<>();
+ for (ConvexSubHyperplane<TestPoint2D> sub : tree.getBoundaries()) {
+ segments.add((TestLineSegment) sub);
+ }
+
+ // assert
+ Assert.assertEquals(4, segments.size());
+
+ assertContainsSegment(segments, new TestPoint2D(0, 0), new TestPoint2D(0, 1));
+ assertContainsSegment(segments, new TestPoint2D(0, 1), new TestPoint2D(1, 1));
+ assertContainsSegment(segments, new TestPoint2D(1, 1), new TestPoint2D(1, 0));
+ assertContainsSegment(segments, new TestPoint2D(1, 0), new TestPoint2D(0, 0));
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ // act/assert
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(3, 1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(-3, -1)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(-3, 1)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(3, -1)));
+
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(4, 5)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-4, -5)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(5, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(4, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(3, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(2, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(1, 0)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(0, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-1, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-2, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-3, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-4, 0)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(-5, 0)));
+ }
+
+ @Test
+ public void testClassify_emptyTree() {
+ // act/assert
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(TestPoint2D.ZERO));
+ }
+
+ @Test
+ public void testClassify_NaN() {
+ // act/assert
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(0, Double.NaN)));
+ }
+
+ @Test
+ public void testContains() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ // act/assert
+ Assert.assertTrue(tree.contains(new TestPoint2D(3, 1)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(-3, -1)));
+
+ Assert.assertFalse(tree.contains(new TestPoint2D(-3, 1)));
+ Assert.assertFalse(tree.contains(new TestPoint2D(3, -1)));
+
+ Assert.assertTrue(tree.contains(new TestPoint2D(4, 5)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(-4, -5)));
+
+ Assert.assertFalse(tree.contains(new TestPoint2D(5, 0)));
+
+ Assert.assertTrue(tree.contains(new TestPoint2D(4, 0)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(3, 0)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(2, 0)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(1, 0)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(0, 0)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(-1, 0)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(-2, 0)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(-3, 0)));
+ Assert.assertTrue(tree.contains(new TestPoint2D(-4, 0)));
+
+ Assert.assertFalse(tree.contains(new TestPoint2D(-5, 0)));
+ }
+
+ @Test
+ public void testSetFull() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ // act
+ tree.setFull();
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(TestPoint2D.ZERO));
+ Assert.assertTrue(tree.contains(TestPoint2D.ZERO));
+ }
+
+ @Test
+ public void testSetEmpty() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ // act
+ tree.setEmpty();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(TestPoint2D.ZERO));
+ Assert.assertFalse(tree.contains(TestPoint2D.ZERO));
+ }
+
+ @Test
+ public void testGetRegionSizeProperties_cachesValueBasedOnVersion() {
+ // act
+ RegionSizeProperties<TestPoint2D> first = tree.getRegionSizeProperties();
+ RegionSizeProperties<TestPoint2D> second = tree.getRegionSizeProperties();
+ tree.getRoot().cut(TestLine.X_AXIS);
+ RegionSizeProperties<TestPoint2D> third = tree.getRegionSizeProperties();
+
+ // assert
+ Assert.assertSame(first, second);
+ Assert.assertNotSame(second, third);
+
+ Assert.assertEquals(1, first.getSize(), PartitionTestUtils.EPS);
+ Assert.assertSame(TestPoint2D.ZERO, first.getBarycenter());
+ }
+
+ @Test
+ public void testGetSize() {
+ // act/assert
+ // make sure our stub value is pulled
+ Assert.assertEquals(1, tree.getSize(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testGetBarycenter() {
+ // act/assert
+ // make sure our stub value is pulled
+ Assert.assertSame(TestPoint2D.ZERO, tree.getBarycenter());
+ }
+
+ @Test
+ public void testGetBoundarySize_fullAndEmpty() {
+ // act/assert
+ Assert.assertEquals(0.0, fullTree().getBoundarySize(), PartitionTestUtils.EPS);
+ Assert.assertEquals(0.0, emptyTree().getBoundarySize(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testGetBoundarySize_infinite() {
+ // arrange
+ TestRegionBSPTree halfPos = new TestRegionBSPTree(true);
+ halfPos.getRoot().cut(TestLine.X_AXIS);
+
+ TestRegionBSPTree halfPosComplement = new TestRegionBSPTree(true);
+ halfPosComplement.complement(halfPos);
+
+ // act/assert
+ Assert.assertEquals(Double.POSITIVE_INFINITY, halfPos.getBoundarySize(), PartitionTestUtils.EPS);
+ Assert.assertEquals(Double.POSITIVE_INFINITY, halfPosComplement.getBoundarySize(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testGetBoundarySize_alignedCuts() {
+ // arrange
+ TestPoint2D p0 = TestPoint2D.ZERO;
+ TestPoint2D p1 = new TestPoint2D(0, 3);
+
+ TestRegionNode node = tree.getRoot();
+
+ tree.cutNode(node, new TestLineSegment(p0, p1));
+ node = node.getMinus();
+
+ tree.cutNode(node, new TestLineSegment(0, 0, new TestLine(p1, new TestPoint2D(-1, 3))));
+ node = node.getMinus();
+
+ tree.cutNode(node, new TestLineSegment(p1, p0));
+ node = node.getMinus();
+
+ tree.cutNode(node, new TestLineSegment(0, 0, new TestLine(p0, new TestPoint2D(1, 3))));
+
+ // act/assert
+ Assert.assertEquals(6, tree.getBoundarySize(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testGetBoundarySize_box() {
+ // arrange
+ insertBox(tree, new TestPoint2D(2, 2), new TestPoint2D(4, 1));
+
+ // act/assert
+ Assert.assertEquals(6.0, tree.getBoundarySize(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testGetBoundarySize_boxComplement() {
+ // arrange
+ insertBox(tree, new TestPoint2D(2, 2), new TestPoint2D(4, 1));
+ tree.complement();
+
+ // act/assert
+ Assert.assertEquals(6.0, tree.getBoundarySize(), PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testGetBoundarySize_recomputesAfterChange() {
+ // arrange
+ insertBox(tree, new TestPoint2D(2, 2), new TestPoint2D(4, 1));
+
+ // act
+ double first = tree.getBoundarySize();
+ tree.insert(new TestLineSegment(new TestPoint2D(3, 1), new TestPoint2D(3, 2)));
+
+ double second = tree.getBoundarySize();
+ double third = tree.getBoundarySize();
+
+ // assert
+ Assert.assertEquals(6.0, first, PartitionTestUtils.EPS);
+ Assert.assertEquals(4.0, second, PartitionTestUtils.EPS);
+ Assert.assertEquals(4.0, third, PartitionTestUtils.EPS);
+ }
+
+ @Test
+ public void testGetCutBoundary_emptyTree() {
+ // act
+ RegionCutBoundary<TestPoint2D> boundary = root.getCutBoundary();
+
+ // assert
+ Assert.assertNull(boundary);
+ }
+
+ @Test
+ public void tetsGetCutBoundary_singleCut() {
+ // arrange
+ tree.insert(new TestLineSegment(new TestPoint2D(0, 0), new TestPoint2D(1, 0)));
+
+ // act
+ RegionCutBoundary<TestPoint2D> boundary = root.getCutBoundary();
+
+ // assert
+ Assert.assertTrue(boundary.getInsideFacing().isEmpty());
+
+ assertCutBoundarySegment(boundary.getOutsideFacing(),
+ new TestPoint2D(Double.NEGATIVE_INFINITY, 0.0), new TestPoint2D(Double.POSITIVE_INFINITY, 0.0));
+ }
+
+ @Test
+ public void tetsGetCutBoundary_singleCut_leafNode() {
+ // arrange
+ tree.insert(new TestLineSegment(new TestPoint2D(0, 0), new TestPoint2D(1, 0)));
+
+ // act
+ RegionCutBoundary<TestPoint2D> boundary = root.getMinus().getCutBoundary();
+
+ // assert
+ Assert.assertNull(boundary);
+ }
+
+ @Test
+ public void tetsGetCutBoundary_singleCorner() {
+ // arrange
+ tree.insert(new TestLineSegment(new TestPoint2D(-1, 0), new TestPoint2D(1, 0)));
+ tree.insert(new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1)));
+
+ // act/assert
+ RegionCutBoundary<TestPoint2D> rootBoundary = root.getCutBoundary();
+
+ Assert.assertTrue(rootBoundary.getInsideFacing().isEmpty());
+ assertCutBoundarySegment(rootBoundary.getOutsideFacing(),
+ new TestPoint2D(Double.NEGATIVE_INFINITY, 0.0), TestPoint2D.ZERO);
+
+ RegionCutBoundary<TestPoint2D> childBoundary = tree.getRoot().getMinus().getCutBoundary();
+ Assert.assertTrue(childBoundary.getInsideFacing().isEmpty());
+ assertCutBoundarySegment(childBoundary.getOutsideFacing(),
+ TestPoint2D.ZERO, new TestPoint2D(0.0, Double.POSITIVE_INFINITY));
+ }
+
+ @Test
+ public void tetsGetCutBoundary_leafNode() {
+ // arrange
+ tree.insert(new TestLineSegment(new TestPoint2D(-1, 0), new TestPoint2D(1, 0)));
+ tree.insert(new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1)));
+
+ // act/assert
+ Assert.assertNull(root.getPlus().getCutBoundary());
+ Assert.assertNull(root.getMinus().getMinus().getCutHyperplane());
+ Assert.assertNull(root.getMinus().getPlus().getCutHyperplane());
+ }
+
+ @Test
+ public void testFullEmpty_fullTree() {
+ // act/assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertEquals(RegionLocation.INSIDE, tree.getRoot().getLocation());
+ }
+
+ @Test
+ public void testFullEmpty_emptyTree() {
+ // arrange
+ tree.complement();
+
+ // act/assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.getRoot().getLocation());
+ }
+
+ @Test
+ public void testTransform_noCuts() {
+ // arrange
+ Transform<TestPoint2D> t = new TestTransform2D(p -> new TestPoint2D(p.getX(), p.getY() + 2));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ assertPointLocations(tree, RegionLocation.INSIDE, TestPoint2D.ZERO);
+ }
+
+ @Test
+ public void testTransform_singleCut() {
+ // arrange
+ tree.getRoot().insertCut(TestLine.X_AXIS);
+
+ Transform<TestPoint2D> t = new TestTransform2D(p -> new TestPoint2D(p.getX(), p.getY() + 2));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ assertPointLocations(tree, RegionLocation.OUTSIDE,
+ new TestPoint2D(0, -1), TestPoint2D.ZERO, new TestPoint2D(0, 1));
+
+ assertPointLocations(tree, RegionLocation.BOUNDARY, new TestPoint2D(0, 2));
+
+ assertPointLocations(tree, RegionLocation.INSIDE,
+ new TestPoint2D(0, 3), new TestPoint2D(0, 4));
+ }
+
+ @Test
+ public void testTransform_multipleCuts() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ Transform<TestPoint2D> t = new TestTransform2D(p -> new TestPoint2D(0.5 * p.getX(), p.getY() + 5));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ assertPointLocations(tree, RegionLocation.INSIDE,
+ new TestPoint2D(0, 5), new TestPoint2D(-1, 4), new TestPoint2D(1, 6));
+
+ assertPointLocations(tree, RegionLocation.BOUNDARY,
+ new TestPoint2D(-2, 4), new TestPoint2D(2, 6));
+
+ assertPointLocations(tree, RegionLocation.OUTSIDE,
+ new TestPoint2D(-3, 5), new TestPoint2D(3, 5));
+ }
+
+ @Test
+ public void testTransform_xAxisReflection() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ Transform<TestPoint2D> t = new TestTransform2D(p -> new TestPoint2D(-p.getX(), p.getY()));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ assertPointLocations(tree, RegionLocation.INSIDE,
+ TestPoint2D.ZERO, new TestPoint2D(-1, 1), new TestPoint2D(1, -1));
+
+ assertPointLocations(tree, RegionLocation.BOUNDARY,
+ new TestPoint2D(0, 1), new TestPoint2D(0, -1),
+ new TestPoint2D(-4, 0), new TestPoint2D(4, 0));
+
+ assertPointLocations(tree, RegionLocation.OUTSIDE,
+ new TestPoint2D(1, 1), new TestPoint2D(-1, -1));
+ }
+
+ @Test
+ public void testTransform_yAxisReflection() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ Transform<TestPoint2D> t = new TestTransform2D(p -> new TestPoint2D(p.getX(), -p.getY()));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ assertPointLocations(tree, RegionLocation.INSIDE,
+ TestPoint2D.ZERO, new TestPoint2D(1, -1), new TestPoint2D(-1, 1));
+
+ assertPointLocations(tree, RegionLocation.BOUNDARY,
+ new TestPoint2D(0, 1), new TestPoint2D(0, -1),
+ new TestPoint2D(-4, 0), new TestPoint2D(4, 0));
+
+ assertPointLocations(tree, RegionLocation.OUTSIDE,
+ new TestPoint2D(-1, -1), new TestPoint2D(1, 1));
+ }
+
+ @Test
+ public void testTransform_xAndYAxisReflection() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ Transform<TestPoint2D> t = new TestTransform2D(p -> new TestPoint2D(-p.getX(), -p.getY()));
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ assertPointLocations(tree, RegionLocation.INSIDE,
+ TestPoint2D.ZERO, new TestPoint2D(1, 1), new TestPoint2D(-1, -1));
+
+ assertPointLocations(tree, RegionLocation.BOUNDARY,
+ new TestPoint2D(0, 1), new TestPoint2D(0, -1),
+ new TestPoint2D(-4, 0), new TestPoint2D(4, 0));
+
+ assertPointLocations(tree, RegionLocation.OUTSIDE,
+ new TestPoint2D(-1, 1), new TestPoint2D(1, -1));
+ }
+
+ @Test
+ public void testTransform_resetsCutBoundary() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ TestRegionNode node = tree.findNode(new TestPoint2D(1, 1)).getParent();
+
+
+ Transform<TestPoint2D> t = new TestTransform2D(p -> new TestPoint2D(0.5 * p.getX(), p.getY() + 5));
+
+ // act
+ RegionCutBoundary<TestPoint2D> origBoundary = node.getCutBoundary();
+
+ tree.transform(t);
+
+ RegionCutBoundary<TestPoint2D> resultBoundary = node.getCutBoundary();
+
+ // assert
+ Assert.assertNotSame(origBoundary, resultBoundary);
+
+ assertCutBoundarySegment(origBoundary.getOutsideFacing(), new TestPoint2D(4, 5), new TestPoint2D(-1, 0));
+
+ assertCutBoundarySegment(resultBoundary.getOutsideFacing(), new TestPoint2D(2, 10), new TestPoint2D(-0.5, 5));
+ }
+
+ @Test
+ public void testComplement_rootOnly() {
+ // act
+ tree.complement();
+
+ // assert
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, root.getLocation());
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(TestPoint2D.ZERO));
+ }
+
+ @Test
+ public void testComplement_singleCut() {
+ // arrange
+ root.insertCut(TestLine.X_AXIS);
+
+ // act
+ tree.complement();
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, root.getMinus().getLocation());
+ Assert.assertEquals(RegionLocation.INSIDE, root.getPlus().getLocation());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(0, 1)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(TestPoint2D.ZERO));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(0, -1)));
+ }
+
+ @Test
+ public void testComplement_skewedBowtie() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ // act
+ tree.complement();
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(3, 1)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(-3, -1)));
+
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(-3, 1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(3, -1)));
+
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(4, 5)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-4, -5)));
+
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(5, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(4, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(3, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(2, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(1, 0)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(0, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-1, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-2, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-3, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-4, 0)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(-5, 0)));
+ }
+
+ @Test
+ public void testComplement_addCutAfterComplement() {
+ // arrange
+ tree.insert(new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)));
+ tree.complement();
+
+ // act
+ tree.insert(new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1)));
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(TestPoint2D.ZERO));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(1, 1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(-1, 1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(1, -1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(-1, -1)));
+ }
+
+ @Test
+ public void testComplement_clearCutAfterComplement() {
+ // arrange
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1))
+ ));
+ tree.complement();
+
+ // act
+ root.getMinus().clearCut();
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(TestPoint2D.ZERO));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(1, 1)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(-1, 1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(1, -1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(-1, -1)));
+ }
+
+ @Test
+ public void testComplement_clearRootAfterComplement() {
+ // arrange
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1))
+ ));
+ tree.complement();
+
+ // act
+ root.clearCut();
+
+ // assert
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(TestPoint2D.ZERO));
+ }
+
+ @Test
+ public void testComplement_isReversible_root() {
+ // act
+ tree.complement();
+ tree.complement();
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertTrue(tree.isFull());
+
+ Assert.assertEquals(RegionLocation.INSIDE, root.getLocation());
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(TestPoint2D.ZERO));
+ }
+
+ @Test
+ public void testComplement_isReversible_skewedBowtie() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ // act
+ tree.complement();
+ tree.complement();
+
+ // assert
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(3, 1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(-3, -1)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(-3, 1)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(3, -1)));
+
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(4, 5)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-4, -5)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(5, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(4, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(3, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(2, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(1, 0)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(0, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-1, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-2, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-3, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, tree.classify(new TestPoint2D(-4, 0)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(new TestPoint2D(-5, 0)));
+ }
+
+ @Test
+ public void testComplement_getCutBoundary() {
+ // arrange
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1))));
+ tree.complement();
+
+ // act
+ RegionCutBoundary<TestPoint2D> xAxisBoundary = root.getCutBoundary();
+ RegionCutBoundary<TestPoint2D> yAxisBoundary = root.getMinus().getCutBoundary();
+
+ // assert
+ Assert.assertTrue(xAxisBoundary.getOutsideFacing().isEmpty());
+ Assert.assertFalse(xAxisBoundary.getInsideFacing().isEmpty());
+
+ TestLineSegmentCollection xAxisInsideFacing = (TestLineSegmentCollection) xAxisBoundary.getInsideFacing();
+ Assert.assertEquals(1, xAxisInsideFacing.getLineSegments().size());
+
+ TestLineSegment xAxisSeg = xAxisInsideFacing.getLineSegments().get(0);
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(Double.NEGATIVE_INFINITY, 0), xAxisSeg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, xAxisSeg.getEndPoint());
+
+ Assert.assertTrue(yAxisBoundary.getOutsideFacing().isEmpty());
+ Assert.assertFalse(yAxisBoundary.getInsideFacing().isEmpty());
+
+ TestLineSegmentCollection yAxisInsideFacing = (TestLineSegmentCollection) yAxisBoundary.getInsideFacing();
+ Assert.assertEquals(1, yAxisInsideFacing.getLineSegments().size());
+
+ TestLineSegment yAxisSeg = yAxisInsideFacing.getLineSegments().get(0);
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, yAxisSeg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, Double.POSITIVE_INFINITY), yAxisSeg.getEndPoint());
+ }
+
+ @Test
+ public void testComplementOf_rootOnly() {
+ // arrange
+ TestRegionBSPTree other = fullTree();
+ insertSkewedBowtie(other);
+
+ // act
+ other.complement(tree);
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertTrue(tree.isFull());
+
+ Assert.assertEquals(RegionLocation.INSIDE, root.getLocation());
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(TestPoint2D.ZERO));
+
+ Assert.assertTrue(other.isEmpty());
+ Assert.assertFalse(other.isFull());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, other.getRoot().getLocation());
+ Assert.assertEquals(RegionLocation.OUTSIDE, other.classify(TestPoint2D.ZERO));
+ }
+
+ @Test
+ public void testComplementOf_skewedBowtie() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ TestRegionBSPTree other = fullTree();
+
+ // act
+ other.complement(tree);
+
+ // assert
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(3, 1)));
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(new TestPoint2D(-3, -1)));
+
+ Assert.assertFalse(other.isEmpty());
+ Assert.assertFalse(other.isFull());
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, other.classify(new TestPoint2D(3, 1)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, other.classify(new TestPoint2D(-3, -1)));
+
+ Assert.assertEquals(RegionLocation.INSIDE, other.classify(new TestPoint2D(-3, 1)));
+ Assert.assertEquals(RegionLocation.INSIDE, other.classify(new TestPoint2D(3, -1)));
+
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(4, 5)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(-4, -5)));
+
+ Assert.assertEquals(RegionLocation.INSIDE, other.classify(new TestPoint2D(5, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(4, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(3, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(2, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(1, 0)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, other.classify(new TestPoint2D(0, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(-1, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(-2, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(-3, 0)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, other.classify(new TestPoint2D(-4, 0)));
+ Assert.assertEquals(RegionLocation.INSIDE, other.classify(new TestPoint2D(-5, 0)));
+ }
+
+ @Test
+ public void testCopy() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ // act
+ TestRegionBSPTree copy = fullTree();
+ copy.copy(tree);
+
+ // assert
+ Assert.assertNotSame(tree, copy);
+ Assert.assertEquals(tree.count(), copy.count());
+
+ List<RegionLocation> origLocations = new ArrayList<>();
+ tree.forEach(n -> origLocations.add(n.getLocationValue()));
+
+ List<RegionLocation> copyLocations = new ArrayList<>();
+ copy.forEach(n -> copyLocations.add(n.getLocationValue()));
+
+ Assert.assertEquals(origLocations, copyLocations);
+ }
+
+ @Test
+ public void testExtract() {
+ // arrange
+ insertSkewedBowtie(tree);
+
+ TestRegionBSPTree result = fullTree();
+
+ TestPoint2D pt = new TestPoint2D(2, 2);
+
+ // act
+ result.extract(tree.findNode(pt));
+
+ // assert
+ assertPointLocations(result, RegionLocation.INSIDE,
+ new TestPoint2D(0, 0.5), new TestPoint2D(2, 2));
+ assertPointLocations(result, RegionLocation.OUTSIDE,
+ new TestPoint2D(-2, 2),
+ new TestPoint2D(-2, -2), new TestPoint2D(0, -0.5), new TestPoint2D(-2, 2));
+
+ assertPointLocations(tree, RegionLocation.INSIDE,
+ new TestPoint2D(0, 0.5), new TestPoint2D(2, 2),
+ new TestPoint2D(-2, -2), new TestPoint2D(0, -0.5));
+ assertPointLocations(tree, RegionLocation.OUTSIDE,
+ new TestPoint2D(2, -2), new TestPoint2D(-2, 2));
+ }
+
+ @Test
+ public void testExtract_complementedTree() {
+ // arrange
+ insertSkewedBowtie(tree);
+ tree.complement();
+
+ TestRegionBSPTree result = fullTree();
+
+ TestPoint2D pt = new TestPoint2D(2, 2);
+
+ // act
+ result.extract(tree.findNode(pt));
+
+ // assert
+ assertPointLocations(result, RegionLocation.OUTSIDE,
+ new TestPoint2D(0, 0.5), new TestPoint2D(2, 2));
+ assertPointLocations(result, RegionLocation.INSIDE,
+ new TestPoint2D(-2, 2),
+ new TestPoint2D(-2, -2), new TestPoint2D(0, -0.5), new TestPoint2D(-2, 2));
+
+ assertPointLocations(tree, RegionLocation.OUTSIDE,
+ new TestPoint2D(0, 0.5), new TestPoint2D(2, 2),
+ new TestPoint2D(-2, -2), new TestPoint2D(0, -0.5));
+ assertPointLocations(tree, RegionLocation.INSIDE,
+ new TestPoint2D(2, -2), new TestPoint2D(-2, 2));
+ }
+
+
+ @Test
+ public void testUnion_singleNodeTrees() {
+ // act/assert
+ unionChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ unionChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+
+ unionChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+
+ unionChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+ }
+
+ @Test
+ public void testUnion_simpleCrossingCuts() {
+ // act/assert
+ unionChecker(AbstractRegionBSPTreeTest::xAxisTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(false)
+ .count(3)
+ .check();
+
+ unionChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::xAxisTree)
+ .full(false)
+ .empty(false)
+ .count(3)
+ .check();
+
+ unionChecker(AbstractRegionBSPTreeTest::yAxisTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+
+ unionChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::yAxisTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+
+ unionChecker(AbstractRegionBSPTreeTest::xAxisTree, AbstractRegionBSPTreeTest::yAxisTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(1, 1), new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
+ .outside(new TestPoint2D(1, -1))
+ .boundary(TestPoint2D.ZERO)
+ .count(5)
+ .check(tree -> {
+ TestLineSegment seg = (TestLineSegment) tree.getRoot().getPlus().getCut();
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.NEGATIVE_INFINITY), seg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, seg.getEndPoint());
+ });
+ }
+
+ @Test
+ public void testUnion_boxTreeWithSingleCutTree() {
+ // arrange
+ Supplier<TestRegionBSPTree> boxFactory = () -> {
+ TestRegionBSPTree box = fullTree();
+ insertBox(box, TestPoint2D.ZERO, new TestPoint2D(2, -4));
+ return box;
+ };
+
+ Supplier<TestRegionBSPTree> horizonalFactory = () -> {
+ TestRegionBSPTree horizontal = fullTree();
+ horizontal.getRoot().insertCut(new TestLine(new TestPoint2D(2, 2), new TestPoint2D(0, 2)));
+
+ return horizontal;
+ };
+
+ // act/assert
+ unionChecker(horizonalFactory, boxFactory)
+ .count(3)
+ .inside(TestPoint2D.ZERO, new TestPoint2D(1, 1), new TestPoint2D(1, -1))
+ .outside(new TestPoint2D(0, 3), new TestPoint2D(3, 3))
+ .boundary(new TestPoint2D(-1, 2), new TestPoint2D(3, 2))
+ .check();
+ }
+
+ @Test
+ public void testUnion_treeWithComplement() {
+ // arrange
+ Supplier<TestRegionBSPTree> treeFactory = () -> {
+ TestRegionBSPTree tree = fullTree();
+ insertSkewedBowtie(tree);
+
+ return tree;
+ };
+ Supplier<TestRegionBSPTree> complementFactory = () -> {
+ TestRegionBSPTree tree = treeFactory.get();
+ tree.complement();
+
+ return tree;
+ };
+
+ // act/assert
+ unionChecker(treeFactory, complementFactory)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+ }
+
+ @Test
+ public void testIntersection_singleNodeTrees() {
+ // act/assert
+ intersectionChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ intersectionChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ intersectionChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ intersectionChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+ }
+
+ @Test
+ public void testIntersection_simpleCrossingCuts() {
+ // act/assert
+ intersectionChecker(AbstractRegionBSPTreeTest::xAxisTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ intersectionChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::xAxisTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ intersectionChecker(AbstractRegionBSPTreeTest::yAxisTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
+ .outside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
+ .boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
+ .count(3)
+ .check();
+
+ intersectionChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::yAxisTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
+ .outside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
+ .boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
+ .count(3)
+ .check();
+
+ intersectionChecker(AbstractRegionBSPTreeTest::xAxisTree, AbstractRegionBSPTreeTest::yAxisTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(-1, 1))
+ .outside(new TestPoint2D(1, 1), new TestPoint2D(1, -1), new TestPoint2D(-1, -1))
+ .boundary(TestPoint2D.ZERO)
+ .count(5)
+ .check(tree -> {
+ TestLineSegment seg = (TestLineSegment) tree.getRoot().getMinus().getCut();
+
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, seg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.POSITIVE_INFINITY), seg.getEndPoint());
+ });
+ }
+
+ @Test
+ public void testIntersection_boxTreeWithSingleCutTree() {
+ // arrange
+ Supplier<TestRegionBSPTree> boxFactory = () -> {
+ TestRegionBSPTree box = fullTree();
+ insertBox(box, TestPoint2D.ZERO, new TestPoint2D(2, -4));
+ return box;
+ };
+
+ Supplier<TestRegionBSPTree> horizonalFactory = () -> {
+ TestRegionBSPTree horizontal = fullTree();
+ horizontal.getRoot().insertCut(new TestLine(new TestPoint2D(2, -2), new TestPoint2D(0, -2)));
+
+ return horizontal;
+ };
+
+ // act/assert
+ intersectionChecker(horizonalFactory, boxFactory)
+ .inside(new TestPoint2D(1, -3))
+ .outside(new TestPoint2D(1, -1), new TestPoint2D(-1, -3),
+ new TestPoint2D(1, -5), new TestPoint2D(3, -3))
+ .boundary(new TestPoint2D(0, -2), new TestPoint2D(2, -2),
+ new TestPoint2D(0, -4), new TestPoint2D(2, -4))
+ .count(9)
+ .check();
+ }
+
+ @Test
+ public void testIntersection_treeWithComplement() {
+ // arrange
+ Supplier<TestRegionBSPTree> treeFactory = () -> {
+ TestRegionBSPTree tree = fullTree();
+ insertSkewedBowtie(tree);
+
+ return tree;
+ };
+ Supplier<TestRegionBSPTree> complementFactory = () -> {
+ TestRegionBSPTree tree = treeFactory.get();
+ tree.complement();
+
+ return tree;
+ };
+
+ // act/assert
+ intersectionChecker(treeFactory, complementFactory)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+ }
+
+ @Test
+ public void testDifference_singleNodeTrees() {
+ // act/assert
+ differenceChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ differenceChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+
+ differenceChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ differenceChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+ }
+
+ @Test
+ public void testDifference_simpleCrossingCuts() {
+ // act/assert
+ differenceChecker(AbstractRegionBSPTreeTest::xAxisTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(0, 1))
+ .outside(new TestPoint2D(0, -1))
+ .boundary(TestPoint2D.ZERO)
+ .count(3)
+ .check();
+
+ differenceChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::xAxisTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ differenceChecker(AbstractRegionBSPTreeTest::yAxisTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ differenceChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::yAxisTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
+ .outside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
+ .boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
+ .count(3)
+ .check();
+
+ differenceChecker(AbstractRegionBSPTreeTest::xAxisTree, AbstractRegionBSPTreeTest::yAxisTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(1, 1))
+ .outside(new TestPoint2D(-1, 1), new TestPoint2D(1, -1), new TestPoint2D(-1, -1))
+ .boundary(TestPoint2D.ZERO)
+ .count(5)
+ .check(tree -> {
+ TestLineSegment seg = (TestLineSegment) tree.getRoot().getMinus().getCut();
+
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, seg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.POSITIVE_INFINITY), seg.getEndPoint());
+ });
+ }
+
+ @Test
+ public void testDifference_boxTreeWithSingleCutTree() {
+ // arrange
+ Supplier<TestRegionBSPTree> boxFactory = () -> {
+ TestRegionBSPTree box = fullTree();
+ insertBox(box, TestPoint2D.ZERO, new TestPoint2D(2, -4));
+ return box;
+ };
+
+ Supplier<TestRegionBSPTree> horizonalFactory = () -> {
+ TestRegionBSPTree horizontal = fullTree();
+ horizontal.getRoot().insertCut(new TestLine(new TestPoint2D(2, -2), new TestPoint2D(0, -2)));
+
+ return horizontal;
+ };
+
+ // act/assert
+ differenceChecker(horizonalFactory, boxFactory)
+ .inside(new TestPoint2D(-1, -3), new TestPoint2D(-1, -5),
+ new TestPoint2D(1, -5), new TestPoint2D(3, -5),
+ new TestPoint2D(4, -3))
+ .outside(new TestPoint2D(1, -1), new TestPoint2D(1, -1),
+ new TestPoint2D(3, -1), new TestPoint2D(1, -3))
+ .boundary(new TestPoint2D(0, -2), new TestPoint2D(0, -4),
+ new TestPoint2D(2, -4), new TestPoint2D(2, -2))
+ .count(9)
+ .check();
+ }
+
+ @Test
+ public void testDifference_treeWithCopy() {
+ // arrange
+ Supplier<TestRegionBSPTree> treeFactory = () -> {
+ TestRegionBSPTree tree = fullTree();
+ insertSkewedBowtie(tree);
+
+ return tree;
+ };
+
+ // act/assert
+ differenceChecker(treeFactory, treeFactory)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+ }
+
+ @Test
+ public void testXor_singleNodeTrees() {
+ // act/assert
+ xorChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+
+ xorChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+
+ xorChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+
+ xorChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(false)
+ .empty(true)
+ .count(1)
+ .check();
+ }
+
+ @Test
+ public void testXor_simpleCrossingCuts() {
+ // act/assert
+ xorChecker(AbstractRegionBSPTreeTest::xAxisTree, AbstractRegionBSPTreeTest::emptyTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(0, 1))
+ .outside(new TestPoint2D(0, -1))
+ .boundary(TestPoint2D.ZERO)
+ .count(3)
+ .check();
+
+ xorChecker(AbstractRegionBSPTreeTest::emptyTree, AbstractRegionBSPTreeTest::xAxisTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(0, 1))
+ .outside(new TestPoint2D(0, -1))
+ .boundary(TestPoint2D.ZERO)
+ .count(3)
+ .check();
+
+ xorChecker(AbstractRegionBSPTreeTest::yAxisTree, AbstractRegionBSPTreeTest::fullTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
+ .outside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
+ .boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
+ .count(3)
+ .check();
+
+ xorChecker(AbstractRegionBSPTreeTest::fullTree, AbstractRegionBSPTreeTest::yAxisTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
+ .outside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
+ .boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
+ .count(3)
+ .check();
+
+ xorChecker(AbstractRegionBSPTreeTest::xAxisTree, AbstractRegionBSPTreeTest::yAxisTree)
+ .full(false)
+ .empty(false)
+ .inside(new TestPoint2D(1, 1), new TestPoint2D(-1, -1))
+ .outside(new TestPoint2D(-1, 1), new TestPoint2D(1, -1))
+ .boundary(TestPoint2D.ZERO)
+ .count(7)
+ .check(tree -> {
+ TestLineSegment minusSeg = (TestLineSegment) tree.getRoot().getMinus().getCut();
+
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, minusSeg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.POSITIVE_INFINITY), minusSeg.getEndPoint());
+
+ TestLineSegment plusSeg = (TestLineSegment) tree.getRoot().getPlus().getCut();
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.NEGATIVE_INFINITY), plusSeg.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, plusSeg.getEndPoint());
+ });
+ }
+
+ @Test
+ public void testXor_boxTreeWithSingleCutTree() {
+ // arrange
+ Supplier<TestRegionBSPTree> boxFactory = () -> {
+ TestRegionBSPTree box = fullTree();
+ insertBox(box, TestPoint2D.ZERO, new TestPoint2D(2, -4));
+ return box;
+ };
+
+ Supplier<TestRegionBSPTree> horizonalFactory = () -> {
+ TestRegionBSPTree horizontal = fullTree();
+ horizontal.getRoot().insertCut(new TestLine(new TestPoint2D(2, -2), new TestPoint2D(0, -2)));
+
+ return horizontal;
+ };
+
+ // act/assert
+ xorChecker(horizonalFactory, boxFactory)
+ .inside(new TestPoint2D(-1, -3), new TestPoint2D(-1, -5),
+ new TestPoint2D(1, -5), new TestPoint2D(3, -5),
+ new TestPoint2D(4, -3), new TestPoint2D(1, -1))
+ .outside(new TestPoint2D(3, -1), new TestPoint2D(1, -3),
+ new TestPoint2D(1, 1), new TestPoint2D(5, -1))
+ .boundary(new TestPoint2D(0, -2), new TestPoint2D(0, -4),
+ new TestPoint2D(2, -4), new TestPoint2D(2, -2),
+ TestPoint2D.ZERO, new TestPoint2D(2, 0))
+ .count(15)
+ .check();
+ }
+
+ @Test
+ public void testXor_treeWithComplement() {
+ // arrange
+ Supplier<TestRegionBSPTree> treeFactory = () -> {
+ TestRegionBSPTree tree = fullTree();
+ insertSkewedBowtie(tree);
+
+ return tree;
+ };
+ Supplier<TestRegionBSPTree> complementFactory = () -> {
+ TestRegionBSPTree tree = treeFactory.get();
+ tree.complement();
+
+ return tree;
+ };
+
+ // act/assert
+ xorChecker(treeFactory, complementFactory)
+ .full(true)
+ .empty(false)
+ .count(1)
+ .check();
+ }
+
+ @Test
+ public void testProject_emptyAndFull() {
+ // arrange
+ TestRegionBSPTree full = fullTree();
+ TestRegionBSPTree empty = emptyTree();
+
+ // act/assert
+ Assert.assertNull(full.project(new TestPoint2D(0, 0)));
+ Assert.assertNull(empty.project(new TestPoint2D(-1, 1)));
+ }
+
+ @Test
+ public void testProject_halfSpace() {
+ // arrange
+ TestRegionBSPTree tree = fullTree();
+ tree.getRoot().cut(TestLine.X_AXIS);
+
+ // act/assert
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, tree.project(TestPoint2D.ZERO));
+
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, tree.project(new TestPoint2D(0, 7)));
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, tree.project(new TestPoint2D(0, -7)));
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(4, 0), tree.project(new TestPoint2D(4, 10)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-5, 0), tree.project(new TestPoint2D(-5, -2)));
+ }
+
+ @Test
+ public void testProject_box() {
+ // arrange
+ TestRegionBSPTree tree = fullTree();
+ insertBox(tree, new TestPoint2D(0, 1), new TestPoint2D(1, 0));
+
+ // act/assert
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, tree.project(TestPoint2D.ZERO));
+ PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, tree.project(new TestPoint2D(-1, -4)));
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(1, 1), tree.project(new TestPoint2D(2, 9)));
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.5, 1), tree.project(new TestPoint2D(0.5, 3)));
+ }
+
+ @Test
+ public void testSplit_empty() {
+ // arrange
+ TestRegionBSPTree tree = emptyTree();
+
+ // act
+ Split<TestRegionBSPTree> split = tree.split(TestLine.X_AXIS);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ TestRegionBSPTree tree = fullTree();
+
+ // act
+ Split<TestRegionBSPTree> split = tree.split(TestLine.X_AXIS);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ TestRegionBSPTree minus = split.getMinus();
+ assertPointLocations(minus, RegionLocation.INSIDE,
+ new TestPoint2D(-1, 1), new TestPoint2D(0, 1), new TestPoint2D(1, 1));
+ assertPointLocations(minus, RegionLocation.BOUNDARY,
+ new TestPoint2D(-1, 0), new TestPoint2D(0, 0), new TestPoint2D(1, 0));
+ assertPointLocations(minus, RegionLocation.OUTSIDE,
+ new TestPoint2D(-1, -1), new TestPoint2D(0, -1), new TestPoint2D(1, -1));
+
+ TestRegionBSPTree plus = split.getPlus();
+ assertPointLocations(plus, RegionLocation.OUTSIDE,
+ new TestPoint2D(-1, 1), new TestPoint2D(0, 1), new TestPoint2D(1, 1));
+ assertPointLocations(plus, RegionLocation.BOUNDARY,
+ new TestPoint2D(-1, 0), new TestPoint2D(0, 0), new TestPoint2D(1, 0));
+ assertPointLocations(plus, RegionLocation.INSIDE,
+ new TestPoint2D(-1, -1), new TestPoint2D(0, -1), new TestPoint2D(1, -1));
+ }
+
+ @Test
+ public void testSplit_halfSpace() {
+ // arrange
+ TestRegionBSPTree tree = fullTree();
+ tree.getRoot().insertCut(TestLine.X_AXIS);
+
+ TestLine splitter = TestLine.Y_AXIS;
+
+ // act
+ Split<TestRegionBSPTree> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ TestRegionBSPTree minus = split.getMinus();
+ assertPointLocations(minus, RegionLocation.INSIDE, new TestPoint2D(-1, 1));
+ assertPointLocations(minus, RegionLocation.OUTSIDE,
+ new TestPoint2D(1, 1), new TestPoint2D(-1, -1), new TestPoint2D(1, -1));
+
+ TestRegionBSPTree plus = split.getPlus();
+ assertPointLocations(plus, RegionLocation.INSIDE, new TestPoint2D(1, 1));
+ assertPointLocations(plus, RegionLocation.OUTSIDE,
+ new TestPoint2D(-1, 1), new TestPoint2D(-1, -1), new TestPoint2D(1, -1));
+ }
+
+ @Test
+ public void testSplit_box() {
+ // arrange
+ TestRegionBSPTree tree = fullTree();
+ insertBox(tree, new TestPoint2D(0, 1), new TestPoint2D(1, 0));
+
+ TestLine splitter = new TestLine(new TestPoint2D(1, 0), new TestPoint2D(0, 1));
+
+ // act
+ Split<TestRegionBSPTree> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ TestRegionBSPTree minus = split.getMinus();
+ assertPointLocations(minus, RegionLocation.INSIDE, new TestPoint2D(0.25, 0.25));
+ assertPointLocations(minus, RegionLocation.BOUNDARY,
+ new TestPoint2D(0.5, 0), new TestPoint2D(0, 0.5));
+ assertPointLocations(minus, RegionLocation.OUTSIDE,
+ new TestPoint2D(1, 0.5), new TestPoint2D(0.5, 1), new TestPoint2D(0.75, 0.75));
+
+ TestRegionBSPTree plus = split.getPlus();
+ assertPointLocations(plus, RegionLocation.INSIDE, new TestPoint2D(0.75, 0.75));
+ assertPointLocations(plus, RegionLocation.OUTSIDE,
+ new TestPoint2D(0.5, 0), new TestPoint2D(0, 0.5), new TestPoint2D(0.25, 0.25));
+ assertPointLocations(plus, RegionLocation.BOUNDARY,
+ new TestPoint2D(1, 0.5), new TestPoint2D(0.5, 1));
+ }
+
+ @Test
+ public void testSplit_box_onMinusOnly() {
+ // arrange
+ TestRegionBSPTree tree = fullTree();
+ insertBox(tree, new TestPoint2D(0, 1), new TestPoint2D(1, 0));
+
+ TestLine splitter = new TestLine(new TestPoint2D(2, 0), new TestPoint2D(1, 1));
+
+ // act
+ Split<TestRegionBSPTree> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ TestRegionBSPTree minus = split.getMinus();
+ assertPointLocations(minus, RegionLocation.INSIDE, new TestPoint2D(0.5, 0.5));
+ assertPointLocations(minus, RegionLocation.BOUNDARY,
+ new TestPoint2D(0.5, 0), new TestPoint2D(0, 0.5),
+ new TestPoint2D(1, 0.5), new TestPoint2D(0.5, 1));
+
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_box_onPlusOnly() {
+ // arrange
+ TestRegionBSPTree tree = fullTree();
+ insertBox(tree, new TestPoint2D(0, 1), new TestPoint2D(1, 0));
+
+ TestLine splitter = new TestLine(new TestPoint2D(0, 0), new TestPoint2D(-1, 1));
+
+ // act
+ Split<TestRegionBSPTree> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+
+ TestRegionBSPTree plus = split.getPlus();
+ assertPointLocations(plus, RegionLocation.INSIDE, new TestPoint2D(0.5, 0.5));
+ assertPointLocations(plus, RegionLocation.BOUNDARY,
+ new TestPoint2D(0.5, 0), new TestPoint2D(0, 0.5),
+ new TestPoint2D(1, 0.5), new TestPoint2D(0.5, 1));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ TestRegionBSPTree tree = fullTree();
+ tree.getRoot().cut(TestLine.X_AXIS);
+
+ // act
+ String str = tree.toString();
+
+ // assert
+ Assert.assertEquals("TestRegionBSPTree[count= 3, height= 1]", str);
+ Assert.assertTrue(tree.getRoot().toString().contains("TestRegionNode"));
+ }
+
+ /** Assert that all given points lie in the expected location of the region.
+ * @param tree region tree
+ * @param points points to test
+ * @param location expected location of all points
+ */
+ private static void assertPointLocations(final TestRegionBSPTree tree, final RegionLocation location,
+ final TestPoint2D ... points) {
+ assertPointLocations(tree, location, Arrays.asList(points));
+ }
+
+ /** Assert that all given points lie in the expected location of the region.
+ * @param tree region tree
+ * @param points points to test
+ * @param location expected location of all points
+ */
+ private static void assertPointLocations(final TestRegionBSPTree tree, final RegionLocation location,
+ final List<TestPoint2D> points) {
+
+ for (TestPoint2D p : points) {
+ Assert.assertEquals("Unexpected location for point " + p, location, tree.classify(p));
+ }
+ }
+
+ private static MergeChecker unionChecker(
+ final Supplier<TestRegionBSPTree> r1,
+ final Supplier<TestRegionBSPTree> r2) {
+
+ MergeOperation constOperation = (a, b) -> {
+ TestRegionBSPTree result = fullTree();
+ result.union(a, b);
+ return result;
+ };
+
+ MergeOperation inPlaceOperation = (a, b) -> {
+ a.union(b);
+ return a;
+ };
+
+ return new MergeChecker(r1, r2, constOperation, inPlaceOperation);
+ }
+
+ private static MergeChecker intersectionChecker(
+ final Supplier<TestRegionBSPTree> tree1Factory,
+ final Supplier<TestRegionBSPTree> tree2Factory) {
+
+ MergeOperation constOperation = (a, b) -> {
+ TestRegionBSPTree result = fullTree();
+ result.intersection(a, b);
+ return result;
+ };
+
+ MergeOperation inPlaceOperation = (a, b) -> {
+ a.intersection(b);
+ return a;
+ };
+
+ return new MergeChecker(tree1Factory, tree2Factory, constOperation, inPlaceOperation);
+ }
+
+ private static MergeChecker differenceChecker(
+ final Supplier<TestRegionBSPTree> tree1Factory,
+ final Supplier<TestRegionBSPTree> tree2Factory) {
+
+ MergeOperation constOperation = (a, b) -> {
+ TestRegionBSPTree result = fullTree();
+ result.difference(a, b);
+ return result;
+ };
+
+ MergeOperation inPlaceOperation = (a, b) -> {
+ a.difference(b);
+ return a;
+ };
+
+ return new MergeChecker(tree1Factory, tree2Factory, constOperation, inPlaceOperation);
+ }
+
+ private static MergeChecker xorChecker(
+ final Supplier<TestRegionBSPTree> tree1Factory,
+ final Supplier<TestRegionBSPTree> tree2Factory) {
+
+ MergeOperation constOperation = (a, b) -> {
+ TestRegionBSPTree result = fullTree();
+ result.xor(a, b);
+ return result;
+ };
+
+ MergeOperation inPlaceOperation = (a, b) -> {
+ a.xor(b);
+ return a;
+ };
+
+ return new MergeChecker(tree1Factory, tree2Factory, constOperation, inPlaceOperation);
+ }
+
+ private static void insertBox(final TestRegionBSPTree tree, final TestPoint2D upperLeft, final TestPoint2D lowerRight) {
+ final TestPoint2D upperRight = new TestPoint2D(lowerRight.getX(), upperLeft.getY());
+ final TestPoint2D lowerLeft = new TestPoint2D(upperLeft.getX(), lowerRight.getY());
+
+ tree.insert(Arrays.asList(
+ new TestLineSegment(lowerRight, upperRight),
+ new TestLineSegment(upperRight, upperLeft),
+ new TestLineSegment(upperLeft, lowerLeft),
+ new TestLineSegment(lowerLeft, lowerRight)
+ ));
+ }
+
+ private static void insertSkewedBowtie(final TestRegionBSPTree tree) {
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+
+ new TestLineSegment(new TestPoint2D(4, 0), new TestPoint2D(4, 1)),
+ new TestLineSegment(new TestPoint2D(-4, 0), new TestPoint2D(-4, -1)),
+
+ new TestLineSegment(new TestPoint2D(4, 5), new TestPoint2D(-1, 0)),
+ new TestLineSegment(new TestPoint2D(-4, -5), new TestPoint2D(1, 0))));
+ }
+
+ private static void assertCutBoundarySegment(final SubHyperplane<TestPoint2D> boundary, final TestPoint2D start, final TestPoint2D end) {
+ Assert.assertFalse("Expected boundary to not be empty", boundary.isEmpty());
+
+ TestLineSegmentCollection segmentCollection = (TestLineSegmentCollection) boundary;
+ Assert.assertEquals(1, segmentCollection.getLineSegments().size());
+
+ TestLineSegment segment = segmentCollection.getLineSegments().get(0);
+ PartitionTestUtils.assertPointsEqual(start, segment.getStartPoint());
+ PartitionTestUtils.assertPointsEqual(end, segment.getEndPoint());
+ }
+
+ private static void assertContainsSegment(final List<TestLineSegment> boundaries, final TestPoint2D start, final TestPoint2D end) {
+ boolean found = false;
+ for (TestLineSegment seg : boundaries) {
+ TestPoint2D startPt = seg.getStartPoint();
+ TestPoint2D endPt = seg.getEndPoint();
+
+ if (PartitionTestUtils.PRECISION.eq(start.getX(), startPt.getX()) &&
+ PartitionTestUtils.PRECISION.eq(start.getY(), startPt.getY()) &&
+ PartitionTestUtils.PRECISION.eq(end.getX(), endPt.getX()) &&
+ PartitionTestUtils.PRECISION.eq(end.getY(), endPt.getY())) {
+ found = true;
+ break;
+ }
+ }
+
+ Assert.assertTrue("Expected to find segment start= " + start + ", end= " + end , found);
+ }
+
+ private static TestRegionBSPTree emptyTree() {
+ return new TestRegionBSPTree(false);
+ }
+
+ private static TestRegionBSPTree fullTree() {
+ return new TestRegionBSPTree(true);
+ }
+
+ private static TestRegionBSPTree xAxisTree() {
+ TestRegionBSPTree tree = fullTree();
+ tree.getRoot().cut(TestLine.X_AXIS);
+
+ return tree;
+ }
+
+ private static TestRegionBSPTree yAxisTree() {
+ TestRegionBSPTree tree = fullTree();
+ tree.getRoot().cut(TestLine.Y_AXIS);
+
+ return tree;
+ }
+
+ /** Helper interface used when testing tree merge operations.
+ */
+ @FunctionalInterface
+ private static interface MergeOperation {
+ TestRegionBSPTree apply(TestRegionBSPTree tree1, TestRegionBSPTree tree2);
+ }
+
+ /** Helper class with a fluent API used to construct assert conditions on tree merge operations.
+ */
+ private static class MergeChecker {
+
+ /** First tree in the merge operation */
+ private final Supplier<TestRegionBSPTree> tree1Factory;
+
+ /** Second tree in the merge operation */
+ private final Supplier<TestRegionBSPTree> tree2Factory;
+
+ /** Merge operation that does not modify either input tree */
+ private final MergeOperation constOperation;
+
+ /** Merge operation that stores the result in the first input tree
+ * and leaves the second one unmodified.
+ */
+ private final MergeOperation inPlaceOperation;
+
+ /** If true, the resulting tree will be printed to stdout to help with
+ * debugging.
+ */
+ private boolean print;
+
+ /** The expected node count of the merged tree */
+ private int expectedCount = -1;
+
+ /** The expected full state of the merged tree */
+ private boolean expectedFull = false;
+
+ /** The expected empty state of the merged tree */
+ private boolean expectedEmpty = false;
+
+ /** Points expected to lie in the inside of the region */
+ private List<TestPoint2D> insidePoints = new ArrayList<>();
+
+ /** Points expected to lie on the outside of the region */
+ private List<TestPoint2D> outsidePoints = new ArrayList<>();
+
+ /** Points expected to lie on the boundary of the region */
+ private List<TestPoint2D> boundaryPoints = new ArrayList<>();
+
+ /** Construct a new instance that will verify the output of performing the given merge operation
+ * on the input trees.
+ * @param tree1 first tree in the merge operation
+ * @param tree2 second tree in the merge operation
+ * @param constOperation object that performs the merge operation in a form that
+ * leaves both argument unmodified
+ * @param inPlaceOperation object that performs the merge operation in a form
+ * that stores the result in the first input tree and leaves the second
+ * input unchanged.
+ */
+ public MergeChecker(
+ final Supplier<TestRegionBSPTree> tree1Factory,
+ final Supplier<TestRegionBSPTree> tree2Factory,
+ final MergeOperation constOperation,
+ final MergeOperation inPlaceOperation) {
+
+ this.tree1Factory = tree1Factory;
+ this.tree2Factory = tree2Factory;
+ this.constOperation = constOperation;
+ this.inPlaceOperation = inPlaceOperation;
+ }
+
+ /** Set the expected node count of the merged tree
+ * @param expectedCount the expected node count of the merged tree
+ * @return this instance
+ */
+ public MergeChecker count(final int expectedCount) {
+ this.expectedCount = expectedCount;
+ return this;
+ }
+
+ /** Set the expected full state of the merged tree.
+ * @param expectedFull the expected full state of the merged tree.
+ * @return this instance
+ */
+ public MergeChecker full(final boolean expectedFull) {
+ this.expectedFull = expectedFull;
+ return this;
+ }
+
+ /** Set the expected empty state of the merged tree.
+ * @param expectedEmpty the expected empty state of the merged tree.
+ * @return this instance
+ */
+ public MergeChecker empty(final boolean expectedEmpty) {
+ this.expectedEmpty = expectedEmpty;
+ return this;
+ }
+
+ /** Add points expected to be on the inside of the merged region.
+ * @param points point expected to be on the inside of the merged
+ * region
+ * @return this instance
+ */
+ public MergeChecker inside(TestPoint2D ... points) {
+ insidePoints.addAll(Arrays.asList(points));
+ return this;
+ }
+
+ /** Add points expected to be on the outside of the merged region.
+ * @param points point expected to be on the outside of the merged
+ * region
+ * @return this instance
+ */
+ public MergeChecker outside(TestPoint2D ... points) {
+ outsidePoints.addAll(Arrays.asList(points));
+ return this;
+ }
+
+ /** Add points expected to be on the boundary of the merged region.
+ * @param points point expected to be on the boundary of the merged
+ * region
+ * @return this instance
+ */
+ public MergeChecker boundary(TestPoint2D ... points) {
+ boundaryPoints.addAll(Arrays.asList(points));
+ return this;
+ }
+
+ /** Set the flag for printing the merged tree to stdout before performing assertions.
+ * This can be useful for debugging tests.
+ * @param print if set to true, the merged tree will be printed to stdout
+ * @return this instance
+ */
+ @SuppressWarnings("unused")
+ public MergeChecker print(final boolean print) {
+ this.print = print;
+ return this;
+ }
+
+ /** Perform the merge operation and verify the output.
+ */
+ public void check() {
+ check(null);
+ }
+
+ /** Perform the merge operation and verify the output. The given consumer
+ * is passed the merge result and can be used to perform extra assertions.
+ * @param assertions consumer that will be passed the merge result; may
+ * be null
+ */
+ public void check(final Consumer<TestRegionBSPTree> assertions) {
+ checkConst(assertions);
+ checkInPlace(assertions);
+ }
+
+ private void checkConst(final Consumer<TestRegionBSPTree> assertions) {
+ checkInternal(false, constOperation, assertions);
+ }
+
+ private void checkInPlace(final Consumer<TestRegionBSPTree> assertions) {
+ checkInternal(true, inPlaceOperation, assertions);
+ }
+
+ private void checkInternal(final boolean inPlace, final MergeOperation operation,
+ final Consumer<TestRegionBSPTree> assertions) {
+
+ final TestRegionBSPTree tree1 = tree1Factory.get();
+ final TestRegionBSPTree tree2 = tree2Factory.get();
+
+ // store the number of nodes in each tree before the operation
+ final int tree1BeforeCount = tree1.count();
+ final int tree2BeforeCount = tree2.count();
+
+ // perform the operation
+ final TestRegionBSPTree result = operation.apply(tree1, tree2);
+
+ if (print) {
+ System.out.println((inPlace ? "In Place" : "Const") + " Result:");
+ System.out.println(result.treeString());
+ }
+
+ // verify the internal consistency of all of the involved trees
+ PartitionTestUtils.assertTreeStructure(tree1);
+ PartitionTestUtils.assertTreeStructure(tree2);
+ PartitionTestUtils.assertTreeStructure(result);
+
+ // check full/empty status
+ Assert.assertEquals("Unexpected tree 'full' property", expectedFull, result.isFull());
+ Assert.assertEquals("Unexpected tree 'empty' property", expectedEmpty, result.isEmpty());
+
+ // check the node count
+ if (expectedCount > -1) {
+ Assert.assertEquals("Unexpected node count", expectedCount, result.count());
+ }
+
+ // check in place or not
+ if (inPlace) {
+ Assert.assertSame("Expected merge operation to be in place", tree1, result);
+ }
+ else {
+ Assert.assertNotSame("Expected merge operation to return a new instance", tree1, result);
+
+ // make sure that tree1 wasn't modified
+ Assert.assertEquals("Tree 1 node count should not have changed", tree1BeforeCount, tree1.count());
+ }
+
+ // make sure that tree2 wasn't modified
+ Assert.assertEquals("Tree 2 node count should not have changed", tree2BeforeCount, tree2.count());
+
+ // check region point locations
+ assertPointLocations(result, RegionLocation.INSIDE, insidePoints);
+ assertPointLocations(result, RegionLocation.OUTSIDE, outsidePoints);
+ assertPointLocations(result, RegionLocation.BOUNDARY, boundaryPoints);
+
+ // pass the result to the given function for any additional assertions
+ if (assertions != null) {
+ assertions.accept(result);
+ }
+ }
+ }
+
+ private static class TestRegionBSPTree extends AbstractRegionBSPTree<TestPoint2D, TestRegionNode> {
+
+ private static final long serialVersionUID = 20190405L;
+
+ TestRegionBSPTree() {
+ this(true);
+ }
+
+ TestRegionBSPTree(final boolean full) {
+ super(full);
+ }
+
+ /**
+ * Expose the direct node cut method for easier creation of test tree structures.
+ */
+ @Override
+ public void cutNode(final TestRegionNode node, final ConvexSubHyperplane<TestPoint2D> cut) {
+ super.cutNode(node, cut);
+ }
+
+ @Override
+ protected TestRegionNode createNode() {
+ return new TestRegionNode(this);
+ }
+
+ @Override
+ protected RegionSizeProperties<TestPoint2D> computeRegionSizeProperties() {
+ // return a set of stub values
+ return new RegionSizeProperties<>(1, TestPoint2D.ZERO);
+ }
+
+ @Override
+ public boolean contains(TestPoint2D pt) {
+ return classify(pt) != RegionLocation.OUTSIDE;
+ }
+
+ @Override
+ public Split<TestRegionBSPTree> split(Hyperplane<TestPoint2D> splitter) {
+ return split(splitter, new TestRegionBSPTree(), new TestRegionBSPTree());
+ }
+ }
+
+ private static class TestRegionNode extends AbstractRegionNode<TestPoint2D, TestRegionNode> {
+
+ private static final long serialVersionUID = 20190405L;
+
+ protected TestRegionNode(AbstractBSPTree<TestPoint2D, TestRegionNode> tree) {
+ super(tree);
+ }
+
+ @Override
+ protected TestRegionNode getSelf() {
+ return this;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTreeTest.java
new file mode 100644
index 0000000..e69425a
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTreeTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import java.util.Arrays;
+
+import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestLineSegment;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.bsp.AttributeBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.AttributeBSPTree.AttributeNode;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AttributeBSPTreeTest {
+
+ @Test
+ public void testInitialization() {
+ // act
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>();
+
+ // assert
+ AttributeNode<TestPoint2D, String> root = tree.getRoot();
+
+ Assert.assertNotNull(root);
+ Assert.assertNull(root.getParent());
+ Assert.assertNull(root.getAttribute());
+
+ PartitionTestUtils.assertIsLeafNode(root);
+ Assert.assertFalse(root.isPlus());
+ Assert.assertFalse(root.isMinus());
+
+ Assert.assertSame(tree, root.getTree());
+ }
+
+ @Test
+ public void testInitialNodeValue_null() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>();
+ tree.getRoot().cut(TestLine.X_AXIS);
+
+ // act/assert
+ Assert.assertNull(tree.getRoot().getAttribute());
+ Assert.assertNull(tree.getRoot().getPlus().getAttribute());
+ Assert.assertNull(tree.getRoot().getMinus().getAttribute());
+ }
+
+ @Test
+ public void testInitialNodeValue_givenValue() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>("a");
+ tree.getRoot().cut(TestLine.X_AXIS);
+
+ // act/assert
+ Assert.assertEquals("a", tree.getRoot().getAttribute());
+ Assert.assertEquals("a", tree.getRoot().getPlus().getAttribute());
+ Assert.assertEquals("a", tree.getRoot().getMinus().getAttribute());
+ }
+
+ @Test
+ public void testSetAttribute_node() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>();
+ AttributeNode<TestPoint2D, String> root = tree.getRoot();
+
+ // act
+ root.setAttribute("a");
+
+ // assert
+ Assert.assertEquals("a", root.getAttribute());
+ }
+
+ @Test
+ public void testAttr_node() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>();
+ AttributeNode<TestPoint2D, String> root = tree.getRoot();
+
+ // act
+ AttributeNode<TestPoint2D, String> result = root.attr("a");
+
+ // assert
+ Assert.assertSame(root, result);
+ Assert.assertEquals("a", root.getAttribute());
+ }
+
+ @Test
+ public void testCopy_rootOnly() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>();
+ tree.getRoot().attr("abc");
+
+ // act
+ AttributeBSPTree<TestPoint2D, String> copy = new AttributeBSPTree<>();
+ copy.copy(tree);
+
+ // assert
+ Assert.assertEquals(1, copy.count());
+ Assert.assertEquals("abc", copy.getRoot().getAttribute());
+ }
+
+ @Test
+ public void testCopy_withCuts() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
+ new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(0, 1))
+ ));
+
+ tree.findNode(new TestPoint2D(1, 1)).attr("a");
+ tree.findNode(new TestPoint2D(-1, 1)).attr("b");
+ tree.findNode(new TestPoint2D(0, -1)).attr("c");
+
+ // act
+ AttributeBSPTree<TestPoint2D, String> copy = new AttributeBSPTree<>();
+ copy.copy(tree);
+
+ // assert
+ Assert.assertEquals(5, copy.count());
+ Assert.assertEquals("a", copy.findNode(new TestPoint2D(1, 1)).getAttribute());
+ Assert.assertEquals("b", copy.findNode(new TestPoint2D(-1, 1)).getAttribute());
+ Assert.assertEquals("c", copy.findNode(new TestPoint2D(0, -1)).getAttribute());
+ }
+
+ @Test
+ public void testExtract() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>();
+ tree.insert(Arrays.asList(
+ new TestLineSegment(new TestPoint2D(-1, -1), new TestPoint2D(1, 1)),
+ new TestLineSegment(new TestPoint2D(-1, 1), new TestPoint2D(1, -1)),
+
+ new TestLineSegment(new TestPoint2D(-1, 3), new TestPoint2D(1, 3)),
+ new TestLineSegment(new TestPoint2D(3, 1), new TestPoint2D(3, -1)),
+ new TestLineSegment(new TestPoint2D(1, -3), new TestPoint2D(-1, -3)),
+ new TestLineSegment(new TestPoint2D(-3, -1), new TestPoint2D(-3, 1))
+ ));
+
+ AttributeNode<TestPoint2D, String> root = tree.getRoot();
+
+ root.attr("R");
+ root.getMinus().attr("A");
+ root.getPlus().attr("B");
+
+ root.getMinus().getMinus().forEach(n -> n.attr("a"));
+ root.getMinus().getPlus().forEach(n -> n.attr("b"));
+
+ root.getPlus().getPlus().forEach(n -> n.attr("c"));
+ root.getPlus().getMinus().forEach(n -> n.attr("d"));
+
+ AttributeBSPTree<TestPoint2D, String> result = new AttributeBSPTree<>();
+
+ // act
+ result.extract(tree.findNode(new TestPoint2D(0, 1)));
+
+ // assert
+ Assert.assertEquals(7, result.count());
+ Assert.assertEquals(15, tree.count());
+
+ // check result tree attributes
+ AttributeNode<TestPoint2D, String> resultRoot = result.getRoot();
+ Assert.assertEquals("R", resultRoot.getAttribute());
+ Assert.assertEquals("A", resultRoot.getMinus().getAttribute());
+ Assert.assertEquals("B", resultRoot.getPlus().getAttribute());
+
+ Assert.assertEquals("a", resultRoot.getMinus().getMinus().getAttribute());
+ Assert.assertEquals("b", resultRoot.getMinus().getPlus().getAttribute());
+
+ Assert.assertEquals(2, resultRoot.getMinus().height());
+ Assert.assertEquals(0, resultRoot.getPlus().height());
+
+ PartitionTestUtils.assertTreeStructure(result);
+
+ // check original tree attributes
+ Assert.assertEquals("R", root.getAttribute());
+ Assert.assertEquals("A", root.getMinus().getAttribute());
+ Assert.assertEquals("B", root.getPlus().getAttribute());
+
+ Assert.assertEquals("a", root.getMinus().getMinus().getAttribute());
+ Assert.assertEquals("b", root.getMinus().getPlus().getAttribute());
+ Assert.assertEquals("c", root.getPlus().getPlus().getAttribute());
+ Assert.assertEquals("d", root.getPlus().getMinus().getAttribute());
+
+ Assert.assertEquals(2, root.getMinus().height());
+ Assert.assertEquals(2, root.getPlus().height());
+
+ PartitionTestUtils.assertTreeStructure(tree);
+ }
+
+ @Test
+ public void testNodeToString() {
+ // arrange
+ AttributeBSPTree<TestPoint2D, String> tree = new AttributeBSPTree<TestPoint2D, String>();
+ tree.getRoot().cut(TestLine.X_AXIS).attr("abc");
+
+ // act
+ String str = tree.getRoot().toString();
+
+ // assert
+ Assert.assertTrue(str.contains("AttributeNode"));
+ Assert.assertTrue(str.contains("cut= TestLineSegment"));
+ Assert.assertTrue(str.contains("attribute= abc"));
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
new file mode 100644
index 0000000..2e692ae
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import org.apache.commons.geometry.core.partition.test.TestBSPTree;
+import org.apache.commons.geometry.core.partition.test.TestBSPTree.TestNode;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.ClosestFirstVisitor;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.FarthestFirstVisitor;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.Order;
+import org.apache.commons.geometry.core.partition.test.TestLine;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BSPTreeVisitorTest {
+
+ @Test
+ public void testDefaultVisitOrder() {
+ // arrange
+ BSPTreeVisitor<TestPoint2D, TestNode> visitor = n -> {};
+
+ // act/assert
+ Assert.assertEquals(Order.NODE_MINUS_PLUS, visitor.visitOrder(null));
+ }
+
+ @Test
+ public void testClosestFirst() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestNode root = tree.getRoot();
+ root.cut(TestLine.X_AXIS);
+ root.getMinus().cut(TestLine.Y_AXIS);
+ root.getPlus().cut(TestLine.Y_AXIS);
+
+ // act
+ checkClosestFirst(new TestPoint2D(1, 1), root, Order.MINUS_NODE_PLUS);
+ checkClosestFirst(new TestPoint2D(1, 1), root.getMinus(), Order.PLUS_NODE_MINUS);
+ checkClosestFirst(new TestPoint2D(1, 1), root.getPlus(), Order.PLUS_NODE_MINUS);
+
+ checkClosestFirst(new TestPoint2D(-1, 1), root, Order.MINUS_NODE_PLUS);
+ checkClosestFirst(new TestPoint2D(-1, 1), root.getMinus(), Order.MINUS_NODE_PLUS);
+ checkClosestFirst(new TestPoint2D(-1, 1), root.getPlus(), Order.MINUS_NODE_PLUS);
+
+ checkClosestFirst(new TestPoint2D(-1, -1), root, Order.PLUS_NODE_MINUS);
+ checkClosestFirst(new TestPoint2D(-1, -1), root.getMinus(), Order.MINUS_NODE_PLUS);
+ checkClosestFirst(new TestPoint2D(-1, -1), root.getPlus(), Order.MINUS_NODE_PLUS);
+
+ checkClosestFirst(new TestPoint2D(1, -1), root, Order.PLUS_NODE_MINUS);
+ checkClosestFirst(new TestPoint2D(1, -1), root.getMinus(), Order.PLUS_NODE_MINUS);
+ checkClosestFirst(new TestPoint2D(1, -1), root.getPlus(), Order.PLUS_NODE_MINUS);
+
+ checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
+ checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
+ checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
+ }
+
+ @Test
+ public void testFarthestFirst() {
+ // arrange
+ TestBSPTree tree = new TestBSPTree();
+ TestNode root = tree.getRoot();
+ root.cut(TestLine.X_AXIS);
+ root.getMinus().cut(TestLine.Y_AXIS);
+ root.getPlus().cut(TestLine.Y_AXIS);
+
+ // act
+ checkFarthestFirst(new TestPoint2D(1, 1), root, Order.PLUS_NODE_MINUS);
+ checkFarthestFirst(new TestPoint2D(1, 1), root.getMinus(), Order.MINUS_NODE_PLUS);
+ checkFarthestFirst(new TestPoint2D(1, 1), root.getPlus(), Order.MINUS_NODE_PLUS);
+
+ checkFarthestFirst(new TestPoint2D(-1, 1), root, Order.PLUS_NODE_MINUS);
+ checkFarthestFirst(new TestPoint2D(-1, 1), root.getMinus(), Order.PLUS_NODE_MINUS);
+ checkFarthestFirst(new TestPoint2D(-1, 1), root.getPlus(), Order.PLUS_NODE_MINUS);
+
+ checkFarthestFirst(new TestPoint2D(-1, -1), root, Order.MINUS_NODE_PLUS);
+ checkFarthestFirst(new TestPoint2D(-1, -1), root.getMinus(), Order.PLUS_NODE_MINUS);
+ checkFarthestFirst(new TestPoint2D(-1, -1), root.getPlus(), Order.PLUS_NODE_MINUS);
+
+ checkFarthestFirst(new TestPoint2D(1, -1), root, Order.MINUS_NODE_PLUS);
+ checkFarthestFirst(new TestPoint2D(1, -1), root.getMinus(), Order.MINUS_NODE_PLUS);
+ checkFarthestFirst(new TestPoint2D(1, -1), root.getPlus(), Order.MINUS_NODE_PLUS);
+
+ checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
+ checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
+ checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
+ }
+
+ private static void checkClosestFirst(TestPoint2D target, TestNode node, Order order) {
+ ClosestFirstStubVisitor visitor = new ClosestFirstStubVisitor(target);
+
+ Assert.assertSame(target, visitor.getTarget());
+ Assert.assertEquals(order, visitor.visitOrder(node));
+ }
+
+ private static void checkFarthestFirst(TestPoint2D target, TestNode node, Order order) {
+ FarthestFirstStubVisitor visitor = new FarthestFirstStubVisitor(target);
+
+ Assert.assertSame(target, visitor.getTarget());
+ Assert.assertEquals(order, visitor.visitOrder(node));
+ }
+
+ private static class ClosestFirstStubVisitor extends ClosestFirstVisitor<TestPoint2D, TestNode> {
+
+ private static final long serialVersionUID = 1L;
+
+ public ClosestFirstStubVisitor(TestPoint2D target) {
+ super(target);
+ }
+
+ @Override
+ public void visit(TestNode node) {
+ }
+ }
+
+ private static class FarthestFirstStubVisitor extends FarthestFirstVisitor<TestPoint2D, TestNode> {
+
+ private static final long serialVersionUID = 1L;
+
+ public FarthestFirstStubVisitor(TestPoint2D target) {
+ super(target);
+ }
+
+ @Override
+ public void visit(TestNode node) {
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java
new file mode 100644
index 0000000..524ec3b
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.core.partitioning.bsp;
+
+import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partition.test.TestLineSegment;
+import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RegionCutBoundaryTest {
+
+ @Test
+ public void testProperties() {
+ // arrange
+ TestLineSegment insideFacing = new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0));
+ TestLineSegment outsideFacing = new TestLineSegment(new TestPoint2D(-1, 0), TestPoint2D.ZERO);
+
+ // act
+ RegionCutBoundary<TestPoint2D> boundary = new RegionCutBoundary<>(insideFacing, outsideFacing);
+
+ // assert
+ Assert.assertSame(insideFacing, boundary.getInsideFacing());
+ Assert.assertSame(outsideFacing, boundary.getOutsideFacing());
+ }
+
+ @Test
+ public void testClosest() {
+ // arrange
+ TestPoint2D a = new TestPoint2D(-1, 0);
+ TestPoint2D b = TestPoint2D.ZERO;
+ TestPoint2D c = new TestPoint2D(1, 0);
+
+ TestLineSegment insideFacing = new TestLineSegment(a, b);
+ TestLineSegment outsideFacing = new TestLineSegment(b, c);
+
+ RegionCutBoundary<TestPoint2D> boundary = new RegionCutBoundary<>(insideFacing, outsideFacing);
+
+ // act/assert
+ PartitionTestUtils.assertPointsEqual(a, boundary.closest(new TestPoint2D(-2, 1)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-0.5, 0), boundary.closest(new TestPoint2D(-0.5, -1)));
+ PartitionTestUtils.assertPointsEqual(b, boundary.closest(TestPoint2D.ZERO));
+ PartitionTestUtils.assertPointsEqual(b, boundary.closest(new TestPoint2D(0, 2)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.5, 0), boundary.closest(new TestPoint2D(0.5, 3)));
+ PartitionTestUtils.assertPointsEqual(c, boundary.closest(new TestPoint2D(1, -4)));
+ PartitionTestUtils.assertPointsEqual(c, boundary.closest(new TestPoint2D(3, -5)));
+ }
+
+ @Test
+ public void testClosest_nullInsideFacing() {
+ // arrange
+ TestPoint2D a = new TestPoint2D(-1, 0);
+ TestPoint2D b = TestPoint2D.ZERO;
+
+ TestLineSegment outsideFacing = new TestLineSegment(a, b);
+
+ RegionCutBoundary<TestPoint2D> boundary = new RegionCutBoundary<>(null, outsideFacing);
+
+ // act/assert
+ PartitionTestUtils.assertPointsEqual(a, boundary.closest(new TestPoint2D(-2, 1)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-0.5, 0), boundary.closest(new TestPoint2D(-0.5, -1)));
+ PartitionTestUtils.assertPointsEqual(b, boundary.closest(TestPoint2D.ZERO));
+ PartitionTestUtils.assertPointsEqual(b, boundary.closest(new TestPoint2D(1, 2)));
+ }
+
+ @Test
+ public void testClosest_nullOutsideFacing() {
+ // arrange
+ TestPoint2D a = new TestPoint2D(-1, 0);
+ TestPoint2D b = TestPoint2D.ZERO;
+
+ TestLineSegment insideFacing = new TestLineSegment(a, b);
+
+ RegionCutBoundary<TestPoint2D> boundary = new RegionCutBoundary<>(insideFacing, null);
+
+ // act/assert
+ PartitionTestUtils.assertPointsEqual(a, boundary.closest(new TestPoint2D(-2, 1)));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-0.5, 0), boundary.closest(new TestPoint2D(-0.5, -1)));
+ PartitionTestUtils.assertPointsEqual(b, boundary.closest(TestPoint2D.ZERO));
+ PartitionTestUtils.assertPointsEqual(b, boundary.closest(new TestPoint2D(1, 2)));
+ }
+
+ @Test
+ public void testClosest_nullInsideAndOutsideFacing() {
+ // arrange
+ RegionCutBoundary<TestPoint2D> boundary = new RegionCutBoundary<>(null, null);
+
+ // act/assert
+ Assert.assertNull(boundary.closest(TestPoint2D.ZERO));
+ Assert.assertNull(boundary.closest(new TestPoint2D(1, 1)));
+ }
+
+ @Test
+ public void testContains() {
+ // arrange
+ TestPoint2D a = new TestPoint2D(-1, 0);
+ TestPoint2D b = TestPoint2D.ZERO;
+ TestPoint2D c = new TestPoint2D(1, 0);
+
+ TestLineSegment insideFacing = new TestLineSegment(a, b);
+ TestLineSegment outsideFacing = new TestLineSegment(b, c);
+
+ RegionCutBoundary<TestPoint2D> boundary = new RegionCutBoundary<>(insideFacing, outsideFacing);
+
+ // act/assert
+ Assert.assertFalse(boundary.contains(new TestPoint2D(-2, 0)));
+
+ Assert.assertTrue(boundary.contains(new TestPoint2D(-1, 0)));
+ Assert.assertTrue(boundary.contains(new TestPoint2D(-0.5, 0)));
+ Assert.assertTrue(boundary.contains(new TestPoint2D(0, 0)));
+ Assert.assertTrue(boundary.contains(new TestPoint2D(0.5, 0)));
+ Assert.assertTrue(boundary.contains(new TestPoint2D(1, 0)));
+
+ Assert.assertFalse(boundary.contains(new TestPoint2D(2, 0)));
+
+ Assert.assertFalse(boundary.contains(new TestPoint2D(-1, 1)));
+ Assert.assertFalse(boundary.contains(new TestPoint2D(0, -1)));
+ Assert.assertFalse(boundary.contains(new TestPoint2D(1, 1)));
+ }
+
+ @Test
+ public void testContains_nullSubHyperplanes() {
+ // arrange
+ RegionCutBoundary<TestPoint2D> boundary = new RegionCutBoundary<>(null, null);
+
+ // act/assert
+ Assert.assertFalse(boundary.contains(new TestPoint2D(-1, 0)));
+ Assert.assertFalse(boundary.contains(new TestPoint2D(0, 0)));
+ Assert.assertFalse(boundary.contains(new TestPoint2D(1, 0)));
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/precision/DoublePrecisionContextTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/precision/DoublePrecisionContextTest.java
index 80d9f8d..17a95a1 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/precision/DoublePrecisionContextTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/precision/DoublePrecisionContextTest.java
@@ -94,6 +94,19 @@
}
@Test
+ public void testSign() {
+ // act/assert
+ Assert.assertEquals(0, ctx.sign(0.0));
+
+ Assert.assertEquals(1, ctx.sign(1e-3));
+ Assert.assertEquals(-1, ctx.sign(-1e-3));
+
+ Assert.assertEquals(1, ctx.sign(Double.NaN));
+ Assert.assertEquals(1, ctx.sign(Double.POSITIVE_INFINITY));
+ Assert.assertEquals(-1, ctx.sign(Double.NEGATIVE_INFINITY));
+ }
+
+ @Test
public void testCompare() {
// act/assert
Assert.assertEquals(0, ctx.compare(1, 1));
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContextTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContextTest.java
index 8bbe10d..ff75ee8 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContextTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContextTest.java
@@ -36,6 +36,28 @@
}
@Test
+ public void testSign() {
+ // arrange
+ double eps = 1e-2;
+
+ EpsilonDoublePrecisionContext ctx = new EpsilonDoublePrecisionContext(eps);
+
+ // act/assert
+ Assert.assertEquals(0, ctx.sign(0.0));
+ Assert.assertEquals(0, ctx.sign(-0.0));
+
+ Assert.assertEquals(0, ctx.sign(1e-2));
+ Assert.assertEquals(0, ctx.sign(-1e-2));
+
+ Assert.assertEquals(1, ctx.sign(1e-1));
+ Assert.assertEquals(-1, ctx.sign(-1e-1));
+
+ Assert.assertEquals(1, ctx.sign(Double.NaN));
+ Assert.assertEquals(1, ctx.sign(Double.POSITIVE_INFINITY));
+ Assert.assertEquals(-1, ctx.sign(Double.NEGATIVE_INFINITY));
+ }
+
+ @Test
public void testCompare_compareToZero() {
// arrange
double eps = 1e-2;
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/EnclosingBall.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/EnclosingBall.java
index 0fb9224..4186454 100644
--- a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/EnclosingBall.java
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/EnclosingBall.java
@@ -45,7 +45,7 @@
* @param support support points used to define the ball
*/
@SafeVarargs
- public EnclosingBall(final P center, final double radius, final P ... support) {
+ public EnclosingBall(final P center, final double radius, final P... support) {
this.center = center;
this.radius = radius;
this.support = support.clone();
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGenerator.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGenerator.java
index 87a7c87..29ce8ee 100644
--- a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGenerator.java
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGenerator.java
@@ -33,7 +33,7 @@
*/
public class SphereGenerator implements SupportBallGenerator<Vector3D> {
- /** Base epsilon value */
+ /** Base epsilon value. */
private static final double BASE_EPS = 1e-10;
/** {@inheritDoc} */
@@ -57,12 +57,13 @@
if (support.size() < 4) {
// delegate to 2D disk generator
- final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(BASE_EPS * (norm1(vA) + norm1(vB) + norm1(vC)));
+ final DoublePrecisionContext precision =
+ new EpsilonDoublePrecisionContext(BASE_EPS * (norm1(vA) + norm1(vB) + norm1(vC)));
final Plane p = Plane.fromPoints(vA, vB, vC, precision);
final EnclosingBall<Vector2D> disk =
- new DiskGenerator().ballOnSupport(Arrays.asList(p.toSubSpace(vA),
- p.toSubSpace(vB),
- p.toSubSpace(vC)));
+ new DiskGenerator().ballOnSupport(Arrays.asList(p.toSubspace(vA),
+ p.toSubspace(vB),
+ p.toSubspace(vC)));
// convert back to 3D
return new EnclosingBall<>(p.toSpace(disk.getCenter()),
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractAffineTransformMatrix.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractAffineTransformMatrix.java
new file mode 100644
index 0000000..8def150
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractAffineTransformMatrix.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+/** Base class for affine transform matrices in Euclidean space.
+ *
+ * @param <V> Vector/point implementation type defining the space.
+ */
+public abstract class AbstractAffineTransformMatrix<V extends EuclideanVector<V>>
+ implements EuclideanTransform<V> {
+
+ /** Apply this transform to the given vector, ignoring translations and normalizing the
+ * result. This is equivalent to {@code transform.applyVector(vec).normalize()} but without
+ * the intermediate vector instance.
+ *
+ * @param vec the vector to transform
+ * @return the new, transformed unit vector
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the transformed vector coordinates
+ * cannot be normalized
+ * @see #applyVector(EuclideanVector)
+ */
+ public abstract V applyDirection(V vec);
+
+ /** Get the determinant of the matrix.
+ * @return the determinant of the matrix
+ */
+ public abstract double determinant();
+
+ /** {@inheritDoc}
+ *
+ * <p>This method returns true if the determinant of the matrix is positive.</p>
+ */
+ @Override
+ public boolean preservesOrientation() {
+ return determinant() > 0.0;
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AffineTransformMatrix.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AffineTransformMatrix.java
deleted file mode 100644
index 7209c3d..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AffineTransformMatrix.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.exception.IllegalNormException;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.partitioning.Transform;
-
-/** Interface representing an affine transform matrix in Euclidean space.
- * Rotation, scaling, and translation are examples of affine transformations.
- *
- * @param <V> Vector/point implementation type defining the space.
- * @param <S> Point type defining the embedded sub-space.
- * @see <a href="https://en.wikipedia.org/wiki/Affine_transformation">Affine transformation</a>
- */
-public interface AffineTransformMatrix<V extends EuclideanVector<V>, S extends Point<S>> extends Transform<V, S> {
-
- /** Apply this transform to the given vector, ignoring translations.
- *
- * <p>This method can be used to transform vector instances representing displacements between points.
- * For example, if {@code v} represents the difference between points {@code p1} and {@code p2},
- * then {@code transform.applyVector(v)} will represent the difference between {@code p1} and {@code p2}
- * after {@code transform} is applied.
- * </p>
- *
- * @param vec the vector to transform
- * @return the new, transformed vector
- * @see #applyDirection(EuclideanVector)
- */
- V applyVector(V vec);
-
- /** Apply this transform to the given vector, ignoring translations and normalizing the
- * result. This is equivalent to {@code transform.applyVector(vec).normalize()} but without
- * the intermediate vector instance.
- *
- * @param vec the vector to transform
- * @return the new, transformed unit vector
- * @throws IllegalNormException if the transformed vector coordinates cannot be normalized
- * @see #applyVector(EuclideanVector)
- */
- V applyDirection(V vec);
-
- /** {@inheritDoc}
- * This operation is not supported. See GEOMETRY-24.
- */
- @Override
- default Hyperplane<V> apply(Hyperplane<V> hyperplane) {
- throw new UnsupportedOperationException("Transforming hyperplanes is not supported");
- }
-
- /** {@inheritDoc}
- * This operation is not supported. See GEOMETRY-24.
- */
- @Override
- default SubHyperplane<S> apply(SubHyperplane<S> sub, Hyperplane<V> original,
- Hyperplane<V> transformed) {
- throw new UnsupportedOperationException("Transforming sub-hyperplanes is not supported");
- }
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanTransform.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanTransform.java
new file mode 100644
index 0000000..0a76f37
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanTransform.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Extension transform interface for Euclidean space. This interface provides an additional method
+ * for transforming vectors as opposed to points.
+ *
+ * @param <V> Vector implementation type
+ */
+public interface EuclideanTransform<V extends EuclideanVector<V>> extends Transform<V> {
+
+ /** Apply this transform to the given vector, ignoring translations.
+ *
+ * <p>This method can be used to transform vector instances representing displacements between points.
+ * For example, if {@code v} represents the difference between points {@code p1} and {@code p2},
+ * then {@code transform.applyVector(v)} will represent the difference between {@code p1} and {@code p2}
+ * after {@code transform} is applied.
+ * </p>
+ *
+ * @param vec the vector to transform
+ * @return the new, transformed vector
+ */
+ V applyVector(V vec);
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanVector.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanVector.java
index f767f97..ccd810f 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanVector.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanVector.java
@@ -20,7 +20,6 @@
import org.apache.commons.geometry.core.Point;
import org.apache.commons.geometry.core.Vector;
-import org.apache.commons.geometry.core.exception.IllegalNormException;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.internal.Vectors;
@@ -34,7 +33,7 @@
public abstract class EuclideanVector<V extends EuclideanVector<V>>
implements Vector<V>, Point<V>, Serializable {
- /** Serializable version identifer */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20181017L;
/** Return the vector representing the displacement from this vector
@@ -51,8 +50,8 @@
* @param v the vector that the returned vector will be directed toward
* @return unit vector representing the direction of displacement <em>from</em> this vector
* <em>to</em> the given vector
- * @throws IllegalNormException if the norm of the vector pointing from this instance to {@code v}
- * is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the vector pointing
+ * from this instance to {@code v} is zero, NaN, or infinite
*/
public abstract V directionTo(V v);
@@ -82,7 +81,7 @@
* @return true if the current instance is considered equal to the given vector when using
* the given precision context; otherwise false
*/
- public abstract boolean equals(V v, DoublePrecisionContext precision);
+ public abstract boolean eq(V v, DoublePrecisionContext precision);
/** Return true if the current instance is considered equal to the zero vector as evaluated by the
* given precision context. This is a convenience method equivalent to
@@ -91,16 +90,18 @@
* @param precision precision context used to determine floating point equality
* @return true if the current instance is considered equal to the zero vector when using
* the given precision context; otherwise false
- * @see #equals(EuclideanVector, DoublePrecisionContext)
+ * @see #eq(EuclideanVector, DoublePrecisionContext)
*/
public boolean isZero(final DoublePrecisionContext precision) {
- return equals(getZero(), precision);
+ return eq(getZero(), precision);
}
- /** Return the vector norm value, throwing an {@link IllegalNormException} if the value
- * is not real (ie, NaN or infinite) or zero.
+ /** Return the vector norm value, throwing an
+ * {@link org.apache.commons.geometry.core.exception.IllegalNormException} if the value is not real
+ * (ie, NaN or infinite) or zero.
* @return the vector norm value, guaranteed to be real and non-zero
- * @throws IllegalNormException if the vector norm is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the vector norm is
+ * zero, NaN, or infinite
*/
protected double getCheckedNorm() {
return Vectors.checkedNorm(this);
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/MultiDimensionalEuclideanVector.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/MultiDimensionalEuclideanVector.java
index 05f6c2e..3cd6365 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/MultiDimensionalEuclideanVector.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/MultiDimensionalEuclideanVector.java
@@ -16,8 +16,6 @@
*/
package org.apache.commons.geometry.euclidean;
-import org.apache.commons.geometry.core.exception.IllegalNormException;
-
/**
* Abstract base class for Euclidean vectors with two or more dimensions.
*
@@ -26,7 +24,7 @@
public abstract class MultiDimensionalEuclideanVector<V extends MultiDimensionalEuclideanVector<V>>
extends EuclideanVector<V> {
- /** Serializable version identifer */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20181017L;
/** Get the projection of the instance onto the given base vector. The returned
@@ -37,7 +35,8 @@
* </code>
* @param base base vector
* @return the vector projection of the instance onto {@code base}
- * @exception IllegalNormException if the norm of the base vector is zero, NaN, or infinite
+ * @exception org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the base vector is
+ * zero, NaN, or infinite
* @see #reject(MultiDimensionalEuclideanVector)
*/
public abstract V project(V base);
@@ -52,15 +51,16 @@
* </code>
* @param base base vector
* @return the vector rejection of the instance from {@code base}
- * @exception IllegalNormException if the norm of the base vector is zero, NaN, or infinite
+ * @exception org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the base vector is
+ * zero, NaN, or infinite
* @see #project(MultiDimensionalEuclideanVector)
*/
public abstract V reject(V base);
/** Get a unit vector orthogonal to the instance.
* @return a unit vector orthogonal to the current instance
- * @throws IllegalNormException if the norm of the current instance is zero, NaN,
- * or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the current instance
+ * is zero, NaN, or infinite
*/
public abstract V orthogonal();
@@ -70,8 +70,8 @@
* @param dir the direction to use for generating the orthogonal vector
* @return unit vector orthogonal to the current vector and pointing in the direction of
* {@code dir} that does not lie along the current vector
- * @throws IllegalNormException if either vector norm is zero, NaN or infinite,
- * or the given vector is collinear with this vector.
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if either vector norm is
+ * zero, NaN or infinite, or the given vector is collinear with this vector.
*/
public abstract V orthogonal(V dir);
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/exception/NonInvertibleTransformException.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/exception/NonInvertibleTransformException.java
index 88748dd..916c75c 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/exception/NonInvertibleTransformException.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/exception/NonInvertibleTransformException.java
@@ -23,7 +23,7 @@
*/
public class NonInvertibleTransformException extends GeometryException {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180927L;
/** Simple constructor accepting an error message.
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/AbstractPathConnector.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/AbstractPathConnector.java
new file mode 100644
index 0000000..08b3a2f
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/AbstractPathConnector.java
@@ -0,0 +1,460 @@
+/*
+ * 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.internal;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NavigableSet;
+import java.util.TreeSet;
+
+/** Abstract base class for joining unconnected path elements into connected, directional
+ * paths. The connection algorithm is exposed as a set of protected methods, allowing subclasses
+ * to define their own public API. Implementations must supply their own subclass of {@link ConnectableElement}
+ * specific for the objects being connected.
+ *
+ * <p>The connection algorithm proceeds as follows:
+ * <ul>
+ * <li>Create a sorted list of {@link ConnectableElement}s.</li>
+ * <li>For each element, attempt to find other elements with start points next the
+ * first instance's end point by calling {@link ConnectableElement#getConnectionSearchKey()} and
+ * using the returned instance to locate a search start location in the sorted element list.</li>
+ * <li>Search up through the sorted list from the start location, testing each element for possible connectivity
+ * with {@link ConnectableElement#canConnectTo(AbstractPathConnector.ConnectableElement)}. Collect possible
+ * connections in a list. Terminate the search when
+ * {@link ConnectableElement#shouldContinueConnectionSearch(AbstractPathConnector.ConnectableElement, boolean)}
+ * returns false.
+ * <li>Repeat the previous step searching downward through the list from the start location.</li>
+ * <li>Select the best connection option from the list of possible connections, using
+ * {@link #selectPointConnection(AbstractPathConnector.ConnectableElement, List)}
+ * and/or {@link #selectConnection(AbstractPathConnector.ConnectableElement, List)} when multiple possibilities
+ * are found.</li>
+ * <li>Repeat the above steps for each element. When done, the elements represent a linked list
+ * of connected paths.</li>
+ * </ul>
+ *
+ * <p>This class is not thread-safe.</p>
+ *
+ * @param <E> Element type
+ * @see ConnectableElement
+ */
+public abstract class AbstractPathConnector<E extends AbstractPathConnector.ConnectableElement<E>>
+ implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191106L;
+
+ /** List of path elements. */
+ private final NavigableSet<E> pathElements = new TreeSet<>();
+
+ /** View of the path element set in descending order. */
+ private final NavigableSet<E> pathElementsDescending = pathElements.descendingSet();
+
+ /** List used to store possible connections for the current element. */
+ private final List<E> possibleConnections = new ArrayList<>();
+
+ /** List used to store possible point-like (zero-length) connections for the current element. */
+ private final List<E> possiblePointConnections = new ArrayList<>();
+
+ /** Add a collection of path elements to the connector and attempt to connect each new element
+ * with previously added ones.
+ * @param elements path elements to connect
+ */
+ protected void connectPathElements(final Iterable<E> elements) {
+ elements.forEach(this::addPathElement);
+
+ for (final E element : elements) {
+ makeForwardConnection(element);
+ }
+ }
+
+ /** Add a single path element to the connector, leaving it unconnected until a later call to
+ * to {@link #connectPathElements(Iterable)} or {@link #computePathRoots()}.
+ * @param element value to add to the connector
+ * @see #connectPathElements(Iterable)
+ * @see #computePathRoots()
+ */
+ protected void addPathElement(final E element) {
+ pathElements.add(element);
+ }
+
+ /** Compute all connected paths and return a list of path elements representing
+ * the roots (start locations) of each. Each returned element is the head of a
+ * (possibly circular) linked list that follows a connected path.
+ *
+ * <p>The connector is reset after this call. Further calls to add elements
+ * will result in new paths being generated.</p>
+ * @return a list of root elements for the computed connected paths
+ */
+ protected List<E> computePathRoots() {
+ for (final E segment : pathElements) {
+ followForwardConnections(segment);
+ }
+
+ List<E> rootEntries = new ArrayList<>();
+ E root;
+ for (final E segment : pathElements) {
+ root = segment.exportPath();
+ if (root != null) {
+ rootEntries.add(root);
+ }
+ }
+
+ pathElements.clear();
+ possibleConnections.clear();
+ possiblePointConnections.clear();
+
+ return rootEntries;
+ }
+
+ /** Find and follow forward connections from the given start element.
+ * @param start element to begin the connection operation with
+ */
+ private void followForwardConnections(final E start) {
+ E current = start;
+
+ while (current != null && current.hasEnd() && !current.hasNext()) {
+ current = makeForwardConnection(current);
+ }
+ }
+
+ /** Connect the end point of the given element to the start point of another element. Returns
+ * the newly connected element or null if no forward connection was made.
+ * @param element element to connect
+ * @return the next element in the path or null if no connection was made
+ */
+ private E makeForwardConnection(final E element) {
+ findPossibleConnections(element);
+
+ E next = null;
+
+ // select from all available connections, handling point-like segments first
+ if (!possiblePointConnections.isEmpty()) {
+ next = (possiblePointConnections.size() == 1) ?
+ possiblePointConnections.get(0) :
+ selectPointConnection(element, possiblePointConnections);
+ } else if (!possibleConnections.isEmpty()) {
+
+ next = (possibleConnections.size() == 1) ?
+ possibleConnections.get(0) :
+ selectConnection(element, possibleConnections);
+ }
+
+ if (next != null) {
+ element.connectTo(next);
+ }
+
+ return next;
+ }
+
+ /** Find possible connections for the given element and place them in the
+ * {@link #possibleConnections} and {@link #possiblePointConnections} lists.
+ * @param element the element to find connections for
+ */
+ private void findPossibleConnections(final E element) {
+ possibleConnections.clear();
+ possiblePointConnections.clear();
+
+ if (element.hasEnd()) {
+ final E searchKey = element.getConnectionSearchKey();
+
+ // search up
+ for (E candidate : pathElements.tailSet(searchKey)) {
+ if (!addPossibleConnection(element, candidate) &&
+ !element.shouldContinueConnectionSearch(candidate, true)) {
+ break;
+ }
+ }
+
+ // search down
+ for (E candidate : pathElementsDescending.tailSet(searchKey, false)) {
+ if (!addPossibleConnection(element, candidate) &&
+ !element.shouldContinueConnectionSearch(candidate, false)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /** Add the candidate to one of the connection lists if it represents a possible connection. Returns
+ * true if the candidate was added, otherwise false.
+ * @param element element to check for connections with
+ * @param candidate candidate connection element
+ * @return true if the candidate is a possible connection
+ */
+ private boolean addPossibleConnection(final E element, final E candidate) {
+ if (element != candidate &&
+ !candidate.hasPrevious() &&
+ candidate.hasStart() &&
+ element.canConnectTo(candidate)) {
+
+ if (element.endPointsEq(candidate)) {
+ possiblePointConnections.add(candidate);
+ } else {
+ possibleConnections.add(candidate);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Method called to select a connection to use for a given element when multiple zero-length connections are
+ * available. The algorithm here attempts to choose the point most likely to produce a logical path by selecting
+ * the outgoing element with the smallest relative angle with the incoming element, with unconnected element
+ * preferred over ones that are already connected (thereby allowing other connections to occur in the path).
+ * @param incoming the incoming element
+ * @param outgoingList list of available outgoing point-like connections
+ * @return the connection to use
+ */
+ protected E selectPointConnection(final E incoming, final List<E> outgoingList) {
+
+ double angle;
+ boolean isUnconnected;
+
+ double smallestAngle = 0.0;
+ E bestElement = null;
+ boolean bestIsUnconnected = false;
+
+ for (final E outgoing : outgoingList) {
+ angle = Math.abs(incoming.getRelativeAngle(outgoing));
+ isUnconnected = !outgoing.hasNext();
+
+ if (bestElement == null || (!bestIsUnconnected && isUnconnected) ||
+ (bestIsUnconnected == isUnconnected && angle < smallestAngle)) {
+
+ smallestAngle = angle;
+ bestElement = outgoing;
+ bestIsUnconnected = isUnconnected;
+ }
+ }
+
+ return bestElement;
+ }
+
+ /** Method called to select a connection to use for a given segment when multiple non-length-zero
+ * connections are available. In this case, the selection of the outgoing connection depends only
+ * on the desired characteristics of the connected path.
+ * @param incoming the incoming segment
+ * @param outgoing list of available outgoing connections; will always contain at least
+ * two elements
+ * @return the connection to use
+ */
+ protected abstract E selectConnection(E incoming, List<E> outgoing);
+
+ /** Class used to represent connectable path elements for use with {@link AbstractPathConnector}.
+ * Subclasses must fulfill the following requirements in order for path connection operations
+ * to work correctly:
+ * <ul>
+ * <li>Implement {@link #compareTo(Object)} such that elements are sorted by their start
+ * point locations. Other criteria may be used as well but elements with start points in close
+ * proximity must be grouped together.</li>
+ * <li>Implement {@link #getConnectionSearchKey()} such that it returns an instance that will be placed
+ * next to elements with start points close to the current instance's end point when sorted with
+ * {@link #compareTo(Object)}.</li>
+ * <li>Implement {@link #shouldContinueConnectionSearch(AbstractPathConnector.ConnectableElement, boolean)}
+ * such that it returns false when the search for possible connections through a sorted list of elements
+ * may terminate.</li>
+ * </ul>
+ *
+ * @param <E> Element type
+ * @see AbstractPathConnector
+ */
+ public abstract static class ConnectableElement<E extends ConnectableElement<E>>
+ implements Comparable<E>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191107L;
+
+ /** Next connected element. */
+ private E next;
+
+ /** Previous connected element. */
+ private E previous;
+
+ /** Flag set to true when this element has exported its value to a path. */
+ private boolean exported = false;
+
+ /** Return true if the instance is connected to another element's start point.
+ * @return true if the instance has a next element
+ */
+ public boolean hasNext() {
+ return next != null;
+ }
+
+ /** Get the next connected element in the path, if any.
+ * @return the next connected segment in the path; may be null
+ */
+ public E getNext() {
+ return next;
+ }
+
+ /** Set the next connected element for this path. This is intended for
+ * internal use only. Callers should use the {@link #connectTo(AbstractPathConnector.ConnectableElement)}
+ * method instead.
+ * @param next next path element
+ */
+ protected void setNext(final E next) {
+ this.next = next;
+ }
+
+ /** Return true if another element is connected to this instance's start point.
+ * @return true if the instance has a previous element
+ */
+ public boolean hasPrevious() {
+ return previous != null;
+ }
+
+ /** Get the previous connected element in the path, if any.
+ * @return the previous connected element in the path; may be null
+ */
+ public E getPrevious() {
+ return previous;
+ }
+
+ /** Set the previous connected element for this path. This is intended for
+ * internal use only. Callers should use the {@link #connectTo(AbstractPathConnector.ConnectableElement)}
+ * method instead.
+ * @param previous previous path element
+ */
+ protected void setPrevious(final E previous) {
+ this.previous = previous;
+ }
+
+ /** Connect this instance's end point to the given element's start point. No validation
+ * is performed in this method. The {@link #canConnectTo(AbstractPathConnector.ConnectableElement)}
+ * method must have been called previously.
+ * @param nextElement the next element in the path
+ */
+ public void connectTo(final E nextElement) {
+ setNext(nextElement);
+ nextElement.setPrevious(getSelf());
+ }
+
+ /** Export the path that this element belongs to, returning the root
+ * segment. This method traverses all connected element, sets their
+ * exported flags to true, and returns the root element of the path
+ * (or this element in the case of a loop). Each path can only be
+ * exported once. Later calls to this method on this instance or any of its
+ * connected elements will return null.
+ * @return the root of the path or null if the path that this element
+ * belongs to has already been exported
+ */
+ public E exportPath() {
+ if (markExported()) {
+
+ // export the connected portions of the path, moving both
+ // forward and backward
+ E current;
+ E root = getSelf();
+
+ // forward
+ current = next;
+ while (current != null && current.markExported()) {
+ current = current.getNext();
+ }
+
+ // backward
+ current = previous;
+ while (current != null && current.markExported()) {
+ root = current;
+ current = current.getPrevious();
+ }
+
+ return root;
+ }
+
+ return null;
+ }
+
+ /** Set the export flag for this instance to true. Returns true
+ * if the flag was changed and false otherwise.
+ * @return true if the flag was changed and false if it was
+ * already set to true
+ */
+ protected boolean markExported() {
+ if (!exported) {
+ exported = true;
+ return true;
+ }
+ return false;
+ }
+
+ /** Return true if this instance has a start point that can be
+ * connected to another element's end point.
+ * @return true if this instance has a start point that can be
+ * connected to another element's end point
+ */
+ public abstract boolean hasStart();
+
+ /** Return true if this instance has an end point that can be
+ * connected to another element's start point.
+ * @return true if this instance has an end point that can be
+ * connected to another element's start point
+ */
+ public abstract boolean hasEnd();
+
+ /** Return true if the end point of this instance should be considered
+ * equivalent to the end point of the argument.
+ * @param other element to compare end points with
+ * @return true if this instance has an end point equivalent to that
+ * of the argument
+ */
+ public abstract boolean endPointsEq(E other);
+
+ /** Return true if this instance's end point can be connected to
+ * the argument's start point.
+ * @param nextElement candidate for the next element in the path; this value
+ * is guaranteed to not be null and to contain a start point
+ * @return true if this instance's end point can be connected to
+ * the argument's start point
+ */
+ public abstract boolean canConnectTo(E nextElement);
+
+ /** Return the relative angle between this element and the argument.
+ * @param other element to compute the angle with
+ * @return the relative angle between this element and the argument
+ */
+ public abstract double getRelativeAngle(E other);
+
+ /** Get a new instance used as a search key to help locate other elements
+ * with start points matching this instance's end point. The only restriction
+ * on the returned instance is that it be compatible with the implementation
+ * class' {@link #compareTo(Object)} method.
+ * @return a new instance used to help locate other path elements with start
+ * points equivalent to this instance's end point
+ */
+ public abstract E getConnectionSearchKey();
+
+ /** Return true if the search for possible connections should continue through
+ * the sorted set of possible path elements given the current candidate element
+ * and search direction. The search operation stops for the given direction
+ * when this method returns false.
+ * @param candidate last tested candidate connection element
+ * @param ascending true if the search is proceeding in an ascending direction;
+ * false otherwise
+ * @return true if the connection search should continue
+ */
+ public abstract boolean shouldContinueConnectionSearch(E candidate, boolean ascending);
+
+ /** Return the current instance as the generic type.
+ * @return the current instance as the generic type.
+ */
+ protected abstract E getSelf();
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Matrices.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Matrices.java
index ec710ba..51ff088 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Matrices.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Matrices.java
@@ -20,7 +20,7 @@
*/
public final class Matrices {
- /** Private constructor */
+ /** Private constructor. */
private Matrices() {}
/** Compute the determinant of the 2x2 matrix represented by the given values.
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.java
index 045c893..25a0de0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.java
@@ -19,12 +19,12 @@
import java.io.Serializable;
import org.apache.commons.geometry.core.internal.DoubleFunction1N;
-import org.apache.commons.geometry.euclidean.AffineTransformMatrix;
+import org.apache.commons.geometry.euclidean.AbstractAffineTransformMatrix;
import org.apache.commons.geometry.euclidean.exception.NonInvertibleTransformException;
import org.apache.commons.geometry.euclidean.internal.Vectors;
import org.apache.commons.numbers.core.Precision;
-/** Class using a matrix to represent a affine transformations in 1 dimensional Euclidean space.
+/** Class using a matrix to represent affine transformations in 1 dimensional Euclidean space.
*
* <p>Instances of this class use a 2x2 matrix for all transform operations.
* The last row of this matrix is always set to the values <code>[0 1]</code> and so
@@ -32,29 +32,30 @@
* use arrays containing 2 elements, instead of 4.
* </p>
*/
-public final class AffineTransformMatrix1D implements AffineTransformMatrix<Vector1D, Vector1D>, Serializable {
+public final class AffineTransformMatrix1D extends AbstractAffineTransformMatrix<Vector1D>
+ implements Transform1D, Serializable {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20181006L;
- /** The number of internal matrix elements */
+ /** The number of internal matrix elements. */
private static final int NUM_ELEMENTS = 2;
- /** String used to start the transform matrix string representation */
+ /** String used to start the transform matrix string representation. */
private static final String MATRIX_START = "[ ";
- /** String used to end the transform matrix string representation */
+ /** String used to end the transform matrix string representation. */
private static final String MATRIX_END = " ]";
- /** String used to separate elements in the matrix string representation */
+ /** String used to separate elements in the matrix string representation. */
private static final String ELEMENT_SEPARATOR = ", ";
/** Shared transform set to the identity matrix. */
private static final AffineTransformMatrix1D IDENTITY_INSTANCE = new AffineTransformMatrix1D(1, 0);
- /** Transform matrix entry <code>m<sub>0,0</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,0</sub></code>. */
private final double m00;
- /** Transform matrix entry <code>m<sub>0,1</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,1</sub></code>. */
private final double m01;
/**
@@ -81,7 +82,7 @@
*/
public double[] toArray() {
return new double[] {
- m00, m01
+ m00, m01
};
}
@@ -107,10 +108,25 @@
* @see #applyVector(Vector1D)
*/
@Override
- public Vector1D applyDirection(final Vector1D vec) {
+ public Vector1D.Unit applyDirection(final Vector1D vec) {
return applyVector(vec, Vector1D.Unit::from);
}
+ /** {@inheritDoc} */
+ @Override
+ public double determinant() {
+ return m00;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This simply returns the current instance.</p>
+ */
+ @Override
+ public AffineTransformMatrix1D toMatrix() {
+ return this;
+ }
+
/** Get a new transform containing the result of applying a translation logically after
* the transformation represented by the current instance. This is achieved by
* creating a new translation transform and pre-multiplying it with the current
@@ -214,7 +230,7 @@
final double invDet = 1.0 / det;
final double c00 = invDet;
- final double c01 = - (this.m01 * invDet);
+ final double c01 = -(this.m01 * invDet);
return new AffineTransformMatrix1D(c00, c01);
}
@@ -288,7 +304,7 @@
* @return a new transform initialized with the given matrix values
* @throws IllegalArgumentException if the array does not have 2 elements
*/
- public static AffineTransformMatrix1D of(final double ... arr) {
+ public static AffineTransformMatrix1D of(final double... arr) {
if (arr.length != NUM_ELEMENTS) {
throw new IllegalArgumentException("Dimension mismatch: " + arr.length + " != " + NUM_ELEMENTS);
}
@@ -341,7 +357,8 @@
* @param b second transform
* @return the transform computed as {@code a x b}
*/
- private static AffineTransformMatrix1D multiply(final AffineTransformMatrix1D a, final AffineTransformMatrix1D b) {
+ private static AffineTransformMatrix1D multiply(final AffineTransformMatrix1D a,
+ final AffineTransformMatrix1D b) {
// calculate the matrix elements
final double c00 = a.m00 * b.m00;
@@ -358,7 +375,8 @@
*/
private static void validateElementForInverse(final double element) {
if (!Double.isFinite(element)) {
- throw new NonInvertibleTransformException("Transform is not invertible; invalid matrix element: " + element);
+ throw new NonInvertibleTransformException("Transform is not invertible; invalid matrix element: " +
+ element);
}
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/FunctionTransform1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/FunctionTransform1D.java
new file mode 100644
index 0000000..5c0ca23
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/FunctionTransform1D.java
@@ -0,0 +1,95 @@
+/*
+ * 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.oned;
+
+import java.util.function.Function;
+
+/** Class that wraps a {@link Function} with the {@link Transform1D} interface.
+ */
+public final class FunctionTransform1D implements Transform1D {
+
+ /** Static instance representing the identity transform. */
+ private static final FunctionTransform1D IDENTITY =
+ new FunctionTransform1D(Function.identity(), true, Vector1D.ZERO);
+
+ /** The underlying function for the transform. */
+ private final Function<Vector1D, Vector1D> fn;
+
+ /** True if the transform preserves spatial orientation. */
+ private final boolean preservesOrientation;
+
+ /** The translation component of the transform. */
+ private final Vector1D translation;
+
+ /** Construct a new instance from its component parts. No validation of the input is performed.
+ * @param fn the underlying function for the transform
+ * @param preservesOrientation true if the transform preserves spatial orientation
+ * @param translation the translation component of the transform
+ */
+ private FunctionTransform1D(final Function<Vector1D, Vector1D> fn, final boolean preservesOrientation,
+ final Vector1D translation) {
+ this.fn = fn;
+ this.preservesOrientation = preservesOrientation;
+ this.translation = translation;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector1D apply(final Vector1D pt) {
+ return fn.apply(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector1D applyVector(final Vector1D vec) {
+ return apply(vec).subtract(translation);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean preservesOrientation() {
+ return preservesOrientation;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AffineTransformMatrix1D toMatrix() {
+ final Vector1D tOne = applyVector(Vector1D.Unit.PLUS);
+
+ return AffineTransformMatrix1D.of(tOne.getX(), translation.getX());
+ }
+
+ /** Return an instance representing the identity transform.
+ * @return an instance representing the identity transform
+ */
+ public static FunctionTransform1D identity() {
+ return IDENTITY;
+ }
+
+ /** Construct a new transform instance from the given function.
+ * @param fn the function to use for the transform
+ * @return a new transform instance using the given function
+ */
+ public static FunctionTransform1D from(final Function<Vector1D, Vector1D> fn) {
+ final Vector1D tOne = fn.apply(Vector1D.Unit.PLUS);
+ final Vector1D tZero = fn.apply(Vector1D.ZERO);
+
+ final boolean preservesOrientation = tOne.getX() > 0.0;
+
+ return new FunctionTransform1D(fn, preservesOrientation, tZero);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
index c4c39af..2003ddc 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
@@ -16,74 +16,501 @@
*/
package org.apache.commons.geometry.euclidean.oned;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
+import java.io.Serializable;
+import java.text.MessageFormat;
-/** This class represents a 1D interval.
- * @see IntervalsSet
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Class representing an interval in one dimension. The interval is defined
+ * by minimum and maximum values. One or both of these values may be infinite
+ * although not with the same sign.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
*/
-public class Interval {
+public final class Interval implements HyperplaneBoundedRegion<Vector1D>, Serializable {
- /** The lower bound of the interval. */
- private final double lower;
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190210L;
- /** The upper bound of the interval. */
- private final double upper;
+ /** Interval instance representing the entire real number line. */
+ private static final Interval FULL = new Interval(null, null);
- /** Simple constructor.
- * @param lower lower bound of the interval
- * @param upper upper bound of the interval
+ /** {@link OrientedPoint} instance representing the min boundary of the interval,
+ * or null if no min boundary exists. If present, this instance will be negative-facing.
+ * Infinite values are allowed but not NaN.
*/
- public Interval(final double lower, final double upper) {
- if (upper < lower) {
- throw new IllegalArgumentException("Endpoints do not specify an interval: [{" + upper + "}, {" + lower + "}]");
+ private final OrientedPoint minBoundary;
+
+ /** {@link OrientedPoint} instance representing the max boundary of the interval,
+ * or null if no max boundary exists. If present, this instance will be negative-facing.
+ * Infinite values are allowed but not NaN.
+ */
+ private final OrientedPoint maxBoundary;
+
+ /** Create an instance from min and max bounding hyperplanes. No validation is performed.
+ * Callers are responsible for ensuring that the given hyperplanes represent a valid
+ * interval.
+ * @param minBoundary the min (negative-facing) hyperplane
+ * @param maxBoundary the max (positive-facing) hyperplane
+ */
+ private Interval(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) {
+ this.minBoundary = minBoundary;
+ this.maxBoundary = maxBoundary;
+ }
+
+ /** Get the minimum value for the interval or {@link Double#NEGATIVE_INFINITY}
+ * if no minimum value exists.
+ * @return the minimum value for the interval or {@link Double#NEGATIVE_INFINITY}
+ * if no minimum value exists.
+ */
+ public double getMin() {
+ return (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY;
+ }
+
+ /** Get the maximum value for the interval or {@link Double#POSITIVE_INFINITY}
+ * if no maximum value exists.
+ * @return the maximum value for the interval or {@link Double#POSITIVE_INFINITY}
+ * if no maximum value exists.
+ */
+ public double getMax() {
+ return (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY;
+ }
+
+ /**
+ * Get the {@link OrientedPoint} forming the minimum bounding hyperplane
+ * of the interval, or null if none exists. If present, This hyperplane
+ * is oriented to point in the negative direction.
+ * @return the hyperplane forming the minimum boundary of the interval or
+ * null if no minimum boundary exists
+ */
+ public OrientedPoint getMinBoundary() {
+ return minBoundary;
+ }
+
+ /**
+ * Get the {@link OrientedPoint} forming the maximum bounding hyperplane
+ * of the interval, or null if none exists. If present, this hyperplane
+ * is oriented to point in the positive direction.
+ * @return the hyperplane forming the maximum boundary of the interval or
+ * null if no maximum boundary exists
+ */
+ public OrientedPoint getMaxBoundary() {
+ return maxBoundary;
+ }
+
+ /** Return true if the interval has a minimum (lower) boundary.
+ * @return true if the interval has minimum (lower) boundary
+ */
+ public boolean hasMinBoundary() {
+ return minBoundary != null;
+ }
+
+ /** Return true if the interval has a maximum (upper) boundary.
+ * @return true if the interval has maximum (upper) boundary
+ */
+ public boolean hasMaxBoundary() {
+ return maxBoundary != null;
+ }
+
+ /** True if the region is infinite, meaning that at least one of the boundaries
+ * does not exist.
+ * @return true if the region is infinite
+ */
+ public boolean isInfinite() {
+ return minBoundary == null || maxBoundary == null;
+ }
+
+ /** True if the region is finite, meaning that both the minimum and maximum
+ * boundaries exist and the region size is finite.
+ * @return true if the region is finite
+ */
+ public boolean isFinite() {
+ return !isInfinite();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionLocation classify(final Vector1D pt) {
+ return classify(pt.getX());
+ }
+
+ /** Classify a point with respect to the interval.
+ * @param location the location to classify
+ * @return the classification of the point with respect to the interval
+ * @see #classify(Vector1D)
+ */
+ public RegionLocation classify(final double location) {
+ final RegionLocation minLoc = classifyWithBoundary(location, minBoundary);
+ final RegionLocation maxLoc = classifyWithBoundary(location, maxBoundary);
+
+ if (minLoc == RegionLocation.BOUNDARY || maxLoc == RegionLocation.BOUNDARY) {
+ return RegionLocation.BOUNDARY;
+ } else if (minLoc == RegionLocation.INSIDE && maxLoc == RegionLocation.INSIDE) {
+ return RegionLocation.INSIDE;
}
- this.lower = lower;
- this.upper = upper;
+ return RegionLocation.OUTSIDE;
}
- /** Get the lower bound of the interval.
- * @return lower bound of the interval
+ /** Classify the location using the given interval boundary, which may be null.
+ * @param location the location to classify
+ * @param boundary interval boundary to classify against
+ * @return the location of the point relative to the boundary
*/
- public double getInf() {
- return lower;
- }
-
- /** Get the upper bound of the interval.
- * @return upper bound of the interval
- */
- public double getSup() {
- return upper;
- }
-
- /** Get the size of the interval.
- * @return size of the interval
- */
- public double getSize() {
- return upper - lower;
- }
-
- /** Get the barycenter of the interval.
- * @return barycenter of the interval
- */
- public double getBarycenter() {
- return 0.5 * (lower + upper);
- }
-
- /** Check a point with respect to the interval.
- * @param point point to check
- * @param tolerance tolerance below which points are considered to
- * belong to the boundary
- * @return a code representing the point status: either {@link
- * Location#INSIDE}, {@link Location#OUTSIDE} or {@link Location#BOUNDARY}
- */
- public Location checkPoint(final double point, final double tolerance) {
- if (point < lower - tolerance || point > upper + tolerance) {
- return Location.OUTSIDE;
- } else if (point > lower + tolerance && point < upper - tolerance) {
- return Location.INSIDE;
+ private RegionLocation classifyWithBoundary(final double location, final OrientedPoint boundary) {
+ if (Double.isNaN(location)) {
+ return RegionLocation.OUTSIDE;
+ } else if (boundary == null) {
+ return RegionLocation.INSIDE;
} else {
- return Location.BOUNDARY;
+ final HyperplaneLocation hyperLoc = boundary.classify(location);
+
+ if (hyperLoc == HyperplaneLocation.ON) {
+ return RegionLocation.BOUNDARY;
+ } else if (hyperLoc == HyperplaneLocation.PLUS) {
+ return RegionLocation.OUTSIDE;
+ }
+ return RegionLocation.INSIDE;
}
}
+ /** Return true if the given point location is on the inside or boundary
+ * of the region.
+ * @param x the location to test
+ * @return true if the location is on the inside or boundary of the region
+ * @see #contains(Point)
+ */
+ public boolean contains(final double x) {
+ return classify(x) != RegionLocation.OUTSIDE;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>The point is projected onto the nearest interval boundary. When a point
+ * is on the inside of the interval and is equidistant from both boundaries,
+ * then the minimum boundary is selected. when a point is on the outside of the
+ * interval and is equidistant from both boundaries (as is the case for intervals
+ * representing a single point), then the boundary facing the point is returned,
+ * ensuring that the returned offset is positive.
+ * </p>
+ */
+ @Override
+ public Vector1D project(Vector1D pt) {
+
+ OrientedPoint boundary = null;
+
+ if (minBoundary != null && maxBoundary != null) {
+ // both boundaries are present; use the closest
+ final double minOffset = minBoundary.offset(pt.getX());
+ final double maxOffset = maxBoundary.offset(pt.getX());
+
+ final double minDist = Math.abs(minOffset);
+ final double maxDist = Math.abs(maxOffset);
+
+ // Project onto the max boundary if it's the closest or the point is on its plus side.
+ // Otherwise, project onto the min boundary.
+ if (maxDist < minDist || maxOffset > 0) {
+ boundary = maxBoundary;
+ } else {
+ boundary = minBoundary;
+ }
+ } else if (minBoundary != null) {
+ // only the min boundary is present
+ boundary = minBoundary;
+ } else if (maxBoundary != null) {
+ // only the max boundary is present
+ boundary = maxBoundary;
+ }
+
+ return (boundary != null) ? boundary.project(pt) : null;
+ }
+
+ /** Return a new instance transformed by the argument.
+ * @param transform transform to apply
+ * @return a new instance transformed by the argument
+ */
+ public Interval transform(final Transform<Vector1D> transform) {
+ final OrientedPoint transformedMin = (minBoundary != null) ?
+ minBoundary.transform(transform) :
+ null;
+ final OrientedPoint transformedMax = (maxBoundary != null) ?
+ maxBoundary.transform(transform) :
+ null;
+
+ return of(transformedMin, transformedMax);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns false since there is always at least
+ * one point that can be classified as not being on the outside of
+ * the region.</p>
+ */
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFull() {
+ return minBoundary == null && maxBoundary == null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ if (isInfinite()) {
+ return Double.POSITIVE_INFINITY;
+ }
+
+ return getMax() - getMin();
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns 0 because boundaries in one dimension do not
+ * have any size.</p>
+ */
+ @Override
+ public double getBoundarySize() {
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector1D getBarycenter() {
+ if (isInfinite()) {
+ return null;
+ }
+
+ final double min = getMin();
+ final double max = getMax();
+
+ return Vector1D.of((0.5 * (max - min)) + min);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<Interval> split(final Hyperplane<Vector1D> splitter) {
+ final OrientedPoint splitOrientedPoint = (OrientedPoint) splitter;
+ final Vector1D splitPoint = splitOrientedPoint.getPoint();
+
+ final HyperplaneLocation splitterMinLoc = (minBoundary != null) ? minBoundary.classify(splitPoint) : null;
+ final HyperplaneLocation splitterMaxLoc = (maxBoundary != null) ? maxBoundary.classify(splitPoint) : null;
+
+ Interval low = null;
+ Interval high = null;
+
+ if (splitterMinLoc != HyperplaneLocation.ON || splitterMaxLoc != HyperplaneLocation.ON) {
+
+ if (splitterMinLoc != null && splitterMinLoc != HyperplaneLocation.MINUS) {
+ // splitter is on or below min boundary
+ high = this;
+ } else if (splitterMaxLoc != null && splitterMaxLoc != HyperplaneLocation.MINUS) {
+ // splitter is on or above max boundary
+ low = this;
+ } else {
+ // the interval is split in two
+ low = new Interval(minBoundary, OrientedPoint.createPositiveFacing(
+ splitPoint, splitOrientedPoint.getPrecision()));
+ high = new Interval(OrientedPoint.createNegativeFacing(
+ splitPoint, splitOrientedPoint.getPrecision()), maxBoundary);
+ }
+ }
+
+ // assign minus/plus based on the orientation of the splitter
+ final boolean lowIsMinus = splitOrientedPoint.isPositiveFacing();
+ final Interval minus = lowIsMinus ? low : high;
+ final Interval plus = lowIsMinus ? high : low;
+
+ return new Split<>(minus, plus);
+ }
+
+ /** Return a {@link RegionBSPTree1D} representing the same region as this instance.
+ * @return a BSP tree representing the same region
+ * @see RegionBSPTree1D#from(Interval, Interval...)
+ */
+ public RegionBSPTree1D toTree() {
+ return RegionBSPTree1D.from(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[min= ")
+ .append(getMin())
+ .append(", max= ")
+ .append(getMax())
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Create a new interval from the given point locations. The returned interval represents
+ * the region between the points, regardless of the order they are given as arguments.
+ * @param a first point location
+ * @param b second point location
+ * @param precision precision context used to compare floating point numbers
+ * @return a new interval representing the region between the given point locations
+ * @throws GeometryException if either number is {@link Double#NaN NaN} or the numbers
+ * are both infinite and have the same sign
+ */
+ public static Interval of(final double a, final double b, final DoublePrecisionContext precision) {
+ validateIntervalValues(a, b);
+
+ final double min = Math.min(a, b);
+ final double max = Math.max(a, b);
+
+ final OrientedPoint minBoundary = Double.isFinite(min) ?
+ OrientedPoint.fromLocationAndDirection(min, false, precision) :
+ null;
+
+ final OrientedPoint maxBoundary = Double.isFinite(max) ?
+ OrientedPoint.fromLocationAndDirection(max, true, precision) :
+ null;
+
+ if (minBoundary == null && maxBoundary == null) {
+ return FULL;
+ }
+
+ return new Interval(minBoundary, maxBoundary);
+ }
+
+ /** Create a new interval from the given points. The returned interval represents
+ * the region between the points, regardless of the order they are given as arguments.
+ * @param a first point
+ * @param b second point
+ * @param precision precision context used to compare floating point numbers
+ * @return a new interval representing the region between the given points
+ * @throws GeometryException if either point is {@link Vector1D#isNaN() NaN} or the points
+ * are both {@link Vector1D#isInfinite() infinite} and have the same sign
+ */
+ public static Interval of(final Vector1D a, final Vector1D b, final DoublePrecisionContext precision) {
+ return of(a.getX(), b.getX(), precision);
+ }
+
+ /** Create a new interval from the given hyperplanes. The hyperplanes may be given in
+ * any order but one must be positive-facing and the other negative-facing, with the positive-facing
+ * hyperplane located above the negative-facing hyperplane. Either or both argument may be null,
+ * in which case the returned interval will extend to infinity in the appropriate direction. If both
+ * arguments are null, an interval representing the full space is returned.
+ * @param a first hyperplane; may be null
+ * @param b second hyperplane; may be null
+ * @return a new interval representing the region between the given hyperplanes
+ * @throws GeometryException if the hyperplanes have the same orientation or
+ * do not form an interval (for example, if the positive-facing hyperplane is below
+ * the negative-facing hyperplane)
+ */
+ public static Interval of(final OrientedPoint a, final OrientedPoint b) {
+ // determine the ordering of the hyperplanes
+ OrientedPoint minBoundary = null;
+ OrientedPoint maxBoundary = null;
+
+ if (a != null && b != null) {
+ // both hyperplanes are present, so validate then against each other
+ if (a.isPositiveFacing() == b.isPositiveFacing()) {
+ throw new GeometryException(
+ MessageFormat.format("Invalid interval: hyperplanes have same orientation: {0}, {1}", a, b));
+ }
+
+ if (a.classify(b.getPoint()) == HyperplaneLocation.PLUS ||
+ b.classify(a.getPoint()) == HyperplaneLocation.PLUS) {
+ throw new GeometryException(
+ MessageFormat.format("Invalid interval: hyperplanes do not form interval: {0}, {1}", a, b));
+ }
+
+ // min boundary faces -infinity, max boundary faces +infinity
+ minBoundary = a.isPositiveFacing() ? b : a;
+ maxBoundary = a.isPositiveFacing() ? a : b;
+ } else if (a == null) {
+ if (b == null) {
+ // no boundaries; return the full number line
+ return FULL;
+ }
+
+ if (b.isPositiveFacing()) {
+ maxBoundary = b;
+ } else {
+ minBoundary = b;
+ }
+ } else {
+ if (a.isPositiveFacing()) {
+ maxBoundary = a;
+ } else {
+ minBoundary = a;
+ }
+ }
+
+ // validate the boundary locations
+ final double minLoc = (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY;
+ final double maxLoc = (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY;
+
+ validateIntervalValues(minLoc, maxLoc);
+
+ // create the interval, replacing infinites with nulls
+ return new Interval(
+ Double.isFinite(minLoc) ? minBoundary : null,
+ Double.isFinite(maxLoc) ? maxBoundary : null);
+ }
+
+ /** Return an interval with the given min value and no max.
+ * @param min min value for the interval
+ * @param precision precision context used to compare floating point numbers
+ * @return an interval with the given min value and no max.
+ */
+ public static Interval min(final double min, final DoublePrecisionContext precision) {
+ return of(min, Double.POSITIVE_INFINITY, precision);
+ }
+
+ /** Return an interval with the given max value and no min.
+ * @param max max value for the interval
+ * @param precision precision context used to compare floating point numbers
+ * @return an interval with the given max value and no min.
+ */
+ public static Interval max(final double max, final DoublePrecisionContext precision) {
+ return of(Double.NEGATIVE_INFINITY, max, precision);
+ }
+
+ /** Return an interval representing a single point at the given location.
+ * @param location the location of the interval
+ * @param precision precision context used to compare floating point numbers
+ * @return an interval representing a single point
+ */
+ public static Interval point(final double location, final DoublePrecisionContext precision) {
+ return of(location, location, precision);
+ }
+
+ /** Return an interval representing the entire real number line. The {@link #isFull()}
+ * method of the instance will return true.
+ * @return an interval representing the entire real number line
+ * @see #isFull()
+ */
+ public static Interval full() {
+ return FULL;
+ }
+
+ /** Validate that the given value can be used to construct an interval. The values
+ * must not be NaN and if infinite, must have opposite signs.
+ * @param a first value
+ * @param b second value
+ * @throws GeometryException if either value is NaN or if both values are infinite
+ * and have the same sign
+ */
+ private static void validateIntervalValues(final double a, final double b) {
+ if (Double.isNaN(a) || Double.isNaN(b) ||
+ (Double.isInfinite(a) && Double.compare(a, b) == 0)) {
+
+ throw new GeometryException(
+ MessageFormat.format("Invalid interval values: [{0}, {1}]", a, b));
+ }
+ }
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/IntervalsSet.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/IntervalsSet.java
deleted file mode 100644
index b7f1dbc..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/IntervalsSet.java
+++ /dev/null
@@ -1,619 +0,0 @@
-/*
- * 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.oned;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-import org.apache.commons.geometry.core.partitioning.AbstractRegion;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-
-/** This class represents a 1D region: a set of intervals.
- */
-public class IntervalsSet extends AbstractRegion<Vector1D, Vector1D> implements Iterable<double[]> {
-
- /** Build an intervals set representing the whole real line.
- * @param precision precision context used to compare floating point values
- */
- public IntervalsSet(final DoublePrecisionContext precision) {
- super(precision);
- }
-
- /** Build an intervals set corresponding to a single interval.
- * @param lower lower bound of the interval, must be lesser or equal
- * to {@code upper} (may be {@code Double.NEGATIVE_INFINITY})
- * @param upper upper bound of the interval, must be greater or equal
- * to {@code lower} (may be {@code Double.POSITIVE_INFINITY})
- * @param precision precision context used to compare floating point values
- */
- public IntervalsSet(final double lower, final double upper, final DoublePrecisionContext precision) {
- super(buildTree(lower, upper, precision), precision);
- }
-
- /** Build an intervals set from an inside/outside BSP tree.
- * <p>The leaf nodes of the BSP tree <em>must</em> have a
- * {@code Boolean} attribute representing the inside status of
- * the corresponding cell (true for inside cells, false for outside
- * cells). In order to avoid building too many small objects, it is
- * recommended to use the predefined constants
- * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
- * @param tree inside/outside BSP tree representing the intervals set
- * @param precision precision context used to compare floating point values
- */
- public IntervalsSet(final BSPTree<Vector1D> tree, final DoublePrecisionContext precision) {
- super(tree, precision);
- }
-
- /** Build an intervals set from a Boundary REPresentation (B-rep).
- * <p>The boundary is provided as a collection of {@link
- * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
- * interior part of the region on its minus side and the exterior on
- * its plus side.</p>
- * <p>The boundary elements can be in any order, and can form
- * several non-connected sets (like for example polygons with holes
- * or a set of disjoints polyhedrons considered as a whole). In
- * fact, the elements do not even need to be connected together
- * (their topological connections are not used here). However, if the
- * boundary does not really separate an inside open from an outside
- * open (open having here its topological meaning), then subsequent
- * calls to the {@link
- * org.apache.commons.geometry.core.partitioning.Region#checkPoint(org.apache.commons.geometry.core.Point)
- * checkPoint} method will not be meaningful anymore.</p>
- * <p>If the boundary is empty, the region will represent the whole
- * space.</p>
- * @param boundary collection of boundary elements
- * @param precision precision context used to compare floating point values
- */
- public IntervalsSet(final Collection<SubHyperplane<Vector1D>> boundary,
- final DoublePrecisionContext precision) {
- super(boundary, precision);
- }
-
- /** Build an inside/outside tree representing a single interval.
- * @param lower lower bound of the interval, must be lesser or equal
- * to {@code upper} (may be {@code Double.NEGATIVE_INFINITY})
- * @param upper upper bound of the interval, must be greater or equal
- * to {@code lower} (may be {@code Double.POSITIVE_INFINITY})
- * @param precision precision context used to compare floating point values
- * @return the built tree
- */
- private static BSPTree<Vector1D> buildTree(final double lower, final double upper,
- final DoublePrecisionContext precision) {
- if (Double.isInfinite(lower) && (lower < 0)) {
- if (Double.isInfinite(upper) && (upper > 0)) {
- // the tree must cover the whole real line
- return new BSPTree<>(Boolean.TRUE);
- }
- // the tree must be open on the negative infinity side
- final SubHyperplane<Vector1D> upperCut =
- OrientedPoint.createPositiveFacing(Vector1D.of(upper), precision).wholeHyperplane();
- return new BSPTree<>(upperCut,
- new BSPTree<Vector1D>(Boolean.FALSE),
- new BSPTree<Vector1D>(Boolean.TRUE),
- null);
- }
- final SubHyperplane<Vector1D> lowerCut =
- OrientedPoint.createNegativeFacing(Vector1D.of(lower), precision).wholeHyperplane();
- if (Double.isInfinite(upper) && (upper > 0)) {
- // the tree must be open on the positive infinity side
- return new BSPTree<>(lowerCut,
- new BSPTree<Vector1D>(Boolean.FALSE),
- new BSPTree<Vector1D>(Boolean.TRUE),
- null);
- }
-
- // the tree must be bounded on the two sides
- final SubHyperplane<Vector1D> upperCut =
- OrientedPoint.createPositiveFacing(Vector1D.of(upper), precision).wholeHyperplane();
- return new BSPTree<>(lowerCut,
- new BSPTree<Vector1D>(Boolean.FALSE),
- new BSPTree<>(upperCut,
- new BSPTree<Vector1D>(Boolean.FALSE),
- new BSPTree<Vector1D>(Boolean.TRUE),
- null),
- null);
-
- }
-
- /** {@inheritDoc} */
- @Override
- public IntervalsSet buildNew(final BSPTree<Vector1D> tree) {
- return new IntervalsSet(tree, getPrecision());
- }
-
- /** {@inheritDoc} */
- @Override
- protected void computeGeometricalProperties() {
- if (getTree(false).getCut() == null) {
- setBarycenter(Vector1D.NaN);
- setSize(((Boolean) getTree(false).getAttribute()) ? Double.POSITIVE_INFINITY : 0);
- } else {
- double size = 0.0;
- double sum = 0.0;
- for (final Interval interval : asList()) {
- size += interval.getSize();
- sum += interval.getSize() * interval.getBarycenter();
- }
- setSize(size);
- if (Double.isInfinite(size)) {
- setBarycenter(Vector1D.NaN);
- } else if (size > 0) {
- setBarycenter(Vector1D.of(sum / size));
- } else {
- setBarycenter(((OrientedPoint) getTree(false).getCut().getHyperplane()).getLocation());
- }
- }
- }
-
- /** Get the lowest value belonging to the instance.
- * @return lowest value belonging to the instance
- * ({@code Double.NEGATIVE_INFINITY} if the instance doesn't
- * have any low bound, {@code Double.POSITIVE_INFINITY} if the
- * instance is empty)
- */
- public double getInf() {
- BSPTree<Vector1D> node = getTree(false);
- double inf = Double.POSITIVE_INFINITY;
- while (node.getCut() != null) {
- final OrientedPoint op = (OrientedPoint) node.getCut().getHyperplane();
- inf = op.getLocation().getX();
- node = op.isPositiveFacing() ? node.getMinus() : node.getPlus();
- }
- return ((Boolean) node.getAttribute()) ? Double.NEGATIVE_INFINITY : inf;
- }
-
- /** Get the highest value belonging to the instance.
- * @return highest value belonging to the instance
- * ({@code Double.POSITIVE_INFINITY} if the instance doesn't
- * have any high bound, {@code Double.NEGATIVE_INFINITY} if the
- * instance is empty)
- */
- public double getSup() {
- BSPTree<Vector1D> node = getTree(false);
- double sup = Double.NEGATIVE_INFINITY;
- while (node.getCut() != null) {
- final OrientedPoint op = (OrientedPoint) node.getCut().getHyperplane();
- sup = op.getLocation().getX();
- node = op.isPositiveFacing() ? node.getPlus() : node.getMinus();
- }
- return ((Boolean) node.getAttribute()) ? Double.POSITIVE_INFINITY : sup;
- }
-
- /** {@inheritDoc}
- */
- @Override
- public BoundaryProjection<Vector1D> projectToBoundary(final Vector1D point) {
-
- // get position of test point
- final double x = point.getX();
-
- double previous = Double.NEGATIVE_INFINITY;
- for (final double[] a : this) {
- if (x < a[0]) {
- // the test point lies between the previous and the current intervals
- // offset will be positive
- final double previousOffset = x - previous;
- final double currentOffset = a[0] - x;
- if (previousOffset < currentOffset) {
- return new BoundaryProjection<>(point, finiteOrNullPoint(previous), previousOffset);
- } else {
- return new BoundaryProjection<>(point, finiteOrNullPoint(a[0]), currentOffset);
- }
- } else if (x <= a[1]) {
- // the test point lies within the current interval
- // offset will be negative
- final double offset0 = a[0] - x;
- final double offset1 = x - a[1];
- if (offset0 < offset1) {
- return new BoundaryProjection<>(point, finiteOrNullPoint(a[1]), offset1);
- } else {
- return new BoundaryProjection<>(point, finiteOrNullPoint(a[0]), offset0);
- }
- }
- previous = a[1];
- }
-
- // the test point if past the last sub-interval
- return new BoundaryProjection<>(point, finiteOrNullPoint(previous), x - previous);
-
- }
-
- /** Build a finite point.
- * @param x abscissa of the point
- * @return a new point for finite abscissa, null otherwise
- */
- private Vector1D finiteOrNullPoint(final double x) {
- return Double.isInfinite(x) ? null : Vector1D.of(x);
- }
-
- /** Build an ordered list of intervals representing the instance.
- * <p>This method builds this intervals set as an ordered list of
- * {@link Interval Interval} elements. If the intervals set has no
- * lower limit, the first interval will have its low bound equal to
- * {@code Double.NEGATIVE_INFINITY}. If the intervals set has
- * no upper limit, the last interval will have its upper bound equal
- * to {@code Double.POSITIVE_INFINITY}. An empty tree will
- * build an empty list while a tree representing the whole real line
- * will build a one element list with both bounds being
- * infinite.</p>
- * @return a new ordered list containing {@link Interval Interval}
- * elements
- */
- public List<Interval> asList() {
- final List<Interval> list = new ArrayList<>();
- for (final double[] a : this) {
- list.add(new Interval(a[0], a[1]));
- }
- return list;
- }
-
- /** Get the first leaf node of a tree.
- * @param root tree root
- * @return first leaf node
- */
- private BSPTree<Vector1D> getFirstLeaf(final BSPTree<Vector1D> root) {
-
- if (root.getCut() == null) {
- return root;
- }
-
- // find the smallest internal node
- BSPTree<Vector1D> smallest = null;
- for (BSPTree<Vector1D> n = root; n != null; n = previousInternalNode(n)) {
- smallest = n;
- }
-
- return leafBefore(smallest);
-
- }
-
- /** Get the node corresponding to the first interval boundary.
- * @return smallest internal node,
- * or null if there are no internal nodes (i.e. the set is either empty or covers the real line)
- */
- private BSPTree<Vector1D> getFirstIntervalBoundary() {
-
- // start search at the tree root
- BSPTree<Vector1D> node = getTree(false);
- if (node.getCut() == null) {
- return null;
- }
-
- // walk tree until we find the smallest internal node
- node = getFirstLeaf(node).getParent();
-
- // walk tree until we find an interval boundary
- while (node != null && !(isIntervalStart(node) || isIntervalEnd(node))) {
- node = nextInternalNode(node);
- }
-
- return node;
-
- }
-
- /** Check if an internal node corresponds to the start abscissa of an interval.
- * @param node internal node to check
- * @return true if the node corresponds to the start abscissa of an interval
- */
- private boolean isIntervalStart(final BSPTree<Vector1D> node) {
-
- if ((Boolean) leafBefore(node).getAttribute()) {
- // it has an inside cell before it, it may end an interval but not start it
- return false;
- }
-
- if (!(Boolean) leafAfter(node).getAttribute()) {
- // it has an outside cell after it, it is a dummy cut away from real intervals
- return false;
- }
-
- // the cell has an outside before and an inside after it
- // it is the start of an interval
- return true;
-
- }
-
- /** Check if an internal node corresponds to the end abscissa of an interval.
- * @param node internal node to check
- * @return true if the node corresponds to the end abscissa of an interval
- */
- private boolean isIntervalEnd(final BSPTree<Vector1D> node) {
-
- if (!(Boolean) leafBefore(node).getAttribute()) {
- // it has an outside cell before it, it may start an interval but not end it
- return false;
- }
-
- if ((Boolean) leafAfter(node).getAttribute()) {
- // it has an inside cell after it, it is a dummy cut in the middle of an interval
- return false;
- }
-
- // the cell has an inside before and an outside after it
- // it is the end of an interval
- return true;
-
- }
-
- /** Get the next internal node.
- * @param node current internal node
- * @return next internal node in ascending order, or null
- * if this is the last internal node
- */
- private BSPTree<Vector1D> nextInternalNode(BSPTree<Vector1D> node) {
-
- if (childAfter(node).getCut() != null) {
- // the next node is in the sub-tree
- return leafAfter(node).getParent();
- }
-
- // there is nothing left deeper in the tree, we backtrack
- while (isAfterParent(node)) {
- node = node.getParent();
- }
- return node.getParent();
-
- }
-
- /** Get the previous internal node.
- * @param node current internal node
- * @return previous internal node in ascending order, or null
- * if this is the first internal node
- */
- private BSPTree<Vector1D> previousInternalNode(BSPTree<Vector1D> node) {
-
- if (childBefore(node).getCut() != null) {
- // the next node is in the sub-tree
- return leafBefore(node).getParent();
- }
-
- // there is nothing left deeper in the tree, we backtrack
- while (isBeforeParent(node)) {
- node = node.getParent();
- }
- return node.getParent();
-
- }
-
- /** Find the leaf node just before an internal node.
- * @param node internal node at which the sub-tree starts
- * @return leaf node just before the internal node
- */
- private BSPTree<Vector1D> leafBefore(BSPTree<Vector1D> node) {
-
- node = childBefore(node);
- while (node.getCut() != null) {
- node = childAfter(node);
- }
-
- return node;
-
- }
-
- /** Find the leaf node just after an internal node.
- * @param node internal node at which the sub-tree starts
- * @return leaf node just after the internal node
- */
- private BSPTree<Vector1D> leafAfter(BSPTree<Vector1D> node) {
-
- node = childAfter(node);
- while (node.getCut() != null) {
- node = childBefore(node);
- }
-
- return node;
-
- }
-
- /** Check if a node is the child before its parent in ascending order.
- * @param node child node considered
- * @return true is the node has a parent end is before it in ascending order
- */
- private boolean isBeforeParent(final BSPTree<Vector1D> node) {
- final BSPTree<Vector1D> parent = node.getParent();
- if (parent == null) {
- return false;
- } else {
- return node == childBefore(parent);
- }
- }
-
- /** Check if a node is the child after its parent in ascending order.
- * @param node child node considered
- * @return true is the node has a parent end is after it in ascending order
- */
- private boolean isAfterParent(final BSPTree<Vector1D> node) {
- final BSPTree<Vector1D> parent = node.getParent();
- if (parent == null) {
- return false;
- } else {
- return node == childAfter(parent);
- }
- }
-
- /** Find the child node just before an internal node.
- * @param node internal node at which the sub-tree starts
- * @return child node just before the internal node
- */
- private BSPTree<Vector1D> childBefore(BSPTree<Vector1D> node) {
- if (isDirect(node)) {
- // smaller abscissas are on minus side, larger abscissas are on plus side
- return node.getMinus();
- } else {
- // smaller abscissas are on plus side, larger abscissas are on minus side
- return node.getPlus();
- }
- }
-
- /** Find the child node just after an internal node.
- * @param node internal node at which the sub-tree starts
- * @return child node just after the internal node
- */
- private BSPTree<Vector1D> childAfter(BSPTree<Vector1D> node) {
- if (isDirect(node)) {
- // smaller abscissas are on minus side, larger abscissas are on plus side
- return node.getPlus();
- } else {
- // smaller abscissas are on plus side, larger abscissas are on minus side
- return node.getMinus();
- }
- }
-
- /** Check if an internal node has a direct oriented point.
- * @param node internal node to check
- * @return true if the oriented point is direct
- */
- private boolean isDirect(final BSPTree<Vector1D> node) {
- return ((OrientedPoint) node.getCut().getHyperplane()).isPositiveFacing();
- }
-
- /** Get the abscissa of an internal node.
- * @param node internal node to check
- * @return abscissa
- */
- private double getAngle(final BSPTree<Vector1D> node) {
- return ((OrientedPoint) node.getCut().getHyperplane()).getLocation().getX();
- }
-
- /** {@inheritDoc}
- * <p>
- * The iterator returns the limit values of sub-intervals in ascending order.
- * </p>
- * <p>
- * The iterator does <em>not</em> support the optional {@code remove} operation.
- * </p>
- */
- @Override
- public Iterator<double[]> iterator() {
- return new SubIntervalsIterator();
- }
-
- /** Local iterator for sub-intervals. */
- private class SubIntervalsIterator implements Iterator<double[]> {
-
- /** Current node. */
- private BSPTree<Vector1D> current;
-
- /** Sub-interval no yet returned. */
- private double[] pending;
-
- /** Simple constructor.
- */
- SubIntervalsIterator() {
-
- current = getFirstIntervalBoundary();
-
- if (current == null) {
- // all the leaf tree nodes share the same inside/outside status
- if ((Boolean) getFirstLeaf(getTree(false)).getAttribute()) {
- // it is an inside node, it represents the full real line
- pending = new double[] {
- Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY
- };
- } else {
- pending = null;
- }
- } else if (isIntervalEnd(current)) {
- // the first boundary is an interval end,
- // so the first interval starts at infinity
- pending = new double[] {
- Double.NEGATIVE_INFINITY, getAngle(current)
- };
- } else {
- selectPending();
- }
- }
-
- /** Walk the tree to select the pending sub-interval.
- */
- private void selectPending() {
-
- // look for the start of the interval
- BSPTree<Vector1D> start = current;
- while (start != null && !isIntervalStart(start)) {
- start = nextInternalNode(start);
- }
-
- if (start == null) {
- // we have exhausted the iterator
- current = null;
- pending = null;
- return;
- }
-
- // look for the end of the interval
- BSPTree<Vector1D> end = start;
- while (end != null && !isIntervalEnd(end)) {
- end = nextInternalNode(end);
- }
-
- if (end != null) {
-
- // we have identified the interval
- pending = new double[] {
- getAngle(start), getAngle(end)
- };
-
- // prepare search for next interval
- current = end;
-
- } else {
-
- // the final interval is open toward infinity
- pending = new double[] {
- getAngle(start), Double.POSITIVE_INFINITY
- };
-
- // there won't be any other intervals
- current = null;
-
- }
-
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean hasNext() {
- return pending != null;
- }
-
- /** {@inheritDoc} */
- @Override
- public double[] next() {
- if (pending == null) {
- throw new NoSuchElementException();
- }
- final double[] next = pending;
- selectPending();
- return next;
- }
-
- /** {@inheritDoc} */
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
-
- }
-
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
index 8888eb9..51c993c 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
@@ -17,11 +17,21 @@
package org.apache.commons.geometry.euclidean.oned;
import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
import java.util.Objects;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Transform;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
/** This class represents a 1D oriented hyperplane.
@@ -31,20 +41,18 @@
*
* <p>Instances of this class are guaranteed to be immutable.</p>
*/
-public final class OrientedPoint implements Hyperplane<Vector1D>, Serializable {
+public final class OrientedPoint extends AbstractHyperplane<Vector1D>
+ implements Hyperplane<Vector1D>, Equivalency<OrientedPoint>, Serializable {
/** Serializable UID. */
private static final long serialVersionUID = 20190210L;
- /** Hyperplane location. */
- private final Vector1D location;
+ /** Hyperplane location as a point. */
+ private final Vector1D point;
/** Hyperplane direction. */
private final boolean positiveFacing;
- /** Precision context used to compare floating point numbers. */
- private final DoublePrecisionContext precision;
-
/** Simple constructor.
* @param point location of the hyperplane
* @param positiveFacing if true, the hyperplane will face toward positive infinity;
@@ -52,16 +60,28 @@
* @param precision precision context used to compare floating point values
*/
private OrientedPoint(final Vector1D point, final boolean positiveFacing, final DoublePrecisionContext precision) {
- this.location = point;
+ super(precision);
+
+ this.point = point;
this.positiveFacing = positiveFacing;
- this.precision = precision;
}
- /** Get the point representing the hyperplane's location on the real line.
- * @return the hyperplane location
+ /** Get the location of the hyperplane as a point.
+ * @return the hyperplane location as a point
+ * @see #getLocation()
*/
- public Vector1D getLocation() {
- return location;
+ public Vector1D getPoint() {
+ return point;
+ }
+
+ /**
+ * Get the location of the hyperplane as a single value. This is
+ * equivalent to {@code pt.getPoint().getX()}.
+ * @return the location of the hyperplane as a single value.
+ * @see #getPoint()
+ */
+ public double getLocation() {
+ return point.getX();
}
/** Get the direction of the hyperplane's plus side.
@@ -83,85 +103,116 @@
/** {@inheritDoc} */
@Override
- public DoublePrecisionContext getPrecision() {
- return precision;
- }
-
- /** Get an instance with the same location and precision but the opposite
- * direction.
- * @return a copy of this instance with the opposite direction
- */
public OrientedPoint reverse() {
- return new OrientedPoint(location, !positiveFacing, precision);
+ return new OrientedPoint(point, !positiveFacing, getPrecision());
}
- /** Return a new instance transformed by the given {@link Transform}.
- * @param transform transform object
- * @return a transformed instance
- */
- public OrientedPoint transform(final Transform<Vector1D, Vector1D> transform) {
- Vector1D transformedLocation = transform.apply(location);
- Vector1D transformedPlusDirPt = transform.apply(location.add(getDirection()));
+ /** {@inheritDoc} */
+ @Override
+ public OrientedPoint transform(final Transform<Vector1D> transform) {
+ final Vector1D transformedPoint = transform.apply(point);
+
+ Vector1D transformedDir;
+ if (point.isInfinite()) {
+ // use a test point to determine if the direction switches or not
+ final Vector1D transformedZero = transform.apply(Vector1D.ZERO);
+ final Vector1D transformedZeroDir = transform.apply(getDirection());
+
+ transformedDir = transformedZero.vectorTo(transformedZeroDir);
+ } else {
+ final Vector1D transformedPointPlusDir = transform.apply(point.add(getDirection()));
+ transformedDir = transformedPoint.vectorTo(transformedPointPlusDir);
+ }
return OrientedPoint.fromPointAndDirection(
- transformedLocation,
- transformedLocation.vectorTo(transformedPlusDirPt),
- precision
+ transformedPoint,
+ transformedDir,
+ getPrecision()
);
}
- /** Copy the instance.
- * <p>Since instances are immutable, this method directly returns
- * the instance.</p>
- * @return the instance itself
- */
- @Override
- public OrientedPoint copySelf() {
- return this;
- }
-
/** {@inheritDoc} */
@Override
- public double getOffset(final Vector1D point) {
- final double delta = point.getX() - location.getX();
+ public double offset(final Vector1D pt) {
+ return offset(pt.getX());
+ }
+
+ /** Compute the offset of the given number line location. This is
+ * a convenience overload of {@link #offset(Vector1D)} for use in
+ * one dimension.
+ * @param location the number line location to compute the offset for
+ * @return the offset of the location from the instance
+ */
+ public double offset(final double location) {
+ final double delta = location - point.getX();
return positiveFacing ? delta : -delta;
}
- /** Build a region covering the whole hyperplane.
- * <p>Since this class represent zero dimension spaces which does
- * not have lower dimension sub-spaces, this method returns a dummy
- * implementation of a {@link
- * org.apache.commons.geometry.core.partitioning.SubHyperplane SubHyperplane}.
- * This implementation is only used to allow the {@link
- * org.apache.commons.geometry.core.partitioning.SubHyperplane
- * SubHyperplane} class implementation to work properly, it should
- * <em>not</em> be used otherwise.</p>
- * @return a dummy sub hyperplane
- */
+ /** {@inheritDoc} */
@Override
- public SubOrientedPoint wholeHyperplane() {
- return new SubOrientedPoint(this, null);
+ public HyperplaneLocation classify(final Vector1D pt) {
+ return classify(pt.getX());
}
- /** Build a region covering the whole space.
- * @return a region containing the instance (really an {@link
- * IntervalsSet IntervalsSet} instance)
+ /** Classify the number line location with respect to the instance.
+ * This is a convenience overload of {@link #classify(Vector1D)} for
+ * use in one dimension.
+ * @param location the number line location to classify
+ * @return the classification of the number line location with respect
+ * to this instance
*/
- @Override
- public IntervalsSet wholeSpace() {
- return new IntervalsSet(precision);
+ public HyperplaneLocation classify(final double location) {
+ final double offsetValue = offset(location);
+
+ final int cmp = getPrecision().sign(offsetValue);
+ if (cmp > 0) {
+ return HyperplaneLocation.PLUS;
+ } else if (cmp < 0) {
+ return HyperplaneLocation.MINUS;
+ }
+ return HyperplaneLocation.ON;
}
/** {@inheritDoc} */
@Override
- public boolean sameOrientationAs(final Hyperplane<Vector1D> other) {
+ public boolean similarOrientation(final Hyperplane<Vector1D> other) {
return positiveFacing == ((OrientedPoint) other).positiveFacing;
}
/** {@inheritDoc} */
@Override
- public Vector1D project(final Vector1D point) {
- return location;
+ public Vector1D project(final Vector1D pt) {
+ return this.point;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubOrientedPoint span() {
+ return new SubOrientedPoint(this);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>Instances are considered equivalent if they
+ * <ul>
+ * <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
+ * <li>have equivalent locations as evaluated by the precision context, and</li>
+ * <li>point in the same direction</li>
+ * </ul>
+ * @param other the point to compare with
+ * @return true if this instance should be considered equivalent to the argument
+ */
+ @Override
+ public boolean eq(final OrientedPoint other) {
+ if (this == other) {
+ return true;
+ }
+
+ final DoublePrecisionContext precision = getPrecision();
+
+ return precision.equals(other.getPrecision()) &&
+ point.eq(other.point, precision) &&
+ positiveFacing == other.positiveFacing;
}
/** {@inheritDoc} */
@@ -170,9 +221,9 @@
final int prime = 31;
int result = 1;
- result = (prime * result) + Objects.hashCode(location);
+ result = (prime * result) + Objects.hashCode(point);
result = (prime * result) + Boolean.hashCode(positiveFacing);
- result = (prime * result) + Objects.hashCode(precision);
+ result = (prime * result) + Objects.hashCode(getPrecision());
return result;
}
@@ -182,16 +233,15 @@
public boolean equals(Object obj) {
if (this == obj) {
return true;
- }
- else if (!(obj instanceof OrientedPoint)) {
+ } else if (!(obj instanceof OrientedPoint)) {
return false;
}
OrientedPoint other = (OrientedPoint) obj;
- return Objects.equals(this.location, other.location) &&
+ return Objects.equals(this.point, other.point) &&
this.positiveFacing == other.positiveFacing &&
- Objects.equals(this.precision, other.precision);
+ Objects.equals(this.getPrecision(), other.getPrecision());
}
/** {@inheritDoc} */
@@ -199,8 +249,8 @@
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName())
- .append("[location= ")
- .append(location)
+ .append("[point= ")
+ .append(point)
.append(", direction= ")
.append(getDirection())
.append(']');
@@ -208,6 +258,18 @@
return sb.toString();
}
+ /** Create a new instance from the given location and boolean direction value.
+ * @param location the location of the hyperplane
+ * @param positiveFacing if true, the hyperplane will face toward positive infinity;
+ * otherwise, it will point toward negative infinity.
+ * @param precision precision context used to compare floating point values
+ * @return a new instance
+ */
+ public static OrientedPoint fromLocationAndDirection(final double location, final boolean positiveFacing,
+ final DoublePrecisionContext precision) {
+ return fromPointAndDirection(Vector1D.of(location), positiveFacing, precision);
+ }
+
/** Create a new instance from the given point and boolean direction value.
* @param point the location of the hyperplane
* @param positiveFacing if true, the hyperplane will face toward positive infinity;
@@ -248,6 +310,15 @@
return new OrientedPoint(point, true, precision);
}
+ /** Create a new instance at the given location, oriented so that it is facing positive infinity.
+ * @param location the location of the hyperplane
+ * @param precision precision context used to compare floating point values
+ * @return a new instance oriented toward positive infinity
+ */
+ public static OrientedPoint createPositiveFacing(final double location, final DoublePrecisionContext precision) {
+ return new OrientedPoint(Vector1D.of(location), true, precision);
+ }
+
/** Create a new instance at the given point, oriented so that it is facing negative infinity.
* @param point the location of the hyperplane
* @param precision precision context used to compare floating point values
@@ -256,4 +327,222 @@
public static OrientedPoint createNegativeFacing(final Vector1D point, final DoublePrecisionContext precision) {
return new OrientedPoint(point, false, precision);
}
+
+ /** Create a new instance at the given location, oriented so that it is facing negative infinity.
+ * @param location the location of the hyperplane
+ * @param precision precision context used to compare floating point values
+ * @return a new instance oriented toward negative infinity
+ */
+ public static OrientedPoint createNegativeFacing(final double location, final DoublePrecisionContext precision) {
+ return new OrientedPoint(Vector1D.of(location), false, precision);
+ }
+
+ /** {@link ConvexSubHyperplane} implementation for Euclidean 1D space. Since there are no subspaces in 1D,
+ * this is effectively a stub implementation, its main use being to allow for the correct functioning of
+ * partitioning code.
+ */
+ public static class SubOrientedPoint implements ConvexSubHyperplane<Vector1D>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190405L;
+
+ /** The underlying hyperplane for this instance. */
+ private final OrientedPoint hyperplane;
+
+ /** Simple constructor.
+ * @param hyperplane underlying hyperplane instance
+ */
+ public SubOrientedPoint(final OrientedPoint hyperplane) {
+ this.hyperplane = hyperplane;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public OrientedPoint getHyperplane() {
+ return hyperplane;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns false.</p>
+ */
+ @Override
+ public boolean isFull() {
+ return false;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns false.</p>
+ */
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns false.</p>
+ */
+ @Override
+ public boolean isInfinite() {
+ return false;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns true.</p>
+ */
+ @Override
+ public boolean isFinite() {
+ return true;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns {@code 0}.</p>
+ */
+ @Override
+ public double getSize() {
+ return 0;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method returns {@link RegionLocation#BOUNDARY} if the
+ * point is on the hyperplane and {@link RegionLocation#OUTSIDE}
+ * otherwise.</p>
+ */
+ @Override
+ public RegionLocation classify(final Vector1D point) {
+ if (hyperplane.contains(point)) {
+ return RegionLocation.BOUNDARY;
+ }
+
+ return RegionLocation.OUTSIDE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector1D closest(Vector1D point) {
+ return hyperplane.project(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<SubOrientedPoint> split(final Hyperplane<Vector1D> splitter) {
+ final HyperplaneLocation side = splitter.classify(hyperplane.getPoint());
+
+ SubOrientedPoint minus = null;
+ SubOrientedPoint plus = null;
+
+ if (side == HyperplaneLocation.MINUS) {
+ minus = this;
+ } else if (side == HyperplaneLocation.PLUS) {
+ plus = this;
+ }
+
+ return new Split<>(minus, plus);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<SubOrientedPoint> toConvex() {
+ return Arrays.asList(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubOrientedPoint transform(final Transform<Vector1D> transform) {
+ return getHyperplane().transform(transform).span();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubOrientedPointBuilder builder() {
+ return new SubOrientedPointBuilder(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubOrientedPoint reverse() {
+ return new SubOrientedPoint(hyperplane.reverse());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[hyperplane= ")
+ .append(hyperplane)
+ .append(']');
+
+ return sb.toString();
+ }
+ }
+
+ /** {@link SubHyperplane.Builder} implementation for Euclidean 1D space. Similar to {@link SubOrientedPoint},
+ * this is effectively a stub implementation since there are no subspaces of 1D space. Its primary use is to allow
+ * for the correct functioning of partitioning code.
+ */
+ public static final class SubOrientedPointBuilder implements SubHyperplane.Builder<Vector1D>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190405L;
+
+ /** Base subhyperplane for the builder. */
+ private final SubOrientedPoint base;
+
+ /** Construct a new instance using the given base subhyperplane.
+ * @param base base subhyperplane for the instance
+ */
+ private SubOrientedPointBuilder(final SubOrientedPoint base) {
+ this.base = base;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final SubHyperplane<Vector1D> sub) {
+ validateHyperplane(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final ConvexSubHyperplane<Vector1D> sub) {
+ validateHyperplane(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubOrientedPoint build() {
+ return base;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[base= ")
+ .append(base)
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Validate the given subhyperplane lies on the same hyperplane.
+ * @param sub subhyperplane to validate
+ */
+ private void validateHyperplane(final SubHyperplane<Vector1D> sub) {
+ final OrientedPoint baseHyper = base.getHyperplane();
+ final OrientedPoint inputHyper = (OrientedPoint) sub.getHyperplane();
+
+ if (!baseHyper.eq(inputHyper)) {
+ throw new GeometryException("Argument is not on the same " +
+ "hyperplane. Expected " + baseHyper + " but was " +
+ inputHyper);
+ }
+ }
+ }
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java
new file mode 100644
index 0000000..8b5a539
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java
@@ -0,0 +1,578 @@
+/*
+ * 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.oned;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
+
+/** Binary space partitioning (BSP) tree representing a region in one dimensional
+ * Euclidean space.
+ */
+public final class RegionBSPTree1D extends AbstractRegionBSPTree<Vector1D, RegionBSPTree1D.RegionNode1D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190405L;
+
+ /** Comparator used to sort BoundaryPairs by ascending location. */
+ private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR = (BoundaryPair a, BoundaryPair b) -> {
+ return Double.compare(a.getMinValue(), b.getMinValue());
+ };
+
+ /** Create a new, empty region.
+ */
+ public RegionBSPTree1D() {
+ this(false);
+ }
+
+ /** Create a new region. If {@code full} is true, then the region will
+ * represent the entire number line. Otherwise, it will be empty.
+ * @param full whether or not the region should contain the entire
+ * number line or be empty
+ */
+ public RegionBSPTree1D(boolean full) {
+ super(full);
+ }
+
+ /** Return a deep copy of this instance.
+ * @return a deep copy of this instance.
+ * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
+ */
+ public RegionBSPTree1D copy() {
+ RegionBSPTree1D result = RegionBSPTree1D.empty();
+ result.copy(this);
+
+ return result;
+ }
+
+ /** Add an interval to this region. The resulting region will be the
+ * union of the interval and the region represented by this instance.
+ * @param interval the interval to add
+ */
+ public void add(final Interval interval) {
+ union(intervalToTree(interval));
+ }
+
+ /** Classify a point location with respect to the region.
+ * @param x the point to classify
+ * @return the location of the point with respect to the region
+ * @see #classify(Point)
+ */
+ public RegionLocation classify(final double x) {
+ return classify(Vector1D.of(x));
+ }
+
+ /** Return true if the given point location is on the inside or boundary
+ * of the region.
+ * @param x the location to test
+ * @return true if the location is on the inside or boundary of the region
+ * @see #contains(Point)
+ */
+ public boolean contains(final double x) {
+ return contains(Vector1D.of(x));
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns 0 because boundaries in one dimension do not
+ * have any size.</p>
+ */
+ @Override
+ public double getBoundarySize() {
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector1D project(final Vector1D pt) {
+ // use our custom projector so that we can disambiguate points that are
+ // actually equidistant from the target point
+ final BoundaryProjector1D projector = new BoundaryProjector1D(pt);
+ accept(projector);
+
+ return projector.getProjected();
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>When splitting trees representing single points with a splitter lying directly
+ * on the point, the result point is placed on one side of the splitter based on its
+ * orientation: if the splitter is positive-facing, the point is placed on the plus
+ * side of the split; if the splitter is negative-facing, the point is placed on the
+ * minus side of the split.</p>
+ */
+ @Override
+ public Split<RegionBSPTree1D> split(final Hyperplane<Vector1D> splitter) {
+ return split(splitter, RegionBSPTree1D.empty(), RegionBSPTree1D.empty());
+ }
+
+ /** Get the minimum value on the inside of the region; returns {@link Double#NEGATIVE_INFINITY}
+ * if the region does not have a minimum value and {@link Double#POSITIVE_INFINITY} if
+ * the region is empty.
+ * @return the minimum value on the inside of the region
+ */
+ public double getMin() {
+ double min = Double.POSITIVE_INFINITY;
+
+ RegionNode1D node = getRoot();
+ OrientedPoint pt;
+
+ while (!node.isLeaf()) {
+ pt = (OrientedPoint) node.getCutHyperplane();
+
+ min = pt.getLocation();
+ node = pt.isPositiveFacing() ? node.getMinus() : node.getPlus();
+ }
+
+ return node.isInside() ? Double.NEGATIVE_INFINITY : min;
+ }
+
+ /** Get the maximum value on the inside of the region; returns {@link Double#POSITIVE_INFINITY}
+ * if the region does not have a maximum value and {@link Double#NEGATIVE_INFINITY} if
+ * the region is empty.
+ * @return the maximum value on the inside of the region
+ */
+ public double getMax() {
+ double max = Double.NEGATIVE_INFINITY;
+
+ RegionNode1D node = getRoot();
+ OrientedPoint pt;
+
+ while (!node.isLeaf()) {
+ pt = (OrientedPoint) node.getCutHyperplane();
+
+ max = pt.getLocation();
+ node = pt.isPositiveFacing() ? node.getPlus() : node.getMinus();
+ }
+
+ return node.isInside() ? Double.POSITIVE_INFINITY : max;
+ }
+
+ /** Convert the region represented by this tree into a list of separate
+ * {@link Interval}s, arranged in order of ascending min value.
+ * @return list of {@link Interval}s representing this region in order of
+ * ascending min value
+ */
+ public List<Interval> toIntervals() {
+
+ final List<BoundaryPair> boundaryPairs = new ArrayList<>();
+
+ visitInsideIntervals((min, max) -> {
+ boundaryPairs.add(new BoundaryPair(min, max));
+ });
+
+ boundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR);
+
+ final List<Interval> intervals = new ArrayList<>();
+
+ BoundaryPair start = null;
+ BoundaryPair end = null;
+
+ for (BoundaryPair current : boundaryPairs) {
+ if (start == null) {
+ start = current;
+ end = current;
+ } else if (Objects.equals(end.getMax(), current.getMin())) {
+ // these intervals should be merged
+ end = current;
+ } else {
+ // these intervals should not be merged
+ intervals.add(createInterval(start, end));
+
+ // queue up the next pair
+ start = current;
+ end = current;
+ }
+ }
+
+ if (start != null && end != null) {
+ intervals.add(createInterval(start, end));
+ }
+
+ return intervals;
+ }
+
+ /** Create an interval instance from the min boundary from the start boundary pair and
+ * the max boundary from the end boundary pair. The hyperplane directions are adjusted
+ * as needed.
+ * @param start starting boundary pair
+ * @param end ending boundary pair
+ * @return an interval created from the min boundary of the given start pair and the
+ * max boundary from the given end pair
+ */
+ private Interval createInterval(final BoundaryPair start, final BoundaryPair end) {
+ OrientedPoint min = start.getMin();
+ OrientedPoint max = end.getMax();
+
+ // flip the hyperplanes if needed since there's no
+ // guarantee that the inside will be on the minus side
+ // of the hyperplane (for example, if the region is complemented)
+
+ if (min != null && min.isPositiveFacing()) {
+ min = min.reverse();
+ }
+ if (max != null && !max.isPositiveFacing()) {
+ max = max.reverse();
+ }
+
+ return Interval.of(min, max);
+ }
+
+ /** Compute the min/max intervals for all interior convex regions in the tree and
+ * pass the values to the given visitor function.
+ * @param visitor the object that will receive the calculated min and max boundary for each
+ * insides node's convex region
+ */
+ private void visitInsideIntervals(final BiConsumer<OrientedPoint, OrientedPoint> visitor) {
+ for (RegionNode1D node : this) {
+ if (node.isInside()) {
+ visitNodeInterval(node, visitor);
+ }
+ }
+ }
+
+ /** Determine the min/max boundaries for the convex region represented by the given node and pass
+ * the values to the visitor function.
+ * @param node the node to compute the interval for
+ * @param visitor the object that will receive the min and max boundaries for the node's
+ * convex region
+ */
+ private void visitNodeInterval(final RegionNode1D node, final BiConsumer<OrientedPoint, OrientedPoint> visitor) {
+ OrientedPoint min = null;
+ OrientedPoint max = null;
+
+ OrientedPoint pt;
+ RegionNode1D child = node;
+ RegionNode1D parent;
+
+ while ((min == null || max == null) && (parent = child.getParent()) != null) {
+ pt = (OrientedPoint) parent.getCutHyperplane();
+
+ if ((pt.isPositiveFacing() && child.isMinus()) ||
+ (!pt.isPositiveFacing() && child.isPlus())) {
+
+ if (max == null) {
+ max = pt;
+ }
+ } else if (min == null) {
+ min = pt;
+ }
+
+ child = parent;
+ }
+
+ visitor.accept(min, max);
+ }
+
+ /** Compute the region represented by the given node.
+ * @param node the node to compute the region for
+ * @return the region represented by the given node
+ */
+ private Interval computeNodeRegion(final RegionNode1D node) {
+ final NodeRegionVisitor visitor = new NodeRegionVisitor();
+ visitNodeInterval(node, visitor);
+
+ return visitor.getInterval();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode1D createNode() {
+ return new RegionNode1D(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionSizeProperties<Vector1D> computeRegionSizeProperties() {
+ RegionSizePropertiesVisitor visitor = new RegionSizePropertiesVisitor();
+
+ visitInsideIntervals(visitor);
+
+ return visitor.getRegionSizeProperties();
+ }
+
+ /** Returns true if the given transform would result in a swapping of the interior
+ * and exterior of the region if applied.
+ *
+ * <p>This method always returns false since no swapping of this kind occurs in
+ * 1D.</p>
+ */
+ @Override
+ protected boolean swapsInsideOutside(final Transform<Vector1D> transform) {
+ return false;
+ }
+
+ /** Return a new {@link RegionBSPTree1D} instance containing the entire space.
+ * @return a new {@link RegionBSPTree1D} instance containing the entire space
+ */
+ public static RegionBSPTree1D full() {
+ return new RegionBSPTree1D(true);
+ }
+
+ /** Return a new, empty {@link RegionBSPTree1D} instance.
+ * @return a new, empty {@link RegionBSPTree1D} instance
+ */
+ public static RegionBSPTree1D empty() {
+ return new RegionBSPTree1D(false);
+ }
+
+ /** Construct a new instance from one or more intervals. The returned tree
+ * represents the same region as the union of all of the input intervals.
+ * @param interval the input interval
+ * @param more additional intervals to add to the region
+ * @return a new instance representing the same region as the union
+ * of all of the given intervals
+ */
+ public static RegionBSPTree1D from(final Interval interval, final Interval... more) {
+ final RegionBSPTree1D tree = intervalToTree(interval);
+
+ for (final Interval additional : more) {
+ tree.add(additional);
+ }
+
+ return tree;
+ }
+
+ /** Construct a new instance from the given collection of intervals.
+ * @param intervals the intervals to populate the region with
+ * @return a new instance constructed from the given collection of intervals
+ */
+ public static RegionBSPTree1D from(final Iterable<Interval> intervals) {
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+
+ for (final Interval interval : intervals) {
+ tree.add(interval);
+ }
+
+ return tree;
+ }
+
+ /** Return a tree representing the same region as the given interval.
+ * @param interval interval to create a tree from
+ * @return a tree representing the same region as the given interval
+ */
+ private static RegionBSPTree1D intervalToTree(final Interval interval) {
+ final OrientedPoint minBoundary = interval.getMinBoundary();
+ final OrientedPoint maxBoundary = interval.getMaxBoundary();
+
+ final RegionBSPTree1D tree = full();
+
+ RegionNode1D node = tree.getRoot();
+
+ if (minBoundary != null) {
+ tree.cutNode(node, minBoundary.span());
+
+ node = node.getMinus();
+ }
+
+ if (maxBoundary != null) {
+ tree.cutNode(node, maxBoundary.span());
+ }
+
+ return tree;
+ }
+
+ /** BSP tree node for one dimensional Euclidean space.
+ */
+ public static final class RegionNode1D extends AbstractRegionBSPTree.AbstractRegionNode<Vector1D, RegionNode1D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190405L;
+
+ /** Simple constructor.
+ * @param tree the owning tree instance
+ */
+ private RegionNode1D(AbstractBSPTree<Vector1D, RegionNode1D> tree) {
+ super(tree);
+ }
+
+ /** Get the region represented by this node. The returned region contains
+ * the entire area contained in this node, regardless of the attributes of
+ * any child nodes.
+ * @return the region represented by this node
+ */
+ public Interval getNodeRegion() {
+ return ((RegionBSPTree1D) getTree()).computeNodeRegion(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode1D getSelf() {
+ return this;
+ }
+ }
+
+ /** Internal class containing pairs of interval boundaries.
+ */
+ private static final class BoundaryPair {
+
+ /** The min boundary. */
+ private final OrientedPoint min;
+
+ /** The max boundary. */
+ private final OrientedPoint max;
+
+ /** Simple constructor.
+ * @param min min boundary hyperplane
+ * @param max max boundary hyperplane
+ */
+ BoundaryPair(final OrientedPoint min, final OrientedPoint max) {
+ this.min = min;
+ this.max = max;
+ }
+
+ /** Get the minimum boundary hyperplane.
+ * @return the minimum boundary hyperplane.
+ */
+ public OrientedPoint getMin() {
+ return min;
+ }
+
+ /** Get the maximum boundary hyperplane.
+ * @return the maximum boundary hyperplane.
+ */
+ public OrientedPoint getMax() {
+ return max;
+ }
+
+ /** Get the minumum value of the interval or {@link Double#NEGATIVE_INFINITY}
+ * if no minimum value exists.
+ * @return the minumum value of the interval or {@link Double#NEGATIVE_INFINITY}
+ * if no minimum value exists.
+ */
+ public double getMinValue() {
+ return (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
+ }
+ }
+
+ /** Class used to project points onto the region boundary.
+ */
+ private static final class BoundaryProjector1D extends BoundaryProjector<Vector1D, RegionNode1D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190405L;
+
+ /** Simple constructor.
+ * @param point the point to project onto the region's boundary
+ */
+ BoundaryProjector1D(Vector1D point) {
+ super(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector1D disambiguateClosestPoint(final Vector1D target, final Vector1D a, final Vector1D b) {
+ final int cmp = Vector1D.COORDINATE_ASCENDING_ORDER.compare(a, b);
+
+ if (target.isInfinite() && target.getX() > 0) {
+ // return the largest value (closest to +Infinity)
+ return cmp < 0 ? b : a;
+ }
+
+ // return the smallest value
+ return cmp < 0 ? a : b;
+ }
+ }
+
+ /** Internal class for calculating the region of a single tree node.
+ */
+ private static final class NodeRegionVisitor implements BiConsumer<OrientedPoint, OrientedPoint> {
+
+ /** The min boundary for the region. */
+ private OrientedPoint min;
+
+ /** The max boundary for the region. */
+ private OrientedPoint max;
+
+ /** {@inheritDoc} */
+ @Override
+ public void accept(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) {
+ // reverse the oriented point directions if needed
+ this.min = (minBoundary != null && minBoundary.isPositiveFacing()) ? minBoundary.reverse() : minBoundary;
+ this.max = (maxBoundary != null && !maxBoundary.isPositiveFacing()) ? maxBoundary.reverse() : maxBoundary;
+ }
+
+ /** Return the computed interval.
+ * @return the computed interval.
+ */
+ public Interval getInterval() {
+ return Interval.of(min, max);
+ }
+ }
+
+ /** Internal class for calculating size-related properties for a {@link RegionBSPTree1D}.
+ */
+ private static final class RegionSizePropertiesVisitor implements BiConsumer<OrientedPoint, OrientedPoint> {
+ /** Number of inside intervals visited. */
+ private int count = 0;
+
+ /** Total computed size of all inside regions. */
+ private double size = 0;
+
+ /** Raw sum of the barycenters of each inside interval. */
+ private double rawBarycenterSum = 0;
+
+ /** The sum of the barycenter of each inside interval, scaled by the size of the interval. */
+ private double scaledBarycenterSum = 0;
+
+ /** {@inheritDoc} */
+ @Override
+ public void accept(OrientedPoint min, OrientedPoint max) {
+ ++count;
+
+ final double minLoc = (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
+ final double maxLoc = (max != null) ? max.getLocation() : Double.POSITIVE_INFINITY;
+
+ final double intervalSize = maxLoc - minLoc;
+ final double intervalBarycenter = 0.5 * (maxLoc + minLoc);
+
+ size += intervalSize;
+ rawBarycenterSum += intervalBarycenter;
+ scaledBarycenterSum += intervalSize * intervalBarycenter;
+ }
+
+ /** Get the computed properties for the region. This must only be called after
+ * every inside interval has been visited.
+ * @return properties for the region
+ */
+ public RegionSizeProperties<Vector1D> getRegionSizeProperties() {
+ Vector1D barycenter = null;
+
+ if (count > 0 && Double.isFinite(size)) {
+ if (size > 0.0) {
+ // use the scaled sum if we have a non-zero size
+ barycenter = Vector1D.of(scaledBarycenterSum / size);
+ } else {
+ // use the raw sum if we don't have a size; this will be
+ // the case if the region only contains points with zero size
+ barycenter = Vector1D.of(rawBarycenterSum / count);
+ }
+ }
+
+ return new RegionSizeProperties<>(size, barycenter);
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPoint.java
deleted file mode 100644
index e228fb0..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPoint.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.oned;
-
-import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-
-/** This class represents sub-hyperplane for {@link OrientedPoint}.
- * <p>An hyperplane in 1D is a simple point, its orientation being a
- * boolean.</p>
- * <p>Instances of this class are guaranteed to be immutable.</p>
- */
-public class SubOrientedPoint extends AbstractSubHyperplane<Vector1D, Vector1D> {
-
- /** Simple constructor.
- * @param hyperplane underlying hyperplane
- * @param remainingRegion remaining region of the hyperplane
- */
- public SubOrientedPoint(final Hyperplane<Vector1D> hyperplane,
- final Region<Vector1D> remainingRegion) {
- super(hyperplane, remainingRegion);
- }
-
- /** {@inheritDoc} */
- @Override
- public double getSize() {
- return 0;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isEmpty() {
- return false;
- }
-
- /** {@inheritDoc} */
- @Override
- protected AbstractSubHyperplane<Vector1D, Vector1D> buildNew(final Hyperplane<Vector1D> hyperplane,
- final Region<Vector1D> remainingRegion) {
- return new SubOrientedPoint(hyperplane, remainingRegion);
- }
-
- /** {@inheritDoc} */
- @Override
- public SplitSubHyperplane<Vector1D> split(final Hyperplane<Vector1D> hyperplane) {
- final OrientedPoint thisHyperplane = (OrientedPoint) getHyperplane();
- final double global = hyperplane.getOffset(thisHyperplane.getLocation());
-
- // use the precision context from our parent hyperplane to determine equality
- final DoublePrecisionContext precision = thisHyperplane.getPrecision();
-
- int comparison = precision.compare(global, 0.0);
-
- if (comparison < 0) {
- return new SplitSubHyperplane<Vector1D>(null, this);
- } else if (comparison > 0) {
- return new SplitSubHyperplane<Vector1D>(this, null);
- } else {
- return new SplitSubHyperplane<Vector1D>(null, null);
- }
- }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Transform1D.java
similarity index 61%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Transform1D.java
index 046defe..1f0eac1 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Transform1D.java
@@ -14,23 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partitioning;
+package org.apache.commons.geometry.euclidean.oned;
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+import org.apache.commons.geometry.euclidean.EuclideanTransform;
+
+/** Extension of the {@link EuclideanTransform} interface for 1D space.
*/
-public enum Side {
+public interface Transform1D extends EuclideanTransform<Vector1D> {
- /** Code for the plus side of the hyperplane. */
- PLUS,
-
- /** Code for the minus side of the hyperplane. */
- MINUS,
-
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
-
- /** Code for the hyperplane itself. */
- HYPER;
-
+ /** Return an affine transform matrix representing the same transform
+ * as this instance.
+ * @return an affine tranform matrix representing the same transform
+ * as this instance
+ */
+ AffineTransformMatrix1D toMatrix();
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
index 48a3c5f..57ff97d 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
@@ -16,8 +16,10 @@
*/
package org.apache.commons.geometry.euclidean.oned;
+import java.util.Comparator;
+import java.util.function.Function;
+
import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.exception.IllegalNormException;
import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.EuclideanVector;
@@ -45,6 +47,24 @@
public static final Vector1D NEGATIVE_INFINITY =
new Vector1D(Double.NEGATIVE_INFINITY);
+ /** Comparator that sorts vectors in component-wise ascending order.
+ * Vectors are only considered equal if their coordinates match exactly.
+ * Null arguments are evaluated as being greater than non-null arguments.
+ */
+ public static final Comparator<Vector1D> COORDINATE_ASCENDING_ORDER = (a, b) -> {
+ int cmp = 0;
+
+ if (a != null && b != null) {
+ cmp = Double.compare(a.getX(), b.getX());
+ } else if (a != null) {
+ cmp = -1;
+ } else if (b != null) {
+ cmp = 1;
+ }
+
+ return cmp;
+ };
+
/** Serializable UID. */
private static final long serialVersionUID = 20180710L;
@@ -54,7 +74,7 @@
/** Simple constructor.
* @param x abscissa (coordinate value)
*/
- private Vector1D(double x) {
+ private Vector1D(final double x) {
this.x = x;
}
@@ -86,19 +106,25 @@
/** {@inheritDoc} */
@Override
- public Vector1D vectorTo(Vector1D v) {
+ public boolean isFinite() {
+ return Double.isFinite(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector1D vectorTo(final Vector1D v) {
return v.subtract(this);
}
/** {@inheritDoc} */
@Override
- public Unit directionTo(Vector1D v) {
+ public Unit directionTo(final Vector1D v) {
return vectorTo(v).normalize();
}
/** {@inheritDoc} */
@Override
- public Vector1D lerp(Vector1D p, double t) {
+ public Vector1D lerp(final Vector1D p, final double t) {
return linearCombination(1.0 - t, this, t, p);
}
@@ -122,32 +148,32 @@
/** {@inheritDoc} */
@Override
- public Vector1D withNorm(double magnitude) {
+ public Vector1D withNorm(final double magnitude) {
getCheckedNorm(); // validate our norm value
- return (x > 0.0)? new Vector1D(magnitude) : new Vector1D(-magnitude);
+ return (x > 0.0) ? new Vector1D(magnitude) : new Vector1D(-magnitude);
}
/** {@inheritDoc} */
@Override
- public Vector1D add(Vector1D v) {
+ public Vector1D add(final Vector1D v) {
return new Vector1D(x + v.x);
}
/** {@inheritDoc} */
@Override
- public Vector1D add(double factor, Vector1D v) {
+ public Vector1D add(final double factor, final Vector1D v) {
return new Vector1D(x + (factor * v.x));
}
/** {@inheritDoc} */
@Override
- public Vector1D subtract(Vector1D v) {
+ public Vector1D subtract(final Vector1D v) {
return new Vector1D(x - v.x);
}
/** {@inheritDoc} */
@Override
- public Vector1D subtract(double factor, Vector1D v) {
+ public Vector1D subtract(final double factor, final Vector1D v) {
return new Vector1D(x - (factor * v.x));
}
@@ -165,25 +191,25 @@
/** {@inheritDoc} */
@Override
- public Vector1D multiply(double a) {
+ public Vector1D multiply(final double a) {
return new Vector1D(a * x);
}
/** {@inheritDoc} */
@Override
- public double distance(Vector1D v) {
+ public double distance(final Vector1D v) {
return Vectors.norm(x - v.x);
}
/** {@inheritDoc} */
@Override
- public double distanceSq(Vector1D v) {
+ public double distanceSq(final Vector1D v) {
return Vectors.normSq(x - v.x);
}
/** {@inheritDoc} */
@Override
- public double dot(Vector1D v) {
+ public double dot(final Vector1D v) {
return x * v.x;
}
@@ -204,19 +230,18 @@
return (sig1 == sig2) ? 0.0 : Geometry.PI;
}
- /** Apply the given transform to this vector, returning the result as a
- * new vector instance.
- * @param transform the transform to apply
- * @return a new, transformed vector
- * @see AffineTransformMatrix1D#apply(Vector1D)
+ /** Convenience method to apply a function to this vector. This
+ * can be used to transform the vector inline with other methods.
+ * @param fn the function to apply
+ * @return the transformed vector
*/
- public Vector1D transform(AffineTransformMatrix1D transform) {
- return transform.apply(this);
+ public Vector1D transform(final Function<Vector1D, Vector1D> fn) {
+ return fn.apply(this);
}
/** {@inheritDoc} */
@Override
- public boolean equals(final Vector1D vec, final DoublePrecisionContext precision) {
+ public boolean eq(final Vector1D vec, final DoublePrecisionContext precision) {
return precision.eq(x, vec.x);
}
@@ -254,7 +279,7 @@
*
*/
@Override
- public boolean equals(Object other) {
+ public boolean equals(final Object other) {
if (this == other) {
return true;
@@ -281,7 +306,7 @@
* @param x vector coordinate
* @return vector instance
*/
- public static Vector1D of(double x) {
+ public static Vector1D of(final double x) {
return new Vector1D(x);
}
@@ -291,7 +316,7 @@
* @return vector instance represented by the string
* @throws IllegalArgumentException if the given string has an invalid format
*/
- public static Vector1D parse(String str) {
+ public static Vector1D parse(final String str) {
return SimpleTupleFormat.getDefault().parse(str, Vector1D::new);
}
@@ -305,7 +330,7 @@
* @param c first coordinate
* @return vector with coordinates calculated by {@code a * c}
*/
- public static Vector1D linearCombination(double a, Vector1D c) {
+ public static Vector1D linearCombination(final double a, final Vector1D c) {
return new Vector1D(a * c.x);
}
@@ -321,7 +346,9 @@
* @param v2 second coordinate
* @return vector with coordinates calculated by {@code (a1 * v1) + (a2 * v2)}
*/
- public static Vector1D linearCombination(double a1, Vector1D v1, double a2, Vector1D v2) {
+ public static Vector1D linearCombination(final double a1, final Vector1D v1,
+ final double a2, final Vector1D v2) {
+
return new Vector1D(
LinearCombination.value(a1, v1.x, a2, v2.x));
}
@@ -340,8 +367,10 @@
* @param v3 third coordinate
* @return vector with coordinates calculated by {@code (a1 * v1) + (a2 * v2) + (a3 * v3)}
*/
- public static Vector1D linearCombination(double a1, Vector1D v1, double a2, Vector1D v2,
- double a3, Vector1D v3) {
+ public static Vector1D linearCombination(final double a1, final Vector1D v1,
+ final double a2, final Vector1D v2,
+ final double a3, final Vector1D v3) {
+
return new Vector1D(
LinearCombination.value(a1, v1.x, a2, v2.x, a3, v3.x));
}
@@ -362,8 +391,11 @@
* @param v4 fourth coordinate
* @return point with coordinates calculated by {@code (a1 * v1) + (a2 * v2) + (a3 * v3) + (a4 * v4)}
*/
- public static Vector1D linearCombination(double a1, Vector1D v1, double a2, Vector1D v2,
- double a3, Vector1D v3, double a4, Vector1D v4) {
+ public static Vector1D linearCombination(final double a1, final Vector1D v1,
+ final double a2, final Vector1D v2,
+ final double a3, final Vector1D v3,
+ final double a4, final Vector1D v4) {
+
return new Vector1D(
LinearCombination.value(a1, v1.x, a2, v2.x, a3, v3.x, a4, v4.x));
}
@@ -373,12 +405,13 @@
* This allows optimizations to be performed for certain operations.
*/
public static final class Unit extends Vector1D {
+
/** Unit vector (coordinates: 1). */
public static final Unit PLUS = new Unit(1d);
/** Negation of unit vector (coordinates: -1). */
public static final Unit MINUS = new Unit(-1d);
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180903L;
/** Simple constructor. Callers are responsible for ensuring that the given
@@ -394,7 +427,8 @@
*
* @param x Vector coordinate.
* @return a vector whose norm is 1.
- * @throws IllegalNormException if the norm of the given value is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given value is
+ * zero, NaN, or infinite
*/
public static Unit from(double x) {
Vectors.checkedNorm(Vectors.norm(x));
@@ -406,7 +440,8 @@
*
* @param v Vector.
* @return a vector whose norm is 1.
- * @throws IllegalNormException if the norm of the given value is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given value is
+ * zero, NaN, or infinite
*/
public static Unit from(Vector1D v) {
return v instanceof Unit ?
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubLine3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubLine3D.java
new file mode 100644
index 0000000..1ab7654
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubLine3D.java
@@ -0,0 +1,62 @@
+/*
+ * 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.threed;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+
+/** Internal base class for 3 dimensional subline implementations.
+ * @param <R> 1D subspace region type
+ */
+abstract class AbstractSubLine3D<R extends Region<Vector1D>> implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190812L;
+
+ /** The line that this instance belongs to. */
+ private final Line3D line;
+
+ /** Construct a new instance belonging to the given line.
+ * @param line line the instance belongs to
+ */
+ protected AbstractSubLine3D(final Line3D line) {
+ this.line = line;
+ }
+
+ /** Get the line that this subline belongs to.
+ * @return the line that this subline belongs to.
+ */
+ public Line3D getLine() {
+ return line;
+ }
+
+ /** Get the precision object used to perform floating point
+ * comparisons for this instance.
+ * @return the precision object for this instance
+ */
+ public DoublePrecisionContext getPrecision() {
+ return line.getPrecision();
+ }
+
+ /** Get the subspace region for the subline.
+ * @return the subspace region for the subline
+ */
+ public abstract R getSubspaceRegion();
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubPlane.java
new file mode 100644
index 0000000..a8ad65e
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubPlane.java
@@ -0,0 +1,147 @@
+/*
+ * 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.threed;
+
+import java.util.function.BiFunction;
+
+import org.apache.commons.geometry.core.partitioning.AbstractEmbeddingSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.SubPlane.SubPlaneBuilder;
+import org.apache.commons.geometry.euclidean.twod.Line;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Internal base class for subplane implementations.
+ * @param <R> Subspace region type
+ */
+abstract class AbstractSubPlane<R extends HyperplaneBoundedRegion<Vector2D>>
+ extends AbstractEmbeddingSubHyperplane<Vector3D, Vector2D, Plane> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190729L;
+
+ /** The plane defining this instance. */
+ private final Plane plane;
+
+ /** Construct a new instance based on the given plane.
+ * @param plane the plane defining the subplane
+ */
+ AbstractSubPlane(final Plane plane) {
+ this.plane = plane;
+ }
+
+ /** Get the plane that this subplane lies on. This method is an alias
+ * for {@link #getHyperplane()}.
+ * @return the plane that this subplane lies on
+ * @see #getHyperplane()
+ */
+ public Plane getPlane() {
+ return getHyperplane();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Plane getHyperplane() {
+ return plane;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubPlaneBuilder builder() {
+ return new SubPlaneBuilder(plane);
+ }
+
+ /** Return the object used to perform floating point comparisons, which is the
+ * same object used by the underlying {@link Plane}).
+ * @return precision object used to perform floating point comparisons.
+ */
+ public DoublePrecisionContext getPrecision() {
+ return plane.getPrecision();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName())
+ .append("[plane= ")
+ .append(getPlane())
+ .append(", subspaceRegion= ")
+ .append(getSubspaceRegion());
+
+
+ return sb.toString();
+ }
+
+ /** Generic, internal split method. Subclasses should call this from their
+ * {@link #split(Hyperplane)} methods.
+ * @param splitter splitting hyperplane
+ * @param thisInstance a reference to the current instance; this is passed as
+ * an argument in order to allow it to be a generic type
+ * @param factory function used to create new subhyperplane instances
+ * @param <T> Subplane implementation type
+ * @return the result of the split operation
+ */
+ protected <T extends AbstractSubPlane<R>> Split<T> splitInternal(final Hyperplane<Vector3D> splitter,
+ final T thisInstance, final BiFunction<Plane, HyperplaneBoundedRegion<Vector2D>, T> factory) {
+
+ final Plane thisPlane = thisInstance.getPlane();
+ final Plane splitterPlane = (Plane) splitter;
+ final DoublePrecisionContext precision = thisInstance.getPrecision();
+
+ final Line3D intersection = thisPlane.intersection(splitterPlane);
+ if (intersection == null) {
+ // the planes are parallel or coincident; check which side of
+ // the splitter we lie on
+ final double offset = splitterPlane.offset(thisPlane);
+ final int comp = precision.compare(offset, 0.0);
+
+ if (comp < 0) {
+ return new Split<>(thisInstance, null);
+ } else if (comp > 0) {
+ return new Split<>(null, thisInstance);
+ } else {
+ return new Split<>(null, null);
+ }
+ } else {
+ // the lines intersect; split the subregion
+ final Vector3D intersectionOrigin = intersection.getOrigin();
+ final Vector2D subspaceP1 = thisPlane.toSubspace(intersectionOrigin);
+ final Vector2D subspaceP2 = thisPlane.toSubspace(intersectionOrigin.add(intersection.getDirection()));
+
+ final Line subspaceSplitter = Line.fromPoints(subspaceP1, subspaceP2, getPrecision());
+
+ final Split<? extends HyperplaneBoundedRegion<Vector2D>> split =
+ thisInstance.getSubspaceRegion().split(subspaceSplitter);
+ final SplitLocation subspaceSplitLoc = split.getLocation();
+
+ if (SplitLocation.MINUS == subspaceSplitLoc) {
+ return new Split<>(thisInstance, null);
+ } else if (SplitLocation.PLUS == subspaceSplitLoc) {
+ return new Split<>(null, thisInstance);
+ }
+
+ final T minus = (split.getMinus() != null) ? factory.apply(getPlane(), split.getMinus()) : null;
+ final T plus = (split.getPlus() != null) ? factory.apply(getPlane(), split.getPlus()) : null;
+
+ return new Split<>(minus, plus);
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3D.java
index 92d2b2a..e88f2c3 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3D.java
@@ -19,12 +19,11 @@
import java.io.Serializable;
import org.apache.commons.geometry.core.internal.DoubleFunction3N;
-import org.apache.commons.geometry.euclidean.AffineTransformMatrix;
+import org.apache.commons.geometry.euclidean.AbstractAffineTransformMatrix;
import org.apache.commons.geometry.euclidean.exception.NonInvertibleTransformException;
import org.apache.commons.geometry.euclidean.internal.Matrices;
import org.apache.commons.geometry.euclidean.internal.Vectors;
import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.arrays.LinearCombination;
import org.apache.commons.numbers.core.Precision;
@@ -36,24 +35,25 @@
* use arrays containing 12 elements, instead of 16.
* </p>
*/
-public final class AffineTransformMatrix3D implements AffineTransformMatrix<Vector3D, Vector2D>, Serializable {
+public final class AffineTransformMatrix3D extends AbstractAffineTransformMatrix<Vector3D>
+ implements Transform3D, Serializable {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180923L;
- /** The number of internal matrix elements */
+ /** The number of internal matrix elements. */
private static final int NUM_ELEMENTS = 12;
- /** String used to start the transform matrix string representation */
+ /** String used to start the transform matrix string representation. */
private static final String MATRIX_START = "[ ";
- /** String used to end the transform matrix string representation */
+ /** String used to end the transform matrix string representation. */
private static final String MATRIX_END = " ]";
- /** String used to separate elements in the matrix string representation */
+ /** String used to separate elements in the matrix string representation. */
private static final String ELEMENT_SEPARATOR = ", ";
- /** String used to separate rows in the matrix string representation */
+ /** String used to separate rows in the matrix string representation. */
private static final String ROW_SEPARATOR = "; ";
/** Shared transform set to the identity matrix. */
@@ -63,31 +63,31 @@
0, 0, 1, 0
);
- /** Transform matrix entry <code>m<sub>0,0</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,0</sub></code>. */
private final double m00;
- /** Transform matrix entry <code>m<sub>0,1</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,1</sub></code>. */
private final double m01;
- /** Transform matrix entry <code>m<sub>0,2</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,2</sub></code>. */
private final double m02;
- /** Transform matrix entry <code>m<sub>0,3</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,3</sub></code>. */
private final double m03;
- /** Transform matrix entry <code>m<sub>1,0</sub></code> */
+ /** Transform matrix entry <code>m<sub>1,0</sub></code>. */
private final double m10;
- /** Transform matrix entry <code>m<sub>1,1</sub></code> */
+ /** Transform matrix entry <code>m<sub>1,1</sub></code>. */
private final double m11;
- /** Transform matrix entry <code>m<sub>1,2</sub></code> */
+ /** Transform matrix entry <code>m<sub>1,2</sub></code>. */
private final double m12;
- /** Transform matrix entry <code>m<sub>1,3</sub></code> */
+ /** Transform matrix entry <code>m<sub>1,3</sub></code>. */
private final double m13;
- /** Transform matrix entry <code>m<sub>2,0</sub></code> */
+ /** Transform matrix entry <code>m<sub>2,0</sub></code>. */
private final double m20;
- /** Transform matrix entry <code>m<sub>2,1</sub></code> */
+ /** Transform matrix entry <code>m<sub>2,1</sub></code>. */
private final double m21;
- /** Transform matrix entry <code>m<sub>2,2</sub></code> */
+ /** Transform matrix entry <code>m<sub>2,2</sub></code>. */
private final double m22;
- /** Transform matrix entry <code>m<sub>2,3</sub></code> */
+ /** Transform matrix entry <code>m<sub>2,3</sub></code>. */
private final double m23;
/**
@@ -142,9 +142,9 @@
*/
public double[] toArray() {
return new double[] {
- m00, m01, m02, m03,
- m10, m11, m12, m13,
- m20, m21, m22, m23
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23
};
}
@@ -198,10 +198,29 @@
* @see #applyVector(Vector3D)
*/
@Override
- public Vector3D applyDirection(final Vector3D vec) {
+ public Vector3D.Unit applyDirection(final Vector3D vec) {
return applyVector(vec, Vector3D.Unit::from);
}
+ /** {@inheritDoc} */
+ @Override
+ public double determinant() {
+ return Matrices.determinant(
+ m00, m01, m02,
+ m10, m11, m12,
+ m20, m21, m22
+ );
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This simply returns the current instance.</p>
+ */
+ @Override
+ public AffineTransformMatrix3D toMatrix() {
+ return this;
+ }
+
/** Apply a translation to the current instance, returning the result as a new transform.
* @param translation vector containing the translation values for each axis
* @return a new transform containing the result of applying a translation to
@@ -263,10 +282,10 @@
* @param rotation the rotation to apply
* @return a new transform containing the result of applying a rotation to the
* current instance
- * @see QuaternionRotation#toTransformMatrix()
+ * @see QuaternionRotation#toMatrix()
*/
public AffineTransformMatrix3D rotate(final QuaternionRotation rotation) {
- return multiply(rotation.toTransformMatrix(), this);
+ return multiply(rotation.toMatrix(), this);
}
/** Apply a rotation around the given center point to the current instance, returning the result
@@ -276,7 +295,7 @@
* @param rotation the rotation to apply
* @return a new transform containing the result of applying a rotation about the given center
* point to the current instance
- * @see QuaternionRotation#toTransformMatrix()
+ * @see QuaternionRotation#toMatrix()
*/
public AffineTransformMatrix3D rotate(final Vector3D center, final QuaternionRotation rotation) {
return multiply(createRotation(center, rotation), this);
@@ -321,12 +340,7 @@
// Our full matrix is 4x4 but we can significantly reduce the amount of computations
// needed here since we know that our last row is [0 0 0 1].
- // compute the determinant of the matrix
- final double det = Matrices.determinant(
- m00, m01, m02,
- m10, m11, m12,
- m20, m21, m22
- );
+ final double det = determinant();
if (!Vectors.isRealNonZero(det)) {
throw new NonInvertibleTransformException("Transform is not invertible; matrix determinant is " + det);
@@ -343,18 +357,18 @@
final double invDet = 1.0 / det;
final double c00 = invDet * Matrices.determinant(m11, m12, m21, m22);
- final double c01 = - invDet * Matrices.determinant(m10, m12, m20, m22);
+ final double c01 = -invDet * Matrices.determinant(m10, m12, m20, m22);
final double c02 = invDet * Matrices.determinant(m10, m11, m20, m21);
- final double c10 = - invDet * Matrices.determinant(m01, m02, m21, m22);
+ final double c10 = -invDet * Matrices.determinant(m01, m02, m21, m22);
final double c11 = invDet * Matrices.determinant(m00, m02, m20, m22);
- final double c12 = - invDet * Matrices.determinant(m00, m01, m20, m21);
+ final double c12 = -invDet * Matrices.determinant(m00, m01, m20, m21);
final double c20 = invDet * Matrices.determinant(m01, m02, m11, m12);
- final double c21 = - invDet * Matrices.determinant(m00, m02, m10, m12);
+ final double c21 = -invDet * Matrices.determinant(m00, m02, m10, m12);
final double c22 = invDet * Matrices.determinant(m00, m01, m10, m11);
- final double c30 = - invDet * Matrices.determinant(
+ final double c30 = -invDet * Matrices.determinant(
m01, m02, m03,
m11, m12, m13,
m21, m22, m23
@@ -364,7 +378,7 @@
m10, m12, m13,
m20, m22, m23
);
- final double c32 = - invDet * Matrices.determinant(
+ final double c32 = -invDet * Matrices.determinant(
m00, m01, m03,
m10, m11, m13,
m20, m21, m23
@@ -383,9 +397,12 @@
final int prime = 31;
int result = 1;
- result = (result * prime) + (Double.hashCode(m00) - Double.hashCode(m01) + Double.hashCode(m02) - Double.hashCode(m03));
- result = (result * prime) + (Double.hashCode(m10) - Double.hashCode(m11) + Double.hashCode(m12) - Double.hashCode(m13));
- result = (result * prime) + (Double.hashCode(m20) - Double.hashCode(m21) + Double.hashCode(m22) - Double.hashCode(m23));
+ result = (result * prime) + (Double.hashCode(m00) - Double.hashCode(m01) +
+ Double.hashCode(m02) - Double.hashCode(m03));
+ result = (result * prime) + (Double.hashCode(m10) - Double.hashCode(m11) +
+ Double.hashCode(m12) - Double.hashCode(m13));
+ result = (result * prime) + (Double.hashCode(m20) - Double.hashCode(m21) +
+ Double.hashCode(m22) - Double.hashCode(m23));
return result;
}
@@ -397,7 +414,7 @@
* @return true if all transform matrix elements are exactly equal; otherwise false
*/
@Override
- public boolean equals(Object obj) {
+ public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
@@ -488,7 +505,7 @@
* @return a new transform initialized with the given matrix values
* @throws IllegalArgumentException if the array does not have 12 elements
*/
- public static AffineTransformMatrix3D of(final double ... arr) {
+ public static AffineTransformMatrix3D of(final double... arr) {
if (arr.length != NUM_ELEMENTS) {
throw new IllegalArgumentException("Dimension mismatch: " + arr.length + " != " + NUM_ELEMENTS);
}
@@ -500,6 +517,40 @@
);
}
+ /** Get a new transform create from the given column vectors. The returned transform
+ * does not include any translation component.
+ * @param u first column vector; this corresponds to the first basis vector
+ * in the coordinate frame
+ * @param v second column vector; this corresponds to the second basis vector
+ * in the coordinate frame
+ * @param w third column vector; this corresponds to the third basis vector
+ * in the coordinate frame
+ * @return a new transform with the given column vectors
+ */
+ public static AffineTransformMatrix3D fromColumnVectors(final Vector3D u, final Vector3D v, final Vector3D w) {
+ return fromColumnVectors(u, v, w, Vector3D.ZERO);
+ }
+
+ /** Get a new transform created from the given column vectors.
+ * @param u first column vector; this corresponds to the first basis vector
+ * in the coordinate frame
+ * @param v second column vector; this corresponds to the second basis vector
+ * in the coordinate frame
+ * @param w third column vector; this corresponds to the third basis vector
+ * in the coordinate frame
+ * @param t fourth column vector; this corresponds to the translation of the transform
+ * @return a new transform with the given column vectors
+ */
+ public static AffineTransformMatrix3D fromColumnVectors(final Vector3D u, final Vector3D v,
+ final Vector3D w, final Vector3D t) {
+
+ return new AffineTransformMatrix3D(
+ u.getX(), v.getX(), w.getX(), t.getX(),
+ u.getY(), v.getY(), w.getY(), t.getY(),
+ u.getZ(), v.getZ(), w.getZ(), t.getZ()
+ );
+ }
+
/** Get the transform representing the identity matrix. This transform does not
* modify point or vector values when applied.
* @return transform representing the identity matrix
@@ -565,7 +616,7 @@
* @param center the center of rotation
* @param rotation the rotation to apply
* @return a new transform representing a rotation about the given center point
- * @see QuaternionRotation#toTransformMatrix()
+ * @see QuaternionRotation#toMatrix()
*/
public static AffineTransformMatrix3D createRotation(final Vector3D center, final QuaternionRotation rotation) {
return createTranslation(center.negate())
@@ -578,7 +629,8 @@
* @param b second transform
* @return the transform computed as {@code a x b}
*/
- private static AffineTransformMatrix3D multiply(final AffineTransformMatrix3D a, final AffineTransformMatrix3D b) {
+ private static AffineTransformMatrix3D multiply(final AffineTransformMatrix3D a,
+ final AffineTransformMatrix3D b) {
// calculate the matrix elements
final double c00 = LinearCombination.value(a.m00, b.m00, a.m01, b.m10, a.m02, b.m20);
@@ -593,8 +645,8 @@
final double c20 = LinearCombination.value(a.m20, b.m00, a.m21, b.m10, a.m22, b.m20);
final double c21 = LinearCombination.value(a.m20, b.m01, a.m21, b.m11, a.m22, b.m21);
- final double c22 = LinearCombination.value(a.m20, b.m02, a.m21 , b.m12, a.m22, b.m22);
- final double c23 = LinearCombination.value(a.m20, b.m03, a.m21 , b.m13, a.m22, b.m23) + a.m23;
+ final double c22 = LinearCombination.value(a.m20, b.m02, a.m21, b.m12, a.m22, b.m22);
+ final double c23 = LinearCombination.value(a.m20, b.m03, a.m21, b.m13, a.m22, b.m23) + a.m23;
return new AffineTransformMatrix3D(
c00, c01, c02, c03,
@@ -611,7 +663,8 @@
*/
private static void validateElementForInverse(final double element) {
if (!Double.isFinite(element)) {
- throw new NonInvertibleTransformException("Transform is not invertible; invalid matrix element: " + element);
+ throw new NonInvertibleTransformException("Transform is not invertible; invalid matrix element: " +
+ element);
}
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlane.java
new file mode 100644
index 0000000..085bf79
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlane.java
@@ -0,0 +1,172 @@
+/*
+ * 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.threed;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Plane.SubspaceTransform;
+import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Class representing a convex subhyperplane in 3 dimensional Euclidean space, meaning
+ * a 2D convex area embedded in a plane.
+ */
+public final class ConvexSubPlane extends AbstractSubPlane<ConvexArea>
+ implements ConvexSubHyperplane<Vector3D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190729L;
+
+ /** The embedded 2D area. */
+ private final ConvexArea area;
+
+ /** Create a new instance from its component parts.
+ * @param plane plane the the convex area is embedded in
+ * @param area the embedded convex area
+ */
+ private ConvexSubPlane(final Plane plane, final ConvexArea area) {
+ super(plane);
+
+ this.area = area;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<ConvexSubPlane> toConvex() {
+ return Arrays.asList(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConvexSubPlane reverse() {
+ final Plane plane = getPlane();
+ final Plane rPlane = plane.reverse();
+
+ final Vector2D rU = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_X));
+ final Vector2D rV = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_Y));
+
+ final AffineTransformMatrix2D transform =
+ AffineTransformMatrix2D.fromColumnVectors(rU, rV);
+
+ return new ConvexSubPlane(rPlane, area.transform(transform));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConvexSubPlane transform(final Transform<Vector3D> transform) {
+ final SubspaceTransform st = getPlane().subspaceTransform(transform);
+ final ConvexArea tArea = area.transform(st.getTransform());
+
+ return fromConvexArea(st.getPlane(), tArea);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConvexArea getSubspaceRegion() {
+ return area;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<ConvexSubPlane> split(Hyperplane<Vector3D> splitter) {
+ return splitInternal(splitter, this, (p, r) -> new ConvexSubPlane(p, (ConvexArea) r));
+ }
+
+ /** Get the vertices for the subplane. The vertices lie at the intersections of the
+ * 2D area bounding lines.
+ * @return the vertices for the subplane
+ */
+ public List<Vector3D> getVertices() {
+ return getPlane().toSpace(area.getVertices());
+ }
+
+ /** Create a new instance from a plane and an embedded convex subspace area.
+ * @param plane embedding plane for the area
+ * @param area area embedded in the plane
+ * @return a new convex sub plane instance
+ */
+ public static ConvexSubPlane fromConvexArea(final Plane plane, final ConvexArea area) {
+ return new ConvexSubPlane(plane, area);
+ }
+
+ /** Create a new instance from the given sequence of points. The points must define a unique plane, meaning that
+ * at least 3 unique vertices must be given. In contrast with the
+ * {@link #fromVertices(Collection, DoublePrecisionContext)} method, the first point in the sequence is included
+ * at the end if needed, in order to form a closed loop.
+ * @param pts collection of points defining the convex subplane
+ * @param precision precision context used to compare floating point values
+ * @return a new instance defined by the given sequence of vertices
+ * @throws IllegalArgumentException if fewer than 3 vertices are given
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if the vertices do not define a
+ * unique plane
+ * @see #fromVertices(Collection, DoublePrecisionContext)
+ * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+ * @see Plane#fromPoints(Collection, DoublePrecisionContext)
+ */
+ public static ConvexSubPlane fromVertexLoop(final Collection<Vector3D> pts,
+ final DoublePrecisionContext precision) {
+ return fromVertices(pts, true, precision);
+ }
+
+ /** Create a new instance from the given sequence of points. The points must define a unique plane, meaning that
+ * at least 3 unique vertices must be given.
+ * @param pts collection of points defining the convex subplane
+ * @param precision precision context used to compare floating point values
+ * @return a new instance defined by the given sequence of vertices
+ * @throws IllegalArgumentException if fewer than 3 vertices are given
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if the vertices do not define a
+ * unique plane
+ * @see #fromVertexLoop(Collection, DoublePrecisionContext)
+ * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+ * @see Plane#fromPoints(Collection, DoublePrecisionContext)
+ */
+ public static ConvexSubPlane fromVertices(final Collection<Vector3D> pts,
+ final DoublePrecisionContext precision) {
+ return fromVertices(pts, false, precision);
+ }
+
+ /** Create a new instance from the given sequence of points. The points must define a unique plane, meaning that
+ * at least 3 unique vertices must be given. If {@code close} is true, the vertices are made into a closed loop
+ * by including the start point at the end if needed.
+ * @param pts collection of points
+ * @param close if true, the point sequence will implicitly include the start point again at the end; otherwise
+ * the vertex sequence is taken as-is
+ * @param precision precision context used to compare floating point values
+ * @return a new convex subplane instance
+ * @see #fromVertexLoop(Collection, DoublePrecisionContext)
+ * @see #fromVertices(Collection, DoublePrecisionContext)
+ * @see Plane#fromPoints(Collection, DoublePrecisionContext)
+ */
+ public static ConvexSubPlane fromVertices(final Collection<Vector3D> pts, final boolean close,
+ final DoublePrecisionContext precision) {
+
+ final Plane plane = Plane.fromPoints(pts, precision);
+
+ final List<Vector2D> subspacePts = plane.toSubspace(pts);
+ final ConvexArea area = ConvexArea.fromVertices(subspacePts, close, precision);
+
+ return new ConvexSubPlane(plane, area);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
new file mode 100644
index 0000000..e772060
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
@@ -0,0 +1,188 @@
+/*
+ * 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.threed;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+
+/** Class representing a finite or infinite convex volume in Euclidean 3D space.
+ * The boundaries of this area, if any, are composed of convex subplanes.
+ */
+public final class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D, ConvexSubPlane> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190811L;
+
+ /** Instance representing the full 3D volume. */
+ private static final ConvexVolume FULL = new ConvexVolume(Collections.emptyList());
+
+ /** Simple constructor. Callers are responsible for ensuring that the given path
+ * represents the boundary of a convex area. No validation is performed.
+ * @param boundaries the boundaries of the convex area
+ */
+ private ConvexVolume(final List<ConvexSubPlane> boundaries) {
+ super(boundaries);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ if (isFull()) {
+ return Double.POSITIVE_INFINITY;
+ }
+
+ double volumeSum = 0.0;
+
+ for (ConvexSubPlane subplane : getBoundaries()) {
+ if (subplane.isInfinite()) {
+ return Double.POSITIVE_INFINITY;
+ }
+
+ final Plane plane = subplane.getPlane();
+ final ConvexArea subarea = subplane.getSubspaceRegion();
+
+ final Vector3D facetBarycenter = subplane.getHyperplane().toSpace(
+ subarea.getBarycenter());
+
+
+ volumeSum += subarea.getSize() * facetBarycenter.dot(plane.getNormal());
+ }
+
+ return volumeSum / 3.0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getBarycenter() {
+ if (isFull()) {
+ return null;
+ }
+
+ double volumeSum = 0.0;
+
+ double sumX = 0.0;
+ double sumY = 0.0;
+ double sumZ = 0.0;
+
+ for (ConvexSubPlane subplane : getBoundaries()) {
+ if (subplane.isInfinite()) {
+ return null;
+ }
+
+ final Plane plane = subplane.getPlane();
+ final ConvexArea subarea = subplane.getSubspaceRegion();
+
+ final Vector3D facetBarycenter = subplane.getHyperplane().toSpace(
+ subarea.getBarycenter());
+
+ double scaledVolume = subarea.getSize() * facetBarycenter.dot(plane.getNormal());
+
+ volumeSum += scaledVolume;
+
+ sumX += scaledVolume * facetBarycenter.getX();
+ sumY += scaledVolume * facetBarycenter.getY();
+ sumZ += scaledVolume * facetBarycenter.getZ();
+ }
+
+ double size = volumeSum / 3.0;
+
+ // Since the volume we used when adding together the facet contributions
+ // was 3x the actual pyramid size, we'll multiply by 1/4 here instead
+ // of 3/4 to adjust for the actual barycenter position in each pyramid.
+ final double barycenterScale = 1.0 / (4 * size);
+ return Vector3D.of(
+ sumX * barycenterScale,
+ sumY * barycenterScale,
+ sumZ * barycenterScale);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<ConvexVolume> split(final Hyperplane<Vector3D> splitter) {
+ return splitInternal(splitter, this, ConvexSubPlane.class, ConvexVolume::new);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConvexSubPlane trim(final ConvexSubHyperplane<Vector3D> convexSubHyperplane) {
+ return (ConvexSubPlane) super.trim(convexSubHyperplane);
+ }
+
+ /** Return a new instance transformed by the argument.
+ * @param transform transform to apply
+ * @return a new instance transformed by the argument
+ */
+ public ConvexVolume transform(final Transform<Vector3D> transform) {
+ return transformInternal(transform, this, ConvexSubPlane.class, ConvexVolume::new);
+ }
+
+ /** Return a BSP tree instance representing the same region as the current instance.
+ * @return a BSP tree instance representing the same region as the current instance
+ */
+ public RegionBSPTree3D toTree() {
+ return RegionBSPTree3D.from(this);
+ }
+
+ /** Return an instance representing the full 3D volume.
+ * @return an instance representing the full 3D volume.
+ */
+ public static ConvexVolume full() {
+ return FULL;
+ }
+
+ /** Create a convex volume formed by the intersection of the negative half-spaces of the
+ * given bounding planes. The returned instance represents the volume that is on the
+ * minus side of all of the given plane. Note that this method does not support volumes
+ * of zero size (ie, infinitely thin volumes or points.)
+ * @param planes planes used to define the convex area
+ * @return a new convex volume instance representing the volume on the minus side of all
+ * of the bounding plane or an instance representing the full space if the collection
+ * is empty
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if the given set of bounding
+ * planes do not form a convex volume, meaning that there is no region that is on the minus side
+ * of all of the bounding planes.
+ */
+ public static ConvexVolume fromBounds(final Plane... planes) {
+ return fromBounds(Arrays.asList(planes));
+ }
+
+ /** Create a convex volume formed by the intersection of the negative half-spaces of the
+ * given bounding planes. The returned instance represents the volume that is on the
+ * minus side of all of the given plane. Note that this method does not support volumes
+ * of zero size (ie, infinitely thin volumes or points.)
+ * @param boundingPlanes planes used to define the convex area
+ * @return a new convex volume instance representing the volume on the minus side of all
+ * of the bounding plane or an instance representing the full space if the collection
+ * is empty
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if the given set of bounding planes
+ * do not form a convex volume, meaning that there is no region that is on the minus side of all of
+ * the bounding planes.
+ */
+ public static ConvexVolume fromBounds(final Iterable<Plane> boundingPlanes) {
+ final List<ConvexSubPlane> subplanes = new ConvexRegionBoundaryBuilder<>(ConvexSubPlane.class)
+ .build(boundingPlanes);
+ return subplanes.isEmpty() ? full() : new ConvexVolume(subplanes);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/FunctionTransform3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/FunctionTransform3D.java
new file mode 100644
index 0000000..15dca12
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/FunctionTransform3D.java
@@ -0,0 +1,107 @@
+/*
+ * 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.threed;
+
+import java.util.function.Function;
+
+import org.apache.commons.geometry.euclidean.internal.Matrices;
+
+/** Class that wraps a {@link Function} with the {@link Transform3D} interface.
+ */
+public final class FunctionTransform3D implements Transform3D {
+
+ /** Static instance representing the identity transform. */
+ private static final FunctionTransform3D IDENTITY =
+ new FunctionTransform3D(Function.identity(), true, Vector3D.ZERO);
+
+ /** The underlying function for the transform. */
+ private final Function<Vector3D, Vector3D> fn;
+
+ /** True if the transform preserves spatial orientation. */
+ private final boolean preservesOrientation;
+
+ /** The translation component of the transform. */
+ private final Vector3D translation;
+
+ /** Construct a new instance from its component parts. No validation of the input is performed.
+ * @param fn the underlying function for the transform
+ * @param preservesOrientation true if the transform preserves spatial orientation
+ * @param translation the translation component of the transform
+ */
+ private FunctionTransform3D(final Function<Vector3D, Vector3D> fn, final boolean preservesOrientation,
+ final Vector3D translation) {
+ this.fn = fn;
+ this.preservesOrientation = preservesOrientation;
+ this.translation = translation;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D apply(final Vector3D pt) {
+ return fn.apply(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D applyVector(final Vector3D vec) {
+ return apply(vec).subtract(translation);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean preservesOrientation() {
+ return preservesOrientation;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AffineTransformMatrix3D toMatrix() {
+ final Vector3D u = applyVector(Vector3D.Unit.PLUS_X);
+ final Vector3D v = applyVector(Vector3D.Unit.PLUS_Y);
+ final Vector3D w = applyVector(Vector3D.Unit.PLUS_Z);
+
+ return AffineTransformMatrix3D.fromColumnVectors(u, v, w, translation);
+ }
+
+ /** Return an instance representing the identity transform.
+ * @return an instance representing the identity transform
+ */
+ public static FunctionTransform3D identity() {
+ return IDENTITY;
+ }
+
+ /** Construct a new transform instance from the given function.
+ * @param fn the function to use for the transform
+ * @return a new transform instance using the given function
+ */
+ public static FunctionTransform3D from(final Function<Vector3D, Vector3D> fn) {
+ final Vector3D tPlusX = fn.apply(Vector3D.Unit.PLUS_X);
+ final Vector3D tPlusY = fn.apply(Vector3D.Unit.PLUS_Y);
+ final Vector3D tPlusZ = fn.apply(Vector3D.Unit.PLUS_Z);
+
+ final Vector3D tZero = fn.apply(Vector3D.ZERO);
+
+ final double det = Matrices.determinant(
+ tPlusX.getX(), tPlusY.getX(), tPlusZ.getX(),
+ tPlusX.getY(), tPlusY.getY(), tPlusZ.getY(),
+ tPlusX.getZ(), tPlusY.getZ(), tPlusZ.getZ()
+ );
+ final boolean preservesOrientation = det > 0;
+
+ return new FunctionTransform3D(fn, preservesOrientation, tZero);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line.java
deleted file mode 100644
index 9f859e7..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * 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.threed;
-
-import java.util.Objects;
-
-import org.apache.commons.geometry.core.partitioning.Embedding;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
-import org.apache.commons.numbers.core.Precision;
-
-/** The class represent lines in a three dimensional space.
-
- * <p>Each oriented line is intrinsically associated with an abscissa
- * which is a coordinate on the line. The point at abscissa 0 is the
- * orthogonal projection of the origin on the line, another equivalent
- * way to express this is to say that it is the point of the line
- * which is closest to the origin. Abscissa increases in the line
- * direction.</p>0
- */
-public class Line implements Embedding<Vector3D, Vector1D> {
-
- /** Line direction. */
- private Vector3D direction;
-
- /** Line point closest to the origin. */
- private Vector3D zero;
-
- /** Precision context used to compare floating point numbers. */
- private final DoublePrecisionContext precision;
-
- /** Build a line from two points.
- * @param p1 first point belonging to the line (this can be any point)
- * @param p2 second point belonging to the line (this can be any point, different from p1)
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if the points are equal
- */
- public Line(final Vector3D p1, final Vector3D p2, final DoublePrecisionContext precision)
- throws IllegalArgumentException {
- reset(p1, p2);
- this.precision = precision;
- }
-
- /** Copy constructor.
- * <p>The created instance is completely independent from the
- * original instance, it is a deep copy.</p>
- * @param line line to copy
- */
- public Line(final Line line) {
- this.direction = line.direction;
- this.zero = line.zero;
- this.precision = line.precision;
- }
-
- /** Reset the instance as if built from two points.
- * @param p1 first point belonging to the line (this can be any point)
- * @param p2 second point belonging to the line (this can be any point, different from p1)
- * @exception IllegalArgumentException if the points are equal
- */
- public void reset(final Vector3D p1, final Vector3D p2) {
- final Vector3D delta = p2.subtract(p1);
- final double norm2 = delta.normSq();
- if (norm2 == 0.0) {
- throw new IllegalArgumentException("Points are equal");
- }
- this.direction = Vector3D.linearCombination(1.0 / Math.sqrt(norm2), delta);
- this.zero = Vector3D.linearCombination(1.0, p1, -p1.dot(delta) / norm2, delta);
- }
-
- /** Get the object used to determine floating point equality for this instance.
- * @return the floating point precision context for the instance
- */
- public DoublePrecisionContext getPrecision() {
- return precision;
- }
-
- /** Get a line with reversed direction.
- * @return a new instance, with reversed direction
- */
- public Line revert() {
- final Line reverted = new Line(this);
- reverted.direction = reverted.direction.negate();
- return reverted;
- }
-
- /** Get the normalized direction vector.
- * @return normalized direction vector
- */
- public Vector3D getDirection() {
- return direction;
- }
-
- /** Get the line point closest to the origin.
- * @return line point closest to the origin
- */
- public Vector3D getOrigin() {
- return zero;
- }
-
- /** Get the abscissa of a point with respect to the line.
- * <p>The abscissa is 0 if the projection of the point and the
- * projection of the frame origin on the line are the same
- * point.</p>
- * @param point point to check
- * @return abscissa of the point
- */
- public double getAbscissa(final Vector3D point) {
- return point.subtract(zero).dot(direction);
- }
-
- /** Get one point from the line.
- * @param abscissa desired abscissa for the point
- * @return one point belonging to the line, at specified abscissa
- */
- public Vector3D pointAt(final double abscissa) {
- return Vector3D.linearCombination(1.0, zero, abscissa, direction);
- }
-
- /** Transform a space point into a sub-space point.
- * @param point n-dimension point of the space
- * @return (n-1)-dimension point of the sub-space corresponding to
- * the specified space point
- */
- @Override
- public Vector1D toSubSpace(final Vector3D point) {
- return Vector1D.of(getAbscissa(point));
- }
-
- /** Transform a sub-space point into a space point.
- * @param point (n-1)-dimension point of the sub-space
- * @return n-dimension point of the space corresponding to the
- * specified sub-space point
- */
- @Override
- public Vector3D toSpace(final Vector1D point) {
- return pointAt(point.getX());
- }
-
- /** Check if the instance is similar to another line.
- * <p>Lines are considered similar if they contain the same
- * points. This does not mean they are equal since they can have
- * opposite directions.</p>
- * @param line line to which instance should be compared
- * @return true if the lines are similar
- */
- public boolean isSimilarTo(final Line line) {
- final double angle = direction.angle(line.direction);
- return (precision.eqZero(angle) || precision.eq(angle, Math.PI)) && contains(line.zero);
- }
-
- /** Check if the instance contains a point.
- * @param p point to check
- * @return true if p belongs to the line
- */
- public boolean contains(final Vector3D p) {
- return precision.eqZero(distance(p));
- }
-
- /** Compute the distance between the instance and a point.
- * @param p to check
- * @return distance between the instance and the point
- */
- public double distance(final Vector3D p) {
- final Vector3D d = p.subtract(zero);
- final Vector3D n = Vector3D.linearCombination(1.0, d, -d.dot(direction), direction);
- return n.norm();
- }
-
- /** Compute the shortest distance between the instance and another line.
- * @param line line to check against the instance
- * @return shortest distance between the instance and the line
- */
- public double distance(final Line line) {
-
- final Vector3D normal = direction.cross(line.direction);
- final double n = normal.norm();
- if (n < Precision.SAFE_MIN) {
- // lines are parallel
- return distance(line.zero);
- }
-
- // signed separation of the two parallel planes that contains the lines
- final double offset = line.zero.subtract(zero).dot(normal) / n;
-
- return Math.abs(offset);
-
- }
-
- /** Compute the point of the instance closest to another line.
- * @param line line to check against the instance
- * @return point of the instance closest to another line
- */
- public Vector3D closestPoint(final Line line) {
-
- final double cos = direction.dot(line.direction);
- final double n = 1 - cos * cos;
- if (n < Precision.EPSILON) {
- // the lines are parallel
- return zero;
- }
-
- final Vector3D delta0 = line.zero.subtract(zero);
- final double a = delta0.dot(direction);
- final double b = delta0.dot(line.direction);
-
- return Vector3D.linearCombination(1, zero, (a - b * cos) / n, direction);
-
- }
-
- /** Get the intersection point of the instance and another line.
- * @param line other line
- * @return intersection point of the instance and the other line
- * or null if there are no intersection points
- */
- public Vector3D intersection(final Line line) {
- final Vector3D closest = closestPoint(line);
- return line.contains(closest) ? closest : null;
- }
-
- /** Build a sub-line covering the whole line.
- * @return a sub-line covering the whole line
- */
- public SubLine wholeLine() {
- return new SubLine(this, new IntervalsSet(precision));
- }
-
- /** {@inheritDoc} */
- @Override
- public int hashCode() {
- return Objects.hash(direction, precision, zero);
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- Line other = (Line) obj;
- return this.direction.equals(other.direction, precision) && this.zero.equals(other.zero, precision);
- }
-
- /** {@inheritDoc} */
- @Override
- public String toString() {
- return "Line [direction=" + direction + ", zero=" + zero + "]";
- }
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
new file mode 100644
index 0000000..883b63e
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
@@ -0,0 +1,432 @@
+/*
+ * 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.threed;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.Embedding;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+
+/** Class representing a line in 3D space.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Line3D implements Embedding<Vector3D, Vector1D>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190704L;
+
+ /** Line point closest to the origin. */
+ private final Vector3D origin;
+
+ /** Line direction. */
+ private final Vector3D direction;
+
+ /** Precision context used to compare floating point numbers. */
+ private final DoublePrecisionContext precision;
+
+ /** Simple constructor.
+ * @param origin the origin of the line, meaning the point on the line closest to the origin of the
+ * 3D space
+ * @param direction the direction of the line
+ * @param precision precision context used to compare floating point numbers
+ */
+ private Line3D(final Vector3D origin, final Vector3D direction, final DoublePrecisionContext precision) {
+ this.origin = origin;
+ this.direction = direction;
+ this.precision = precision;
+ }
+
+ /** Get the line point closest to the origin.
+ * @return line point closest to the origin
+ */
+ public Vector3D getOrigin() {
+ return origin;
+ }
+
+ /** Get the normalized direction vector.
+ * @return normalized direction vector
+ */
+ public Vector3D getDirection() {
+ return direction;
+ }
+
+ /** Get the object used to determine floating point equality for this instance.
+ * @return the floating point precision context for the instance
+ */
+ public DoublePrecisionContext getPrecision() {
+ return precision;
+ }
+
+ /** Return a line containing the same points as this instance but pointing
+ * in the opposite direction.
+ * @return an instance containing the same points but pointing in the opposite
+ * direction
+ */
+ public Line3D reverse() {
+ return new Line3D(origin, direction.negate(), precision);
+ }
+
+ /** Transform this instance.
+ * @param transform object used to transform the instance
+ * @return a transformed instance
+ */
+ public Line3D transform(final Transform<Vector3D> transform) {
+ final Vector3D p1 = transform.apply(origin);
+ final Vector3D p2 = transform.apply(origin.add(direction));
+
+ return fromPoints(p1, p2, precision);
+ }
+
+ /** Get an object containing the current line transformed by the argument along with a
+ * 1D transform that can be applied to subspace points. The subspace transform transforms
+ * subspace points such that their 3D location in the transformed line is the same as their
+ * 3D location in the original line after the 3D transform is applied. For example, consider
+ * the code below:
+ * <pre>
+ * SubspaceTransform st = line.subspaceTransform(transform);
+ *
+ * Vector1D subPt = Vector1D.of(1);
+ *
+ * Vector3D a = transform.apply(line.toSpace(subPt)); // transform in 3D space
+ * Vector3D b = st.getLine().toSpace(st.getTransform().apply(subPt)); // transform in 1D space
+ * </pre>
+ * At the end of execution, the points {@code a} (which was transformed using the original
+ * 3D transform) and {@code b} (which was transformed in 1D using the subspace transform)
+ * are equivalent.
+ *
+ * @param transform the transform to apply to this instance
+ * @return an object containing the transformed line along with a transform that can be applied
+ * to subspace points
+ * @see #transform(Transform)
+ */
+ public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
+ final Vector3D p1 = transform.apply(origin);
+ final Vector3D p2 = transform.apply(origin.add(direction));
+
+ final Line3D tLine = fromPoints(p1, p2, precision);
+
+ final Vector1D tSubspaceOrigin = tLine.toSubspace(p1);
+ final Vector1D tSubspaceDirection = tSubspaceOrigin.vectorTo(tLine.toSubspace(p2));
+
+ final double translation = tSubspaceOrigin.getX();
+ final double scale = tSubspaceDirection.getX();
+
+ final AffineTransformMatrix1D subspaceTransform = AffineTransformMatrix1D.of(scale, translation);
+
+ return new SubspaceTransform(tLine, subspaceTransform);
+ }
+
+ /** Get the abscissa of the given point on the line. The abscissa represents
+ * the distance the projection of the point on the line is from the line's
+ * origin point (the point on the line closest to the origin of the
+ * 2D space). Abscissa values increase in the direction of the line. This method
+ * is exactly equivalent to {@link #toSubspace(Vector3D)} except that this method
+ * returns a double instead of a {@link Vector1D}.
+ * @param point point to compute the abscissa for
+ * @return abscissa value of the point
+ * @see #toSubspace(Vector3D)
+ */
+ public double abscissa(final Vector3D point) {
+ return point.subtract(origin).dot(direction);
+ }
+
+ /** Get one point from the line.
+ * @param abscissa desired abscissa for the point
+ * @return one point belonging to the line, at specified abscissa
+ */
+ public Vector3D pointAt(final double abscissa) {
+ return Vector3D.linearCombination(1.0, origin, abscissa, direction);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector1D toSubspace(Vector3D pt) {
+ return Vector1D.of(abscissa(pt));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D toSpace(Vector1D pt) {
+ return toSpace(pt.getX());
+ }
+
+ /** Get the 3 dimensional point at the given abscissa position
+ * on the line.
+ * @param abscissa location on the line
+ * @return the 3 dimensional point at the given abscissa position
+ * on the line
+ */
+ public Vector3D toSpace(final double abscissa) {
+ return pointAt(abscissa);
+ }
+
+ /** Check if the instance is similar to another line.
+ * <p>Lines are considered similar if they contain the same
+ * points. This does not mean they are equal since they can have
+ * opposite directions.</p>
+ * @param line line to which instance should be compared
+ * @return true if the lines are similar
+ */
+ public boolean isSimilarTo(final Line3D line) {
+ final double angle = direction.angle(line.direction);
+ return (precision.eqZero(angle) || precision.eq(Math.abs(angle), Math.PI)) &&
+ contains(line.origin);
+ }
+
+ /** Check if the instance contains a point.
+ * @param pt point to check
+ * @return true if p belongs to the line
+ */
+ public boolean contains(final Vector3D pt) {
+ return precision.eqZero(distance(pt));
+ }
+
+ /** Compute the distance between the instance and a point.
+ * @param pt to check
+ * @return distance between the instance and the point
+ */
+ public double distance(final Vector3D pt) {
+ final Vector3D delta = pt.subtract(origin);
+ final Vector3D orthogonal = delta.reject(direction);
+
+ return orthogonal.norm();
+ }
+
+ /** Compute the shortest distance between the instance and another line.
+ * @param line line to check against the instance
+ * @return shortest distance between the instance and the line
+ */
+ public double distance(final Line3D line) {
+
+ final Vector3D normal = direction.cross(line.direction);
+ final double norm = normal.norm();
+
+ if (precision.eqZero(norm)) {
+ // the lines are parallel
+ return distance(line.origin);
+ }
+
+ // signed separation of the two parallel planes that contains the lines
+ final double offset = line.origin.subtract(origin).dot(normal) / norm;
+
+ return Math.abs(offset);
+ }
+
+ /** Compute the point of the instance closest to another line.
+ * @param line line to check against the instance
+ * @return point of the instance closest to another line
+ */
+ public Vector3D closest(final Line3D line) {
+
+ final double cos = direction.dot(line.direction);
+ final double n = 1 - cos * cos;
+
+ if (precision.eqZero(n)) {
+ // the lines are parallel
+ return origin;
+ }
+
+ final Vector3D delta = line.origin.subtract(origin);
+ final double a = delta.dot(direction);
+ final double b = delta.dot(line.direction);
+
+ return Vector3D.linearCombination(1, origin, (a - (b * cos)) / n, direction);
+ }
+
+ /** Get the intersection point of the instance and another line.
+ * @param line other line
+ * @return intersection point of the instance and the other line
+ * or null if there are no intersection points
+ */
+ public Vector3D intersection(final Line3D line) {
+ final Vector3D closestPt = closest(line);
+ return line.contains(closestPt) ? closestPt : null;
+ }
+
+ /** Return a new infinite segment representing the entire line.
+ * @return a new infinite segment representing the entire line
+ */
+ public Segment3D span() {
+ return Segment3D.fromInterval(this, Interval.full());
+ }
+
+ /** Create a new line segment from the given interval.
+ * @param interval interval representing the 1D region for the line segment
+ * @return a new line segment on this line
+ */
+ public Segment3D segment(final Interval interval) {
+ return Segment3D.fromInterval(this, interval);
+ }
+
+ /** Create a new line segment from the given interval.
+ * @param a first 1D location for the interval
+ * @param b second 1D location for the interval
+ * @return a new line segment on this line
+ */
+ public Segment3D segment(final double a, final double b) {
+ return Segment3D.fromInterval(this, a, b);
+ }
+
+ /** Create a new line segment between the projections of the two
+ * given points onto this line.
+ * @param a first point
+ * @param b second point
+ * @return a new line segment on this line
+ */
+ public Segment3D segment(final Vector3D a, final Vector3D b) {
+ return Segment3D.fromInterval(this, toSubspace(a), toSubspace(b));
+ }
+
+ /** Create a new line segment that starts at infinity and continues along
+ * the line up to the projection of the given point.
+ * @param pt point defining the end point of the line segment; the end point
+ * is equal to the projection of this point onto the line
+ * @return a new, half-open line segment
+ */
+ public Segment3D segmentTo(final Vector3D pt) {
+ return segment(Double.NEGATIVE_INFINITY, toSubspace(pt).getX());
+ }
+
+ /** Create a new line segment that starts at the projection of the given point
+ * and continues in the direction of the line to infinity, similar to a ray.
+ * @param pt point defining the start point of the line segment; the start point
+ * is equal to the projection of this point onto the line
+ * @return a new, half-open line segment
+ */
+ public Segment3D segmentFrom(final Vector3D pt) {
+ return segment(toSubspace(pt).getX(), Double.POSITIVE_INFINITY);
+ }
+
+ /** Create a new, empty subline based on this line.
+ * @return a new, empty subline based on this line
+ */
+ public SubLine3D subline() {
+ return new SubLine3D(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(origin, direction, precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Line3D)) {
+ return false;
+ }
+ Line3D other = (Line3D) obj;
+ return this.origin.equals(other.origin) &&
+ this.direction.equals(other.direction) &&
+ this.precision.equals(other.precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName())
+ .append("[origin= ")
+ .append(origin)
+ .append(", direction= ")
+ .append(direction)
+ .append("]");
+
+ return sb.toString();
+ }
+
+ /** Create a new line instance from two points that lie on the line. The line
+ * direction points from the first point to the second point.
+ * @param p1 first point on the line
+ * @param p2 second point on the line
+ * @param precision floating point precision context
+ * @return a new line instance that contains both of the given point and that has
+ * a direction going from the first point to the second point
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the points lie too close to
+ * create a non-zero direction vector
+ */
+ public static Line3D fromPoints(final Vector3D p1, final Vector3D p2,
+ final DoublePrecisionContext precision) {
+ return fromPointAndDirection(p1, p1.directionTo(p2), precision);
+ }
+
+ /** Create a new line instance from a point and a direction.
+ * @param pt a point lying on the line
+ * @param direction the direction of the line
+ * @param precision floating point precision context
+ * @return a new line instance that contains the given point and points in the
+ * given direction
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the direction cannot be normalized
+ */
+ public static Line3D fromPointAndDirection(final Vector3D pt, final Vector3D direction,
+ final DoublePrecisionContext precision) {
+
+ final Vector3D normDirection = direction.normalize();
+ final Vector3D origin = pt.reject(normDirection);
+
+ return new Line3D(origin, normDirection, precision);
+ }
+
+ /** Class containing a transformed line instance along with a subspace (1D) transform. The subspace
+ * transform produces the equivalent of the 3D transform in 1D.
+ */
+ public static final class SubspaceTransform implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190809L;
+
+ /** The transformed line. */
+ private final Line3D line;
+
+ /** The subspace transform instance. */
+ private final AffineTransformMatrix1D transform;
+
+ /** Simple constructor.
+ * @param line the transformed line
+ * @param transform 1D transform that can be applied to subspace points
+ */
+ public SubspaceTransform(final Line3D line, final AffineTransformMatrix1D transform) {
+ this.line = line;
+ this.transform = transform;
+ }
+
+ /** Get the transformed line instance.
+ * @return the transformed line instance
+ */
+ public Line3D getLine() {
+ return line;
+ }
+
+ /** Get the 1D transform that can be applied to subspace points. This transform can be used
+ * to perform the equivalent of the 3D transform in 1D space.
+ * @return the subspace transform instance
+ */
+ public AffineTransformMatrix1D getTransform() {
+ return transform;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java
deleted file mode 100644
index 48c8181..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * 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.threed;
-
-import java.util.ArrayList;
-
-import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
-import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
-
-/** Extractor for {@link PolygonsSet polyhedrons sets} outlines.
- * <p>This class extracts the 2D outlines from {{@link PolygonsSet
- * polyhedrons sets} in a specified projection plane.</p>
- */
-public class OutlineExtractor {
-
- /** Abscissa axis of the projection plane. */
- private final Vector3D u;
-
- /** Ordinate axis of the projection plane. */
- private final Vector3D v;
-
- /** Normal of the projection plane (viewing direction). */
- private final Vector3D w;
-
- /** Build an extractor for a specific projection plane.
- * @param u abscissa axis of the projection point
- * @param v ordinate axis of the projection point
- */
- public OutlineExtractor(final Vector3D u, final Vector3D v) {
- this.u = u;
- this.v = v;
- this.w = u.cross(v);
- }
-
- /** Extract the outline of a polyhedrons set.
- * @param polyhedronsSet polyhedrons set whose outline must be extracted
- * @return an outline, as an array of loops.
- */
- public Vector2D[][] getOutline(final PolyhedronsSet polyhedronsSet) {
-
- // project all boundary facets into one polygons set
- final BoundaryProjector projector = new BoundaryProjector(polyhedronsSet.getPrecision());
- polyhedronsSet.getTree(true).visit(projector);
- final PolygonsSet projected = projector.getProjected();
-
- // Remove the spurious intermediate vertices from the outline
- final Vector2D[][] outline = projected.getVertices();
- for (int i = 0; i < outline.length; ++i) {
- final Vector2D[] rawLoop = outline[i];
- int end = rawLoop.length;
- int j = 0;
- while (j < end) {
- if (pointIsBetween(rawLoop, end, j)) {
- // the point should be removed
- for (int k = j; k < (end - 1); ++k) {
- rawLoop[k] = rawLoop[k + 1];
- }
- --end;
- } else {
- // the point remains in the loop
- ++j;
- }
- }
- if (end != rawLoop.length) {
- // resize the array
- outline[i] = new Vector2D[end];
- System.arraycopy(rawLoop, 0, outline[i], 0, end);
- }
- }
-
- return outline;
-
- }
-
- /** Check if a point is geometrically between its neighbor in an array.
- * <p>The neighbors are computed considering the array is a loop
- * (i.e. point at index (n-1) is before point at index 0)</p>
- * @param loop points array
- * @param n number of points to consider in the array
- * @param i index of the point to check (must be between 0 and n-1)
- * @return true if the point is exactly between its neighbors
- */
- private boolean pointIsBetween(final Vector2D[] loop, final int n, final int i) {
- final Vector2D previous = loop[(i + n - 1) % n];
- final Vector2D current = loop[i];
- final Vector2D next = loop[(i + 1) % n];
- final double dx1 = current.getX() - previous.getX();
- final double dy1 = current.getY() - previous.getY();
- final double dx2 = next.getX() - current.getX();
- final double dy2 = next.getY() - current.getY();
- final double cross = dx1 * dy2 - dx2 * dy1;
- final double dot = dx1 * dx2 + dy1 * dy2;
- final double d1d2 = Math.sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2));
- return (Math.abs(cross) <= (1.0e-6 * d1d2)) && (dot >= 0.0);
- }
-
- /** Visitor projecting the boundary facets on a plane. */
- private class BoundaryProjector implements BSPTreeVisitor<Vector3D> {
-
- /** Projection of the polyhedrons set on the plane. */
- private PolygonsSet projected;
-
- /** Precision context used to compare floating point numbers. */
- private final DoublePrecisionContext precision;
-
- /** Simple constructor.
- * @param precision precision context used to compare floating point values
- */
- BoundaryProjector(final DoublePrecisionContext precision) {
- this.projected = new PolygonsSet(new BSPTree<Vector2D>(Boolean.FALSE), precision);
- this.precision = precision;
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<Vector3D> node) {
- return Order.MINUS_SUB_PLUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<Vector3D> node) {
- @SuppressWarnings("unchecked")
- final BoundaryAttribute<Vector3D> attribute =
- (BoundaryAttribute<Vector3D>) node.getAttribute();
- if (attribute.getPlusOutside() != null) {
- addContribution(attribute.getPlusOutside(), false);
- }
- if (attribute.getPlusInside() != null) {
- addContribution(attribute.getPlusInside(), true);
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<Vector3D> node) {
- }
-
- /** Add he contribution of a boundary facet.
- * @param facet boundary facet
- * @param reversed if true, the facet has the inside on its plus side
- */
- private void addContribution(final SubHyperplane<Vector3D> facet, final boolean reversed) {
-
- // extract the vertices of the facet
- @SuppressWarnings("unchecked")
- final AbstractSubHyperplane<Vector3D, Vector2D> absFacet =
- (AbstractSubHyperplane<Vector3D, Vector2D>) facet;
- final Plane plane = (Plane) facet.getHyperplane();
-
- final double scal = plane.getNormal().dot(w);
- if (Math.abs(scal) > 1.0e-3) {
- Vector2D[][] vertices =
- ((PolygonsSet) absFacet.getRemainingRegion()).getVertices();
-
- if ((scal < 0) ^ reversed) {
- // the facet is seen from the inside,
- // we need to invert its boundary orientation
- final Vector2D[][] newVertices = new Vector2D[vertices.length][];
- for (int i = 0; i < vertices.length; ++i) {
- final Vector2D[] loop = vertices[i];
- final Vector2D[] newLoop = new Vector2D[loop.length];
- if (loop[0] == null) {
- newLoop[0] = null;
- for (int j = 1; j < loop.length; ++j) {
- newLoop[j] = loop[loop.length - j];
- }
- } else {
- for (int j = 0; j < loop.length; ++j) {
- newLoop[j] = loop[loop.length - (j + 1)];
- }
- }
- newVertices[i] = newLoop;
- }
-
- // use the reverted vertices
- vertices = newVertices;
-
- }
-
- // compute the projection of the facet in the outline plane
- final ArrayList<SubHyperplane<Vector2D>> edges = new ArrayList<>();
- for (Vector2D[] loop : vertices) {
- final boolean closed = loop[0] != null;
- int previous = closed ? (loop.length - 1) : 1;
- Vector3D previous3D = plane.toSpace(loop[previous]);
- int current = (previous + 1) % loop.length;
- Vector2D pPoint = Vector2D.of(previous3D.dot(u),
- previous3D.dot(v));
- while (current < loop.length) {
-
- final Vector3D current3D = plane.toSpace(loop[current]);
- final Vector2D cPoint = Vector2D.of(current3D.dot(u),
- current3D.dot(v));
- final org.apache.commons.geometry.euclidean.twod.Line line =
- org.apache.commons.geometry.euclidean.twod.Line.fromPoints(pPoint, cPoint, precision);
- SubHyperplane<Vector2D> edge = line.wholeHyperplane();
-
- if (closed || (previous != 1)) {
- // the previous point is a real vertex
- // it defines one bounding point of the edge
- final double angle = line.getAngle() + 0.5 * Math.PI;
- final org.apache.commons.geometry.euclidean.twod.Line l =
- org.apache.commons.geometry.euclidean.twod.Line.fromPointAndAngle(pPoint, angle, precision);
- edge = edge.split(l).getPlus();
- }
-
- if (closed || (current != (loop.length - 1))) {
- // the current point is a real vertex
- // it defines one bounding point of the edge
- final double angle = line.getAngle() + 0.5 * Math.PI;
- final org.apache.commons.geometry.euclidean.twod.Line l =
- org.apache.commons.geometry.euclidean.twod.Line.fromPointAndAngle(cPoint, angle, precision);
- edge = edge.split(l).getMinus();
- }
-
- edges.add(edge);
-
- previous = current++;
- previous3D = current3D;
- pPoint = cPoint;
-
- }
- }
- final PolygonsSet projectedFacet = new PolygonsSet(edges, precision);
-
- // add the contribution of the facet to the global outline
- projected = (PolygonsSet) new RegionFactory<Vector2D>().union(projected, projectedFacet);
-
- }
- }
-
- /** Get the projection of the polyhedrons set on the plane.
- * @return projection of the polyhedrons set on the plane
- */
- public PolygonsSet getProjected() {
- return projected;
- }
-
- }
-
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
index a253d9b..48597ae 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
@@ -16,25 +16,32 @@
*/
package org.apache.commons.geometry.euclidean.threed;
+import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
+import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.exception.GeometryException;
-import org.apache.commons.geometry.core.exception.IllegalNormException;
-import org.apache.commons.geometry.core.partitioning.Embedding;
+import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
-/**
- * The class represents a plane in a three dimensional space.
+/** Class representing a plane in 3 dimensional Euclidean space.
*/
-public final class Plane implements Hyperplane<Vector3D>, Embedding<Vector3D, Vector2D> {
+public final class Plane extends AbstractHyperplane<Vector3D>
+ implements EmbeddingHyperplane<Vector3D, Vector2D>, Equivalency<Plane> {
+
+ /** Serializable version UID. */
+ private static final long serialVersionUID = 20190702L;
/** First normalized vector of the plane frame (in plane). */
private final Vector3D u;
@@ -48,9 +55,6 @@
/** Offset of the origin with respect to the plane. */
private final double originOffset;
- /** Precision context used to compare floating point numbers. */
- private final DoublePrecisionContext precision;
-
/**
* Constructor to build a new plane with the given values.
* Made private to prevent inheritance.
@@ -59,196 +63,17 @@
* @param w unit normal vector
* @param originOffset offset of the origin with respect to the plane.
* @param precision precision context used to compare floating point values
- * @throws IllegalArgumentException if the provided vectors are coplanar or not normalized
*/
private Plane(final Vector3D u, final Vector3D v, final Vector3D w, double originOffset,
final DoublePrecisionContext precision) {
+
+ super(precision);
+
this.u = u;
this.v = v;
this.w = w;
- if (areCoplanar(u, v, w, precision)) {
- throw new IllegalArgumentException("Provided vectors must not be coplanar.");
- }
+
this.originOffset = originOffset;
- this.precision = precision;
- }
-
- /**
- * Build a plane from a point and two (on plane) vectors.
- * @param p the provided point (on plane)
- * @param u u vector (on plane)
- * @param v v vector (on plane)
- * @param precision precision context used to compare floating point values
- * @return a new plane
- * @throws IllegalNormException if the norm of the given values is zero, NaN, or infinite.
- * @throws IllegalArgumentException if the provided vectors are collinear
- */
- public static Plane fromPointAndPlaneVectors (final Vector3D p, final Vector3D u, final Vector3D v, final DoublePrecisionContext precision) {
- final Vector3D uNorm = u.normalize();
- final Vector3D vNorm = uNorm.orthogonal(v);
- final Vector3D wNorm = uNorm.cross(vNorm).normalize();
- final double originOffset = -p.dot(wNorm);
-
- return new Plane(uNorm, vNorm, wNorm, originOffset, precision);
- }
-
- /**
- * Build a plane from a normal.
- * Chooses origin as point on plane.
- * @param normal normal direction to the plane
- * @param precision precision context used to compare floating point values
- * @return a new plane
- * @throws IllegalNormException if the norm of the given values is zero, NaN, or infinite.
- */
- public static Plane fromNormal(final Vector3D normal, final DoublePrecisionContext precision) {
- return fromPointAndNormal(Vector3D.ZERO, normal, precision);
- }
-
- /**
- * Build a plane from a point and a normal.
- *
- * @param p point belonging to the plane
- * @param normal normal direction to the plane
- * @param precision precision context used to compare floating point values
- * @return a new plane
- * @throws IllegalNormException if the norm of the given values is zero, NaN, or infinite.
- */
- public static Plane fromPointAndNormal(final Vector3D p, final Vector3D normal, final DoublePrecisionContext precision) {
- final Vector3D w = normal.normalize();
- final double originOffset = -p.dot(w);
- final Vector3D u = w.orthogonal();
- final Vector3D v = w.cross(u);
-
- return new Plane(u, v, w, originOffset, precision);
- }
-
- /**
- * Build a plane from three points.
- * <p>
- * The plane is oriented in the direction of {@code (p2-p1) ^ (p3-p1)}
- * </p>
- *
- * @param p1 first point belonging to the plane
- * @param p2 second point belonging to the plane
- * @param p3 third point belonging to the plane
- * @param precision precision context used to compare floating point values
- * @return a new plane
- * @throws GeometryException if the points do not define a unique plane
- */
- public static Plane fromPoints(final Vector3D p1, final Vector3D p2, final Vector3D p3,
- final DoublePrecisionContext precision) {
- return Plane.fromPoints(Arrays.asList(p1, p2, p3), precision);
- }
-
- /** Construct a plane from a collection of points lying on the plane. The plane orientation is
- * determined by the overall orientation of the point sequence. For example, if the points wind
- * around the z-axis in a counter-clockwise direction, then the plane normal will point up the
- * +z axis. If the points wind in the opposite direction, then the plane normal will point down
- * the -z axis. The {@code u} vector for the plane is set to the first non-zero vector between
- * points in the sequence (ie, the first direction in the path).
- *
- * @param pts collection of sequenced points lying on the plane
- * @param precision precision context used to compare floating point values
- * @return a new plane containing the given points
- * @throws IllegalArgumentException if the given collection does not contain at least 3 points
- * @throws GeometryException if the points do not define a unique plane
- */
- public static Plane fromPoints(final Collection<Vector3D> pts, final DoublePrecisionContext precision) {
-
- if (pts.size() < 3) {
- throw new IllegalArgumentException("At least 3 points are required to define a plane; " +
- "argument contains only " + pts.size() + ".");
- }
-
- final Iterator<Vector3D> it = pts.iterator();
-
- Vector3D startPt = it.next();
-
- Vector3D u = null;
- Vector3D w = null;
-
- Vector3D currentPt;
-
- Vector3D currentVector = null;
- Vector3D prevVector = null;
-
- Vector3D cross = null;
- double crossNorm;
- double crossSumX = 0;
- double crossSumY = 0;
- double crossSumZ = 0;
-
- boolean nonPlanar = false;
-
- while (it.hasNext()) {
- currentPt = it.next();
- currentVector = startPt.vectorTo(currentPt);
-
- if (!precision.eqZero(currentVector.norm())) {
-
- if (u == null) {
- // save the first non-zero vector as our u vector
- u = currentVector.normalize();
- }
- if (prevVector != null) {
- cross = prevVector.cross(currentVector);
-
- crossSumX += cross.getX();
- crossSumY += cross.getY();
- crossSumZ += cross.getZ();
-
- crossNorm = cross.norm();
-
- if (!precision.eqZero(crossNorm)) {
- // the cross product has non-zero magnitude
- if (w == null) {
- // save the first non-zero cross product as our normal
- w = cross.normalize();
- }
- else if (!precision.eq(1.0, Math.abs(w.dot(cross) / crossNorm))) {
- // if the normalized dot product is not either +1 or -1, then
- // the points are not coplanar
- nonPlanar = true;
- break;
- }
- }
- }
-
- prevVector = currentVector;
- }
- }
-
- if (u == null ||
- w == null ||
- nonPlanar) {
- throw new GeometryException("Points do not define a plane: " + pts);
- }
-
- if (w.dot(Vector3D.of(crossSumX, crossSumY, crossSumZ)) < 0) {
- w = w.negate();
- }
-
- final Vector3D v = w.cross(u);
- final double originOffset = -startPt.dot(w);
-
- return new Plane(u, v, w, originOffset, precision);
- }
-
- // This should be removed after GEOMETRY-32 is done.
- /**
- * Copy the instance.
- * <p>
- * The instance created is completely independent of the original one. A deep
- * copy is used, none of the underlying objects are shared (except for immutable
- * objects).
- * </p>
- *
- * @return a new hyperplane, copy of the instance
- */
- @Deprecated
- @Override
- public Plane copySelf() {
- return new Plane(this.u, this.v, this.w, this.originOffset, this.precision);
}
/**
@@ -261,7 +86,7 @@
}
/**
- * Get the offset of the origin with respect to the plane.
+ * Get the offset of the spatial origin ({@code 0, 0, 0}) with respect to the plane.
*
* @return the offset of the origin with respect to the plane.
*/
@@ -300,30 +125,33 @@
}
/**
- * Get the normalized normal vector, alias for getNormal().
+ * Get the normalized normal vector.
* <p>
- * The frame defined by ({@link #getU getU}, {@link #getV getV},
- * {@link #getNormal getNormal}) is a right-handed orthonormalized frame).
+ * The frame defined by {@link #getU()}, {@link #getV()},
+ * {@link #getW()} is a right-handed orthonormalized frame.
* </p>
*
* @return normalized normal vector
- * @see #getU
- * @see #getV
+ * @see #getU()
+ * @see #getV()
+ * @see #getNormal()
*/
public Vector3D getW() {
return w;
}
/**
- * Get the normalized normal vector.
+ * Get the normalized normal vector. This method is an alias
+ * for {@link #getW()}.
* <p>
- * The frame defined by ({@link #getU getU}, {@link #getV getV},
- * {@link #getNormal getNormal}) is a right-handed orthonormalized frame).
+ * The frame defined by {@link #getU()}, {@link #getV()},
+ * {@link #getW()} is a right-handed orthonormalized frame.
* </p>
*
* @return normalized normal vector
- * @see #getU
- * @see #getV
+ * @see #getU()
+ * @see #getV()
+ * @see #getW()
*/
public Vector3D getNormal() {
return w;
@@ -332,28 +160,23 @@
/** {@inheritDoc} */
@Override
public Vector3D project(final Vector3D point) {
- return toSpace(toSubSpace(point));
+ return toSpace(toSubspace(point));
}
/**
- * 3D line projected onto plane
+ * Project a 3D line onto the plane.
* @param line the line to project
* @return the projection of the given line onto the plane.
*/
- public Line project(final Line line) {
+ public Line3D project(final Line3D line) {
Vector3D direction = line.getDirection();
- Vector3D projection = w.multiply(direction.dot(w) * (1/w.normSq()));
+ Vector3D projection = w.multiply(direction.dot(w) * (1 / w.normSq()));
+
Vector3D projectedLineDirection = direction.subtract(projection);
Vector3D p1 = project(line.getOrigin());
Vector3D p2 = p1.add(projectedLineDirection);
- return new Line(p1,p2, precision);
- }
-
- /** {@inheritDoc} */
- @Override
- public DoublePrecisionContext getPrecision() {
- return precision;
+ return Line3D.fromPoints(p1, p2, getPrecision());
}
/**
@@ -369,14 +192,9 @@
* </p>
* @return a new reversed plane
*/
+ @Override
public Plane reverse() {
- final Vector3D tmp = u;
- Vector3D uTmp = v;
- Vector3D vTmp = tmp;
- Vector3D wTmp = this.w.negate();
- double originOffsetTmp = -this.originOffset;
-
- return new Plane(uTmp, vTmp, wTmp, originOffsetTmp, this.precision);
+ return new Plane(v, u, w.negate(), -originOffset, getPrecision());
}
/**
@@ -387,7 +205,7 @@
* @see #toSpace
*/
@Override
- public Vector2D toSubSpace(final Vector3D point) {
+ public Vector2D toSubspace(final Vector3D point) {
return Vector2D.of(point.dot(u), point.dot(v));
}
@@ -396,13 +214,107 @@
*
* @param point in-plane point (must be a {@link Vector2D} instance)
* @return 3D space point
- * @see #toSubSpace
+ * @see #toSubspace(Vector3D)
*/
@Override
public Vector3D toSpace(final Vector2D point) {
return Vector3D.linearCombination(point.getX(), u, point.getY(), v, -originOffset, w);
}
+ /** {@inheritDoc} */
+ @Override
+ public Plane transform(final Transform<Vector3D> transform) {
+ final Vector3D origin = getOrigin();
+
+ final Vector3D p1 = transform.apply(origin);
+ final Vector3D p2 = transform.apply(origin.add(u));
+ final Vector3D p3 = transform.apply(origin.add(v));
+
+ return fromPoints(p1, p2, p3, getPrecision());
+ }
+
+ /** Get an object containing the current plane transformed by the argument along with a
+ * 2D transform that can be applied to subspace points. The subspace transform transforms
+ * subspace points such that their 3D location in the transformed plane is the same as their
+ * 3D location in the original plane after the 3D transform is applied. For example, consider
+ * the code below:
+ * <pre>
+ * SubspaceTransform st = plane.subspaceTransform(transform);
+ *
+ * Vector2D subPt = Vector2D.of(1, 1);
+ *
+ * Vector3D a = transform.apply(plane.toSpace(subPt)); // transform in 3D space
+ * Vector3D b = st.getPlane().toSpace(st.getTransform().apply(subPt)); // transform in 2D space
+ * </pre>
+ * At the end of execution, the points {@code a} (which was transformed using the original
+ * 3D transform) and {@code b} (which was transformed in 2D using the subspace transform)
+ * are equivalent.
+ *
+ * @param transform the transform to apply to this instance
+ * @return an object containing the transformed plane along with a transform that can be applied
+ * to subspace points
+ * @see #transform(Transform)
+ */
+ public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
+ final Vector3D origin = getOrigin();
+
+ final Vector3D p1 = transform.apply(origin);
+ final Vector3D p2 = transform.apply(origin.add(u));
+ final Vector3D p3 = transform.apply(origin.add(v));
+
+ final Plane tPlane = fromPoints(p1, p2, p3, getPrecision());
+
+ final Vector2D tSubspaceOrigin = tPlane.toSubspace(p1);
+ final Vector2D tSubspaceU = tSubspaceOrigin.vectorTo(tPlane.toSubspace(p2));
+ final Vector2D tSubspaceV = tSubspaceOrigin.vectorTo(tPlane.toSubspace(p3));
+
+ final AffineTransformMatrix2D subspaceTransform =
+ AffineTransformMatrix2D.fromColumnVectors(tSubspaceU, tSubspaceV, tSubspaceOrigin);
+
+ return new SubspaceTransform(tPlane, subspaceTransform);
+ }
+
+ /**
+ * Rotate the plane around the specified point.
+ * <p>
+ * The instance is not modified, a new instance is created.
+ * </p>
+ *
+ * @param center rotation center
+ * @param rotation 3-dimensional rotation
+ * @return a new plane
+ */
+ public Plane rotate(final Vector3D center, final QuaternionRotation rotation) {
+ final Vector3D delta = getOrigin().subtract(center);
+ final Vector3D p = center.add(rotation.apply(delta));
+ final Vector3D normal = rotation.apply(this.w);
+ final Vector3D wTmp = normal.normalize();
+
+ final double originOffsetTmp = -p.dot(wTmp);
+ final Vector3D uTmp = rotation.apply(this.u);
+ final Vector3D vTmp = rotation.apply(this.v);
+
+ return new Plane(uTmp, vTmp, wTmp, originOffsetTmp, getPrecision());
+ }
+
+ /**
+ * Translate the plane by the specified amount.
+ * <p>
+ * The instance is not modified, a new instance is created.
+ * </p>
+ *
+ * @param translation translation to apply
+ * @return a new plane
+ */
+ public Plane translate(final Vector3D translation) {
+ Vector3D p = getOrigin().add(translation);
+ Vector3D normal = this.w;
+ Vector3D wTmp = normal.normalize();
+ double originOffsetTmp = -p.dot(wTmp);
+
+ return new Plane(this.u, this.v, wTmp, originOffsetTmp, getPrecision());
+ }
+
/**
* Get one point from the 3D-space.
*
@@ -427,53 +339,12 @@
*/
public boolean contains(final Plane plane) {
final double angle = w.angle(plane.w);
+ final DoublePrecisionContext precision = getPrecision();
return ((precision.eqZero(angle)) && precision.eq(originOffset, plane.originOffset)) ||
((precision.eq(angle, Math.PI)) && precision.eq(originOffset, -plane.originOffset));
}
-
-
- /**
- * Rotate the plane around the specified point.
- * <p>
- * The instance is not modified, a new instance is created.
- * </p>
- *
- * @param center rotation center
- * @param rotation 3-dimensional rotation
- * @return a new plane
- */
- public Plane rotate(final Vector3D center, final QuaternionRotation rotation) {
- final Vector3D delta = getOrigin().subtract(center);
- Vector3D p = center.add(rotation.apply(delta));
- Vector3D normal = rotation.apply(this.w);
- Vector3D wTmp = normal.normalize();
- double originOffsetTmp = -p.dot(wTmp);
- Vector3D uTmp = rotation.apply(this.u);
- Vector3D vTmp = rotation.apply(this.v);
-
- return new Plane(uTmp, vTmp, wTmp, originOffsetTmp, this.precision);
- }
-
- /**
- * Translate the plane by the specified amount.
- * <p>
- * The instance is not modified, a new instance is created.
- * </p>
- *
- * @param translation translation to apply
- * @return a new plane
- */
- public Plane translate(final Vector3D translation) {
- Vector3D p = getOrigin().add(translation);
- Vector3D normal = this.w;
- Vector3D wTmp = normal.normalize();
- double originOffsetTmp = -p.dot(wTmp);
-
- return new Plane(this.u, this.v, wTmp, originOffsetTmp, this.precision);
- }
-
/**
* Get the intersection of a line with the instance.
*
@@ -481,42 +352,47 @@
* @return intersection point between between the line and the instance (null if
* the line is parallel to the instance)
*/
- public Vector3D intersection(final Line line) {
+ public Vector3D intersection(final Line3D line) {
final Vector3D direction = line.getDirection();
final double dot = w.dot(direction);
- if (precision.eqZero(dot)) {
+ if (getPrecision().eqZero(dot)) {
return null;
}
final Vector3D point = line.toSpace(Vector1D.ZERO);
final double k = -(originOffset + w.dot(point)) / dot;
-
return Vector3D.linearCombination(1.0, point, k, direction);
}
/**
- * Build the line shared by the instance and another plane.
+ * Get the line formed by the intersection of this instance with the given plane.
+ * The returned line lies in both planes and points in the direction of
+ * the cross product <code>n<sub>1</sub> x n<sub>2</sub></code>, where <code>n<sub>1</sub></code>
+ * is the normal of the current instance and <code>n<sub>2</sub></code> is the normal
+ * of the argument.
+ *
+ * <p>Null is returned if the planes are parallel.</p>
*
* @param other other plane
- * @return line at the intersection of the instance and the other plane (really
- * a {@link Line Line} instance)
+ * @return line at the intersection of the instance and the other plane, or null
+ * if no such line exists
*/
- public Line intersection(final Plane other) {
+ public Line3D intersection(final Plane other) {
final Vector3D direction = w.cross(other.w);
- if (precision.eqZero(direction.norm())) {
+ if (getPrecision().eqZero(direction.norm())) {
return null;
}
- final Vector3D point = intersection(this, other, Plane.fromNormal(direction, precision));
-
- return new Line(point, point.add(direction), precision);
+ final Vector3D point = intersection(this, other, Plane.fromNormal(direction, getPrecision()));
+ return Line3D.fromPointAndDirection(point, direction, getPrecision());
}
/**
- * Get the intersection point of three planes.
+ * Get the intersection point of three planes. Returns null if no unique intersection point
+ * exists (ie, there are no intersection points or an infinite number).
*
* @param plane1 first plane1
* @param plane2 second plane2
* @param plane3 third plane2
- * @return intersection point of three planes, null if some planes are parallel
+ * @return intersection point of the three planes or null if no unique intersection point exists
*/
public static Vector3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {
@@ -538,40 +414,27 @@
// direct Cramer resolution of the linear system
// (this is still feasible for a 3x3 system)
- final double a23 = b2 * c3 - b3 * c2;
- final double b23 = c2 * a3 - c3 * a2;
- final double c23 = a2 * b3 - a3 * b2;
- final double determinant = a1 * a23 + b1 * b23 + c1 * c23;
- if (Math.abs(determinant) < 1.0e-10) {
+ final double a23 = (b2 * c3) - (b3 * c2);
+ final double b23 = (c2 * a3) - (c3 * a2);
+ final double c23 = (a2 * b3) - (a3 * b2);
+ final double determinant = (a1 * a23) + (b1 * b23) + (c1 * c23);
+
+ // use the precision context of the first plane to determine equality
+ if (plane1.getPrecision().eqZero(determinant)) {
return null;
}
final double r = 1.0 / determinant;
-
return Vector3D.of((-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r,
(-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r,
(-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r);
+
}
- /**
- * Build a region covering the whole hyperplane.
- *
- * @return a region covering the whole hyperplane
- */
+ /** {@inheritDoc} */
@Override
- public SubPlane wholeHyperplane() {
- return new SubPlane(this, new PolygonsSet(precision));
- }
-
- /**
- * Build a region covering the whole space.
- *
- * @return a region containing the instance (really a {@link PolyhedronsSet
- * PolyhedronsSet} instance)
- */
- @Override
- public PolyhedronsSet wholeSpace() {
- return new PolyhedronsSet(precision);
+ public ConvexSubPlane span() {
+ return ConvexSubPlane.fromConvexArea(this, ConvexArea.full());
}
/**
@@ -580,33 +443,28 @@
* @param p point to check
* @return true if p belongs to the plane
*/
+ @Override
public boolean contains(final Vector3D p) {
- return precision.eqZero(getOffset(p));
+ return getPrecision().eqZero(offset(p));
}
/**
- * Check if the instance contains a line
+ * Check if the instance contains a line.
* @param line line to check
* @return true if line is contained in this plane
*/
- public boolean contains(final Line line) {
- Vector3D origin = line.getOrigin();
- Vector3D direction = line.getDirection();
- return contains(origin) &&
- areCoplanar(u, v, direction, precision);
+ public boolean contains(final Line3D line) {
+ return isParallel(line) && contains(line.getOrigin());
}
- /** Check, if the line is parallel to the instance.
+ /** Check if the line is parallel to the instance.
* @param line line to check.
* @return true if the line is parallel to the instance, false otherwise.
*/
- public boolean isParallel(final Line line) {
- final Vector3D direction = line.getDirection();
- final double dot = w.dot(direction);
- if (precision.eqZero(dot)) {
- return true;
- }
- return false;
+ public boolean isParallel(final Line3D line) {
+ final double dot = w.dot(line.getDirection());
+
+ return getPrecision().eqZero(dot);
}
/** Check, if the plane is parallel to the instance.
@@ -614,82 +472,79 @@
* @return true if the plane is parallel to the instance, false otherwise.
*/
public boolean isParallel(final Plane plane) {
- return precision.eqZero(w.cross(plane.w).norm());
- }
-
-
- /**
- * Get the offset (oriented distance) of a parallel plane.
- * <p>
- * This method should be called only for parallel planes otherwise the result is
- * not meaningful.
- * </p>
- * <p>
- * The offset is 0 if both planes are the same, it is positive if the plane is
- * on the plus side of the instance and negative if it is on the minus side,
- * according to its natural orientation.
- * </p>
- *
- * @param plane plane to check
- * @return offset of the plane
- */
- public double getOffset(final Plane plane) {
- return originOffset + (sameOrientationAs(plane) ?
- -plane.originOffset :
- plane.originOffset);
+ return getPrecision().eqZero(w.cross(plane.w).norm());
}
/**
- * Returns the distance of the given line to the plane instance.
- * Returns 0.0, if the line is not parallel to the plane instance.
- * @param line to calculate the distance to the plane instance
- * @return the distance or 0.0, if the line is not parallel to the plane instance.
+ * Get the offset (oriented distance) of the given plane with respect to this instance. The value
+ * closest to zero is returned, which will always be zero if the planes are not parallel.
+ * @param plane plane to calculate the offset of
+ * @return the offset of the plane with respect to this instance or 0.0 if the planes
+ * are not parallel.
*/
- public double getOffset(final Line line) {
- if (!isParallel(line)) {
- return 0;
+ public double offset(final Plane plane) {
+ if (!isParallel(plane)) {
+ return 0.0;
}
- return getOffset(line.getOrigin());
+ return originOffset + (similarOrientation(plane) ? -plane.originOffset : plane.originOffset);
}
/**
- * Get the offset (oriented distance) of a point.
- * <p>
- * The offset is 0 if the point is on the underlying hyperplane, it is positive
- * if the point is on one particular side of the hyperplane, and it is negative
- * if the point is on the other side, according to the hyperplane natural
- * orientation.
- * </p>
- *
- * @param point point to check
- * @return offset of the point
+ * Get the offset (oriented distance) of the given line with respect to the plane. The value
+ * closest to zero is returned, which will always be zero if the line is not parallel to the plane.
+ * @param line line to calculate the offset of
+ * @return the offset of the line with respect to the plane or 0.0 if the line
+ * is not parallel to the plane.
*/
+ public double offset(final Line3D line) {
+ if (!isParallel(line)) {
+ return 0.0;
+ }
+ return offset(line.getOrigin());
+ }
+
+ /** {@inheritDoc} */
@Override
- public double getOffset(final Vector3D point) {
+ public double offset(final Vector3D point) {
return point.dot(w) + originOffset;
}
- /**
- * Check if the instance has the same orientation as another hyperplane.
- *
- * @param other other hyperplane to check against the instance
- * @return true if the instance and the other hyperplane have the same
- * orientation
- */
+ /** {@inheritDoc} */
@Override
- public boolean sameOrientationAs(final Hyperplane<Vector3D> other) {
+ public boolean similarOrientation(final Hyperplane<Vector3D> other) {
return (((Plane) other).w).dot(w) > 0;
}
+
+ /** {@inheritDoc}
+ *
+ * <p>Instances are considered equivalent if they
+ * <ul>
+ * <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
+ * <li>have equivalent origins (as evaluated by the precision context), and</li>
+ * <li>have equivalent {@code u} and {@code v} vectors (as evaluated by the precision context)</li>
+ * </ul>
+ * @param other the point to compare with
+ * @return true if this instance should be considered equivalent to the argument
+ */
@Override
- public String toString() {
- return "Plane [u=" + u + ", v=" + v + ", w=" + w + "]";
+ public boolean eq(Plane other) {
+ if (this == other) {
+ return true;
+ }
+
+ final DoublePrecisionContext precision = getPrecision();
+
+ return precision.equals(other.getPrecision()) &&
+ getOrigin().eq(other.getOrigin(), precision) &&
+ u.eq(other.u, precision) &&
+ v.eq(other.v, precision);
}
/** {@inheritDoc} */
@Override
public int hashCode() {
- return Objects.hash(originOffset, u, v, w);
+ return Objects.hash(u, v, w, originOffset, getPrecision());
}
/** {@inheritDoc} */
@@ -697,27 +552,238 @@
public boolean equals(Object obj) {
if (this == obj) {
return true;
- }
- if (obj == null) {
+ } else if (!(obj instanceof Plane)) {
return false;
}
- if (getClass() != obj.getClass()) {
- return false;
- }
+
Plane other = (Plane) obj;
- return Double.doubleToLongBits(originOffset) == Double.doubleToLongBits(other.originOffset) &&
- Objects.equals(u, other.u) && Objects.equals(v, other.v) && Objects.equals(w, other.w);
+
+ return Objects.equals(this.u, other.u) &&
+ Objects.equals(this.v, other.v) &&
+ Objects.equals(this.w, other.w) &&
+ Double.compare(this.originOffset, other.originOffset) == 0 &&
+ Objects.equals(this.getPrecision(), other.getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName())
+ .append("[origin= ")
+ .append(getOrigin())
+ .append(", u= ")
+ .append(u)
+ .append(", v= ")
+ .append(v)
+ .append(", w= ")
+ .append(w)
+ .append(']');
+
+ return sb.toString();
}
/**
- * Check if provided vectors are coplanar.
- * @param u first vector
- * @param v second vector
- * @param w third vector
+ * Build a plane from a point and two (on plane) vectors.
+ * @param p the provided point (on plane)
+ * @param u u vector (on plane)
+ * @param v v vector (on plane)
* @param precision precision context used to compare floating point values
- * @return true if vectors are coplanar, false otherwise.
+ * @return a new plane
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given
+ * values is zero, NaN, or infinite.
*/
- private static boolean areCoplanar(final Vector3D u, final Vector3D v, final Vector3D w, final DoublePrecisionContext precision) {
- return precision.eqZero(u.dot(v.cross(w)));
+ public static Plane fromPointAndPlaneVectors(final Vector3D p, final Vector3D u, final Vector3D v,
+ final DoublePrecisionContext precision) {
+ Vector3D uNorm = u.normalize();
+ Vector3D vNorm = uNorm.orthogonal(v);
+ Vector3D wNorm = uNorm.cross(vNorm).normalize();
+ double originOffset = -p.dot(wNorm);
+
+ return new Plane(uNorm, vNorm, wNorm, originOffset, precision);
+ }
+
+ /**
+ * Build a plane from a normal.
+ * Chooses origin as point on plane.
+ * @param normal normal direction to the plane
+ * @param precision precision context used to compare floating point values
+ * @return a new plane
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given
+ * values is zero, NaN, or infinite.
+ */
+ public static Plane fromNormal(final Vector3D normal, final DoublePrecisionContext precision) {
+ return fromPointAndNormal(Vector3D.ZERO, normal, precision);
+ }
+
+ /**
+ * Build a plane from a point and a normal.
+ *
+ * @param p point belonging to the plane
+ * @param normal normal direction to the plane
+ * @param precision precision context used to compare floating point values
+ * @return a new plane
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given
+ * values is zero, NaN, or infinite.
+ */
+ public static Plane fromPointAndNormal(final Vector3D p, final Vector3D normal,
+ final DoublePrecisionContext precision) {
+ Vector3D w = normal.normalize();
+ double originOffset = -p.dot(w);
+
+ Vector3D u = w.orthogonal();
+ Vector3D v = w.cross(u);
+
+ return new Plane(u, v, w, originOffset, precision);
+ }
+
+ /**
+ * Build a plane from three points.
+ * <p>
+ * The plane is oriented in the direction of {@code (p2-p1) ^ (p3-p1)}
+ * </p>
+ *
+ * @param p1 first point belonging to the plane
+ * @param p2 second point belonging to the plane
+ * @param p3 third point belonging to the plane
+ * @param precision precision context used to compare floating point values
+ * @return a new plane
+ * @throws GeometryException if the points do not define a unique plane
+ */
+ public static Plane fromPoints(final Vector3D p1, final Vector3D p2, final Vector3D p3,
+ final DoublePrecisionContext precision) {
+ return Plane.fromPoints(Arrays.asList(p1, p2, p3), precision);
+ }
+
+ /** Construct a plane from a collection of points lying on the plane. The plane orientation is
+ * determined by the overall orientation of the point sequence. For example, if the points wind
+ * around the z-axis in a counter-clockwise direction, then the plane normal will point up the
+ * +z axis. If the points wind in the opposite direction, then the plane normal will point down
+ * the -z axis. The {@code u} vector for the plane is set to the first non-zero vector between
+ * points in the sequence (ie, the first direction in the path).
+ *
+ * @param pts collection of sequenced points lying on the plane
+ * @param precision precision context used to compare floating point values
+ * @return a new plane containing the given points
+ * @throws IllegalArgumentException if the given collection does not contain at least 3 points
+ * @throws GeometryException if the points do not define a unique plane
+ */
+ public static Plane fromPoints(final Collection<Vector3D> pts, final DoublePrecisionContext precision) {
+
+ if (pts.size() < 3) {
+ throw new IllegalArgumentException("At least 3 points are required to define a plane; " +
+ "argument contains only " + pts.size() + ".");
+ }
+
+ final Iterator<Vector3D> it = pts.iterator();
+
+ Vector3D startPt = it.next();
+
+ Vector3D u = null;
+ Vector3D w = null;
+
+ Vector3D currentPt;
+ Vector3D prevPt = startPt;
+
+ Vector3D currentVector = null;
+ Vector3D prevVector = null;
+
+ Vector3D cross = null;
+ double crossNorm;
+ double crossSumX = 0.0;
+ double crossSumY = 0.0;
+ double crossSumZ = 0.0;
+
+ boolean nonPlanar = false;
+
+ while (it.hasNext()) {
+ currentPt = it.next();
+
+ if (!currentPt.eq(prevPt, precision)) {
+ currentVector = startPt.vectorTo(currentPt);
+
+ if (u == null) {
+ // save the first non-zero vector as our u vector
+ u = currentVector.normalize();
+ }
+ if (prevVector != null) {
+ cross = prevVector.cross(currentVector);
+
+ crossSumX += cross.getX();
+ crossSumY += cross.getY();
+ crossSumZ += cross.getZ();
+
+ crossNorm = cross.norm();
+
+ if (!precision.eqZero(crossNorm)) {
+ // the cross product has non-zero magnitude
+ if (w == null) {
+ // save the first non-zero cross product as our normal
+ w = cross.normalize();
+ } else if (!precision.eq(1.0, Math.abs(w.dot(cross) / crossNorm))) {
+ // if the normalized dot product is not either +1 or -1, then
+ // the points are not coplanar
+ nonPlanar = true;
+ break;
+ }
+ }
+ }
+
+ prevVector = currentVector;
+ prevPt = currentPt;
+ }
+ }
+
+ if (u == null || w == null || nonPlanar) {
+ throw new GeometryException("Points do not define a plane: " + pts);
+ }
+
+ if (w.dot(Vector3D.of(crossSumX, crossSumY, crossSumZ)) < 0) {
+ w = w.negate();
+ }
+
+ final Vector3D v = w.cross(u);
+ final double originOffset = -startPt.dot(w);
+
+ return new Plane(u, v, w, originOffset, precision);
+ }
+
+ /** Class containing a transformed plane instance along with a subspace (2D) transform. The subspace
+ * transform produces the equivalent of the 3D transform in 2D.
+ */
+ public static final class SubspaceTransform implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190807L;
+
+ /** The transformed plane. */
+ private final Plane plane;
+
+ /** The subspace transform instance. */
+ private final AffineTransformMatrix2D transform;
+
+ /** Simple constructor.
+ * @param plane the transformed plane
+ * @param transform 2D transform that can be applied to subspace points
+ */
+ public SubspaceTransform(final Plane plane, final AffineTransformMatrix2D transform) {
+ this.plane = plane;
+ this.transform = transform;
+ }
+
+ /** Get the transformed plane instance.
+ * @return the transformed plane instance
+ */
+ public Plane getPlane() {
+ return plane;
+ }
+
+ /** Get the 2D transform that can be applied to subspace points. This transform can be used
+ * to perform the equivalent of the 3D transform in 2D space.
+ * @return the subspace transform instance
+ */
+ public AffineTransformMatrix2D getTransform() {
+ return transform;
+ }
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java
deleted file mode 100644
index bea93e8..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java
+++ /dev/null
@@ -1,704 +0,0 @@
-/*
- * 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.threed;
-
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.partitioning.AbstractRegion;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
-import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.partitioning.Transform;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
-import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
-import org.apache.commons.geometry.euclidean.twod.SubLine;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
-
-/** This class represents a 3D region: a set of polyhedrons.
- */
-public class PolyhedronsSet extends AbstractRegion<Vector3D, Vector2D> {
-
- /** Build a polyhedrons set representing the whole real line.
- * @param precision precision context used to compare floating point values
- */
- public PolyhedronsSet(final DoublePrecisionContext precision) {
- super(precision);
- }
-
- /** Build a polyhedrons set from a BSP tree.
- * <p>The leaf nodes of the BSP tree <em>must</em> have a
- * {@code Boolean} attribute representing the inside status of
- * the corresponding cell (true for inside cells, false for outside
- * cells). In order to avoid building too many small objects, it is
- * recommended to use the predefined constants
- * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
- * <p>
- * This constructor is aimed at expert use, as building the tree may
- * be a difficult task. It is not intended for general use and for
- * performances reasons does not check thoroughly its input, as this would
- * require walking the full tree each time. Failing to provide a tree with
- * the proper attributes, <em>will</em> therefore generate problems like
- * {@link NullPointerException} or {@link ClassCastException} only later on.
- * This limitation is known and explains why this constructor is for expert
- * use only. The caller does have the responsibility to provided correct arguments.
- * </p>
- * @param tree inside/outside BSP tree representing the region
- * @param precision precision context used to compare floating point values
- */
- public PolyhedronsSet(final BSPTree<Vector3D> tree, final DoublePrecisionContext precision) {
- super(tree, precision);
- }
-
- /** Build a polyhedrons set from a Boundary REPresentation (B-rep) specified by sub-hyperplanes.
- * <p>The boundary is provided as a collection of {@link
- * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
- * interior part of the region on its minus side and the exterior on
- * its plus side.</p>
- * <p>The boundary elements can be in any order, and can form
- * several non-connected sets (like for example polyhedrons with holes
- * or a set of disjoint polyhedrons considered as a whole). In
- * fact, the elements do not even need to be connected together
- * (their topological connections are not used here). However, if the
- * boundary does not really separate an inside open from an outside
- * open (open having here its topological meaning), then subsequent
- * calls to the {@link Region#checkPoint(Point) checkPoint} method will
- * not be meaningful anymore.</p>
- * <p>If the boundary is empty, the region will represent the whole
- * space.</p>
- * @param boundary collection of boundary elements, as a
- * collection of {@link SubHyperplane SubHyperplane} objects
- * @param precision precision context used to compare floating point values
- */
- public PolyhedronsSet(final Collection<SubHyperplane<Vector3D>> boundary,
- final DoublePrecisionContext precision) {
- super(boundary, precision);
- }
-
- /** Build a polyhedrons set from a Boundary REPresentation (B-rep) specified by connected vertices.
- * <p>
- * The boundary is provided as a list of vertices and a list of facets.
- * Each facet is specified as an integer array containing the arrays vertices
- * indices in the vertices list. Each facet normal is oriented by right hand
- * rule to the facet vertices list.
- * </p>
- * <p>
- * Some basic sanity checks are performed but not everything is thoroughly
- * assessed, so it remains under caller responsibility to ensure the vertices
- * and facets are consistent and properly define a polyhedrons set.
- * </p>
- * @param vertices list of polyhedrons set vertices
- * @param facets list of facets, as vertices indices in the vertices list
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if some basic sanity checks fail
- */
- public PolyhedronsSet(final List<Vector3D> vertices, final List<int[]> facets,
- final DoublePrecisionContext precision) {
- super(buildBoundary(vertices, facets, precision), precision);
- }
-
- /** Build a parallellepipedic box.
- * @param xMin low bound along the x direction
- * @param xMax high bound along the x direction
- * @param yMin low bound along the y direction
- * @param yMax high bound along the y direction
- * @param zMin low bound along the z direction
- * @param zMax high bound along the z direction
- * @param precision precision context used to compare floating point values
- */
- public PolyhedronsSet(final double xMin, final double xMax,
- final double yMin, final double yMax,
- final double zMin, final double zMax,
- final DoublePrecisionContext precision) {
- super(buildBoundary(xMin, xMax, yMin, yMax, zMin, zMax, precision), precision);
- }
-
- /** Build a parallellepipedic box boundary.
- * @param xMin low bound along the x direction
- * @param xMax high bound along the x direction
- * @param yMin low bound along the y direction
- * @param yMax high bound along the y direction
- * @param zMin low bound along the z direction
- * @param zMax high bound along the z direction
- * @param precision precision context used to compare floating point values
- * @return boundary tree
- */
- private static BSPTree<Vector3D> buildBoundary(final double xMin, final double xMax,
- final double yMin, final double yMax,
- final double zMin, final double zMax,
- final DoublePrecisionContext precision) {
- if (precision.eq(xMin, xMax) ||
- precision.eq(yMin, yMax) ||
- precision.eq(zMin, zMax)) {
- // too thin box, build an empty polygons set
- return new BSPTree<>(Boolean.FALSE);
- }
- final Plane pxMin = Plane.fromPointAndNormal(Vector3D.of(xMin, 0, 0), Vector3D.Unit.MINUS_X, precision);
- final Plane pxMax = Plane.fromPointAndNormal(Vector3D.of(xMax, 0, 0), Vector3D.Unit.PLUS_X, precision);
- final Plane pyMin = Plane.fromPointAndNormal(Vector3D.of(0, yMin, 0), Vector3D.Unit.MINUS_Y, precision);
- final Plane pyMax = Plane.fromPointAndNormal(Vector3D.of(0, yMax, 0), Vector3D.Unit.PLUS_Y, precision);
- final Plane pzMin = Plane.fromPointAndNormal(Vector3D.of(0, 0, zMin), Vector3D.Unit.MINUS_Z, precision);
- final Plane pzMax = Plane.fromPointAndNormal(Vector3D.of(0, 0, zMax), Vector3D.Unit.PLUS_Z, precision);
- final Region<Vector3D> boundary =
- new RegionFactory<Vector3D>().buildConvex(pxMin, pxMax, pyMin, pyMax, pzMin, pzMax);
- return boundary.getTree(false);
- }
-
- /** Build boundary from vertices and facets.
- * @param vertices list of polyhedrons set vertices
- * @param facets list of facets, as vertices indices in the vertices list
- * @param precision precision context used to compare floating point values
- * @return boundary as a list of sub-hyperplanes
- * @exception IllegalArgumentException if some basic sanity checks fail
- */
- private static List<SubHyperplane<Vector3D>> buildBoundary(final List<Vector3D> vertices,
- final List<int[]> facets,
- final DoublePrecisionContext precision) {
-
- // check vertices distances
- for (int i = 0; i < vertices.size() - 1; ++i) {
- final Vector3D vi = vertices.get(i);
- for (int j = i + 1; j < vertices.size(); ++j) {
- if (precision.eqZero(vi.distance(vertices.get(j)))) {
- throw new IllegalArgumentException("Vertices are too close near point " + vi);
- }
- }
- }
-
- // find how vertices are referenced by facets
- final int[][] references = findReferences(vertices, facets);
-
- // find how vertices are linked together by edges along the facets they belong to
- final int[][] successors = successors(vertices, facets, references);
-
- // check edges orientations
- for (int vA = 0; vA < vertices.size(); ++vA) {
- for (final int vB : successors[vA]) {
-
- if (vB >= 0) {
- // when facets are properly oriented, if vB is the successor of vA on facet f1,
- // then there must be an adjacent facet f2 where vA is the successor of vB
- boolean found = false;
- for (final int v : successors[vB]) {
- found = found || (v == vA);
- }
- if (!found) {
- final Vector3D start = vertices.get(vA);
- final Vector3D end = vertices.get(vB);
- throw new IllegalArgumentException(MessageFormat.format("Edge joining points {0} and {1} is connected to one facet only", start, end));
- }
- }
- }
- }
-
- final List<SubHyperplane<Vector3D>> boundary = new ArrayList<>();
- final List<Vector3D> facetVertices = new ArrayList<>();
-
- for (final int[] facet : facets) {
-
- for (int i = 0; i < facet.length; ++i) {
- facetVertices.add(vertices.get(facet[i]));
- }
-
- Plane plane = Plane.fromPoints(facetVertices, precision);
-
- // convert to subspace points
- final Vector2D[] two2Points = new Vector2D[facet.length];
- for (int i = 0 ; i < facet.length; ++i) {
- two2Points[i] = plane.toSubSpace(facetVertices.get(i));
- }
-
- // create the polygonal facet
- boundary.add(new SubPlane(plane, new PolygonsSet(precision, two2Points)));
-
- facetVertices.clear();
- }
-
- return boundary;
- }
-
- /** Find the facets that reference each edges.
- * @param vertices list of polyhedrons set vertices
- * @param facets list of facets, as vertices indices in the vertices list
- * @return references array such that r[v][k] = f for some k if facet f contains vertex v
- * @exception IllegalArgumentException if some facets have fewer than 3 vertices
- */
- private static int[][] findReferences(final List<Vector3D> vertices, final List<int[]> facets) {
-
- // find the maximum number of facets a vertex belongs to
- final int[] nbFacets = new int[vertices.size()];
- int maxFacets = 0;
- for (final int[] facet : facets) {
- if (facet.length < 3) {
- throw new IllegalArgumentException("3 points are required, got only " + facet.length);
- }
- for (final int index : facet) {
- maxFacets = Math.max(maxFacets, ++nbFacets[index]);
- }
- }
-
- // set up the references array
- final int[][] references = new int[vertices.size()][maxFacets];
- for (int[] r : references) {
- Arrays.fill(r, -1);
- }
- for (int f = 0; f < facets.size(); ++f) {
- for (final int v : facets.get(f)) {
- // vertex v is referenced by facet f
- int k = 0;
- while (k < maxFacets &&
- references[v][k] >= 0) {
- ++k;
- }
- references[v][k] = f;
- }
- }
-
- return references;
- }
-
- /** Find the successors of all vertices among all facets they belong to.
- * @param vertices list of polyhedrons set vertices
- * @param facets list of facets, as vertices indices in the vertices list
- * @param references facets references array
- * @return indices of vertices that follow vertex v in some facet (the array
- * may contain extra entries at the end, set to negative indices)
- * @exception IllegalArgumentException if the same vertex appears more than
- * once in the successors list (which means one facet orientation is wrong)
-
- */
- private static int[][] successors(final List<Vector3D> vertices, final List<int[]> facets,
- final int[][] references) {
-
- // create an array large enough
- final int[][] successors = new int[vertices.size()][references[0].length];
- for (final int[] s : successors) {
- Arrays.fill(s, -1);
- }
-
- for (int v = 0; v < vertices.size(); ++v) {
- for (int k = 0; k < successors[v].length && references[v][k] >= 0; ++k) {
-
- // look for vertex v
- final int[] facet = facets.get(references[v][k]);
- int i = 0;
- while (i < facet.length &&
- facet[i] != v) {
- ++i;
- }
-
- // we have found vertex v, we deduce its successor on current facet
- successors[v][k] = facet[(i + 1) % facet.length];
- for (int l = 0; l < k; ++l) {
- if (successors[v][l] == successors[v][k]) {
- final Vector3D start = vertices.get(v);
- final Vector3D end = vertices.get(successors[v][k]);
- throw new IllegalArgumentException(MessageFormat.format("Facet orientation mismatch around edge joining points {0} and {1}", start, end));
- }
- }
- }
- }
-
- return successors;
-
- }
-
- /** {@inheritDoc} */
- @Override
- public PolyhedronsSet buildNew(final BSPTree<Vector3D> tree) {
- return new PolyhedronsSet(tree, getPrecision());
- }
-
- /** {@inheritDoc} */
- @Override
- protected void computeGeometricalProperties() {
- // check simple cases first
- if (isEmpty()) {
- setSize(0);
- setBarycenter(Vector3D.NaN);
- }
- else if (isFull()) {
- setSize(Double.POSITIVE_INFINITY);
- setBarycenter(Vector3D.NaN);
- }
- else {
- // not empty or full; compute the contribution of all boundary facets
- final FacetsContributionVisitor contributionVisitor = new FacetsContributionVisitor();
- getTree(true).visit(contributionVisitor);
-
- final double size = contributionVisitor.getSize();
- final Vector3D barycenter = contributionVisitor.getBarycenter();
-
- if (size < 0) {
- // the polyhedrons set is a finite outside surrounded by an infinite inside
- setSize(Double.POSITIVE_INFINITY);
- setBarycenter(Vector3D.NaN);
- } else {
- // the polyhedrons set is finite
- setSize(size);
- setBarycenter(barycenter);
- }
- }
- }
-
- /** Visitor computing polyhedron geometrical properties.
- * The volume of the polyhedron is computed using the equation
- * <code>V = (1/3)*Σ<sub>F</sub>[(C<sub>F</sub>⋅N<sub>F</sub>)*area(F)]</code>,
- * where <code>F</code> represents each face in the polyhedron, <code>C<sub>F</sub></code>
- * represents the barycenter of the face, and <code>N<sub>F</sub></code> represents the
- * normal of the face. (More details can be found in the article
- * <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume">here</a>.)
- * This essentially splits up the polyhedron into pyramids with a polyhedron
- * face forming the base of each pyramid.
- * The barycenter is computed in a similar way. The barycenter of each pyramid
- * is calculated using the fact that it is located 3/4 of the way along the
- * line from the apex to the base. The polyhedron barycenter then becomes
- * the volume-weighted average of these pyramid centers.
- */
- private static class FacetsContributionVisitor implements BSPTreeVisitor<Vector3D> {
-
- /** Accumulator for facet volume contributions. */
- private double volumeSum;
-
- /** Accumulator for barycenter contributions. */
- private Vector3D barycenterSum = Vector3D.ZERO;
-
- /** Returns the total computed size (ie, volume) of the polyhedron.
- * This value will be negative if the polyhedron is "inside-out", meaning
- * that it has a finite outside surrounded by an infinite inside.
- * @return the volume.
- */
- public double getSize() {
- // apply the 1/3 pyramid volume scaling factor
- return volumeSum / 3;
- }
-
- /** Returns the computed barycenter. This is the volume-weighted average
- * of contributions from all facets. All coordinates will be NaN if the
- * region is infinite.
- * @return the barycenter.
- */
- public Vector3D getBarycenter() {
- // Since the volume we used when adding together the facet contributions
- // was 3x the actual pyramid size, we'll multiply by 1/4 here instead
- // of 3/4 to adjust for the actual barycenter position in each pyramid.
- return Vector3D.linearCombination(1.0 / (4 * getSize()), barycenterSum);
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<Vector3D> node) {
- return Order.MINUS_SUB_PLUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<Vector3D> node) {
- @SuppressWarnings("unchecked")
- final BoundaryAttribute<Vector3D> attribute =
- (BoundaryAttribute<Vector3D>) node.getAttribute();
- if (attribute.getPlusOutside() != null) {
- addContribution(attribute.getPlusOutside(), false);
- }
- if (attribute.getPlusInside() != null) {
- addContribution(attribute.getPlusInside(), true);
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<Vector3D> node) {}
-
- /** Add the contribution of a boundary facet.
- * @param facet boundary facet
- * @param reversed if true, the facet has the inside on its plus side
- */
- private void addContribution(final SubHyperplane<Vector3D> facet, final boolean reversed) {
-
- final Region<Vector2D> polygon = ((SubPlane) facet).getRemainingRegion();
- final double area = polygon.getSize();
-
- if (Double.isInfinite(area)) {
- volumeSum = Double.POSITIVE_INFINITY;
- barycenterSum = Vector3D.NaN;
- } else {
- final Plane plane = (Plane) facet.getHyperplane();
- final Vector3D facetBarycenter = plane.toSpace(polygon.getBarycenter());
-
- // the volume here is actually 3x the actual pyramid volume; we'll apply
- // the final scaling all at once at the end
- double scaledVolume = area * facetBarycenter.dot(plane.getNormal());
- if (reversed) {
- scaledVolume = -scaledVolume;
- }
-
- volumeSum += scaledVolume;
- barycenterSum = Vector3D.linearCombination(1.0, barycenterSum, scaledVolume, facetBarycenter);
- }
- }
- }
-
- /** Get the first sub-hyperplane crossed by a semi-infinite line.
- * @param point start point of the part of the line considered
- * @param line line to consider (contains point)
- * @return the first sub-hyperplane crossed by the line after the
- * given point, or null if the line does not intersect any
- * sub-hyperplane
- */
- public SubHyperplane<Vector3D> firstIntersection(final Vector3D point, final Line line) {
- return recurseFirstIntersection(getTree(true), point, line);
- }
-
- /** Get the first sub-hyperplane crossed by a semi-infinite line.
- * @param node current node
- * @param point start point of the part of the line considered
- * @param line line to consider (contains point)
- * @return the first sub-hyperplane crossed by the line after the
- * given point, or null if the line does not intersect any
- * sub-hyperplane
- */
- private SubHyperplane<Vector3D> recurseFirstIntersection(final BSPTree<Vector3D> node,
- final Vector3D point,
- final Line line) {
-
- final SubHyperplane<Vector3D> cut = node.getCut();
- if (cut == null) {
- return null;
- }
- final BSPTree<Vector3D> minus = node.getMinus();
- final BSPTree<Vector3D> plus = node.getPlus();
- final Plane plane = (Plane) cut.getHyperplane();
-
- // establish search order
- final double offset = plane.getOffset(point);
- final boolean in = getPrecision().eqZero(Math.abs(offset));
- final BSPTree<Vector3D> near;
- final BSPTree<Vector3D> far;
- if (offset < 0) {
- near = minus;
- far = plus;
- } else {
- near = plus;
- far = minus;
- }
-
- if (in) {
- // search in the cut hyperplane
- final SubHyperplane<Vector3D> facet = boundaryFacet(point, node);
-
- // only return the facet here if it exists and intersects the plane
- // (ie, is not parallel it)
- if (facet != null &&
- plane.intersection(line) != null) {
- return facet;
- }
- }
-
- // search in the near branch
- final SubHyperplane<Vector3D> crossed = recurseFirstIntersection(near, point, line);
- if (crossed != null) {
- return crossed;
- }
-
- if (!in) {
- // search in the cut hyperplane
- final Vector3D hit3D = plane.intersection(line);
- if (hit3D != null &&
- line.getAbscissa(hit3D) > line.getAbscissa(point)) {
- final SubHyperplane<Vector3D> facet = boundaryFacet(hit3D, node);
- if (facet != null) {
- return facet;
- }
- }
- }
-
- // search in the far branch
- return recurseFirstIntersection(far, point, line);
-
- }
-
- /** Check if a point belongs to the boundary part of a node.
- * @param point point to check
- * @param node node containing the boundary facet to check
- * @return the boundary facet this points belongs to (or null if it
- * does not belong to any boundary facet)
- */
- private SubHyperplane<Vector3D> boundaryFacet(final Vector3D point,
- final BSPTree<Vector3D> node) {
- final Vector2D Vector2D = ((Plane) node.getCut().getHyperplane()).toSubSpace(point);
- @SuppressWarnings("unchecked")
- final BoundaryAttribute<Vector3D> attribute =
- (BoundaryAttribute<Vector3D>) node.getAttribute();
- if ((attribute.getPlusOutside() != null) &&
- (((SubPlane) attribute.getPlusOutside()).getRemainingRegion().checkPoint(Vector2D) != Location.OUTSIDE)) {
- return attribute.getPlusOutside();
- }
- if ((attribute.getPlusInside() != null) &&
- (((SubPlane) attribute.getPlusInside()).getRemainingRegion().checkPoint(Vector2D) != Location.OUTSIDE)) {
- return attribute.getPlusInside();
- }
- return null;
- }
-
- /** Rotate the region around the specified point.
- * <p>The instance is not modified, a new instance is created.</p>
- * @param center rotation center
- * @param rotation 3-dimensional rotation
- * @return a new instance representing the rotated region
- */
- public PolyhedronsSet rotate(final Vector3D center, final QuaternionRotation rotation) {
- return (PolyhedronsSet) applyTransform(new RotationTransform(center, rotation));
- }
-
- /** 3D rotation as a Transform. */
- private static class RotationTransform implements Transform<Vector3D, Vector2D> {
-
- /** Center point of the rotation. */
- private final Vector3D center;
-
- /** Quaternion rotation. */
- private final QuaternionRotation rotation;
-
- /** Cached original hyperplane. */
- private Plane cachedOriginal;
-
- /** Cached 2D transform valid inside the cached original hyperplane. */
- private Transform<Vector2D, Vector1D> cachedTransform;
-
- /** Build a rotation transform.
- * @param center center point of the rotation
- * @param rotation vectorial rotation
- */
- RotationTransform(final Vector3D center, final QuaternionRotation rotation) {
- this.center = center;
- this.rotation = rotation;
- }
-
- /** {@inheritDoc} */
- @Override
- public Vector3D apply(final Vector3D point) {
- final Vector3D delta = point.subtract(center);
- return Vector3D.linearCombination(1.0, center, 1.0, rotation.apply(delta));
- }
-
- /** {@inheritDoc} */
- @Override
- public Plane apply(final Hyperplane<Vector3D> hyperplane) {
- return ((Plane) hyperplane).rotate(center, rotation);
- }
-
- /** {@inheritDoc} */
- @Override
- public SubHyperplane<Vector2D> apply(final SubHyperplane<Vector2D> sub,
- final Hyperplane<Vector3D> original,
- final Hyperplane<Vector3D> transformed) {
- if (original != cachedOriginal) {
- // we have changed hyperplane, reset the in-hyperplane transform
-
- final Plane oPlane = (Plane) original;
- final Plane tPlane = (Plane) transformed;
- final Vector3D p00 = oPlane.getOrigin();
- final Vector3D p10 = oPlane.toSpace(Vector2D.of(1.0, 0.0));
- final Vector3D p01 = oPlane.toSpace(Vector2D.of(0.0, 1.0));
- final Vector2D tP00 = tPlane.toSubSpace(apply(p00));
- final Vector2D tP10 = tPlane.toSubSpace(apply(p10));
- final Vector2D tP01 = tPlane.toSubSpace(apply(p01));
-
- cachedOriginal = (Plane) original;
- cachedTransform =
- org.apache.commons.geometry.euclidean.twod.Line.getTransform(tP00.vectorTo(tP10),
- tP00.vectorTo(tP01),
- tP00);
- }
- return ((SubLine) sub).applyTransform(cachedTransform);
- }
-
- }
-
- /** Translate the region by the specified amount.
- * <p>The instance is not modified, a new instance is created.</p>
- * @param translation translation to apply
- * @return a new instance representing the translated region
- */
- public PolyhedronsSet translate(final Vector3D translation) {
- return (PolyhedronsSet) applyTransform(new TranslationTransform(translation));
- }
-
- /** 3D translation as a transform. */
- private static class TranslationTransform implements Transform<Vector3D, Vector2D> {
-
- /** Translation vector. */
- private final Vector3D translation;
-
- /** Cached original hyperplane. */
- private Plane cachedOriginal;
-
- /** Cached 2D transform valid inside the cached original hyperplane. */
- private Transform<Vector2D, Vector1D> cachedTransform;
-
- /** Build a translation transform.
- * @param translation translation vector
- */
- TranslationTransform(final Vector3D translation) {
- this.translation = translation;
- }
-
- /** {@inheritDoc} */
- @Override
- public Vector3D apply(final Vector3D point) {
- return Vector3D.linearCombination(1.0, point, 1.0, translation);
- }
-
- /** {@inheritDoc} */
- @Override
- public Plane apply(final Hyperplane<Vector3D> hyperplane) {
- return ((Plane) hyperplane).translate(translation);
- }
-
- /** {@inheritDoc} */
- @Override
- public SubHyperplane<Vector2D> apply(final SubHyperplane<Vector2D> sub,
- final Hyperplane<Vector3D> original,
- final Hyperplane<Vector3D> transformed) {
- if (original != cachedOriginal) {
- // we have changed hyperplane, reset the in-hyperplane transform
-
- final Plane oPlane = (Plane) original;
- final Plane tPlane = (Plane) transformed;
- final Vector2D shift = tPlane.toSubSpace(apply(oPlane.getOrigin()));
-
- cachedOriginal = (Plane) original;
- cachedTransform =
- org.apache.commons.geometry.euclidean.twod.Line.getTransform(Vector2D.Unit.PLUS_X,
- Vector2D.Unit.PLUS_Y,
- shift);
- }
-
- return ((SubLine) sub).applyTransform(cachedTransform);
- }
- }
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
new file mode 100644
index 0000000..23e5792
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
@@ -0,0 +1,698 @@
+/*
+ * 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.threed;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
+import org.apache.commons.geometry.euclidean.twod.Polyline;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Binary space partitioning (BSP) tree representing a region in three dimensional
+ * Euclidean space.
+ */
+public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, RegionBSPTree3D.RegionNode3D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190702L;
+
+ /** Create a new, empty region. */
+ public RegionBSPTree3D() {
+ this(false);
+ }
+
+ /** Create a new region. If {@code full} is true, then the region will
+ * represent the entire 3D space. Otherwise, it will be empty.
+ * @param full whether or not the region should contain the entire
+ * 3D space or be empty
+ */
+ public RegionBSPTree3D(boolean full) {
+ super(full);
+ }
+
+ /** Return a deep copy of this instance.
+ * @return a deep copy of this instance.
+ * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
+ */
+ public RegionBSPTree3D copy() {
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.copy(this);
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable<ConvexSubPlane> boundaries() {
+ return createBoundaryIterable(b -> (ConvexSubPlane) b);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<ConvexSubPlane> getBoundaries() {
+ return createBoundaryList(b -> (ConvexSubPlane) b);
+ }
+
+ /** Return a list of {@link ConvexVolume}s representing the same region
+ * as this instance. One convex volume is returned for each interior leaf
+ * node in the tree.
+ * @return a list of convex volumes representing the same region as this
+ * instance
+ */
+ public List<ConvexVolume> toConvex() {
+ final List<ConvexVolume> result = new ArrayList<>();
+
+ toConvexRecursive(getRoot(), ConvexVolume.full(), result);
+
+ return result;
+ }
+
+ /** Recursive method to compute the convex volumes of all inside leaf nodes in the subtree rooted at the given
+ * node. The computed convex volumes are added to the given list.
+ * @param node root of the subtree to compute the convex volumes for
+ * @param nodeVolume the convex volume for the current node; this will be split by the node's cut hyperplane to
+ * form the convex volumes for any child nodes
+ * @param result list containing the results of the computation
+ */
+ private void toConvexRecursive(final RegionNode3D node, final ConvexVolume nodeVolume,
+ final List<ConvexVolume> result) {
+
+ if (node.isLeaf()) {
+ // base case; only add to the result list if the node is inside
+ if (node.isInside()) {
+ result.add(nodeVolume);
+ }
+ } else {
+ // recurse
+ Split<ConvexVolume> split = nodeVolume.split(node.getCutHyperplane());
+
+ toConvexRecursive(node.getMinus(), split.getMinus(), result);
+ toConvexRecursive(node.getPlus(), split.getPlus(), result);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<RegionBSPTree3D> split(final Hyperplane<Vector3D> splitter) {
+ return split(splitter, RegionBSPTree3D.empty(), RegionBSPTree3D.empty());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D project(Vector3D pt) {
+ // use our custom projector so that we can disambiguate points that are
+ // actually equidistant from the target point
+ final BoundaryProjector3D projector = new BoundaryProjector3D(pt);
+ accept(projector);
+
+ return projector.getProjected();
+ }
+
+ /** Find the first intersection of the given ray/line segment with the boundary of
+ * the region. The return value is the cut subhyperplane of the node containing the
+ * intersection. Null is returned if no intersection exists.
+ * @param ray ray to intersect with the region
+ * @return the node cut subhyperplane containing the intersection or null if no
+ * intersection exists
+ */
+ public ConvexSubPlane raycastFirst(final Segment3D ray) {
+ return raycastFirstRecursive(getRoot(), ray);
+ }
+
+ /** Recursive method used to find the first intersection of the given ray/line segment
+ * with the boundary of the region.
+ * @param node current BSP tree node
+ * @param ray the ray used for the raycast operation
+ * @return the node cut subhyperplane containing the intersection or null if no
+ * intersection exists
+ */
+ private ConvexSubPlane raycastFirstRecursive(final RegionNode3D node, final Segment3D ray) {
+ if (node.isLeaf()) {
+ // no boundary to intersect with on leaf nodes
+ return null;
+ }
+
+ // establish search order
+ final Plane cut = (Plane) node.getCutHyperplane();
+ final Line3D line = ray.getLine();
+
+ final boolean plusIsNear = line.getDirection().dot(cut.getNormal()) < 0;
+
+ final RegionNode3D nearNode = plusIsNear ? node.getPlus() : node.getMinus();
+ final RegionNode3D farNode = plusIsNear ? node.getMinus() : node.getPlus();
+
+ // check the near node
+ final ConvexSubPlane nearResult = raycastFirstRecursive(nearNode, ray);
+ if (nearResult != null) {
+ return nearResult;
+ }
+
+ // check ourselves
+ final Vector3D intersection = computeRegionCutBoundaryIntersection(node, ray);
+ if (intersection != null) {
+ // we intersect, so our cut is the answer
+ return (ConvexSubPlane) node.getCut();
+ }
+
+ // check the far node
+ final ConvexSubPlane farResult = raycastFirstRecursive(farNode, ray);
+ if (farResult != null) {
+ return farResult;
+ }
+
+ return null;
+ }
+
+ /** Compute the intersection point between the region cut boundary and the given line segment.
+ * @param node BSP tree node to compute the region cut boundary intersection for
+ * @param segment line segment to compute the intersection for
+ * @return the intersection point between the region cut boundary and the given line segment or
+ * null if one does not exist.
+ */
+ private Vector3D computeRegionCutBoundaryIntersection(final RegionNode3D node, final Segment3D segment) {
+ if (node.isInternal()) {
+ final Line3D line = segment.getLine();
+ final Vector3D intersection = ((Plane) node.getCutHyperplane()).intersection(line);
+
+ if (intersection != null && segment.contains(intersection)) {
+
+ final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
+
+ if ((boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(intersection)) ||
+ boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(intersection)) {
+
+ return intersection;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionSizeProperties<Vector3D> computeRegionSizeProperties() {
+ // handle simple cases
+ if (isFull()) {
+ return new RegionSizeProperties<>(Double.POSITIVE_INFINITY, null);
+ } else if (isEmpty()) {
+ return new RegionSizeProperties<>(0, null);
+ }
+
+ RegionSizePropertiesVisitor visitor = new RegionSizePropertiesVisitor();
+ accept(visitor);
+
+ return visitor.getRegionSizeProperties();
+ }
+
+ /** Compute the region represented by the given node.
+ * @param node the node to compute the region for
+ * @return the region represented by the given node
+ */
+ private ConvexVolume computeNodeRegion(final RegionNode3D node) {
+ ConvexVolume volume = ConvexVolume.full();
+
+ RegionNode3D child = node;
+ RegionNode3D parent;
+
+ while ((parent = child.getParent()) != null) {
+ Split<ConvexVolume> split = volume.split(parent.getCutHyperplane());
+
+ volume = child.isMinus() ? split.getMinus() : split.getPlus();
+
+ child = parent;
+ }
+
+ return volume;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode3D createNode() {
+ return new RegionNode3D(this);
+ }
+
+ /** Return a new instance containing all of 3D space.
+ * @return a new instance containing all of 3D space.
+ */
+ public static RegionBSPTree3D full() {
+ return new RegionBSPTree3D(true);
+ }
+
+ /** Return a new, empty instance. The represented region is completely empty.
+ * @return a new, empty instance.
+ */
+ public static RegionBSPTree3D empty() {
+ return new RegionBSPTree3D(false);
+ }
+
+ /** Create a new BSP tree instance representing the same region as the argument.
+ * @param volume convex volume instance
+ * @return a new BSP tree instance representing the same region as the argument
+ */
+ public static RegionBSPTree3D from(final ConvexVolume volume) {
+ RegionBSPTree3D tree = RegionBSPTree3D.full();
+ tree.insert(volume.getBoundaries());
+
+ return tree;
+ }
+
+ /** Create a new {@link RegionBSPTree3D.Builder} instance for creating BSP
+ * trees from boundary representations.
+ * @param precision precision context to use for floating point comparisons.
+ * @return a new builder instance
+ */
+ public static Builder builder(final DoublePrecisionContext precision) {
+ return new Builder(precision);
+ }
+
+ /** BSP tree node for three dimensional Euclidean space.
+ */
+ public static final class RegionNode3D extends AbstractRegionBSPTree.AbstractRegionNode<Vector3D, RegionNode3D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190702L;
+
+ /** Simple constructor.
+ * @param tree the owning tree instance
+ */
+ protected RegionNode3D(AbstractBSPTree<Vector3D, RegionNode3D> tree) {
+ super(tree);
+ }
+
+ /** Get the region represented by this node. The returned region contains
+ * the entire area contained in this node, regardless of the attributes of
+ * any child nodes.
+ * @return the region represented by this node
+ */
+ public ConvexVolume getNodeRegion() {
+ return ((RegionBSPTree3D) getTree()).computeNodeRegion(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode3D getSelf() {
+ return this;
+ }
+ }
+
+ /** Class used to construct {@link RegionBSPTree3D} instances from boundary representations.
+ */
+ public static final class Builder {
+
+ /** Precision object used to perform floating point comparisons. This object is
+ * used when constructing geometric types.
+ */
+ private final DoublePrecisionContext precision;
+
+ /** The BSP tree being constructed. */
+ private final RegionBSPTree3D tree = RegionBSPTree3D.empty();
+
+ /** List of vertices to use for indexed facet operations. */
+ private List<Vector3D> vertexList;
+
+ /** Create a new builder instance. The given precision context will be used when
+ * constructing geometric types.
+ * @param precision precision object used to perform floating point comparisons
+ */
+ public Builder(final DoublePrecisionContext precision) {
+ this.precision = precision;
+ }
+
+ /** Set the list of vertices to use for indexed facet operations.
+ * @param vertices array of vertices
+ * @return this builder instance
+ * @see #addIndexedFacet(int...)
+ * @see #addIndexedFacet(List)
+ * @see #addIndexedFacets(int[][])
+ */
+ public Builder withVertexList(final Vector3D... vertices) {
+ return withVertexList(Arrays.asList(vertices));
+ }
+
+ /** Set the list of vertices to use for indexed facet operations.
+ * @param vertices list of vertices
+ * @return this builder instance
+ * @see #addIndexedFacet(int...)
+ * @see #addIndexedFacet(List)
+ * @see #addIndexedFacets(int[][])
+ */
+ public Builder withVertexList(final List<Vector3D> vertices) {
+ this.vertexList = vertices;
+ return this;
+ }
+
+ /** Add a subplane to the tree.
+ * @param subplane subplane to add
+ * @return this builder instance
+ */
+ public Builder add(final SubPlane subplane) {
+ tree.insert(subplane);
+ return this;
+ }
+
+ /** Add a convex subplane to the tree.
+ * @param convex convex subplane to add
+ * @return this builder instance
+ */
+ public Builder add(final ConvexSubPlane convex) {
+ tree.insert(convex);
+ return this;
+ }
+
+ /** Add a facet defined by the given array of vertices. The vertices
+ * are considered to form a loop, even if the first vertex is not included
+ * again at the end of the array.
+ * @param vertices array of vertices defining the facet
+ * @return this builder instance
+ */
+ public Builder addFacet(final Vector3D... vertices) {
+ return addFacet(Arrays.asList(vertices));
+ }
+
+ /** Add a facet defined by the given list of vertices. The vertices
+ * are considered to form a loop, even if the first vertex is not included
+ * again at the end of the list.
+ * @param vertices list of vertices defining the facet
+ * @return this builder instance
+ */
+ public Builder addFacet(final List<Vector3D> vertices) {
+ // if there are only 3 vertices, then we know for certain that the area is convex
+ if (vertices.size() < 4) {
+ return add(ConvexSubPlane.fromVertexLoop(vertices, precision));
+ }
+
+ final Plane plane = Plane.fromPoints(vertices, precision);
+ final List<Vector2D> subspaceVertices = plane.toSubspace(vertices);
+
+ final Polyline path = Polyline.fromVertexLoop(subspaceVertices, precision);
+ return add(new SubPlane(plane, path.toTree()));
+ }
+
+ /** Add multiple facets, each one defined by an array of indices into the current
+ * vertex list.
+ * @param facets array of facet definitions, where each definition consists of an
+ * array of indices into the current vertex list
+ * @return this builder instance
+ * @see #withVertexList(List)
+ */
+ public Builder addIndexedFacets(int[][] facets) {
+ for (int[] facet : facets) {
+ addIndexedFacet(facet);
+ }
+
+ return this;
+ }
+
+ /** Add a facet defined by an array of indices into the current vertex list.
+ * @param vertexIndices indices into the current vertex list, defining the vertices
+ * for the facet
+ * @return this builder instance
+ * @see #withVertexList(List)
+ */
+ public Builder addIndexedFacet(int... vertexIndices) {
+ final Vector3D[] vertices = new Vector3D[vertexIndices.length];
+ for (int i = 0; i < vertexIndices.length; ++i) {
+ vertices[i] = vertexList.get(vertexIndices[i]);
+ }
+
+ return addFacet(vertices);
+ }
+
+ /** Add a facet defined by a list of indices into the current vertex list.
+ * @param vertexIndices indices into the current vertex list, defining the vertices
+ * for the facet
+ * @return this builder instance
+ * @see #withVertexList(List)
+ */
+ public Builder addIndexedFacet(final List<Integer> vertexIndices) {
+ final List<Vector3D> vertices = vertexIndices.stream()
+ .map(idx -> vertexList.get(idx)).collect(Collectors.toList());
+
+ return addFacet(vertices);
+ }
+
+ /** Add an axis-oriented cube with the given dimensions to this instance.
+ * @param center the cube center point
+ * @param size the size of the cube
+ * @return this builder instance
+ * @throws GeometryValueException if the width, height, or depth of the defined region is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addCenteredCube(final Vector3D center, final double size) {
+ return addCenteredRect(center, size, size, size);
+ }
+
+ /** Add an axis-oriented cube with the given dimensions to this instance.
+ * @param corner a corner of the cube
+ * @param size the size of the cube
+ * @return this builder instance
+ * @throws GeometryValueException if the width, height, or depth of the defined region is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addCube(final Vector3D corner, final double size) {
+ return addRect(corner, size, size, size);
+ }
+
+ /** Add an axis-oriented rectangular prism to this instance. The prism is centered at the given point and
+ * has the specified dimensions.
+ * @param center center point for the rectangular prism
+ * @param xSize size of the prism along the x-axis
+ * @param ySize size of the prism along the y-axis
+ * @param zSize size of the prism along the z-axis
+ * @return this builder instance
+ * @throws GeometryValueException if the width, height, or depth of the defined region is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addCenteredRect(final Vector3D center, final double xSize, final double ySize,
+ final double zSize) {
+
+ return addRect(Vector3D.of(
+ center.getX() - (xSize * 0.5),
+ center.getY() - (ySize * 0.5),
+ center.getZ() - (zSize * 0.5)
+ ), xSize, ySize, zSize);
+ }
+
+ /** Add an axis-oriented rectangular prism to this instance. The prism
+ * is constructed by taking {@code pt} as one corner of the region and adding {@code xDelta},
+ * {@code yDelta}, and {@code zDelta} to its components to create the opposite corner.
+ * @param pt point lying in a corner of the region
+ * @param xDelta distance to move along the x axis to place the other points in the
+ * prism; this value may be negative.
+ * @param yDelta distance to move along the y axis to place the other points in the
+ * prism; this value may be negative.
+ * @param zDelta distance to move along the z axis to place the other points in the
+ * prism; this value may be negative.
+ * @return this builder instance
+ * @throws GeometryValueException if the width, height, or depth of the defined region is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addRect(final Vector3D pt, final double xDelta, final double yDelta, final double zDelta) {
+ return addRect(pt, Vector3D.of(
+ pt.getX() + xDelta,
+ pt.getY() + yDelta,
+ pt.getZ() + zDelta));
+ }
+
+ /** Add an axis-oriented rectangular prism to this instance. The points {@code a} and {@code b}
+ * are taken to represent opposite corner points in the prism and may be specified in any order.
+ * @param a first corner point in the rectangular prism (opposite of {@code b})
+ * @param b second corner point in the rectangular prism (opposite of {@code a})
+ * @return this builder instance
+ * @throws GeometryValueException if the width, height, or depth of the defined region is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addRect(final Vector3D a, final Vector3D b) {
+ final double minX = Math.min(a.getX(), b.getX());
+ final double maxX = Math.max(a.getX(), b.getX());
+
+ final double minY = Math.min(a.getY(), b.getY());
+ final double maxY = Math.max(a.getY(), b.getY());
+
+ final double minZ = Math.min(a.getZ(), b.getZ());
+ final double maxZ = Math.max(a.getZ(), b.getZ());
+
+ if (precision.eq(minX, maxX) || precision.eq(minY, maxY) || precision.eq(minZ, maxZ)) {
+ throw new GeometryValueException("Rectangular prism has zero size: " + a + ", " + b + ".");
+ }
+
+ final Vector3D[] vertices = {
+ Vector3D.of(minX, minY, minZ),
+ Vector3D.of(maxX, minY, minZ),
+ Vector3D.of(maxX, maxY, minZ),
+ Vector3D.of(minX, maxY, minZ),
+
+ Vector3D.of(minX, minY, maxZ),
+ Vector3D.of(maxX, minY, maxZ),
+ Vector3D.of(maxX, maxY, maxZ),
+ Vector3D.of(minX, maxY, maxZ)
+ };
+
+ addFacet(vertices[0], vertices[3], vertices[2], vertices[1]);
+ addFacet(vertices[4], vertices[5], vertices[6], vertices[7]);
+
+ addFacet(vertices[5], vertices[1], vertices[2], vertices[6]);
+ addFacet(vertices[0], vertices[4], vertices[7], vertices[3]);
+
+ addFacet(vertices[0], vertices[1], vertices[5], vertices[4]);
+ addFacet(vertices[3], vertices[7], vertices[6], vertices[2]);
+
+ return this;
+ }
+
+ /** Get the created BSP tree.
+ * @return the created BSP tree
+ */
+ public RegionBSPTree3D build() {
+ return tree;
+ }
+ }
+
+ /** Class used to project points onto the 3D region boundary.
+ */
+ private static final class BoundaryProjector3D extends BoundaryProjector<Vector3D, RegionNode3D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190811L;
+
+ /** Simple constructor.
+ * @param point the point to project onto the region's boundary
+ */
+ private BoundaryProjector3D(Vector3D point) {
+ super(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector3D disambiguateClosestPoint(final Vector3D target, final Vector3D a, final Vector3D b) {
+ // return the point with the smallest coordinate values
+ final int cmp = Vector3D.COORDINATE_ASCENDING_ORDER.compare(a, b);
+ return cmp < 0 ? a : b;
+ }
+ }
+
+ /** Visitor for computing geometric properties for 3D BSP tree instances.
+ * The volume of the region is computed using the equation
+ * <code>V = (1/3)*Σ<sub>F</sub>[(C<sub>F</sub>⋅N<sub>F</sub>)*area(F)]</code>,
+ * where <code>F</code> represents each face in the region, <code>C<sub>F</sub></code>
+ * represents the barycenter of the face, and <code>N<sub>F</sub></code> represents the
+ * normal of the face. (More details can be found in the article
+ * <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume">here</a>.)
+ * This essentially splits up the region into pyramids with a 2D face forming
+ * the base of each pyramid. The barycenter is computed in a similar way. The barycenter
+ * of each pyramid is calculated using the fact that it is located 3/4 of the way along the
+ * line from the apex to the base. The region barycenter then becomes the volume-weighted
+ * average of these pyramid centers.
+ * @see https://en.wikipedia.org/wiki/Polyhedron#Volume
+ */
+ private static final class RegionSizePropertiesVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> {
+
+ /** Accumulator for facet volume contributions. */
+ private double volumeSum;
+
+ /** Barycenter contribution x coordinate accumulator. */
+ private double sumX;
+
+ /** Barycenter contribution y coordinate accumulator. */
+ private double sumY;
+
+ /** Barycenter contribution z coordinate accumulator. */
+ private double sumZ;
+
+ /** {@inheritDoc} */
+ @Override
+ public void visit(final RegionNode3D node) {
+ if (node.isInternal()) {
+ RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
+ addFacetContribution(boundary.getOutsideFacing(), false);
+ addFacetContribution(boundary.getInsideFacing(), true);
+ }
+ }
+
+ /** Return the computed size properties for the visited region.
+ * @return the computed size properties for the visited region.
+ */
+ public RegionSizeProperties<Vector3D> getRegionSizeProperties() {
+ double size = Double.POSITIVE_INFINITY;
+ Vector3D barycenter = null;
+
+ // we only have a finite size if the volume sum is finite and positive
+ // (negative indicates a finite outside surrounded by an infinite inside)
+ if (Double.isFinite(volumeSum) && volumeSum > 0.0) {
+ // apply the 1/3 pyramid volume scaling factor
+ size = volumeSum / 3.0;
+
+ // Since the volume we used when adding together the facet contributions
+ // was 3x the actual pyramid size, we'll multiply by 1/4 here instead
+ // of 3/4 to adjust for the actual barycenter position in each pyramid.
+ final double barycenterScale = 1.0 / (4 * size);
+ barycenter = Vector3D.of(
+ sumX * barycenterScale,
+ sumY * barycenterScale,
+ sumZ * barycenterScale);
+ }
+
+ return new RegionSizeProperties<Vector3D>(size, barycenter);
+ }
+
+ /** Add the facet contribution of the given node cut boundary. If {@code reverse} is true,
+ * the volume of the facet contribution is reversed before being added to the total.
+ * @param boundary node cut boundary
+ * @param reverse if true, the facet contribution is reversed before being added to the total.
+ */
+ private void addFacetContribution(final SubHyperplane<Vector3D> boundary, boolean reverse) {
+ SubPlane subplane = (SubPlane) boundary;
+ RegionBSPTree2D base = subplane.getSubspaceRegion();
+
+ double area = base.getSize();
+ Vector2D baseBarycenter = base.getBarycenter();
+
+ if (Double.isInfinite(area)) {
+ volumeSum = Double.POSITIVE_INFINITY;
+ } else if (baseBarycenter != null) {
+ Plane plane = subplane.getPlane();
+ Vector3D facetBarycenter = plane.toSpace(base.getBarycenter());
+
+ // the volume here is actually 3x the actual pyramid volume; we'll apply
+ // the final scaling all at once at the end
+ double scaledVolume = area * facetBarycenter.dot(plane.getNormal());
+ if (reverse) {
+ scaledVolume = -scaledVolume;
+ }
+
+ volumeSum += scaledVolume;
+
+ sumX += scaledVolume * facetBarycenter.getX();
+ sumY += scaledVolume * facetBarycenter.getY();
+ sumZ += scaledVolume * facetBarycenter.getZ();
+ }
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment.java
deleted file mode 100644
index 8823278..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.threed;
-
-
-/** Simple container for a two-points segment.
- */
-public class Segment {
-
- /** Start point of the segment. */
- private final Vector3D start;
-
- /** End point of the segments. */
- private final Vector3D end;
-
- /** Line containing the segment. */
- private final Line line;
-
- /** Build a segment.
- * @param start start point of the segment
- * @param end end point of the segment
- * @param line line containing the segment
- */
- public Segment(final Vector3D start, final Vector3D end, final Line line) {
- this.start = start;
- this.end = end;
- this.line = line;
- }
-
- /** Get the start point of the segment.
- * @return start point of the segment
- */
- public Vector3D getStart() {
- return start;
- }
-
- /** Get the end point of the segment.
- * @return end point of the segment
- */
- public Vector3D getEnd() {
- return end;
- }
-
- /** Get the line containing the segment.
- * @return line containing the segment
- */
- public Line getLine() {
- return line;
- }
-
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment3D.java
new file mode 100644
index 0000000..e330f1b
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment3D.java
@@ -0,0 +1,271 @@
+/*
+ * 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.threed;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.threed.Line3D.SubspaceTransform;
+
+/** Class representing a line segment in 3 dimensional Euclidean space.
+ *
+ * <p>This class is guaranteed to be immutable.</p>
+ */
+public final class Segment3D extends AbstractSubLine3D<Interval> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190702L;
+
+ /** String used to indicate the start point of the segment in the toString() representation. */
+ private static final String START_STR = "start= ";
+
+ /** String used to indicate the direction the segment in the toString() representation. */
+ private static final String DIR_STR = "direction= ";
+
+ /** String used to indicate the end point of the segment in the toString() representation. */
+ private static final String END_STR = "end= ";
+
+ /** String used as a separator value in the toString() representation. */
+ private static final String SEP_STR = ", ";
+
+ /** The interval representing the region of the line contained in
+ * the line segment.
+ */
+ private final Interval interval;
+
+ /** Construct a line segment from an underlying line and a 1D interval
+ * on it.
+ * @param line the underlying line
+ * @param interval 1D interval on the line defining the line segment
+ */
+ private Segment3D(final Line3D line, final Interval interval) {
+ super(line);
+
+ this.interval = interval;
+ }
+
+ /** Get the start value in the 1D subspace of the line.
+ * @return the start value in the 1D subspace of the line.
+ */
+ public double getSubspaceStart() {
+ return interval.getMin();
+ }
+
+ /** Get the end value in the 1D subspace of the line.
+ * @return the end value in the 1D subspace of the line
+ */
+ public double getSubspaceEnd() {
+ return interval.getMax();
+ }
+
+ /** Get the start point of the line segment or null if no start point
+ * exists (ie, the segment is infinite).
+ * @return the start point of the line segment or null if no start point
+ * exists
+ */
+ public Vector3D getStartPoint() {
+ return interval.hasMinBoundary() ? getLine().toSpace(interval.getMin()) : null;
+ }
+
+ /** Get the end point of the line segment or null if no end point
+ * exists (ie, the segment is infinite).
+ * @return the end point of the line segment or null if no end point
+ * exists
+ */
+ public Vector3D getEndPoint() {
+ return interval.hasMaxBoundary() ? getLine().toSpace(interval.getMax()) : null;
+ }
+
+ /** Return true if the segment is infinite.
+ * @return true if the segment is infinite.
+ */
+ public boolean isInfinite() {
+ return interval.isInfinite();
+ }
+
+ /** Return true if the segment is finite.
+ * @return true if the segment is finite.
+ */
+ public boolean isFinite() {
+ return interval.isFinite();
+ }
+
+ /** Return the 1D interval for the line segment.
+ * @return the 1D interval for the line segment
+ * @see #getSubspaceRegion()
+ */
+ public Interval getInterval() {
+ return interval;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This is an alias for {@link #getInterval()}.</p>
+ */
+ @Override
+ public Interval getSubspaceRegion() {
+ return getInterval();
+ }
+
+ /** Return true if the given point lies in the segment.
+ * @param pt point to check
+ * @return true if the point lies in the segment
+ */
+ public boolean contains(final Vector3D pt) {
+ final Line3D line = getLine();
+ return line.contains(pt) && interval.contains(line.toSubspace(pt));
+ }
+
+ /** Transform this instance.
+ * @param transform the transform to apply
+ * @return a new, transformed instance
+ */
+ public Segment3D transform(final Transform<Vector3D> transform) {
+ final SubspaceTransform st = getLine().subspaceTransform(transform);
+
+ return new Segment3D(st.getLine(), interval.transform(st.getTransform()));
+ }
+
+ /** Return a string representation of the segment.
+ *
+ * <p>In order to keep the representation short but informative, the exact format used
+ * depends on the properties of the instance, as demonstrated in the examples
+ * below.
+ * <ul>
+ * <li>Infinite segment -
+ * {@code "Segment3D[lineOrigin= (0.0, 0.0, 0.0), lineDirection= (1.0, 0.0, 0.0)]}"</li>
+ * <li>Start point but no end point -
+ * {@code "Segment3D[start= (0.0, 0.0, 0.0), direction= (1.0, 0.0, 0.0)]}"</li>
+ * <li>End point but no start point -
+ * {@code "Segment3D[direction= (1.0, 0.0, 0.0), end= (0.0, 0.0, 0.0)]}"</li>
+ * <li>Start point and end point -
+ * {@code "Segment3D[start= (0.0, 0.0, 0.0), end= (1.0, 0.0, 0.0)]}"</li>
+ * </ul>
+ */
+ @Override
+ public String toString() {
+ final Vector3D startPoint = getStartPoint();
+ final Vector3D endPoint = getEndPoint();
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append('[');
+
+ if (startPoint != null && endPoint != null) {
+ sb.append(START_STR)
+ .append(startPoint)
+ .append(SEP_STR)
+ .append(END_STR)
+ .append(endPoint);
+ } else if (startPoint != null) {
+ sb.append(START_STR)
+ .append(startPoint)
+ .append(SEP_STR)
+ .append(DIR_STR)
+ .append(getLine().getDirection());
+ } else if (endPoint != null) {
+ sb.append(DIR_STR)
+ .append(getLine().getDirection())
+ .append(SEP_STR)
+ .append(END_STR)
+ .append(endPoint);
+ } else {
+ final Line3D line = getLine();
+
+ sb.append("lineOrigin= ")
+ .append(line.getOrigin())
+ .append(SEP_STR)
+ .append("lineDirection= ")
+ .append(line.getDirection());
+ }
+
+ sb.append(']');
+
+ return sb.toString();
+ }
+
+ /** Create a line segment between two points. The underlying line points in the direction from {@code start}
+ * to {@code end}.
+ * @param start start point for the line segment
+ * @param end end point for the line segment
+ * @param precision precision context used to determine floating point equality
+ * @return a new line segment between {@code start} and {@code end}.
+ */
+ public static Segment3D fromPoints(final Vector3D start, final Vector3D end,
+ final DoublePrecisionContext precision) {
+
+ final Line3D line = Line3D.fromPoints(start, end, precision);
+ return fromPointsOnLine(line, start, end);
+ }
+
+ /** Construct a line segment from a starting point and a direction that the line should extend to
+ * infinity from. This is equivalent to constructing a ray.
+ * @param start start point for the segment
+ * @param direction direction that the line should extend from the segment
+ * @param precision precision context used to determine floating point equality
+ * @return a new line segment starting from the given point and extending to infinity in the
+ * specified direction
+ */
+ public static Segment3D fromPointAndDirection(final Vector3D start, final Vector3D direction,
+ final DoublePrecisionContext precision) {
+ final Line3D line = Line3D.fromPointAndDirection(start, direction, precision);
+ return fromInterval(line, Interval.min(line.toSubspace(start).getX(), precision));
+ }
+
+ /** Create a line segment from an underlying line and a 1D interval on the line.
+ * @param line the line that the line segment will belong to
+ * @param interval 1D interval on the line
+ * @return a line segment defined by the given line and interval
+ */
+ public static Segment3D fromInterval(final Line3D line, final Interval interval) {
+ return new Segment3D(line, interval);
+ }
+
+ /** Create a line segment from an underlying line and a 1D interval on the line.
+ * @param line the line that the line segment will belong to
+ * @param a first 1D location on the line
+ * @param b second 1D location on the line
+ * @return a line segment defined by the given line and interval
+ */
+ public static Segment3D fromInterval(final Line3D line, final double a, final double b) {
+ return fromInterval(line, Interval.of(a, b, line.getPrecision()));
+ }
+
+ /** Create a line segment from an underlying line and a 1D interval on the line.
+ * @param line the line that the line segment will belong to
+ * @param a first 1D point on the line; must not be null
+ * @param b second 1D point on the line; must not be null
+ * @return a line segment defined by the given line and interval
+ */
+ public static Segment3D fromInterval(final Line3D line, final Vector1D a, final Vector1D b) {
+ return fromInterval(line, a.getX(), b.getX());
+ }
+
+ /** Create a new line segment from a line and points known to lie on the line.
+ * @param line the line that the line segment will belong to
+ * @param start line segment start point known to lie on the line
+ * @param end line segment end poitn known to lie on the line
+ * @return a new line segment created from the line and points
+ */
+ private static Segment3D fromPointsOnLine(final Line3D line, final Vector3D start, final Vector3D end) {
+ final double subspaceStart = line.toSubspace(start).getX();
+ final double subspaceEnd = line.toSubspace(end).getX();
+
+ return fromInterval(line, subspaceStart, subspaceEnd);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinates.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinates.java
index b863ec5..9759606 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinates.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinates.java
@@ -21,6 +21,7 @@
import org.apache.commons.geometry.core.Geometry;
import org.apache.commons.geometry.core.Spatial;
import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
+import org.apache.commons.geometry.euclidean.internal.Vectors;
import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
import org.apache.commons.numbers.angle.PlaneAngleRadians;
@@ -137,6 +138,12 @@
return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth) || Double.isInfinite(polar));
}
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return Double.isFinite(radius) && Double.isFinite(azimuth) && Double.isFinite(polar);
+ }
+
/** Convert this set of spherical coordinates to a Cartesian form.
* @return A 3-dimensional vector with an equivalent set of
* Cartesian coordinates.
@@ -221,7 +228,7 @@
* @return a set of spherical coordinates equivalent to the given Cartesian coordinates
*/
public static SphericalCoordinates fromCartesian(final double x, final double y, final double z) {
- final double radius = Math.sqrt((x*x) + (y*y) + (z*z));
+ final double radius = Vectors.norm(x, y, z);
final double azimuth = Math.atan2(y, x);
// default the polar angle to 0 when the radius is 0
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine.java
deleted file mode 100644
index adb0194..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.threed;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.Interval;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
-
-/** This class represents a subset of a {@link Line}.
- */
-public class SubLine {
-
- /** Underlying line. */
- private final Line line;
-
- /** Remaining region of the hyperplane. */
- private final IntervalsSet remainingRegion;
-
- /** Simple constructor.
- * @param line underlying line
- * @param remainingRegion remaining region of the line
- */
- public SubLine(final Line line, final IntervalsSet remainingRegion) {
- this.line = line;
- this.remainingRegion = remainingRegion;
- }
-
- /** Create a sub-line from two endpoints.
- * @param start start point
- * @param end end point
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if the points are equal
- */
- public SubLine(final Vector3D start, final Vector3D end, final DoublePrecisionContext precision)
- throws IllegalArgumentException {
- this(new Line(start, end, precision), buildIntervalSet(start, end, precision));
- }
-
- /** Create a sub-line from a segment.
- * @param segment single segment forming the sub-line
- * @exception IllegalArgumentException if the segment endpoints are equal
- */
- public SubLine(final Segment segment) {
- this(segment.getLine(),
- buildIntervalSet(segment.getStart(), segment.getEnd(), segment.getLine().getPrecision()));
- }
-
- /** Get the endpoints of the sub-line.
- * <p>
- * A subline may be any arbitrary number of disjoints segments, so the endpoints
- * are provided as a list of endpoint pairs. Each element of the list represents
- * one segment, and each segment contains a start point at index 0 and an end point
- * at index 1. If the sub-line is unbounded in the negative infinity direction,
- * the start point of the first segment will have infinite coordinates. If the
- * sub-line is unbounded in the positive infinity direction, the end point of the
- * last segment will have infinite coordinates. So a sub-line covering the whole
- * line will contain just one row and both elements of this row will have infinite
- * coordinates. If the sub-line is empty, the returned list will contain 0 segments.
- * </p>
- * @return list of segments endpoints
- */
- public List<Segment> getSegments() {
-
- final List<Interval> list = remainingRegion.asList();
- final List<Segment> segments = new ArrayList<>(list.size());
-
- for (final Interval interval : list) {
- final Vector3D start = line.toSpace(Vector1D.of(interval.getInf()));
- final Vector3D end = line.toSpace(Vector1D.of(interval.getSup()));
- segments.add(new Segment(start, end, line));
- }
-
- return segments;
-
- }
-
- /** Get the intersection of the instance and another sub-line.
- * <p>
- * This method is related to the {@link Line#intersection(Line)
- * intersection} method in the {@link Line Line} class, but in addition
- * to compute the point along infinite lines, it also checks the point
- * lies on both sub-line ranges.
- * </p>
- * @param subLine other sub-line which may intersect instance
- * @param includeEndPoints if true, endpoints are considered to belong to
- * instance (i.e. they are closed sets) and may be returned, otherwise endpoints
- * are considered to not belong to instance (i.e. they are open sets) and intersection
- * occurring on endpoints lead to null being returned
- * @return the intersection point if there is one, null if the sub-lines don't intersect
- */
- public Vector3D intersection(final SubLine subLine, final boolean includeEndPoints) {
-
- // compute the intersection on infinite line
- Vector3D v1D = line.intersection(subLine.line);
- if (v1D == null) {
- return null;
- }
-
- // check location of point with respect to first sub-line
- Location loc1 = remainingRegion.checkPoint(line.toSubSpace(v1D));
-
- // check location of point with respect to second sub-line
- Location loc2 = subLine.remainingRegion.checkPoint(subLine.line.toSubSpace(v1D));
-
- if (includeEndPoints) {
- return ((loc1 != Location.OUTSIDE) && (loc2 != Location.OUTSIDE)) ? v1D : null;
- } else {
- return ((loc1 == Location.INSIDE) && (loc2 == Location.INSIDE)) ? v1D : null;
- }
-
- }
-
- /** Build an interval set from two points.
- * @param start start point
- * @param end end point
- * @return an interval set
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if the points are equal
- */
- private static IntervalsSet buildIntervalSet(final Vector3D start, final Vector3D end, final DoublePrecisionContext precision)
- throws IllegalArgumentException {
- final Line line = new Line(start, end, precision);
- return new IntervalsSet(line.toSubSpace(start).getX(),
- line.toSubSpace(end).getX(),
- precision);
- }
-
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine3D.java
new file mode 100644
index 0000000..b847a96
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine3D.java
@@ -0,0 +1,124 @@
+/*
+ * 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.threed;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
+import org.apache.commons.geometry.euclidean.threed.Line3D.SubspaceTransform;
+
+/** Class representing an arbitrary region of a 3 dimensional line. This class can represent
+ * both convex and non-convex regions of its underlying line.
+ *
+ * <p>This class is mutable and <em>not</em> thread safe.</p>
+ */
+public final class SubLine3D extends AbstractSubLine3D<RegionBSPTree1D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190702L;
+
+ /** The 1D region representing the area on the line. */
+ private final RegionBSPTree1D region;
+
+ /** Construct a new, empty subline for the given line.
+ * @param line line defining the subline
+ */
+ public SubLine3D(final Line3D line) {
+ this(line, false);
+ }
+
+ /** Construct a new subline for the given line. If {@code full}
+ * is true, then the subline will cover the entire line; otherwise,
+ * it will be empty.
+ * @param line line defining the subline
+ * @param full if true, the subline will cover the entire space;
+ * otherwise it will be empty
+ */
+ public SubLine3D(final Line3D line, boolean full) {
+ this(line, new RegionBSPTree1D(full));
+ }
+
+ /** Construct a new instance from its defining line and subspace region.
+ * @param line line defining the subline
+ * @param region subspace region for the subline
+ */
+ public SubLine3D(final Line3D line, final RegionBSPTree1D region) {
+ super(line);
+
+ this.region = region;
+ }
+
+ /** Transform this instance.
+ * @param transform the transform to apply
+ * @return a new, transformed instance
+ */
+ public SubLine3D transform(final Transform<Vector3D> transform) {
+ final SubspaceTransform st = getLine().subspaceTransform(transform);
+
+ final RegionBSPTree1D tRegion = RegionBSPTree1D.empty();
+ tRegion.copy(region);
+ tRegion.transform(st.getTransform());
+
+ return new SubLine3D(st.getLine(), tRegion);
+ }
+
+ /** Return a list of {@link Segment3D} instances representing the same region
+ * as this subline.
+ * @return a list of {@link Segment3D} instances representing the same region
+ * as this instance.
+ */
+ public List<Segment3D> toConvex() {
+ final List<Interval> intervals = region.toIntervals();
+
+ final Line3D line = getLine();
+ final List<Segment3D> segments = new ArrayList<>(intervals.size());
+
+ for (Interval interval : intervals) {
+ segments.add(Segment3D.fromInterval(line, interval));
+ }
+
+ return segments;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionBSPTree1D getSubspaceRegion() {
+ return region;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final Line3D line = getLine();
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append('[')
+ .append("lineOrigin= ")
+ .append(line.getOrigin())
+ .append(", lineDirection= ")
+ .append(line.getDirection())
+ .append(", region= ")
+ .append(region)
+ .append(']');
+
+ return sb.toString();
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java
index 0520658..4a22af0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java
@@ -16,92 +16,189 @@
*/
package org.apache.commons.geometry.euclidean.threed;
-import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
-import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
-/** This class represents a sub-hyperplane for {@link Plane}.
+/** Class representing an arbitrary region of a plane. This class can represent
+ * both convex and non-convex regions of its underlying plane.
+ *
+ * <p>This class is mutable and <em>not</em> thread safe.</p>
*/
-public class SubPlane extends AbstractSubHyperplane<Vector3D, Vector2D> {
+public final class SubPlane extends AbstractSubPlane<RegionBSPTree2D> implements Serializable {
- /** Simple constructor.
- * @param hyperplane underlying hyperplane
- * @param remainingRegion remaining region of the hyperplane
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190717L;
+
+ /** The 2D region representing the area on the plane. */
+ private final RegionBSPTree2D region;
+
+ /** Construct a new, empty subplane for the given plane.
+ * @param plane plane defining the subplane
*/
- public SubPlane(final Hyperplane<Vector3D> hyperplane,
- final Region<Vector2D> remainingRegion) {
- super(hyperplane, remainingRegion);
+ public SubPlane(final Plane plane) {
+ this(plane, false);
+ }
+
+ /** Construct a new subplane for the given plane. If {@code full}
+ * is true, then the subplane will cover the entire plane; otherwise,
+ * it will be empty.
+ * @param plane plane defining the subplane
+ * @param full if true, the subplane will cover the entire space;
+ * otherwise it will be empty
+ */
+ public SubPlane(final Plane plane, boolean full) {
+ this(plane, new RegionBSPTree2D(full));
+ }
+
+ /** Construct a new instance from its defining plane and subspace region.
+ * @param plane plane defining the subplane
+ * @param region subspace region for the subplane
+ */
+ public SubPlane(final Plane plane, final RegionBSPTree2D region) {
+ super(plane);
+
+ this.region = region;
}
/** {@inheritDoc} */
@Override
- protected AbstractSubHyperplane<Vector3D, Vector2D> buildNew(final Hyperplane<Vector3D> hyperplane,
- final Region<Vector2D> remainingRegion) {
- return new SubPlane(hyperplane, remainingRegion);
+ public List<ConvexSubPlane> toConvex() {
+ final List<ConvexArea> areas = region.toConvex();
+
+ final Plane plane = getPlane();
+ final List<ConvexSubPlane> subplanes = new ArrayList<>(areas.size());
+
+ for (final ConvexArea area : areas) {
+ subplanes.add(ConvexSubPlane.fromConvexArea(plane, area));
+ }
+
+ return subplanes;
}
- /** Split the instance in two parts by an hyperplane.
- * @param hyperplane splitting hyperplane
- * @return an object containing both the part of the instance
- * on the plus side of the instance and the part of the
- * instance on the minus side of the instance
+ /** {@inheritDoc}
+ *
+ * <p>In all cases, the current instance is not modified. However, In order to avoid
+ * unnecessary copying, this method will use the current instance as the split value when
+ * the instance lies entirely on the plus or minus side of the splitter. For example, if
+ * this instance lies entirely on the minus side of the splitter, the subplane
+ * returned by {@link Split#getMinus()} will be this instance. Similarly, {@link Split#getPlus()}
+ * will return the current instance if it lies entirely on the plus side. Callers need to make
+ * special note of this, since this class is mutable.</p>
*/
@Override
- public SplitSubHyperplane<Vector3D> split(Hyperplane<Vector3D> hyperplane) {
-
- final Plane otherPlane = (Plane) hyperplane;
- final Plane thisPlane = (Plane) getHyperplane();
- final Line inter = otherPlane.intersection(thisPlane);
- final DoublePrecisionContext precision = thisPlane.getPrecision();
-
- if (inter == null) {
- // the hyperplanes are parallel
- final double global = otherPlane.getOffset(thisPlane);
- final int comparison = precision.compare(global, 0.0);
-
- if (comparison < 0) {
- return new SplitSubHyperplane<>(null, this);
- } else if (comparison > 0) {
- return new SplitSubHyperplane<>(this, null);
- } else {
- return new SplitSubHyperplane<>(null, null);
- }
- }
-
- // the hyperplanes do intersect
- Vector2D p = thisPlane.toSubSpace(inter.toSpace(Vector1D.ZERO));
- Vector2D q = thisPlane.toSubSpace(inter.toSpace(Vector1D.Unit.PLUS));
- Vector3D crossP = inter.getDirection().cross(thisPlane.getNormal());
- if (crossP.dot(otherPlane.getNormal()) < 0) {
- final Vector2D tmp = p;
- p = q;
- q = tmp;
- }
- final SubHyperplane<Vector2D> l2DMinus =
- org.apache.commons.geometry.euclidean.twod.Line.fromPoints(p, q, precision).wholeHyperplane();
- final SubHyperplane<Vector2D> l2DPlus =
- org.apache.commons.geometry.euclidean.twod.Line.fromPoints(q, p, precision).wholeHyperplane();
-
- final BSPTree<Vector2D> splitTree = getRemainingRegion().getTree(false).split(l2DMinus);
- final BSPTree<Vector2D> plusTree = getRemainingRegion().isEmpty(splitTree.getPlus()) ?
- new BSPTree<Vector2D>(Boolean.FALSE) :
- new BSPTree<>(l2DPlus, new BSPTree<Vector2D>(Boolean.FALSE),
- splitTree.getPlus(), null);
-
- final BSPTree<Vector2D> minusTree = getRemainingRegion().isEmpty(splitTree.getMinus()) ?
- new BSPTree<Vector2D>(Boolean.FALSE) :
- new BSPTree<>(l2DMinus, new BSPTree<Vector2D>(Boolean.FALSE),
- splitTree.getMinus(), null);
-
- return new SplitSubHyperplane<>(new SubPlane(thisPlane.copySelf(), new PolygonsSet(plusTree, precision)),
- new SubPlane(thisPlane.copySelf(), new PolygonsSet(minusTree, precision)));
-
+ public Split<SubPlane> split(final Hyperplane<Vector3D> splitter) {
+ return splitInternal(splitter, this, (p, r) -> new SubPlane(p, (RegionBSPTree2D) r));
}
+ /** {@inheritDoc} */
+ @Override
+ public RegionBSPTree2D getSubspaceRegion() {
+ return region;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubPlane transform(final Transform<Vector3D> transform) {
+ final Plane.SubspaceTransform subTransform = getPlane().subspaceTransform(transform);
+
+ final RegionBSPTree2D tRegion = RegionBSPTree2D.empty();
+ tRegion.copy(region);
+ tRegion.transform(subTransform.getTransform());
+
+ return new SubPlane(subTransform.getPlane(), tRegion);
+ }
+
+ /** Add a convex subplane to this instance.
+ * @param subplane convex subplane to add
+ * @throws GeometryException if the given convex subplane is not from
+ * a plane equivalent to this instance
+ */
+ public void add(final ConvexSubPlane subplane) {
+ validatePlane(subplane.getPlane());
+
+ region.add(subplane.getSubspaceRegion());
+ }
+
+ /** Add a subplane to this instance.
+ * @param subplane subplane to add
+ * @throws GeometryException if the given convex subplane is not from
+ * a plane equivalent to this instance
+ */
+ public void add(final SubPlane subplane) {
+ validatePlane(subplane.getPlane());
+
+ region.union(subplane.getSubspaceRegion());
+ }
+
+ /** Validate that the given plane is equivalent to the plane
+ * defining this subplane.
+ * @param inputPlane plane to validate
+ * @throws GeometryException if the given plane is not equivalent
+ * to the plane for this instance
+ */
+ private void validatePlane(final Plane inputPlane) {
+ final Plane plane = getPlane();
+
+ if (!plane.eq(inputPlane)) {
+ throw new GeometryException("Argument is not on the same " +
+ "plane. Expected " + plane + " but was " +
+ inputPlane);
+ }
+ }
+
+ /** {@link Builder} implementation for sublines.
+ */
+ public static class SubPlaneBuilder implements SubHyperplane.Builder<Vector3D> {
+
+ /** Subplane instance created by this builder. */
+ private final SubPlane subplane;
+
+ /** Construct a new instance for building subplane region for the given plane.
+ * @param plane the underlying plane for the subplane region
+ */
+ public SubPlaneBuilder(final Plane plane) {
+ this.subplane = new SubPlane(plane);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final SubHyperplane<Vector3D> sub) {
+ addInternal(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final ConvexSubHyperplane<Vector3D> sub) {
+ addInternal(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubPlane build() {
+ return subplane;
+ }
+
+ /** Internal method for adding subhyperplanes to this builder.
+ * @param sub the subhyperplane to add; either convex or non-convex
+ */
+ private void addInternal(final SubHyperplane<Vector3D> sub) {
+ if (sub instanceof ConvexSubPlane) {
+ subplane.add((ConvexSubPlane) sub);
+ } else if (sub instanceof SubPlane) {
+ subplane.add((SubPlane) sub);
+ } else {
+ throw new IllegalArgumentException("Unsupported subhyperplane type: " + sub.getClass().getName());
+ }
+ }
+ }
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Transform3D.java
similarity index 61%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Transform3D.java
index 046defe..493a70a 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Transform3D.java
@@ -14,23 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partitioning;
+package org.apache.commons.geometry.euclidean.threed;
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+import org.apache.commons.geometry.euclidean.EuclideanTransform;
+
+/** Extension of the {@link EuclideanTransform} interface for 3D points.
*/
-public enum Side {
+public interface Transform3D extends EuclideanTransform<Vector3D> {
- /** Code for the plus side of the hyperplane. */
- PLUS,
-
- /** Code for the minus side of the hyperplane. */
- MINUS,
-
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
-
- /** Code for the hyperplane itself. */
- HYPER;
-
+ /** Return an affine transform matrix representing the same transform
+ * as this instance.
+ * @return an affine tranform matrix representing the same transform
+ * as this instance
+ */
+ AffineTransformMatrix3D toMatrix();
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
index 1434841..a8b8d47 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
@@ -16,8 +16,9 @@
*/
package org.apache.commons.geometry.euclidean.threed;
+import java.util.Comparator;
+import java.util.function.Function;
-import org.apache.commons.geometry.core.exception.IllegalNormException;
import org.apache.commons.geometry.core.internal.DoubleFunction3N;
import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
@@ -46,16 +47,40 @@
public static final Vector3D NEGATIVE_INFINITY =
new Vector3D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
- /** Serializable version identifier */
+ /** Comparator that sorts vectors in component-wise ascending order.
+ * Vectors are only considered equal if their coordinates match exactly.
+ * Null arguments are evaluated as being greater than non-null arguments.
+ */
+ public static final Comparator<Vector3D> COORDINATE_ASCENDING_ORDER = (a, b) -> {
+ int cmp = 0;
+
+ if (a != null && b != null) {
+ cmp = Double.compare(a.getX(), b.getX());
+ if (cmp == 0) {
+ cmp = Double.compare(a.getY(), b.getY());
+ if (cmp == 0) {
+ cmp = Double.compare(a.getZ(), b.getZ());
+ }
+ }
+ } else if (a != null) {
+ cmp = -1;
+ } else if (b != null) {
+ cmp = 1;
+ }
+
+ return cmp;
+ };
+
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180903L;
- /** Abscissa (first coordinate value) */
+ /** Abscissa (first coordinate value). */
private final double x;
- /** Ordinate (second coordinate value) */
+ /** Ordinate (second coordinate value). */
private final double y;
- /** Height (third coordinate value)*/
+ /** Height (third coordinate value). */
private final double z;
/** Simple constructor.
@@ -95,7 +120,7 @@
* @return the coordinates for this instance
*/
public double[] toArray() {
- return new double[] { x, y, z };
+ return new double[]{x, y, z};
}
/** {@inheritDoc} */
@@ -118,25 +143,31 @@
/** {@inheritDoc} */
@Override
+ public boolean isFinite() {
+ return Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Vector3D getZero() {
return ZERO;
}
/** {@inheritDoc} */
@Override
- public Vector3D vectorTo(Vector3D v) {
+ public Vector3D vectorTo(final Vector3D v) {
return v.subtract(this);
}
/** {@inheritDoc} */
@Override
- public Unit directionTo(Vector3D v) {
+ public Unit directionTo(final Vector3D v) {
return vectorTo(v).normalize();
}
/** {@inheritDoc} */
@Override
- public Vector3D lerp(Vector3D p, double t) {
+ public Vector3D lerp(final Vector3D p, final double t) {
return linearCombination(1.0 - t, this, t, p);
}
@@ -154,7 +185,7 @@
/** {@inheritDoc} */
@Override
- public Vector3D withNorm(double magnitude) {
+ public Vector3D withNorm(final double magnitude) {
final double m = magnitude / getCheckedNorm();
return new Vector3D(
@@ -166,7 +197,7 @@
/** {@inheritDoc} */
@Override
- public Vector3D add(Vector3D v) {
+ public Vector3D add(final Vector3D v) {
return new Vector3D(
x + v.x,
y + v.y,
@@ -176,7 +207,7 @@
/** {@inheritDoc} */
@Override
- public Vector3D add(double factor, Vector3D v) {
+ public Vector3D add(final double factor, final Vector3D v) {
return new Vector3D(
x + (factor * v.x),
y + (factor * v.y),
@@ -186,7 +217,7 @@
/** {@inheritDoc} */
@Override
- public Vector3D subtract(Vector3D v) {
+ public Vector3D subtract(final Vector3D v) {
return new Vector3D(
x - v.x,
y - v.y,
@@ -196,7 +227,7 @@
/** {@inheritDoc} */
@Override
- public Vector3D subtract(double factor, Vector3D v) {
+ public Vector3D subtract(final double factor, final Vector3D v) {
return new Vector3D(
x - (factor * v.x),
y - (factor * v.y),
@@ -218,13 +249,13 @@
/** {@inheritDoc} */
@Override
- public Vector3D multiply(double a) {
+ public Vector3D multiply(final double a) {
return new Vector3D(a * x, a * y, a * z);
}
/** {@inheritDoc} */
@Override
- public double distance(Vector3D v) {
+ public double distance(final Vector3D v) {
return Vectors.norm(
x - v.x,
y - v.y,
@@ -234,7 +265,7 @@
/** {@inheritDoc} */
@Override
- public double distanceSq(Vector3D v) {
+ public double distanceSq(final Vector3D v) {
return Vectors.normSq(
x - v.x,
y - v.y,
@@ -251,7 +282,7 @@
* @see LinearCombination#value(double, double, double, double, double, double)
*/
@Override
- public double dot(Vector3D v) {
+ public double dot(final Vector3D v) {
return LinearCombination.value(x, v.x, y, v.y, z, v.z);
}
@@ -263,7 +294,7 @@
* other.</p>
*/
@Override
- public double angle(Vector3D v) {
+ public double angle(final Vector3D v) {
double normProduct = getCheckedNorm() * v.getCheckedNorm();
double dot = dot(v);
@@ -283,13 +314,13 @@
/** {@inheritDoc} */
@Override
- public Vector3D project(Vector3D base) {
+ public Vector3D project(final Vector3D base) {
return getComponent(base, false, Vector3D::new);
}
/** {@inheritDoc} */
@Override
- public Vector3D reject(Vector3D base) {
+ public Vector3D reject(final Vector3D base) {
return getComponent(base, true, Vector3D::new);
}
@@ -303,30 +334,31 @@
* <pre><code>
* Vector3D k = u.normalize();
* Vector3D i = k.orthogonal();
- * Vector3D j = k.crossProduct(i);
+ * Vector3D j = k.cross(i);
* </code></pre>
* @return a unit vector orthogonal to the instance
- * @throws IllegalNormException if the norm of the instance is zero, NaN,
- * or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the instance
+ * is zero, NaN, or infinite
*/
@Override
- public Vector3D orthogonal() {
+ public Vector3D.Unit orthogonal() {
double threshold = 0.6 * getCheckedNorm();
+ double inverse;
if (Math.abs(x) <= threshold) {
- double inverse = 1 / Math.sqrt(y * y + z * z);
- return new Vector3D(0, inverse * z, -inverse * y);
+ inverse = 1 / Vectors.norm(y, z);
+ return new Unit(0, inverse * z, -inverse * y);
} else if (Math.abs(y) <= threshold) {
- double inverse = 1 / Math.sqrt(x * x + z * z);
- return new Vector3D(-inverse * z, 0, inverse * x);
+ inverse = 1 / Vectors.norm(x, z);
+ return new Unit(-inverse * z, 0, inverse * x);
}
- double inverse = 1 / Math.sqrt(x * x + y * y);
- return new Vector3D(inverse * y, -inverse * x, 0);
+ inverse = 1 / Vectors.norm(x, y);
+ return new Unit(inverse * y, -inverse * x, 0);
}
/** {@inheritDoc} */
@Override
- public Vector3D orthogonal(Vector3D dir) {
+ public Vector3D.Unit orthogonal(Vector3D dir) {
return dir.getComponent(this, true, Vector3D.Unit::from);
}
@@ -340,19 +372,18 @@
LinearCombination.value(x, v.y, -y, v.x));
}
- /** Apply the given transform to this vector, returning the result as a
- * new vector instance.
- * @param transform the transform to apply
- * @return a new, transformed vector
- * @see AffineTransformMatrix3D#apply(Vector3D)
+ /** Convenience method to apply a function to this vector. This
+ * can be used to transform the vector inline with other methods.
+ * @param fn the function to apply
+ * @return the transformed vector
*/
- public Vector3D transform(AffineTransformMatrix3D transform) {
- return transform.apply(this);
+ public Vector3D transform(final Function<Vector3D, Vector3D> fn) {
+ return fn.apply(this);
}
/** {@inheritDoc} */
@Override
- public boolean equals(final Vector3D vec, final DoublePrecisionContext precision) {
+ public boolean eq(final Vector3D vec, final DoublePrecisionContext precision) {
return precision.eq(x, vec.x) &&
precision.eq(y, vec.y) &&
precision.eq(z, vec.z);
@@ -422,11 +453,13 @@
* returned. If false, the projection of this instance onto {@code base}
* is returned.
* @param factory factory function used to build the final vector
+ * @param <V> Vector implementation type
* @return The projection or rejection of this instance relative to {@code base},
* depending on the value of {@code reject}.
- * @throws IllegalNormException if {@code base} has a zero, NaN, or infinite norm
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if {@code base} has a zero, NaN,
+ * or infinite norm
*/
- private Vector3D getComponent(Vector3D base, boolean reject, DoubleFunction3N<Vector3D> factory) {
+ private <V extends Vector3D> V getComponent(Vector3D base, boolean reject, DoubleFunction3N<V> factory) {
final double aDotB = dot(base);
// We need to check the norm value here to ensure that it's legal. However, we don't
@@ -456,7 +489,7 @@
* @param z height (third coordinate value)
* @return vector instance
*/
- public static Vector3D of(double x, double y, double z) {
+ public static Vector3D of(final double x, final double y, final double z) {
return new Vector3D(x, y, z);
}
@@ -465,7 +498,7 @@
* @return new vector
* @exception IllegalArgumentException if the array does not have 3 elements
*/
- public static Vector3D of(double[] v) {
+ public static Vector3D of(final double[] v) {
if (v.length != 3) {
throw new IllegalArgumentException("Dimension mismatch: " + v.length + " != 3");
}
@@ -478,7 +511,7 @@
* @return vector instance represented by the string
* @throws IllegalArgumentException if the given string has an invalid format
*/
- public static Vector3D parse(String str) {
+ public static Vector3D parse(final String str) {
return SimpleTupleFormat.getDefault().parse(str, Vector3D::new);
}
@@ -492,7 +525,7 @@
* @param c first coordinate
* @return vector with coordinates calculated by {@code a * c}
*/
- public static Vector3D linearCombination(double a, Vector3D c) {
+ public static Vector3D linearCombination(final double a, final Vector3D c) {
return c.multiply(a);
}
@@ -508,7 +541,8 @@
* @param v2 second coordinate
* @return vector with coordinates calculated by {@code (a1 * v1) + (a2 * v2)}
*/
- public static Vector3D linearCombination(double a1, Vector3D v1, double a2, Vector3D v2) {
+ public static Vector3D linearCombination(final double a1, final Vector3D v1,
+ final double a2, final Vector3D v2) {
return new Vector3D(
LinearCombination.value(a1, v1.x, a2, v2.x),
LinearCombination.value(a1, v1.y, a2, v2.y),
@@ -529,8 +563,9 @@
* @param v3 third coordinate
* @return vector with coordinates calculated by {@code (a1 * v1) + (a2 * v2) + (a3 * v3)}
*/
- public static Vector3D linearCombination(double a1, Vector3D v1, double a2, Vector3D v2,
- double a3, Vector3D v3) {
+ public static Vector3D linearCombination(final double a1, final Vector3D v1,
+ final double a2, final Vector3D v2,
+ final double a3, final Vector3D v3) {
return new Vector3D(
LinearCombination.value(a1, v1.x, a2, v2.x, a3, v3.x),
LinearCombination.value(a1, v1.y, a2, v2.y, a3, v3.y),
@@ -553,8 +588,10 @@
* @param v4 fourth coordinate
* @return point with coordinates calculated by {@code (a1 * v1) + (a2 * v2) + (a3 * v3) + (a4 * v4)}
*/
- public static Vector3D linearCombination(double a1, Vector3D v1, double a2, Vector3D v2,
- double a3, Vector3D v3, double a4, Vector3D v4) {
+ public static Vector3D linearCombination(final double a1, final Vector3D v1,
+ final double a2, final Vector3D v2,
+ final double a3, final Vector3D v3,
+ final double a4, final Vector3D v4) {
return new Vector3D(
LinearCombination.value(a1, v1.x, a2, v2.x, a3, v3.x, a4, v4.x),
LinearCombination.value(a1, v1.y, a2, v2.y, a3, v3.y, a4, v4.y),
@@ -579,7 +616,7 @@
/** Negation of unit vector (coordinates: 0, 0, -1). */
public static final Unit MINUS_Z = new Unit(0d, 0d, -1d);
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180903L;
/** Simple constructor. Callers are responsible for ensuring that the given
@@ -599,9 +636,10 @@
* @param y Vector coordinate.
* @param z Vector coordinate.
* @return a vector whose norm is 1.
- * @throws IllegalNormException if the norm of the given value is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given value
+ * is zero, NaN, or infinite
*/
- public static Unit from(double x, double y, double z) {
+ public static Unit from(final double x, final double y, final double z) {
final double invNorm = 1 / Vectors.checkedNorm(Vectors.norm(x, y, z));
return new Unit(x * invNorm, y * invNorm, z * invNorm);
}
@@ -611,9 +649,10 @@
*
* @param v Vector.
* @return a vector whose norm is 1.
- * @throws IllegalNormException if the norm of the given value is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given
+ * value is zero, NaN, or infinite
*/
- public static Unit from(Vector3D v) {
+ public static Unit from(final Vector3D v) {
return v instanceof Unit ?
(Unit) v :
from(v.getX(), v.getY(), v.getZ());
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/AxisAngleSequence.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/AxisAngleSequence.java
index f59a76f..a4c701b 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/AxisAngleSequence.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/AxisAngleSequence.java
@@ -63,7 +63,7 @@
*/
public final class AxisAngleSequence implements Serializable {
- /** Serializable identifier*/
+ /** Serializable identifier. */
private static final long serialVersionUID = 20181125L;
/** Reference frame for defining axis positions. */
@@ -88,8 +88,8 @@
* @param angle2 angle around the second axis in radians
* @param angle3 angle around the third axis in radians
*/
- public AxisAngleSequence(final AxisReferenceFrame referenceFrame, final AxisSequence axisSequence, final double angle1,
- final double angle2, final double angle3) {
+ public AxisAngleSequence(final AxisReferenceFrame referenceFrame, final AxisSequence axisSequence,
+ final double angle1, final double angle2, final double angle3) {
this.referenceFrame = referenceFrame;
this.axisSequence = axisSequence;
@@ -137,7 +137,7 @@
* @return an array containing the 3 rotation angles
*/
public double[] getAngles() {
- return new double[] { angle1, angle2, angle3 };
+ return new double[]{angle1, angle2, angle3};
}
/** {@inheritDoc} */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/AxisSequence.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/AxisSequence.java
index 813d6c5..6db6243 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/AxisSequence.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/AxisSequence.java
@@ -95,7 +95,7 @@
/** Axis of the third rotation. */
private final Vector3D axis3;
- /** Simple constructor
+ /** Simple constructor.
* @param type the axis sequence type
* @param axis1 first rotation axis
* @param axis2 second rotation axis
@@ -141,6 +141,6 @@
* @return a 3-element array containing the rotation axes in order
*/
public Vector3D[] toArray() {
- return new Vector3D[] { axis1, axis2, axis3 };
+ return new Vector3D[]{axis1, axis2, axis3};
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
index b32762f..0c6f423 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
@@ -20,6 +20,7 @@
import java.util.Objects;
import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.exception.GeometryValueException;
import org.apache.commons.geometry.core.exception.IllegalNormException;
import org.apache.commons.geometry.core.internal.GeometryInternalError;
import org.apache.commons.geometry.euclidean.internal.Vectors;
@@ -40,7 +41,7 @@
*/
public final class QuaternionRotation implements Rotation3D, Serializable {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20181018L;
/** Threshold value for the dot product of antiparallel vectors. If the dot product of two vectors is
@@ -92,8 +93,7 @@
// vector is to just try to normalize it and see if we fail
try {
return Vector3D.Unit.from(quat.getX(), quat.getY(), quat.getZ());
- }
- catch (IllegalNormException exc) {
+ } catch (IllegalNormException exc) {
return Vector3D.Unit.PLUS_X;
}
}
@@ -155,6 +155,70 @@
);
}
+ /** {@inheritDoc}
+ *
+ * <p>This method simply calls {@code apply(vec)} since rotations treat
+ * points and vectors similarly.</p>
+ */
+ @Override
+ public Vector3D applyVector(final Vector3D vec) {
+ return apply(vec);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns true since rotations always preserve the orientation
+ * of the space.</p>
+ */
+ @Override
+ public boolean preservesOrientation() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AffineTransformMatrix3D toMatrix() {
+
+ final double qw = quat.getW();
+ final double qx = quat.getX();
+ final double qy = quat.getY();
+ final double qz = quat.getZ();
+
+ // pre-calculate products that we'll need
+ final double xx = qx * qx;
+ final double xy = qx * qy;
+ final double xz = qx * qz;
+ final double xw = qx * qw;
+
+ final double yy = qy * qy;
+ final double yz = qy * qz;
+ final double yw = qy * qw;
+
+ final double zz = qz * qz;
+ final double zw = qz * qw;
+
+ final double m00 = 1.0 - (2.0 * (yy + zz));
+ final double m01 = 2.0 * (xy - zw);
+ final double m02 = 2.0 * (xz + yw);
+ final double m03 = 0.0;
+
+ final double m10 = 2.0 * (xy + zw);
+ final double m11 = 1.0 - (2.0 * (xx + zz));
+ final double m12 = 2.0 * (yz - xw);
+ final double m13 = 0.0;
+
+ final double m20 = 2.0 * (xz - yw);
+ final double m21 = 2.0 * (yz + xw);
+ final double m22 = 1.0 - (2.0 * (xx + yy));
+ final double m23 = 0.0;
+
+ return AffineTransformMatrix3D.of(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23
+ );
+ }
+
/**
* Multiply this instance by the given argument, returning the result as
* a new instance. This is equivalent to the expression {@code t * q} where
@@ -209,55 +273,6 @@
return new Slerp(quat, end.quat);
}
- /**
- * Return a {@link AffineTransformMatrix3D} that performs the rotation
- * represented by this instance.
- *
- * @return an {@link AffineTransformMatrix3D} instance that performs the rotation
- * represented by this instance
- */
- public AffineTransformMatrix3D toTransformMatrix() {
-
- final double qw = quat.getW();
- final double qx = quat.getX();
- final double qy = quat.getY();
- final double qz = quat.getZ();
-
- // pre-calculate products that we'll need
- final double xx = qx * qx;
- final double xy = qx * qy;
- final double xz = qx * qz;
- final double xw = qx * qw;
-
- final double yy = qy * qy;
- final double yz = qy * qz;
- final double yw = qy * qw;
-
- final double zz = qz * qz;
- final double zw = qz * qw;
-
- final double m00 = 1.0 - (2.0 * (yy + zz));
- final double m01 = 2.0 * (xy - zw);
- final double m02 = 2.0 * (xz + yw);
- final double m03 = 0.0;
-
- final double m10 = 2.0 * (xy + zw);
- final double m11 = 1.0 - (2.0 * (xx + zz));
- final double m12 = 2.0 * (yz - xw);
- final double m13 = 0.0;
-
- final double m20 = 2.0 * (xz - yw);
- final double m21 = 2.0 * (yz + xw);
- final double m22 = 1.0 - (2.0 * (xx + yy));
- final double m23 = 0.0;
-
- return AffineTransformMatrix3D.of(
- m00, m01, m02, m03,
- m10, m11, m12, m13,
- m20, m21, m22, m23
- );
- }
-
/** Get a sequence of axis-angle rotations that produce an overall rotation equivalent to this instance.
*
* <p>
@@ -349,16 +364,13 @@
if (frame == AxisReferenceFrame.RELATIVE) {
if (sequenceType == AxisSequenceType.TAIT_BRYAN) {
return getRelativeTaitBryanAngles(axis1, axis2, axis3);
- }
- else if (sequenceType == AxisSequenceType.EULER) {
+ } else if (sequenceType == AxisSequenceType.EULER) {
return getRelativeEulerAngles(axis1, axis2, axis3);
}
- }
- else if (frame == AxisReferenceFrame.ABSOLUTE) {
+ } else if (frame == AxisReferenceFrame.ABSOLUTE) {
if (sequenceType == AxisSequenceType.TAIT_BRYAN) {
return getAbsoluteTaitBryanAngles(axis1, axis2, axis3);
- }
- else if (sequenceType == AxisSequenceType.EULER) {
+ } else if (sequenceType == AxisSequenceType.EULER) {
return getAbsoluteEulerAngles(axis1, axis2, axis3);
}
}
@@ -394,7 +406,9 @@
final double angle1TanY = vec2.dot(axis1.cross(axis2));
final double angle1TanX = vec2.dot(axis2);
- final double angle2 = angle2Sin > AXIS_ANGLE_SINGULARITY_THRESHOLD ? Geometry.HALF_PI : Geometry.MINUS_HALF_PI;
+ final double angle2 = angle2Sin > AXIS_ANGLE_SINGULARITY_THRESHOLD ?
+ Geometry.HALF_PI :
+ Geometry.MINUS_HALF_PI;
return new double[] {
Math.atan2(angle1TanY, angle1TanX),
@@ -560,7 +574,7 @@
* @return a new instance representing the defined rotation
*
* @throws IllegalNormException if the given axis cannot be normalized
- * @throws IllegalArgumentException if the angle is NaN or infinite
+ * @throws GeometryValueException if the angle is NaN or infinite
*/
public static QuaternionRotation fromAxisAngle(final Vector3D axis, final double angle) {
// reference formula:
@@ -568,7 +582,7 @@
final Vector3D normAxis = axis.normalize();
if (!Double.isFinite(angle)) {
- throw new IllegalArgumentException("Invalid angle: " + angle);
+ throw new GeometryValueException("Invalid angle: " + angle);
}
final double halfAngle = 0.5 * angle;
@@ -766,8 +780,7 @@
y = (m02 - m20) * sinv;
z = (m10 - m01) * sinv;
w = 0.25 * s;
- }
- else if ((m00 > m11) && (m00 > m22)) {
+ } else if ((m00 > m11) && (m00 > m22)) {
// let s = 4*x
final double s = 2.0 * Math.sqrt(1.0 + m00 - m11 - m22);
final double sinv = 1.0 / s;
@@ -776,8 +789,7 @@
y = (m01 + m10) * sinv;
z = (m02 + m20) * sinv;
w = (m21 - m12) * sinv;
- }
- else if (m11 > m22) {
+ } else if (m11 > m22) {
// let s = 4*y
final double s = 2.0 * Math.sqrt(1.0 + m11 - m00 - m22);
final double sinv = 1.0 / s;
@@ -786,8 +798,7 @@
y = 0.25 * s;
z = (m21 + m12) * sinv;
w = (m02 - m20) * sinv;
- }
- else {
+ } else {
// let s = 4*z
final double s = 2.0 * Math.sqrt(1.0 + m22 - m00 - m11);
final double sinv = 1.0 / s;
@@ -813,7 +824,7 @@
int i;
int j;
- for (i=0, j=len-1; i < len / 2; ++i, --j) {
+ for (i = 0, j = len - 1; i < len / 2; ++i, --j) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/Rotation3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/Rotation3D.java
index 9b96ee8..395f9f0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/Rotation3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/Rotation3D.java
@@ -16,16 +16,13 @@
*/
package org.apache.commons.geometry.euclidean.threed.rotation;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.partitioning.Transform;
+import org.apache.commons.geometry.euclidean.threed.Transform3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
/** Interface representing a generic rotation in 3-dimensional Euclidean
* space.
*/
-public interface Rotation3D extends Transform<Vector3D, Vector2D> {
+public interface Rotation3D extends Transform3D {
/** Apply this rotation to the given argument. Since rotations do
* not affect vector magnitudes, this method can be applied to
@@ -61,21 +58,4 @@
* @return the inverse rotation.
*/
Rotation3D inverse();
-
- /** {@inheritDoc}
- * This operation is not supported. See GEOMETRY-24.
- */
- @Override
- default Hyperplane<Vector3D> apply(Hyperplane<Vector3D> hyperplane) {
- throw new UnsupportedOperationException("Transforming hyperplanes is not supported");
- }
-
- /** {@inheritDoc}
- * This operation is not supported. See GEOMETRY-24.
- */
- @Override
- default SubHyperplane<Vector2D> apply(SubHyperplane<Vector2D> sub, Hyperplane<Vector3D> original,
- Hyperplane<Vector3D> transformed) {
- throw new UnsupportedOperationException("Transforming sub-hyperplanes is not supported");
- }
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AbstractSegmentConnector.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AbstractSegmentConnector.java
new file mode 100644
index 0000000..e7cbef2
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AbstractSegmentConnector.java
@@ -0,0 +1,306 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.geometry.euclidean.internal.AbstractPathConnector;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
+
+/** Abstract class for joining collections of line segments into connected
+ * paths. This class is not thread-safe.
+ */
+public abstract class AbstractSegmentConnector
+ extends AbstractPathConnector<AbstractSegmentConnector.ConnectableSegment> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190528L;
+
+ /** Add a line segment to the connector, leaving it unconnected until a later call to
+ * to {@link #connect(Iterable)} or {@link #connectAll()}.
+ * @param segment line segment to add
+ * @see #connect(Iterable)
+ * @see #connectAll()
+ */
+ public void add(final Segment segment) {
+ addPathElement(new ConnectableSegment(segment));
+ }
+
+ /** Add a collection of line segments to the connector, leaving them unconnected
+ * until a later call to {@link #connect(Iterable)} or
+ * {@link #connectAll()}.
+ * @param segments line segments to add
+ * @see #connect(Iterable)
+ * @see #connectAll()
+ * @see #add(Segment)
+ */
+ public void add(final Iterable<Segment> segments) {
+ for (Segment segment : segments) {
+ add(segment);
+ }
+ }
+
+ /** Add a collection of line segments to the connector and attempt to connect each new
+ * segment with existing segments. Connections made at this time will not be
+ * overwritten by subsequent calls to this or other connection methods.
+ * (eg, {@link #connectAll()}).
+ *
+ * <p>The connector is not reset by this call. Additional segments can still be added
+ * to the current set of paths.</p>
+ * @param segments line segments to connect
+ * @see #connectAll()
+ */
+ public void connect(final Iterable<Segment> segments) {
+ List<ConnectableSegment> newEntries = new ArrayList<>();
+
+ for (Segment segment : segments) {
+ newEntries.add(new ConnectableSegment(segment));
+ }
+
+ connectPathElements(newEntries);
+ }
+
+ /** Add the given line segments to this instance and connect all current
+ * segments into polylines (ie, line segment paths). This call is equivalent to
+ * <pre>
+ * connector.add(segments);
+ * List<Polyline> result = connector.connectAll();
+ * </pre>
+ *
+ * <p>The connector is reset after this call. Further calls to
+ * add or connect line segments will result in new paths being
+ * generated.</p>
+ * @param segments line segments to add
+ * @return the connected line segment paths
+ * @see #add(Iterable)
+ * @see #connectAll()
+ */
+ public List<Polyline> connectAll(final Iterable<Segment> segments) {
+ add(segments);
+ return connectAll();
+ }
+
+ /** Connect all current segments into connected paths, returning the result as a
+ * list of polylines.
+ *
+ * <p>The connector is reset after this call. Further calls to
+ * add or connect line segments will result in new paths being
+ * generated.</p>
+ * @return the connected line segments paths
+ */
+ public List<Polyline> connectAll() {
+ final List<ConnectableSegment> roots = computePathRoots();
+ final List<Polyline> paths = new ArrayList<>(roots.size());
+
+ for (ConnectableSegment root : roots) {
+ paths.add(toPolyline(root));
+ }
+
+ return paths;
+ }
+
+ /** Convert the linked list of path elements starting at the argument
+ * into a {@link Polyline}.
+ * @param root root of a connected path linked list
+ * @return a polyline representing the linked list path
+ */
+ private Polyline toPolyline(final ConnectableSegment root) {
+ final Polyline.Builder builder = Polyline.builder(null);
+
+ builder.append(root.getSegment());
+
+ ConnectableSegment current = root.getNext();
+
+ while (current != null && current != root) {
+ builder.append(current.getSegment());
+ current = current.getNext();
+ }
+
+ return builder.build();
+ }
+
+ /** Internal class used to connect line segments together.
+ */
+ protected static class ConnectableSegment extends AbstractPathConnector.ConnectableElement<ConnectableSegment> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191107L;
+
+ /** Segment start point. This will be used to connect to other path elements. */
+ private final Vector2D start;
+
+ /** Line segment for the entry. */
+ private final Segment segment;
+
+ /** Create a new instance with the given start point. This constructor is
+ * intended only for performing searches for other path elements.
+ * @param start start point
+ */
+ public ConnectableSegment(final Vector2D start) {
+ this(start, null);
+ }
+
+ /** Create a new instance from the given line segment.
+ * @param segment line segment
+ */
+ public ConnectableSegment(final Segment segment) {
+ this(segment.getStartPoint(), segment);
+ }
+
+ /** Create a new instance with the given start point and line segment.
+ * @param start start point
+ * @param segment line segment
+ */
+ private ConnectableSegment(final Vector2D start, final Segment segment) {
+ this.start = start;
+ this.segment = segment;
+ }
+
+ /** Get the line segment for this instance.
+ * @return the line segment for this instance
+ */
+ public Segment getSegment() {
+ return segment;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasStart() {
+ return start != null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasEnd() {
+ return segment != null && segment.getEndPoint() != null;
+ }
+
+ /** Return true if this instance has a size equivalent to zero.
+ * @return true if this instance has a size equivalent to zero.
+ */
+ public boolean hasZeroSize() {
+ return segment != null && segment.getPrecision().eqZero(segment.getSize());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean endPointsEq(final ConnectableSegment other) {
+ if (hasEnd() && other.hasEnd()) {
+ return segment.getEndPoint()
+ .eq(other.segment.getEndPoint(), segment.getPrecision());
+ }
+
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean canConnectTo(final ConnectableSegment next) {
+ final Vector2D end = segment.getEndPoint();
+ final Vector2D nextStart = next.start;
+
+ return end != null && nextStart != null &&
+ end.eq(nextStart, segment.getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getRelativeAngle(final ConnectableSegment next) {
+ return segment.getLine().angle(next.getSegment().getLine());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConnectableSegment getConnectionSearchKey() {
+ return new ConnectableSegment(segment.getEndPoint());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean shouldContinueConnectionSearch(final ConnectableSegment candidate, final boolean ascending) {
+
+ if (candidate.hasStart()) {
+ final double candidateX = candidate.getSegment().getStartPoint().getX();
+ final double thisX = segment.getEndPoint().getX();
+ final int cmp = segment.getPrecision().compare(candidateX, thisX);
+
+ return ascending ? cmp <= 0 : cmp >= 0;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int compareTo(ConnectableSegment other) {
+ // sort by coordinates
+ int cmp = Vector2D.COORDINATE_ASCENDING_ORDER.compare(start, other.start);
+ if (cmp == 0) {
+ // sort entries without segments before ones with segments
+ final boolean thisHasSegment = segment != null;
+ final boolean otherHasSegment = other.segment != null;
+
+ cmp = Boolean.compare(thisHasSegment, otherHasSegment);
+
+ if (cmp == 0 && thisHasSegment) {
+ // place point-like segments before ones with non-zero length
+ cmp = Boolean.compare(this.hasZeroSize(), other.hasZeroSize());
+
+ if (cmp == 0) {
+ // sort by line angle
+ final double aAngle = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(
+ this.getSegment().getLine().getAngle());
+ final double bAngle = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(
+ other.getSegment().getLine().getAngle());
+
+ cmp = Double.compare(aAngle, bAngle);
+ }
+ }
+ }
+ return cmp;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(start, segment);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !this.getClass().equals(obj.getClass())) {
+ return false;
+ }
+
+ final ConnectableSegment other = (ConnectableSegment) obj;
+ return Objects.equals(this.start, other.start) &&
+ Objects.equals(this.segment, other.segment);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected ConnectableSegment getSelf() {
+ return this;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AbstractSubLine.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AbstractSubLine.java
new file mode 100644
index 0000000..f039905
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AbstractSubLine.java
@@ -0,0 +1,131 @@
+/*
+ * 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;
+
+import java.util.function.BiFunction;
+
+import org.apache.commons.geometry.core.partitioning.AbstractEmbeddingSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.twod.SubLine.SubLineBuilder;
+
+/** Internal base class for subline implementations.
+ */
+abstract class AbstractSubLine extends AbstractEmbeddingSubHyperplane<Vector2D, Vector1D, Line> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190729L;
+
+ /** The line defining this instance. */
+ private final Line line;
+
+ /** Construct a new instance based on the given line.
+ * @param line line forming the base of the instance
+ */
+ AbstractSubLine(final Line line) {
+ this.line = line;
+ }
+
+ /** Get the line that this segment lies on. This method is an alias
+ * for {@link #getHyperplane()}.
+ * @return the line that this segment lies on
+ * @see #getHyperplane()
+ */
+ public Line getLine() {
+ return getHyperplane();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Line getHyperplane() {
+ return line;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubLineBuilder builder() {
+ return new SubLineBuilder(line);
+ }
+
+ /** Return the object used to perform floating point comparisons, which is the
+ * same object used by the underlying {@link Line}).
+ * @return precision object used to perform floating point comparisons.
+ */
+ public DoublePrecisionContext getPrecision() {
+ return line.getPrecision();
+ }
+
+ /** Generic, internal split method. Subclasses should call this from their
+ * {@link #split(Hyperplane)} methods.
+ * @param splitter splitting hyperplane
+ * @param thisInstance a reference to the current instance; this is passed as
+ * an argument in order to allow it to be a generic type
+ * @param factory function used to create new subhyperplane instances
+ * @param <T> Subline implementation type
+ * @return the result of the split operation
+ */
+ protected <T extends AbstractSubLine> Split<T> splitInternal(final Hyperplane<Vector2D> splitter,
+ final T thisInstance, final BiFunction<Line, HyperplaneBoundedRegion<Vector1D>, T> factory) {
+
+ final Line thisLine = getLine();
+ final Line splitterLine = (Line) splitter;
+ final DoublePrecisionContext precision = getPrecision();
+
+ final Vector2D intersection = splitterLine.intersection(thisLine);
+ if (intersection == null) {
+ // the lines are parallel or coincident; check which side of
+ // the splitter we lie on
+ final double offset = splitterLine.offset(thisLine);
+ final int comp = precision.compare(offset, 0.0);
+
+ if (comp < 0) {
+ return new Split<>(thisInstance, null);
+ } else if (comp > 0) {
+ return new Split<>(null, thisInstance);
+ } else {
+ return new Split<>(null, null);
+ }
+ } else {
+ // the lines intersect; split the subregion
+ final Vector1D splitPt = thisLine.toSubspace(intersection);
+ final boolean positiveFacing = thisLine.angle(splitterLine) > 0.0;
+
+ final OrientedPoint subspaceSplitter = OrientedPoint.fromPointAndDirection(splitPt,
+ positiveFacing, getPrecision());
+
+ final Split<? extends HyperplaneBoundedRegion<Vector1D>> split =
+ thisInstance.getSubspaceRegion().split(subspaceSplitter);
+ final SplitLocation subspaceSplitLoc = split.getLocation();
+
+ if (SplitLocation.MINUS == subspaceSplitLoc) {
+ return new Split<>(thisInstance, null);
+ } else if (SplitLocation.PLUS == subspaceSplitLoc) {
+ return new Split<>(null, thisInstance);
+ }
+
+ final T minus = (split.getMinus() != null) ? factory.apply(thisLine, split.getMinus()) : null;
+ final T plus = (split.getPlus() != null) ? factory.apply(thisLine, split.getPlus()) : null;
+
+ return new Split<>(minus, plus);
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java
index ae80335..e32bfe2 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java
@@ -19,11 +19,10 @@
import java.io.Serializable;
import org.apache.commons.geometry.core.internal.DoubleFunction2N;
-import org.apache.commons.geometry.euclidean.AffineTransformMatrix;
+import org.apache.commons.geometry.euclidean.AbstractAffineTransformMatrix;
import org.apache.commons.geometry.euclidean.exception.NonInvertibleTransformException;
import org.apache.commons.geometry.euclidean.internal.Matrices;
import org.apache.commons.geometry.euclidean.internal.Vectors;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.numbers.arrays.LinearCombination;
import org.apache.commons.numbers.core.Precision;
@@ -35,24 +34,25 @@
* use arrays containing 6 elements, instead of 9.
* </p>
*/
-public final class AffineTransformMatrix2D implements AffineTransformMatrix<Vector2D, Vector1D>, Serializable {
+public final class AffineTransformMatrix2D extends AbstractAffineTransformMatrix<Vector2D>
+ implements Transform2D, Serializable {
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20181005L;
- /** The number of internal matrix elements */
+ /** The number of internal matrix elements. */
private static final int NUM_ELEMENTS = 6;
- /** String used to start the transform matrix string representation */
+ /** String used to start the transform matrix string representation. */
private static final String MATRIX_START = "[ ";
- /** String used to end the transform matrix string representation */
+ /** String used to end the transform matrix string representation. */
private static final String MATRIX_END = " ]";
- /** String used to separate elements in the matrix string representation */
+ /** String used to separate elements in the matrix string representation. */
private static final String ELEMENT_SEPARATOR = ", ";
- /** String used to separate rows in the matrix string representation */
+ /** String used to separate rows in the matrix string representation. */
private static final String ROW_SEPARATOR = "; ";
/** Shared transform set to the identity matrix. */
@@ -61,18 +61,18 @@
0, 1, 0
);
- /** Transform matrix entry <code>m<sub>0,0</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,0</sub></code>. */
private final double m00;
- /** Transform matrix entry <code>m<sub>0,1</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,1</sub></code>. */
private final double m01;
- /** Transform matrix entry <code>m<sub>0,2</sub></code> */
+ /** Transform matrix entry <code>m<sub>0,2</sub></code>. */
private final double m02;
- /** Transform matrix entry <code>m<sub>1,0</sub></code> */
+ /** Transform matrix entry <code>m<sub>1,0</sub></code>. */
private final double m10;
- /** Transform matrix entry <code>m<sub>1,1</sub></code> */
+ /** Transform matrix entry <code>m<sub>1,1</sub></code>. */
private final double m11;
- /** Transform matrix entry <code>m<sub>1,2</sub></code> */
+ /** Transform matrix entry <code>m<sub>1,2</sub></code>. */
private final double m12;
/**
@@ -112,8 +112,8 @@
*/
public double[] toArray() {
return new double[] {
- m00, m01, m02,
- m10, m11, m12
+ m00, m01, m02,
+ m10, m11, m12
};
}
@@ -163,10 +163,28 @@
* @see #applyVector(Vector2D)
*/
@Override
- public Vector2D applyDirection(final Vector2D vec) {
+ public Vector2D.Unit applyDirection(final Vector2D vec) {
return applyVector(vec, Vector2D.Unit::from);
}
+ /** {@inheritDoc} */
+ @Override
+ public double determinant() {
+ return Matrices.determinant(
+ m00, m01,
+ m10, m11
+ );
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This simply returns the current instance.</p>
+ */
+ @Override
+ public AffineTransformMatrix2D toMatrix() {
+ return this;
+ }
+
/** Apply a translation to the current instance, returning the result as a new transform.
* @param translation vector containing the translation values for each axis
* @return a new transform containing the result of applying a translation to
@@ -281,11 +299,7 @@
// Our full matrix is 3x3 but we can significantly reduce the amount of computations
// needed here since we know that our last row is [0 0 1].
- // compute the determinant of the matrix
- final double det = Matrices.determinant(
- m00, m01,
- m10, m11
- );
+ final double det = determinant();
if (!Vectors.isRealNonZero(det)) {
throw new NonInvertibleTransformException("Transform is not invertible; matrix determinant is " + det);
@@ -301,13 +315,13 @@
final double invDet = 1.0 / det;
final double c00 = invDet * m11;
- final double c01 = - invDet * m10;
+ final double c01 = -invDet * m10;
- final double c10 = - invDet * m01;
+ final double c10 = -invDet * m01;
final double c11 = invDet * m00;
final double c20 = invDet * Matrices.determinant(m01, m02, m11, m12);
- final double c21 = - invDet * Matrices.determinant(m00, m02, m10, m12);
+ final double c21 = -invDet * Matrices.determinant(m00, m02, m10, m12);
return new AffineTransformMatrix2D(
c00, c10, c20,
@@ -403,7 +417,7 @@
* @return a new transform initialized with the given matrix values
* @throws IllegalArgumentException if the array does not have 6 elements
*/
- public static AffineTransformMatrix2D of(final double ... arr) {
+ public static AffineTransformMatrix2D of(final double... arr) {
if (arr.length != NUM_ELEMENTS) {
throw new IllegalArgumentException("Dimension mismatch: " + arr.length + " != " + NUM_ELEMENTS);
}
@@ -537,7 +551,8 @@
* @param b second transform
* @return the transform computed as {@code a x b}
*/
- private static AffineTransformMatrix2D multiply(final AffineTransformMatrix2D a, final AffineTransformMatrix2D b) {
+ private static AffineTransformMatrix2D multiply(final AffineTransformMatrix2D a,
+ final AffineTransformMatrix2D b) {
final double c00 = LinearCombination.value(a.m00, b.m00, a.m01, b.m10);
final double c01 = LinearCombination.value(a.m00, b.m01, a.m01, b.m11);
@@ -561,7 +576,8 @@
*/
private static void validateElementForInverse(final double element) {
if (!Double.isFinite(element)) {
- throw new NonInvertibleTransformException("Transform is not invertible; invalid matrix element: " + element);
+ throw new NonInvertibleTransformException(
+ "Transform is not invertible; invalid matrix element: " + element);
}
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
new file mode 100644
index 0000000..b152b05
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
@@ -0,0 +1,294 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Class representing a finite or infinite convex area in Euclidean 2D space.
+ * The boundaries of this area, if any, are composed of line segments.
+ */
+public final class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D, Segment> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190619L;
+
+ /** Instance representing the full 2D plane. */
+ private static final ConvexArea FULL = new ConvexArea(Collections.emptyList());
+
+ /** Simple constructor. Callers are responsible for ensuring that the given path
+ * represents the boundary of a convex area. No validation is performed.
+ * @param boundaries the boundaries of the convex area
+ */
+ private ConvexArea(final List<Segment> boundaries) {
+ super(boundaries);
+ }
+
+ /** Get the connected line segment paths comprising the boundary of the area. The
+ * segments are oriented so that their minus sides point toward the interior of the
+ * region. The size of the returned list is
+ * <ul>
+ * <li><strong>0</strong> if the convex area is full,</li>
+ * <li><strong>1</strong> if at least one boundary is present and
+ * a single path can connect all segments (this will be the case
+ * for most instances), and</li>
+ * <li><strong>2</strong> if only two boundaries exist and they are
+ * parallel to each other (in which case they cannot be connected
+ * as a single path).</li>
+ * </ul>
+ * @return the line segment paths comprising the boundary of the area.
+ */
+ public List<Polyline> getBoundaryPaths() {
+ return InteriorAngleSegmentConnector.connectMinimized(getBoundaries());
+ }
+
+ /** Get the vertices for the area. The vertices lie at the intersections of the
+ * area bounding lines.
+ * @return the vertices for the area
+ */
+ public List<Vector2D> getVertices() {
+ final List<Polyline> path = getBoundaryPaths();
+
+ // we will only have vertices if we have a single path; otherwise, we have a full
+ // area or two non-intersecting infinite segments
+ if (path.size() == 1) {
+ return path.get(0).getVertices();
+ }
+
+ return Collections.emptyList();
+ }
+
+ /** Return a new instance transformed by the argument.
+ * @param transform transform to apply
+ * @return a new instance transformed by the argument
+ */
+ public ConvexArea transform(final Transform<Vector2D> transform) {
+ return transformInternal(transform, this, Segment.class, ConvexArea::new);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Segment trim(final ConvexSubHyperplane<Vector2D> convexSubHyperplane) {
+ return (Segment) super.trim(convexSubHyperplane);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ if (isFull()) {
+ return Double.POSITIVE_INFINITY;
+ }
+
+ double quadrilateralAreaSum = 0.0;
+
+ for (Segment segment : getBoundaries()) {
+ if (segment.isInfinite()) {
+ return Double.POSITIVE_INFINITY;
+ }
+
+ quadrilateralAreaSum += segment.getStartPoint().signedArea(segment.getEndPoint());
+ }
+
+ return 0.5 * quadrilateralAreaSum;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D getBarycenter() {
+ List<Segment> boundaries = getBoundaries();
+
+ if (!boundaries.isEmpty()) {
+ double quadrilateralAreaSum = 0.0;
+ double scaledSumX = 0.0;
+ double scaledSumY = 0.0;
+
+ double signedArea;
+ Vector2D startPoint;
+ Vector2D endPoint;
+
+ for (Segment seg : boundaries) {
+ if (seg.isInfinite()) {
+ // infinite => no barycenter
+ return null;
+ }
+
+ startPoint = seg.getStartPoint();
+ endPoint = seg.getEndPoint();
+
+ signedArea = startPoint.signedArea(endPoint);
+
+ quadrilateralAreaSum += signedArea;
+
+ scaledSumX += signedArea * (startPoint.getX() + endPoint.getX());
+ scaledSumY += signedArea * (startPoint.getY() + endPoint.getY());
+ }
+
+ return Vector2D.of(scaledSumX, scaledSumY).multiply(1.0 / (3.0 * quadrilateralAreaSum));
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<ConvexArea> split(final Hyperplane<Vector2D> splitter) {
+ return splitInternal(splitter, this, Segment.class, ConvexArea::new);
+ }
+
+ /** Return a BSP tree instance representing the same region as the current instance.
+ * @return a BSP tree instance representing the same region as the current instance
+ */
+ public RegionBSPTree2D toTree() {
+ return RegionBSPTree2D.from(this);
+ }
+
+ /** Return an instance representing the full 2D area.
+ * @return an instance representing the full 2D area.
+ */
+ public static ConvexArea full() {
+ return FULL;
+ }
+
+ /** Construct a convex area by creating lines between adjacent vertices. The vertices must be given in a
+ * counter-clockwise around order the interior of the shape. If the area is intended to be closed, the
+ * beginning point must be repeated at the end of the path.
+ * @param vertices vertices to use to construct the area
+ * @param precision precision context used to create new line instances
+ * @return a convex area constructed using lines between adjacent vertices
+ * @see #fromVertexLoop(Collection, DoublePrecisionContext)
+ * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+ */
+ public static ConvexArea fromVertices(final Collection<Vector2D> vertices,
+ final DoublePrecisionContext precision) {
+ return fromVertices(vertices, false, precision);
+ }
+
+ /** Construct a convex area by creating lines between adjacent vertices. An implicit line is created between the
+ * last vertex given and the first one. The vertices must be given in a counter-clockwise around order the interior
+ * of the shape.
+ * @param vertices vertices to use to construct the area
+ * @param precision precision context used to create new line instances
+ * @return a convex area constructed using lines between adjacent vertices
+ * @see #fromVertices(Collection, DoublePrecisionContext)
+ * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+ */
+ public static ConvexArea fromVertexLoop(final Collection<Vector2D> vertices,
+ final DoublePrecisionContext precision) {
+ return fromVertices(vertices, true, precision);
+ }
+
+ /** Construct a convex area from lines between adjacent vertices.
+ * @param vertices vertices to use to construct the area
+ * @param close if true, an additional line will be created between the last and first vertex
+ * @param precision precision context used to create new line instances
+ * @return a convex area constructed using lines between adjacent vertices
+ */
+ public static ConvexArea fromVertices(final Collection<Vector2D> vertices, boolean close,
+ final DoublePrecisionContext precision) {
+ if (vertices.isEmpty()) {
+ return full();
+ }
+
+ final List<Line> lines = new ArrayList<>();
+
+ Vector2D first = null;
+ Vector2D prev = null;
+ Vector2D cur = null;
+
+ for (Vector2D vertex : vertices) {
+ cur = vertex;
+
+ if (first == null) {
+ first = cur;
+ }
+
+ if (prev != null && !cur.eq(prev, precision)) {
+ lines.add(Line.fromPoints(prev, cur, precision));
+ }
+
+ prev = cur;
+ }
+
+ if (close && cur != null && !cur.eq(first, precision)) {
+ lines.add(Line.fromPoints(cur, first, precision));
+ }
+
+ if (!vertices.isEmpty() && lines.isEmpty()) {
+ throw new IllegalStateException("Unable to create convex area: only a single unique vertex provided");
+ }
+
+ return fromBounds(lines);
+ }
+
+ /** Construct a convex area from a line segment path. The area represents the intersection of all of the negative
+ * half-spaces of the lines in the path. The boundaries of the returned area may therefore not match the line
+ * segments in the path.
+ * @param path path to construct the area from
+ * @return a convex area constructed from the lines in the given path
+ */
+ public static ConvexArea fromPath(final Polyline path) {
+ final List<Line> lines = new ArrayList<>();
+ for (Segment segment : path) {
+ lines.add(segment.getLine());
+ }
+
+ return fromBounds(lines);
+ }
+
+ /** Create a convex area formed by the intersection of the negative half-spaces of the
+ * given bounding lines. The returned instance represents the area that is on the
+ * minus side of all of the given lines. Note that this method does not support areas
+ * of zero size (ie, infinitely thin areas or points.)
+ * @param bounds lines used to define the convex area
+ * @return a new convex area instance representing the area on the minus side of all
+ * of the bounding lines or an instance representing the full area if no lines are
+ * given
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if the given set of bounding lines do
+ * not form a convex area, meaning that there is no region that is on the minus side of all of the bounding
+ * lines.
+ */
+ public static ConvexArea fromBounds(final Line... bounds) {
+ return fromBounds(Arrays.asList(bounds));
+ }
+
+ /** Create a convex area formed by the intersection of the negative half-spaces of the
+ * given bounding lines. The returned instance represents the area that is on the
+ * minus side of all of the given lines. Note that this method does not support areas
+ * of zero size (ie, infinitely thin areas or points.)
+ * @param bounds lines used to define the convex area
+ * @return a new convex area instance representing the area on the minus side of all
+ * of the bounding lines or an instance representing the full area if the collection
+ * is empty
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if the given set of bounding lines do
+ * not form a convex area, meaning that there is no region that is on the minus side of all of the bounding
+ * lines.
+ */
+ public static ConvexArea fromBounds(final Iterable<Line> bounds) {
+ final List<Segment> segments = new ConvexRegionBoundaryBuilder<>(Segment.class).build(bounds);
+ return segments.isEmpty() ? full() : new ConvexArea(segments);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/FunctionTransform2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/FunctionTransform2D.java
new file mode 100644
index 0000000..7062f1c
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/FunctionTransform2D.java
@@ -0,0 +1,103 @@
+/*
+ * 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;
+
+import java.util.function.Function;
+
+import org.apache.commons.geometry.euclidean.internal.Matrices;
+
+/** Class that wraps a {@link Function} with the {@link Transform2D} interface.
+ */
+public final class FunctionTransform2D implements Transform2D {
+
+ /** Static instance representing the identity transform. */
+ private static final FunctionTransform2D IDENTITY =
+ new FunctionTransform2D(Function.identity(), true, Vector2D.ZERO);
+
+ /** The underlying function for the transform. */
+ private final Function<Vector2D, Vector2D> fn;
+
+ /** True if the transform preserves spatial orientation. */
+ private final boolean preservesOrientation;
+
+ /** The translation component of the transform. */
+ private final Vector2D translation;
+
+ /** Construct a new instance from its component parts. No validation of the input is performed.
+ * @param fn the underlying function for the transform
+ * @param preservesOrientation true if the transform preserves spatial orientation
+ * @param translation the translation component of the transform
+ */
+ private FunctionTransform2D(final Function<Vector2D, Vector2D> fn, final boolean preservesOrientation,
+ final Vector2D translation) {
+ this.fn = fn;
+ this.preservesOrientation = preservesOrientation;
+ this.translation = translation;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D apply(final Vector2D pt) {
+ return fn.apply(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D applyVector(final Vector2D vec) {
+ return apply(vec).subtract(translation);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean preservesOrientation() {
+ return preservesOrientation;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AffineTransformMatrix2D toMatrix() {
+ final Vector2D u = applyVector(Vector2D.Unit.PLUS_X);
+ final Vector2D v = applyVector(Vector2D.Unit.PLUS_Y);
+
+ return AffineTransformMatrix2D.fromColumnVectors(u, v, translation);
+ }
+
+ /** Return an instance representing the identity transform.
+ * @return an instance representing the identity transform
+ */
+ public static FunctionTransform2D identity() {
+ return IDENTITY;
+ }
+
+ /** Construct a new transform instance from the given function.
+ * @param fn the function to use for the transform
+ * @return a new transform instance using the given function
+ */
+ public static FunctionTransform2D from(final Function<Vector2D, Vector2D> fn) {
+ final Vector2D tPlusX = fn.apply(Vector2D.Unit.PLUS_X);
+ final Vector2D tPlusY = fn.apply(Vector2D.Unit.PLUS_Y);
+ final Vector2D tZero = fn.apply(Vector2D.ZERO);
+
+ final double det = Matrices.determinant(
+ tPlusX.getX(), tPlusY.getX(),
+ tPlusX.getY(), tPlusY.getY()
+ );
+ final boolean preservesOrientation = det > 0;
+
+ return new FunctionTransform2D(fn, preservesOrientation, tZero);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/InteriorAngleSegmentConnector.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/InteriorAngleSegmentConnector.java
new file mode 100644
index 0000000..65eff62
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/InteriorAngleSegmentConnector.java
@@ -0,0 +1,125 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+
+/** Line segment connector that selects between multiple connection options
+ * based on the resulting interior angle. An interior angle in this
+ * case is the angle created between an incoming segment and an outgoing segment
+ * as measured on the minus (interior) side of the incoming line. If looking
+ * along the direction of the incoming line segment, smaller interior angles
+ * point more to the left and larger ones point more to the right.
+ *
+ * <p>This class provides two concrete implementations: {@link Maximize} and
+ * {@link Minimize}, which choose connections with the largest or smallest interior
+ * angles respectively.
+ * </p>
+ */
+public abstract class InteriorAngleSegmentConnector extends AbstractSegmentConnector {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190530L;
+
+ /** {@inheritDoc} */
+ @Override
+ protected ConnectableSegment selectConnection(ConnectableSegment incoming, List<ConnectableSegment> outgoing) {
+
+ // search for the best connection
+ final Line segmentLine = incoming.getSegment().getLine();
+
+ double selectedInteriorAngle = Double.POSITIVE_INFINITY;
+ ConnectableSegment selected = null;
+
+ for (ConnectableSegment candidate : outgoing) {
+ double interiorAngle = Geometry.PI - segmentLine.angle(candidate.getSegment().getLine());
+
+ if (selected == null || isBetterAngle(interiorAngle, selectedInteriorAngle)) {
+ selectedInteriorAngle = interiorAngle;
+ selected = candidate;
+ }
+ }
+
+ return selected;
+ }
+
+ /** Return true if {@code newAngle} represents a better interior angle than {@code previousAngle}.
+ * @param newAngle the new angle under consideration
+ * @param previousAngle the previous best angle
+ * @return true if {@code newAngle} represents a better interior angle than {@code previousAngle}
+ */
+ protected abstract boolean isBetterAngle(double newAngle, double previousAngle);
+
+ /** Convenience method for connecting a set of line segments with interior angles maximized
+ * when possible. This method is equivalent to {@code new Maximize().connect(segments)}.
+ * @param segments line segments to connect
+ * @return a list of connected line segment paths
+ * @see Maximize
+ */
+ public static List<Polyline> connectMaximized(final Collection<Segment> segments) {
+ return new Maximize().connectAll(segments);
+ }
+
+ /** Convenience method for connecting a set of line segments with interior angles minimized
+ * when possible. This method is equivalent to {@code new Minimize().connect(segments)}.
+ * @param segments line segments to connect
+ * @return a list of connected line segment paths
+ * @see Minimize
+ */
+ public static List<Polyline> connectMinimized(final Collection<Segment> segments) {
+ return new Minimize().connectAll(segments);
+ }
+
+ /** Implementation of {@link InteriorAngleSegmentConnector} that chooses line segment
+ * connections that produce the largest interior angles. Another way to visualize this is
+ * that when presented multiple connection options for a given line segment, this class will
+ * choose the option that points most to the right when viewed in the direction of the incoming
+ * line segment.
+ */
+ public static class Maximize extends InteriorAngleSegmentConnector {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190530L;
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean isBetterAngle(double newAngle, double previousAngle) {
+ return newAngle > previousAngle;
+ }
+ }
+
+ /** Implementation of {@link InteriorAngleSegmentConnector} that chooses line segment
+ * connections that produce the smallest interior angles. Another way to visualize this is
+ * that when presented multiple connection options for a given line segment, this class will
+ * choose the option that points most to the left when viewed in the direction of the incoming
+ * line segment.
+ */
+ public static class Minimize extends InteriorAngleSegmentConnector {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190530L;
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean isBetterAngle(double newAngle, double previousAngle) {
+ return newAngle < previousAngle;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
index 3ac9d9d..16c370f 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
@@ -19,14 +19,15 @@
import java.io.Serializable;
import java.util.Objects;
+import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.exception.GeometryValueException;
-import org.apache.commons.geometry.core.partitioning.Embedding;
+import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.partitioning.Transform;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
+import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
+import org.apache.commons.geometry.euclidean.oned.Interval;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.numbers.angle.PlaneAngleRadians;
import org.apache.commons.numbers.arrays.LinearCombination;
@@ -57,7 +58,8 @@
* points with negative offsets and the right half plane is the set of
* points with positive offsets.</p>
*/
-public final class Line implements Hyperplane<Vector2D>, Embedding<Vector2D, Vector1D>, Serializable {
+public final class Line extends AbstractHyperplane<Vector2D>
+ implements EmbeddingHyperplane<Vector2D, Vector1D>, Equivalency<Line> {
/** Serializable UID. */
private static final long serialVersionUID = 20190120L;
@@ -68,18 +70,16 @@
/** The distance between the origin and the line. */
private final double originOffset;
- /** Precision context used to compare floating point numbers. */
- private final DoublePrecisionContext precision;
-
/** Simple constructor.
* @param direction The direction of the line.
* @param originOffset The signed distance between the line and the origin.
* @param precision Precision context used to compare floating point numbers.
*/
private Line(final Vector2D direction, final double originOffset, final DoublePrecisionContext precision) {
+ super(precision);
+
this.direction = direction;
this.originOffset = originOffset;
- this.precision = precision;
}
/** Get the angle of the line in radians with respect to the abscissa (+x) axis. The
@@ -127,35 +127,154 @@
/** {@inheritDoc} */
@Override
- public DoublePrecisionContext getPrecision() {
- return precision;
- }
-
- /** {@inheritDoc} */
- @Override
- public Line copySelf() {
- return this;
- }
-
- /** Get the reverse of the instance, meaning a line containing the same
- * points but with the opposite orientation.
- * @return a new line, with orientation opposite to the instance orientation
- */
public Line reverse() {
- return new Line(direction.negate(), -originOffset, precision);
+ return new Line(direction.negate(), -originOffset, getPrecision());
}
/** {@inheritDoc} */
@Override
- public Vector1D toSubSpace(final Vector2D point) {
- return Vector1D.of(direction.dot(point));
+ public Line transform(final Transform<Vector2D> transform) {
+ final Vector2D origin = getOrigin();
+
+ final Vector2D tOrigin = transform.apply(origin);
+ final Vector2D tOriginPlusDir = transform.apply(origin.add(getDirection()));
+
+ return fromPoints(tOrigin, tOriginPlusDir, getPrecision());
+ }
+
+ /** Get an object containing the current line transformed by the argument along with a
+ * 1D transform that can be applied to subspace points. The subspace transform transforms
+ * subspace points such that their 2D location in the transformed line is the same as their
+ * 2D location in the original line after the 2D transform is applied. For example, consider
+ * the code below:
+ * <pre>
+ * SubspaceTransform st = line.subspaceTransform(transform);
+ *
+ * Vector1D subPt = Vector1D.of(1);
+ *
+ * Vector2D a = transform.apply(line.toSpace(subPt)); // transform in 2D space
+ * Vector2D b = st.getLine().toSpace(st.getTransform().apply(subPt)); // transform in 1D space
+ * </pre>
+ * At the end of execution, the points {@code a} (which was transformed using the original
+ * 2D transform) and {@code b} (which was transformed in 1D using the subspace transform)
+ * are equivalent.
+ *
+ * @param transform the transform to apply to this instance
+ * @return an object containing the transformed line along with a transform that can be applied
+ * to subspace points
+ * @see #transform(Transform)
+ */
+ public SubspaceTransform subspaceTransform(final Transform<Vector2D> transform) {
+ final Vector2D origin = getOrigin();
+
+ final Vector2D p1 = transform.apply(origin);
+ final Vector2D p2 = transform.apply(origin.add(direction));
+
+ final Line tLine = Line.fromPoints(p1, p2, getPrecision());
+
+ final Vector1D tSubspaceOrigin = tLine.toSubspace(p1);
+ final Vector1D tSubspaceDirection = tSubspaceOrigin.vectorTo(tLine.toSubspace(p2));
+
+ final double translation = tSubspaceOrigin.getX();
+ final double scale = tSubspaceDirection.getX();
+
+ final AffineTransformMatrix1D subspaceTransform = AffineTransformMatrix1D.of(scale, translation);
+
+ return new SubspaceTransform(tLine, subspaceTransform);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Segment span() {
+ return segment(Interval.full());
+ }
+
+ /** Create a new line segment from the given interval.
+ * @param interval interval representing the 1D region for the line segment
+ * @return a new line segment on this line
+ */
+ public Segment segment(final Interval interval) {
+ return Segment.fromInterval(this, interval);
+ }
+
+ /** Create a new line segment from the given interval.
+ * @param a first 1D location for the interval
+ * @param b second 1D location for the interval
+ * @return a new line segment on this line
+ */
+ public Segment segment(final double a, final double b) {
+ return Segment.fromInterval(this, a, b);
+ }
+
+ /** Create a new line segment between the projections of the two
+ * given points onto this line.
+ * @param a first point
+ * @param b second point
+ * @return a new line segment on this line
+ */
+ public Segment segment(final Vector2D a, final Vector2D b) {
+ return Segment.fromInterval(this, toSubspace(a), toSubspace(b));
+ }
+
+ /** Create a new line segment that starts at infinity and continues along
+ * the line up to the projection of the given point.
+ * @param pt point defining the end point of the line segment; the end point
+ * is equal to the projection of this point onto the line
+ * @return a new, half-open line segment
+ */
+ public Segment segmentTo(final Vector2D pt) {
+ return segment(Double.NEGATIVE_INFINITY, toSubspace(pt).getX());
+ }
+
+ /** Create a new line segment that starts at the projection of the given point
+ * and continues in the direction of the line to infinity, similar to a ray.
+ * @param pt point defining the start point of the line segment; the start point
+ * is equal to the projection of this point onto the line
+ * @return a new, half-open line segment
+ */
+ public Segment segmentFrom(final Vector2D pt) {
+ return segment(toSubspace(pt).getX(), Double.POSITIVE_INFINITY);
+ }
+
+ /** Create a new, empty subline based on this line.
+ * @return a new, empty subline based on this line
+ */
+ public SubLine subline() {
+ return new SubLine(this);
+ }
+
+ /** Get the abscissa of the given point on the line. The abscissa represents
+ * the distance the projection of the point on the line is from the line's
+ * origin point (the point on the line closest to the origin of the
+ * 2D space). Abscissa values increase in the direction of the line. This method
+ * is exactly equivalent to {@link #toSubspace(Vector2D)} except that this method
+ * returns a double instead of a {@link Vector1D}.
+ * @param point point to compute the abscissa for
+ * @return abscissa value of the point
+ * @see #toSubspace(Vector2D)
+ */
+ public double abscissa(final Vector2D point) {
+ return direction.dot(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector1D toSubspace(final Vector2D point) {
+ return Vector1D.of(abscissa(point));
}
/** {@inheritDoc} */
@Override
public Vector2D toSpace(final Vector1D point) {
- final double abscissa = point.getX();
+ return toSpace(point.getX());
+ }
+ /** Convert the given abscissa value (1D location on the line)
+ * into a 2D point.
+ * @param abscissa value to convert
+ * @return 2D point corresponding to the line abscissa value
+ */
+ public Vector2D toSpace(final double abscissa) {
// The 2D coordinate is equal to the projection of the
// 2D origin onto the line plus the direction multiplied
// by the abscissa. We can combine everything into a single
@@ -175,7 +294,7 @@
*/
public Vector2D intersection(final Line other) {
final double area = this.direction.signedArea(other.direction);
- if (precision.eqZero(area)) {
+ if (getPrecision().eqZero(area)) {
// lines are parallel
return null;
}
@@ -191,36 +310,35 @@
return Vector2D.of(x, y);
}
+ /** Compute the angle in radians between this instance's direction and the direction
+ * of the given line. The return value is in the range {@code [-pi, +pi)}. This method
+ * always returns a value, even for parallel or coincident lines.
+ * @param other other line
+ * @return the angle required to rotate this line to point in the direction of
+ * the given line
+ */
+ public double angle(final Line other) {
+ final double thisAngle = Math.atan2(direction.getY(), direction.getX());
+ final double otherAngle = Math.atan2(other.direction.getY(), other.direction.getX());
+
+ return PlaneAngleRadians.normalizeBetweenMinusPiAndPi(otherAngle - thisAngle);
+ }
+
/** {@inheritDoc} */
@Override
public Vector2D project(final Vector2D point) {
- return toSpace(toSubSpace(point));
+ return toSpace(toSubspace(point));
}
/** {@inheritDoc} */
@Override
- public SubLine wholeHyperplane() {
- return new SubLine(this, new IntervalsSet(precision));
- }
-
- /** Build a region covering the whole space.
- * @return a region containing the instance (really a {@link
- * PolygonsSet PolygonsSet} instance)
- */
- @Override
- public PolygonsSet wholeSpace() {
- return new PolygonsSet(precision);
- }
-
- /** {@inheritDoc} */
- @Override
- public double getOffset(final Vector2D point) {
+ public double offset(final Vector2D point) {
return originOffset - direction.signedArea(point);
}
- /** Get the offset (oriented distance) of a line. Since an infinite
- * number of distances can be calculated between points on two different
- * lines, this methods returns the value closest to zero. For intersecting
+ /** Get the offset (oriented distance) of the given line relative to this instance.
+ * Since an infinite number of distances can be calculated between points on two
+ * different lines, this method returns the value closest to zero. For intersecting
* lines, this will simply be zero. For parallel lines, this will be the
* perpendicular distance between the two lines, as a signed value.
*
@@ -232,7 +350,7 @@
* @return offset of the line
* @see #distance(Line)
*/
- public double getOffset(final Line line) {
+ public double offset(final Line line) {
if (isParallel(line)) {
// since the lines are parallel, the offset between
// them is simply the difference between their origin offsets,
@@ -248,7 +366,7 @@
/** {@inheritDoc} */
@Override
- public boolean sameOrientationAs(final Hyperplane<Vector2D> other) {
+ public boolean similarOrientation(final Hyperplane<Vector2D> other) {
final Line otherLine = (Line) other;
return direction.dot(otherLine.direction) >= 0.0;
}
@@ -273,8 +391,9 @@
* @param p point to check
* @return true if p belongs to the line
*/
+ @Override
public boolean contains(final Vector2D p) {
- return precision.eqZero(getOffset(p));
+ return getPrecision().eqZero(offset(p));
}
/** Check if this instance completely contains the other line.
@@ -284,7 +403,7 @@
* @return true if this instance contains all points in the given line
*/
public boolean contains(final Line line) {
- return isParallel(line) && precision.eqZero(getOffset(line));
+ return isParallel(line) && getPrecision().eqZero(offset(line));
}
/** Compute the distance between the instance and a point.
@@ -296,7 +415,7 @@
* @return distance between the instance and the point
*/
public double distance(final Vector2D p) {
- return Math.abs(getOffset(p));
+ return Math.abs(offset(p));
}
/** Compute the shortest distance between this instance and
@@ -305,10 +424,10 @@
* @param line line to compute the closest distance to
* @return the shortest distance between this instance and the
* given line
- * @see #getOffset(Line)
+ * @see #offset(Line)
*/
public double distance(final Line line) {
- return Math.abs(getOffset(line));
+ return Math.abs(offset(line));
}
/** Check if the instance is parallel to another line.
@@ -318,20 +437,31 @@
*/
public boolean isParallel(final Line line) {
final double area = direction.signedArea(line.direction);
- return precision.eqZero(area);
+ return getPrecision().eqZero(area);
}
- /** Transform this instance with the given transform.
- * @param transform transform to apply to this instance
- * @return a new transformed line
- */
- public Line transform(final Transform<Vector2D, Vector1D> transform) {
- final Vector2D origin = getOrigin();
+ /** {@inheritDoc}
+ *
+ * <p>Instances are considered equivalent if they
+ * <ul>
+ * <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
+ * <li>have equivalent locations (as evaluated by the precision context), and</li>
+ * <li>point in the same direction (as evaluated by the precision context)</li>
+ * </ul>
+ * @param other the point to compare with
+ * @return true if this instance should be considered equivalent to the argument
+ */
+ @Override
+ public boolean eq(final Line other) {
+ if (this == other) {
+ return true;
+ }
- final Vector2D p1 = transform.apply(origin);
- final Vector2D p2 = transform.apply(origin.add(direction));
+ final DoublePrecisionContext precision = getPrecision();
- return Line.fromPoints(p1, p2, precision);
+ return precision.equals(other.getPrecision()) &&
+ getOrigin().eq(other.getOrigin(), precision) &&
+ precision.eq(getAngle(), other.getAngle());
}
/** {@inheritDoc} */
@@ -342,7 +472,7 @@
int result = 1;
result = (prime * result) + Objects.hashCode(direction);
result = (prime * result) + Double.hashCode(originOffset);
- result = (prime * result) + Objects.hashCode(precision);
+ result = (prime * result) + Objects.hashCode(getPrecision());
return result;
}
@@ -352,8 +482,7 @@
public boolean equals(Object obj) {
if (this == obj) {
return true;
- }
- else if (!(obj instanceof Line)) {
+ } else if (!(obj instanceof Line)) {
return false;
}
@@ -361,7 +490,7 @@
return Objects.equals(this.direction, other.direction) &&
Double.compare(this.originOffset, other.originOffset) == 0 &&
- Objects.equals(this.precision, other.precision);
+ Objects.equals(this.getPrecision(), other.getPrecision());
}
/** {@inheritDoc} */
@@ -400,7 +529,8 @@
* @throws GeometryValueException If {@code dir} has zero length, as evaluated by the
* given precision context
*/
- public static Line fromPointAndDirection(final Vector2D pt, final Vector2D dir, final DoublePrecisionContext precision) {
+ public static Line fromPointAndDirection(final Vector2D pt, final Vector2D dir,
+ final DoublePrecisionContext precision) {
if (dir.isZero(precision)) {
throw new GeometryValueException("Line direction cannot be zero");
}
@@ -419,67 +549,48 @@
* @return new line containing {@code pt} and forming the given angle with the
* abscissa (x) axis.
*/
- public static Line fromPointAndAngle(final Vector2D pt, final double angle, final DoublePrecisionContext precision) {
+ public static Line fromPointAndAngle(final Vector2D pt, final double angle,
+ final DoublePrecisionContext precision) {
final Vector2D.Unit dir = Vector2D.Unit.from(Math.cos(angle), Math.sin(angle));
return fromPointAndDirection(pt, dir, precision);
}
- // TODO: Remove this method and associated class after the Transform interface has been simplified.
- // See GEOMETRY-24.
-
- /** Create a {@link Transform} instance from a set of column vectors. The returned object can be used
- * to transform {@link SubLine} instances.
- * @param u first column vector; this corresponds to the first basis vector
- * in the coordinate frame
- * @param v second column vector; this corresponds to the second basis vector
- * in the coordinate frame
- * @param t third column vector; this corresponds to the translation of the transform
- * @return a new transform instance
+ /** Class containing a transformed line instance along with a subspace (1D) transform. The subspace
+ * transform produces the equivalent of the 2D transform in 1D.
*/
- public static Transform<Vector2D, Vector1D> getTransform(final Vector2D u, final Vector2D v, final Vector2D t) {
- final AffineTransformMatrix2D matrix = AffineTransformMatrix2D.fromColumnVectors(u, v, t);
- return new LineTransform(matrix);
- }
+ public static final class SubspaceTransform implements Serializable {
- /** Class wrapping an {@link AffineTransformMatrix2D} with the methods necessary to fulfill the full
- * {@link Transform} interface.
- */
- private static class LineTransform implements Transform<Vector2D, Vector1D> {
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190809L;
- /** Transform matrix */
- private final AffineTransformMatrix2D matrix;
+ /** The transformed line. */
+ private final Line line;
+
+ /** The subspace transform instance. */
+ private final AffineTransformMatrix1D transform;
/** Simple constructor.
- * @param matrix transform matrix
+ * @param line the transformed line
+ * @param transform 1D transform that can be applied to subspace points
*/
- LineTransform(final AffineTransformMatrix2D matrix) {
- this.matrix = matrix;
+ public SubspaceTransform(final Line line, final AffineTransformMatrix1D transform) {
+ this.line = line;
+ this.transform = transform;
}
- /** {@inheritDoc} */
- @Override
- public Vector2D apply(final Vector2D point) {
- return matrix.apply(point);
+ /** Get the transformed line instance.
+ * @return the transformed line instance
+ */
+ public Line getLine() {
+ return line;
}
- /** {@inheritDoc} */
- @Override
- public Line apply(final Hyperplane<Vector2D> hyperplane) {
- final Line line = (Line) hyperplane;
- return line.transform(matrix);
- }
-
- /** {@inheritDoc} */
- @Override
- public SubHyperplane<Vector1D> apply(final SubHyperplane<Vector1D> sub,
- final Hyperplane<Vector2D> original,
- final Hyperplane<Vector2D> transformed) {
- final OrientedPoint op = (OrientedPoint) sub.getHyperplane();
- final Line originalLine = (Line) original;
- final Line transformedLine = (Line) transformed;
- final Vector1D newLoc =
- transformedLine.toSubSpace(apply(originalLine.toSpace(op.getLocation())));
- return OrientedPoint.fromPointAndDirection(newLoc, op.getDirection(), originalLine.precision).wholeHyperplane();
+ /** Get the 1D transform that can be applied to subspace points. This transform can be used
+ * to perform the equivalent of the 2D transform in 1D space.
+ * @return the subspace transform instance
+ */
+ public AffineTransformMatrix1D getTransform() {
+ return transform;
}
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java
deleted file mode 100644
index d6dbbf9..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * 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;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-
-/** This class represent a tree of nested 2D boundary loops.
-
- * <p>This class is used for piecewise polygons construction.
- * Polygons are built using the outline edges as
- * representative of boundaries, the orientation of these lines are
- * meaningful. However, we want to allow the user to specify its
- * outline loops without having to take care of this orientation. This
- * class is devoted to correct mis-oriented loops.<p>
-
- * <p>Orientation is computed assuming the piecewise polygon is finite,
- * i.e. the outermost loops have their exterior side facing points at
- * infinity, and hence are oriented counter-clockwise. The orientation of
- * internal loops is computed as the reverse of the orientation of
- * their immediate surrounding loop.</p>
- */
-class NestedLoops {
-
- /** Boundary loop. */
- private Vector2D[] loop;
-
- /** Surrounded loops. */
- private List<NestedLoops> surrounded;
-
- /** Polygon enclosing a finite region. */
- private Region<Vector2D> polygon;
-
- /** Indicator for original loop orientation. */
- private boolean originalIsClockwise;
-
- /** Precision context used to compare floating point numbers. */
- private final DoublePrecisionContext precision;
-
- /** Simple Constructor.
- * <p>Build an empty tree of nested loops. This instance will become
- * the root node of a complete tree, it is not associated with any
- * loop by itself, the outermost loops are in the root tree child
- * nodes.</p>
- * @param precision precision context used to compare floating point values
- */
- NestedLoops(final DoublePrecisionContext precision) {
- this.surrounded = new ArrayList<>();
- this.precision = precision;
- }
-
- /** Constructor.
- * <p>Build a tree node with neither parent nor children</p>
- * @param loop boundary loop (will be reversed in place if needed)
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if an outline has an open boundary loop
- */
- private NestedLoops(final Vector2D[] loop, DoublePrecisionContext precision)
- throws IllegalArgumentException {
-
- if (loop[0] == null) {
- throw new IllegalArgumentException("An outline boundary loop is open");
- }
-
- this.loop = loop;
- this.surrounded = new ArrayList<>();
- this.precision = precision;
-
- // build the polygon defined by the loop
- final ArrayList<SubHyperplane<Vector2D>> edges = new ArrayList<>();
- Vector2D current = loop[loop.length - 1];
- for (int i = 0; i < loop.length; ++i) {
- final Vector2D previous = current;
- current = loop[i];
- final Line line = Line.fromPoints(previous, current, precision);
- final IntervalsSet region =
- new IntervalsSet(line.toSubSpace(previous).getX(),
- line.toSubSpace(current).getX(),
- precision);
- edges.add(new SubLine(line, region));
- }
- polygon = new PolygonsSet(edges, precision);
-
- // ensure the polygon encloses a finite region of the plane
- if (Double.isInfinite(polygon.getSize())) {
- polygon = new RegionFactory<Vector2D>().getComplement(polygon);
- originalIsClockwise = false;
- } else {
- originalIsClockwise = true;
- }
-
- }
-
- /** Add a loop in a tree.
- * @param bLoop boundary loop (will be reversed in place if needed)
- * @exception IllegalArgumentException if an outline has crossing
- * boundary loops or open boundary loops
- */
- public void add(final Vector2D[] bLoop) {
- add(new NestedLoops(bLoop, precision));
- }
-
- /** Add a loop in a tree.
- * @param node boundary loop (will be reversed in place if needed)
- * @exception IllegalArgumentException if an outline has boundary
- * loops that cross each other
- */
- private void add(final NestedLoops node) {
-
- // check if we can go deeper in the tree
- for (final NestedLoops child : surrounded) {
- if (child.polygon.contains(node.polygon)) {
- child.add(node);
- return;
- }
- }
-
- // check if we can absorb some of the instance children
- for (final Iterator<NestedLoops> iterator = surrounded.iterator(); iterator.hasNext();) {
- final NestedLoops child = iterator.next();
- if (node.polygon.contains(child.polygon)) {
- node.surrounded.add(child);
- iterator.remove();
- }
- }
-
- // we should be separate from the remaining children
- RegionFactory<Vector2D> factory = new RegionFactory<>();
- for (final NestedLoops child : surrounded) {
- if (!factory.intersection(node.polygon, child.polygon).isEmpty()) {
- throw new IllegalArgumentException("Some outline boundary loops cross each other");
- }
- }
-
- surrounded.add(node);
-
- }
-
- /** Correct the orientation of the loops contained in the tree.
- * <p>This is this method that really inverts the loops that where
- * provided through the {@link #add(Vector2D[]) add} method if
- * they are mis-oriented</p>
- */
- public void correctOrientation() {
- for (NestedLoops child : surrounded) {
- child.setClockWise(true);
- }
- }
-
- /** Set the loop orientation.
- * @param clockwise if true, the loop should be set to clockwise
- * orientation
- */
- private void setClockWise(final boolean clockwise) {
-
- if (originalIsClockwise ^ clockwise) {
- // we need to inverse the original loop
- int min = -1;
- int max = loop.length;
- while (++min < --max) {
- final Vector2D tmp = loop[min];
- loop[min] = loop[max];
- loop[max] = tmp;
- }
- }
-
- // go deeper in the tree
- for (final NestedLoops child : surrounded) {
- child.setClockWise(!clockwise);
- }
-
- }
-
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java
index adffb8d..5f679f9 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java
@@ -51,13 +51,13 @@
*/
public final class PolarCoordinates implements Spatial, Serializable {
- /** Serializable version UID */
+ /** Serializable version UID. */
private static final long serialVersionUID = 20180630L;
- /** Radius value */
+ /** Radius value. */
private final double radius;
- /** Azimuth angle in radians */
+ /** Azimuth angle in radians. */
private final double azimuth;
/** Simple constructor. Input values are normalized.
@@ -72,7 +72,7 @@
}
this.radius = radius;
- this.azimuth = normalizeAzimuth(azimuth);;
+ this.azimuth = normalizeAzimuth(azimuth);
}
/** Return the radius value. The value will be greater than or equal to 0.
@@ -108,6 +108,12 @@
return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth));
}
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return Double.isFinite(radius) && Double.isFinite(azimuth);
+ }
+
/** Convert this set of polar coordinates to Cartesian coordinates.
* @return A 2-dimensional vector with an equivalent set of
* coordinates in Cartesian form
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java
deleted file mode 100644
index ee76ff9..0000000
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java
+++ /dev/null
@@ -1,1101 +0,0 @@
-/*
- * 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;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-import org.apache.commons.geometry.core.partitioning.AbstractRegion;
-import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
-import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Side;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.Interval;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
-import org.apache.commons.numbers.core.Precision;
-
-/** This class represents a 2D region: a set of polygons.
- */
-public class PolygonsSet extends AbstractRegion<Vector2D, Vector1D> {
-
- /** Vertices organized as boundary loops. */
- private Vector2D[][] vertices;
-
- /** Build a polygons set representing the whole plane.
- * @param precision precision context used to compare floating point values
- */
- public PolygonsSet(final DoublePrecisionContext precision) {
- super(precision);
- }
-
- /** Build a polygons set from a BSP tree.
- * <p>The leaf nodes of the BSP tree <em>must</em> have a
- * {@code Boolean} attribute representing the inside status of
- * the corresponding cell (true for inside cells, false for outside
- * cells). In order to avoid building too many small objects, it is
- * recommended to use the predefined constants
- * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
- * <p>
- * This constructor is aimed at expert use, as building the tree may
- * be a difficult task. It is not intended for general use and for
- * performances reasons does not check thoroughly its input, as this would
- * require walking the full tree each time. Failing to provide a tree with
- * the proper attributes, <em>will</em> therefore generate problems like
- * {@link NullPointerException} or {@link ClassCastException} only later on.
- * This limitation is known and explains why this constructor is for expert
- * use only. The caller does have the responsibility to provided correct arguments.
- * </p>
- * @param tree inside/outside BSP tree representing the region
- * @param precision precision context used to compare floating point values
- */
- public PolygonsSet(final BSPTree<Vector2D> tree, final DoublePrecisionContext precision) {
- super(tree, precision);
- }
-
- /** Build a polygons set from a Boundary REPresentation (B-rep).
- * <p>The boundary is provided as a collection of {@link
- * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
- * interior part of the region on its minus side and the exterior on
- * its plus side.</p>
- * <p>The boundary elements can be in any order, and can form
- * several non-connected sets (like for example polygons with holes
- * or a set of disjoint polygons considered as a whole). In
- * fact, the elements do not even need to be connected together
- * (their topological connections are not used here). However, if the
- * boundary does not really separate an inside open from an outside
- * open (open having here its topological meaning), then subsequent
- * calls to the {@link
- * org.apache.commons.geometry.core.partitioning.Region#checkPoint(org.apache.commons.geometry.core.Point)
- * checkPoint} method will not be meaningful anymore.</p>
- * <p>If the boundary is empty, the region will represent the whole
- * space.</p>
- * @param boundary collection of boundary elements, as a
- * collection of {@link SubHyperplane SubHyperplane} objects
- * @param precision precision context used to compare floating point values
- */
- public PolygonsSet(final Collection<SubHyperplane<Vector2D>> boundary, final DoublePrecisionContext precision) {
- super(boundary, precision);
- }
-
- /** Build a parallellepipedic box.
- * @param xMin low bound along the x direction
- * @param xMax high bound along the x direction
- * @param yMin low bound along the y direction
- * @param yMax high bound along the y direction
- * @param precision precision context used to compare floating point values
- */
- public PolygonsSet(final double xMin, final double xMax,
- final double yMin, final double yMax,
- final DoublePrecisionContext precision) {
- super(boxBoundary(xMin, xMax, yMin, yMax, precision), precision);
- }
-
- /** Build a polygon from a simple list of vertices.
- * <p>The boundary is provided as a list of points considering to
- * represent the vertices of a simple loop. The interior part of the
- * region is on the left side of this path and the exterior is on its
- * right side.</p>
- * <p>This constructor does not handle polygons with a boundary
- * forming several disconnected paths (such as polygons with holes).</p>
- * <p>For cases where this simple constructor applies, it is expected to
- * be numerically more robust than the {@link #PolygonsSet(Collection, DoublePrecisionContext) general
- * constructor} using {@link SubHyperplane subhyperplanes}.</p>
- * <p>If the list is empty, the region will represent the whole
- * space.</p>
- * <p>
- * Polygons with thin pikes or dents are inherently difficult to handle because
- * they involve lines with almost opposite directions at some vertices. Polygons
- * whose vertices come from some physical measurement with noise are also
- * difficult because an edge that should be straight may be broken in lots of
- * different pieces with almost equal directions. In both cases, computing the
- * lines intersections is not numerically robust due to the almost 0 or almost
- * π angle. Such cases need to carefully adjust the {@code hyperplaneThickness}
- * parameter. A too small value would often lead to completely wrong polygons
- * with large area wrongly identified as inside or outside. Large values are
- * often much safer. As a rule of thumb, a value slightly below the size of the
- * most accurate detail needed is a good value for the {@code hyperplaneThickness}
- * parameter.
- * </p>
- * @param precision precision context used to compare floating point values
- * @param vertices vertices of the simple loop boundary
- */
- public PolygonsSet(final DoublePrecisionContext precision, final Vector2D ... vertices) {
- super(verticesToTree(precision, vertices), precision);
- }
-
- /** Create a list of hyperplanes representing the boundary of a box.
- * @param xMin low bound along the x direction
- * @param xMax high bound along the x direction
- * @param yMin low bound along the y direction
- * @param yMax high bound along the y direction
- * @param precision precision context used to compare floating point values
- * @return boundary of the box
- */
- private static Line[] boxBoundary(final double xMin, final double xMax,
- final double yMin, final double yMax,
- final DoublePrecisionContext precision) {
- if (precision.eq(xMin, xMax) || precision.eq(yMin, yMax)) {
- // too thin box, build an empty polygons set
- return null;
- }
- final Vector2D minMin = Vector2D.of(xMin, yMin);
- final Vector2D minMax = Vector2D.of(xMin, yMax);
- final Vector2D maxMin = Vector2D.of(xMax, yMin);
- final Vector2D maxMax = Vector2D.of(xMax, yMax);
- return new Line[] {
- Line.fromPoints(minMin, maxMin, precision),
- Line.fromPoints(maxMin, maxMax, precision),
- Line.fromPoints(maxMax, minMax, precision),
- Line.fromPoints(minMax, minMin, precision)
- };
- }
-
- /** Build the BSP tree of a polygons set from a simple list of vertices.
- * <p>The boundary is provided as a list of points considering to
- * represent the vertices of a simple loop. The interior part of the
- * region is on the left side of this path and the exterior is on its
- * right side.</p>
- * <p>This constructor does not handle polygons with a boundary
- * forming several disconnected paths (such as polygons with holes).</p>
- * <p>For cases where this simple constructor applies, it is expected to
- * be numerically more robust than the {@link #PolygonsSet(Collection,double) general
- * constructor} using {@link SubHyperplane subhyperplanes}.</p>
- * @param precision precision context used to compare floating point values
- * @param vertices vertices of the simple loop boundary
- * @return the BSP tree of the input vertices
- */
- private static BSPTree<Vector2D> verticesToTree(final DoublePrecisionContext precision,
- final Vector2D ... vertices) {
-
- final int n = vertices.length;
- if (n == 0) {
- // the tree represents the whole space
- return new BSPTree<>(Boolean.TRUE);
- }
-
- // build the vertices
- final Vertex[] vArray = new Vertex[n];
- for (int i = 0; i < n; ++i) {
- vArray[i] = new Vertex(vertices[i]);
- }
-
- // build the edges
- List<Edge> edges = new ArrayList<>(n);
- for (int i = 0; i < n; ++i) {
-
- // get the endpoints of the edge
- final Vertex start = vArray[i];
- final Vertex end = vArray[(i + 1) % n];
-
- // get the line supporting the edge, taking care not to recreate it
- // if it was already created earlier due to another edge being aligned
- // with the current one
- Line line = start.sharedLineWith(end);
- if (line == null) {
- line = Line.fromPoints(start.getLocation(), end.getLocation(), precision);
- }
-
- // create the edge and store it
- edges.add(new Edge(start, end, line));
-
- // check if another vertex also happens to be on this line
- for (final Vertex vertex : vArray) {
- if (vertex != start && vertex != end &&
- precision.eqZero(line.getOffset(vertex.getLocation()))) {
- vertex.bindWith(line);
- }
- }
-
- }
-
- // build the tree top-down
- final BSPTree<Vector2D> tree = new BSPTree<>();
- insertEdges(precision, tree, edges);
-
- return tree;
-
- }
-
- /** Recursively build a tree by inserting cut sub-hyperplanes.
- * @param precision precision context used to compare floating point values
- * @param node current tree node (it is a leaf node at the beginning
- * of the call)
- * @param edges list of edges to insert in the cell defined by this node
- * (excluding edges not belonging to the cell defined by this node)
- */
- private static void insertEdges(final DoublePrecisionContext precision,
- final BSPTree<Vector2D> node,
- final List<Edge> edges) {
-
- // find an edge with an hyperplane that can be inserted in the node
- int index = 0;
- Edge inserted =null;
- while (inserted == null && index < edges.size()) {
- inserted = edges.get(index++);
- if (inserted.getNode() == null) {
- if (node.insertCut(inserted.getLine())) {
- inserted.setNode(node);
- } else {
- inserted = null;
- }
- } else {
- inserted = null;
- }
- }
-
- if (inserted == null) {
- // no suitable edge was found, the node remains a leaf node
- // we need to set its inside/outside boolean indicator
- final BSPTree<Vector2D> parent = node.getParent();
- if (parent == null || node == parent.getMinus()) {
- node.setAttribute(Boolean.TRUE);
- } else {
- node.setAttribute(Boolean.FALSE);
- }
- return;
- }
-
- // we have split the node by inserting an edge as a cut sub-hyperplane
- // distribute the remaining edges in the two sub-trees
- final List<Edge> plusList = new ArrayList<>();
- final List<Edge> minusList = new ArrayList<>();
- for (final Edge edge : edges) {
- if (edge != inserted) {
- final double startOffset = inserted.getLine().getOffset(edge.getStart().getLocation());
- final double endOffset = inserted.getLine().getOffset(edge.getEnd().getLocation());
- Side startSide = precision.eqZero(Math.abs(startOffset)) ?
- Side.HYPER : ((startOffset < 0) ? Side.MINUS : Side.PLUS);
- Side endSide = precision.eqZero(endOffset) ?
- Side.HYPER : ((endOffset < 0) ? Side.MINUS : Side.PLUS);
- switch (startSide) {
- case PLUS:
- if (endSide == Side.MINUS) {
- // we need to insert a split point on the hyperplane
- final Vertex splitPoint = edge.split(inserted.getLine());
- minusList.add(splitPoint.getOutgoing());
- plusList.add(splitPoint.getIncoming());
- } else {
- plusList.add(edge);
- }
- break;
- case MINUS:
- if (endSide == Side.PLUS) {
- // we need to insert a split point on the hyperplane
- final Vertex splitPoint = edge.split(inserted.getLine());
- minusList.add(splitPoint.getIncoming());
- plusList.add(splitPoint.getOutgoing());
- } else {
- minusList.add(edge);
- }
- break;
- default:
- if (endSide == Side.PLUS) {
- plusList.add(edge);
- } else if (endSide == Side.MINUS) {
- minusList.add(edge);
- }
- break;
- }
- }
- }
-
- // recurse through lower levels
- if (!plusList.isEmpty()) {
- insertEdges(precision, node.getPlus(), plusList);
- } else {
- node.getPlus().setAttribute(Boolean.FALSE);
- }
- if (!minusList.isEmpty()) {
- insertEdges(precision, node.getMinus(), minusList);
- } else {
- node.getMinus().setAttribute(Boolean.TRUE);
- }
-
- }
-
- /** Internal class for holding vertices while they are processed to build a BSP tree. */
- private static class Vertex {
-
- /** Vertex location. */
- private final Vector2D location;
-
- /** Incoming edge. */
- private Edge incoming;
-
- /** Outgoing edge. */
- private Edge outgoing;
-
- /** Lines bound with this vertex. */
- private final List<Line> lines;
-
- /** Build a non-processed vertex not owned by any node yet.
- * @param location vertex location
- */
- Vertex(final Vector2D location) {
- this.location = location;
- this.incoming = null;
- this.outgoing = null;
- this.lines = new ArrayList<>();
- }
-
- /** Get Vertex location.
- * @return vertex location
- */
- public Vector2D getLocation() {
- return location;
- }
-
- /** Bind a line considered to contain this vertex.
- * @param line line to bind with this vertex
- */
- public void bindWith(final Line line) {
- lines.add(line);
- }
-
- /** Get the common line bound with both the instance and another vertex, if any.
- * <p>
- * When two vertices are both bound to the same line, this means they are
- * already handled by node associated with this line, so there is no need
- * to create a cut hyperplane for them.
- * </p>
- * @param vertex other vertex to check instance against
- * @return line bound with both the instance and another vertex, or null if the
- * two vertices do not share a line yet
- */
- public Line sharedLineWith(final Vertex vertex) {
- for (final Line line1 : lines) {
- for (final Line line2 : vertex.lines) {
- if (line1 == line2) {
- return line1;
- }
- }
- }
- return null;
- }
-
- /** Set incoming edge.
- * <p>
- * The line supporting the incoming edge is automatically bound
- * with the instance.
- * </p>
- * @param incoming incoming edge
- */
- public void setIncoming(final Edge incoming) {
- this.incoming = incoming;
- bindWith(incoming.getLine());
- }
-
- /** Get incoming edge.
- * @return incoming edge
- */
- public Edge getIncoming() {
- return incoming;
- }
-
- /** Set outgoing edge.
- * <p>
- * The line supporting the outgoing edge is automatically bound
- * with the instance.
- * </p>
- * @param outgoing outgoing edge
- */
- public void setOutgoing(final Edge outgoing) {
- this.outgoing = outgoing;
- bindWith(outgoing.getLine());
- }
-
- /** Get outgoing edge.
- * @return outgoing edge
- */
- public Edge getOutgoing() {
- return outgoing;
- }
-
- }
-
- /** Internal class for holding edges while they are processed to build a BSP tree. */
- private static class Edge {
-
- /** Start vertex. */
- private final Vertex start;
-
- /** End vertex. */
- private final Vertex end;
-
- /** Line supporting the edge. */
- private final Line line;
-
- /** Node whose cut hyperplane contains this edge. */
- private BSPTree<Vector2D> node;
-
- /** Build an edge not contained in any node yet.
- * @param start start vertex
- * @param end end vertex
- * @param line line supporting the edge
- */
- Edge(final Vertex start, final Vertex end, final Line line) {
-
- this.start = start;
- this.end = end;
- this.line = line;
- this.node = null;
-
- // connect the vertices back to the edge
- start.setOutgoing(this);
- end.setIncoming(this);
-
- }
-
- /** Get start vertex.
- * @return start vertex
- */
- public Vertex getStart() {
- return start;
- }
-
- /** Get end vertex.
- * @return end vertex
- */
- public Vertex getEnd() {
- return end;
- }
-
- /** Get the line supporting this edge.
- * @return line supporting this edge
- */
- public Line getLine() {
- return line;
- }
-
- /** Set the node whose cut hyperplane contains this edge.
- * @param node node whose cut hyperplane contains this edge
- */
- public void setNode(final BSPTree<Vector2D> node) {
- this.node = node;
- }
-
- /** Get the node whose cut hyperplane contains this edge.
- * @return node whose cut hyperplane contains this edge
- * (null if edge has not yet been inserted into the BSP tree)
- */
- public BSPTree<Vector2D> getNode() {
- return node;
- }
-
- /** Split the edge.
- * <p>
- * Once split, this edge is not referenced anymore by the vertices,
- * it is replaced by the two half-edges and an intermediate splitting
- * vertex is introduced to connect these two halves.
- * </p>
- * @param splitLine line splitting the edge in two halves
- * @return split vertex (its incoming and outgoing edges are the two halves)
- */
- public Vertex split(final Line splitLine) {
- final Vertex splitVertex = new Vertex(line.intersection(splitLine));
- splitVertex.bindWith(splitLine);
- final Edge startHalf = new Edge(start, splitVertex, line);
- final Edge endHalf = new Edge(splitVertex, end, line);
- startHalf.node = node;
- endHalf.node = node;
- return splitVertex;
- }
-
- }
-
- /** {@inheritDoc} */
- @Override
- public PolygonsSet buildNew(final BSPTree<Vector2D> tree) {
- return new PolygonsSet(tree, getPrecision());
- }
-
- /** {@inheritDoc} */
- @Override
- protected void computeGeometricalProperties() {
-
- final Vector2D[][] v = getVertices();
-
- if (v.length == 0) {
- final BSPTree<Vector2D> tree = getTree(false);
- if (tree.getCut() == null && (Boolean) tree.getAttribute()) {
- // the instance covers the whole space
- setSize(Double.POSITIVE_INFINITY);
- setBarycenter(Vector2D.NaN);
- } else {
- setSize(0);
- setBarycenter(Vector2D.NaN);
- }
- } else if (v[0][0] == null) {
- // there is at least one open-loop: the polygon is infinite
- setSize(Double.POSITIVE_INFINITY);
- setBarycenter(Vector2D.NaN);
- } else {
- // all loops are closed, we compute some integrals around the shape
-
- double sum = 0;
- double sumX = 0;
- double sumY = 0;
-
- for (Vector2D[] loop : v) {
- double x1 = loop[loop.length - 1].getX();
- double y1 = loop[loop.length - 1].getY();
- for (final Vector2D point : loop) {
- final double x0 = x1;
- final double y0 = y1;
- x1 = point.getX();
- y1 = point.getY();
- final double factor = x0 * y1 - y0 * x1;
- sum += factor;
- sumX += factor * (x0 + x1);
- sumY += factor * (y0 + y1);
- }
- }
-
- if (sum < 0) {
- // the polygon as a finite outside surrounded by an infinite inside
- setSize(Double.POSITIVE_INFINITY);
- setBarycenter(Vector2D.NaN);
- } else {
- setSize(sum / 2);
- setBarycenter(Vector2D.of(sumX / (3 * sum), sumY / (3 * sum)));
- }
-
- }
-
- }
-
- /** Get the vertices of the polygon.
- * <p>The polygon boundary can be represented as an array of loops,
- * each loop being itself an array of vertices.</p>
- * <p>In order to identify open loops which start and end by
- * infinite edges, the open loops arrays start with a null point. In
- * this case, the first non null point and the last point of the
- * array do not represent real vertices, they are dummy points
- * intended only to get the direction of the first and last edge. An
- * open loop consisting of a single infinite line will therefore be
- * represented by a three elements array with one null point
- * followed by two dummy points. The open loops are always the first
- * ones in the loops array.</p>
- * <p>If the polygon has no boundary at all, a zero length loop
- * array will be returned.</p>
- * <p>All line segments in the various loops have the inside of the
- * region on their left side and the outside on their right side
- * when moving in the underlying line direction. This means that
- * closed loops surrounding finite areas obey the direct
- * trigonometric orientation.</p>
- * @return vertices of the polygon, organized as oriented boundary
- * loops with the open loops first (the returned value is guaranteed
- * to be non-null)
- */
- public Vector2D[][] getVertices() {
- if (vertices == null) {
- if (getTree(false).getCut() == null) {
- vertices = new Vector2D[0][];
- } else {
-
- // build the unconnected segments
- final SegmentsBuilder visitor = new SegmentsBuilder(getPrecision());
- getTree(true).visit(visitor);
- final List<ConnectableSegment> segments = visitor.getSegments();
-
- // connect all segments, using topological criteria first
- // and using Euclidean distance only as a last resort
- int pending = segments.size();
- pending -= naturalFollowerConnections(segments);
- if (pending > 0) {
- pending -= splitEdgeConnections(segments);
- }
- if (pending > 0) {
- pending -= closeVerticesConnections(segments);
- }
-
- // create the segment loops
- final ArrayList<List<Segment>> loops = new ArrayList<>();
- for (ConnectableSegment s = getUnprocessed(segments); s != null; s = getUnprocessed(segments)) {
- final List<Segment> loop = followLoop(s);
- if (loop != null) {
- // an open loop is one that has fewer than two segments or has a null
- // start point; the case where we have two segments in a closed loop
- // (ie, an infinitely thin, degenerate loop) will result in null being
- // returned from the followLoops method
- if (loop.size() < 2 || loop.get(0).getStart() == null) {
- // this is an open loop, we put it on the front
- loops.add(0, loop);
- } else {
- // this is a closed loop, we put it on the back
- loops.add(loop);
- }
- }
- }
-
- // transform the loops in an array of arrays of points
- vertices = new Vector2D[loops.size()][];
- int i = 0;
-
- for (final List<Segment> loop : loops) {
- if (loop.size() < 2 ||
- (loop.size() == 2 && loop.get(0).getStart() == null && loop.get(1).getEnd() == null)) {
- // single infinite line
- final Line line = loop.get(0).getLine();
- vertices[i++] = new Vector2D[] {
- null,
- line.toSpace(Vector1D.of(-Float.MAX_VALUE)),
- line.toSpace(Vector1D.of(+Float.MAX_VALUE))
- };
- } else if (loop.get(0).getStart() == null) {
- // open loop with at least one real point
- final Vector2D[] array = new Vector2D[loop.size() + 2];
- int j = 0;
- for (Segment segment : loop) {
-
- if (j == 0) {
- // null point and first dummy point
- double x = segment.getLine().toSubSpace(segment.getEnd()).getX();
- x -= Math.max(1.0, Math.abs(x / 2));
- array[j++] = null;
- array[j++] = segment.getLine().toSpace(Vector1D.of(x));
- }
-
- if (j < (array.length - 1)) {
- // current point
- array[j++] = segment.getEnd();
- } else if (j == (array.length - 1)) {
- // last dummy point
- double x = segment.getLine().toSubSpace(segment.getStart()).getX();
- x += Math.max(1.0, Math.abs(x / 2));
- array[j++] = segment.getLine().toSpace(Vector1D.of(x));
- }
-
- }
- vertices[i++] = array;
- } else {
- final Vector2D[] array = new Vector2D[loop.size()];
- int j = 0;
- for (Segment segment : loop) {
- array[j++] = segment.getStart();
- }
- vertices[i++] = array;
- }
- }
-
- }
- }
-
- return vertices.clone();
-
- }
-
- /** Connect the segments using only natural follower information.
- * @param segments segments complete segments list
- * @return number of connections performed
- */
- private int naturalFollowerConnections(final List<ConnectableSegment> segments) {
- int connected = 0;
- for (final ConnectableSegment segment : segments) {
- if (segment.getNext() == null) {
- final BSPTree<Vector2D> node = segment.getNode();
- final BSPTree<Vector2D> end = segment.getEndNode();
- for (final ConnectableSegment candidateNext : segments) {
- if (candidateNext.getPrevious() == null &&
- candidateNext.getNode() == end &&
- candidateNext.getStartNode() == node) {
- // connect the two segments
- segment.setNext(candidateNext);
- candidateNext.setPrevious(segment);
- ++connected;
- break;
- }
- }
- }
- }
- return connected;
- }
-
- /** Connect the segments resulting from a line splitting a straight edge.
- * @param segments segments complete segments list
- * @return number of connections performed
- */
- private int splitEdgeConnections(final List<ConnectableSegment> segments) {
- int connected = 0;
- for (final ConnectableSegment segment : segments) {
- if (segment.getNext() == null && segment.getEndNode() != null) {
- final Hyperplane<Vector2D> hyperplane = segment.getNode().getCut().getHyperplane();
- final BSPTree<Vector2D> end = segment.getEndNode();
- for (final ConnectableSegment candidateNext : segments) {
- if (candidateNext.getPrevious() == null &&
- candidateNext.getNode().getCut().getHyperplane().equals(hyperplane) &&
- candidateNext.getStartNode() == end) {
- // connect the two segments
- segment.setNext(candidateNext);
- candidateNext.setPrevious(segment);
- ++connected;
- break;
- }
- }
- }
- }
- return connected;
- }
-
- /** Connect the segments using Euclidean distance.
- * <p>
- * This connection heuristic should be used last, as it relies
- * only on a fuzzy distance criterion.
- * </p>
- * @param segments segments complete segments list
- * @return number of connections performed
- */
- private int closeVerticesConnections(final List<ConnectableSegment> segments) {
- int connected = 0;
- for (final ConnectableSegment segment : segments) {
- if (segment.getNext() == null && segment.getEnd() != null) {
- final Vector2D end = segment.getEnd();
- ConnectableSegment selectedNext = null;
- double min = Double.POSITIVE_INFINITY;
- for (final ConnectableSegment candidateNext : segments) {
- if (candidateNext.getPrevious() == null && candidateNext.getStart() != null) {
- final double distance = end.distance(candidateNext.getStart());
- if (distance < min) {
- selectedNext = candidateNext;
- min = distance;
- }
- }
- }
- if (getPrecision().eqZero(min)) {
- // connect the two segments
- segment.setNext(selectedNext);
- selectedNext.setPrevious(segment);
- ++connected;
- }
- }
- }
- return connected;
- }
-
- /** Get first unprocessed segment from a list.
- * @param segments segments list
- * @return first segment that has not been processed yet
- * or null if all segments have been processed
- */
- private ConnectableSegment getUnprocessed(final List<ConnectableSegment> segments) {
- for (final ConnectableSegment segment : segments) {
- if (!segment.isProcessed()) {
- return segment;
- }
- }
- return null;
- }
-
- /** Build the loop containing a segment.
- * <p>
- * The segment put in the loop will be marked as processed.
- * </p>
- * @param defining segment used to define the loop
- * @return loop containing the segment (may be null if the loop is a
- * degenerated infinitely thin 2 points loop
- */
- private List<Segment> followLoop(final ConnectableSegment defining) {
-
- final List<Segment> loop = new ArrayList<>();
- loop.add(defining);
- defining.setProcessed(true);
-
- // add segments in connection order
- ConnectableSegment next = defining.getNext();
- while (next != defining && next != null) {
- loop.add(next);
- next.setProcessed(true);
- next = next.getNext();
- }
-
- if (next == null) {
- // the loop is open and we have found its end,
- // we need to find its start too
- ConnectableSegment previous = defining.getPrevious();
- while (previous != null) {
- loop.add(0, previous);
- previous.setProcessed(true);
- previous = previous.getPrevious();
- }
- }
-
- // filter out spurious vertices
- filterSpuriousVertices(loop);
-
- if (loop.size() == 2 && loop.get(0).getStart() != null) {
- // this is a degenerated infinitely thin closed loop, we simply ignore it
- return null;
- } else {
- return loop;
- }
-
- }
-
- /** Filter out spurious vertices on straight lines (at machine precision).
- * @param loop segments loop to filter (will be modified in-place)
- */
- private void filterSpuriousVertices(final List<Segment> loop) {
- // we need at least 2 segments in order for one of the contained vertices
- // to be unnecessary
- if (loop.size() > 1) {
- // Go through the list and compare each segment with the next
- // one in line. We can remove the shared vertex if the segments
- // are not infinite and they lie on the same line.
- for (int i = 0; i < loop.size(); ++i) {
- final Segment previous = loop.get(i);
- int j = (i + 1) % loop.size();
- final Segment next = loop.get(j);
- if (next != null &&
- previous.getStart() != null && next.getEnd() != null &&
- Precision.equals(previous.getLine().getAngle(), next.getLine().getAngle(), Precision.EPSILON)) {
- // the vertex between the two edges is a spurious one
- // replace the two segments by a single one
- loop.set(j, new Segment(previous.getStart(), next.getEnd(), previous.getLine()));
- loop.remove(i--);
- }
- }
- }
- }
-
- /** Private extension of Segment allowing connection. */
- private static class ConnectableSegment extends Segment {
-
- /** Node containing segment. */
- private final BSPTree<Vector2D> node;
-
- /** Node whose intersection with current node defines start point. */
- private final BSPTree<Vector2D> startNode;
-
- /** Node whose intersection with current node defines end point. */
- private final BSPTree<Vector2D> endNode;
-
- /** Previous segment. */
- private ConnectableSegment previous;
-
- /** Next segment. */
- private ConnectableSegment next;
-
- /** Indicator for completely processed segments. */
- private boolean processed;
-
- /** Build a segment.
- * @param start start point of the segment
- * @param end end point of the segment
- * @param line line containing the segment
- * @param node node containing the segment
- * @param startNode node whose intersection with current node defines start point
- * @param endNode node whose intersection with current node defines end point
- */
- ConnectableSegment(final Vector2D start, final Vector2D end, final Line line,
- final BSPTree<Vector2D> node,
- final BSPTree<Vector2D> startNode,
- final BSPTree<Vector2D> endNode) {
- super(start, end, line);
- this.node = node;
- this.startNode = startNode;
- this.endNode = endNode;
- this.previous = null;
- this.next = null;
- this.processed = false;
- }
-
- /** Get the node containing segment.
- * @return node containing segment
- */
- public BSPTree<Vector2D> getNode() {
- return node;
- }
-
- /** Get the node whose intersection with current node defines start point.
- * @return node whose intersection with current node defines start point
- */
- public BSPTree<Vector2D> getStartNode() {
- return startNode;
- }
-
- /** Get the node whose intersection with current node defines end point.
- * @return node whose intersection with current node defines end point
- */
- public BSPTree<Vector2D> getEndNode() {
- return endNode;
- }
-
- /** Get the previous segment.
- * @return previous segment
- */
- public ConnectableSegment getPrevious() {
- return previous;
- }
-
- /** Set the previous segment.
- * @param previous previous segment
- */
- public void setPrevious(final ConnectableSegment previous) {
- this.previous = previous;
- }
-
- /** Get the next segment.
- * @return next segment
- */
- public ConnectableSegment getNext() {
- return next;
- }
-
- /** Set the next segment.
- * @param next previous segment
- */
- public void setNext(final ConnectableSegment next) {
- this.next = next;
- }
-
- /** Set the processed flag.
- * @param processed processed flag to set
- */
- public void setProcessed(final boolean processed) {
- this.processed = processed;
- }
-
- /** Check if the segment has been processed.
- * @return true if the segment has been processed
- */
- public boolean isProcessed() {
- return processed;
- }
-
- }
-
- /** Visitor building segments. */
- private static class SegmentsBuilder implements BSPTreeVisitor<Vector2D> {
-
- /** Object used to determine floating point equality */
- private final DoublePrecisionContext precision;
-
- /** Built segments. */
- private final List<ConnectableSegment> segments;
-
- /** Simple constructor.
- * @param precision precision context used to compare floating point values
- */
- SegmentsBuilder(final DoublePrecisionContext precision) {
- this.precision = precision;
- this.segments = new ArrayList<>();
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<Vector2D> node) {
- return Order.MINUS_SUB_PLUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<Vector2D> node) {
- @SuppressWarnings("unchecked")
- final BoundaryAttribute<Vector2D> attribute = (BoundaryAttribute<Vector2D>) node.getAttribute();
- final Iterable<BSPTree<Vector2D>> splitters = attribute.getSplitters();
- if (attribute.getPlusOutside() != null) {
- addContribution(attribute.getPlusOutside(), node, splitters, false);
- }
- if (attribute.getPlusInside() != null) {
- addContribution(attribute.getPlusInside(), node, splitters, true);
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<Vector2D> node) {
- }
-
- /** Add the contribution of a boundary facet.
- * @param sub boundary facet
- * @param node node containing segment
- * @param splitters splitters for the boundary facet
- * @param reversed if true, the facet has the inside on its plus side
- */
- private void addContribution(final SubHyperplane<Vector2D> sub,
- final BSPTree<Vector2D> node,
- final Iterable<BSPTree<Vector2D>> splitters,
- final boolean reversed) {
- @SuppressWarnings("unchecked")
- final AbstractSubHyperplane<Vector2D, Vector1D> absSub =
- (AbstractSubHyperplane<Vector2D, Vector1D>) sub;
- final Line line = (Line) sub.getHyperplane();
- final List<Interval> intervals = ((IntervalsSet) absSub.getRemainingRegion()).asList();
- for (final Interval i : intervals) {
-
- // find the 2D points
- final Vector2D startV = Double.isInfinite(i.getInf()) ?
- null : line.toSpace(Vector1D.of(i.getInf()));
- final Vector2D endV = Double.isInfinite(i.getSup()) ?
- null : line.toSpace(Vector1D.of(i.getSup()));
-
- // recover the connectivity information
- final BSPTree<Vector2D> startN = selectClosest(startV, splitters);
- final BSPTree<Vector2D> endN = selectClosest(endV, splitters);
-
- if (reversed) {
- segments.add(new ConnectableSegment(endV, startV, line.reverse(),
- node, endN, startN));
- } else {
- segments.add(new ConnectableSegment(startV, endV, line,
- node, startN, endN));
- }
-
- }
- }
-
- /** Select the node whose cut sub-hyperplane is closest to specified point.
- * @param point reference point
- * @param candidates candidate nodes
- * @return node closest to point, or null if point is null or no node is closer than tolerance
- */
- private BSPTree<Vector2D> selectClosest(final Vector2D point, final Iterable<BSPTree<Vector2D>> candidates) {
- if (point != null) {
- BSPTree<Vector2D> selected = null;
- double min = Double.POSITIVE_INFINITY;
-
- for (final BSPTree<Vector2D> node : candidates) {
- final double distance = Math.abs(node.getCut().getHyperplane().getOffset(point));
- if (distance < min) {
- selected = node;
- min = distance;
- }
- }
-
- if (precision.eqZero(min)) {
- return selected;
- }
- }
- return null;
- }
-
- /** Get the segments.
- * @return built segments
- */
- public List<ConnectableSegment> getSegments() {
- return segments;
- }
-
- }
-
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java
new file mode 100644
index 0000000..1722934
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java
@@ -0,0 +1,861 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Class representing a polyline, ie, a connected series of line segments. The line
+ * segments in the polyline are connected end to end, with the end vertex of the previous
+ * line segment equivalent to the start vertex of the next line segment. The first segment,
+ * the last segment, or both may be infinite.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ * @see <a href="https://en.wikipedia.org/wiki/Polygonal_chain">Polygonal chain</a>
+ */
+public class Polyline implements Iterable<Segment>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190522L;
+
+ /** Polyline instance containing no segments. */
+ private static final Polyline EMPTY = new Polyline(Collections.emptyList());
+
+ /** List of line segments comprising the instance. */
+ private final List<Segment> segments;
+
+ /** Simple constructor. No validation is performed on the input segments.
+ * @param segments line segments comprising the instance
+ */
+ private Polyline(final List<Segment> segments) {
+ this.segments = Collections.unmodifiableList(segments);
+ }
+
+ /** Get the line segments comprising the polyline.
+ * @return the line segments comprising the polyline
+ */
+ public List<Segment> getSegments() {
+ return segments;
+ }
+
+ /** Get the start segment for the polyline or null if the polyline is empty.
+ * @return the start segment for the polyline or null if the polyline is empty
+ */
+ public Segment getStartSegment() {
+ if (!isEmpty()) {
+ return segments.get(0);
+ }
+ return null;
+ }
+
+ /** Get the end segment for the polyline or null if the polyline is empty.
+ * @return the end segment for the polyline or null if the polyline is empty
+ */
+ public Segment getEndSegment() {
+ if (!isEmpty()) {
+ return segments.get(segments.size() - 1);
+ }
+ return null;
+ }
+
+ /** Get the start vertex for the polyline or null if the polyline is empty
+ * or has an infinite start segment.
+ * @return the start vertex for the polyline
+ */
+ public Vector2D getStartVertex() {
+ final Segment seg = getStartSegment();
+ return (seg != null) ? seg.getStartPoint() : null;
+ }
+
+ /** Get the end vertex for the polyline or null if the polyline is empty
+ * or has an infinite end segment.
+ * @return the end vertex for the polyline
+ */
+ public Vector2D getEndVertex() {
+ final Segment seg = getEndSegment();
+ return (seg != null) ? seg.getEndPoint() : null;
+ }
+
+ /** Get the vertices contained in the polyline in the order they appear.
+ * Closed polyline contain the start point at the beginning of the list
+ * as well as the end.
+ * @return the vertices contained in the polyline in order they appear
+ */
+ public List<Vector2D> getVertices() {
+ final List<Vector2D> vertices = new ArrayList<>();
+
+ Vector2D pt;
+
+ // add the start point, if present
+ pt = getStartVertex();
+ if (pt != null) {
+ vertices.add(pt);
+ }
+
+ // add end points
+ for (Segment seg : segments) {
+ pt = seg.getEndPoint();
+ if (pt != null) {
+ vertices.add(pt);
+ }
+ }
+
+ return vertices;
+ }
+
+ /** Return true if the polyline has a start of end line segment that
+ * extends to infinity.
+ * @return true if the polyline is infinite
+ */
+ public boolean isInfinite() {
+ return !isEmpty() && (getStartVertex() == null || getEndVertex() == null);
+ }
+
+ /** Return true if the polyline has a finite length. This will be true if there are
+ * no segments in the polyline or if all segments have a finite length.
+ * @return true if the polyline is finite
+ */
+ public boolean isFinite() {
+ return !isInfinite();
+ }
+
+ /** Return true if the polyline does not contain any line segments.
+ * @return true if the polyline does not contain any line segments
+ */
+ public boolean isEmpty() {
+ return segments.isEmpty();
+ }
+
+ /** Return true if the polyline is closed, meaning that the end
+ * point for the last line segment is equal to the start point
+ * for the polyline.
+ * @return true if the end point for the last line segment is
+ * equal to the start point for the polyline.
+ */
+ public boolean isClosed() {
+ final Segment endSegment = getEndSegment();
+
+ if (endSegment != null) {
+ final Vector2D start = getStartVertex();
+ final Vector2D end = endSegment.getEndPoint();
+
+ return start != null && end != null && start.eq(end, endSegment.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 Polyline transform(final Transform<Vector2D> transform) {
+ if (!isEmpty()) {
+ final List<Segment> transformed = segments.stream()
+ .map(s -> s.transform(transform))
+ .collect(Collectors.toCollection(() -> new ArrayList<>()));
+
+ return new Polyline(transformed);
+ }
+
+ return this;
+ }
+
+ /** Return a new instance with all line segment directions, and their order,
+ * reversed. The last segment in this instance will be the first in the
+ * returned instance.
+ * @return a new instance with the polyline reversed
+ */
+ public Polyline reverse() {
+ if (!isEmpty()) {
+ final List<Segment> reversed = segments.stream()
+ .map(s -> s.reverse())
+ .collect(Collectors.toCollection(() -> new ArrayList<>()));
+ Collections.reverse(reversed);
+
+ return new Polyline(reversed);
+ }
+
+ return this;
+ }
+
+ /** Construct a {@link RegionBSPTree2D} from the line segments in this instance.
+ * @return a bsp tree constructed from the line segments in this instance
+ */
+ public RegionBSPTree2D toTree() {
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.insert(this);
+
+ return tree;
+ }
+
+ /** Simplify this polyline, if possible, by combining adjacent segments that lie on the
+ * same line (as determined by {@link Line#equals(Object)}).
+ * @return a simplified instance
+ */
+ public Polyline simplify() {
+ final List<Segment> simplified = new ArrayList<>();
+
+ final int size = segments.size();
+
+ Segment current;
+ Line currentLine;
+ double end;
+
+ int idx = 0;
+ int testIdx;
+ while (idx < size) {
+ current = segments.get(idx);
+ currentLine = current.getLine();
+ end = current.getSubspaceEnd();
+
+ // try to combine with forward neighbors
+ testIdx = idx + 1;
+ while (testIdx < size && currentLine.equals(segments.get(testIdx).getLine())) {
+ end = Math.max(end, segments.get(testIdx).getSubspaceEnd());
+ ++testIdx;
+ }
+
+ if (testIdx > idx + 1) {
+ // we found something to merge
+ simplified.add(currentLine.segment(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 Segment startSegment = simplified.get(0);
+ final Segment endSegment = simplified.remove(simplified.size() - 1);
+
+ final Segment combined = endSegment.getLine().segment(endSegment.getSubspaceStart(),
+ startSegment.getSubspaceEnd());
+
+ simplified.set(0, combined);
+ }
+
+ return new SimplifiedPolyline(simplified);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterator<Segment> iterator() {
+ return segments.iterator();
+ }
+
+ /** Return a string representation of the segment polyline.
+ *
+ * <p>In order to keep the string representation short but useful, the exact format of the return
+ * value depends on the properties of the polyline. See below for examples.
+ *
+ * <ul>
+ * <li>Empty path
+ * <ul>
+ * <li>{@code Polyline[empty= true]}</li>
+ * </ul>
+ * </li>
+ * <li>Single segment
+ * <ul>
+ * <li>{@code Polyline[segment= Segment[lineOrigin= (0.0, 0.0), lineDirection= (1.0, 0.0)]]}</li>
+ * <li>{@code Polyline[segment= Segment[start= (0.0, 0.0), end= (1.0, 0.0)]]}</li>
+ * </ul>
+ * </li>
+ * <li>Path with infinite start segment
+ * <ul>
+ * <li>{@code Polyline[startDirection= (1.0, 0.0), vertices= [(1.0, 0.0), (1.0, 1.0)]]}</li>
+ * </ul>
+ * </li>
+ * <li>Path with infinite end segment
+ * <ul>
+ * <li>{@code Polyline[vertices= [(0.0, 1.0), (0.0, 0.0)], endDirection= (1.0, 0.0)]}</li>
+ * </ul>
+ * </li>
+ * <li>Path with infinite start and end segments
+ * <ul>
+ * <li>{@code Polyline[startDirection= (0.0, 1.0), vertices= [(0.0, 0.0)], endDirection= (1.0, 0.0)]}</li>
+ * </ul>
+ * </li>
+ * <li>Path with no infinite segments
+ * <ul>
+ * <li>{@code Polyline[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 (segments.isEmpty()) {
+ sb.append("empty= true");
+ } else if (segments.size() == 1) {
+ sb.append("segment= ")
+ .append(segments.get(0));
+ } else {
+ final Segment startSegment = getStartSegment();
+ if (startSegment.getStartPoint() == null) {
+ sb.append("startDirection= ")
+ .append(startSegment.getLine().getDirection())
+ .append(", ");
+ }
+
+ sb.append("vertices= ")
+ .append(getVertices());
+
+ final Segment endSegment = getEndSegment();
+ if (endSegment.getEndPoint() == null) {
+ sb.append(", endDirection= ")
+ .append(endSegment.getLine().getDirection());
+ }
+ }
+
+ sb.append(']');
+
+ return sb.toString();
+ }
+
+ /** 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);
+ }
+
+ /** Build a new polyline from the given segments.
+ * @param segments the segment to comprise the polyline
+ * @return new polyline containing the given line segment in order
+ * @throws IllegalStateException if the segments do not form a connected polyline
+ */
+ public static Polyline fromSegments(final Segment... segments) {
+ return fromSegments(Arrays.asList(segments));
+ }
+
+ /** Build a new polyline from the given segments.
+ * @param segments the segment to comprise the path
+ * @return new polyline containing the given line segments in order
+ * @throws IllegalStateException if the segments do not form a connected polyline
+ */
+ public static Polyline fromSegments(final Collection<Segment> segments) {
+ Builder builder = builder(null);
+
+ for (Segment segment : segments) {
+ builder.append(segment);
+ }
+
+ return builder.build();
+ }
+
+ /** Build a new polyline 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 polyline constructed from the given vertices
+ * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+ */
+ public static Polyline fromVertexLoop(final Collection<Vector2D> vertices,
+ final DoublePrecisionContext precision) {
+
+ return fromVertices(vertices, true, precision);
+ }
+
+ /** Build a new polyline 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 polyline constructed from the given vertices
+ * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+ */
+ public static Polyline fromVertices(final Collection<Vector2D> vertices,
+ final DoublePrecisionContext precision) {
+
+ return fromVertices(vertices, false, precision);
+ }
+
+ /** Build a new polyline 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 polyline constructed from the given vertices
+ */
+ public static Polyline fromVertices(final Collection<Vector2D> vertices,
+ final boolean close, final DoublePrecisionContext precision) {
+
+ return builder(precision)
+ .appendVertices(vertices)
+ .build(close);
+ }
+
+ /** Return a line segment path containing no segments.
+ * @return a line segment path containing no segments.
+ */
+ public static Polyline empty() {
+ return EMPTY;
+ }
+
+ /** Class used to build polylines.
+ */
+ public static final class Builder implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190522L;
+
+ /** Line segments appended to the polyline. */
+ private List<Segment> appendedSegments = null;
+
+ /** Line segments prepended to the polyline. */
+ private List<Segment> prependedSegments = null;
+
+ /** Precision context used when creating line segments directly from vertices. */
+ private DoublePrecisionContext precision;
+
+ /** The current vertex at the start of the polyline. */
+ private Vector2D startVertex;
+
+ /** The current vertex at the end of the polyline. */
+ 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 Segment} 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 segment at the start of the polyline or null if
+ * it does not exist.
+ * @return the line segment at the start of the polyline
+ */
+ public Segment getStartSegment() {
+ Segment start = getLast(prependedSegments);
+ if (start == null) {
+ start = getFirst(appendedSegments);
+ }
+ return start;
+ }
+
+ /** Get the line segment at the end of the polyline or null if
+ * it does not exist.
+ * @return the line segment at the end of the polyline
+ */
+ public Segment getEndSegment() {
+ Segment end = getLast(appendedSegments);
+ if (end == null) {
+ end = getFirst(prependedSegments);
+ }
+ return end;
+ }
+
+ /** Append a line segment to the end of the polyline.
+ * @param segment line segment to append to the polyline
+ * @return the current builder instance
+ * @throws IllegalStateException if the polyline contains a previous segment
+ * and the end vertex of the previous segment is not equivalent to the
+ * start vertex of the given segment.
+ */
+ public Builder append(final Segment segment) {
+ validateSegmentsConnected(getEndSegment(), segment);
+ appendInternal(segment);
+
+ return this;
+ }
+
+ /** Add a vertex to the end of this polyline. If the polyline 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 segment
+ final Segment end = getEndSegment();
+ if (end != null) {
+ throw new IllegalStateException(
+ MessageFormat.format("Cannot add vertex {0} after infinite line segment: {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 segment
+ appendInternal(Segment.fromPoints(endVertex, vertex, endVertexPrecision));
+ }
+
+ return this;
+ }
+
+ /** Convenience method for appending a collection of vertices to the polyline in a single method call.
+ * @param vertices the vertices to append
+ * @return this instance
+ * @see #append(Vector2D)
+ */
+ public Builder appendVertices(final Collection<Vector2D> vertices) {
+ for (Vector2D vertex : vertices) {
+ append(vertex);
+ }
+
+ return this;
+ }
+
+ /** Convenience method for appending multiple vertices to the polyline 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 segment to the beginning of the polyline.
+ * @param segment line segment to prepend to the polyline
+ * @return the current builder instance
+ * @throws IllegalStateException if the polyline contains a start segment
+ * and the end vertex of the given segment is not equivalent to the
+ * start vertex of the start segment.
+ */
+ public Builder prepend(final Segment segment) {
+ validateSegmentsConnected(segment, getStartSegment());
+ prependInternal(segment);
+
+ return this;
+ }
+
+ /** Add a vertex to the front of this polyline. If the polyline 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 segment
+ final Segment start = getStartSegment();
+ if (start != null) {
+ throw new IllegalStateException(
+ MessageFormat.format("Cannot add vertex {0} before infinite line segment: {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 segment
+ prependInternal(Segment.fromPoints(vertex, startVertex, vertexPrecision));
+ }
+
+ return this;
+ }
+
+ /** Convenience method for prepending a collection of vertices to the polyline 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 polyline 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 polyline 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 polyline 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 polyline and build a new {@link Polyline} instance. This method is equivalent
+ * to {@code builder.build(true)}.
+ * @return new closed polyline instance
+ */
+ public Polyline close() {
+ return build(true);
+ }
+
+ /** Build a {@link Polyline} instance from the configured polyline. This method is equivalent
+ * to {@code builder.build(false)}.
+ * @return new polyline instance
+ */
+ public Polyline build() {
+ return build(false);
+ }
+
+ /** Build a {@link Polyline} instance from the configured polyline.
+ * @param close if true, the path will be closed by adding an end point equivalent to the
+ * start point
+ * @return new polyline instance
+ */
+ public Polyline build(final boolean close) {
+ if (close) {
+ closePath();
+ }
+
+ // combine all of the segments
+ List<Segment> result = null;
+
+ if (prependedSegments != null) {
+ result = prependedSegments;
+ Collections.reverse(result);
+ }
+
+ if (appendedSegments != null) {
+ if (result == null) {
+ result = appendedSegments;
+ } else {
+ result.addAll(appendedSegments);
+ }
+ }
+
+ if (result == null) {
+ result = Collections.emptyList();
+ }
+
+ if (result.isEmpty() && startVertex != null) {
+ throw new IllegalStateException(
+ MessageFormat.format("Unable to create polyline; only a single vertex provided: {0} ",
+ startVertex));
+ }
+
+ // clear internal state
+ appendedSegments = null;
+ prependedSegments = null;
+
+ // build the final polyline instance, using the shared empty instance if
+ // no segments are present
+ return result.isEmpty() ? empty() : new Polyline(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 Segment end = getEndSegment();
+
+ if (end != null) {
+ if (startVertex != null && endVertex != null) {
+ if (!endVertex.eq(startVertex, endVertexPrecision)) {
+ appendInternal(Segment.fromPoints(endVertex, startVertex, endVertexPrecision));
+ }
+ } else {
+ throw new IllegalStateException("Unable to close polyline: polyline is infinite");
+ }
+ }
+ }
+
+ /** Validate that the given segments are connected, meaning that the end vertex of {@code previous}
+ * is equivalent to the start vertex of {@code next}. The segments are considered valid if either
+ * segment is null.
+ * @param previous previous segment
+ * @param next next segment
+ * @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 validateSegmentsConnected(final Segment previous, final Segment 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("Polyline segments are not connected: previous= {0}, next= {1}",
+ previous, next));
+ }
+ }
+ }
+
+ /** Get the precision context used when adding raw vertices to the polyline. 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 segment to the polyline.
+ * @param segment validated segment to append
+ */
+ private void appendInternal(final Segment segment) {
+ if (appendedSegments == null) {
+ appendedSegments = new ArrayList<>();
+ }
+
+ if (appendedSegments.isEmpty() &&
+ (prependedSegments == null || prependedSegments.isEmpty())) {
+ startVertex = segment.getStartPoint();
+ }
+
+ endVertex = segment.getEndPoint();
+ endVertexPrecision = segment.getPrecision();
+
+ appendedSegments.add(segment);
+ }
+
+ /** Prepend the given, validated segment to the polyline.
+ * @param segment validated segment to prepend
+ */
+ private void prependInternal(final Segment segment) {
+ if (prependedSegments == null) {
+ prependedSegments = new ArrayList<>();
+ }
+
+ startVertex = segment.getStartPoint();
+
+ if (prependedSegments.isEmpty() &&
+ (appendedSegments == null || appendedSegments.isEmpty())) {
+ endVertex = segment.getEndPoint();
+ endVertexPrecision = segment.getPrecision();
+ }
+
+ prependedSegments.add(segment);
+ }
+
+ /** 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 Segment getFirst(final List<Segment> list) {
+ if (list != null && list.size() > 0) {
+ 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 Segment getLast(final List<Segment> list) {
+ if (list != null && list.size() > 0) {
+ return list.get(list.size() - 1);
+ }
+ return null;
+ }
+ }
+
+ /** Internal class returned when a polyline is simplified to remove
+ * unecessary line segments divisions. The {@link #simplify()} method on this
+ * class simply returns the same instance.
+ */
+ private static final class SimplifiedPolyline extends Polyline {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190619;
+
+ /** Create a new instance containing the given line segments. No validation is
+ * performed on the inputs. Caller must ensure that the given segments represent
+ * a valid, simplified path.
+ * @param segments line segments comprising the path
+ */
+ private SimplifiedPolyline(final List<Segment> segments) {
+ super(segments);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Polyline simplify() {
+ // already simplified
+ return this;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
new file mode 100644
index 0000000..c577eb3
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
@@ -0,0 +1,506 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Binary space partitioning (BSP) tree representing a region in two dimensional
+ * Euclidean space.
+ */
+public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, RegionBSPTree2D.RegionNode2D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190519L;
+
+ /** List of line segment paths comprising the region boundary. */
+ private List<Polyline> boundaryPaths;
+
+ /** Create a new, empty region.
+ */
+ public RegionBSPTree2D() {
+ this(false);
+ }
+
+ /** Create a new region. If {@code full} is true, then the region will
+ * represent the entire 2D space. Otherwise, it will be empty.
+ * @param full whether or not the region should contain the entire
+ * 2D space or be empty
+ */
+ public RegionBSPTree2D(boolean full) {
+ super(full);
+ }
+
+ /** Return a deep copy of this instance.
+ * @return a deep copy of this instance.
+ * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
+ */
+ public RegionBSPTree2D copy() {
+ RegionBSPTree2D result = RegionBSPTree2D.empty();
+ result.copy(this);
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable<Segment> boundaries() {
+ return createBoundaryIterable(b -> (Segment) b);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<Segment> getBoundaries() {
+ return createBoundaryList(b -> (Segment) b);
+ }
+
+ /** Get the boundary of the region as a list of connected line segment paths. The
+ * line segments are oriented such that their minus (left) side lies on the
+ * interior of the region.
+ * @return line segment paths representing the region boundary
+ */
+ public List<Polyline> getBoundaryPaths() {
+ if (boundaryPaths == null) {
+ boundaryPaths = Collections.unmodifiableList(computeBoundaryPaths());
+ }
+ return boundaryPaths;
+ }
+
+ /** Add a convex area to this region. The resulting region will be the
+ * union of the convex area and the region represented by this instance.
+ * @param area the convex area to add
+ */
+ public void add(final ConvexArea area) {
+ union(from(area));
+ }
+
+ /** Return a list of {@link ConvexArea}s representing the same region
+ * as this instance. One convex area is returned for each interior leaf
+ * node in the tree.
+ * @return a list of convex areas representing the same region as this
+ * instance
+ */
+ public List<ConvexArea> toConvex() {
+ final List<ConvexArea> result = new ArrayList<>();
+
+ toConvexRecursive(getRoot(), ConvexArea.full(), result);
+
+ return result;
+ }
+
+ /** Recursive method to compute the convex areas of all inside leaf nodes in the subtree rooted at the given
+ * node. The computed convex areas are added to the given list.
+ * @param node root of the subtree to compute the convex areas for
+ * @param nodeArea the convex area for the current node; this will be split by the node's cut hyperplane to
+ * form the convex areas for any child nodes
+ * @param result list containing the results of the computation
+ */
+ private void toConvexRecursive(final RegionNode2D node, final ConvexArea nodeArea, final List<ConvexArea> result) {
+ if (node.isLeaf()) {
+ // base case; only add to the result list if the node is inside
+ if (node.isInside()) {
+ result.add(nodeArea);
+ }
+ } else {
+ // recurse
+ Split<ConvexArea> split = nodeArea.split(node.getCutHyperplane());
+
+ toConvexRecursive(node.getMinus(), split.getMinus(), result);
+ toConvexRecursive(node.getPlus(), split.getPlus(), result);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<RegionBSPTree2D> split(final Hyperplane<Vector2D> splitter) {
+ return split(splitter, RegionBSPTree2D.empty(), RegionBSPTree2D.empty());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D project(final Vector2D pt) {
+ // use our custom projector so that we can disambiguate points that are
+ // actually equidistant from the target point
+ final BoundaryProjector2D projector = new BoundaryProjector2D(pt);
+ accept(projector);
+
+ return projector.getProjected();
+ }
+
+ /** Compute the line segment paths comprising the region boundary.
+ * @return the line segment paths comprising the region boundary
+ */
+ private List<Polyline> computeBoundaryPaths() {
+ final InteriorAngleSegmentConnector connector = new InteriorAngleSegmentConnector.Minimize();
+ connector.connect(boundaries());
+
+ return connector.connectAll().stream()
+ .map(Polyline::simplify).collect(Collectors.toList());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionSizeProperties<Vector2D> computeRegionSizeProperties() {
+ // handle simple cases
+ if (isFull()) {
+ return new RegionSizeProperties<>(Double.POSITIVE_INFINITY, null);
+ } else if (isEmpty()) {
+ return new RegionSizeProperties<>(0, null);
+ }
+
+ // compute the size based on the boundary segments
+ double quadrilateralAreaSum = 0.0;
+
+ double scaledSumX = 0.0;
+ double scaledSumY = 0.0;
+
+ Vector2D startPoint;
+ Vector2D endPoint;
+ double signedArea;
+
+ for (Segment segment : boundaries()) {
+
+ if (segment.isInfinite()) {
+ // at least on boundary is infinite, meaning that
+ // the size is also infinite
+ quadrilateralAreaSum = Double.POSITIVE_INFINITY;
+
+ break;
+ }
+
+ startPoint = segment.getStartPoint();
+ endPoint = segment.getEndPoint();
+
+ // compute the area
+ signedArea = startPoint.signedArea(endPoint);
+
+ quadrilateralAreaSum += signedArea;
+
+ // compute scaled coordinate values for the barycenter
+ scaledSumX += signedArea * (startPoint.getX() + endPoint.getX());
+ scaledSumY += signedArea * (startPoint.getY() + endPoint.getY());
+ }
+
+ double size = Double.POSITIVE_INFINITY;
+ Vector2D barycenter = null;
+
+ // The area is finite only if the computed quadrilateral area is finite and non-negative.
+ // Negative areas indicate that the region is inside-out, with a finite outside surrounded
+ // by an infinite inside.
+ if (quadrilateralAreaSum >= 0.0 && Double.isFinite(quadrilateralAreaSum)) {
+ size = 0.5 * quadrilateralAreaSum;
+
+ if (quadrilateralAreaSum > 0.0) {
+ barycenter = Vector2D.of(scaledSumX, scaledSumY).multiply(1.0 / (3.0 * quadrilateralAreaSum));
+ }
+ }
+
+ return new RegionSizeProperties<>(size, barycenter);
+ }
+
+ /** Compute the region represented by the given node.
+ * @param node the node to compute the region for
+ * @return the region represented by the given node
+ */
+ private ConvexArea computeNodeRegion(final RegionNode2D node) {
+ ConvexArea area = ConvexArea.full();
+
+ RegionNode2D child = node;
+ RegionNode2D parent;
+
+ while ((parent = child.getParent()) != null) {
+ Split<ConvexArea> split = area.split(parent.getCutHyperplane());
+
+ area = child.isMinus() ? split.getMinus() : split.getPlus();
+
+ child = parent;
+ }
+
+ return area;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void invalidate() {
+ super.invalidate();
+
+ boundaryPaths = null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode2D createNode() {
+ return new RegionNode2D(this);
+ }
+
+ /** Return a new {@link RegionBSPTree2D} instance containing the entire space.
+ * @return a new {@link RegionBSPTree2D} instance containing the entire space
+ */
+ public static RegionBSPTree2D full() {
+ return new RegionBSPTree2D(true);
+ }
+
+ /** Return a new, empty {@link RegionBSPTree2D} instance.
+ * @return a new, empty {@link RegionBSPTree2D} instance
+ */
+ public static RegionBSPTree2D empty() {
+ return new RegionBSPTree2D(false);
+ }
+
+ /** Construct a tree from a convex area.
+ * @param area the area to construct a tree from
+ * @return tree instance representing the same area as the given
+ * convex area
+ */
+ public static RegionBSPTree2D from(final ConvexArea area) {
+ final RegionBSPTree2D tree = RegionBSPTree2D.full();
+ tree.insert(area.getBoundaries());
+
+ return tree;
+ }
+
+ /** Create a new {@link RegionBSPTree2D.Builder} instance for creating BSP
+ * trees from boundary representations.
+ * @param precision precision context to use for floating point comparisons.
+ * @return a new builder instance
+ */
+ public static Builder builder(final DoublePrecisionContext precision) {
+ return new Builder(precision);
+ }
+
+ /** BSP tree node for two dimensional Euclidean space.
+ */
+ public static final class RegionNode2D extends AbstractRegionBSPTree.AbstractRegionNode<Vector2D, RegionNode2D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190519L;
+
+ /** Simple constructor.
+ * @param tree the owning tree instance
+ */
+ private RegionNode2D(AbstractBSPTree<Vector2D, RegionNode2D> tree) {
+ super(tree);
+ }
+
+ /** Get the region represented by this node. The returned region contains
+ * the entire area contained in this node, regardless of the attributes of
+ * any child nodes.
+ * @return the region represented by this node
+ */
+ public ConvexArea getNodeRegion() {
+ return ((RegionBSPTree2D) getTree()).computeNodeRegion(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode2D getSelf() {
+ return this;
+ }
+ }
+
+ /** Class used to construct {@link RegionBSPTree2D} instances from boundary representations.
+ */
+ public static final class Builder {
+
+ /** Precision object used to perform floating point comparisons. This object is
+ * used when constructing geometric types.
+ */
+ private final DoublePrecisionContext precision;
+
+ /** The BSP tree being constructed. */
+ private final RegionBSPTree2D tree = RegionBSPTree2D.empty();
+
+ /** Create a new builder instance. The given precision context will be used when
+ * constructing geometric types.
+ * @param precision precision object used to perform floating point comparisons
+ */
+ public Builder(final DoublePrecisionContext precision) {
+ this.precision = precision;
+ }
+
+ /** Add a subline to the tree.
+ * @param subline subline to add
+ * @return this builder instance
+ */
+ public Builder add(final SubLine subline) {
+ tree.insert(subline);
+ return this;
+ }
+
+ /** Add a segment to the tree.
+ * @param segment segment to add
+ * @return this builder instance
+ */
+ public Builder add(final Segment segment) {
+ tree.insert(segment);
+ return this;
+ }
+
+ /** Add the line segments defined in the given segment path.
+ * @param path path containing line segments to add
+ * @return this builder instance
+ */
+ public Builder add(final Polyline path) {
+ for (Segment segment : path) {
+ add(segment);
+ }
+
+ return this;
+ }
+
+ /** Add a segment defined by the given points.
+ * @param start start point
+ * @param end end point
+ * @return this builder instance
+ */
+ public Builder addSegment(final Vector2D start, final Vector2D end) {
+ return add(Segment.fromPoints(start, end, precision));
+ }
+
+ /** Add segments defining an axis-oriented square with the given corner point and size.
+ * @param center center point of the square
+ * @param size the size of the square
+ * @return this builder instance
+ * @throws GeometryValueException if the width or height of the defined rectangle is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addCenteredSquare(final Vector2D center, final double size) {
+ return addCenteredRect(center, size, size);
+ }
+
+ /** Add segments defining an axis-oriented square with the given corner point and size.
+ * @param corner point in the corner of the square
+ * @param size the size of the square
+ * @return this builder instance
+ * @throws GeometryValueException if the width or height of the defined rectangle is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addSquare(final Vector2D corner, final double size) {
+ return addRect(corner, size, size);
+ }
+
+ /** Add segments defining an axis-oriented rectangular region with the given center point and size.
+ * @param center center point for the region
+ * @param xSize size along the x-axis
+ * @param ySize size along the y-axis
+ * @return this builder instance
+ * @throws GeometryValueException if the width or height of the defined rectangle is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addCenteredRect(final Vector2D center, final double xSize, final double ySize) {
+ return addRect(Vector2D.of(
+ center.getX() - (0.5 * xSize),
+ center.getY() - (0.5 * ySize)
+ ), xSize, ySize);
+ }
+
+ /** Add segments defining an axis-oriented rectangular region. The region
+ * is constructed by taking {@code pt} as one corner of the region and adding {@code xDelta}
+ * and {@code yDelta} to its components to create the opposite corner. If {@code xDelta}
+ * and {@code yDelta} are both positive, then the constructed rectangle will have {@code pt}
+ * as its lower-left corner and will have a width and height of {@code xDelta} and {@code yDelta}
+ * respectively.
+ * @param pt point lying in a corner of the region
+ * @param xDelta distance to move along the x axis to place the other points in the
+ * rectangle; this value may be negative, in which case {@code pt} will lie
+ * on the right side of the constructed rectangle
+ * @param yDelta distance to move along the y axis to place the other points in the
+ * rectangle; this value may be negative, in which case {@code pt} will lie
+ * on the top of the rectangle
+ * @return this builder instance
+ * @throws GeometryValueException if the width or height of the defined rectangle is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addRect(final Vector2D pt, final double xDelta, final double yDelta) {
+ return addRect(pt, Vector2D.of(
+ pt.getX() + xDelta,
+ pt.getY() + yDelta));
+ }
+
+ /** Add segments defining an axis-oriented rectangular region. The points {@code a} and {@code b}
+ * are taken to represent opposite corner points in the rectangle and may be specified in any order.
+ * @param a first corner point in the rectangle (opposite of {@code b})
+ * @param b second corner point in the rectangle (opposite of {@code a})
+ * @return this builder instance
+ * @throws GeometryValueException if the width or height of the defined rectangle is zero
+ * as evaluated by the precision context.
+ */
+ public Builder addRect(final Vector2D a, final Vector2D b) {
+ final double minX = Math.min(a.getX(), b.getX());
+ final double maxX = Math.max(a.getX(), b.getX());
+
+ final double minY = Math.min(a.getY(), b.getY());
+ final double maxY = Math.max(a.getY(), b.getY());
+
+ if (precision.eq(minX, maxX) || precision.eq(minY, maxY)) {
+ throw new GeometryValueException("Rectangle has zero size: " + a + ", " + b + ".");
+ }
+
+ final Vector2D lowerLeft = Vector2D.of(minX, minY);
+ final Vector2D upperLeft = Vector2D.of(minX, maxY);
+
+ final Vector2D upperRight = Vector2D.of(maxX, maxY);
+ final Vector2D lowerRight = Vector2D.of(maxX, minY);
+
+ addSegment(lowerLeft, lowerRight);
+ addSegment(upperRight, upperLeft);
+ addSegment(lowerRight, upperRight);
+ addSegment(upperLeft, lowerLeft);
+
+ return this;
+ }
+
+ /** Get the created BSP tree.
+ * @return the created BSP tree
+ */
+ public RegionBSPTree2D build() {
+ return tree;
+ }
+ }
+
+ /** Class used to project points onto the 2D region boundary.
+ */
+ private static final class BoundaryProjector2D extends BoundaryProjector<Vector2D, RegionNode2D> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190811L;
+
+ /** Simple constructor.
+ * @param point the point to project onto the region's boundary
+ */
+ BoundaryProjector2D(final Vector2D point) {
+ super(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector2D disambiguateClosestPoint(final Vector2D target, final Vector2D a, final Vector2D b) {
+ // return the point with the smallest coordinate values
+ final int cmp = Vector2D.COORDINATE_ASCENDING_ORDER.compare(a, b);
+ return cmp < 0 ? a : b;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
index b2f82b3..2f959fc 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
@@ -16,92 +16,294 @@
*/
package org.apache.commons.geometry.euclidean.twod;
-/** Simple container for a two-points segment.
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.oned.FunctionTransform1D;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.twod.Line.SubspaceTransform;
+
+/** <p>Class representing a line segment in 2D Euclidean space. Segments
+ * need not be finite, in which case the start or end point (or both)
+ * will be null.</p>
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
*/
-public class Segment {
+public final class Segment extends AbstractSubLine
+ implements ConvexSubHyperplane<Vector2D> {
- /** Start point of the segment. */
- private final Vector2D start;
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190729L;
- /** End point of the segment. */
- private final Vector2D end;
+ /** String used to indicate the start point of the segment in the toString() representation. */
+ private static final String START_STR = "start= ";
- /** Line containing the segment. */
- private final Line line;
+ /** String used to indicate the direction the segment in the toString() representation. */
+ private static final String DIR_STR = "direction= ";
- /** Build a segment.
- * @param start start point of the segment
- * @param end end point of the segment
- * @param line line containing the segment
+ /** String used to indicate the end point of the segment in the toString() representation. */
+ private static final String END_STR = "end= ";
+
+ /** String used as a separator value in the toString() representation. */
+ private static final String SEP_STR = ", ";
+
+ /** The interval representing the region of the line contained in
+ * the line segment.
*/
- public Segment(final Vector2D start, final Vector2D end, final Line line) {
- this.start = start;
- this.end = end;
- this.line = line;
+ private final Interval interval;
+
+ /** Construct a line segment from an underlying line and a 1D interval
+ * on it.
+ * @param line the underlying line
+ * @param interval 1D interval on the line defining the line segment
+ */
+ private Segment(final Line line, final Interval interval) {
+ super(line);
+
+ this.interval = interval;
}
- /** Get the start point of the segment.
- * @return start point of the segment
+ /** Get the start value in the 1D subspace of the line.
+ * @return the start value in the 1D subspace of the line.
*/
- public Vector2D getStart() {
- return start;
+ public double getSubspaceStart() {
+ return interval.getMin();
}
- /** Get the end point of the segment.
- * @return end point of the segment
+ /** Get the end value in the 1D subspace of the line.
+ * @return the end value in the 1D subspace of the line
*/
- public Vector2D getEnd() {
- return end;
+ public double getSubspaceEnd() {
+ return interval.getMax();
}
- /** Get the line containing the segment.
- * @return line containing the segment
+ /** Get the start point of the line segment or null if no start point
+ * exists (ie, the segment is infinite).
+ * @return the start point of the line segment or null if no start point
+ * exists
*/
- public Line getLine() {
- return line;
+ public Vector2D getStartPoint() {
+ return interval.hasMinBoundary() ? getLine().toSpace(interval.getMin()) : null;
}
- /** Calculates the shortest distance from a point to this line segment.
- * <p>
- * If the perpendicular extension from the point to the line does not
- * cross in the bounds of the line segment, the shortest distance to
- * the two end points will be returned.
- * </p>
- *
- * Algorithm adapted from:
- * <a href="http://www.codeguru.com/forum/printthread.php?s=cc8cf0596231f9a7dba4da6e77c29db3&t=194400&pp=15&page=1">
- * Thread @ Codeguru</a>
- *
- * @param p to check
- * @return distance between the instance and the point
+ /** Get the end point of the line segment or null if no end point
+ * exists (ie, the segment is infinite).
+ * @return the end point of the line segment or null if no end point
+ * exists
*/
- public double distance(final Vector2D p) {
- final double deltaX = end.getX() - start.getX();
- final double deltaY = end.getY() - start.getY();
+ public Vector2D getEndPoint() {
+ return interval.hasMaxBoundary() ? getLine().toSpace(interval.getMax()) : null;
+ }
- final double r = ((p.getX() - start.getX()) * deltaX + (p.getY() - start.getY()) * deltaY) /
- (deltaX * deltaX + deltaY * deltaY);
+ /** Return the 1D interval for the line segment.
+ * @return the 1D interval for the line segment
+ * @see #getSubspaceRegion()
+ */
+ public Interval getInterval() {
+ return interval;
+ }
- // r == 0 => P = startPt
- // r == 1 => P = endPt
- // r < 0 => P is on the backward extension of the segment
- // r > 1 => P is on the forward extension of the segment
- // 0 < r < 1 => P is on the segment
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return interval.isInfinite();
+ }
- // if point isn't on the line segment, just return the shortest distance to the end points
- if (r < 0 || r > 1) {
- final double dist1 = getStart().distance(p);
- final double dist2 = getEnd().distance(p);
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return interval.isFinite();
+ }
- return Math.min(dist1, dist2);
+ /** {@inheritDoc} */
+ @Override
+ public Interval getSubspaceRegion() {
+ return getInterval();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<Segment> toConvex() {
+ return Arrays.asList(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<Segment> split(final Hyperplane<Vector2D> splitter) {
+ return splitInternal(splitter, this, (line, region) -> new Segment(line, (Interval) region));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Segment transform(Transform<Vector2D> transform) {
+ final Line line = getLine();
+ final SubspaceTransform st = line.subspaceTransform(transform);
+
+ return fromInterval(st.getLine(), interval.transform(st.getTransform()));
+ }
+
+ /** Get the unique intersection of this segment with the given line. Null is
+ * returned if no unique intersection point exists (ie, the lines are
+ * parallel or coincident) or the line does not intersect the segment.
+ * @param line line to intersect with this segment
+ * @return the unique intersection point between the line and this segment
+ * or null if no such point exists.
+ * @see Line#intersection(Line)
+ */
+ public Vector2D intersection(final Line line) {
+ final Vector2D pt = getLine().intersection(line);
+ return (pt != null && contains(pt)) ? pt : null;
+ }
+
+ /** Get the unique intersection of this instance with the given segment. Null
+ * is returned if the lines containing the segments do not have a unique intersection
+ * point (ie, they are parallel or coincident) or the intersection point is unique
+ * but in not contained in both segments.
+ * @param segment segment to intersect with
+ * @return the unique intersection point between this segment and the argument or
+ * null if no such point exists.
+ * @see Line#intersection(Line)
+ */
+ public Vector2D intersection(final Segment segment) {
+ final Vector2D pt = intersection(segment.getLine());
+ return (pt != null && segment.contains(pt)) ? pt : null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Segment reverse() {
+ final Interval reversedInterval = interval.transform(FunctionTransform1D.from(Vector1D::negate));
+ return fromInterval(getLine().reverse(), reversedInterval);
+ }
+
+ /** Return a string representation of the segment.
+ *
+ * <p>In order to keep the representation short but informative, the exact format used
+ * depends on the properties of the instance, as demonstrated in the examples
+ * below.
+ * <ul>
+ * <li>Infinite segment -
+ * {@code "Segment[lineOrigin= (0.0, 0.0), lineDirection= (1.0, 0.0)]"}</li>
+ * <li>Start point but no end point -
+ * {@code "Segment[start= (0.0, 0.0), direction= (1.0, 0.0)]"}</li>
+ * <li>End point but no start point -
+ * {@code "Segment[direction= (1.0, 0.0), end= (0.0, 0.0)]"}</li>
+ * <li>Start point and end point -
+ * {@code "Segment[start= (0.0, 0.0), end= (1.0, 0.0)]"}</li>
+ * </ul>
+ */
+ @Override
+ public String toString() {
+ final Vector2D startPoint = getStartPoint();
+ final Vector2D endPoint = getEndPoint();
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append('[');
+
+ if (startPoint != null && endPoint != null) {
+ sb.append(START_STR)
+ .append(startPoint)
+ .append(SEP_STR)
+ .append(END_STR)
+ .append(endPoint);
+ } else if (startPoint != null) {
+ sb.append(START_STR)
+ .append(startPoint)
+ .append(SEP_STR)
+ .append(DIR_STR)
+ .append(getLine().getDirection());
+ } else if (endPoint != null) {
+ sb.append(DIR_STR)
+ .append(getLine().getDirection())
+ .append(SEP_STR)
+ .append(END_STR)
+ .append(endPoint);
+ } else {
+ final Line line = getLine();
+
+ sb.append("lineOrigin= ")
+ .append(line.getOrigin())
+ .append(", lineDirection= ")
+ .append(line.getDirection());
}
- else {
- // find point on line and see if it is in the line segment
- final double px = start.getX() + r * deltaX;
- final double py = start.getY() + r * deltaY;
- final Vector2D interPt = Vector2D.of(px, py);
- return interPt.distance(p);
- }
+ sb.append(']');
+
+ return sb.toString();
+ }
+
+ /** Create a line segment between two points. The underlying line points in the direction from {@code start}
+ * to {@code end}.
+ * @param start start point for the line segment
+ * @param end end point for the line segment
+ * @param precision precision context used to determine floating point equality
+ * @return a new line segment between {@code start} and {@code end}.
+ */
+ public static Segment fromPoints(final Vector2D start, final Vector2D end, final DoublePrecisionContext precision) {
+ final Line line = Line.fromPoints(start, end, precision);
+ return fromPointsOnLine(line, start, end);
+ }
+
+ /** Construct a line segment from a starting point and a direction that the line should extend to
+ * infinity from. This is equivalent to constructing a ray.
+ * @param start start point for the segment
+ * @param direction direction that the line should extend from the segment
+ * @param precision precision context used to determine floating point equality
+ * @return a new line segment starting from the given point and extending to infinity in the
+ * specified direction
+ */
+ public static Segment fromPointAndDirection(final Vector2D start, final Vector2D direction,
+ final DoublePrecisionContext precision) {
+ final Line line = Line.fromPointAndDirection(start, direction, precision);
+ return fromInterval(line, Interval.min(line.toSubspace(start).getX(), precision));
+ }
+
+ /** Create a line segment from an underlying line and a 1D interval on the line.
+ * @param line the line that the line segment will belong to
+ * @param interval 1D interval on the line
+ * @return a line segment defined by the given line and interval
+ */
+ public static Segment fromInterval(final Line line, final Interval interval) {
+ return new Segment(line, interval);
+ }
+
+ /** Create a line segment from an underlying line and a 1D interval on the line.
+ * @param line the line that the line segment will belong to
+ * @param a first 1D location on the line
+ * @param b second 1D location on the line
+ * @return a line segment defined by the given line and interval
+ */
+ public static Segment fromInterval(final Line line, final double a, final double b) {
+ return fromInterval(line, Interval.of(a, b, line.getPrecision()));
+ }
+
+ /** Create a line segment from an underlying line and a 1D interval on the line.
+ * @param line the line that the line segment will belong to
+ * @param a first 1D point on the line; must not be null
+ * @param b second 1D point on the line; must not be null
+ * @return a line segment defined by the given line and interval
+ */
+ public static Segment fromInterval(final Line line, final Vector1D a, final Vector1D b) {
+ return fromInterval(line, a.getX(), b.getX());
+ }
+
+ /** Create a new line segment from a line and points known to lie on the line.
+ * @param line the line that the line segment will belong to
+ * @param start line segment start point known to lie on the line
+ * @param end line segment end poitn known to lie on the line
+ * @return a new line segment created from the line and points
+ */
+ private static Segment fromPointsOnLine(final Line line, final Vector2D start, final Vector2D end) {
+ final double subspaceStart = line.toSubspace(start).getX();
+ final double subspaceEnd = line.toSubspace(end).getX();
+
+ return fromInterval(line, subspaceStart, subspaceEnd);
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java
index 3e07ffa..db234a4 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java
@@ -16,184 +16,210 @@
*/
package org.apache.commons.geometry.euclidean.twod;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
-import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.partitioning.SubHyperplane;
import org.apache.commons.geometry.euclidean.oned.Interval;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
+import org.apache.commons.geometry.euclidean.twod.Line.SubspaceTransform;
-/** This class represents a sub-hyperplane for {@link Line}.
+/** Class representing an arbitrary region of a line. This class can represent
+ * both convex and non-convex regions of its underlying line.
+ *
+ * <p>This class is mutable and <em>not</em> thread safe.</p>
*/
-public class SubLine extends AbstractSubHyperplane<Vector2D, Vector1D> {
+public final class SubLine extends AbstractSubLine implements Serializable {
- /** Simple constructor.
- * @param hyperplane underlying hyperplane
- * @param remainingRegion remaining region of the hyperplane
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190717L;
+
+ /** The 1D region representing the area on the line. */
+ private final RegionBSPTree1D region;
+
+ /** Construct a new, empty subline for the given line.
+ * @param line line defining the subline
*/
- public SubLine(final Hyperplane<Vector2D> hyperplane,
- final Region<Vector1D> remainingRegion) {
- super(hyperplane, remainingRegion);
+ public SubLine(final Line line) {
+ this(line, false);
}
- /** Create a sub-line from two endpoints.
- * @param start start point
- * @param end end point
- * @param precision precision context used to compare floating point values
+ /** Construct a new subline for the given line. If {@code full}
+ * is true, then the subline will cover the entire line; otherwise,
+ * it will be empty.
+ * @param line line defining the subline
+ * @param full if true, the subline will cover the entire space;
+ * otherwise it will be empty
*/
- public SubLine(final Vector2D start, final Vector2D end, final DoublePrecisionContext precision) {
- super(Line.fromPoints(start, end, precision), buildIntervalSet(start, end, precision));
+ public SubLine(final Line line, boolean full) {
+ this(line, new RegionBSPTree1D(full));
}
- /** Create a sub-line from a segment.
- * @param segment single segment forming the sub-line
+ /** Construct a new instance from its defining line and subspace region.
+ * @param line line defining the subline
+ * @param region subspace region for the subline
*/
- public SubLine(final Segment segment) {
- super(segment.getLine(),
- buildIntervalSet(segment.getStart(), segment.getEnd(), segment.getLine().getPrecision()));
+ public SubLine(final Line line, final RegionBSPTree1D region) {
+ super(line);
+
+ this.region = region;
}
- /** Get the endpoints of the sub-line.
- * <p>
- * A subline may be any arbitrary number of disjoints segments, so the endpoints
- * are provided as a list of endpoint pairs. Each element of the list represents
- * one segment, and each segment contains a start point at index 0 and an end point
- * at index 1. If the sub-line is unbounded in the negative infinity direction,
- * the start point of the first segment will have infinite coordinates. If the
- * sub-line is unbounded in the positive infinity direction, the end point of the
- * last segment will have infinite coordinates. So a sub-line covering the whole
- * line will contain just one row and both elements of this row will have infinite
- * coordinates. If the sub-line is empty, the returned list will contain 0 segments.
- * </p>
- * @return list of segments endpoints
- */
- public List<Segment> getSegments() {
+ /** {@inheritDoc} */
+ @Override
+ public SubLine transform(final Transform<Vector2D> transform) {
+ final SubspaceTransform st = getLine().subspaceTransform(transform);
- final Line line = (Line) getHyperplane();
- final List<Interval> list = ((IntervalsSet) getRemainingRegion()).asList();
- final List<Segment> segments = new ArrayList<>(list.size());
+ final RegionBSPTree1D tRegion = RegionBSPTree1D.empty();
+ tRegion.copy(region);
+ tRegion.transform(st.getTransform());
- for (final Interval interval : list) {
- final Vector2D start = line.toSpace(Vector1D.of(interval.getInf()));
- final Vector2D end = line.toSpace(Vector1D.of(interval.getSup()));
- segments.add(new Segment(start, end, line));
+ return new SubLine(st.getLine(), tRegion);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<Segment> toConvex() {
+ final List<Interval> intervals = region.toIntervals();
+
+ final Line line = getLine();
+ final List<Segment> segments = new ArrayList<>(intervals.size());
+
+ for (final Interval interval : intervals) {
+ segments.add(Segment.fromInterval(line, interval));
}
return segments;
-
- }
-
- /** Get the intersection of the instance and another sub-line.
- * <p>
- * This method is related to the {@link Line#intersection(Line)
- * intersection} method in the {@link Line Line} class, but in addition
- * to compute the point along infinite lines, it also checks the point
- * lies on both sub-line ranges.
- * </p>
- * @param subLine other sub-line which may intersect instance
- * @param includeEndPoints if true, endpoints are considered to belong to
- * instance (i.e. they are closed sets) and may be returned, otherwise endpoints
- * are considered to not belong to instance (i.e. they are open sets) and intersection
- * occurring on endpoints lead to null being returned
- * @return the intersection point if there is one, null if the sub-lines don't intersect
- */
- public Vector2D intersection(final SubLine subLine, final boolean includeEndPoints) {
-
- // retrieve the underlying lines
- Line line1 = (Line) getHyperplane();
- Line line2 = (Line) subLine.getHyperplane();
-
- // compute the intersection on infinite line
- Vector2D v2D = line1.intersection(line2);
- if (v2D == null) {
- return null;
- }
-
- // check location of point with respect to first sub-line
- Location loc1 = getRemainingRegion().checkPoint(line1.toSubSpace(v2D));
-
- // check location of point with respect to second sub-line
- Location loc2 = subLine.getRemainingRegion().checkPoint(line2.toSubSpace(v2D));
-
- if (includeEndPoints) {
- return ((loc1 != Location.OUTSIDE) && (loc2 != Location.OUTSIDE)) ? v2D : null;
- } else {
- return ((loc1 == Location.INSIDE) && (loc2 == Location.INSIDE)) ? v2D : null;
- }
-
- }
-
- /** Build an interval set from two points.
- * @param start start point
- * @param end end point
- * @param precision precision context used to compare floating point values
- * @return an interval set
- */
- private static IntervalsSet buildIntervalSet(final Vector2D start, final Vector2D end, final DoublePrecisionContext precision) {
- final Line line = Line.fromPoints(start, end, precision);
- return new IntervalsSet(line.toSubSpace(start).getX(),
- line.toSubSpace(end).getX(),
- precision);
}
/** {@inheritDoc} */
@Override
- protected AbstractSubHyperplane<Vector2D, Vector1D> buildNew(final Hyperplane<Vector2D> hyperplane,
- final Region<Vector1D> remainingRegion) {
- return new SubLine(hyperplane, remainingRegion);
+ public RegionBSPTree1D getSubspaceRegion() {
+ return region;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>In all cases, the current instance is not modified. However, In order to avoid
+ * unnecessary copying, this method will use the current instance as the split value when
+ * the instance lies entirely on the plus or minus side of the splitter. For example, if
+ * this instance lies entirely on the minus side of the splitter, the subplane
+ * returned by {@link Split#getMinus()} will be this instance. Similarly, {@link Split#getPlus()}
+ * will return the current instance if it lies entirely on the plus side. Callers need to make
+ * special note of this, since this class is mutable.</p>
+ */
+ @Override
+ public Split<SubLine> split(final Hyperplane<Vector2D> splitter) {
+ return splitInternal(splitter, this, (line, reg) -> new SubLine(line, (RegionBSPTree1D) reg));
+ }
+
+ /** Add a line segment to this instance..
+ * @param segment line segment to add
+ * @throws GeometryException if the given line segment is not from
+ * a line equivalent to this instance
+ */
+ public void add(final Segment segment) {
+ validateLine(segment.getLine());
+
+ region.add(segment.getSubspaceRegion());
+ }
+
+ /** Add the region represented by the given subline to this instance.
+ * The argument is not modified.
+ * @param subline subline to add
+ * @throws GeometryException if the given subline is not from
+ * a line equivalent to this instance
+ */
+ public void add(final SubLine subline) {
+ validateLine(subline.getLine());
+
+ region.union(subline.getSubspaceRegion());
}
/** {@inheritDoc} */
@Override
- public SplitSubHyperplane<Vector2D> split(final Hyperplane<Vector2D> hyperplane) {
+ public String toString() {
+ final Line line = getLine();
- final Line thisLine = (Line) getHyperplane();
- final Line otherLine = (Line) hyperplane;
- final Vector2D crossing = thisLine.intersection(otherLine);
- final DoublePrecisionContext precision = thisLine.getPrecision();
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append('[')
+ .append("lineOrigin= ")
+ .append(line.getOrigin())
+ .append(", lineDirection= ")
+ .append(line.getDirection())
+ .append(", region= ")
+ .append(region)
+ .append(']');
- if (crossing == null) {
- // the lines are parallel
- final double global = otherLine.getOffset(thisLine);
- final int comparison = precision.compare(global, 0.0);
+ return sb.toString();
+ }
- if (comparison < 0) {
- return new SplitSubHyperplane<>(null, this);
- } else if (comparison > 0) {
- return new SplitSubHyperplane<>(this, null);
+ /** Validate that the given line is equivalent to the line
+ * defining this subline.
+ * @param inputLine the line to validate
+ * @throws GeometryException if the given line is not equivalent
+ * to the line for this instance
+ */
+ private void validateLine(final Line inputLine) {
+ final Line line = getLine();
+
+ if (!line.eq(inputLine)) {
+ throw new GeometryException("Argument is not on the same " +
+ "line. Expected " + line + " but was " +
+ inputLine);
+ }
+ }
+
+ /** {@link Builder} implementation for sublines.
+ */
+ public static final class SubLineBuilder implements SubHyperplane.Builder<Vector2D> {
+
+ /** SubLine instance created by this builder. */
+ private final SubLine subline;
+
+ /** Construct a new instance for building subline region for the given line.
+ * @param line the underlying line for the subline region
+ */
+ public SubLineBuilder(final Line line) {
+ this.subline = new SubLine(line);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final SubHyperplane<Vector2D> sub) {
+ addInternal(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final ConvexSubHyperplane<Vector2D> sub) {
+ addInternal(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubLine build() {
+ return subline;
+ }
+
+ /** Internal method for adding subhyperplanes to this builder.
+ * @param sub the subhyperplane to add; either convex or non-convex
+ */
+ private void addInternal(final SubHyperplane<Vector2D> sub) {
+ if (sub instanceof Segment) {
+ subline.add((Segment) sub);
+ } else if (sub instanceof SubLine) {
+ subline.add((SubLine) sub);
} else {
- return new SplitSubHyperplane<>(null, null);
+ throw new IllegalArgumentException("Unsupported subhyperplane type: " + sub.getClass().getName());
}
}
-
- // the lines do intersect
- final boolean direct = Math.sin(thisLine.getAngle() - otherLine.getAngle()) < 0;
- final Vector1D x = thisLine.toSubSpace(crossing);
- final SubHyperplane<Vector1D> subPlus =
- OrientedPoint.fromPointAndDirection(x, !direct, precision).wholeHyperplane();
- final SubHyperplane<Vector1D> subMinus =
- OrientedPoint.fromPointAndDirection(x, direct, precision).wholeHyperplane();
-
- final BSPTree<Vector1D> splitTree = getRemainingRegion().getTree(false).split(subMinus);
- final BSPTree<Vector1D> plusTree = getRemainingRegion().isEmpty(splitTree.getPlus()) ?
- new BSPTree<Vector1D>(Boolean.FALSE) :
- new BSPTree<>(subPlus, new BSPTree<Vector1D>(Boolean.FALSE),
- splitTree.getPlus(), null);
- final BSPTree<Vector1D> minusTree = getRemainingRegion().isEmpty(splitTree.getMinus()) ?
- new BSPTree<Vector1D>(Boolean.FALSE) :
- new BSPTree<>(subMinus, new BSPTree<Vector1D>(Boolean.FALSE),
- splitTree.getMinus(), null);
- return new SplitSubHyperplane<>(new SubLine(thisLine.copySelf(), new IntervalsSet(plusTree, precision)),
- new SubLine(thisLine.copySelf(), new IntervalsSet(minusTree, precision)));
-
}
-
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Transform2D.java
similarity index 61%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Transform2D.java
index 046defe..94f8a4e 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Transform2D.java
@@ -14,23 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partitioning;
+package org.apache.commons.geometry.euclidean.twod;
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+import org.apache.commons.geometry.euclidean.EuclideanTransform;
+
+/** Extension of the {@link EuclideanTransform} interface for 2D space.
*/
-public enum Side {
+public interface Transform2D extends EuclideanTransform<Vector2D> {
- /** Code for the plus side of the hyperplane. */
- PLUS,
-
- /** Code for the minus side of the hyperplane. */
- MINUS,
-
- /** Code for elements crossing the hyperplane from plus to minus side. */
- BOTH,
-
- /** Code for the hyperplane itself. */
- HYPER;
-
+ /** Return an affine transform matrix representing the same transform
+ * as this instance.
+ * @return an affine tranform matrix representing the same transform
+ * as this instance
+ */
+ AffineTransformMatrix2D toMatrix();
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
index d6ba2de..15ab254 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
@@ -16,7 +16,9 @@
*/
package org.apache.commons.geometry.euclidean.twod;
-import org.apache.commons.geometry.core.exception.IllegalNormException;
+import java.util.Comparator;
+import java.util.function.Function;
+
import org.apache.commons.geometry.core.internal.DoubleFunction2N;
import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
@@ -45,20 +47,41 @@
public static final Vector2D NEGATIVE_INFINITY =
new Vector2D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
- /** Serializable UID */
+ /** Comparator that sorts vectors in component-wise ascending order.
+ * Vectors are only considered equal if their coordinates match exactly.
+ * Null arguments are evaluated as being greater than non-null arguments.
+ */
+ public static final Comparator<Vector2D> COORDINATE_ASCENDING_ORDER = (a, b) -> {
+ int cmp = 0;
+
+ if (a != null && b != null) {
+ cmp = Double.compare(a.getX(), b.getX());
+ if (cmp == 0) {
+ cmp = Double.compare(a.getY(), b.getY());
+ }
+ } else if (a != null) {
+ cmp = -1;
+ } else if (b != null) {
+ cmp = 1;
+ }
+
+ return cmp;
+ };
+
+ /** Serializable UID. */
private static final long serialVersionUID = 20180710L;
- /** Abscissa (first coordinate) */
+ /** Abscissa (first coordinate). */
private final double x;
- /** Ordinate (second coordinate) */
+ /** Ordinate (second coordinate). */
private final double y;
/** Simple constructor.
* @param x abscissa (first coordinate)
* @param y ordinate (second coordinate)
*/
- private Vector2D(double x, double y) {
+ private Vector2D(final double x, final double y) {
this.x = x;
this.y = y;
}
@@ -81,7 +104,7 @@
* @return coordinates for this instance
*/
public double[] toArray() {
- return new double[] { x, y };
+ return new double[]{x, y};
}
/** {@inheritDoc} */
@@ -104,19 +127,25 @@
/** {@inheritDoc} */
@Override
- public Vector2D vectorTo(Vector2D v) {
+ public boolean isFinite() {
+ return Double.isFinite(x) && Double.isFinite(y);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D vectorTo(final Vector2D v) {
return v.subtract(this);
}
/** {@inheritDoc} */
@Override
- public Unit directionTo(Vector2D v) {
+ public Unit directionTo(final Vector2D v) {
return vectorTo(v).normalize();
}
/** {@inheritDoc} */
@Override
- public Vector2D lerp(Vector2D p, double t) {
+ public Vector2D lerp(final Vector2D p, final double t) {
return linearCombination(1.0 - t, this, t, p);
}
@@ -140,7 +169,7 @@
/** {@inheritDoc} */
@Override
- public Vector2D withNorm(double magnitude) {
+ public Vector2D withNorm(final double magnitude) {
final double invNorm = 1.0 / getCheckedNorm();
return new Vector2D(
@@ -151,25 +180,25 @@
/** {@inheritDoc} */
@Override
- public Vector2D add(Vector2D v) {
+ public Vector2D add(final Vector2D v) {
return new Vector2D(x + v.x, y + v.y);
}
/** {@inheritDoc} */
@Override
- public Vector2D add(double factor, Vector2D v) {
+ public Vector2D add(final double factor, final Vector2D v) {
return new Vector2D(x + (factor * v.x), y + (factor * v.y));
}
/** {@inheritDoc} */
@Override
- public Vector2D subtract(Vector2D v) {
+ public Vector2D subtract(final Vector2D v) {
return new Vector2D(x - v.x, y - v.y);
}
/** {@inheritDoc} */
@Override
- public Vector2D subtract(double factor, Vector2D v) {
+ public Vector2D subtract(final double factor, final Vector2D v) {
return new Vector2D(x - (factor * v.x), y - (factor * v.y));
}
@@ -187,25 +216,25 @@
/** {@inheritDoc} */
@Override
- public Vector2D multiply(double a) {
+ public Vector2D multiply(final double a) {
return new Vector2D(a * x, a * y);
}
/** {@inheritDoc} */
@Override
- public double distance(Vector2D v) {
+ public double distance(final Vector2D v) {
return Vectors.norm(x - v.x, y - v.y);
}
/** {@inheritDoc} */
@Override
- public double distanceSq(Vector2D v) {
+ public double distanceSq(final Vector2D v) {
return Vectors.normSq(x - v.x, y - v.y);
}
/** {@inheritDoc} */
@Override
- public double dot(Vector2D v) {
+ public double dot(final Vector2D v) {
return LinearCombination.value(x, v.x, y, v.y);
}
@@ -217,7 +246,7 @@
* other.</p>
*/
@Override
- public double angle(Vector2D v) {
+ public double angle(final Vector2D v) {
double normProduct = getCheckedNorm() * v.getCheckedNorm();
double dot = dot(v);
@@ -237,13 +266,13 @@
/** {@inheritDoc} */
@Override
- public Vector2D project(Vector2D base) {
+ public Vector2D project(final Vector2D base) {
return getComponent(base, false, Vector2D::new);
}
/** {@inheritDoc} */
@Override
- public Vector2D reject(Vector2D base) {
+ public Vector2D reject(final Vector2D base) {
return getComponent(base, true, Vector2D::new);
}
@@ -253,17 +282,17 @@
* called on a vector pointing along the positive x-axis, then a unit vector representing
* the positive y-axis is returned.
* @return a unit vector orthogonal to the current instance
- * @throws IllegalNormException if the norm of the current instance is zero, NaN,
- * or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the current instance
+ * is zero, NaN, or infinite
*/
@Override
- public Vector2D orthogonal() {
+ public Vector2D.Unit orthogonal() {
return Unit.from(-y, x);
}
/** {@inheritDoc} */
@Override
- public Vector2D orthogonal(Vector2D dir) {
+ public Vector2D.Unit orthogonal(final Vector2D dir) {
return dir.getComponent(this, true, Vector2D.Unit::from);
}
@@ -287,19 +316,18 @@
-y, v.x);
}
- /** Apply the given transform to this vector, returning the result as a
- * new vector instance.
- * @param transform the transform to apply
- * @return a new, transformed vector
- * @see AffineTransformMatrix2D#apply(Vector2D)
+ /** Convenience method to apply a function to this vector. This
+ * can be used to transform the vector inline with other methods.
+ * @param fn the function to apply
+ * @return the transformed vector
*/
- public Vector2D transform(AffineTransformMatrix2D transform) {
- return transform.apply(this);
+ public Vector2D transform(final Function<Vector2D, Vector2D> fn) {
+ return fn.apply(this);
}
/** {@inheritDoc} */
@Override
- public boolean equals(final Vector2D vec, final DoublePrecisionContext precision) {
+ public boolean eq(final Vector2D vec, final DoublePrecisionContext precision) {
return precision.eq(x, vec.x) &&
precision.eq(y, vec.y);
}
@@ -339,7 +367,7 @@
*
*/
@Override
- public boolean equals(Object other) {
+ public boolean equals(final Object other) {
if (this == other) {
return true;
}
@@ -369,11 +397,14 @@
* returned. If false, the projection of this instance onto {@code base}
* is returned.
* @param factory factory function used to build the final vector
+ * @param <T> Vector implementation type
* @return The projection or rejection of this instance relative to {@code base},
* depending on the value of {@code reject}.
- * @throws IllegalNormException if {@code base} has a zero, NaN, or infinite norm
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if {@code base} has a
+ * zero, NaN, or infinite norm
*/
- private Vector2D getComponent(Vector2D base, boolean reject, DoubleFunction2N<Vector2D> factory) {
+ private <T extends Vector2D> T getComponent(final Vector2D base, final boolean reject,
+ final DoubleFunction2N<T> factory) {
final double aDotB = dot(base);
// We need to check the norm value here to ensure that it's legal. However, we don't
@@ -401,7 +432,7 @@
* @param y abscissa (second coordinate value)
* @return vector instance
*/
- public static Vector2D of(double x, double y) {
+ public static Vector2D of(final double x, final double y) {
return new Vector2D(x, y);
}
@@ -410,7 +441,7 @@
* @return new vector
* @exception IllegalArgumentException if the array does not have 2 elements
*/
- public static Vector2D of(double[] v) {
+ public static Vector2D of(final double[] v) {
if (v.length != 2) {
throw new IllegalArgumentException("Dimension mismatch: " + v.length + " != 2");
}
@@ -423,7 +454,7 @@
* @return vector instance represented by the string
* @throws IllegalArgumentException if the given string has an invalid format
*/
- public static Vector2D parse(String str) {
+ public static Vector2D parse(final String str) {
return SimpleTupleFormat.getDefault().parse(str, Vector2D::new);
}
@@ -437,7 +468,7 @@
* @param c first coordinate
* @return vector with coordinates calculated by {@code a * c}
*/
- public static Vector2D linearCombination(double a, Vector2D c) {
+ public static Vector2D linearCombination(final double a, final Vector2D c) {
return new Vector2D(a * c.x, a * c.y);
}
@@ -453,7 +484,8 @@
* @param v2 second coordinate
* @return vector with coordinates calculated by {@code (a1 * v1) + (a2 * v2)}
*/
- public static Vector2D linearCombination(double a1, Vector2D v1, double a2, Vector2D v2) {
+ public static Vector2D linearCombination(final double a1, final Vector2D v1,
+ final double a2, final Vector2D v2) {
return new Vector2D(
LinearCombination.value(a1, v1.x, a2, v2.x),
LinearCombination.value(a1, v1.y, a2, v2.y));
@@ -473,8 +505,9 @@
* @param v3 third coordinate
* @return vector with coordinates calculated by {@code (a1 * v1) + (a2 * v2) + (a3 * v3)}
*/
- public static Vector2D linearCombination(double a1, Vector2D v1, double a2, Vector2D v2,
- double a3, Vector2D v3) {
+ public static Vector2D linearCombination(final double a1, final Vector2D v1,
+ final double a2, final Vector2D v2,
+ final double a3, final Vector2D v3) {
return new Vector2D(
LinearCombination.value(a1, v1.x, a2, v2.x, a3, v3.x),
LinearCombination.value(a1, v1.y, a2, v2.y, a3, v3.y));
@@ -496,8 +529,10 @@
* @param v4 fourth coordinate
* @return point with coordinates calculated by {@code (a1 * v1) + (a2 * v2) + (a3 * v3) + (a4 * v4)}
*/
- public static Vector2D linearCombination(double a1, Vector2D v1, double a2, Vector2D v2,
- double a3, Vector2D v3, double a4, Vector2D v4) {
+ public static Vector2D linearCombination(final double a1, Vector2D v1,
+ final double a2, final Vector2D v2,
+ final double a3, final Vector2D v3,
+ final double a4, final Vector2D v4) {
return new Vector2D(
LinearCombination.value(a1, v1.x, a2, v2.x, a3, v3.x, a4, v4.x),
LinearCombination.value(a1, v1.y, a2, v2.y, a3, v3.y, a4, v4.y));
@@ -517,7 +552,7 @@
/** Negation of unit vector (coordinates: 0, -1). */
public static final Unit MINUS_Y = new Unit(0d, -1d);
- /** Serializable version identifier */
+ /** Serializable version identifier. */
private static final long serialVersionUID = 20180903L;
/** Simple constructor. Callers are responsible for ensuring that the given
@@ -535,9 +570,10 @@
* @param x Vector coordinate.
* @param y Vector coordinate.
* @return a vector whose norm is 1.
- * @throws IllegalNormException if the norm of the given value is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given value
+ * is zero, NaN, or infinite
*/
- public static Unit from(double x, double y) {
+ public static Unit from(final double x, final double y) {
final double invNorm = 1 / Vectors.checkedNorm(Vectors.norm(x, y));
return new Unit(x * invNorm, y * invNorm);
}
@@ -547,9 +583,10 @@
*
* @param v Vector.
* @return a vector whose norm is 1.
- * @throws IllegalNormException if the norm of the given value is zero, NaN, or infinite
+ * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm of the given
+ * value is zero, NaN, or infinite
*/
- public static Unit from(Vector2D v) {
+ public static Unit from(final Vector2D v) {
return v instanceof Unit ?
(Unit) v :
from(v.getX(), v.getY());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java
deleted file mode 100644
index f172467..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java
+++ /dev/null
@@ -1,427 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.Iterator;
-
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.twod.Line;
-import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
-import org.apache.commons.geometry.euclidean.twod.SubLine;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
-import org.junit.Assert;
-import org.junit.Test;
-
-/** Tests for partitioning characterization. This is designed to test code
- * in commons-geometry-core but is placed here to allow access to the euclidean
- * spatial primitives.
- */
-public class CharacterizationTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION = new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testCharacterize_insideLeaf() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- SubLine sub = buildSubLine(Vector2D.of(0, -1), Vector2D.of(0, 1));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertSame(sub, ch.insideTouching());
- Assert.assertEquals(0, size(ch.getInsideSplitters()));
-
- Assert.assertEquals(false, ch.touchOutside());
- Assert.assertEquals(null, ch.outsideTouching());
- Assert.assertEquals(0, size(ch.getOutsideSplitters()));
- }
-
- @Test
- public void testCharacterize_outsideLeaf() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.FALSE);
- SubLine sub = buildSubLine(Vector2D.of(0, -1), Vector2D.of(0, 1));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(false, ch.touchInside());
- Assert.assertSame(null, ch.insideTouching());
- Assert.assertEquals(0, size(ch.getInsideSplitters()));
-
- Assert.assertEquals(true, ch.touchOutside());
- Assert.assertEquals(sub, ch.outsideTouching());
- Assert.assertEquals(0, size(ch.getOutsideSplitters()));
- }
-
- @Test
- public void testCharacterize_onPlusSide() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
-
- SubLine sub = buildSubLine(Vector2D.of(0, -1), Vector2D.of(0, -2));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(false, ch.touchInside());
- Assert.assertSame(null, ch.insideTouching());
- Assert.assertEquals(0, size(ch.getInsideSplitters()));
-
- Assert.assertEquals(true, ch.touchOutside());
- Assert.assertEquals(sub, ch.outsideTouching());
- Assert.assertEquals(0, size(ch.getOutsideSplitters()));
- }
-
- @Test
- public void testCharacterize_onMinusSide() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
-
- SubLine sub = buildSubLine(Vector2D.of(0, 1), Vector2D.of(0, 2));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertSame(sub, ch.insideTouching());
- Assert.assertEquals(0, size(ch.getInsideSplitters()));
-
- Assert.assertEquals(false, ch.touchOutside());
- Assert.assertEquals(null, ch.outsideTouching());
- Assert.assertEquals(0, size(ch.getOutsideSplitters()));
- }
-
- @Test
- public void testCharacterize_onBothSides() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
-
- SubLine sub = buildSubLine(Vector2D.of(0, -1), Vector2D.of(0, 1));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine inside = (SubLine) ch.insideTouching();
- Assert.assertEquals(1, inside.getSegments().size());
- assertVectorEquals(Vector2D.of(0, 0), inside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(0, 1), inside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(1, size(ch.getInsideSplitters()));
- Iterator<BSPTree<Vector2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
- Assert.assertSame(tree, insideSplitterIter.next());
-
- Assert.assertEquals(true, ch.touchOutside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine outside = (SubLine) ch.outsideTouching();
- Assert.assertEquals(1, outside.getSegments().size());
- assertVectorEquals(Vector2D.of(0, -1), outside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(0, 0), outside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(1, size(ch.getOutsideSplitters()));
- Iterator<BSPTree<Vector2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
- Assert.assertSame(tree, outsideSplitterIter.next());
- }
-
- @Test
- public void testCharacterize_multipleSplits_reunitedOnPlusSide() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
- cut(tree.getMinus(), buildLine(Vector2D.of(-1, 0), Vector2D.of(0, 1)));
-
- SubLine sub = buildSubLine(Vector2D.of(0, -2), Vector2D.of(0, 2));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine inside = (SubLine) ch.insideTouching();
- Assert.assertEquals(1, inside.getSegments().size());
- assertVectorEquals(Vector2D.of(0, 1), inside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(0, 2), inside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(2, size(ch.getInsideSplitters()));
- Iterator<BSPTree<Vector2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
- Assert.assertSame(tree, insideSplitterIter.next());
- Assert.assertSame(tree.getMinus(), insideSplitterIter.next());
-
- Assert.assertEquals(true, ch.touchOutside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine outside = (SubLine) ch.outsideTouching();
- Assert.assertEquals(1, outside.getSegments().size());
- assertVectorEquals(Vector2D.of(0, -2), outside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(0, 1), outside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(2, size(ch.getOutsideSplitters()));
- Iterator<BSPTree<Vector2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
- Assert.assertSame(tree, outsideSplitterIter.next());
- Assert.assertSame(tree.getMinus(), outsideSplitterIter.next());
- }
-
- @Test
- public void testCharacterize_multipleSplits_reunitedOnMinusSide() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
- cut(tree.getMinus(), buildLine(Vector2D.of(-1, 0), Vector2D.of(0, 1)));
- cut(tree.getMinus().getPlus(), buildLine(Vector2D.of(-0.5, 0.5), Vector2D.of(0, 0)));
-
- SubLine sub = buildSubLine(Vector2D.of(0, -2), Vector2D.of(0, 2));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine inside = (SubLine) ch.insideTouching();
- Assert.assertEquals(1, inside.getSegments().size());
- assertVectorEquals(Vector2D.of(0, 0), inside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(0, 2), inside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(2, size(ch.getInsideSplitters()));
- Iterator<BSPTree<Vector2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
- Assert.assertSame(tree, insideSplitterIter.next());
- Assert.assertSame(tree.getMinus(), insideSplitterIter.next());
-
- Assert.assertEquals(true, ch.touchOutside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine outside = (SubLine) ch.outsideTouching();
- Assert.assertEquals(1, outside.getSegments().size());
- assertVectorEquals(Vector2D.of(0, -2), outside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(0, 0), outside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(1, size(ch.getOutsideSplitters()));
- Iterator<BSPTree<Vector2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
- Assert.assertSame(tree, outsideSplitterIter.next());
- }
-
- @Test
- public void testCharacterize_onHyperplane_sameOrientation() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
-
- SubLine sub = buildSubLine(Vector2D.of(0, 0), Vector2D.of(1, 0));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertSame(sub, ch.insideTouching());
- Assert.assertEquals(0, size(ch.getInsideSplitters()));
-
- Assert.assertEquals(false, ch.touchOutside());
- Assert.assertEquals(null, ch.outsideTouching());
- Assert.assertEquals(0, size(ch.getOutsideSplitters()));
- }
-
- @Test
- public void testCharacterize_onHyperplane_oppositeOrientation() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
-
- SubLine sub = buildSubLine(Vector2D.of(1, 0), Vector2D.of(0, 0));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertSame(sub, ch.insideTouching());
- Assert.assertEquals(0, size(ch.getInsideSplitters()));
-
- Assert.assertEquals(false, ch.touchOutside());
- Assert.assertEquals(null, ch.outsideTouching());
- Assert.assertEquals(0, size(ch.getOutsideSplitters()));
- }
-
- @Test
- public void testCharacterize_onHyperplane_multipleSplits_sameOrientation() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
- cut(tree.getMinus(), buildLine(Vector2D.of(-1, 0), Vector2D.of(0, 1)));
-
- SubLine sub = buildSubLine(Vector2D.of(-2, 0), Vector2D.of(2, 0));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine inside = (SubLine) ch.insideTouching();
- Assert.assertEquals(1, inside.getSegments().size());
- assertVectorEquals(Vector2D.of(-2, 0), inside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(-1, 0), inside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(1, size(ch.getInsideSplitters()));
- Iterator<BSPTree<Vector2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
- Assert.assertSame(tree.getMinus(), insideSplitterIter.next());
-
- Assert.assertEquals(true, ch.touchOutside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine outside = (SubLine) ch.outsideTouching();
- Assert.assertEquals(1, outside.getSegments().size());
- assertVectorEquals(Vector2D.of(-1, 0), outside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(2, 0), outside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(1, size(ch.getOutsideSplitters()));
- Iterator<BSPTree<Vector2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
- Assert.assertSame(tree.getMinus(), outsideSplitterIter.next());
- }
-
- @Test
- public void testCharacterize_onHyperplane_multipleSplits_oppositeOrientation() {
- // arrange
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- cut(tree, buildLine(Vector2D.of(0, 0), Vector2D.of(1, 0)));
- cut(tree.getMinus(), buildLine(Vector2D.of(-1, 0), Vector2D.of(0, 1)));
-
- SubLine sub = buildSubLine(Vector2D.of(2, 0), Vector2D.of(-2, 0));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine inside = (SubLine) ch.insideTouching();
- Assert.assertEquals(1, inside.getSegments().size());
- assertVectorEquals(Vector2D.of(-1, 0), inside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(-2, 0), inside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(1, size(ch.getInsideSplitters()));
- Iterator<BSPTree<Vector2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
- Assert.assertSame(tree.getMinus(), insideSplitterIter.next());
-
- Assert.assertEquals(true, ch.touchOutside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine outside = (SubLine) ch.outsideTouching();
- Assert.assertEquals(1, outside.getSegments().size());
- assertVectorEquals(Vector2D.of(2, 0), outside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(-1, 0), outside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(1, size(ch.getOutsideSplitters()));
- Iterator<BSPTree<Vector2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
- Assert.assertSame(tree.getMinus(), outsideSplitterIter.next());
- }
-
- @Test
- public void testCharacterize_onHyperplane_box() {
- // arrange
- PolygonsSet poly = new PolygonsSet(0, 1, 0, 1, TEST_PRECISION);
- BSPTree<Vector2D> tree = poly.getTree(false);
-
- SubLine sub = buildSubLine(Vector2D.of(2, 0), Vector2D.of(-2, 0));
-
- // act
- Characterization<Vector2D> ch = new Characterization<>(tree, sub);
-
- // assert
- Assert.assertEquals(true, ch.touchInside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine inside = (SubLine) ch.insideTouching();
- Assert.assertEquals(1, inside.getSegments().size());
- assertVectorEquals(Vector2D.of(1, 0), inside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(0, 0), inside.getSegments().get(0).getEnd());
-
- Assert.assertEquals(2, size(ch.getInsideSplitters()));
-
- Assert.assertEquals(true, ch.touchOutside());
- Assert.assertNotSame(sub, ch.insideTouching());
-
- SubLine outside = (SubLine) ch.outsideTouching();
- Assert.assertEquals(2, outside.getSegments().size());
- assertVectorEquals(Vector2D.of(2, 0), outside.getSegments().get(0).getStart());
- assertVectorEquals(Vector2D.of(1, 0), outside.getSegments().get(0).getEnd());
- assertVectorEquals(Vector2D.of(0, 0), outside.getSegments().get(1).getStart());
- assertVectorEquals(Vector2D.of(-2, 0), outside.getSegments().get(1).getEnd());
-
- Assert.assertEquals(2, size(ch.getOutsideSplitters()));
- }
-
- private void cut(BSPTree<Vector2D> tree, Line line) {
- if (tree.insertCut(line)) {
- tree.setAttribute(null);
- tree.getPlus().setAttribute(Boolean.FALSE);
- tree.getMinus().setAttribute(Boolean.TRUE);
- }
- }
-
- private int size(NodesSet<Vector2D> nodes) {
- Iterator<BSPTree<Vector2D>> it = nodes.iterator();
-
- int size = 0;
- while (it.hasNext()) {
- it.next();
- ++size;
- }
-
- return size;
- }
-
- private Line buildLine(Vector2D p1, Vector2D p2) {
- return Line.fromPoints(p1, p2, TEST_PRECISION);
- }
-
- private SubLine buildSubLine(Vector2D start, Vector2D end) {
- Line line = Line.fromPoints(start, end, TEST_PRECISION);
- double lower = (line.toSubSpace(start)).getX();
- double upper = (line.toSubSpace(end)).getX();
- return new SubLine(line, new IntervalsSet(lower, upper, TEST_PRECISION));
- }
-
- private void assertVectorEquals(Vector2D expected, Vector2D actual) {
- String msg = "Expected vector to equal " + expected + " but was " + actual + ";";
- Assert.assertEquals(msg, expected.getX(), actual.getX(), TEST_EPS);
- Assert.assertEquals(msg, expected.getY(), actual.getY(), TEST_EPS);
- }
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
index 5b7b0db..f74aaf8 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
@@ -16,28 +16,10 @@
*/
package org.apache.commons.geometry.euclidean;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.TreeBuilder;
-import org.apache.commons.geometry.core.partitioning.TreeDumper;
-import org.apache.commons.geometry.core.partitioning.TreePrinter;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
-import org.apache.commons.geometry.euclidean.oned.SubOrientedPoint;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
-import org.apache.commons.geometry.euclidean.threed.Plane;
-import org.apache.commons.geometry.euclidean.threed.PolyhedronsSet;
-import org.apache.commons.geometry.euclidean.threed.SubPlane;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.euclidean.twod.Line;
-import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
-import org.apache.commons.geometry.euclidean.twod.SubLine;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.junit.Assert;
@@ -210,283 +192,39 @@
Assert.assertTrue(msg, value < 0);
}
- /**
- * Get a string representation of an {@link IntervalsSet}.
- *
- * @param intervalsSet region to dump
- * @return string representation of the region
+ /** Assert that all of the given points lie within the specified location relative to
+ * {@code region}.
+ * @param region
+ * @param loc
+ * @param pts
*/
- public static String dump(final IntervalsSet intervalsSet) {
- final TreeDumper<Vector1D> visitor = new TreeDumper<Vector1D>("IntervalsSet") {
-
- /** {@inheritDoc} */
- @Override
- protected void formatHyperplane(final Hyperplane<Vector1D> hyperplane) {
- final OrientedPoint h = (OrientedPoint) hyperplane;
- getFormatter().format("%22.15e %b", h.getLocation().getX(), h.isPositiveFacing());
- }
-
- };
- intervalsSet.getTree(false).visit(visitor);
- return visitor.getDump();
- }
-
- /**
- * Get a string representation of a {@link PolygonsSet}.
- *
- * @param polygonsSet region to dump
- * @return string representation of the region
- */
- public static String dump(final PolygonsSet polygonsSet) {
- final TreeDumper<Vector2D> visitor = new TreeDumper<Vector2D>("PolygonsSet") {
-
- /** {@inheritDoc} */
- @Override
- protected void formatHyperplane(final Hyperplane<Vector2D> hyperplane) {
- final Line h = (Line) hyperplane;
- final Vector2D p = h.toSpace(Vector1D.ZERO);
- getFormatter().format("%22.15e %22.15e %22.15e",
- p.getX(), p.getY(), h.getAngle());
- }
-
- };
- polygonsSet.getTree(false).visit(visitor);
- return visitor.getDump();
- }
-
- /**
- * Get a string representation of a {@link PolyhedronsSet}.
- *
- * @param polyhedronsSet region to dump
- * @return string representation of the region
- */
- public static String dump(final PolyhedronsSet polyhedronsSet) {
- final TreeDumper<Vector3D> visitor = new TreeDumper<Vector3D>("PolyhedronsSet") {
-
- /** {@inheritDoc} */
- @Override
- protected void formatHyperplane(final Hyperplane<Vector3D> hyperplane) {
- final Plane h = (Plane) hyperplane;
- final Vector3D p = h.toSpace(Vector2D.ZERO);
- getFormatter().format("%22.15e %22.15e %22.15e %22.15e %22.15e %22.15e",
- p.getX(), p.getY(), p.getZ(),
- h.getNormal().getX(), h.getNormal().getY(), h.getNormal().getZ());
- }
-
- };
- polyhedronsSet.getTree(false).visit(visitor);
- return visitor.getDump();
- }
-
- /**
- * Parse a string representation of an {@link IntervalsSet}.
- *
- * @param str string to parse
- * @param precision precision context to use for the region
- * @return parsed region
- * @exception ParseException if the string cannot be parsed
- */
- public static IntervalsSet parseIntervalsSet(final String str, final DoublePrecisionContext precision)
- throws ParseException {
- final TreeBuilder<Vector1D> builder = new TreeBuilder<Vector1D>("IntervalsSet", str, precision) {
-
- /** {@inheritDoc} */
- @Override
- public OrientedPoint parseHyperplane()
- throws ParseException {
- return OrientedPoint.fromPointAndDirection(Vector1D.of(getNumber()), getBoolean(), getPrecision());
- }
-
- };
- return new IntervalsSet(builder.getTree(), builder.getPrecision());
- }
-
- /**
- * Parse a string representation of a {@link PolygonsSet}.
- *
- * @param str string to parse
- * @param precision precision context to use for the region
- * @return parsed region
- * @exception ParseException if the string cannot be parsed
- */
- public static PolygonsSet parsePolygonsSet(final String str, final DoublePrecisionContext precision)
- throws ParseException {
- final TreeBuilder<Vector2D> builder = new TreeBuilder<Vector2D>("PolygonsSet", str, precision) {
-
- /** {@inheritDoc} */
- @Override
- public Line parseHyperplane()
- throws ParseException {
- return Line.fromPointAndAngle(Vector2D.of(getNumber(), getNumber()), getNumber(), getPrecision());
- }
-
- };
- return new PolygonsSet(builder.getTree(), builder.getPrecision());
- }
-
- /**
- * Parse a string representation of a {@link PolyhedronsSet}.
- *
- * @param str string to parse
- * @param precision precision context to use for the region
- * @return parsed region
- * @exception ParseException if the string cannot be parsed
- */
- public static PolyhedronsSet parsePolyhedronsSet(final String str, final DoublePrecisionContext precision)
- throws ParseException {
- final TreeBuilder<Vector3D> builder = new TreeBuilder<Vector3D>("PolyhedronsSet", str, precision) {
-
- /** {@inheritDoc} */
- @Override
- public Plane parseHyperplane()
- throws ParseException {
- return Plane.fromPointAndNormal(Vector3D.of(getNumber(), getNumber(), getNumber()),
- Vector3D.of(getNumber(), getNumber(), getNumber()),
- getPrecision());
- }
-
- };
- return new PolyhedronsSet(builder.getTree(), builder.getPrecision());
- }
-
- /**
- * Prints a string representation of the given 1D {@link BSPTree} to the
- * console. This is intended for quick debugging of small trees.
- *
- * @param tree
- */
- public static void printTree1D(BSPTree<Vector1D> tree) {
- TreePrinter1D printer = new TreePrinter1D();
- System.out.println(printer.writeAsString(tree));
- }
-
- /**
- * Prints a string representation of the given 2D {@link BSPTree} to the
- * console. This is intended for quick debugging of small trees.
- *
- * @param tree
- */
- public static void printTree2D(BSPTree<Vector2D> tree) {
- TreePrinter2D printer = new TreePrinter2D();
- System.out.println(printer.writeAsString(tree));
- }
-
- /**
- * Prints a string representation of the given 3D {@link BSPTree} to the
- * console. This is intended for quick debugging of small trees.
- *
- * @param tree
- */
- public static void printTree3D(BSPTree<Vector3D> tree) {
- TreePrinter3D printer = new TreePrinter3D();
- System.out.println(printer.writeAsString(tree));
- }
-
- /**
- * Class for creating string representations of 1D {@link BSPTree}s.
- */
- public static class TreePrinter1D extends TreePrinter<Vector1D> {
-
- /** {@inheritDoc} */
- @Override
- protected void writeInternalNode(BSPTree<Vector1D> node) {
- SubOrientedPoint cut = (SubOrientedPoint) node.getCut();
-
- OrientedPoint hyper = (OrientedPoint) cut.getHyperplane();
- write("cut = { hyperplane: ");
- if (hyper.isPositiveFacing()) {
- write("[" + hyper.getLocation().getX() + ", inf)");
- } else {
- write("(-inf, " + hyper.getLocation().getX() + "]");
- }
-
- IntervalsSet remainingRegion = (IntervalsSet) cut.getRemainingRegion();
- if (remainingRegion != null) {
- write(", remainingRegion: [");
-
- boolean isFirst = true;
- for (double[] interval : remainingRegion) {
- if (isFirst) {
- isFirst = false;
- } else {
- write(", ");
- }
- write(Arrays.toString(interval));
- }
-
- write("]");
- }
-
- write("}");
+ public static void assertRegionLocation(Region<Vector1D> region, RegionLocation loc, Vector1D ... pts) {
+ for (Vector1D pt : pts) {
+ Assert.assertEquals("Unexpected region location for point " + pt, loc, region.classify(pt));
}
}
- /**
- * Class for creating string representations of 2D {@link BSPTree}s.
+ /** Assert that all of the given points lie within the specified location relative to
+ * {@code region}.
+ * @param region
+ * @param loc
+ * @param pts
*/
- public static class TreePrinter2D extends TreePrinter<Vector2D> {
-
- /** {@inheritDoc} */
- @Override
- protected void writeInternalNode(BSPTree<Vector2D> node) {
- SubLine cut = (SubLine) node.getCut();
- Line line = (Line) cut.getHyperplane();
- IntervalsSet remainingRegion = (IntervalsSet) cut.getRemainingRegion();
-
- write("cut = { angle: " + Math.toDegrees(line.getAngle()) + ", origin: " + line.toSpace(Vector1D.ZERO) + "}");
- write(", remainingRegion: [");
-
- boolean isFirst = true;
- for (double[] interval : remainingRegion) {
- if (isFirst) {
- isFirst = false;
- } else {
- write(", ");
- }
- write(Arrays.toString(interval));
- }
-
- write("]");
+ public static void assertRegionLocation(Region<Vector2D> region, RegionLocation loc, Vector2D ... pts) {
+ for (Vector2D pt : pts) {
+ Assert.assertEquals("Unexpected region location for point " + pt, loc, region.classify(pt));
}
}
- /**
- * Class for creating string representations of 3D {@link BSPTree}s.
+ /** Assert that all of the given points lie within the specified location relative to
+ * {@code region}.
+ * @param region
+ * @param loc
+ * @param pts
*/
- public static class TreePrinter3D extends TreePrinter<Vector3D> {
-
- /** {@inheritDoc} */
- @Override
- protected void writeInternalNode(BSPTree<Vector3D> node) {
- SubPlane cut = (SubPlane) node.getCut();
- Plane plane = (Plane) cut.getHyperplane();
- PolygonsSet polygon = (PolygonsSet) cut.getRemainingRegion();
-
- write("cut = { normal: " + plane.getNormal() + ", origin: " + plane.getOrigin() + "}");
- write(", remainingRegion = [");
-
- boolean isFirst = true;
- for (Vector2D[] loop : polygon.getVertices()) {
- // convert to 3-space for easier debugging
- List<Vector3D> loop3 = new ArrayList<>();
- for (Vector2D vertex : loop) {
- if (vertex != null) {
- loop3.add(plane.toSpace(vertex));
- } else {
- loop3.add(null);
- }
- }
-
- if (isFirst) {
- isFirst = false;
- } else {
- write(", ");
- }
-
- write(loop3.toString());
- }
-
- write("]");
+ public static void assertRegionLocation(Region<Vector3D> region, RegionLocation loc, Vector3D ... pts) {
+ for (Vector3D pt : pts) {
+ Assert.assertEquals("Unexpected region location for point " + pt, loc, region.classify(pt));
}
}
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1DTest.java
index 87cc71f..7b27f91 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1DTest.java
@@ -34,6 +34,8 @@
AffineTransformMatrix1D transform = AffineTransformMatrix1D.of(1, 2);
// assert
+ Assert.assertTrue(transform.preservesOrientation());
+
double[] result = transform.toArray();
Assert.assertArrayEquals(new double[] { 1, 2 }, result, 0.0);
}
@@ -52,6 +54,8 @@
AffineTransformMatrix1D transform = AffineTransformMatrix1D.identity();
// assert
+ Assert.assertTrue(transform.preservesOrientation());
+
double[] expected = { 1, 0 };
Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
}
@@ -62,6 +66,8 @@
AffineTransformMatrix1D transform = AffineTransformMatrix1D.createTranslation(2);
// assert
+ Assert.assertTrue(transform.preservesOrientation());
+
double[] expected = { 1, 2 };
Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
}
@@ -72,6 +78,8 @@
AffineTransformMatrix1D transform = AffineTransformMatrix1D.createTranslation(Vector1D.of(5));
// assert
+ Assert.assertTrue(transform.preservesOrientation());
+
double[] expected = { 1, 5 };
Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
}
@@ -85,6 +93,8 @@
AffineTransformMatrix1D result = a.translate(4);
// assert
+ Assert.assertTrue(result.preservesOrientation());
+
double[] expected = { 2, 14 };
Assert.assertArrayEquals(expected, result.toArray(), 0.0);
}
@@ -98,6 +108,8 @@
AffineTransformMatrix1D result = a.translate(Vector1D.of(7));
// assert
+ Assert.assertTrue(result.preservesOrientation());
+
double[] expected = { 2, 17 };
Assert.assertArrayEquals(expected, result.toArray(), 0.0);
}
@@ -108,6 +120,8 @@
AffineTransformMatrix1D transform = AffineTransformMatrix1D.createScale(Vector1D.of(4));
// assert
+ Assert.assertTrue(transform.preservesOrientation());
+
double[] expected = { 4, 0 };
Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
}
@@ -118,6 +132,8 @@
AffineTransformMatrix1D transform = AffineTransformMatrix1D.createScale(7);
// assert
+ Assert.assertTrue(transform.preservesOrientation());
+
double[] expected = { 7, 0 };
Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
}
@@ -131,6 +147,8 @@
AffineTransformMatrix1D result = a.scale(4);
// assert
+ Assert.assertTrue(result.preservesOrientation());
+
double[] expected = { 8, 40 };
Assert.assertArrayEquals(expected, result.toArray(), 0.0);
}
@@ -144,6 +162,8 @@
AffineTransformMatrix1D result = a.scale(Vector1D.of(7));
// assert
+ Assert.assertTrue(result.preservesOrientation());
+
double[] expected = { 14, 70 };
Assert.assertArrayEquals(expected, result.toArray(), 0.0);
}
@@ -393,6 +413,31 @@
}
@Test
+ public void testDeterminant() {
+ // act/assert
+ Assert.assertEquals(0.0, AffineTransformMatrix1D.of(0, 1).determinant(), EPS);
+ Assert.assertEquals(1.0, AffineTransformMatrix1D.of(1, 0).determinant(), EPS);
+ Assert.assertEquals(-1.0, AffineTransformMatrix1D.of(-1, 2).determinant(), EPS);
+ }
+
+ @Test
+ public void testPreservesOrientation() {
+ // act/assert
+ Assert.assertFalse(AffineTransformMatrix1D.of(0, 1).preservesOrientation());
+ Assert.assertTrue(AffineTransformMatrix1D.of(1, 0).preservesOrientation());
+ Assert.assertFalse(AffineTransformMatrix1D.of(-1, 2).preservesOrientation());
+ }
+
+ @Test
+ public void testToMatrix() {
+ // arrange
+ AffineTransformMatrix1D t = AffineTransformMatrix1D.of(1, 1);
+
+ // act/assert
+ Assert.assertSame(t, t.toMatrix());
+ }
+
+ @Test
public void testMultiply() {
// arrange
AffineTransformMatrix1D a = AffineTransformMatrix1D.of(2, 3);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/FunctionTransform1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/FunctionTransform1DTest.java
new file mode 100644
index 0000000..af6b378
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/FunctionTransform1DTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.oned;
+
+import java.util.function.Function;
+
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FunctionTransform1DTest {
+
+ private static final double TEST_EPS = 1e-15;
+
+ @Test
+ public void testIdentity() {
+ // arrange
+ Vector1D p0 = Vector1D.of(0);
+ Vector1D p1 = Vector1D.of(1);
+ Vector1D p2 = Vector1D.of(-1);
+
+ // act
+ Transform1D t = FunctionTransform1D.identity();
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_identity() {
+ // arrange
+ Vector1D p0 = Vector1D.of(0);
+ Vector1D p1 = Vector1D.of(1);
+ Vector1D p2 = Vector1D.of(-1);
+
+ // act
+ Transform1D t = FunctionTransform1D.from(Function.identity());
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_scaleAndTranslate() {
+ // arrange
+ Vector1D p0 = Vector1D.of(0);
+ Vector1D p1 = Vector1D.of(1);
+ Vector1D p2 = Vector1D.of(-1);
+
+ // act
+ Transform1D t = FunctionTransform1D.from(v -> Vector1D.of((v.getX() + 2) * 3));
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(6), t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(9), t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(3), t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_reflection() {
+ // arrange
+ Vector1D p0 = Vector1D.of(0);
+ Vector1D p1 = Vector1D.of(1);
+ Vector1D p2 = Vector1D.of(-1);
+
+ // act
+ Transform1D t = FunctionTransform1D.from(Vector1D::negate);
+
+ // assert
+ Assert.assertFalse(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testApply() {
+ // arrange
+ Transform1D t = FunctionTransform1D.from(v -> {
+ double x = v.getX();
+ return Vector1D.of((-2 * x) + 1);
+ });
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), t.apply(Vector1D.ZERO), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(-1), t.apply(Vector1D.Unit.PLUS), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(-3), t.apply(Vector1D.of(2)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(3), t.apply(Vector1D.of(-1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(5), t.apply(Vector1D.of(-2)), TEST_EPS);
+ }
+
+ @Test
+ public void testApplyVector() {
+ // arrange
+ Transform1D t = FunctionTransform1D.from(v -> {
+ double x = v.getX();
+ return Vector1D.of((-2 * x) + 1);
+ });
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.ZERO, t.applyVector(Vector1D.ZERO), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(-2), t.applyVector(Vector1D.Unit.PLUS), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(-4), t.applyVector(Vector1D.of(2)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2), t.applyVector(Vector1D.of(-1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(4), t.applyVector(Vector1D.of(-2)), TEST_EPS);
+ }
+
+ @Test
+ public void testToMatrix() {
+ // act/assert
+ Assert.assertArrayEquals(new double[] { 1, 0 },
+ FunctionTransform1D.identity().toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] { 1, 2 },
+ FunctionTransform1D.from(v -> v.add(Vector1D.of(2))).toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] { 3, 0 },
+ FunctionTransform1D.from(v -> v.multiply(3)).toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] { 3, 6 },
+ FunctionTransform1D.from(v -> v.add(Vector1D.of(2)).multiply(3)).toMatrix().toArray(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransformRoundTrip() {
+ // arrange
+ double eps = 1e-8;
+ double delta = 0.11;
+
+ Vector1D p1 = Vector1D.of(1.1);
+ Vector1D p2 = Vector1D.of(-5);
+ Vector1D vec = p1.vectorTo(p2);
+
+ EuclideanTestUtils.permuteSkipZero(-2, 2, delta, (translate, scale) -> {
+
+ FunctionTransform1D t = FunctionTransform1D.from(v -> {
+ return v.multiply(scale * 0.5)
+ .add(Vector1D.of(translate))
+ .multiply(scale * 1.5);
+ });
+
+ // act
+ Vector1D t1 = t.apply(p1);
+ Vector1D t2 = t.apply(p2);
+ Vector1D tvec = t.applyVector(vec);
+
+ Transform1D inverse = t.toMatrix().inverse();
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(tvec, t1.vectorTo(t2), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, inverse.apply(t1), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, inverse.apply(t2), eps);
+ });
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java
index ac808a6..dd79284 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java
@@ -16,166 +16,930 @@
*/
package org.apache.commons.geometry.euclidean.oned;
-import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
-import org.apache.commons.numbers.core.Precision;
import org.junit.Assert;
import org.junit.Test;
public class IntervalTest {
- private static final double TEST_TOLERANCE = 1e-10;
+ private static final double TEST_EPS = 1e-15;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
@Test
- public void testBasicProperties() {
+ public void testOf_doubles() {
+ // act/assert
+ checkInterval(Interval.of(0, 0, TEST_PRECISION), 0, 0);
+
+ checkInterval(Interval.of(1, 2, TEST_PRECISION), 1, 2);
+ checkInterval(Interval.of(2, 1, TEST_PRECISION), 1, 2);
+
+ checkInterval(Interval.of(-2, -1, TEST_PRECISION), -2, -1);
+ checkInterval(Interval.of(-1, -2, TEST_PRECISION), -2, -1);
+
+ checkInterval(Interval.of(1, Double.POSITIVE_INFINITY, TEST_PRECISION),
+ 1, Double.POSITIVE_INFINITY);
+ checkInterval(Interval.of(Double.POSITIVE_INFINITY, 1, TEST_PRECISION),
+ 1, Double.POSITIVE_INFINITY);
+
+ checkInterval(Interval.of(Double.NEGATIVE_INFINITY, 1, TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, 1);
+ checkInterval(Interval.of(1, Double.NEGATIVE_INFINITY, TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, 1);
+
+ checkInterval(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+ checkInterval(Interval.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testOf_doubles_invalidIntervals() {
// arrange
- Interval interval = new Interval(2.3, 5.7);
+ Class<?> excType = GeometryException.class;
// act/assert
- Assert.assertEquals(3.4, interval.getSize(), TEST_TOLERANCE);
- Assert.assertEquals(4.0, interval.getBarycenter(), TEST_TOLERANCE);
- Assert.assertEquals(2.3, interval.getInf(), TEST_TOLERANCE);
- Assert.assertEquals(5.7, interval.getSup(), TEST_TOLERANCE);
+ GeometryTestUtils.assertThrows(() -> Interval.of(1, Double.NaN, TEST_PRECISION), excType);
+ GeometryTestUtils.assertThrows(() -> Interval.of(Double.NaN, 1, TEST_PRECISION), excType);
+ GeometryTestUtils.assertThrows(() -> Interval.of(Double.NaN, Double.NaN, TEST_PRECISION), excType);
+
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION), excType);
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, TEST_PRECISION), excType);
}
@Test
- public void testBasicProperties_negativeValues() {
+ public void testOf_points() {
+ // act/assert
+ checkInterval(Interval.of(Vector1D.of(1), Vector1D.of(2), TEST_PRECISION), 1, 2);
+ checkInterval(Interval.of(Vector1D.of(Double.POSITIVE_INFINITY), Vector1D.of(Double.NEGATIVE_INFINITY), TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testOf_points_invalidIntervals() {
// arrange
- Interval interval = new Interval(-5.7, -2.3);
+ Class<?> excType = GeometryException.class;
// act/assert
- Assert.assertEquals(3.4, interval.getSize(), TEST_TOLERANCE);
- Assert.assertEquals(-4.0, interval.getBarycenter(), TEST_TOLERANCE);
- Assert.assertEquals(-5.7, interval.getInf(), TEST_TOLERANCE);
- Assert.assertEquals(-2.3, interval.getSup(), TEST_TOLERANCE);
- }
-
- // MATH-1256
- @Test(expected = IllegalArgumentException.class)
- public void testStrictOrdering() {
- new Interval(0, -1);
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(Vector1D.of(1), Vector1D.of(Double.NaN), TEST_PRECISION), excType);
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(Vector1D.of(Double.POSITIVE_INFINITY), Vector1D.of(Double.POSITIVE_INFINITY), TEST_PRECISION), excType);
}
@Test
- public void testCheckPoint() {
+ public void testOf_hyperplanes() {
+ // act/assert
+ checkInterval(Interval.of(
+ OrientedPoint.fromLocationAndDirection(1, true, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(1, false, TEST_PRECISION)), 1, 1);
+ checkInterval(Interval.of(
+ OrientedPoint.fromLocationAndDirection(1, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(1, true, TEST_PRECISION)), 1, 1);
+
+ checkInterval(Interval.of(
+ OrientedPoint.fromLocationAndDirection(-2, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(5, true, TEST_PRECISION)), -2, 5);
+ checkInterval(Interval.of(
+ OrientedPoint.fromLocationAndDirection(5, true, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(-2, false, TEST_PRECISION)), -2, 5);
+
+ checkInterval(Interval.of(
+ null,
+ OrientedPoint.fromLocationAndDirection(5, true, TEST_PRECISION)), Double.NEGATIVE_INFINITY, 5);
+ checkInterval(Interval.of(
+ OrientedPoint.fromLocationAndDirection(5, true, TEST_PRECISION),
+ null), Double.NEGATIVE_INFINITY, 5);
+ checkInterval(Interval.of(
+ OrientedPoint.fromLocationAndDirection(Double.NEGATIVE_INFINITY, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(5, true, TEST_PRECISION)), Double.NEGATIVE_INFINITY, 5);
+
+ checkInterval(Interval.of(
+ null,
+ OrientedPoint.fromLocationAndDirection(5, false, TEST_PRECISION)), 5, Double.POSITIVE_INFINITY);
+ checkInterval(Interval.of(
+ OrientedPoint.fromLocationAndDirection(5, false, TEST_PRECISION),
+ null), 5, Double.POSITIVE_INFINITY);
+ checkInterval(Interval.of(
+ OrientedPoint.fromLocationAndDirection(Double.POSITIVE_INFINITY, true, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(5, false, TEST_PRECISION)), 5, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testOf_hyperplanes_invalidArgs() {
// arrange
- Interval interval = new Interval(2.3, 5.7);
+ Class<?> excType = GeometryException.class;
// act/assert
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(1.2, TEST_TOLERANCE));
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(
+ OrientedPoint.fromLocationAndDirection(1, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(1, false, TEST_PRECISION)), excType);
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(2.2, TEST_TOLERANCE));
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.3, TEST_TOLERANCE));
- Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(2.4, TEST_TOLERANCE));
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(
+ OrientedPoint.fromLocationAndDirection(2, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(1, true, TEST_PRECISION)), excType);
- Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(3.0, TEST_TOLERANCE));
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(
+ OrientedPoint.fromLocationAndDirection(Double.POSITIVE_INFINITY, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(Double.POSITIVE_INFINITY, true, TEST_PRECISION)), excType);
- Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(5.6, TEST_TOLERANCE));
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(5.7, TEST_TOLERANCE));
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(5.8, TEST_TOLERANCE));
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(
+ OrientedPoint.fromLocationAndDirection(Double.NaN, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(1, true, TEST_PRECISION)), excType);
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(8.7, TEST_TOLERANCE));
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(
+ OrientedPoint.fromLocationAndDirection(1, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(Double.NaN, true, TEST_PRECISION)), excType);
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(Double.NEGATIVE_INFINITY, TEST_TOLERANCE));
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(Double.POSITIVE_INFINITY, TEST_TOLERANCE));
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(Double.NaN, TEST_TOLERANCE));
+ GeometryTestUtils.assertThrows(
+ () -> Interval.of(
+ OrientedPoint.fromLocationAndDirection(Double.NaN, false, TEST_PRECISION),
+ OrientedPoint.fromLocationAndDirection(Double.NaN, true, TEST_PRECISION)), excType);
}
@Test
- public void testCheckPoint_tolerance() {
+ public void testPoint() {
+ // act/assert
+ checkInterval(Interval.point(0, TEST_PRECISION), 0, 0);
+ checkInterval(Interval.point(1, TEST_PRECISION), 1, 1);
+ checkInterval(Interval.point(-1, TEST_PRECISION), -1, -1);
+ }
+
+ @Test
+ public void testPoint_invalidArgs() {
// arrange
- Interval interval = new Interval(2.3, 5.7);
+ Class<?> excType = GeometryException.class;
// act/assert
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(2.29, 1e-3));
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1e-2));
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1e-1));
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1));
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 2));
-
- Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-3));
- Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-2));
- Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-1));
- Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1));
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(4.0, 2));
+ GeometryTestUtils.assertThrows(
+ () -> Interval.point(Double.NEGATIVE_INFINITY, TEST_PRECISION), excType);
+ GeometryTestUtils.assertThrows(
+ () -> Interval.point(Double.POSITIVE_INFINITY, TEST_PRECISION), excType);
+ GeometryTestUtils.assertThrows(
+ () -> Interval.point(Double.NaN, TEST_PRECISION), excType);
}
@Test
- public void testInfinite_inf() {
+ public void testMin() {
+ // act/assert
+ checkInterval(Interval.min(Double.NEGATIVE_INFINITY, TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+ checkInterval(Interval.min(0, TEST_PRECISION), 0, Double.POSITIVE_INFINITY);
+ checkInterval(Interval.min(1, TEST_PRECISION), 1, Double.POSITIVE_INFINITY);
+ checkInterval(Interval.min(-1, TEST_PRECISION), -1, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testMin_invalidArgs() {
+ // arrange
+ Class<?> excType = GeometryException.class;
+
+ // act/assert
+ GeometryTestUtils.assertThrows(
+ () -> Interval.min(Double.POSITIVE_INFINITY, TEST_PRECISION), excType);
+ GeometryTestUtils.assertThrows(
+ () -> Interval.min(Double.NaN, TEST_PRECISION), excType);
+ }
+
+ @Test
+ public void testMax() {
+ // act/assert
+ checkInterval(Interval.max(Double.POSITIVE_INFINITY, TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+ checkInterval(Interval.max(0, TEST_PRECISION), Double.NEGATIVE_INFINITY, 0);
+ checkInterval(Interval.max(1, TEST_PRECISION), Double.NEGATIVE_INFINITY, 1);
+ checkInterval(Interval.max(-1, TEST_PRECISION), Double.NEGATIVE_INFINITY, -1);
+ }
+
+ @Test
+ public void testMax_invalidArgs() {
+ // arrange
+ Class<?> excType = GeometryException.class;
+
+ // act/assert
+ GeometryTestUtils.assertThrows(
+ () -> Interval.max(Double.NEGATIVE_INFINITY, TEST_PRECISION), excType);
+ GeometryTestUtils.assertThrows(
+ () -> Interval.max(Double.NaN, TEST_PRECISION), excType);
+ }
+
+ @Test
+ public void testIsInfinite() {
+ // act/assert
+ Assert.assertFalse(Interval.of(1, 2, TEST_PRECISION).isInfinite());
+
+ Assert.assertTrue(Interval.of(Double.NEGATIVE_INFINITY, 2, TEST_PRECISION).isInfinite());
+ Assert.assertTrue(Interval.of(2, Double.POSITIVE_INFINITY, TEST_PRECISION).isInfinite());
+ Assert.assertTrue(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION).isInfinite());
+ }
+
+ @Test
+ public void testIsFinite() {
+ // act/assert
+ Assert.assertTrue(Interval.of(1, 2, TEST_PRECISION).isFinite());
+
+ Assert.assertFalse(Interval.of(Double.NEGATIVE_INFINITY, 2, TEST_PRECISION).isFinite());
+ Assert.assertFalse(Interval.of(2, Double.POSITIVE_INFINITY, TEST_PRECISION).isFinite());
+ Assert.assertFalse(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION).isFinite());
+ }
+
+ @Test
+ public void testClassify_finite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(-1, 1, precision);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.OUTSIDE,
+ Double.NEGATIVE_INFINITY, -2, -1.1,
+ 1.1, 2, Double.POSITIVE_INFINITY);
+
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ -1.001, -1, -0.999,
+ 0.999, 1, 1.001);
+
+ checkClassify(interval, RegionLocation.INSIDE, -0.9, 0, 0.9);
+
+ checkClassify(interval, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void testClassify_singlePoint() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(1, 1, precision);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.OUTSIDE,
+ Double.NEGATIVE_INFINITY, 0, 0.9, 1.1, 2, Double.POSITIVE_INFINITY);
+
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ 0.999, 1, 1.0001);
+
+ checkClassify(interval, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void testClassify_maxInfinite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(-1, Double.POSITIVE_INFINITY, precision);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.OUTSIDE,
+ Double.NEGATIVE_INFINITY, -2, -1.1);
+
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ -1.001, -1, -0.999);
+
+ checkClassify(interval, RegionLocation.INSIDE,
+ -0.9, 0, 1.0, Double.POSITIVE_INFINITY);
+
+ checkClassify(interval, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void testClassify_minInfinite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(Double.NEGATIVE_INFINITY, 1, precision);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.INSIDE,
+ Double.NEGATIVE_INFINITY, 0, 0.9);
+
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ 0.999, 1, 1.001);
+
+ checkClassify(interval, RegionLocation.OUTSIDE,
+ 1.1, 2, Double.POSITIVE_INFINITY);
+
+ checkClassify(interval, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void testClassify_minMaxInfinite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, precision);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.INSIDE,
+ Double.NEGATIVE_INFINITY, -1, 0, 1, Double.POSITIVE_INFINITY);
+
+ checkClassify(interval, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void testContains_finite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(-1, 1, precision);
+
+ // act/assert
+ checkContains(interval, true,
+ -1.001, -1, -0.999,
+ 0.999, 1, 1.001,
+
+ -0.9, 0, 0.9);
+
+ checkContains(interval, false,
+ Double.NEGATIVE_INFINITY, -2, -1.1,
+ 1.1, 2, Double.POSITIVE_INFINITY);
+
+ checkContains(interval, false, Double.NaN);
+ }
+
+ @Test
+ public void testIsFull() {
+ // act/assert
+ Assert.assertFalse(Interval.of(1, 1, TEST_PRECISION).isFull());
+ Assert.assertFalse(Interval.of(-2, 2, TEST_PRECISION).isFull());
+
+ Assert.assertFalse(Interval.of(1, Double.POSITIVE_INFINITY, TEST_PRECISION).isFull());
+ Assert.assertFalse(Interval.of(Double.NEGATIVE_INFINITY, 1, TEST_PRECISION).isFull());
+
+ Assert.assertTrue(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION).isFull());
+ }
+
+ @Test
+ public void testGetSize() {
+ // act/assert
+ Assert.assertEquals(0, Interval.of(1, 1, TEST_PRECISION).getSize(), TEST_EPS);
+
+ Assert.assertEquals(4, Interval.of(-2, 2, TEST_PRECISION).getSize(), TEST_EPS);
+ Assert.assertEquals(5, Interval.of(2, -3, TEST_PRECISION).getSize(), TEST_EPS);
+
+ Assert.assertEquals(Double.POSITIVE_INFINITY,
+ Interval.of(1, Double.POSITIVE_INFINITY, TEST_PRECISION).getSize(), TEST_EPS);
+ Assert.assertEquals(Double.POSITIVE_INFINITY,
+ Interval.of(Double.NEGATIVE_INFINITY, 1, TEST_PRECISION).getSize(), TEST_EPS);
+
+ Assert.assertEquals(Double.POSITIVE_INFINITY,
+ Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION).getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBoundarySize() {
+ // act/assert
+ Assert.assertEquals(0, Interval.of(1, 1, TEST_PRECISION).getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0, Interval.of(-2, 5, TEST_PRECISION).getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0, Interval.full().getBoundarySize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBarycenter() {
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.ZERO,
+ Interval.of(-1, 1, TEST_PRECISION).getBarycenter(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(10),
+ Interval.of(10, 10, TEST_PRECISION).getBarycenter(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2),
+ Interval.of(1, 3, TEST_PRECISION).getBarycenter(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(-1),
+ Interval.of(-2, 0, TEST_PRECISION).getBarycenter(), TEST_EPS);
+
+ Assert.assertNull(Interval.of(1, Double.POSITIVE_INFINITY, TEST_PRECISION).getBarycenter());
+ Assert.assertNull(Interval.of(Double.NEGATIVE_INFINITY, 1, TEST_PRECISION).getBarycenter());
+ Assert.assertNull(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION).getBarycenter());
+ }
+
+ @Test
+ public void checkToTree_finite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(-1, 1, precision);
+
// act
- Interval interval = new Interval(Double.NEGATIVE_INFINITY, 9);
+ RegionBSPTree1D tree = interval.toTree();
// assert
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(9.0, TEST_TOLERANCE));
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(9.4, TEST_TOLERANCE));
- for (double e = 1.0; e <= 6.0; e += 1.0) {
- Assert.assertEquals(Region.Location.INSIDE,
- interval.checkPoint(-1 * Math.pow(10.0, e), TEST_TOLERANCE));
+ Assert.assertEquals(5, tree.count());
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Double.NEGATIVE_INFINITY, -2, -1.1,
+ 1.1, 2, Double.POSITIVE_INFINITY);
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ -1.001, -1, -0.999,
+ 0.999, 1, 1.001);
+
+ checkClassify(tree, RegionLocation.INSIDE, -0.9, 0, 0.9);
+
+ checkClassify(tree, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void checkToTree_singlePoint() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(1, 1, precision);
+
+ // act
+ RegionBSPTree1D tree = interval.toTree();
+
+ // assert
+ Assert.assertEquals(5, tree.count());
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Double.NEGATIVE_INFINITY, 0, 0.9, 1.1, 2, Double.POSITIVE_INFINITY);
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ 0.999, 1, 1.0001);
+
+ checkClassify(tree, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void checkToTree_maxInfinite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(-1, Double.POSITIVE_INFINITY, precision);
+
+ // act
+ RegionBSPTree1D tree = interval.toTree();
+
+ // assert
+ Assert.assertEquals(3, tree.count());
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Double.NEGATIVE_INFINITY, -2, -1.1);
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ -1.001, -1, -0.999);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ -0.9, 0, 1.0, Double.POSITIVE_INFINITY);
+
+ checkClassify(interval, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void checkToTree_minInfinite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(Double.NEGATIVE_INFINITY, 1, precision);
+
+ // act
+ RegionBSPTree1D tree = interval.toTree();
+
+ // assert
+ Assert.assertEquals(3, tree.count());
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Double.NEGATIVE_INFINITY, 0, 0.9);
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ 0.999, 1, 1.001);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ 1.1, 2, Double.POSITIVE_INFINITY);
+
+ checkClassify(tree, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void checkToTree_minMaxInfinite() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, precision);
+
+ // act
+ RegionBSPTree1D tree = interval.toTree();
+
+ // assert
+ Assert.assertEquals(1, tree.count());
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Double.NEGATIVE_INFINITY, -1, 0, 1, Double.POSITIVE_INFINITY);
+
+ checkClassify(tree, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void testProjectToBoundary_full() {
+ // arrange
+ Interval full = Interval.full();
+
+
+ // act/assert
+ Assert.assertNull(full.project(Vector1D.of(Double.NEGATIVE_INFINITY)));
+ Assert.assertNull(full.project(Vector1D.of(0)));
+ Assert.assertNull(full.project(Vector1D.of(Double.POSITIVE_INFINITY)));
+ }
+
+ @Test
+ public void testProjectToBoundary_singlePoint() {
+ // arrange
+ Interval interval = Interval.point(1, TEST_PRECISION);
+
+ // act/assert
+ checkBoundaryProjection(interval, -1, 1);
+ checkBoundaryProjection(interval, 0, 1);
+
+ checkBoundaryProjection(interval, 1, 1);
+
+ checkBoundaryProjection(interval, 2, 1);
+ checkBoundaryProjection(interval, 3, 1);
+
+ checkBoundaryProjection(interval, Double.NEGATIVE_INFINITY, 1);
+ checkBoundaryProjection(interval, Double.POSITIVE_INFINITY, 1);
+ }
+
+ @Test
+ public void testProjectToBoundary_closedInterval() {
+ // arrange
+ Interval interval = Interval.of(1, 3, TEST_PRECISION);
+
+ // act/assert
+ checkBoundaryProjection(interval, -1, 1);
+ checkBoundaryProjection(interval, 0, 1);
+ checkBoundaryProjection(interval, 1, 1);
+
+ checkBoundaryProjection(interval, 1.9, 1);
+ checkBoundaryProjection(interval, 2, 1);
+ checkBoundaryProjection(interval, 2.1, 3);
+
+ checkBoundaryProjection(interval, 3, 3);
+ checkBoundaryProjection(interval, 4, 3);
+ checkBoundaryProjection(interval, 5, 3);
+
+ checkBoundaryProjection(interval, Double.NEGATIVE_INFINITY, 1);
+ checkBoundaryProjection(interval, Double.POSITIVE_INFINITY, 3);
+ }
+
+ @Test
+ public void testProjectToBoundary_noMinBoundary() {
+ // arrange
+ Interval interval = Interval.of(Double.NEGATIVE_INFINITY, 1, TEST_PRECISION);
+
+ // act/assert
+ checkBoundaryProjection(interval, -1, 1);
+ checkBoundaryProjection(interval, 0, 1);
+ checkBoundaryProjection(interval, 1, 1);
+ checkBoundaryProjection(interval, 2, 1);
+ checkBoundaryProjection(interval, 3, 1);
+
+ checkBoundaryProjection(interval, Double.NEGATIVE_INFINITY, 1);
+ checkBoundaryProjection(interval, Double.POSITIVE_INFINITY, 1);
+ }
+
+ @Test
+ public void testProjectToBoundary_noMaxBoundary() {
+ // arrange
+ Interval interval = Interval.of(1, Double.POSITIVE_INFINITY, TEST_PRECISION);
+
+ // act/assert
+ checkBoundaryProjection(interval, -1, 1);
+ checkBoundaryProjection(interval, 0, 1);
+ checkBoundaryProjection(interval, 1, 1);
+ checkBoundaryProjection(interval, 2, 1);
+ checkBoundaryProjection(interval, 3, 1);
+
+ checkBoundaryProjection(interval, Double.NEGATIVE_INFINITY, 1);
+ checkBoundaryProjection(interval, Double.POSITIVE_INFINITY, 1);
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ Transform1D transform = FunctionTransform1D.from((p) -> Vector1D.of(2.0 * p.getX()));
+
+ // act/assert
+ checkInterval(Interval.of(-1, 2, TEST_PRECISION).transform(transform), -2, 4);
+
+ checkInterval(Interval.of(Double.NEGATIVE_INFINITY, 2, TEST_PRECISION).transform(transform),
+ Double.NEGATIVE_INFINITY, 4);
+
+ checkInterval(Interval.of(-1, Double.POSITIVE_INFINITY, TEST_PRECISION).transform(transform), -2,
+ Double.POSITIVE_INFINITY);
+
+ checkInterval(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION).transform(transform),
+ Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testTransform_reflection() {
+ // arrange
+ Transform1D transform = FunctionTransform1D.from(Vector1D::negate);
+
+ // act/assert
+ checkInterval(Interval.of(-1, 2, TEST_PRECISION).transform(transform), -2, 1);
+
+ checkInterval(Interval.of(Double.NEGATIVE_INFINITY, 2, TEST_PRECISION).transform(transform),
+ -2, Double.POSITIVE_INFINITY);
+
+ checkInterval(Interval.of(-1, Double.POSITIVE_INFINITY, TEST_PRECISION).transform(transform),
+ Double.NEGATIVE_INFINITY, 1);
+ }
+
+ @Test
+ public void testSplit_full_positiveFacingSplitter() {
+ // arrange
+ Interval interval = Interval.full();
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(1), true, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), Double.NEGATIVE_INFINITY, 1);
+ checkInterval(split.getPlus(), 1, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testSplit_full_negativeFacingSplitter() {
+ // arrange
+ Interval interval = Interval.full();
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(1), true, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), Double.NEGATIVE_INFINITY, 1);
+ checkInterval(split.getPlus(), 1, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testSplit_halfSpace_positiveFacingSplitter() {
+ // arrange
+ Interval interval = Interval.min(-1, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(1), false, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), 1, Double.POSITIVE_INFINITY);
+ checkInterval(split.getPlus(), -1, 1);
+ }
+
+
+ @Test
+ public void testSplit_halfSpace_negativeFacingSplitter() {
+ // arrange
+ Interval interval = Interval.min(-1, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(1), false, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), 1, Double.POSITIVE_INFINITY);
+ checkInterval(split.getPlus(), -1, 1);
+ }
+
+ @Test
+ public void testSplit_splitterBelowInterval() {
+ // arrange
+ Interval interval = Interval.of(5, 10, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(1), true, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertSame(interval, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_splitterOnMinBoundary() {
+ // arrange
+ Interval interval = Interval.of(5, 10, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(5), false, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(interval, split.getMinus());
+ }
+
+ @Test
+ public void testSplit_splitterAboveInterval() {
+ // arrange
+ Interval interval = Interval.of(5, 10, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(11), true, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(interval, split.getMinus());
+ }
+
+ @Test
+ public void testSplit_splitterOnMaxBoundary() {
+ // arrange
+ Interval interval = Interval.of(5, 10, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(10), false, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertSame(interval, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_point_minusOnly() {
+ // arrange
+ Interval interval = Interval.point(2, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(1), false, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ checkInterval(split.getMinus(), 2, 2);
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_point_plusOnly() {
+ // arrange
+ Interval interval = Interval.point(2, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(1), true, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ checkInterval(split.getPlus(), 2, 2);
+ }
+
+ @Test
+ public void testSplit_point_onPoint() {
+ // arrange
+ Interval interval = Interval.point(1, TEST_PRECISION);
+ OrientedPoint splitter = OrientedPoint.fromPointAndDirection(
+ Vector1D.of(1), true, TEST_PRECISION);
+
+ // act
+ Split<Interval> split = interval.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Interval interval = Interval.of(2, 1, TEST_PRECISION);
+
+ // act
+ String str = interval.toString();
+
+ // assert
+ Assert.assertTrue(str.contains("Interval"));
+ Assert.assertTrue(str.contains("min= 1.0"));
+ Assert.assertTrue(str.contains("max= 2.0"));
+ }
+
+ @Test
+ public void testFull() {
+ // act
+ Interval full = Interval.full();
+
+ // assert
+ Assert.assertTrue(full.isFull());
+ Assert.assertFalse(full.isEmpty());
+ Assert.assertFalse(full.hasMinBoundary());
+ Assert.assertFalse(full.hasMaxBoundary());
+ Assert.assertTrue(full.isInfinite());
+
+ Assert.assertEquals(RegionLocation.INSIDE, full.classify(Double.NEGATIVE_INFINITY));
+ Assert.assertEquals(RegionLocation.INSIDE, full.classify(Double.POSITIVE_INFINITY));
+ }
+
+ private static void checkContains(Interval interval, boolean contains, double ... points) {
+ for (double x : points) {
+ String msg = "Unexpected contains status for point " + x;
+
+ Assert.assertEquals(msg, contains, interval.contains(x));
+ Assert.assertEquals(msg, contains, interval.contains(Vector1D.of(x)));
}
- EuclideanTestUtils.assertPositiveInfinity(interval.getSize());
- EuclideanTestUtils.assertNegativeInfinity(interval.getInf());
- Assert.assertEquals(9.0, interval.getSup(), TEST_TOLERANCE);
}
- @Test
- public void testInfinite_sup() {
- // act
- Interval interval = new Interval(9.0, Double.POSITIVE_INFINITY);
+ private static void checkClassify(Interval interval, RegionLocation loc, double ... points) {
+ for (double x : points) {
+ String msg = "Unexpected location for point " + x;
- // assert
- Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(9.0, TEST_TOLERANCE));
- Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(8.4, TEST_TOLERANCE));
- for (double e = 1.0; e <= 6.0; e += 1.0) {
- Assert.assertEquals(Region.Location.INSIDE,
- interval.checkPoint(Math.pow(10.0, e), TEST_TOLERANCE));
+ Assert.assertEquals(msg, loc, interval.classify(x));
+ Assert.assertEquals(msg, loc, interval.classify(Vector1D.of(x)));
}
- EuclideanTestUtils.assertPositiveInfinity(interval.getSize());
- Assert.assertEquals(9.0, interval.getInf(), TEST_TOLERANCE);
- EuclideanTestUtils.assertPositiveInfinity(interval.getSup());
}
- @Test
- public void testInfinite_infAndSup() {
- // act
- Interval interval = new Interval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+ private static void checkClassify(RegionBSPTree1D tree, RegionLocation loc, double ... points) {
+ for (double x : points) {
+ String msg = "Unexpected location for point " + x;
- // assert
- for (double e = 1.0; e <= 6.0; e += 1.0) {
- Assert.assertEquals(Region.Location.INSIDE,
- interval.checkPoint(Math.pow(10.0, e), TEST_TOLERANCE));
+ Assert.assertEquals(msg, loc, tree.classify(x));
+ Assert.assertEquals(msg, loc, tree.classify(Vector1D.of(x)));
}
- EuclideanTestUtils.assertPositiveInfinity(interval.getSize());
- EuclideanTestUtils.assertNegativeInfinity(interval.getInf());
- EuclideanTestUtils.assertPositiveInfinity(interval.getSup());
}
- @Test
- public void testSinglePoint() {
- // act
- Interval interval = new Interval(1.0, 1.0);
+ private static void checkBoundaryProjection(Interval interval, double location, double projectedLocation) {
+ Vector1D pt = Vector1D.of(location);
- // assert
- Assert.assertEquals(0.0, interval.getSize(), Precision.SAFE_MIN);
- Assert.assertEquals(1.0, interval.getBarycenter(), Precision.EPSILON);
+ Vector1D proj = interval.project(pt);
+
+ Assert.assertEquals(projectedLocation, proj.getX(), TEST_EPS);
}
- @Test
- public void testSingleInfinitePoint_positive() {
- // act
- Interval interval = new Interval(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
-
- // assert
- Assert.assertTrue(Double.isNaN(interval.getSize())); // inf - inf = NaN according to floating point spec
- EuclideanTestUtils.assertPositiveInfinity(interval.getBarycenter());
+ /** Check that the given interval matches the arguments and is internally consistent.
+ * @param interval
+ * @param min
+ * @param max
+ */
+ private static void checkInterval(Interval interval, double min, double max) {
+ checkInterval(interval, min, max, TEST_PRECISION);
}
- @Test
- public void testSingleInfinitePoint_negative() {
- // act
- Interval interval = new Interval(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
+ /** Check that the given interval matches the arguments and is internally consistent.
+ * @param interval
+ * @param min
+ * @param max
+ * @param precision
+ */
+ private static void checkInterval(Interval interval, double min, double max, DoublePrecisionContext precision) {
+ Assert.assertEquals(min, interval.getMin(), TEST_EPS);
+ Assert.assertEquals(max, interval.getMax(), TEST_EPS);
- // assert
- Assert.assertTrue(Double.isNaN(interval.getSize())); // inf - inf = NaN according to floating point spec
- EuclideanTestUtils.assertNegativeInfinity(interval.getBarycenter());
+ boolean finiteMin = Double.isFinite(min);
+ boolean finiteMax = Double.isFinite(max);
+
+ Assert.assertEquals(finiteMin, interval.hasMinBoundary());
+ Assert.assertEquals(finiteMax, interval.hasMaxBoundary());
+
+ if (finiteMin) {
+ Assert.assertEquals(min, interval.getMinBoundary().getLocation(), TEST_EPS);
+ }
+ else {
+ Assert.assertNull(interval.getMinBoundary());
+ }
+
+ if (finiteMax) {
+ Assert.assertEquals(max, interval.getMaxBoundary().getLocation(), TEST_EPS);
+ }
+ else {
+ Assert.assertNull(interval.getMaxBoundary());
+ }
+
+ Assert.assertFalse(interval.isEmpty()); // always false
}
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalsSetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalsSetTest.java
deleted file mode 100644
index 85f27e4..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalsSetTest.java
+++ /dev/null
@@ -1,592 +0,0 @@
-/*
- * 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.oned;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class IntervalsSetTest {
-
- private static final double TEST_EPS = 1e-15;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testInterval_wholeNumberLine() {
- // act
- IntervalsSet set = new IntervalsSet(TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- EuclideanTestUtils.assertNegativeInfinity(set.getInf());
- EuclideanTestUtils.assertPositiveInfinity(set.getSup());
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- BSPTree<Vector1D> tree = set.getTree(true);
- Assert.assertEquals(Boolean.TRUE, tree.getAttribute());
- Assert.assertNull(tree.getCut());
- Assert.assertNull(tree.getMinus());
- Assert.assertNull(tree.getPlus());
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testInterval_doubleOpenInterval() {
- // act
- IntervalsSet set = new IntervalsSet(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- EuclideanTestUtils.assertNegativeInfinity(set.getInf());
- EuclideanTestUtils.assertPositiveInfinity(set.getSup());
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- BSPTree<Vector1D> tree = set.getTree(true);
- Assert.assertEquals(Boolean.TRUE, tree.getAttribute());
- Assert.assertNull(tree.getCut());
- Assert.assertNull(tree.getMinus());
- Assert.assertNull(tree.getPlus());
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testInterval_openInterval_positive() {
- // act
- IntervalsSet set = new IntervalsSet(9.0, Double.POSITIVE_INFINITY, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(9.0, set.getInf(), TEST_EPS);
- EuclideanTestUtils.assertPositiveInfinity(set.getSup());
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(9.0, Double.POSITIVE_INFINITY, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.OUTSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
- assertLocation(Region.Location.INSIDE, set, 10.0);
- assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testInterval_openInterval_negative() {
- // act
- IntervalsSet set = new IntervalsSet(Double.NEGATIVE_INFINITY, 9.0, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- EuclideanTestUtils.assertNegativeInfinity(set.getInf());
- Assert.assertEquals(9.0, set.getSup(), TEST_EPS);
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(Double.NEGATIVE_INFINITY, 9.0, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
- assertLocation(Region.Location.OUTSIDE, set, 10.0);
- assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testInterval_singleClosedInterval() {
- // act
- IntervalsSet set = new IntervalsSet(-1.0, 9.0, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(-1.0, set.getInf(), TEST_EPS);
- Assert.assertEquals(9.0, set.getSup(), TEST_EPS);
- Assert.assertEquals(10.0, set.getSize(), TEST_EPS);
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(4.0), set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(-1.0, 9.0, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.OUTSIDE, set, -2.0);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
- assertLocation(Region.Location.OUTSIDE, set, 10.0);
- assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testInterval_singlePoint() {
- // act
- IntervalsSet set = new IntervalsSet(1.0, 1.0, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(1.0, set.getInf(), TEST_EPS);
- Assert.assertEquals(1.0, set.getSup(), TEST_EPS);
- Assert.assertEquals(0.0, set.getSize(), TEST_EPS);
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1.0), set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(1.0, 1.0, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.OUTSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 1.0);
- assertLocation(Region.Location.OUTSIDE, set, 2.0);
- assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testFromBoundaries_wholeNumberLine() {
- // arrange
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
-
- // act
- IntervalsSet set = new IntervalsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- EuclideanTestUtils.assertNegativeInfinity(set.getInf());
- EuclideanTestUtils.assertPositiveInfinity(set.getSup());
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- BSPTree<Vector1D> tree = set.getTree(true);
- Assert.assertEquals(Boolean.TRUE, tree.getAttribute());
- Assert.assertNull(tree.getCut());
- Assert.assertNull(tree.getMinus());
- Assert.assertNull(tree.getPlus());
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testFromBoundaries_openInterval_positive() {
- // arrange
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
- boundaries.add(subOrientedPoint(9.0, false));
-
- // act
- IntervalsSet set = new IntervalsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(9.0, set.getInf(), TEST_EPS);
- EuclideanTestUtils.assertPositiveInfinity(set.getSup());
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(9.0, Double.POSITIVE_INFINITY, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.OUTSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
- assertLocation(Region.Location.INSIDE, set, 10.0);
- assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testFromBoundaries_openInterval_negative() {
- // arrange
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
- boundaries.add(subOrientedPoint(9.0, true));
-
- // act
- IntervalsSet set = new IntervalsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- EuclideanTestUtils.assertNegativeInfinity(set.getInf());
- Assert.assertEquals(9.0, set.getSup(), TEST_EPS);
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(Double.NEGATIVE_INFINITY, 9.0, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
- assertLocation(Region.Location.OUTSIDE, set, 10.0);
- assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testFromBoundaries_singleClosedInterval() {
- // arrange
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
- boundaries.add(subOrientedPoint(-1.0, false));
- boundaries.add(subOrientedPoint(9.0, true));
-
- // act
- IntervalsSet set = new IntervalsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(-1.0, set.getInf(), TEST_EPS);
- Assert.assertEquals(9.0, set.getSup(), TEST_EPS);
- Assert.assertEquals(10.0, set.getSize(), TEST_EPS);
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(4.0), set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(-1.0, 9.0, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.OUTSIDE, set, -2.0);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
- assertLocation(Region.Location.OUTSIDE, set, 10.0);
- assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testFromBoundaries_multipleClosedIntervals() {
- // arrange
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
- boundaries.add(subOrientedPoint(-1.0, false));
- boundaries.add(subOrientedPoint(2.0, true));
- boundaries.add(subOrientedPoint(5.0, false));
- boundaries.add(subOrientedPoint(9.0, true));
-
- // act
- IntervalsSet set = new IntervalsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(-1.0, set.getInf(), TEST_EPS);
- Assert.assertEquals(9.0, set.getSup(), TEST_EPS);
- Assert.assertEquals(7.0, set.getSize(), TEST_EPS);
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(29.5 / 7.0), set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(2, intervals.size());
- assertInterval(-1.0, 2.0, intervals.get(0), TEST_EPS);
- assertInterval(5.0, 9.0, intervals.get(1), TEST_EPS);
-
- assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.OUTSIDE, set, -2.0);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.OUTSIDE, set, 3.0);
- assertLocation(Region.Location.INSIDE, set, 6.0);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
- assertLocation(Region.Location.OUTSIDE, set, 10.0);
- assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testFromBoundaries_mixedOpenAndClosedIntervals() {
- // arrange
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
- boundaries.add(subOrientedPoint(-2.0, true));
- boundaries.add(subOrientedPoint(-1.0, false));
- boundaries.add(subOrientedPoint(2.0, true));
- boundaries.add(subOrientedPoint(5.0, false));
- boundaries.add(subOrientedPoint(9.0, true));
- boundaries.add(subOrientedPoint(10.0, false));
-
- // act
- IntervalsSet set = new IntervalsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- EuclideanTestUtils.assertNegativeInfinity(set.getInf());
- EuclideanTestUtils.assertPositiveInfinity(set.getSup());
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(Double.NaN), set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(4, intervals.size());
- assertInterval(Double.NEGATIVE_INFINITY, -2.0, intervals.get(0), TEST_EPS);
- assertInterval(-1.0, 2.0, intervals.get(1), TEST_EPS);
- assertInterval(5.0, 9.0, intervals.get(2), TEST_EPS);
- assertInterval(10.0, Double.POSITIVE_INFINITY, intervals.get(3), TEST_EPS);
-
- assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
- assertLocation(Region.Location.INSIDE, set, -3);
- assertLocation(Region.Location.OUTSIDE, set, -1.5);
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.OUTSIDE, set, 3.0);
- assertLocation(Region.Location.INSIDE, set, 6.0);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
- assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
- assertLocation(Region.Location.OUTSIDE, set, 9.5);
- assertLocation(Region.Location.INSIDE, set, 11.0);
- assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
- }
-
- @Test
- public void testFromBoundaries_intervalEqualToEpsilon_onlyFirstBoundaryUsed() {
- // arrange
- double eps = 1e-3;
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
- double first = 1.0;
- double second = 1.0 + eps;
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
- boundaries.add(subOrientedPoint(first, true, precision));
- boundaries.add(subOrientedPoint(second, false, precision));
-
- // act
- IntervalsSet set = new IntervalsSet(boundaries, precision);
-
- // assert
- Assert.assertSame(precision, set.getPrecision());
- EuclideanTestUtils.assertNegativeInfinity(set.getInf());
- Assert.assertEquals(first, set.getSup(), TEST_EPS);
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(Double.NEGATIVE_INFINITY, first, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.INSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 1.0);
- assertLocation(Region.Location.OUTSIDE, set, 2.0);
- }
-
- @Test
- public void testFromBoundaries_intervalSmallerThanTolerance_onlyFirstBoundaryUsed() {
- // arrange
- double eps = 1e-3;
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
- double first = 1.0;
- double second = 1.0 - 1e-4;
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
- boundaries.add(subOrientedPoint(first, false, precision));
- boundaries.add(subOrientedPoint(second, true, precision));
-
- // act
- IntervalsSet set = new IntervalsSet(boundaries, precision);
-
- // assert
- Assert.assertSame(precision, set.getPrecision());
- Assert.assertEquals(first, set.getInf(), TEST_EPS);
- EuclideanTestUtils.assertPositiveInfinity(set.getSup());
- EuclideanTestUtils.assertPositiveInfinity(set.getSize());
- Assert.assertEquals(0.0, set.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector1D.NaN, set.getBarycenter(), TEST_EPS);
-
- List<Interval> intervals = set.asList();
- Assert.assertEquals(1, intervals.size());
- assertInterval(first, Double.POSITIVE_INFINITY, intervals.get(0), TEST_EPS);
-
- assertLocation(Region.Location.OUTSIDE, set, 0.0);
- assertLocation(Region.Location.BOUNDARY, set, 1.0);
- assertLocation(Region.Location.INSIDE, set, 2.0);
- }
-
- @Test
- public void testProjectToBoundary() {
- // arrange
- List<SubHyperplane<Vector1D>> boundaries = new ArrayList<>();
- boundaries.add(subOrientedPoint(-2.0, true));
- boundaries.add(subOrientedPoint(-1.0, false));
- boundaries.add(subOrientedPoint(2.0, true));
- boundaries.add(subOrientedPoint(5.0, false));
- boundaries.add(subOrientedPoint(9.0, true));
- boundaries.add(subOrientedPoint(10.0, false));
-
- IntervalsSet set = new IntervalsSet(boundaries, TEST_PRECISION);
-
- // act/assert
- assertProjection(Vector1D.of(-2), -1, set, Vector1D.of(-3));
- assertProjection(Vector1D.of(-2), 0, set, Vector1D.of(-2));
- assertProjection(Vector1D.of(-2), 0.1, set, Vector1D.of(-1.9));
-
- assertProjection(Vector1D.of(-1), 0.5, set, Vector1D.of(-1.5));
- assertProjection(Vector1D.of(-1), 0.1, set, Vector1D.of(-1.1));
- assertProjection(Vector1D.of(-1), 0, set, Vector1D.of(-1));
- assertProjection(Vector1D.of(-1), -1, set, Vector1D.of(0));
-
- assertProjection(Vector1D.of(2), -1, set, Vector1D.of(1));
- assertProjection(Vector1D.of(2), 0, set, Vector1D.of(2));
- assertProjection(Vector1D.of(2), 1, set, Vector1D.of(3));
-
- assertProjection(Vector1D.of(5), 1, set, Vector1D.of(4));
- assertProjection(Vector1D.of(5), 0, set, Vector1D.of(5));
-
- assertProjection(Vector1D.of(5), -1, set, Vector1D.of(6));
- assertProjection(Vector1D.of(5), -2, set, Vector1D.of(7));
-
- assertProjection(Vector1D.of(9), -1, set, Vector1D.of(8));
- assertProjection(Vector1D.of(9), 0, set, Vector1D.of(9));
- assertProjection(Vector1D.of(9), 0.1, set, Vector1D.of(9.1));
-
- assertProjection(Vector1D.of(10), 0, set, Vector1D.of(10));
- assertProjection(Vector1D.of(10), -1, set, Vector1D.of(11));
- }
-
- @Test
- public void testInterval() {
- IntervalsSet set = new IntervalsSet(2.3, 5.7, TEST_PRECISION);
- Assert.assertEquals(3.4, set.getSize(), 1.0e-10);
- Assert.assertEquals(4.0, set.getBarycenter().getX(), 1.0e-10);
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(Vector1D.of(2.3)));
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(Vector1D.of(5.7)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(Vector1D.of(1.2)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(Vector1D.of(8.7)));
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(Vector1D.of(3.0)));
- Assert.assertEquals(2.3, set.getInf(), 1.0e-10);
- Assert.assertEquals(5.7, set.getSup(), 1.0e-10);
- }
-
- @Test
- public void testInfinite() {
- IntervalsSet set = new IntervalsSet(9.0, Double.POSITIVE_INFINITY, TEST_PRECISION);
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(Vector1D.of(9.0)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(Vector1D.of(8.4)));
- for (double e = 1.0; e <= 6.0; e += 1.0) {
- Assert.assertEquals(Region.Location.INSIDE,
- set.checkPoint(Vector1D.of(Math.pow(10.0, e))));
- }
- Assert.assertTrue(Double.isInfinite(set.getSize()));
- Assert.assertEquals(9.0, set.getInf(), 1.0e-10);
- Assert.assertTrue(Double.isInfinite(set.getSup()));
-
- set = (IntervalsSet) new RegionFactory<Vector1D>().getComplement(set);
- Assert.assertEquals(9.0, set.getSup(), 1.0e-10);
- Assert.assertTrue(Double.isInfinite(set.getInf()));
-
- }
-
- @Test
- public void testBooleanOperations() {
- // arrange
- RegionFactory<Vector1D> factory = new RegionFactory<>();
-
- // act
- IntervalsSet set = (IntervalsSet)
- factory.intersection(factory.union(factory.difference(new IntervalsSet(1.0, 6.0, TEST_PRECISION),
- new IntervalsSet(3.0, 5.0, TEST_PRECISION)),
- new IntervalsSet(9.0, Double.POSITIVE_INFINITY, TEST_PRECISION)),
- new IntervalsSet(Double.NEGATIVE_INFINITY, 11.0, TEST_PRECISION));
-
- // arrange
- Assert.assertEquals(1.0, set.getInf(), TEST_EPS);
- Assert.assertEquals(11.0, set.getSup(), TEST_EPS);
-
- Assert.assertEquals(5.0, set.getSize(), TEST_EPS);
- Assert.assertEquals(5.9, set.getBarycenter().getX(), TEST_EPS);
-
- assertLocation(Region.Location.OUTSIDE, set, 0.0);
- assertLocation(Region.Location.OUTSIDE, set, 4.0);
- assertLocation(Region.Location.OUTSIDE, set, 8.0);
- assertLocation(Region.Location.OUTSIDE, set, 12.0);
- assertLocation(Region.Location.INSIDE, set, 1.2);
- assertLocation(Region.Location.INSIDE, set, 5.9);
- assertLocation(Region.Location.INSIDE, set, 9.01);
- assertLocation(Region.Location.BOUNDARY, set, 5.0);
- assertLocation(Region.Location.BOUNDARY, set, 11.0);
-
- List<Interval> list = set.asList();
- Assert.assertEquals(3, list.size());
- assertInterval(1.0, 3.0, list.get(0), TEST_EPS);
- assertInterval(5.0, 6.0, list.get(1), TEST_EPS);
- assertInterval(9.0, 11.0, list.get(2), TEST_EPS);
- }
-
- private void assertLocation(Region.Location location, IntervalsSet set, double pt) {
- Assert.assertEquals(location, set.checkPoint(Vector1D.of(pt)));
- }
-
- private void assertInterval(double expectedInf, double expectedSup, Interval actual, double tolerance) {
- Assert.assertEquals(expectedInf, actual.getInf(), tolerance);
- Assert.assertEquals(expectedSup, actual.getSup(), tolerance);
- }
-
- private void assertProjection(Vector1D expectedProjection, double expectedOffset,
- IntervalsSet set, Vector1D toProject) {
- BoundaryProjection<Vector1D> proj = set.projectToBoundary(toProject);
-
- EuclideanTestUtils.assertCoordinatesEqual(toProject, proj.getOriginal(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(expectedProjection, proj.getProjected(), TEST_EPS);
- Assert.assertEquals(expectedOffset, proj.getOffset(), TEST_EPS);
- }
-
- private SubOrientedPoint subOrientedPoint(double location, boolean direct) {
- return subOrientedPoint(location, direct, TEST_PRECISION);
- }
-
- private SubOrientedPoint subOrientedPoint(double location, boolean direct, DoublePrecisionContext precision) {
- // the remaining region isn't necessary for creating 1D boundaries so we can set it to null here
- return new SubOrientedPoint(OrientedPoint.fromPointAndDirection(Vector1D.of(location), direct, precision), null);
- }
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
index f0d2dff..d322ea9 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
@@ -16,11 +16,22 @@
*/
package org.apache.commons.geometry.euclidean.oned;
+import java.util.List;
+
import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane.Builder;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.OrientedPoint.SubOrientedPoint;
+import org.apache.commons.geometry.euclidean.oned.OrientedPoint.SubOrientedPointBuilder;
import org.apache.commons.numbers.core.Precision;
import org.junit.Assert;
import org.junit.Test;
@@ -82,6 +93,23 @@
}
@Test
+ public void testTransform_locationAtInfinity() {
+ // arrange
+ OrientedPoint pos = OrientedPoint.createNegativeFacing(Double.POSITIVE_INFINITY, TEST_PRECISION);
+ OrientedPoint neg = OrientedPoint.createPositiveFacing(Double.NEGATIVE_INFINITY, TEST_PRECISION);
+
+ Transform<Vector1D> scaleAndTranslate = AffineTransformMatrix1D.identity().scale(10.0).translate(5.0);
+ Transform<Vector1D> negate = FunctionTransform1D.from(Vector1D::negate);
+
+ // act/assert
+ assertOrientedPoint(pos.transform(scaleAndTranslate), Double.POSITIVE_INFINITY, false, TEST_PRECISION);
+ assertOrientedPoint(neg.transform(scaleAndTranslate), Double.NEGATIVE_INFINITY, true, TEST_PRECISION);
+
+ assertOrientedPoint(pos.transform(negate), Double.NEGATIVE_INFINITY, true, TEST_PRECISION);
+ assertOrientedPoint(neg.transform(negate), Double.POSITIVE_INFINITY, false, TEST_PRECISION);
+ }
+
+ @Test
public void testTransform_zeroScale() {
// arrange
AffineTransformMatrix1D zeroScale = AffineTransformMatrix1D.createScale(0.0);
@@ -95,78 +123,98 @@
}
@Test
- public void testCopySelf() {
- // arrange
- OrientedPoint orig = OrientedPoint.fromPointAndDirection(Vector1D.of(2.0), true, TEST_PRECISION);
-
- // act
- OrientedPoint copy = orig.copySelf();
-
- // assert
- Assert.assertSame(orig, copy);
- assertOrientedPoint(copy, 2.0, true, TEST_PRECISION);
- }
-
- @Test
- public void testGetOffset_positiveFacing() {
+ public void testOffset_positiveFacing() {
// arrange
OrientedPoint pt = OrientedPoint.fromPointAndDirection(Vector1D.of(-2.0), true, TEST_PRECISION);
// act/assert
- Assert.assertEquals(-98.0, pt.getOffset(Vector1D.of(-100)), Precision.EPSILON);
- Assert.assertEquals(-0.1, pt.getOffset(Vector1D.of(-2.1)), Precision.EPSILON);
- Assert.assertEquals(0.0, pt.getOffset(Vector1D.of(-2)), Precision.EPSILON);
- Assert.assertEquals(0.99, pt.getOffset(Vector1D.of(-1.01)), Precision.EPSILON);
- Assert.assertEquals(1.0, pt.getOffset(Vector1D.of(-1.0)), Precision.EPSILON);
- Assert.assertEquals(1.01, pt.getOffset(Vector1D.of(-0.99)), Precision.EPSILON);
- Assert.assertEquals(2.0, pt.getOffset(Vector1D.of(0)), Precision.EPSILON);
- Assert.assertEquals(102, pt.getOffset(Vector1D.of(100)), Precision.EPSILON);
+ Assert.assertEquals(-98.0, pt.offset(Vector1D.of(-100)), Precision.EPSILON);
+ Assert.assertEquals(-0.1, pt.offset(Vector1D.of(-2.1)), Precision.EPSILON);
+ Assert.assertEquals(0.0, pt.offset(Vector1D.of(-2)), Precision.EPSILON);
+ Assert.assertEquals(0.99, pt.offset(Vector1D.of(-1.01)), Precision.EPSILON);
+ Assert.assertEquals(1.0, pt.offset(Vector1D.of(-1.0)), Precision.EPSILON);
+ Assert.assertEquals(1.01, pt.offset(Vector1D.of(-0.99)), Precision.EPSILON);
+ Assert.assertEquals(2.0, pt.offset(Vector1D.of(0)), Precision.EPSILON);
+ Assert.assertEquals(102, pt.offset(Vector1D.of(100)), Precision.EPSILON);
}
@Test
- public void testGetOffset_negativeFacing() {
+ public void testOffset_negativeFacing() {
// arrange
OrientedPoint pt = OrientedPoint.fromPointAndDirection(Vector1D.of(-2.0), false, TEST_PRECISION);
// act/assert
- Assert.assertEquals(98.0, pt.getOffset(Vector1D.of(-100)), Precision.EPSILON);
- Assert.assertEquals(0.1, pt.getOffset(Vector1D.of(-2.1)), Precision.EPSILON);
- Assert.assertEquals(0.0, pt.getOffset(Vector1D.of(-2)), Precision.EPSILON);
- Assert.assertEquals(-0.99, pt.getOffset(Vector1D.of(-1.01)), Precision.EPSILON);
- Assert.assertEquals(-1.0, pt.getOffset(Vector1D.of(-1.0)), Precision.EPSILON);
- Assert.assertEquals(-1.01, pt.getOffset(Vector1D.of(-0.99)), Precision.EPSILON);
- Assert.assertEquals(-2, pt.getOffset(Vector1D.of(0)), Precision.EPSILON);
- Assert.assertEquals(-102, pt.getOffset(Vector1D.of(100)), Precision.EPSILON);
+ Assert.assertEquals(98.0, pt.offset(Vector1D.of(-100)), Precision.EPSILON);
+ Assert.assertEquals(0.1, pt.offset(Vector1D.of(-2.1)), Precision.EPSILON);
+ Assert.assertEquals(0.0, pt.offset(Vector1D.of(-2)), Precision.EPSILON);
+ Assert.assertEquals(-0.99, pt.offset(Vector1D.of(-1.01)), Precision.EPSILON);
+ Assert.assertEquals(-1.0, pt.offset(Vector1D.of(-1.0)), Precision.EPSILON);
+ Assert.assertEquals(-1.01, pt.offset(Vector1D.of(-0.99)), Precision.EPSILON);
+ Assert.assertEquals(-2, pt.offset(Vector1D.of(0)), Precision.EPSILON);
+ Assert.assertEquals(-102, pt.offset(Vector1D.of(100)), Precision.EPSILON);
}
@Test
- public void testWholeHyperplane() {
+ public void testOffset_infinityArguments() {
+ // arrange
+ OrientedPoint pt = OrientedPoint.fromPointAndDirection(Vector1D.of(-2.0), true, TEST_PRECISION);
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(pt.offset(Vector1D.of(Double.POSITIVE_INFINITY)));
+ GeometryTestUtils.assertNegativeInfinity(pt.offset(Vector1D.of(Double.NEGATIVE_INFINITY)));
+ }
+
+ @Test
+ public void testOffset_infinityLocation() {
+ // arrange
+ OrientedPoint pt = OrientedPoint.fromPointAndDirection(Vector1D.of(Double.POSITIVE_INFINITY), true, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(Double.isNaN(pt.offset(Vector1D.of(Double.POSITIVE_INFINITY))));
+ GeometryTestUtils.assertNegativeInfinity(pt.offset(Vector1D.of(Double.NEGATIVE_INFINITY)));
+
+ GeometryTestUtils.assertNegativeInfinity(pt.offset(Vector1D.of(0)));
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ DoublePrecisionContext smallPrecision = new EpsilonDoublePrecisionContext(1e-10);
+ DoublePrecisionContext largePrecision = new EpsilonDoublePrecisionContext(1e-1);
+
+ OrientedPoint smallPosFacing = OrientedPoint.fromLocationAndDirection(1.0, true, smallPrecision);
+ OrientedPoint largeNegFacing = OrientedPoint.fromLocationAndDirection(1.0, false, largePrecision);
+
+ // act/assert
+ assertClassify(HyperplaneLocation.MINUS, smallPosFacing,
+ Double.NEGATIVE_INFINITY, -10, 0, 0.9, 0.99999, 1 - 1e-9);
+ assertClassify(HyperplaneLocation.ON, smallPosFacing,
+ 1 - 1e-11, 1, 1 + 1e-11);
+ assertClassify(HyperplaneLocation.PLUS, smallPosFacing,
+ 1 + 1e-9, 2, 10, Double.POSITIVE_INFINITY);
+
+ assertClassify(HyperplaneLocation.PLUS, largeNegFacing,
+ Double.NEGATIVE_INFINITY, -10, 0, 0.89);
+ assertClassify(HyperplaneLocation.ON, largeNegFacing,
+ 0.91, 0.9999, 1, 1.001, 1.09);
+ assertClassify(HyperplaneLocation.MINUS, largeNegFacing,
+ 1.11, 2, 10, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testSpan() {
// arrange
OrientedPoint pt = OrientedPoint.fromPointAndDirection(Vector1D.of(1.0), false, TEST_PRECISION);
// act
- SubOrientedPoint subPt = pt.wholeHyperplane();
+ SubOrientedPoint result = pt.span();
// assert
- Assert.assertSame(pt, subPt.getHyperplane());
- Assert.assertNull(subPt.getRemainingRegion());
+ Assert.assertSame(pt, result.getHyperplane());
}
@Test
- public void testWholeSpace() {
- // arrange
- OrientedPoint pt = OrientedPoint.fromPointAndDirection(Vector1D.of(1.0), false, TEST_PRECISION);
-
- // act
- IntervalsSet set = pt.wholeSpace();
-
- // assert
- EuclideanTestUtils.assertNegativeInfinity(set.getInf());
- EuclideanTestUtils.assertPositiveInfinity(set.getSup());
- }
-
- @Test
- public void testSameOrientationAs() {
+ public void testSimilarOrientation() {
// arrange
OrientedPoint negativeDir1 = OrientedPoint.fromPointAndDirection(Vector1D.of(1.0), false, TEST_PRECISION);
OrientedPoint negativeDir2 = OrientedPoint.fromPointAndDirection(Vector1D.of(-1.0), false, TEST_PRECISION);
@@ -174,16 +222,16 @@
OrientedPoint positiveDir2 = OrientedPoint.fromPointAndDirection(Vector1D.of(-2.0), true, TEST_PRECISION);
// act/assert
- Assert.assertTrue(negativeDir1.sameOrientationAs(negativeDir1));
- Assert.assertTrue(negativeDir1.sameOrientationAs(negativeDir2));
- Assert.assertTrue(negativeDir2.sameOrientationAs(negativeDir1));
+ Assert.assertTrue(negativeDir1.similarOrientation(negativeDir1));
+ Assert.assertTrue(negativeDir1.similarOrientation(negativeDir2));
+ Assert.assertTrue(negativeDir2.similarOrientation(negativeDir1));
- Assert.assertTrue(positiveDir1.sameOrientationAs(positiveDir1));
- Assert.assertTrue(positiveDir1.sameOrientationAs(positiveDir2));
- Assert.assertTrue(positiveDir2.sameOrientationAs(positiveDir1));
+ Assert.assertTrue(positiveDir1.similarOrientation(positiveDir1));
+ Assert.assertTrue(positiveDir1.similarOrientation(positiveDir2));
+ Assert.assertTrue(positiveDir2.similarOrientation(positiveDir1));
- Assert.assertFalse(negativeDir1.sameOrientationAs(positiveDir1));
- Assert.assertFalse(positiveDir1.sameOrientationAs(negativeDir1));
+ Assert.assertFalse(negativeDir1.similarOrientation(positiveDir1));
+ Assert.assertFalse(positiveDir1.similarOrientation(negativeDir1));
}
@Test
@@ -198,6 +246,31 @@
Assert.assertEquals(1.0, pt.project(Vector1D.of(100.0)).getX(), Precision.EPSILON);
}
+
+ @Test
+ public void testEq() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ OrientedPoint a = OrientedPoint.createPositiveFacing(0, precision);
+
+ OrientedPoint b = OrientedPoint.createPositiveFacing(0, TEST_PRECISION);
+ OrientedPoint c = OrientedPoint.createNegativeFacing(0, precision);
+ OrientedPoint d = OrientedPoint.createPositiveFacing(2e-3, precision);
+
+ OrientedPoint e = OrientedPoint.createPositiveFacing(1e-4, precision);
+
+ // act/assert
+ Assert.assertTrue(a.eq(a));
+
+ Assert.assertFalse(a.eq(b));
+ Assert.assertFalse(a.eq(c));
+ Assert.assertFalse(a.eq(d));
+
+ Assert.assertTrue(a.eq(e));
+ Assert.assertTrue(e.eq(a));
+ }
+
@Test
public void testHashCode() {
// arrange
@@ -262,50 +335,35 @@
// assert
Assert.assertTrue(str.contains("OrientedPoint"));
- Assert.assertTrue(str.contains("location= (2.0)"));
+ Assert.assertTrue(str.contains("point= (2.0)"));
Assert.assertTrue(str.contains("direction= (1.0)"));
}
@Test
- public void testFromPointAndDirection_trueBooleanArg() {
- // act
- OrientedPoint pt = OrientedPoint.fromPointAndDirection(Vector1D.of(2.0), true, TEST_PRECISION);
-
- // assert
- assertOrientedPoint(pt, 2.0, true, TEST_PRECISION);
- Assert.assertEquals(1.0, pt.getDirection().getX(), Precision.EPSILON);
+ public void testFromLocationAndDirection() {
+ // act/assert
+ assertOrientedPoint(OrientedPoint.fromLocationAndDirection(3.0, true, TEST_PRECISION),
+ 3.0, true, TEST_PRECISION);
+ assertOrientedPoint(OrientedPoint.fromLocationAndDirection(2.0, false, TEST_PRECISION),
+ 2.0, false, TEST_PRECISION);
}
@Test
- public void testFromPointAndDirection_falseBooleanArg() {
- // act
- OrientedPoint pt = OrientedPoint.fromPointAndDirection(Vector1D.of(2.0), false, TEST_PRECISION);
-
- // assert
- assertOrientedPoint(pt, 2.0, false, TEST_PRECISION);
- Assert.assertEquals(-1.0, pt.getDirection().getX(), Precision.EPSILON);
+ public void testFromPointAndDirection_pointAndBooleanArgs() {
+ // act/assert
+ assertOrientedPoint(OrientedPoint.fromPointAndDirection(Vector1D.of(3.0), true, TEST_PRECISION),
+ 3.0, true, TEST_PRECISION);
+ assertOrientedPoint(OrientedPoint.fromPointAndDirection(Vector1D.of(2.0), false, TEST_PRECISION),
+ 2.0, false, TEST_PRECISION);
}
@Test
- public void testFromPointAndDirection_positiveVectorArg() {
- // act
- OrientedPoint pt = OrientedPoint.fromPointAndDirection(
- Vector1D.of(-2.0), Vector1D.of(0.1), TEST_PRECISION);
-
- // assert
- assertOrientedPoint(pt, -2.0, true, TEST_PRECISION);
- Assert.assertEquals(1.0, pt.getDirection().getX(), Precision.EPSILON);
- }
-
- @Test
- public void testFromPointAndDirection_negativeVectorArg() {
- // act
- OrientedPoint pt = OrientedPoint.fromPointAndDirection(
- Vector1D.of(2.0), Vector1D.of(-10.1), TEST_PRECISION);
-
- // assert
- assertOrientedPoint(pt, 2.0, false, TEST_PRECISION);
- Assert.assertEquals(-1.0, pt.getDirection().getX(), Precision.EPSILON);
+ public void testFromPointAndDirection_pointAndVectorArgs() {
+ // act/assert
+ assertOrientedPoint(OrientedPoint.fromPointAndDirection(Vector1D.of(-2.0), Vector1D.of(0.1), TEST_PRECISION),
+ -2.0, true, TEST_PRECISION);
+ assertOrientedPoint(OrientedPoint.fromPointAndDirection(Vector1D.of(2.0), Vector1D.of(-10.1), TEST_PRECISION),
+ 2.0, false, TEST_PRECISION);
}
@Test
@@ -320,35 +378,257 @@
GeometryTestUtils.assertThrows(
() -> OrientedPoint.fromPointAndDirection(Vector1D.of(2.0), Vector1D.of(-0.09), precision),
GeometryValueException.class, "Oriented point direction cannot be zero");
- ;
}
@Test
public void testCreatePositiveFacing() {
- // act
- OrientedPoint pt = OrientedPoint.createPositiveFacing(
- Vector1D.of(-2.0), TEST_PRECISION);
-
- // assert
- assertOrientedPoint(pt, -2.0, true, TEST_PRECISION);
- Assert.assertEquals(1.0, pt.getDirection().getX(), Precision.EPSILON);
+ // act/assert
+ assertOrientedPoint(OrientedPoint.createPositiveFacing(Vector1D.of(-2.0), TEST_PRECISION),
+ -2.0, true, TEST_PRECISION);
+ assertOrientedPoint(OrientedPoint.createPositiveFacing(-4.0, TEST_PRECISION),
+ -4.0, true, TEST_PRECISION);
}
@Test
public void testCreateNegativeFacing() {
- // act
- OrientedPoint pt = OrientedPoint.createNegativeFacing(
- Vector1D.of(2.0), TEST_PRECISION);
-
- // assert
- assertOrientedPoint(pt, 2.0, false, TEST_PRECISION);
- Assert.assertEquals(-1.0, pt.getDirection().getX(), Precision.EPSILON);
+ // act/assert
+ assertOrientedPoint(OrientedPoint.createNegativeFacing(Vector1D.of(2.0), TEST_PRECISION),
+ 2.0, false, TEST_PRECISION);
+ assertOrientedPoint(OrientedPoint.createNegativeFacing(4, TEST_PRECISION),
+ 4.0, false, TEST_PRECISION);
}
- private static void assertOrientedPoint(OrientedPoint pt, double location,
- boolean positiveFacing, DoublePrecisionContext precision) {
- Assert.assertEquals(location, pt.getLocation().getX(), TEST_EPS);
+ @Test
+ public void testSubHyperplane_split() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(-1.5, precision);
+ SubOrientedPoint sub = pt.span();
+
+ // act/assert
+ checkSplit(sub, OrientedPoint.createPositiveFacing(1.0, precision), true, false);
+ checkSplit(sub, OrientedPoint.createPositiveFacing(-1.5 + 1e-2, precision), true, false);
+
+ checkSplit(sub, OrientedPoint.createNegativeFacing(1.0, precision), false, true);
+ checkSplit(sub, OrientedPoint.createNegativeFacing(-1.5 + 1e-2, precision), false, true);
+
+ checkSplit(sub, OrientedPoint.createNegativeFacing(-1.5, precision), false, false);
+ checkSplit(sub, OrientedPoint.createNegativeFacing(-1.5 + 1e-4, precision), false, false);
+ checkSplit(sub, OrientedPoint.createNegativeFacing(-1.5 - 1e-4, precision), false, false);
+ }
+
+ private void checkSplit(SubOrientedPoint sub, OrientedPoint splitter, boolean minus, boolean plus) {
+ Split<SubOrientedPoint> split = sub.split(splitter);
+
+ Assert.assertSame(minus ? sub : null, split.getMinus());
+ Assert.assertSame(plus ? sub : null, split.getPlus());
+ }
+
+ @Test
+ public void testSubHyperplane_simpleMethods() {
+ // arrange
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(0, TEST_PRECISION);
+ SubOrientedPoint sub = pt.span();
+
+ // act/assert
+ Assert.assertSame(pt, sub.getHyperplane());
+ Assert.assertFalse(sub.isFull());
+ Assert.assertFalse(sub.isEmpty());
+ Assert.assertFalse(sub.isInfinite());
+ Assert.assertTrue(sub.isFinite());
+ Assert.assertEquals(0.0, sub.getSize(), TEST_EPS);
+
+ List<SubOrientedPoint> list = sub.toConvex();
+ Assert.assertEquals(1, list.size());
+ Assert.assertSame(sub, list.get(0));
+ }
+
+ @Test
+ public void testSubHyperplane_classify() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(1, precision);
+ SubOrientedPoint sub = pt.span();
+
+ // act/assert
+ Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(Vector1D.of(0.95)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(Vector1D.of(1)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(Vector1D.of(1.05)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Vector1D.of(1.11)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Vector1D.of(0.89)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Vector1D.of(-3)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Vector1D.of(10)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Vector1D.NEGATIVE_INFINITY));
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Vector1D.POSITIVE_INFINITY));
+ }
+
+ @Test
+ public void testSubHyperplane_contains() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(1, precision);
+ SubOrientedPoint sub = pt.span();
+
+ // act/assert
+ Assert.assertTrue(sub.contains(Vector1D.of(0.95)));
+ Assert.assertTrue(sub.contains(Vector1D.of(1)));
+ Assert.assertTrue(sub.contains(Vector1D.of(1.05)));
+
+ Assert.assertFalse(sub.contains(Vector1D.of(1.11)));
+ Assert.assertFalse(sub.contains(Vector1D.of(0.89)));
+
+ Assert.assertFalse(sub.contains(Vector1D.of(-3)));
+ Assert.assertFalse(sub.contains(Vector1D.of(10)));
+
+ Assert.assertFalse(sub.contains(Vector1D.NEGATIVE_INFINITY));
+ Assert.assertFalse(sub.contains(Vector1D.POSITIVE_INFINITY));
+ }
+
+ @Test
+ public void testSubHyperplane_closestContained() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(1, precision);
+ SubOrientedPoint sub = pt.span();
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), sub.closest(Vector1D.NEGATIVE_INFINITY), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), sub.closest(Vector1D.of(0)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), sub.closest(Vector1D.of(1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), sub.closest(Vector1D.of(2)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), sub.closest(Vector1D.POSITIVE_INFINITY), TEST_EPS);
+ }
+
+ @Test
+ public void testSubHyperplane_transform() {
+ // arrange
+ AffineTransformMatrix1D scaleAndTranslate = AffineTransformMatrix1D
+ .createScale(0.5)
+ .translate(-10);
+
+ AffineTransformMatrix1D reflect = AffineTransformMatrix1D.createScale(-2);
+
+ SubOrientedPoint a = OrientedPoint.createPositiveFacing(Vector1D.of(2.0), TEST_PRECISION).span();
+ SubOrientedPoint b = OrientedPoint.createNegativeFacing(Vector1D.of(-3.0), TEST_PRECISION).span();
+
+ // act/assert
+ assertOrientedPoint(a.transform(scaleAndTranslate).getHyperplane(), -9.0, true, TEST_PRECISION);
+ assertOrientedPoint(b.transform(scaleAndTranslate).getHyperplane(), -11.5, false, TEST_PRECISION);
+
+ assertOrientedPoint(a.transform(reflect).getHyperplane(), -4.0, false, TEST_PRECISION);
+ assertOrientedPoint(b.transform(reflect).getHyperplane(), 6.0, true, TEST_PRECISION);
+ }
+
+ @Test
+ public void testSubHyperplane_reverse() {
+ // arrange
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(2.0, TEST_PRECISION);
+ SubOrientedPoint sub = pt.span();
+
+ // act
+ SubOrientedPoint result = sub.reverse();
+
+ // assert
+ Assert.assertEquals(2.0, result.getHyperplane().getLocation(), TEST_EPS);
+ Assert.assertFalse(result.getHyperplane().isPositiveFacing());
+
+ Assert.assertEquals(sub.getHyperplane(), result.reverse().getHyperplane());
+ }
+
+ @Test
+ public void testSubHyperplane_toString() {
+ // arrange
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(2, TEST_PRECISION);
+ SubOrientedPoint sub = pt.span();
+
+ // act
+ String str = sub.toString();
+
+ //assert
+ Assert.assertTrue(str.contains("SubOrientedPoint"));
+ Assert.assertTrue(str.contains("point= (2.0)"));
+ Assert.assertTrue(str.contains("direction= (1.0)"));
+ }
+
+ @Test
+ public void testBuilder() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(0, precision);
+ SubOrientedPoint sub = pt.span();
+
+ // act
+ Builder<Vector1D> builder = sub.builder();
+
+ builder.add(sub);
+ builder.add(OrientedPoint.createPositiveFacing(1e-4, precision).span());
+ builder.add((SubHyperplane<Vector1D>) sub);
+
+ SubHyperplane<Vector1D> result = builder.build();
+
+ // assert
+ Assert.assertSame(sub, result);
+ }
+
+ @Test
+ public void testBuilder_invalidArgs() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(0, precision);
+ SubOrientedPoint sub = pt.span();
+
+ Builder<Vector1D> builder = sub.builder();
+
+ // act/assert
+ GeometryTestUtils.assertThrows(
+ () -> builder.add(OrientedPoint.createPositiveFacing(2e-3, precision).span()),
+ GeometryException.class);
+ GeometryTestUtils.assertThrows(
+ () -> builder.add(OrientedPoint.createNegativeFacing(2e-3, precision).span()),
+ GeometryException.class);
+
+ GeometryTestUtils.assertThrows(
+ () -> builder.add((SubHyperplane<Vector1D>) OrientedPoint.createPositiveFacing(2e-3, precision).span()),
+ GeometryException.class);
+ }
+
+ @Test
+ public void testBuilder_toString() {
+ // arrange
+ OrientedPoint pt = OrientedPoint.createPositiveFacing(2, TEST_PRECISION);
+ SubOrientedPointBuilder builder = pt.span().builder();
+
+ // act
+ String str = builder.toString();
+
+ //assert
+ Assert.assertTrue(str.contains("SubOrientedPointBuilder"));
+ Assert.assertTrue(str.contains("base= SubOrientedPoint"));
+ Assert.assertTrue(str.contains("point= (2.0)"));
+ Assert.assertTrue(str.contains("direction= (1.0)"));
+ }
+
+ private static void assertOrientedPoint(OrientedPoint pt, double location, boolean positiveFacing,
+ DoublePrecisionContext precision) {
+ Assert.assertEquals(location, pt.getPoint().getX(), TEST_EPS);
+ Assert.assertEquals(location, pt.getLocation(), TEST_EPS);
+ Assert.assertEquals(positiveFacing ? 1.0 : -1.0, pt.getDirection().getX(), TEST_EPS);
Assert.assertEquals(positiveFacing, pt.isPositiveFacing());
Assert.assertSame(precision, pt.getPrecision());
}
+
+ private static void assertClassify(HyperplaneLocation expected, OrientedPoint pt, double ... locations) {
+ for (double location : locations) {
+ String msg = "Unexpected classification for location " + location;
+
+ Assert.assertEquals(msg, expected, pt.classify(location));
+ Assert.assertEquals(msg, expected, pt.classify(Vector1D.of(location)));
+ }
+ }
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1DTest.java
new file mode 100644
index 0000000..25804c4
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1DTest.java
@@ -0,0 +1,1231 @@
+/*
+ * 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.oned;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D.RegionNode1D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RegionBSPTree1DTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testCopy() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D(true);
+ tree.getRoot().cut(OrientedPoint.createPositiveFacing(1.0, TEST_PRECISION));
+
+ // act
+ RegionBSPTree1D copy = tree.copy();
+
+ // assert
+ Assert.assertNotSame(tree, copy);
+ Assert.assertEquals(3, copy.count());
+ }
+
+ @Test
+ public void testClassify_fullRegion() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D(true);
+
+ // act/assert
+ checkClassify(tree, RegionLocation.INSIDE,
+ Double.NEGATIVE_INFINITY, -1, 0, 1, Double.POSITIVE_INFINITY);
+
+ checkClassify(tree, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void testClassify_emptyRegion() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+
+ // act/assert
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Double.NEGATIVE_INFINITY, -1, 0, 1, Double.POSITIVE_INFINITY);
+
+ checkClassify(tree, RegionLocation.OUTSIDE, Double.NaN);
+ }
+
+ @Test
+ public void testClassify_singleClosedInterval() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D();
+ tree.insert(Arrays.asList(
+ OrientedPoint.createNegativeFacing(Vector1D.of(-1), TEST_PRECISION).span(),
+ OrientedPoint.createPositiveFacing(Vector1D.of(9), TEST_PRECISION).span()
+ ));
+
+ // act/assert
+ checkClassify(tree, RegionLocation.OUTSIDE, Double.NEGATIVE_INFINITY);
+ checkClassify(tree, RegionLocation.OUTSIDE,-2.0);
+ checkClassify(tree, RegionLocation.INSIDE, 0.0);
+ checkClassify(tree, RegionLocation.BOUNDARY, 9.0 - 1e-16);
+ checkClassify(tree, RegionLocation.BOUNDARY, 9.0 + 1e-16);
+ checkClassify(tree, RegionLocation.OUTSIDE, 10.0);
+ checkClassify(tree, RegionLocation.OUTSIDE, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testContains_fullRegion() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D(true);
+
+ // act/assert
+ checkContains(tree, true,
+ Double.NEGATIVE_INFINITY, -1, 0, 1, Double.POSITIVE_INFINITY);
+
+ checkContains(tree, false, Double.NaN);
+ }
+
+ @Test
+ public void testContains_emptyRegion() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+
+ // act/assert
+ checkContains(tree, false,
+ Double.NEGATIVE_INFINITY, -1, 0, 1, Double.POSITIVE_INFINITY);
+
+ checkContains(tree, false, Double.NaN);
+ }
+
+ @Test
+ public void testContains_singleClosedInterval() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D();
+ tree.insert(Arrays.asList(
+ OrientedPoint.createNegativeFacing(Vector1D.of(-1), TEST_PRECISION).span(),
+ OrientedPoint.createPositiveFacing(Vector1D.of(9), TEST_PRECISION).span()
+ ));
+
+ // act/assert
+ checkContains(tree, false, Double.NEGATIVE_INFINITY);
+ checkContains(tree, false,-2.0);
+ checkContains(tree, true, 0.0);
+ checkContains(tree, true, 9.0 - 1e-16);
+ checkContains(tree, true, 9.0 + 1e-16);
+ checkContains(tree, false, 10.0);
+ checkContains(tree, false, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testGetBoundarySize_alwaysReturnsZero() {
+ // act/assert
+ Assert.assertEquals(0.0, RegionBSPTree1D.full().getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.0, RegionBSPTree1D.empty().getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.0, RegionBSPTree1D.from(
+ Interval.of(1, 2, TEST_PRECISION),
+ Interval.of(4, 5, TEST_PRECISION)
+ ).getBoundarySize(), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_full() {
+ // arrange
+ RegionBSPTree1D full = RegionBSPTree1D.full();
+
+ // act/assert
+ Assert.assertNull(full.project(Vector1D.of(Double.NEGATIVE_INFINITY)));
+ Assert.assertNull(full.project(Vector1D.of(0)));
+ Assert.assertNull(full.project(Vector1D.of(Double.POSITIVE_INFINITY)));
+ }
+
+ @Test
+ public void testProject_empty() {
+ // arrange
+ RegionBSPTree1D empty = RegionBSPTree1D.empty();
+
+ // act/assert
+ Assert.assertNull(empty.project(Vector1D.of(Double.NEGATIVE_INFINITY)));
+ Assert.assertNull(empty.project(Vector1D.of(0)));
+ Assert.assertNull(empty.project(Vector1D.of(Double.POSITIVE_INFINITY)));
+ }
+
+ @Test
+ public void testProject_singlePoint() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Interval.point(1, TEST_PRECISION));
+
+ // act/assert
+ checkBoundaryProjection(tree, -1, 1);
+ checkBoundaryProjection(tree, 0, 1);
+
+ checkBoundaryProjection(tree, 1, 1);
+
+ checkBoundaryProjection(tree, 2, 1);
+ checkBoundaryProjection(tree, 3, 1);
+
+ checkBoundaryProjection(tree, Double.NEGATIVE_INFINITY, 1);
+ checkBoundaryProjection(tree, Double.POSITIVE_INFINITY, 1);
+ }
+
+ @Test
+ public void testProject_noMinBoundary() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Interval.of(Double.NEGATIVE_INFINITY, 1, TEST_PRECISION));
+
+ // act/assert
+ checkBoundaryProjection(tree, -1, 1);
+ checkBoundaryProjection(tree, 0, 1);
+ checkBoundaryProjection(tree, 1, 1);
+ checkBoundaryProjection(tree, 2, 1);
+ checkBoundaryProjection(tree, 3, 1);
+
+ checkBoundaryProjection(tree, Double.NEGATIVE_INFINITY, 1);
+ checkBoundaryProjection(tree, Double.POSITIVE_INFINITY, 1);
+ }
+
+ @Test
+ public void testProject_noMaxBoundary() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Interval.of(1, Double.POSITIVE_INFINITY, TEST_PRECISION));
+
+ // act/assert
+ checkBoundaryProjection(tree, -1, 1);
+ checkBoundaryProjection(tree, 0, 1);
+ checkBoundaryProjection(tree, 1, 1);
+ checkBoundaryProjection(tree, 2, 1);
+ checkBoundaryProjection(tree, 3, 1);
+
+ checkBoundaryProjection(tree, Double.NEGATIVE_INFINITY, 1);
+ checkBoundaryProjection(tree, Double.POSITIVE_INFINITY, 1);
+ }
+
+ @Test
+ public void testProject_closedInterval() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Interval.of(1, 3, TEST_PRECISION));
+
+ // act/assert
+ checkBoundaryProjection(tree, -1, 1);
+ checkBoundaryProjection(tree, 0, 1);
+ checkBoundaryProjection(tree, 1, 1);
+
+ checkBoundaryProjection(tree, 1.9, 1);
+ checkBoundaryProjection(tree, 2, 1);
+ checkBoundaryProjection(tree, 2.1, 3);
+
+ checkBoundaryProjection(tree, 3, 3);
+ checkBoundaryProjection(tree, 4, 3);
+ checkBoundaryProjection(tree, 5, 3);
+
+ checkBoundaryProjection(tree, Double.NEGATIVE_INFINITY, 1);
+ checkBoundaryProjection(tree, Double.POSITIVE_INFINITY, 3);
+ }
+
+ @Test
+ public void testProject_multipleIntervals() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(
+ Interval.max(-1, TEST_PRECISION),
+ Interval.point(1, TEST_PRECISION),
+ Interval.of(2, 3, TEST_PRECISION),
+ Interval.of(5, 6, TEST_PRECISION)
+ );
+
+ // act/assert
+ checkBoundaryProjection(tree, Double.NEGATIVE_INFINITY, -1);
+ checkBoundaryProjection(tree, -2, -1);
+ checkBoundaryProjection(tree, -1, -1);
+
+ checkBoundaryProjection(tree, -0.5, -1);
+ checkBoundaryProjection(tree, 0, -1);
+ checkBoundaryProjection(tree, 0.5, 1);
+
+ checkBoundaryProjection(tree, 0.9, 1);
+ checkBoundaryProjection(tree, 1, 1);
+ checkBoundaryProjection(tree, 1.1, 1);
+
+ checkBoundaryProjection(tree, 0.5, 1);
+
+ checkBoundaryProjection(tree, 1.9, 2);
+ checkBoundaryProjection(tree, 2, 2);
+ checkBoundaryProjection(tree, 2.1, 2);
+ checkBoundaryProjection(tree, 2.5, 2);
+ checkBoundaryProjection(tree, 2.9, 3);
+ checkBoundaryProjection(tree, 3, 3);
+ checkBoundaryProjection(tree, 3.1, 3);
+
+ checkBoundaryProjection(tree, 3.9, 3);
+ checkBoundaryProjection(tree, 4, 3);
+ checkBoundaryProjection(tree, 4.1, 5);
+
+ checkBoundaryProjection(tree, 4.9, 5);
+ checkBoundaryProjection(tree, 5, 5);
+ checkBoundaryProjection(tree, 5.1, 5);
+ checkBoundaryProjection(tree, 5.49, 5);
+ checkBoundaryProjection(tree, 5.5, 5);
+ checkBoundaryProjection(tree, 5.51, 6);
+ checkBoundaryProjection(tree, 5.9, 6);
+ checkBoundaryProjection(tree, 6, 6);
+ checkBoundaryProjection(tree, 6.1, 6);
+
+ checkBoundaryProjection(tree, 7, 6);
+
+ checkBoundaryProjection(tree, Double.POSITIVE_INFINITY, 6);
+ }
+
+ @Test
+ public void testAdd_interval() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // act
+ tree.add(Interval.of(Double.NEGATIVE_INFINITY, -10, TEST_PRECISION));
+ tree.add(Interval.of(-1, 1, TEST_PRECISION));
+ tree.add(Interval.of(10, Double.POSITIVE_INFINITY, TEST_PRECISION));
+
+ // assert
+ checkClassify(tree, RegionLocation.INSIDE,
+ Double.NEGATIVE_INFINITY, -11, 0, 11, Double.POSITIVE_INFINITY);
+
+ checkClassify(tree, RegionLocation.BOUNDARY, -10, -1, 1, 10);
+
+ checkClassify(tree, RegionLocation.OUTSIDE, -9, -2, 2, 9);
+ }
+
+ @Test
+ public void testAdd_adjacentIntervals() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // act
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(2, 3, TEST_PRECISION));
+
+ // assert
+ checkClassify(tree, RegionLocation.INSIDE, 1.1, 2, 2.9);
+
+ checkClassify(tree, RegionLocation.BOUNDARY, 1, 3);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Double.NEGATIVE_INFINITY, 0, 0.9, 3.1, 4, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testAdd_addFullInterval() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // act
+ tree.add(Interval.of(-1, 1, TEST_PRECISION));
+ tree.add(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION));
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testAdd_interval_duplicateBoundaryPoint() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+
+ // act
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(2, 3, TEST_PRECISION));
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(0, 1, TEST_PRECISION));
+
+ // assert
+ checkClassify(tree, RegionLocation.INSIDE, 0.1, 1, 2, 2.9);
+
+ checkClassify(tree, RegionLocation.BOUNDARY, 0, 3);
+
+ checkClassify(tree, RegionLocation.OUTSIDE, -1, -0.1, 3.1, 4);
+ }
+
+ @Test
+ public void testToIntervals_fullRegion() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D(true);
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+ checkInterval(intervals.get(0), Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testToIntervals_emptyRegion() {
+ // arrange
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(0, intervals.size());
+ }
+
+ @Test
+ public void testToIntervals_halfOpen_negative() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D();
+ tree.getRoot().cut(OrientedPoint.fromLocationAndDirection(1.0, true, precision));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+ checkInterval(intervals.get(0), Double.NEGATIVE_INFINITY, 1);
+ }
+
+ @Test
+ public void testToIntervals_halfOpen_positive() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D();
+ tree.getRoot().cut(OrientedPoint.fromLocationAndDirection(-1.0, false, precision));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+ checkInterval(intervals.get(0), -1, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testToIntervals_singleClosedInterval() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+ tree.add(Interval.of(-1, 1, precision));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+ checkInterval(intervals.get(0), -1, 1);
+ }
+
+ @Test
+ public void testToIntervals_singleClosedInterval_complement() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+ tree.add(Interval.of(-1, 1, precision));
+ tree.complement();
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(2, intervals.size());
+ checkInterval(intervals.get(0), Double.NEGATIVE_INFINITY, -1);
+ checkInterval(intervals.get(1), 1, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testToIntervals_openAndClosedIntervals() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+ tree.add(Interval.of(Double.NEGATIVE_INFINITY, -10, precision));
+ tree.add(Interval.of(-1, 1, precision));
+ tree.add(Interval.of(10, Double.POSITIVE_INFINITY, precision));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(3, intervals.size());
+ checkInterval(intervals.get(0), Double.NEGATIVE_INFINITY, -10);
+ checkInterval(intervals.get(1), -1, 1);
+ checkInterval(intervals.get(2), 10, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testToIntervals_singlePoint() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+ tree.add(Interval.of(1, 1, precision));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+ checkInterval(intervals.get(0), 1, 1);
+ }
+
+ @Test
+ public void testToIntervals_singlePoint_complement() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+ tree.add(Interval.of(1, 1, precision));
+ tree.complement();
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(2, intervals.size());
+ checkInterval(intervals.get(0), Double.NEGATIVE_INFINITY, 1);
+ checkInterval(intervals.get(1), 1, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testToIntervals_multiplePoints() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+ tree.add(Interval.of(1, 1, precision));
+ tree.add(Interval.of(2, 2, precision));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(2, intervals.size());
+ checkInterval(intervals.get(0), 1, 1);
+ checkInterval(intervals.get(1), 2, 2);
+ }
+
+ @Test
+ public void testToIntervals_multiplePoints_complement() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+ tree.add(Interval.of(1, 1, precision));
+ tree.add(Interval.of(2, 2, precision));
+ tree.complement();
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(3, intervals.size());
+ checkInterval(intervals.get(0), Double.NEGATIVE_INFINITY, 1);
+ checkInterval(intervals.get(1), 1, 2);
+ checkInterval(intervals.get(2), 2, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testToIntervals_adjacentIntervals() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(2, 3, TEST_PRECISION));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+ checkInterval(intervals.get(0), 1, 3);
+ }
+
+ @Test
+ public void testToIntervals_multipleAdjacentIntervals() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(2, 3, TEST_PRECISION));
+ tree.add(Interval.of(3, 4, TEST_PRECISION));
+
+ tree.add(Interval.of(-2, -1, TEST_PRECISION));
+ tree.add(Interval.of(5, 6, TEST_PRECISION));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(3, intervals.size());
+ checkInterval(intervals.get(0), -2, -1);
+ checkInterval(intervals.get(1), 1, 4);
+ checkInterval(intervals.get(2), 5, 6);
+ }
+
+ @Test
+ public void testToIntervals() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ RegionBSPTree1D tree = new RegionBSPTree1D(false);
+ tree.add(Interval.of(-1, 6, precision));
+
+ // act
+ List<Interval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+ checkInterval(intervals.get(0), -1, 6);
+ }
+
+ @Test
+ public void testGetNodeRegion() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ RegionNode1D root = tree.getRoot();
+ root.cut(OrientedPoint.createPositiveFacing(1.0, TEST_PRECISION));
+
+ RegionNode1D minus = root.getMinus();
+ minus.cut(OrientedPoint.createNegativeFacing(0.0, TEST_PRECISION));
+
+ // act/assert
+ checkInterval(root.getNodeRegion(), Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+ checkInterval(minus.getNodeRegion(), Double.NEGATIVE_INFINITY, 1.0);
+ checkInterval(root.getPlus().getNodeRegion(), 1.0, Double.POSITIVE_INFINITY);
+
+ checkInterval(minus.getPlus().getNodeRegion(), Double.NEGATIVE_INFINITY, 0.0);
+ checkInterval(minus.getMinus().getNodeRegion(), 0.0, 1.0);
+ }
+
+ @Test
+ public void testTransform_full() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.full();
+
+ Transform1D transform = AffineTransformMatrix1D.createScale(2);
+
+ // act
+ tree.transform(transform);
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ }
+
+ @Test
+ public void testTransform_noReflection() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(
+ Interval.of(1, 2, TEST_PRECISION),
+ Interval.min(3, TEST_PRECISION)
+ );
+
+ Transform1D transform = AffineTransformMatrix1D.createScale(2)
+ .translate(3);
+
+ // act
+ tree.transform(transform);
+
+ // assert
+ List<Interval> intervals = tree.toIntervals();
+
+ Assert.assertEquals(2, intervals.size());
+ checkInterval(intervals.get(0), 5, 7);
+ checkInterval(intervals.get(1), 9, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testTransform_withReflection() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(
+ Interval.of(1, 2, TEST_PRECISION),
+ Interval.min(3, TEST_PRECISION)
+ );
+
+ Transform1D transform = AffineTransformMatrix1D.createScale(-2)
+ .translate(3);
+
+ // act
+ tree.transform(transform);
+
+ // assert
+ List<Interval> intervals = tree.toIntervals();
+
+ Assert.assertEquals(2, intervals.size());
+ checkInterval(intervals.get(0), Double.NEGATIVE_INFINITY, -3);
+ checkInterval(intervals.get(1), -1, 1);
+ }
+
+ @Test
+ public void testTransform_withReflection_functionBasedTransform() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(
+ Interval.of(1, 2, TEST_PRECISION),
+ Interval.min(3, TEST_PRECISION)
+ );
+
+ Transform1D transform = FunctionTransform1D.from(Vector1D::negate);
+
+ // act
+ tree.transform(transform);
+
+ // assert
+ List<Interval> intervals = tree.toIntervals();
+
+ Assert.assertEquals(2, intervals.size());
+ checkInterval(intervals.get(0), Double.NEGATIVE_INFINITY, -3);
+ checkInterval(intervals.get(1), -2, -1);
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.full();
+ OrientedPoint splitter = OrientedPoint.fromLocationAndDirection(2, true, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ List<Interval> minusIntervals = split.getMinus().toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), Double.NEGATIVE_INFINITY, 2);
+
+ List<Interval> plusIntervals = split.getPlus().toIntervals();
+ Assert.assertEquals(1, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 2, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testSplit_empty() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ OrientedPoint splitter = OrientedPoint.fromLocationAndDirection(2, true, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_bothSides() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.max(-2, TEST_PRECISION));
+ tree.add(Interval.of(1, 4, TEST_PRECISION));
+
+ OrientedPoint splitter = OrientedPoint.fromLocationAndDirection(2, false, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ List<Interval> minusIntervals = split.getMinus().toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 2, 4);
+
+ List<Interval> plusIntervals = split.getPlus().toIntervals();
+ Assert.assertEquals(2, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), Double.NEGATIVE_INFINITY, -2);
+ checkInterval(plusIntervals.get(1), 1, 2);
+ }
+
+ @Test
+ public void testSplit_splitterOnBoundary_minus() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(1, 4, TEST_PRECISION));
+
+ OrientedPoint splitter = OrientedPoint.fromLocationAndDirection(1, false, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ List<Interval> minusIntervals = split.getMinus().toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 1, 4);
+
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_splitterOnBoundary_plus() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(1, 4, TEST_PRECISION));
+
+ OrientedPoint splitter = OrientedPoint.fromLocationAndDirection(4, false, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+
+ List<Interval> plusIntervals = split.getPlus().toIntervals();
+ Assert.assertEquals(1, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 1, 4);
+ }
+
+ @Test
+ public void testSplit_point() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Interval.point(1.0, TEST_PRECISION));
+
+ OrientedPoint splitter = OrientedPoint.fromLocationAndDirection(2, false, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+
+ List<Interval> plusIntervals = split.getPlus().toIntervals();
+ Assert.assertEquals(1, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 1, 1);
+ }
+
+ @Test
+ public void testSplit_point_splitOnPoint_positiveFacingSplitter() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Interval.point(1, TEST_PRECISION));
+
+ OrientedPoint splitter = OrientedPoint.fromLocationAndDirection(1, true, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+
+ List<Interval> plusIntervals = split.getPlus().toIntervals();
+ Assert.assertEquals(1, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 1, 1);
+ }
+
+ @Test
+ public void testSplit_point_splitOnPoint_negativeFacingSplitter() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Interval.point(1, TEST_PRECISION));
+
+ OrientedPoint splitter = OrientedPoint.fromLocationAndDirection(1, false, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ List<Interval> minusIntervals = split.getMinus().toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 1, 1);
+
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testGetSize_infinite() {
+ // arrange
+ RegionBSPTree1D full = RegionBSPTree1D.full();
+
+ RegionBSPTree1D posHalfSpace = RegionBSPTree1D.empty();
+ posHalfSpace.getRoot().cut(OrientedPoint.createNegativeFacing(-2.0, TEST_PRECISION));
+
+ RegionBSPTree1D negHalfSpace = RegionBSPTree1D.empty();
+ negHalfSpace.getRoot().cut(OrientedPoint.createPositiveFacing(3.0, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertEquals(Double.POSITIVE_INFINITY, full.getSize(), TEST_EPS);
+ Assert.assertEquals(Double.POSITIVE_INFINITY, posHalfSpace.getSize(), TEST_EPS);
+ Assert.assertEquals(Double.POSITIVE_INFINITY, negHalfSpace.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetSize_empty() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // act/assert
+ Assert.assertEquals(0, tree.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetSize_exactPoints() {
+ // arrange
+ RegionBSPTree1D singlePoint = RegionBSPTree1D.empty();
+ singlePoint.add(Interval.of(1, 1, TEST_PRECISION));
+
+ RegionBSPTree1D multiplePoints = RegionBSPTree1D.empty();
+ multiplePoints.add(Interval.of(1, 1, TEST_PRECISION));
+ multiplePoints.add(Interval.of(-1, -1, TEST_PRECISION));
+ multiplePoints.add(Interval.of(2, 2, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertEquals(0, singlePoint.getSize(), TEST_EPS);
+ Assert.assertEquals(0, multiplePoints.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetSize_pointsWithinPrecision() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+
+ RegionBSPTree1D singlePoint = RegionBSPTree1D.empty();
+ singlePoint.add(Interval.of(1, 1.02, precision));
+
+ RegionBSPTree1D multiplePoints = RegionBSPTree1D.empty();
+ multiplePoints.add(Interval.of(1, 1.02, precision));
+ multiplePoints.add(Interval.of(-1.02, -1, precision));
+ multiplePoints.add(Interval.of(2, 2.02, precision));
+
+ // act/assert
+ Assert.assertEquals(0.02, singlePoint.getSize(), TEST_EPS);
+ Assert.assertEquals(0.06, multiplePoints.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetSize_nonEmptyIntervals() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(3, 5, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertEquals(3, tree.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetSize_intervalWithPoints() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(3, 3, TEST_PRECISION));
+ tree.add(Interval.of(5, 5, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertEquals(1, tree.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetSize_complementedRegion() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(Double.NEGATIVE_INFINITY, 2, TEST_PRECISION));
+ tree.add(Interval.of(4, Double.POSITIVE_INFINITY, TEST_PRECISION));
+
+ tree.complement();
+
+ // act/assert
+ Assert.assertEquals(2, tree.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBarycenter_infinite() {
+ // arrange
+ RegionBSPTree1D full = RegionBSPTree1D.full();
+
+ RegionBSPTree1D posHalfSpace = RegionBSPTree1D.empty();
+ posHalfSpace.getRoot().cut(OrientedPoint.createNegativeFacing(-2.0, TEST_PRECISION));
+
+ RegionBSPTree1D negHalfSpace = RegionBSPTree1D.empty();
+ negHalfSpace.getRoot().cut(OrientedPoint.createPositiveFacing(3.0, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertNull(full.getBarycenter());
+ Assert.assertNull(posHalfSpace.getBarycenter());
+ Assert.assertNull(negHalfSpace.getBarycenter());
+ }
+
+ @Test
+ public void testGetBarycenter_empty() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // act/assert
+ Assert.assertNull(tree.getBarycenter());
+ }
+
+ @Test
+ public void testGetBarycenter_exactPoints() {
+ // arrange
+ RegionBSPTree1D singlePoint = RegionBSPTree1D.empty();
+ singlePoint.add(Interval.of(1, 1, TEST_PRECISION));
+
+ RegionBSPTree1D multiplePoints = RegionBSPTree1D.empty();
+ multiplePoints.add(Interval.of(1, 1, TEST_PRECISION));
+ multiplePoints.add(Interval.of(-1, -1, TEST_PRECISION));
+ multiplePoints.add(Interval.of(6, 6, TEST_PRECISION));
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), singlePoint.getBarycenter(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2), multiplePoints.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBarycenter_pointsWithinPrecision() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+
+ RegionBSPTree1D singlePoint = RegionBSPTree1D.empty();
+ singlePoint.add(Interval.of(1, 1.02, precision));
+
+ RegionBSPTree1D multiplePoints = RegionBSPTree1D.empty();
+ multiplePoints.add(Interval.of(1, 1.02, precision));
+ multiplePoints.add(Interval.of(-1.02, -1, precision));
+ multiplePoints.add(Interval.of(6, 6.02, precision));
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1.01), singlePoint.getBarycenter(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(6.01 / 3), multiplePoints.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBarycenter_nonEmptyIntervals() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(3, 5, TEST_PRECISION));
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(9.5 / 3), tree.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBarycenter_complementedRegion() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(Double.NEGATIVE_INFINITY, 2, TEST_PRECISION));
+ tree.add(Interval.of(4, Double.POSITIVE_INFINITY, TEST_PRECISION));
+
+ tree.complement();
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(3), tree.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBarycenter_intervalWithPoints() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(1, 2, TEST_PRECISION));
+ tree.add(Interval.of(3, 3, TEST_PRECISION));
+ tree.add(Interval.of(5, 5, TEST_PRECISION));
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1.5), tree.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetMinMax_full() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getMin());
+ GeometryTestUtils.assertNegativeInfinity(tree.getMax());
+ }
+
+ @Test
+ public void testGetMinMax_empty() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getMin());
+ GeometryTestUtils.assertNegativeInfinity(tree.getMax());
+ }
+
+ @Test
+ public void testGetMinMax_halfSpaces() {
+ // arrange
+ RegionBSPTree1D posHalfSpace = RegionBSPTree1D.empty();
+ posHalfSpace.getRoot().cut(OrientedPoint.createNegativeFacing(-2.0, TEST_PRECISION));
+
+ RegionBSPTree1D negHalfSpace = RegionBSPTree1D.empty();
+ negHalfSpace.getRoot().cut(OrientedPoint.createPositiveFacing(3.0, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertEquals(-2, posHalfSpace.getMin(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(posHalfSpace.getMax());
+
+ GeometryTestUtils.assertNegativeInfinity(negHalfSpace.getMin());
+ Assert.assertEquals(3, negHalfSpace.getMax(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetMinMax_multipleIntervals() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Arrays.asList(
+ Interval.of(3, 5, TEST_PRECISION),
+ Interval.of(-4, -2, TEST_PRECISION),
+ Interval.of(0, 0, TEST_PRECISION)
+ ));
+
+ // act/assert
+ Assert.assertEquals(-4, tree.getMin(), TEST_EPS);
+ Assert.assertEquals(5, tree.getMax(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetMinMax_pointsAtMinAndMax() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Arrays.asList(
+ Interval.of(5, 5, TEST_PRECISION),
+ Interval.of(-4, -4, TEST_PRECISION),
+ Interval.of(0, 0, TEST_PRECISION)
+ ));
+
+ // act/assert
+ Assert.assertEquals(-4, tree.getMin(), TEST_EPS);
+ Assert.assertEquals(5, tree.getMax(), TEST_EPS);
+ }
+
+ @Test
+ public void testFull_factoryMethod() {
+ // act
+ RegionBSPTree1D tree = RegionBSPTree1D.full();
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertNotSame(tree, RegionBSPTree1D.full());
+ }
+
+ @Test
+ public void testEmpty_factoryMethod() {
+ // act
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertNotSame(tree, RegionBSPTree1D.full());
+ }
+
+ @Test
+ public void testFromIntervals_iterable() {
+ // act
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Arrays.asList(
+ Interval.of(1, 2, TEST_PRECISION),
+ Interval.of(3, 4, TEST_PRECISION)
+ ));
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ checkClassify(tree, RegionLocation.INSIDE, 1.5, 3.5);
+ checkClassify(tree, RegionLocation.BOUNDARY, 1, 2, 3, 4);
+ checkClassify(tree, RegionLocation.OUTSIDE, 0, 2.5, 5);
+
+ Assert.assertEquals(2, tree.toIntervals().size());
+ }
+
+ @Test
+ public void testFromIntervals_iterable_noItervals() {
+ // act
+ RegionBSPTree1D tree = RegionBSPTree1D.from(Arrays.asList());
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+
+ Assert.assertEquals(0, tree.toIntervals().size());
+ }
+
+ @Test
+ public void testFromIntervals_varargs() {
+ // act
+ RegionBSPTree1D tree = RegionBSPTree1D.from(
+ Interval.of(1, 2, TEST_PRECISION),
+ Interval.of(3, 4, TEST_PRECISION)
+ );
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ checkClassify(tree, RegionLocation.INSIDE, 1.5, 3.5);
+ checkClassify(tree, RegionLocation.BOUNDARY, 1, 2, 3, 4);
+ checkClassify(tree, RegionLocation.OUTSIDE, 0, 2.5, 5);
+
+ Assert.assertEquals(2, tree.toIntervals().size());
+ }
+
+ private static void checkClassify(RegionBSPTree1D tree, RegionLocation loc, double ... points) {
+ for (double x : points) {
+ String msg = "Unexpected location for point " + x;
+
+ Assert.assertEquals(msg, loc, tree.classify(x));
+ Assert.assertEquals(msg, loc, tree.classify(Vector1D.of(x)));
+ }
+ }
+
+ private static void checkContains(RegionBSPTree1D tree, boolean contains, double ... points) {
+ for (double x : points) {
+ String msg = "Unexpected contains status for point " + x;
+
+ Assert.assertEquals(msg, contains, tree.contains(x));
+ Assert.assertEquals(msg, contains, tree.contains(Vector1D.of(x)));
+ }
+ }
+
+ private static void checkBoundaryProjection(RegionBSPTree1D tree, double location, double projectedLocation) {
+ Vector1D pt = Vector1D.of(location);
+
+ Vector1D proj = tree.project(pt);
+
+ Assert.assertEquals(projectedLocation, proj.getX(), TEST_EPS);
+ }
+
+ private static void checkInterval(Interval interval, double min, double max) {
+ checkInterval(interval, min, max, TEST_PRECISION);
+ }
+
+ private static void checkInterval(Interval interval, double min, double max, DoublePrecisionContext precision) {
+ Assert.assertEquals(min, interval.getMin(), TEST_EPS);
+ Assert.assertEquals(max, interval.getMax(), TEST_EPS);
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPointTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPointTest.java
deleted file mode 100644
index 5935309..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPointTest.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * 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.oned;
-
-import org.apache.commons.geometry.core.partitioning.Side;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane.SplitSubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class SubOrientedPointTest {
-
- private static final double TEST_EPS = 1e-15;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testGetSize() {
- // arrange
- OrientedPoint hyperplane = OrientedPoint.fromPointAndDirection(Vector1D.of(1), true, TEST_PRECISION);
- SubOrientedPoint pt = hyperplane.wholeHyperplane();
-
- // act/assert
- Assert.assertEquals(0.0, pt.getSize(), TEST_EPS);
- }
-
- @Test
- public void testIsEmpty() {
- // arrange
- OrientedPoint hyperplane = OrientedPoint.fromPointAndDirection(Vector1D.of(1), true, TEST_PRECISION);
- SubOrientedPoint pt = hyperplane.wholeHyperplane();
-
- // act/assert
- Assert.assertFalse(pt.isEmpty());
- }
-
- @Test
- public void testBuildNew() {
- // arrange
- OrientedPoint originalHyperplane = OrientedPoint.fromPointAndDirection(Vector1D.of(1), true, TEST_PRECISION);
- SubOrientedPoint pt = originalHyperplane.wholeHyperplane();
-
- OrientedPoint hyperplane = OrientedPoint.fromPointAndDirection(Vector1D.of(2), true, TEST_PRECISION);
- IntervalsSet intervals = new IntervalsSet(2, 3, TEST_PRECISION);
-
- // act
- SubHyperplane<Vector1D> result = pt.buildNew(hyperplane, intervals);
-
- // assert
- Assert.assertTrue(result instanceof SubOrientedPoint);
- Assert.assertSame(hyperplane, result.getHyperplane());
- Assert.assertSame(intervals, ((SubOrientedPoint) result).getRemainingRegion());
- }
-
- @Test
- public void testSplit_resultOnMinusSide() {
- // arrange
- OrientedPoint hyperplane = OrientedPoint.fromPointAndDirection(Vector1D.of(1), true, TEST_PRECISION);
- IntervalsSet interval = new IntervalsSet(TEST_PRECISION);
- SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval);
-
- OrientedPoint splitter = OrientedPoint.fromPointAndDirection(Vector1D.of(2), true, TEST_PRECISION);
-
- // act
- SplitSubHyperplane<Vector1D> split = pt.split(splitter);
-
- // assert
- Assert.assertEquals(Side.MINUS, split.getSide());
-
- SubOrientedPoint minusSub = ((SubOrientedPoint) split.getMinus());
- Assert.assertNotNull(minusSub);
-
- OrientedPoint minusHyper = (OrientedPoint) minusSub.getHyperplane();
- Assert.assertEquals(1, minusHyper.getLocation().getX(), TEST_EPS);
-
- Assert.assertSame(interval, minusSub.getRemainingRegion());
-
- Assert.assertNull(split.getPlus());
- }
-
- @Test
- public void testSplit_resultOnPlusSide() {
- // arrange
- OrientedPoint hyperplane = OrientedPoint.fromPointAndDirection(Vector1D.of(1), true, TEST_PRECISION);
- IntervalsSet interval = new IntervalsSet(TEST_PRECISION);
- SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval);
-
- OrientedPoint splitter = OrientedPoint.fromPointAndDirection(Vector1D.of(0), true, TEST_PRECISION);
-
- // act
- SplitSubHyperplane<Vector1D> split = pt.split(splitter);
-
- // assert
- Assert.assertEquals(Side.PLUS, split.getSide());
-
- Assert.assertNull(split.getMinus());
-
- SubOrientedPoint plusSub = ((SubOrientedPoint) split.getPlus());
- Assert.assertNotNull(plusSub);
-
- OrientedPoint plusHyper = (OrientedPoint) plusSub.getHyperplane();
- Assert.assertEquals(1, plusHyper.getLocation().getX(), TEST_EPS);
-
- Assert.assertSame(interval, plusSub.getRemainingRegion());
- }
-
- @Test
- public void testSplit_equivalentHyperplanes() {
- // arrange
- OrientedPoint hyperplane = OrientedPoint.fromPointAndDirection(Vector1D.of(1), true, TEST_PRECISION);
- IntervalsSet interval = new IntervalsSet(TEST_PRECISION);
- SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval);
-
- OrientedPoint splitter = OrientedPoint.fromPointAndDirection(Vector1D.of(1), true, TEST_PRECISION);
-
- // act
- SplitSubHyperplane<Vector1D> split = pt.split(splitter);
-
- // assert
- Assert.assertEquals(Side.HYPER, split.getSide());
-
- Assert.assertNull(split.getMinus());
- Assert.assertNull(split.getPlus());
- }
-
- @Test
- public void testSplit_usesToleranceFromParentHyperplane() {
- // arrange
- DoublePrecisionContext parentPrecision = new EpsilonDoublePrecisionContext(0.1);
- DoublePrecisionContext otherPrecision = new EpsilonDoublePrecisionContext(1e-10);
-
- OrientedPoint hyperplane = OrientedPoint.fromPointAndDirection(Vector1D.of(1), true, parentPrecision);
- SubOrientedPoint pt = hyperplane.wholeHyperplane();
-
- // act/assert
- SplitSubHyperplane<Vector1D> plusSplit = pt.split(OrientedPoint.fromPointAndDirection(Vector1D.of(0.899), true, otherPrecision));
- Assert.assertNull(plusSplit.getMinus());
- Assert.assertNotNull(plusSplit.getPlus());
-
- SplitSubHyperplane<Vector1D> lowWithinTolerance = pt.split(OrientedPoint.fromPointAndDirection(Vector1D.of(0.901), true, otherPrecision));
- Assert.assertNull(lowWithinTolerance.getMinus());
- Assert.assertNull(lowWithinTolerance.getPlus());
-
- SplitSubHyperplane<Vector1D> highWithinTolerance = pt.split(OrientedPoint.fromPointAndDirection(Vector1D.of(1.09), true, otherPrecision));
- Assert.assertNull(highWithinTolerance.getMinus());
- Assert.assertNull(highWithinTolerance.getPlus());
-
- SplitSubHyperplane<Vector1D> minusSplit = pt.split(OrientedPoint.fromPointAndDirection(Vector1D.of(1.101), true, otherPrecision));
- Assert.assertNotNull(minusSplit.getMinus());
- Assert.assertNull(minusSplit.getPlus());
- }
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
index a0d7804..917b667 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.commons.geometry.euclidean.oned;
+import java.util.Comparator;
import java.util.regex.Pattern;
import org.apache.commons.geometry.core.Geometry;
@@ -59,6 +60,25 @@
}
@Test
+ public void testCoordinateAscendingOrderComparator() {
+ // arrange
+ Comparator<Vector1D> cmp = Vector1D.COORDINATE_ASCENDING_ORDER;
+
+ // act/assert
+ Assert.assertEquals(0, cmp.compare(Vector1D.of(1), Vector1D.of(1)));
+ Assert.assertEquals(1, cmp.compare(Vector1D.of(2), Vector1D.of(1)));
+ Assert.assertEquals(-1, cmp.compare(Vector1D.of(0), Vector1D.of(1)));
+
+ Assert.assertEquals(0, cmp.compare(Vector1D.of(0), Vector1D.of(0)));
+ Assert.assertEquals(1, cmp.compare(Vector1D.of(1e-15), Vector1D.of(0)));
+ Assert.assertEquals(-1, cmp.compare(Vector1D.of(-1e-15), Vector1D.of(0)));
+
+ Assert.assertEquals(-1, cmp.compare(Vector1D.of(1), null));
+ Assert.assertEquals(1, cmp.compare(null, Vector1D.of(1)));
+ Assert.assertEquals(0, cmp.compare(null, null));
+ }
+
+ @Test
public void testCoordinates() {
// act/assert
Assert.assertEquals(-1, Vector1D.of(-1).getX(), 0.0);
@@ -95,6 +115,18 @@
}
@Test
+ public void testFinite() {
+ // act/assert
+ Assert.assertTrue(Vector1D.ZERO.isFinite());
+ Assert.assertTrue(Vector1D.of(1).isFinite());
+
+ Assert.assertFalse(Vector1D.of(Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(Vector1D.of(Double.POSITIVE_INFINITY).isFinite());
+
+ Assert.assertFalse(Vector1D.of(Double.NaN).isFinite());
+ }
+
+ @Test
public void testZero() {
// act
Vector1D zero = Vector1D.of(1).getZero();
@@ -503,17 +535,17 @@
Vector1D vec = Vector1D.of(1);
// act/assert
- Assert.assertTrue(vec.equals(vec, smallEps));
- Assert.assertTrue(vec.equals(vec, largeEps));
+ Assert.assertTrue(vec.eq(vec, smallEps));
+ Assert.assertTrue(vec.eq(vec, largeEps));
- Assert.assertTrue(vec.equals(Vector1D.of(1.0000007), smallEps));
- Assert.assertTrue(vec.equals(Vector1D.of(1.0000007), largeEps));
+ Assert.assertTrue(vec.eq(Vector1D.of(1.0000007), smallEps));
+ Assert.assertTrue(vec.eq(Vector1D.of(1.0000007), largeEps));
- Assert.assertFalse(vec.equals(Vector1D.of(1.004), smallEps));
- Assert.assertTrue(vec.equals(Vector1D.of(1.004), largeEps));
+ Assert.assertFalse(vec.eq(Vector1D.of(1.004), smallEps));
+ Assert.assertTrue(vec.eq(Vector1D.of(1.004), largeEps));
- Assert.assertFalse(vec.equals(Vector1D.of(2), smallEps));
- Assert.assertFalse(vec.equals(Vector1D.of(-2), largeEps));
+ Assert.assertFalse(vec.eq(Vector1D.of(2), smallEps));
+ Assert.assertFalse(vec.eq(Vector1D.of(-2), largeEps));
}
@Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3DTest.java
index 754532e..10bf43a 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3DTest.java
@@ -57,6 +57,43 @@
}
@Test
+ public void testFromColumnVectors_threeVectors() {
+ // arrange
+ Vector3D u = Vector3D.of(1, 2, 3);
+ Vector3D v = Vector3D.of(4, 5, 6);
+ Vector3D w = Vector3D.of(7, 8, 9);
+
+ // act
+ AffineTransformMatrix3D transform = AffineTransformMatrix3D.fromColumnVectors(u, v, w);
+
+ // assert
+ Assert.assertArrayEquals(new double[] {
+ 1, 4, 7, 0,
+ 2, 5, 8, 0,
+ 3, 6, 9, 0
+ }, transform.toArray(), 0.0);
+ }
+
+ @Test
+ public void testFromColumnVectors_fourVectors() {
+ // arrange
+ Vector3D u = Vector3D.of(1, 2, 3);
+ Vector3D v = Vector3D.of(4, 5, 6);
+ Vector3D w = Vector3D.of(7, 8, 9);
+ Vector3D t = Vector3D.of(10, 11, 12);
+
+ // act
+ AffineTransformMatrix3D transform = AffineTransformMatrix3D.fromColumnVectors(u, v, w, t);
+
+ // assert
+ Assert.assertArrayEquals(new double[] {
+ 1, 4, 7, 10,
+ 2, 5, 8, 11,
+ 3, 6, 9, 12
+ }, transform.toArray(), 0.0);
+ }
+
+ @Test
public void testIdentity() {
// act
AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
@@ -621,6 +658,90 @@
}
@Test
+ public void testDeterminant() {
+ // act/assert
+ Assert.assertEquals(1.0, AffineTransformMatrix3D.identity().determinant(), EPS);
+ Assert.assertEquals(1.0, AffineTransformMatrix3D.of(
+ 1, 0, 0, 10,
+ 0, 1, 0, 11,
+ 0, 0, 1, 12
+ ).determinant(), EPS);
+ Assert.assertEquals(-1.0, AffineTransformMatrix3D.of(
+ -1, 0, 0, 10,
+ 0, 1, 0, 11,
+ 0, 0, 1, 12
+ ).determinant(), EPS);
+ Assert.assertEquals(1.0, AffineTransformMatrix3D.of(
+ -1, 0, 0, 10,
+ 0, -1, 0, 11,
+ 0, 0, 1, 12
+ ).determinant(), EPS);
+ Assert.assertEquals(-1.0, AffineTransformMatrix3D.of(
+ -1, 0, 0, 10,
+ 0, -1, 0, 11,
+ 0, 0, -1, 12
+ ).determinant(), EPS);
+ Assert.assertEquals(49.0, AffineTransformMatrix3D.of(
+ 2, -3, 1, 10,
+ 2, 0, -1, 11,
+ 1, 4, 5, -12
+ ).determinant(), EPS);
+ Assert.assertEquals(0.0, AffineTransformMatrix3D.of(
+ 1, 2, 3, 0,
+ 4, 5, 6, 0,
+ 7, 8, 9, 0
+ ).determinant(), EPS);
+ }
+
+ @Test
+ public void testPreservesOrientation() {
+ // act/assert
+ Assert.assertTrue(AffineTransformMatrix3D.identity().preservesOrientation());
+ Assert.assertTrue(AffineTransformMatrix3D.of(
+ 1, 0, 0, 10,
+ 0, 1, 0, 11,
+ 0, 0, 1, 12
+ ).preservesOrientation());
+ Assert.assertTrue(AffineTransformMatrix3D.of(
+ 2, -3, 1, 10,
+ 2, 0, -1, 11,
+ 1, 4, 5, -12
+ ).preservesOrientation());
+
+ Assert.assertFalse(AffineTransformMatrix3D.of(
+ -1, 0, 0, 10,
+ 0, 1, 0, 11,
+ 0, 0, 1, 12
+ ).preservesOrientation());
+
+ Assert.assertTrue(AffineTransformMatrix3D.of(
+ -1, 0, 0, 10,
+ 0, -1, 0, 11,
+ 0, 0, 1, 12
+ ).preservesOrientation());
+
+ Assert.assertFalse(AffineTransformMatrix3D.of(
+ -1, 0, 0, 10,
+ 0, -1, 0, 11,
+ 0, 0, -1, 12
+ ).preservesOrientation());
+ Assert.assertFalse(AffineTransformMatrix3D.of(
+ 1, 2, 3, 0,
+ 4, 5, 6, 0,
+ 7, 8, 9, 0
+ ).preservesOrientation());
+ }
+
+ @Test
+ public void testToMatrix() {
+ // arrange
+ AffineTransformMatrix3D m = AffineTransformMatrix3D.createScale(3);
+
+ // act/assert
+ Assert.assertSame(m, m.toMatrix());
+ }
+
+ @Test
public void testMultiply_combinesTransformOperations() {
// arrange
Vector3D translation1 = Vector3D.of(1, 2, 3);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java
new file mode 100644
index 0000000..bfad866
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java
@@ -0,0 +1,641 @@
+/*
+ * 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.threed;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Line;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ConvexSubPlaneTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testFromConvexArea() {
+ // arrange
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1),
+ Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.of(1, 0),
+ Vector2D.of(3, 0),
+ Vector2D.of(3, 1),
+ Vector2D.of(1, 1)
+ ), TEST_PRECISION);
+
+ // act
+ ConvexSubPlane sp = ConvexSubPlane.fromConvexArea(plane, area);
+
+ // assert
+ Assert.assertFalse(sp.isFull());
+ Assert.assertFalse(sp.isEmpty());
+ Assert.assertTrue(sp.isFinite());
+
+ Assert.assertEquals(2, sp.getSize(), TEST_EPS);
+
+ Assert.assertSame(plane, sp.getPlane());
+ Assert.assertSame(plane, sp.getHyperplane());
+ Assert.assertSame(area, sp.getSubspaceRegion());
+ }
+
+ @Test
+ public void testFromVertices_infinite() {
+ // act
+ ConvexSubPlane sp = ConvexSubPlane.fromVertices(Arrays.asList(
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(1, 1, 0),
+ Vector3D.of(1, 1, 1)
+ ), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(sp.isFull());
+ Assert.assertFalse(sp.isEmpty());
+ Assert.assertFalse(sp.isFinite());
+
+ EuclideanTestUtils.assertPositiveInfinity(sp.getSize());
+
+ checkPlane(sp.getPlane(), Vector3D.of(1, 0, 0), Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z);
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(0, 1, 1), Vector3D.of(0, 1, 0), Vector3D.of(0, 1, -1),
+ Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 0), Vector3D.of(0, 0, -1),
+ Vector3D.of(0, -1, 1), Vector3D.of(0, -1, 0), Vector3D.of(0, -1, -1));
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(1, 1, -1), Vector3D.of(1, 0, -1), Vector3D.of(1, -1, -1));
+
+ checkPoints(sp, RegionLocation.BOUNDARY,
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, 0),
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(1, -1, 0));
+
+ checkPoints(sp, RegionLocation.INSIDE,
+ Vector3D.of(1, 0, 1), Vector3D.of(1, -1, 1));
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(2, 1, 1), Vector3D.of(2, 1, 0), Vector3D.of(2, 1, -1),
+ Vector3D.of(2, 0, 1), Vector3D.of(2, 0, 0), Vector3D.of(2, 0, -1),
+ Vector3D.of(2, -1, 1), Vector3D.of(2, -1, 0), Vector3D.of(2, -1, -1));
+ }
+
+ @Test
+ public void testFromVertices_finite() {
+ // act
+ ConvexSubPlane sp = ConvexSubPlane.fromVertices(Arrays.asList(
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(1, 1, 0),
+ Vector3D.of(1, 1, 2),
+ Vector3D.of(1, 0, 0)
+ ), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(sp.isFull());
+ Assert.assertFalse(sp.isEmpty());
+ Assert.assertTrue(sp.isFinite());
+
+ Assert.assertEquals(1, sp.getSize(), TEST_EPS);
+
+ checkPlane(sp.getPlane(), Vector3D.of(1, 0, 0), Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z);
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(0, 1, 1), Vector3D.of(0, 1, 0), Vector3D.of(0, 1, -1),
+ Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 0), Vector3D.of(0, 0, -1),
+ Vector3D.of(0, -1, 1), Vector3D.of(0, -1, 0), Vector3D.of(0, -1, -1));
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(1, 1, -1),
+ Vector3D.of(1, 0, 1), Vector3D.of(1, 0, -1),
+ Vector3D.of(1, -1, 1), Vector3D.of(1, -1, 0), Vector3D.of(1, -1, -1));
+
+ checkPoints(sp, RegionLocation.BOUNDARY,
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, 0),
+ Vector3D.of(1, 0, 0));
+
+ checkPoints(sp, RegionLocation.INSIDE, Vector3D.of(1, 0.5, 0.5));
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(2, 1, 1), Vector3D.of(2, 1, 0), Vector3D.of(2, 1, -1),
+ Vector3D.of(2, 0, 1), Vector3D.of(2, 0, 0), Vector3D.of(2, 0, -1),
+ Vector3D.of(2, -1, 1), Vector3D.of(2, -1, 0), Vector3D.of(2, -1, -1));
+ }
+
+ @Test
+ public void testFromVertexLoop() {
+ // act
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(1, 1, 0),
+ Vector3D.of(1, 1, 2)
+ ), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(sp.isFull());
+ Assert.assertFalse(sp.isEmpty());
+ Assert.assertTrue(sp.isFinite());
+
+ Assert.assertEquals(1, sp.getSize(), TEST_EPS);
+
+ checkPlane(sp.getPlane(), Vector3D.of(1, 0, 0), Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z);
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(0, 1, 1), Vector3D.of(0, 1, 0), Vector3D.of(0, 1, -1),
+ Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 0), Vector3D.of(0, 0, -1),
+ Vector3D.of(0, -1, 1), Vector3D.of(0, -1, 0), Vector3D.of(0, -1, -1));
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(1, 1, -1),
+ Vector3D.of(1, 0, 1), Vector3D.of(1, 0, -1),
+ Vector3D.of(1, -1, 1), Vector3D.of(1, -1, 0), Vector3D.of(1, -1, -1));
+
+ checkPoints(sp, RegionLocation.BOUNDARY,
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, 0),
+ Vector3D.of(1, 0, 0));
+
+ checkPoints(sp, RegionLocation.INSIDE, Vector3D.of(1, 0.5, 0.5));
+
+ checkPoints(sp, RegionLocation.OUTSIDE,
+ Vector3D.of(2, 1, 1), Vector3D.of(2, 1, 0), Vector3D.of(2, 1, -1),
+ Vector3D.of(2, 0, 1), Vector3D.of(2, 0, 0), Vector3D.of(2, 0, -1),
+ Vector3D.of(2, -1, 1), Vector3D.of(2, -1, 0), Vector3D.of(2, -1, -1));
+ }
+
+ @Test
+ public void testToConvex() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(
+ Arrays.asList(Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z), TEST_PRECISION);
+
+ // act
+ List<ConvexSubPlane> convex = sp.toConvex();
+
+ // assert
+ Assert.assertEquals(1, convex.size());
+ Assert.assertSame(sp, convex.get(0));
+ }
+
+ @Test
+ public void testGetVertices_full() {
+ // arrange
+ Plane plane = Plane.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ ConvexSubPlane sp = ConvexSubPlane.fromConvexArea(plane, ConvexArea.full());
+
+ // act
+ List<Vector3D> vertices = sp.getVertices();
+
+ // assert
+ Assert.assertEquals(0, vertices.size());
+ }
+
+ @Test
+ public void testGetVertices_twoParallelLines() {
+ // arrange
+ Plane plane = Plane.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ ConvexSubPlane sp = ConvexSubPlane.fromConvexArea(plane, ConvexArea.fromBounds(
+ Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(0, -1), Geometry.ZERO_PI, TEST_PRECISION)
+ ));
+
+ // act
+ List<Vector3D> vertices = sp.getVertices();
+
+ // assert
+ Assert.assertEquals(0, vertices.size());
+ }
+
+ @Test
+ public void testGetVertices_infiniteWithVertices() {
+ // arrange
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ ConvexSubPlane sp = ConvexSubPlane.fromConvexArea(plane, ConvexArea.fromBounds(
+ Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(0, -1), Geometry.ZERO_PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(1, 0), Geometry.HALF_PI, TEST_PRECISION)
+ ));
+
+ // act
+ List<Vector3D> vertices = sp.getVertices();
+
+ // assert
+ Assert.assertEquals(2, vertices.size());
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -1, 1), vertices.get(0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), vertices.get(1), TEST_EPS);
+ }
+
+ @Test
+ public void testGetVertices_finite() {
+ // arrange
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ ConvexSubPlane sp = ConvexSubPlane.fromConvexArea(plane, ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_X,
+ Vector2D.Unit.PLUS_Y
+ ), TEST_PRECISION));
+
+ // act
+ List<Vector3D> vertices = sp.getVertices();
+
+ // assert
+ Assert.assertEquals(4, vertices.size());
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), vertices.get(0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 1), vertices.get(1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, 1), vertices.get(2), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), vertices.get(3), TEST_EPS);
+ }
+
+ @Test
+ public void testReverse() {
+ // arrange
+ Vector3D p1 = Vector3D.of(1, 0, 1);
+ Vector3D p2 = Vector3D.of(2, 0, 1);
+ Vector3D p3 = Vector3D.of(1, 1, 1);
+
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(p1, p2, p3), TEST_PRECISION);
+
+ // act
+ ConvexSubPlane reversed = sp.reverse();
+
+ // assert
+ Assert.assertEquals(sp.getPlane().reverse(), reversed.getPlane());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_Z, reversed.getPlane().getNormal(), TEST_EPS);
+
+ Assert.assertEquals(0.5, reversed.getSize(), TEST_EPS);
+
+ checkVertices(reversed, p1, p3, p2, p1);
+
+ checkPoints(reversed, RegionLocation.INSIDE, Vector3D.of(1.25, 0.25, 1));
+
+ checkPoints(reversed, RegionLocation.BOUNDARY, p1, p2, p3);
+ }
+
+ @Test
+ public void testTransform_full() {
+ // arrange
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ ConvexSubPlane sp = ConvexSubPlane.fromConvexArea(plane, ConvexArea.full());
+
+ AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_X, Geometry.HALF_PI))
+ .translate(Vector3D.Unit.PLUS_Y);
+
+ // act
+ ConvexSubPlane transformed = sp.transform(transform);
+
+ // assert
+ Assert.assertTrue(transformed.isFull());
+ Assert.assertFalse(transformed.isEmpty());
+
+ checkPlane(transformed.getPlane(), Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z);
+ }
+
+ @Test
+ public void testTransform_halfSpace() {
+ // arrange
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ ConvexSubPlane sp = ConvexSubPlane.fromConvexArea(plane,
+ ConvexArea.fromBounds(Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(1, 1), TEST_PRECISION)));
+
+ AffineTransformMatrix3D transform = AffineTransformMatrix3D.createRotation(Vector3D.Unit.PLUS_Z,
+ QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ ConvexSubPlane transformed = sp.transform(transform);
+
+ // assert
+ Assert.assertFalse(transformed.isFull());
+ Assert.assertFalse(transformed.isEmpty());
+
+ checkPlane(transformed.getPlane(), Vector3D.ZERO, Vector3D.Unit.MINUS_Z, Vector3D.Unit.PLUS_Y);
+ }
+
+ @Test
+ public void testTransform_finite() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(
+ Arrays.asList(Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), Vector3D.of(0, 0, 1)), TEST_PRECISION);
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.createScale(2)
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ ConvexSubPlane transformed = sp.transform(transform);
+
+ // assert
+ Vector3D midpt = Vector3D.of(2, 2, -2).multiply(1 / 3.0);
+ Vector3D normal = midpt.normalize();
+ Vector3D u = Vector3D.of(0, 2, 2).normalize();
+
+ checkPlane(transformed.getPlane(), midpt, u, normal.cross(u));
+
+ checkVertices(transformed, Vector3D.of(0, 0, -2), Vector3D.of(0, 2, 0),
+ Vector3D.of(2, 0, 0), Vector3D.of(0, 0, -2));
+
+ checkPoints(transformed, RegionLocation.INSIDE, midpt);
+ }
+
+ @Test
+ public void testTransform_reflection() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(
+ Arrays.asList(Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), Vector3D.of(0, 0, 1)), TEST_PRECISION);
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.createScale(-1, 1, 1);
+
+ // act
+ ConvexSubPlane transformed = sp.transform(transform);
+
+ // assert
+ Vector3D midpt = Vector3D.of(-1, 1, 1).multiply(1 / 3.0);
+ Vector3D normal = midpt.negate().normalize();
+ Vector3D u = Vector3D.of(1, 1, 0).normalize();
+
+ checkPlane(transformed.getPlane(), midpt, u, normal.cross(u));
+
+ checkVertices(transformed, Vector3D.of(-1, 0, 0), Vector3D.of(0, 1, 0),
+ Vector3D.of(0, 0, 1), Vector3D.of(-1, 0, 0));
+
+ checkPoints(transformed, RegionLocation.INSIDE, Vector3D.of(-1, 1, 1).multiply(1 / 3.0));
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ ConvexSubPlane sp = ConvexSubPlane.fromConvexArea(plane, ConvexArea.full());
+
+ Plane splitter = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexSubPlane minus = split.getMinus();
+ Assert.assertEquals(1, minus.getSubspaceRegion().getBoundaries().size());
+ checkPoints(minus, RegionLocation.BOUNDARY, Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Y);
+ checkPoints(minus, RegionLocation.INSIDE, Vector3D.Unit.MINUS_X);
+ checkPoints(minus, RegionLocation.OUTSIDE, Vector3D.Unit.PLUS_X);
+
+ ConvexSubPlane plus = split.getPlus();
+ Assert.assertEquals(1, plus.getSubspaceRegion().getBoundaries().size());
+ checkPoints(plus, RegionLocation.BOUNDARY, Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Y);
+ checkPoints(plus, RegionLocation.INSIDE, Vector3D.Unit.PLUS_X);
+ checkPoints(plus, RegionLocation.OUTSIDE, Vector3D.Unit.MINUS_X);
+ }
+
+ @Test
+ public void testSplit_both() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane splitter = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexSubPlane minus = split.getMinus();
+ checkVertices(minus, Vector3D.of(1, 1, 0), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0), Vector3D.of(1, 1, 0));
+
+ ConvexSubPlane plus = split.getPlus();
+ checkVertices(plus, Vector3D.of(1, 1, 1), Vector3D.of(1, 1, 0), Vector3D.of(0, 2, 0), Vector3D.of(1, 1, 1));
+ }
+
+ @Test
+ public void testSplit_plusOnly() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, -3.1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+
+ ConvexSubPlane plus = split.getPlus();
+ checkVertices(plus, Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0), Vector3D.of(1, 1, 1));
+ }
+
+ @Test
+ public void testSplit_minusOnly() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1.1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ ConvexSubPlane minus = split.getMinus();
+ checkVertices(minus, Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0), Vector3D.of(1, 1, 1));
+
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_parallelSplitter_on() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane splitter = sp.getPlane();
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_parallelSplitter_minus() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane plane = sp.getPlane();
+ Plane splitter = plane.translate(plane.getNormal());
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(sp, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_parallelSplitter_plus() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane plane = sp.getPlane();
+ Plane splitter = plane.translate(plane.getNormal().negate());
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(sp, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_antiParallelSplitter_on() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane splitter = sp.getPlane().reverse();
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_antiParallelSplitter_minus() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane plane = sp.getPlane().reverse();
+ Plane splitter = plane.translate(plane.getNormal());
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(sp, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_antiParallelSplitter_plus() {
+ // arrange
+ ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+ Vector3D.of(1, 1, 1), Vector3D.of(1, 1, -3), Vector3D.of(0, 2, 0)
+ ), TEST_PRECISION);
+
+ Plane plane = sp.getPlane().reverse();
+ Plane splitter = plane.translate(plane.getNormal().negate());
+
+ // act
+ Split<ConvexSubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(sp, split.getPlus());
+ }
+
+ private static void checkPlane(Plane plane, Vector3D origin, Vector3D u, Vector3D v) {
+ u = u.normalize();
+ v = v.normalize();
+ Vector3D w = u.cross(v);
+
+ EuclideanTestUtils.assertCoordinatesEqual(origin, plane.getOrigin(), TEST_EPS);
+ Assert.assertTrue(plane.contains(origin));
+
+ EuclideanTestUtils.assertCoordinatesEqual(u, plane.getU(), TEST_EPS);
+ Assert.assertEquals(1.0, plane.getU().norm(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(v, plane.getV(), TEST_EPS);
+ Assert.assertEquals(1.0, plane.getV().norm(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(w, plane.getW(), TEST_EPS);
+ Assert.assertEquals(1.0, plane.getW().norm(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(w, plane.getNormal(), TEST_EPS);
+ Assert.assertEquals(1.0, plane.getNormal().norm(), TEST_EPS);
+
+ double offset = plane.getOriginOffset();
+ Assert.assertEquals(Vector3D.ZERO.distance(plane.getOrigin()), Math.abs(offset), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(origin, plane.getNormal().multiply(-offset), TEST_EPS);
+ }
+
+ private static void checkPoints(ConvexSubPlane sp, RegionLocation loc, Vector3D ... pts) {
+ for (Vector3D pt : pts) {
+ Assert.assertEquals("Unexpected subplane location for point " + pt, loc, sp.classify(pt));
+ }
+ }
+
+ private static void checkVertices(ConvexSubPlane sp, Vector3D ... pts) {
+ List<Vector3D> actual = sp.getPlane().toSpace(
+ sp.getSubspaceRegion().getBoundaryPaths().get(0).getVertices());
+
+ Assert.assertEquals(pts.length, actual.size());
+
+ for (int i=0; i<pts.length; ++i) {
+ EuclideanTestUtils.assertCoordinatesEqual(pts[i], actual.get(i), TEST_EPS);
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
new file mode 100644
index 0000000..ed062a2
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
@@ -0,0 +1,228 @@
+/*
+ * 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.threed;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ConvexVolumeTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testFull() {
+ // act
+ ConvexVolume vol = ConvexVolume.full();
+
+ // assert
+ Assert.assertTrue(vol.isFull());
+ Assert.assertFalse(vol.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(vol.getSize());
+ Assert.assertNull(vol.getBarycenter());
+
+ Assert.assertEquals(0, vol.getBoundaries().size());
+ Assert.assertEquals(0, vol.getBoundarySize(), TEST_EPS);
+ }
+
+ @Test
+ public void testTOTree() {
+ // arrange
+ ConvexVolume volume = ConvexVolume.fromBounds(
+ Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_X, TEST_PRECISION),
+ Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Y, TEST_PRECISION),
+ Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Z, TEST_PRECISION),
+
+ Plane.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, TEST_PRECISION),
+ Plane.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y, TEST_PRECISION),
+ Plane.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION)
+ );
+
+ // act
+ RegionBSPTree3D tree = volume.toTree();
+
+ // assert
+ Assert.assertEquals(1, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0.5, 0.5), Vector3D.of(2, 0.5, 0.5),
+ Vector3D.of(0.5, -1, 0.5), Vector3D.of(0.5, 2, 0.5),
+ Vector3D.of(0.5, 0.5, -1), Vector3D.of(0.5, 0.5, 2));
+ checkClassify(tree, RegionLocation.BOUNDARY, Vector3D.ZERO);
+ checkClassify(tree, RegionLocation.INSIDE, Vector3D.of(0.5, 0.5, 0.5));
+ }
+
+ @Test
+ public void testFromBounds_noPlanes() {
+ // act
+ ConvexVolume vol = ConvexVolume.fromBounds();
+
+ // assert
+ Assert.assertSame(ConvexVolume.full(), vol);
+ }
+
+ @Test
+ public void testFromBounds_halfspace() {
+ // act
+ ConvexVolume vol = ConvexVolume.fromBounds(Plane.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
+
+ // assert
+ Assert.assertFalse(vol.isFull());
+ Assert.assertFalse(vol.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(vol.getSize());
+ Assert.assertNull(vol.getBarycenter());
+
+ Assert.assertEquals(1, vol.getBoundaries().size());
+ GeometryTestUtils.assertPositiveInfinity(vol.getBoundarySize());
+
+ checkClassify(vol, RegionLocation.OUTSIDE, Vector3D.of(0, 0, 1));
+ checkClassify(vol, RegionLocation.BOUNDARY, Vector3D.of(0, 0, 0));
+ checkClassify(vol, RegionLocation.INSIDE, Vector3D.of(0, 0, -1));
+ }
+
+ @Test
+ public void testFromBounds_cube() {
+ // act
+ ConvexVolume vol = rect(Vector3D.of(1, 1, 1), 0.5, 1, 2);
+
+ // assert
+ Assert.assertFalse(vol.isFull());
+ Assert.assertFalse(vol.isEmpty());
+
+ Assert.assertEquals(8, vol.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), vol.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(6, vol.getBoundaries().size());
+ Assert.assertEquals(28, vol.getBoundarySize(), TEST_EPS);
+
+ checkClassify(vol, RegionLocation.INSIDE, Vector3D.of(1, 1, 1));
+
+ checkClassify(vol, RegionLocation.BOUNDARY,
+ Vector3D.of(0.5, 0, -1), Vector3D.of(1.5, 2, 3));
+
+ checkClassify(vol, RegionLocation.OUTSIDE,
+ Vector3D.of(0, 1, 1), Vector3D.of(2, 1, 1),
+ Vector3D.of(1, -1, 1), Vector3D.of(1, 3, 1),
+ Vector3D.of(1, 1, -2), Vector3D.of(1, 1, 4));
+ }
+
+ @Test
+ public void testTrim() {
+ // arrange
+ ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
+
+ ConvexSubPlane subplane = ConvexSubPlane.fromConvexArea(
+ Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION), ConvexArea.full());
+
+ // act
+ ConvexSubPlane trimmed = vol.trim(subplane);
+
+ // assert
+ Assert.assertEquals(1, trimmed.getSize(), TEST_EPS);
+
+ List<Vector3D> vertices = trimmed.getPlane().toSpace(
+ trimmed.getSubspaceRegion().getBoundaryPaths().get(0).getVertices());
+
+ Assert.assertEquals(5, vertices.size());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, -0.5), vertices.get(0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, 0.5), vertices.get(1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -0.5, 0.5), vertices.get(2), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -0.5, -0.5), vertices.get(3), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, -0.5), vertices.get(4), TEST_EPS);
+ }
+
+ @Test
+ public void testSplit() {
+ // arrange
+ ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
+
+ Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Split<ConvexVolume> split = vol.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexVolume minus = split.getMinus();
+ Assert.assertEquals(0.5, minus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.25, 0, 0), minus.getBarycenter(), TEST_EPS);
+
+ ConvexVolume plus = split.getPlus();
+ Assert.assertEquals(0.5, plus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.25, 0, 0), plus.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.identity()
+ .translate(Vector3D.of(1, 2, 3))
+ .scale(Vector3D.of(2, 1, 1));
+
+ // act
+ ConvexVolume transformed = vol.transform(transform);
+
+ // assert
+ Assert.assertEquals(2, transformed.getSize(), TEST_EPS);
+ Assert.assertEquals(10, transformed.getBoundarySize(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 3), transformed.getBarycenter(), TEST_EPS);
+ }
+
+ private static ConvexVolume rect(Vector3D center, double xDelta, double yDelta, double zDelta) {
+ List<Plane> planes = Arrays.asList(
+ Plane.fromPointAndNormal(center.add(Vector3D.of(xDelta, 0, 0)), Vector3D.Unit.PLUS_X, TEST_PRECISION),
+ Plane.fromPointAndNormal(center.add(Vector3D.of(-xDelta, 0, 0)), Vector3D.Unit.MINUS_X, TEST_PRECISION),
+
+ Plane.fromPointAndNormal(center.add(Vector3D.of(0, yDelta, 0)), Vector3D.Unit.PLUS_Y, TEST_PRECISION),
+ Plane.fromPointAndNormal(center.add(Vector3D.of(0, -yDelta, 0)), Vector3D.Unit.MINUS_Y, TEST_PRECISION),
+
+ Plane.fromPointAndNormal(center.add(Vector3D.of(0, 0, zDelta)), Vector3D.Unit.PLUS_Z, TEST_PRECISION),
+ Plane.fromPointAndNormal(center.add(Vector3D.of( 0, 0, -zDelta)), Vector3D.Unit.MINUS_Z, TEST_PRECISION)
+ );
+
+ return ConvexVolume.fromBounds(planes);
+ }
+
+ private static void checkClassify(Region<Vector3D> region, RegionLocation loc, Vector3D ... pts) {
+ for (Vector3D pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, loc, region.classify(pt));
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/FunctionTransform3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/FunctionTransform3DTest.java
new file mode 100644
index 0000000..cc32706
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/FunctionTransform3DTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.threed;
+
+import java.util.function.Function;
+
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FunctionTransform3DTest {
+
+ private static final double TEST_EPS = 1e-15;
+
+ @Test
+ public void testIdentity() {
+ // arrange
+ Vector3D p0 = Vector3D.of(0, 0, 0);
+ Vector3D p1 = Vector3D.of(1, 1, 1);
+ Vector3D p2 = Vector3D.of(-1, -1, -1);
+
+ // act
+ FunctionTransform3D t = FunctionTransform3D.identity();
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_identity() {
+ // arrange
+ Vector3D p0 = Vector3D.of(0, 0, 0);
+ Vector3D p1 = Vector3D.of(1, 1, 1);
+ Vector3D p2 = Vector3D.of(-1, -1, -1);
+
+ // act
+ FunctionTransform3D t = FunctionTransform3D.from(Function.identity());
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_scaleAndTranslate() {
+ // arrange
+ Vector3D p0 = Vector3D.of(0, 0, 0);
+ Vector3D p1 = Vector3D.of(1, 2, 3);
+ Vector3D p2 = Vector3D.of(-1, -2, -3);
+
+ // act
+ FunctionTransform3D t = FunctionTransform3D.from(v -> v.multiply(2).add(Vector3D.of(1, -1, 2)));
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -1, 2), t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 3, 8), t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, -5, -4), t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_reflection_singleAxis() {
+ // arrange
+ Vector3D p0 = Vector3D.of(0, 0, 0);
+ Vector3D p1 = Vector3D.of(1, 2, 3);
+ Vector3D p2 = Vector3D.of(-1, -2, -3);
+
+ // act
+ FunctionTransform3D t = FunctionTransform3D.from(v -> Vector3D.of(-v.getX(), v.getY(), v.getZ()));
+
+ // assert
+ Assert.assertFalse(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 2, 3), t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -2, -3), t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_reflection_twoAxes() {
+ // arrange
+ Vector3D p0 = Vector3D.of(0, 0, 0);
+ Vector3D p1 = Vector3D.of(1, 2, 3);
+ Vector3D p2 = Vector3D.of(-1, -2, -3);
+
+ // act
+ FunctionTransform3D t = FunctionTransform3D.from(v -> Vector3D.of(-v.getX(), -v.getY(), v.getZ()));
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, -2, 3), t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, -3), t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_reflection_allAxes() {
+ // arrange
+ Vector3D p0 = Vector3D.of(0, 0, 0);
+ Vector3D p1 = Vector3D.of(1, 2, 3);
+ Vector3D p2 = Vector3D.of(-1, -2, -3);
+
+ // act
+ FunctionTransform3D t = FunctionTransform3D.from(Vector3D::negate);
+
+ // assert
+ Assert.assertFalse(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, -2, -3), t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testToMatrix() {
+ // act/assert
+ Assert.assertArrayEquals(new double[] {
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0
+ },
+ FunctionTransform3D.identity().toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] {
+ 1, 0, 0, 2,
+ 0, 1, 0, 3,
+ 0, 0, 1, -4
+ },
+ FunctionTransform3D.from(v -> v.add(Vector3D.of(2, 3, -4))).toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] {
+ 3, 0, 0, 0,
+ 0, 3, 0, 0,
+ 0, 0, 3, 0
+ },
+ FunctionTransform3D.from(v -> v.multiply(3)).toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] {
+ 3, 0, 0, 6,
+ 0, 3, 0, 9,
+ 0, 0, 3, 12
+ },
+ FunctionTransform3D.from(v -> v.add(Vector3D.of(2, 3, 4)).multiply(3)).toMatrix().toArray(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransformRoundTrip() {
+ // arrange
+ double eps = 1e-8;
+ double delta = 0.11;
+
+ Vector3D p1 = Vector3D.of(1.1, -3, 0);
+ Vector3D p2 = Vector3D.of(-5, 0.2, 2);
+ Vector3D vec = p1.vectorTo(p2);
+
+ EuclideanTestUtils.permuteSkipZero(-2, 2, delta, (translate, scale) -> {
+
+ FunctionTransform3D t = FunctionTransform3D.from(v -> {
+ return v.multiply(scale * 0.5)
+ .add(Vector3D.of(translate, 0.5 * translate, 0.25 * translate))
+ .multiply(scale * 1.5);
+ });
+
+ // act
+ Vector3D t1 = t.apply(p1);
+ Vector3D t2 = t.apply(p2);
+ Vector3D tvec = t.applyVector(vec);
+
+ Transform3D inverse = t.toMatrix().inverse();
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(tvec, t1.vectorTo(t2), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, inverse.apply(t1), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, inverse.apply(t2), eps);
+ });
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java
new file mode 100644
index 0000000..4f01e68
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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.threed;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.IllegalNormException;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Line3DTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testFromPointAndDirection() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(-1, 1, 0), Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0), line.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Y, line.getDirection(), TEST_EPS);
+ Assert.assertSame(TEST_PRECISION, line.getPrecision());
+ }
+
+ @Test
+ public void testFromPointAndDirection_normalizesDirection() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, line.getOrigin(), TEST_EPS);
+
+ double invSqrt3 = 1.0 / Math.sqrt(3);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(invSqrt3, invSqrt3, invSqrt3), line.getDirection(), TEST_EPS);
+ Assert.assertSame(TEST_PRECISION, line.getPrecision());
+ }
+
+ @Test(expected = IllegalNormException.class)
+ public void testFromPointAndDirection_illegalDirectionNorm() {
+ // act/assert
+ Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.ZERO, TEST_PRECISION);
+ }
+
+ @Test
+ public void testFromPoints() {
+ // arrange
+ Line3D line = Line3D.fromPoints(Vector3D.of(-1, 1, 0), Vector3D.of(-1, 7, 0), TEST_PRECISION);
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0), line.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Y, line.getDirection(), TEST_EPS);
+ Assert.assertSame(TEST_PRECISION, line.getPrecision());
+ }
+
+ @Test(expected = IllegalNormException.class)
+ public void testFromPoints_pointsTooClose() {
+ // act/assert
+ Line3D.fromPoints(Vector3D.of(1, 1, 1), Vector3D.of(1, 1, 1 + 1e-16), TEST_PRECISION);
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ Vector3D pt = Vector3D.of(1, 2, 3);
+ Line3D line = Line3D.fromPointAndDirection(pt, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ AffineTransformMatrix3D mat = AffineTransformMatrix3D.createRotation(pt,
+ QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ Line3D result = line.transform(mat);
+
+ // assert
+ Assert.assertTrue(result.contains(pt));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, -1).normalize(), result.getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_reflectionInOneAxis() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ Transform3D transform = FunctionTransform3D.from(v -> Vector3D.of(v.getX(), v.getY(), -v.getZ()));
+
+ // act
+ Line3D result = line.transform(transform);
+
+ // assert
+ Assert.assertTrue(result.contains(Vector3D.of(1, 0, 0)));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, -1).normalize(), result.getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_reflectionInTwoAxes() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ Transform3D transform = FunctionTransform3D.from(v -> Vector3D.of(v.getX(), -v.getY(), -v.getZ()));
+
+ // act
+ Line3D result = line.transform(transform);
+
+ // assert
+ Assert.assertTrue(result.contains(Vector3D.of(1, 0, 0)));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -1, -1).normalize(), result.getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_reflectionInThreeAxes() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ Transform3D transform = FunctionTransform3D.from(Vector3D::negate);
+
+ // act
+ Line3D result = line.transform(transform);
+
+ // assert
+ Assert.assertTrue(result.contains(Vector3D.of(-1, 0, 0)));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, -1, -1).normalize(), result.getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testSubspaceTransform() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0, 0, 1), Vector3D.of(1, 0, 0), TEST_PRECISION);
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.identity()
+ .scale(2, 1, 1)
+ .translate(0.5, 1, 0)
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ Line3D.SubspaceTransform result = line.subspaceTransform(transform);
+
+ // assert
+ Line3D tLine = result.getLine();
+ Transform<Vector1D> tSub = result.getTransform();
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 0), tLine.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1), tLine.getDirection(), TEST_EPS);
+
+ Assert.assertEquals(0.5, tSub.apply(Vector1D.ZERO).getX(), TEST_EPS);
+ Assert.assertEquals(4.5, tSub.apply(Vector1D.of(2)).getX(), TEST_EPS);
+ }
+
+ @Test
+ public void testAbscissa() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0, 0, -1), Vector3D.of(4, 3, 0), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(0.0, line.abscissa(line.getOrigin()), TEST_EPS);
+
+ Assert.assertEquals(5.0, line.abscissa(Vector3D.of(4, 3, 0)), TEST_EPS);
+ Assert.assertEquals(5.0, line.abscissa(Vector3D.of(4, 3, 10)), TEST_EPS);
+
+ Assert.assertEquals(-5.0, line.abscissa(Vector3D.of(-4, -3, 0)), TEST_EPS);
+ Assert.assertEquals(-5.0, line.abscissa(Vector3D.of(-4, -3, -10)), TEST_EPS);
+ }
+
+ @Test
+ public void testToSubspace() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0, 0, -1), Vector3D.of(4, 3, 0), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(0.0, line.toSubspace(line.getOrigin()).getX(), TEST_EPS);
+
+ Assert.assertEquals(5.0, line.toSubspace(Vector3D.of(4, 3, -1)).getX(), TEST_EPS);
+ Assert.assertEquals(5.0, line.toSubspace(Vector3D.of(4, 3, 10)).getX(), TEST_EPS);
+
+ Assert.assertEquals(-5.0, line.toSubspace(Vector3D.of(-4, -3, -1)).getX(), TEST_EPS);
+ Assert.assertEquals(-5.0, line.toSubspace(Vector3D.of(-4, -3, -10)).getX(), TEST_EPS);
+ }
+
+ @Test
+ public void testPointAt() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0, 0, -1), Vector3D.of(4, 3, 0), TEST_PRECISION);
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(line.getOrigin(), line.pointAt(0.0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4, 3, -1), line.pointAt(5.0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-4, -3, -1), line.pointAt(-5.0), TEST_EPS);
+ }
+
+ @Test
+ public void testToSpace() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0, 0, -1), Vector3D.of(4, 3, 0), TEST_PRECISION);
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(line.getOrigin(), line.toSpace(Vector1D.of(0.0)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4, 3, -1), line.toSpace(Vector1D.of(5.0)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-4, -3, -1), line.toSpace(Vector1D.of(-5.0)), TEST_EPS);
+ }
+
+ @Test
+ public void testContains() {
+ Vector3D p1 = Vector3D.of(0, 0, 1);
+ Line3D l = Line3D.fromPoints(p1, Vector3D.of(0, 0, 2), TEST_PRECISION);
+ Assert.assertTrue(l.contains(p1));
+ Assert.assertTrue(l.contains(Vector3D.linearCombination(1.0, p1, 0.3, l.getDirection())));
+ Vector3D u = l.getDirection().orthogonal();
+ Vector3D v = l.getDirection().cross(u);
+ for (double alpha = 0; alpha < 2 * Math.PI; alpha += 0.3) {
+ Assert.assertTrue(! l.contains(p1.add(Vector3D.linearCombination(Math.cos(alpha), u,
+ Math.sin(alpha), v))));
+ }
+ }
+
+ @Test
+ public void testSimilar() {
+ Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
+ Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
+ Line3D lA = Line3D.fromPoints(p1, p2, TEST_PRECISION);
+ Line3D lB = Line3D.fromPoints(p2, p1, TEST_PRECISION);
+ Assert.assertTrue(lA.isSimilarTo(lB));
+ Assert.assertTrue(!lA.isSimilarTo(Line3D.fromPoints(p1, p1.add(lA.getDirection().orthogonal()), TEST_PRECISION)));
+ }
+
+ @Test
+ public void testPointDistance() {
+ Line3D l = Line3D.fromPoints(Vector3D.of(0, 1, 1), Vector3D.of(0, 2, 2), TEST_PRECISION);
+ Assert.assertEquals(Math.sqrt(3.0 / 2.0), l.distance(Vector3D.of(1, 0, 1)), TEST_EPS);
+ Assert.assertEquals(0, l.distance(Vector3D.of(0, -4, -4)), TEST_EPS);
+ }
+
+ @Test
+ public void testLineDistance() {
+ Line3D l = Line3D.fromPoints(Vector3D.of(0, 1, 1), Vector3D.of(0, 2, 2), TEST_PRECISION);
+ Assert.assertEquals(1.0,
+ l.distance(Line3D.fromPoints(Vector3D.of(1, 0, 1), Vector3D.of(1, 0, 2), TEST_PRECISION)),
+ 1.0e-10);
+ Assert.assertEquals(0.5,
+ l.distance(Line3D.fromPoints(Vector3D.of(-0.5, 0, 0), Vector3D.of(-0.5, -1, -1), TEST_PRECISION)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.distance(l),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.distance(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(0, -5, -5), TEST_PRECISION)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.distance(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(0, -3, -4), TEST_PRECISION)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.distance(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(1, -4, -4), TEST_PRECISION)),
+ 1.0e-10);
+ Assert.assertEquals(Math.sqrt(8),
+ l.distance(Line3D.fromPoints(Vector3D.of(0, -4, 0), Vector3D.of(1, -4, 0), TEST_PRECISION)),
+ 1.0e-10);
+ }
+
+ @Test
+ public void testClosest() {
+ Line3D l = Line3D.fromPoints(Vector3D.of(0, 1, 1), Vector3D.of(0, 2, 2), TEST_PRECISION);
+ Assert.assertEquals(0.0,
+ l.closest(Line3D.fromPoints(Vector3D.of(1, 0, 1), Vector3D.of(1, 0, 2), TEST_PRECISION)).distance(Vector3D.of(0, 0, 0)),
+ 1.0e-10);
+ Assert.assertEquals(0.5,
+ l.closest(Line3D.fromPoints(Vector3D.of(-0.5, 0, 0), Vector3D.of(-0.5, -1, -1), TEST_PRECISION)).distance(Vector3D.of(-0.5, 0, 0)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.closest(l).distance(Vector3D.of(0, 0, 0)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.closest(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(0, -5, -5), TEST_PRECISION)).distance(Vector3D.of(0, 0, 0)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.closest(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(0, -3, -4), TEST_PRECISION)).distance(Vector3D.of(0, -4, -4)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.closest(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(1, -4, -4), TEST_PRECISION)).distance(Vector3D.of(0, -4, -4)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.closest(Line3D.fromPoints(Vector3D.of(0, -4, 0), Vector3D.of(1, -4, 0), TEST_PRECISION)).distance(Vector3D.of(0, -2, -2)),
+ 1.0e-10);
+ }
+
+ @Test
+ public void testIntersection() {
+ Line3D l = Line3D.fromPoints(Vector3D.of(0, 1, 1), Vector3D.of(0, 2, 2), TEST_PRECISION);
+ Assert.assertNull(l.intersection(Line3D.fromPoints(Vector3D.of(1, 0, 1), Vector3D.of(1, 0, 2), TEST_PRECISION)));
+ Assert.assertNull(l.intersection(Line3D.fromPoints(Vector3D.of(-0.5, 0, 0), Vector3D.of(-0.5, -1, -1), TEST_PRECISION)));
+ Assert.assertEquals(0.0,
+ l.intersection(l).distance(Vector3D.of(0, 0, 0)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.intersection(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(0, -5, -5), TEST_PRECISION)).distance(Vector3D.of(0, 0, 0)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.intersection(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(0, -3, -4), TEST_PRECISION)).distance(Vector3D.of(0, -4, -4)),
+ 1.0e-10);
+ Assert.assertEquals(0.0,
+ l.intersection(Line3D.fromPoints(Vector3D.of(0, -4, -4), Vector3D.of(1, -4, -4), TEST_PRECISION)).distance(Vector3D.of(0, -4, -4)),
+ 1.0e-10);
+ Assert.assertNull(l.intersection(Line3D.fromPoints(Vector3D.of(0, -4, 0), Vector3D.of(1, -4, 0), TEST_PRECISION)));
+ }
+
+ @Test
+ public void testReverse() {
+ // arrange
+ Line3D line = Line3D.fromPoints(Vector3D.of(1653345.6696423641, 6170370.041579291, 90000),
+ Vector3D.of(1650757.5050732433, 6160710.879908984, 0.9),
+ TEST_PRECISION);
+ Vector3D expected = line.getDirection().negate();
+
+ // act
+ Line3D reversed = line.reverse();
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(expected, reversed.getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testSpan() {
+ // arrange
+ Line3D line = Line3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Segment3D segment = line.span();
+
+ // assert
+ Assert.assertTrue(segment.isInfinite());
+ Assert.assertSame(line, segment.getLine());
+ Assert.assertTrue(segment.getInterval().isFull());
+ }
+
+ @Test
+ public void testSegment() {
+ // arrange
+ Line3D line = Line3D.fromPoints(Vector3D.of(0, 3, 0), Vector3D.of(1, 3, 0), TEST_PRECISION);
+ Interval interval = Interval.of(1, 2, TEST_PRECISION);
+
+ // act/assert
+ Segment3D intervalArgResult = line.segment(interval);
+ Assert.assertSame(line, intervalArgResult.getLine());
+ Assert.assertSame(interval, intervalArgResult.getInterval());
+
+ Segment3D doubleArgResult = line.segment(3, 4);
+ Assert.assertSame(line, doubleArgResult.getLine());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 3, 0), doubleArgResult.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4, 3, 0), doubleArgResult.getEndPoint(), TEST_EPS);
+
+ Segment3D ptArgResult = line.segment(Vector3D.of(0, 4, 0), Vector3D.of(2, 5, 1));
+ Assert.assertSame(line, ptArgResult.getLine());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 3, 0), ptArgResult.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 3, 0), ptArgResult.getEndPoint(), TEST_EPS);
+
+ Segment3D fromResult = line.segmentFrom(Vector3D.of(1, 4, 0));
+ Assert.assertSame(line, fromResult.getLine());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 3, 0), fromResult.getStartPoint(), TEST_EPS);
+ Assert.assertNull(fromResult.getEndPoint());
+
+ Segment3D toResult = line.segmentTo(Vector3D.of(1, 4, 0));
+ Assert.assertSame(line, toResult.getLine());
+ Assert.assertNull(toResult.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 3, 0), toResult.getEndPoint(), TEST_EPS);
+ }
+
+ @Test
+ public void testSubLine() {
+ // arrange
+ Line3D line = Line3D.fromPoints(Vector3D.of(0, 3, 0), Vector3D.of(1, 3, 0), TEST_PRECISION);
+
+ // act
+ SubLine3D sub = line.subline();
+
+ // assert
+ Assert.assertSame(line, sub.getLine());
+ Assert.assertTrue(sub.getSubspaceRegion().isEmpty());
+ }
+
+ @Test
+ public void testHashCode() {
+ // arrange
+ Line3D a = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION);
+ Line3D b = Line3D.fromPointAndDirection(Vector3D.of(1, 2, -1), Vector3D.of(4, 5, 6), TEST_PRECISION);
+ Line3D c = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, -1), TEST_PRECISION);
+ Line3D d = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), new EpsilonDoublePrecisionContext(TEST_EPS + 1e-3));
+
+ Line3D e = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), new EpsilonDoublePrecisionContext(TEST_EPS));
+
+ int hash = a.hashCode();
+
+ // act/assert
+ Assert.assertEquals(hash, a.hashCode());
+
+ Assert.assertNotEquals(hash, b.hashCode());
+ Assert.assertNotEquals(hash, c.hashCode());
+ Assert.assertNotEquals(hash, d.hashCode());
+
+ Assert.assertEquals(hash, e.hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // arrange
+ Line3D a = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION);
+ Line3D b = Line3D.fromPointAndDirection(Vector3D.of(1, 2, -1), Vector3D.of(4, 5, 6), TEST_PRECISION);
+ Line3D c = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, -1), TEST_PRECISION);
+ Line3D d = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), new EpsilonDoublePrecisionContext(TEST_EPS + 1e-3));
+
+ Line3D e = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), new EpsilonDoublePrecisionContext(TEST_EPS));
+
+ // act/assert
+ Assert.assertTrue(a.equals(a));
+
+ Assert.assertFalse(a.equals(null));
+ Assert.assertFalse(a.equals(new Object()));
+
+ Assert.assertFalse(a.equals(b));
+ Assert.assertFalse(a.equals(c));
+ Assert.assertFalse(a.equals(d));
+
+ Assert.assertTrue(a.equals(e));
+ Assert.assertTrue(e.equals(a));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ String str = line.toString();
+
+ // assert
+ Assert.assertTrue(str.contains("Line3D"));
+ Assert.assertTrue(str.matches(".*origin= \\(0(\\.0)?, 0(\\.0)?, 0(\\.0)?\\).*"));
+ Assert.assertTrue(str.matches(".*direction= \\(1(\\.0)?, 0(\\.0)?, 0(\\.0)?\\).*"));
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LineTest.java
deleted file mode 100644
index fe667e0..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LineTest.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * 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.threed;
-
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class LineTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testContains() {
- Vector3D p1 = Vector3D.of(0, 0, 1);
- Line l = new Line(p1, Vector3D.of(0, 0, 2), TEST_PRECISION);
- Assert.assertTrue(l.contains(p1));
- Assert.assertTrue(l.contains(Vector3D.linearCombination(1.0, p1, 0.3, l.getDirection())));
- Vector3D u = l.getDirection().orthogonal();
- Vector3D v = l.getDirection().cross(u);
- for (double alpha = 0; alpha < 2 * Math.PI; alpha += 0.3) {
- Assert.assertTrue(! l.contains(p1.add(Vector3D.linearCombination(Math.cos(alpha), u,
- Math.sin(alpha), v))));
- }
- }
-
- @Test
- public void testSimilar() {
- Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
- Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
- Line lA = new Line(p1, p2, TEST_PRECISION);
- Line lB = new Line(p2, p1, TEST_PRECISION);
- Assert.assertTrue(lA.isSimilarTo(lB));
- Assert.assertTrue(!lA.isSimilarTo(new Line(p1, p1.add(lA.getDirection().orthogonal()), TEST_PRECISION)));
- }
-
- @Test
- public void testPointDistance() {
- Line l = new Line(Vector3D.of(0, 1, 1), Vector3D.of(0, 2, 2), TEST_PRECISION);
- Assert.assertEquals(Math.sqrt(3.0 / 2.0), l.distance(Vector3D.of(1, 0, 1)), TEST_EPS);
- Assert.assertEquals(0, l.distance(Vector3D.of(0, -4, -4)), TEST_EPS);
- }
-
- @Test
- public void testLineDistance() {
- Line l = new Line(Vector3D.of(0, 1, 1), Vector3D.of(0, 2, 2), TEST_PRECISION);
- Assert.assertEquals(1.0,
- l.distance(new Line(Vector3D.of(1, 0, 1), Vector3D.of(1, 0, 2), TEST_PRECISION)),
- 1.0e-10);
- Assert.assertEquals(0.5,
- l.distance(new Line(Vector3D.of(-0.5, 0, 0), Vector3D.of(-0.5, -1, -1), TEST_PRECISION)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.distance(l),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.distance(new Line(Vector3D.of(0, -4, -4), Vector3D.of(0, -5, -5), TEST_PRECISION)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.distance(new Line(Vector3D.of(0, -4, -4), Vector3D.of(0, -3, -4), TEST_PRECISION)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.distance(new Line(Vector3D.of(0, -4, -4), Vector3D.of(1, -4, -4), TEST_PRECISION)),
- 1.0e-10);
- Assert.assertEquals(Math.sqrt(8),
- l.distance(new Line(Vector3D.of(0, -4, 0), Vector3D.of(1, -4, 0), TEST_PRECISION)),
- 1.0e-10);
- }
-
- @Test
- public void testClosest() {
- Line l = new Line(Vector3D.of(0, 1, 1), Vector3D.of(0, 2, 2), TEST_PRECISION);
- Assert.assertEquals(0.0,
- l.closestPoint(new Line(Vector3D.of(1, 0, 1), Vector3D.of(1, 0, 2), TEST_PRECISION)).distance(Vector3D.of(0, 0, 0)),
- 1.0e-10);
- Assert.assertEquals(0.5,
- l.closestPoint(new Line(Vector3D.of(-0.5, 0, 0), Vector3D.of(-0.5, -1, -1), TEST_PRECISION)).distance(Vector3D.of(-0.5, 0, 0)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.closestPoint(l).distance(Vector3D.of(0, 0, 0)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.closestPoint(new Line(Vector3D.of(0, -4, -4), Vector3D.of(0, -5, -5), TEST_PRECISION)).distance(Vector3D.of(0, 0, 0)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.closestPoint(new Line(Vector3D.of(0, -4, -4), Vector3D.of(0, -3, -4), TEST_PRECISION)).distance(Vector3D.of(0, -4, -4)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.closestPoint(new Line(Vector3D.of(0, -4, -4), Vector3D.of(1, -4, -4), TEST_PRECISION)).distance(Vector3D.of(0, -4, -4)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.closestPoint(new Line(Vector3D.of(0, -4, 0), Vector3D.of(1, -4, 0), TEST_PRECISION)).distance(Vector3D.of(0, -2, -2)),
- 1.0e-10);
- }
-
- @Test
- public void testIntersection() {
- Line l = new Line(Vector3D.of(0, 1, 1), Vector3D.of(0, 2, 2), TEST_PRECISION);
- Assert.assertNull(l.intersection(new Line(Vector3D.of(1, 0, 1), Vector3D.of(1, 0, 2), TEST_PRECISION)));
- Assert.assertNull(l.intersection(new Line(Vector3D.of(-0.5, 0, 0), Vector3D.of(-0.5, -1, -1), TEST_PRECISION)));
- Assert.assertEquals(0.0,
- l.intersection(l).distance(Vector3D.of(0, 0, 0)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.intersection(new Line(Vector3D.of(0, -4, -4), Vector3D.of(0, -5, -5), TEST_PRECISION)).distance(Vector3D.of(0, 0, 0)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.intersection(new Line(Vector3D.of(0, -4, -4), Vector3D.of(0, -3, -4), TEST_PRECISION)).distance(Vector3D.of(0, -4, -4)),
- 1.0e-10);
- Assert.assertEquals(0.0,
- l.intersection(new Line(Vector3D.of(0, -4, -4), Vector3D.of(1, -4, -4), TEST_PRECISION)).distance(Vector3D.of(0, -4, -4)),
- 1.0e-10);
- Assert.assertNull(l.intersection(new Line(Vector3D.of(0, -4, 0), Vector3D.of(1, -4, 0), TEST_PRECISION)));
- }
-
- @Test
- public void testRevert() {
-
- // setup
- Line line = new Line(Vector3D.of(1653345.6696423641, 6170370.041579291, 90000),
- Vector3D.of(1650757.5050732433, 6160710.879908984, 0.9),
- TEST_PRECISION);
- Vector3D expected = line.getDirection().negate();
-
- // action
- Line reverted = line.revert();
-
- // verify
- Assert.assertArrayEquals(expected.toArray(), reverted.getDirection().toArray(), 0);
-
- }
-
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/OBJWriter.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/OBJWriter.java
deleted file mode 100644
index 4346306..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/OBJWriter.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * 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.threed;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.Writer;
-import java.nio.file.Files;
-import java.text.DecimalFormat;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
-import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
-
-/** This class creates simple OBJ files from {@link PolyhedronsSet} instances.
- * The output files can be opened in a 3D viewer for visual debugging of 3D
- * regions. This class is only intended for use in testing.
- *
- * @see https://en.wikipedia.org/wiki/Wavefront_.obj_file
- */
-public class OBJWriter {
-
- /** Writes an OBJ file representing the given {@link PolyhedronsSet}. Only
- * finite boundaries are written. Infinite boundaries are ignored.
- * @param file The path of the file to write
- * @param poly The input PolyhedronsSet
- * @throws IOException
- */
- public static void write(String file, PolyhedronsSet poly) throws IOException {
- write(new File(file), poly);
- }
-
- /** Writes an OBJ file representing the given {@link PolyhedronsSet}. Only
- * finite boundaries are written. Infinite boundaries are ignored.
- * @param file The file to write
- * @param poly The input PolyhedronsSet
- * @throws IOException
- */
- public static void write(File file, PolyhedronsSet poly) throws IOException {
- // get the vertices and faces
- MeshBuilder meshBuilder = new MeshBuilder(poly.getPrecision());
- poly.getTree(true).visit(meshBuilder);
-
- // write them to the file
- try (Writer writer = Files.newBufferedWriter(file.toPath())) {
- writer.write("# Generated by " + OBJWriter.class.getName() + " on " + new Date() + "\n");
- writeVertices(writer, meshBuilder.getVertices());
- writeFaces(writer, meshBuilder.getFaces());
- }
- }
-
- /** Writes the given list of vertices to the file in the OBJ format.
- * @param writer
- * @param vertices
- * @throws IOException
- */
- private static void writeVertices(Writer writer, List<Vector3D> vertices) throws IOException {
- DecimalFormat df = new DecimalFormat("0.######");
-
- for (Vector3D v : vertices) {
- writer.write("v ");
- writer.write(df.format(v.getX()));
- writer.write(" ");
- writer.write(df.format(v.getY()));
- writer.write(" ");
- writer.write(df.format(v.getZ()));
- writer.write("\n");
- }
- }
-
- /** Writes the given list of face vertex indices to the file in the OBJ format. The indices
- * are expected to be 0-based and are converted to the 1-based format used by OBJ.
- * @param writer
- * @param faces
- * @throws IOException
- */
- private static void writeFaces(Writer writer, List<int[]> faces) throws IOException {
- for (int[] face : faces) {
- writer.write("f ");
- for (int idx : face) {
- writer.write(String.valueOf(idx + 1)); // obj indices are 1-based
- writer.write(" ");
- }
- writer.write("\n");
- }
- }
-
- /** Class used to impose a strict sorting on 3D vertices.
- * If all of the components of two vertices are within tolerance of each
- * other, then the vertices are considered equal. This helps to avoid
- * writing duplicate vertices in the OBJ output.
- */
- private static class VertexComparator implements Comparator<Vector3D> {
-
- /** Precision context to deteremine floating-point equality */
- private final DoublePrecisionContext precision;
-
- /** Creates a new instance with the given tolerance value.
- * @param tolerance
- */
- public VertexComparator(final DoublePrecisionContext precision) {
- this.precision = precision;
- }
-
- /** {@inheritDoc} */
- @Override
- public int compare(Vector3D a, Vector3D b) {
- int result = precision.compare(a.getX(), b.getX());
- if (result == 0) {
- result = precision.compare(a.getY(), b.getY());
- if (result == 0) {
- result = precision.compare(a.getZ(), b.getZ());
- }
- }
- return result;
- }
- }
-
- /** Class for converting a 3D BSPTree into a list of vertices
- * and face vertex indices.
- */
- private static class MeshBuilder implements BSPTreeVisitor<Vector3D> {
-
- /** Precision context to deteremine floating-point equality */
- private final DoublePrecisionContext precision;
-
- /** Map of vertices to their index in the vertices list */
- private Map<Vector3D, Integer> vertexIndexMap;
-
- /** List of unique vertices in the BSPTree boundary */
- private List<Vector3D> vertices;
-
- /**
- * List of face vertex indices. Each face will have 3 indices. Indices
- * are 0-based.
- * */
- private List<int[]> faces;
-
- /** Creates a new instance with the given tolerance.
- * @param tolerance
- */
- public MeshBuilder(final DoublePrecisionContext precision) {
- this.precision = precision;
- this.vertexIndexMap = new TreeMap<>(new VertexComparator(precision));
- this.vertices = new ArrayList<>();
- this.faces = new ArrayList<>();
- }
-
- /** Returns the list of unique vertices found in the BSPTree.
- * @return
- */
- public List<Vector3D> getVertices() {
- return vertices;
- }
-
- /** Returns the list of 0-based face vertex indices for the BSPTree. Each face is
- * a triangle with 3 indices.
- * @return
- */
- public List<int[]> getFaces() {
- return faces;
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(BSPTree<Vector3D> node) {
- return Order.SUB_MINUS_PLUS;
- }
-
- /** {@inheritDoc} */
- @SuppressWarnings("unchecked")
- @Override
- public void visitInternalNode(BSPTree<Vector3D> node) {
- BoundaryAttribute<Vector3D> attr = (BoundaryAttribute<Vector3D>) node.getAttribute();
-
- if (attr.getPlusOutside() != null) {
- addBoundary((SubPlane) attr.getPlusOutside());
- }
- else if (attr.getPlusInside() != null) {
- addBoundary((SubPlane) attr.getPlusInside());
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(BSPTree<Vector3D> node) {
- // do nothing
- }
-
- /** Adds the region boundary defined by the given {@link SubPlane}
- * to the mesh.
- * @param subplane
- */
- private void addBoundary(SubPlane subplane) {
- Plane plane = (Plane) subplane.getHyperplane();
- PolygonsSet poly = (PolygonsSet) subplane.getRemainingRegion();
-
- TriangleExtractor triExtractor = new TriangleExtractor(precision);
- poly.getTree(true).visit(triExtractor);
-
- Vector3D v1, v2, v3;
- for (Vector2D[] tri : triExtractor.getTriangles()) {
- v1 = plane.toSpace(tri[0]);
- v2 = plane.toSpace(tri[1]);
- v3 = plane.toSpace(tri[2]);
-
- faces.add(new int[] {
- getVertexIndex(v1),
- getVertexIndex(v2),
- getVertexIndex(v3)
- });
- }
- }
-
- /** Returns the 0-based index of the given vertex in the <code>vertices</code>
- * list. If the vertex has not been encountered before, it is added
- * to the list.
- * @param vertex
- * @return
- */
- private int getVertexIndex(Vector3D vertex) {
- Integer idx = vertexIndexMap.get(vertex);
- if (idx == null) {
- idx = vertices.size();
-
- vertices.add(vertex);
- vertexIndexMap.put(vertex, idx);
- }
- return idx.intValue();
- }
- }
-
- /** Visitor for extracting a collection of triangles from a 2D BSPTree.
- */
- private static class TriangleExtractor implements BSPTreeVisitor<Vector2D> {
-
- /** Precision context to deteremine floating-point equality */
- private final DoublePrecisionContext precision;
-
- /** List of extracted triangles */
- private List<Vector2D[]> triangles = new ArrayList<>();
-
- /** Creates a new instance with the given geometric tolerance.
- * @param tolerance
- */
- public TriangleExtractor(final DoublePrecisionContext precision) {
- this.precision = precision;
- }
-
- /** Returns the list of extracted triangles.
- * @return
- */
- public List<Vector2D[]> getTriangles() {
- return triangles;
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(BSPTree<Vector2D> node) {
- return Order.SUB_MINUS_PLUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(BSPTree<Vector2D> node) {
- // do nothing
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(BSPTree<Vector2D> node) {
- if ((Boolean) node.getAttribute()) {
- PolygonsSet convexPoly = new PolygonsSet(node.pruneAroundConvexCell(Boolean.TRUE,
- Boolean.FALSE, null), precision);
-
- for (Vector2D[] loop : convexPoly.getVertices()) {
- if (loop.length > 0 && loop[0] != null) { // skip unclosed loops
- addTriangles(loop);
- }
- }
- }
- }
-
- /** Splits the 2D convex area defined by the given vertices into
- * triangles and adds them to the internal list.
- * @param vertices
- */
- private void addTriangles(Vector2D[] vertices) {
- // use a triangle fan to add the convex region
- for (int i=2; i<vertices.length; ++i) {
- triangles.add(new Vector2D[] { vertices[0], vertices[i-1], vertices[i] });
- }
- }
- }
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PLYParser.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PLYParser.java
deleted file mode 100644
index b1d6e35..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PLYParser.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * 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.threed;
-
-import java.io.BufferedReader;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.StringTokenizer;
-
-import org.apache.commons.numbers.core.Precision;
-
-/** This class is a small and incomplete parser for PLY files.
- * <p>
- * This parser is only intended for test purposes, it does not
- * parse the full header, it does not handle all properties,
- * it has rudimentary error handling.
- * </p>
- */
-public class PLYParser {
-
- /** Parsed vertices. */
- private Vector3D[] vertices;
-
- /** Parsed faces. */
- private int[][] faces;
-
- /** Reader for PLY data. */
- private BufferedReader br;
-
- /** Last parsed line. */
- private String line;
-
- /** Simple constructor.
- * @param stream stream to parse (closing it remains caller responsibility)
- * @exception IOException if stream cannot be read
- * @exception ParseException if stream content cannot be parsed
- */
- public PLYParser(final InputStream stream)
- throws IOException, ParseException {
-
- try {
- br = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
-
- // parse the header
- List<Field> fields = parseNextLine();
- if (fields.size() != 1 || fields.get(0).getToken() != Token.PLY) {
- complain();
- }
-
- boolean parsing = true;
- int nbVertices = -1;
- int nbFaces = -1;
- int xIndex = -1;
- int yIndex = -1;
- int zIndex = -1;
- int vPropertiesNumber = -1;
- boolean inVertexElt = false;
- boolean inFaceElt = false;
- while (parsing) {
- fields = parseNextLine();
- if (fields.size() < 1) {
- complain();
- }
- switch (fields.get(0).getToken()) {
- case FORMAT:
- if (fields.size() != 3 ||
- fields.get(1).getToken() != Token.ASCII ||
- fields.get(2).getToken() != Token.UNKNOWN ||
- !Precision.equals(Double.parseDouble(fields.get(2).getValue()), 1.0, 0.001)) {
- complain();
- }
- inVertexElt = false;
- inFaceElt = false;
- break;
- case COMMENT:
- // we just ignore this line
- break;
- case ELEMENT:
- if (fields.size() != 3 ||
- (fields.get(1).getToken() != Token.VERTEX && fields.get(1).getToken() != Token.FACE) ||
- fields.get(2).getToken() != Token.UNKNOWN) {
- complain();
- }
- if (fields.get(1).getToken() == Token.VERTEX) {
- nbVertices = Integer.parseInt(fields.get(2).getValue());
- inVertexElt = true;
- inFaceElt = false;
- } else {
- nbFaces = Integer.parseInt(fields.get(2).getValue());
- inVertexElt = false;
- inFaceElt = true;
- }
- break;
- case PROPERTY:
- if (inVertexElt) {
- ++vPropertiesNumber;
- if (fields.size() != 3 ||
- (fields.get(1).getToken() != Token.CHAR &&
- fields.get(1).getToken() != Token.UCHAR &&
- fields.get(1).getToken() != Token.SHORT &&
- fields.get(1).getToken() != Token.USHORT &&
- fields.get(1).getToken() != Token.INT &&
- fields.get(1).getToken() != Token.UINT &&
- fields.get(1).getToken() != Token.FLOAT &&
- fields.get(1).getToken() != Token.DOUBLE)) {
- complain();
- }
- if (fields.get(2).getToken() == Token.X) {
- xIndex = vPropertiesNumber;
- }else if (fields.get(2).getToken() == Token.Y) {
- yIndex = vPropertiesNumber;
- }else if (fields.get(2).getToken() == Token.Z) {
- zIndex = vPropertiesNumber;
- }
- } else if (inFaceElt) {
- if (fields.size() != 5 ||
- fields.get(1).getToken() != Token.LIST &&
- (fields.get(2).getToken() != Token.CHAR &&
- fields.get(2).getToken() != Token.UCHAR &&
- fields.get(2).getToken() != Token.SHORT &&
- fields.get(2).getToken() != Token.USHORT &&
- fields.get(2).getToken() != Token.INT &&
- fields.get(2).getToken() != Token.UINT) ||
- (fields.get(3).getToken() != Token.CHAR &&
- fields.get(3).getToken() != Token.UCHAR &&
- fields.get(3).getToken() != Token.SHORT &&
- fields.get(3).getToken() != Token.USHORT &&
- fields.get(3).getToken() != Token.INT &&
- fields.get(3).getToken() != Token.UINT) ||
- fields.get(4).getToken() != Token.VERTEX_INDICES) {
- complain();
- }
- } else {
- complain();
- }
- break;
- case END_HEADER:
- inVertexElt = false;
- inFaceElt = false;
- parsing = false;
- break;
- default:
- throw new ParseException("unable to parse line: " + line, 0);
- }
- }
- ++vPropertiesNumber;
-
- // parse vertices
- vertices = new Vector3D[nbVertices];
- for (int i = 0; i < nbVertices; ++i) {
- fields = parseNextLine();
- if (fields.size() != vPropertiesNumber ||
- fields.get(xIndex).getToken() != Token.UNKNOWN ||
- fields.get(yIndex).getToken() != Token.UNKNOWN ||
- fields.get(zIndex).getToken() != Token.UNKNOWN) {
- complain();
- }
- vertices[i] = Vector3D.of(Double.parseDouble(fields.get(xIndex).getValue()),
- Double.parseDouble(fields.get(yIndex).getValue()),
- Double.parseDouble(fields.get(zIndex).getValue()));
- }
-
- // parse faces
- faces = new int[nbFaces][];
- for (int i = 0; i < nbFaces; ++i) {
- fields = parseNextLine();
- if (fields.isEmpty() ||
- fields.size() != (Integer.parseInt(fields.get(0).getValue()) + 1)) {
- complain();
- }
- faces[i] = new int[fields.size() - 1];
- for (int j = 0; j < faces[i].length; ++j) {
- faces[i][j] = Integer.parseInt(fields.get(j + 1).getValue());
- }
- }
-
- } catch (NumberFormatException nfe) {
- complain();
- }
- }
-
- /** Complain about a bad line.
- * @exception ParseException always thrown
- */
- private void complain() throws ParseException {
- throw new ParseException("unable to parse line: " + line, 0);
- }
-
- /** Parse next line.
- * @return parsed fields
- * @exception IOException if stream cannot be read
- * @exception ParseException if the line does not contain the expected number of fields
- */
- private List<Field> parseNextLine()
- throws IOException, ParseException {
- final List<Field> fields = new ArrayList<>();
- line = br.readLine();
- if (line == null) {
- throw new EOFException();
- }
- final StringTokenizer tokenizer = new StringTokenizer(line);
- while (tokenizer.hasMoreTokens()) {
- fields.add(new Field(tokenizer.nextToken()));
- }
- return fields;
- }
-
- /** Get the parsed vertices.
- * @return parsed vertices
- */
- public List<Vector3D> getVertices() {
- return Arrays.asList(vertices);
- }
-
- /** Get the parsed faces.
- * @return parsed faces
- */
- public List<int[]> getFaces() {
- return Arrays.asList(faces);
- }
-
- /** Tokens from PLY files. */
- private static enum Token {
- PLY, FORMAT, ASCII, BINARY_BIG_ENDIAN, BINARY_LITTLE_ENDIAN,
- COMMENT, ELEMENT, VERTEX, FACE, PROPERTY, LIST, OBJ_INFO,
- CHAR, UCHAR, SHORT, USHORT, INT, UINT, FLOAT, DOUBLE,
- X, Y, Z, VERTEX_INDICES, END_HEADER, UNKNOWN;
- }
-
- /** Parsed line fields. */
- private static class Field {
-
- /** Token. */
- private final Token token;
-
- /** Value. */
- private final String value;
-
- /** Simple constructor.
- * @param value field value
- */
- public Field(final String value) {
- Token parsedToken = null;
- try {
- parsedToken = Token.valueOf(value.toUpperCase());
- } catch (IllegalArgumentException iae) {
- parsedToken = Token.UNKNOWN;
- }
- this.token = parsedToken;
- this.value = value;
- }
-
- /** Get the recognized token.
- * @return recognized token
- */
- public Token getToken() {
- return token;
- }
-
- /** Get the field value.
- * @return field value
- */
- public String getValue() {
- return value;
- }
-
- }
-
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java
index d28c713..55d0930 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java
@@ -20,17 +20,20 @@
import java.util.Arrays;
import java.util.List;
+import org.apache.commons.geometry.core.Geometry;
import org.apache.commons.geometry.core.GeometryTestUtils;
import org.apache.commons.geometry.core.exception.GeometryException;
import org.apache.commons.geometry.core.exception.IllegalNormException;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.Plane.SubspaceTransform;
import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.junit.Assert;
import org.junit.Test;
-@SuppressWarnings("javadoc")
public class PlaneTest {
private static final double TEST_EPS = 1e-10;
@@ -38,178 +41,158 @@
private static final DoublePrecisionContext TEST_PRECISION =
new EpsilonDoublePrecisionContext(TEST_EPS);
- @Test(expected=IllegalNormException.class)
- public void testUAndVAreIdentical() {
- Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
- }
+ @Test
+ public void testFromNormal() {
+ // act/assert
+ checkPlane(Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION),
+ Vector3D.ZERO, Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_Y);
+ checkPlane(Plane.fromNormal(Vector3D.of(7, 0, 0), TEST_PRECISION),
+ Vector3D.ZERO, Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_Y);
- @Test(expected=IllegalNormException.class)
- public void testUAndVAreCollinear() {
- Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 2), TEST_PRECISION);
- }
+ checkPlane(Plane.fromNormal(Vector3D.Unit.PLUS_Y, TEST_PRECISION),
+ Vector3D.ZERO, Vector3D.Unit.MINUS_Z, Vector3D.Unit.MINUS_X);
+ checkPlane(Plane.fromNormal(Vector3D.of(0, 5, 0), TEST_PRECISION),
+ Vector3D.ZERO, Vector3D.Unit.MINUS_Z, Vector3D.Unit.MINUS_X);
- @Test(expected=IllegalNormException.class)
- public void testUAndVAreCollinear2() {
- Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), Vector3D.of(0, 0, -2), TEST_PRECISION);
- }
-
- @Test(expected=GeometryException.class)
- public void testPointsDoNotConstituteAPlane() {
- Plane.fromPoints(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), Vector3D.of(0, 1, 0), TEST_PRECISION);
+ checkPlane(Plane.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION),
+ Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X);
+ checkPlane(Plane.fromNormal(Vector3D.of(0, 0, 0.01), TEST_PRECISION),
+ Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X);
}
@Test
- public void testContains() {
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
- Assert.assertTrue(plane.contains(Vector3D.of(0, 0, 1)));
- Assert.assertTrue(plane.contains(Vector3D.of(17, -32, 1)));
- Assert.assertTrue(! plane.contains(Vector3D.of(17, -32, 1.001)));
+ public void testFromNormal_illegalArguments() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromNormal(Vector3D.ZERO, TEST_PRECISION);
+ }, IllegalNormException.class);
}
@Test
- public void testContainsLine() {
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
- Line line = new Line(Vector3D.of(1, 0, 1), Vector3D.of(2, 0, 1), TEST_PRECISION);
- Assert.assertTrue(plane.contains(line));
- }
+ public void testFromPointAndNormal() {
+ // arrange
+ Vector3D pt = Vector3D.of(1, 2, 3);
- @Test(expected=IllegalNormException.class)
- public void testFromPointPlaneVectorsWithZeroVector()
- {
- Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.ZERO, Vector3D.of(1,0,0), TEST_PRECISION);
- }
-
- @Test(expected=IllegalNormException.class)
- public void testFromPointAndNormalWithZeroNormal()
- {
- Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.ZERO, TEST_PRECISION);
- }
-
- @Test(expected=IllegalNormException.class)
- public void testFromNormal()
- {
- Plane.fromNormal(Vector3D.ZERO, TEST_PRECISION);
+ // act/assert
+ checkPlane(Plane.fromPointAndNormal(pt, Vector3D.of(0.1, 0, 0), TEST_PRECISION),
+ Vector3D.of(1, 0, 0), Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_Y);
+ checkPlane(Plane.fromPointAndNormal(pt, Vector3D.of(0, 2, 0), TEST_PRECISION),
+ Vector3D.of(0, 2, 0), Vector3D.Unit.MINUS_Z, Vector3D.Unit.MINUS_X);
+ checkPlane(Plane.fromPointAndNormal(pt, Vector3D.of(0, 0, 5), TEST_PRECISION),
+ Vector3D.of(0, 0, 3), Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X);
}
@Test
- public void testIsParallelAndGetOffset()
- {
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
- Line parallelLine = new Line(Vector3D.of(1, 0, 2), Vector3D.of(2, 0, 2), TEST_PRECISION);
- Assert.assertTrue(plane.isParallel(parallelLine));
- Assert.assertEquals(1.0, plane.getOffset(parallelLine), TEST_EPS);
- Line nonParallelLine = new Line(Vector3D.of(1, 0, 2), Vector3D.of(2, 0, 1), TEST_PRECISION);
- Assert.assertFalse(plane.isParallel(nonParallelLine));
- Assert.assertEquals(0.0, plane.getOffset(nonParallelLine), TEST_EPS);
+ public void testFromPointAndNormal_illegalArguments() {
+ // arrange
+ Vector3D pt = Vector3D.of(1, 2, 3);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPointAndNormal(pt, Vector3D.ZERO, TEST_PRECISION);
+ }, IllegalNormException.class);
}
@Test
- public void testCreation()
- {
- Vector3D normalAliasW = Vector3D.of(0, 0, 1);
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1),normalAliasW , TEST_PRECISION);
- Assert.assertEquals(normalAliasW, plane.getW());
- double expectedX = 1.0;
- Assert.assertEquals(expectedX, Math.abs(plane.getV().getX()), TEST_EPS);
- double expectedY = 1.0;
- Assert.assertEquals(expectedY, Math.abs(plane.getU().getY()), TEST_EPS);
- Assert.assertEquals(-1.0, plane.getOriginOffset(), TEST_EPS);
- Vector3D expectedOrigin = Vector3D.of(0, 0, 1);
- Assert.assertEquals(expectedOrigin, plane.getOrigin());
+ public void testFromPointAndPlaneVectors() {
+ // arrange
+ Vector3D pt = Vector3D.of(1, 2, 3);
+
+ // act/assert
+ checkPlane(Plane.fromPointAndPlaneVectors(pt, Vector3D.of(2, 0, 0), Vector3D.of(3, 0.1, 0), TEST_PRECISION),
+ Vector3D.of(0, 0, 3), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y);
+
+ checkPlane(Plane.fromPointAndPlaneVectors(pt, Vector3D.of(2, 0, 0), Vector3D.of(3, -0.1, 0), TEST_PRECISION),
+ Vector3D.of(0, 0, 3), Vector3D.Unit.PLUS_X, Vector3D.Unit.MINUS_Y);
+
+ checkPlane(Plane.fromPointAndPlaneVectors(pt, Vector3D.of(0, 0.1, 0), Vector3D.of(0, -3, 1), TEST_PRECISION),
+ Vector3D.of(1, 0, 0), Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z);
}
@Test
- public void testReverse()
- {
- Vector3D normalAliasW = Vector3D.of(0, 0, 1);
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1),normalAliasW , TEST_PRECISION);
- Assert.assertEquals(-1.0, plane.getOriginOffset(), TEST_EPS);
- Vector3D p1 = Vector3D.of(1,0,1);
- Assert.assertTrue(plane.contains(p1));
- Plane reversePlane = plane.reverse();
- Assert.assertEquals(1.0, reversePlane.getOriginOffset(), TEST_EPS);
- Vector3D p1XYswapped = Vector3D.of(0,1,1);
- Assert.assertTrue(reversePlane.contains(p1XYswapped));
+ public void testFromPointAndPlaneVectors_illegalArguments() {
+ // arrange
+ Vector3D pt = Vector3D.of(1, 2, 3);
+
+ // act/assert
+
+ // identical vectors
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPointAndPlaneVectors(pt, Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
+ }, IllegalNormException.class);
+
+ // zero vector
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPointAndPlaneVectors(pt, Vector3D.of(0, 0, 1), Vector3D.ZERO, TEST_PRECISION);
+ }, IllegalNormException.class);
+
+ // collinear vectors
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPointAndPlaneVectors(pt, Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 2), TEST_PRECISION);
+ }, IllegalNormException.class);
+
+ // collinear vectors - reversed
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPointAndPlaneVectors(pt, Vector3D.of(0, 0, 1), Vector3D.of(0, 0, -2), TEST_PRECISION);
+ }, IllegalNormException.class);
}
@Test
- public void testIsPlaneParallel()
- {
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
- Plane parallelPlane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
- Plane parallelPlane2 = Plane.fromPointAndNormal(Vector3D.of(0, 0, 2), Vector3D.of(0, 0, 1), TEST_PRECISION);
- Assert.assertTrue(plane.isParallel(parallelPlane));
- Assert.assertTrue(plane.isParallel(parallelPlane2));
- Plane nonParallelPlane = Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.of(1, 1.5, 1), Vector3D.of(0,1,1), TEST_PRECISION);
- Assert.assertFalse(plane.isParallel(nonParallelPlane));
+ public void testFromPoints() {
+ // arrange
+ Vector3D a = Vector3D.of(1, 1, 1);
+ Vector3D b = Vector3D.of(1, 1, 4.3);
+ Vector3D c = Vector3D.of(2.5, 1, 1);
+
+ // act/assert
+ checkPlane(Plane.fromPoints(a, b, c, TEST_PRECISION),
+ Vector3D.of(0, 1, 0), Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X);
+
+ checkPlane(Plane.fromPoints(a, c, b, TEST_PRECISION),
+ Vector3D.of(0, 1, 0), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z);
}
@Test
- public void testProjectLine() {
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
- Line line = new Line(Vector3D.of(1, 0, 1), Vector3D.of(2, 0, 2), TEST_PRECISION);
- Line expectedProjection = new Line(Vector3D.of(1, 0, 1),Vector3D.of(2, 0, 1), TEST_PRECISION);
- Assert.assertEquals(expectedProjection, plane.project(line));
- }
-
- @Test
- public void testOffset() {
- Vector3D p1 = Vector3D.of(1, 1, 1);
- Plane plane = Plane.fromPointAndNormal(p1, Vector3D.of(0.2, 0, 0), TEST_PRECISION);
- Assert.assertEquals(-5.0, plane.getOffset(Vector3D.of(-4, 0, 0)), TEST_EPS);
- Assert.assertEquals(+5.0, plane.getOffset(Vector3D.of(6, 10, -12)), TEST_EPS);
- Assert.assertEquals(0.3,
- plane.getOffset(Vector3D.linearCombination(1.0, p1, 0.3, plane.getNormal())),
- TEST_EPS);
- Assert.assertEquals(-0.3,
- plane.getOffset(Vector3D.linearCombination(1.0, p1, -0.3, plane.getNormal())),
- TEST_EPS);
- }
-
- @Test(expected=IllegalNormException.class)
- public void testVectorsAreColinear()
- {
- Plane.fromPointAndPlaneVectors(Vector3D.of(1, 1, 1), Vector3D.of(2, 0, 0), Vector3D.of(2,0,0), TEST_PRECISION);
- }
-
-
- @Test
- public void testVectorsAreNormalizedForSuppliedUAndV() {
- Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.of(1, 1, 1), Vector3D.of(2, 0, 0), Vector3D.of(0,2,0), TEST_PRECISION);
- Assert.assertEquals(1.0, plane.getNormal().norm(), TEST_EPS);
- Assert.assertEquals(1.0, plane.getV().norm(), TEST_EPS);
- Assert.assertEquals(1.0, plane.getU().norm(), TEST_EPS);
- }
-
-
-
- @Test
- public void testVectorsAreNormalized() {
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(2, -3, 1), Vector3D.of(1, 4, 9), TEST_PRECISION);
- Assert.assertEquals(1.0, plane.getNormal().norm(), TEST_EPS);
- Assert.assertEquals(1.0, plane.getV().norm(), TEST_EPS);
- Assert.assertEquals(1.0, plane.getU().norm(), TEST_EPS);
- }
-
-
- @Test
- public void testPoint() {
- Plane plane = Plane.fromPointAndNormal(Vector3D.of(2, -3, 1), Vector3D.of(1, 4, 9), TEST_PRECISION);
- Assert.assertTrue(plane.contains(plane.getOrigin()));
- }
-
- @Test
- public void testThreePoints() {
+ public void testFromPoints_planeContainsSourcePoints() {
+ // arrange
Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
- Plane plane = Plane.fromPoints(p1, p2, p3, TEST_PRECISION);
+
+ // act
+ Plane plane = Plane.fromPoints(p1, p2, p3, TEST_PRECISION);
+
+ // assert
Assert.assertTrue(plane.contains(p1));
Assert.assertTrue(plane.contains(p2));
Assert.assertTrue(plane.contains(p3));
}
@Test
+ public void testFromPoints_illegalArguments() {
+ // arrange
+ Vector3D a = Vector3D.of(1, 0, 0);
+ Vector3D b = Vector3D.of(0, 1, 0);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPoints(a, a, a, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPoints(a, a, b, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPoints(a, b, a, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Plane.fromPoints(b, a, a, TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
public void testFromPoints_collection_threePoints() {
// arrange
List<Vector3D> pts = Arrays.asList(
@@ -450,105 +433,655 @@
}
@Test
- public void testRotate() {
+ public void testContains_point() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
+ double halfEps = 0.5 * TEST_EPS;
+
+ // act/assert
+ EuclideanTestUtils.permute(-100, 100, 5, (x, y) -> {
+
+ Assert.assertTrue(plane.contains(Vector3D.of(x, y, 1)));
+ Assert.assertTrue(plane.contains(Vector3D.of(x, y, 1 + halfEps)));
+ Assert.assertTrue(plane.contains(Vector3D.of(x, y, 1 - halfEps)));
+
+ Assert.assertFalse(plane.contains(Vector3D.of(x, y, 0.5)));
+ Assert.assertFalse(plane.contains(Vector3D.of(x, y, 1.5)));
+ });
+ }
+
+ @Test
+ public void testContains_line() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(plane.contains(
+ Line3D.fromPoints(Vector3D.of(1, 0, 0), Vector3D.of(2, 0, 0), TEST_PRECISION)));
+ Assert.assertTrue(plane.contains(
+ Line3D.fromPoints(Vector3D.of(-1, 0, 0), Vector3D.of(-2, 0, 0), TEST_PRECISION)));
+
+ Assert.assertFalse(plane.contains(
+ Line3D.fromPoints(Vector3D.of(1, 0, 2), Vector3D.of(2, 0, 2), TEST_PRECISION)));
+ Assert.assertFalse(plane.contains(
+ Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(2, 0, 2), TEST_PRECISION)));
+ }
+
+ @Test
+ public void testContains_plane() {
+ // arrange
Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
- Plane plane = Plane.fromPoints(p1, p2, p3, TEST_PRECISION);
+ Plane planeA = Plane.fromPoints(p1, p2, p3, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(planeA.contains(planeA));
+ Assert.assertTrue(planeA.contains(Plane.fromPoints(p1, p3, p2, TEST_PRECISION)));
+ Assert.assertTrue(planeA.contains(Plane.fromPoints(p3, p1, p2, TEST_PRECISION)));
+ Assert.assertTrue(planeA.contains(Plane.fromPoints(p3, p2, p1, TEST_PRECISION)));
+
+ Assert.assertFalse(planeA.contains(Plane.fromPoints(p1, Vector3D.of(11.4, -3.8, 5.1), p2, TEST_PRECISION)));
+
+ Vector3D offset = planeA.getNormal().multiply(1e-8);
+ Assert.assertFalse(planeA.contains(Plane.fromPoints(p1.add(offset), p2, p3, TEST_PRECISION)));
+ Assert.assertFalse(planeA.contains(Plane.fromPoints(p1, p2.add(offset), p3, TEST_PRECISION)));
+ Assert.assertFalse(planeA.contains(Plane.fromPoints(p1, p2, p3.add(offset), TEST_PRECISION)));
+
+ Assert.assertFalse(planeA.contains(Plane.fromPoints(p1.add(offset),
+ p2.add(offset),
+ p3.add(offset), TEST_PRECISION)));
+ }
+
+ @Test
+ public void testReverse() {
+ // arrange
+ Vector3D pt = Vector3D.of(0, 0, 1);
+ Plane plane = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // act
+ Plane reversed = plane.reverse();
+
+ // assert
+ checkPlane(reversed, pt, Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_X);
+
+ Assert.assertTrue(reversed.contains(Vector3D.of(1, 1, 1)));
+ Assert.assertTrue(reversed.contains(Vector3D.of(-1, -1, 1)));
+ Assert.assertFalse(reversed.contains(Vector3D.ZERO));
+
+ Assert.assertEquals(1.0, reversed.offset(Vector3D.ZERO), TEST_EPS);
+ }
+
+ @Test
+ public void testIsParallelAndOffset_line() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
+
+ Line3D parallelLine = Line3D.fromPoints(Vector3D.of(1, 0, 2), Vector3D.of(2, 0, 2), TEST_PRECISION);
+ Line3D nonParallelLine = Line3D.fromPoints(Vector3D.of(1, 0, 2), Vector3D.of(2, 0, 1), TEST_PRECISION);
+ Line3D containedLine = Line3D.fromPoints(Vector3D.of(2, 0, 1), Vector3D.of(1, 0, 1), TEST_PRECISION);
+
+ // act
+ Assert.assertTrue(plane.isParallel(parallelLine));
+ Assert.assertEquals(1.0, plane.offset(parallelLine), TEST_EPS);
+
+ Assert.assertFalse(plane.isParallel(nonParallelLine));
+ Assert.assertEquals(0.0, plane.offset(nonParallelLine), TEST_EPS);
+
+ Assert.assertTrue(plane.isParallel(containedLine));
+ Assert.assertEquals(0.0, plane.offset(containedLine), TEST_EPS);
+ }
+
+ @Test
+ public void testIsParallelAndOffset_plane() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
+ Plane parallelPlane = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
+ Plane parallelPlane2 = Plane.fromPointAndNormal(Vector3D.of(0, 0, 2), Vector3D.of(0, 0, 1), TEST_PRECISION);
+ Plane parallelPlane3 = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(0, 0, 1), TEST_PRECISION).reverse();
+ Plane nonParallelPlane = Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.of(1, 1.5, 1), Vector3D.of(0,1,1), TEST_PRECISION);
+ Plane reversedPlane = plane.reverse();
+
+ // act/assert
+ Assert.assertTrue(plane.isParallel(parallelPlane));
+ Assert.assertEquals(0.0, plane.offset(parallelPlane), TEST_EPS);
+
+ Assert.assertTrue(plane.isParallel(parallelPlane2));
+ Assert.assertEquals(1.0, plane.offset(parallelPlane2), TEST_EPS);
+
+ Assert.assertTrue(plane.isParallel(parallelPlane3));
+ Assert.assertEquals(-1.0, plane.offset(parallelPlane3), TEST_EPS);
+
+ Assert.assertFalse(plane.isParallel(nonParallelPlane));
+ Assert.assertEquals(0.0, plane.offset(nonParallelPlane), TEST_EPS);
+
+ Assert.assertTrue(plane.isParallel(reversedPlane));
+ Assert.assertEquals(0.0, plane.offset(nonParallelPlane), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_line() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ Line3D line = Line3D.fromPoints(Vector3D.of(1, 0, 1), Vector3D.of(2, 0, 2), TEST_PRECISION);
+
+ // act
+ Line3D projected = plane.project(line);
+
+ // assert
+ Line3D expectedProjection = Line3D.fromPoints(Vector3D.of(1, 0, 1),Vector3D.of(2, 0, 1), TEST_PRECISION);
+ Assert.assertEquals(expectedProjection, projected);
+
+ Assert.assertTrue(plane.contains(projected));
+
+ Assert.assertTrue(projected.contains(Vector3D.of(1, 0, 1)));
+ Assert.assertTrue(projected.contains(Vector3D.of(2, 0, 1)));
+ }
+
+ @Test
+ public void testOffset_point() {
+ // arrange
+ Vector3D p1 = Vector3D.of(1, 1, 1);
+ Plane plane = Plane.fromPointAndNormal(p1, Vector3D.of(0.2, 0, 0), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(-5.0, plane.offset(Vector3D.of(-4, 0, 0)), TEST_EPS);
+ Assert.assertEquals(+5.0, plane.offset(Vector3D.of(6, 10, -12)), TEST_EPS);
+ Assert.assertEquals(0.3,
+ plane.offset(Vector3D.linearCombination(1.0, p1, 0.3, plane.getNormal())),
+ TEST_EPS);
+ Assert.assertEquals(-0.3,
+ plane.offset(Vector3D.linearCombination(1.0, p1, -0.3, plane.getNormal())),
+ TEST_EPS);
+ }
+
+ @Test
+ public void testPointAt() {
+ // arrange
+ Vector3D pt = Vector3D.of(0, 0, 1);
+ Plane plane = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X, TEST_PRECISION);
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(pt, plane.pointAt(Vector2D.ZERO, 0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, plane.pointAt(Vector2D.ZERO, -1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1), plane.pointAt(Vector2D.ZERO, -2), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 2), plane.pointAt(Vector2D.ZERO, 1), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 2, 1), plane.pointAt(Vector2D.of(2, 1), 0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4, -3, 6), plane.pointAt(Vector2D.of(-3, -4), 5), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_rotationAroundPoint() {
+ // arrange
+ Vector3D pt = Vector3D.of(0, 0, 1);
+ Plane plane = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X, TEST_PRECISION);
+
+ AffineTransformMatrix3D mat = AffineTransformMatrix3D.createRotation(pt,
+ QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ Plane result = plane.transform(mat);
+
+ // assert
+ checkPlane(result, Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z);
+ }
+
+ @Test
+ public void testTransform_asymmetricScaling() {
+ // arrange
+ Vector3D pt = Vector3D.of(0, 1, 0);
+ Plane plane = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.MINUS_Z, Vector3D.of(-1, 1, 0), TEST_PRECISION);
+
+ AffineTransformMatrix3D mat = AffineTransformMatrix3D.createScale(2, 1, 1);
+
+ // act
+ Plane result = plane.transform(mat);
+
+ // assert
+ Vector3D expectedU = Vector3D.Unit.MINUS_Z;
+ Vector3D expectedV = Vector3D.Unit.of(-2, 1, 0);
+ Vector3D expectedNormal = Vector3D.Unit.of(1, 2, 0);
+
+ Vector3D transformedPt = mat.apply(plane.getOrigin());
+ Vector3D expectedOrigin = transformedPt.project(expectedNormal);
+
+ checkPlane(result, expectedOrigin, expectedU, expectedV);
+
+ Assert.assertTrue(result.contains(transformedPt));
+ Assert.assertFalse(plane.contains(transformedPt));
+ }
+
+ @Test
+ public void testTransform_negateOneComponent() {
+ // arrange
+ Vector3D pt = Vector3D.of(0, 0, 1);
+ Plane plane = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ Transform3D transform = FunctionTransform3D.from(v -> Vector3D.of(-v.getX(), v.getY(), v.getZ()));
+
+ // act
+ Plane result = plane.transform(transform);
+
+ // assert
+ checkPlane(result, Vector3D.of(0, 0, 1), Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Y);
+ }
+
+ @Test
+ public void testTransform_negateTwoComponents() {
+ // arrange
+ Vector3D pt = Vector3D.of(0, 0, 1);
+ Plane plane = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ Transform3D transform = FunctionTransform3D.from(v -> Vector3D.of(-v.getX(), -v.getY(), v.getZ()));
+
+ // act
+ Plane result = plane.transform(transform);
+
+ // assert
+ checkPlane(result, Vector3D.of(0, 0, 1), Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_Y);
+ }
+
+ @Test
+ public void testTransform_negateAllComponents() {
+ // arrange
+ Vector3D pt = Vector3D.of(0, 0, 1);
+ Plane plane = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ Transform3D transform = FunctionTransform3D.from(Vector3D::negate);
+
+ // act
+ Plane result = plane.transform(transform);
+
+ // assert
+ checkPlane(result, Vector3D.of(0, 0, -1), Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_Y);
+ }
+
+ @Test
+ public void testSubspaceTransform() {
+ // arrange
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // act/assert
+ checkSubspaceTransform(plane.subspaceTransform(AffineTransformMatrix3D.createScale(2, 3, 4)),
+ Vector3D.of(0, 0, 4), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y,
+ Vector3D.of(0, 0, 4), Vector3D.of(2, 0, 4), Vector3D.of(0, 3, 4));
+
+ checkSubspaceTransform(plane.subspaceTransform(AffineTransformMatrix3D.createTranslation(2, 3, 4)),
+ Vector3D.of(0, 0, 5), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y,
+ Vector3D.of(2, 3, 5), Vector3D.of(3, 3, 5), Vector3D.of(2, 4, 5));
+
+ checkSubspaceTransform(plane.subspaceTransform(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI)),
+ Vector3D.of(1, 0, 0), Vector3D.Unit.MINUS_Z, Vector3D.Unit.PLUS_Y,
+ Vector3D.of(1, 0, 0), Vector3D.of(1, 0, -1), Vector3D.of(1, 1, 0));
+ }
+
+ private void checkSubspaceTransform(SubspaceTransform st,
+ Vector3D origin, Vector3D u, Vector3D v,
+ Vector3D tOrigin, Vector3D tU, Vector3D tV) {
+
+ Plane plane = st.getPlane();
+ AffineTransformMatrix2D transform = st.getTransform();
+
+ checkPlane(plane, origin, u, v);
+
+ EuclideanTestUtils.assertCoordinatesEqual(tOrigin, plane.toSpace(transform.apply(Vector2D.ZERO)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(tU, plane.toSpace(transform.apply(Vector2D.Unit.PLUS_X)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(tV, plane.toSpace(transform.apply(Vector2D.Unit.PLUS_Y)), TEST_EPS);
+ }
+
+ @Test
+ public void testSubspaceTransform_transformsPointsCorrectly() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.of(1, 2, 3), Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ EuclideanTestUtils.permuteSkipZero(-2, 2, 0.5, (a, b, c) -> {
+ // create a somewhat complicate transform to try to hit all of the edge cases
+ AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(Vector3D.of(a, b, c))
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.of(b, c, a), Geometry.PI * c))
+ .scale(0.1, 4, 8);
+
+ // act
+ SubspaceTransform st = plane.subspaceTransform(transform);
+
+ // assert
+ EuclideanTestUtils.permute(-5, 5, 1, (x, y) -> {
+ Vector2D subPt = Vector2D.of(x, y);
+ Vector3D expected = transform.apply(plane.toSpace(subPt));
+ Vector3D actual = st.getPlane().toSpace(
+ st.getTransform().apply(subPt));
+
+ EuclideanTestUtils.assertCoordinatesEqual(expected, actual, TEST_EPS);
+ });
+ });
+ }
+
+ @Test
+ public void testRotate() {
+ // arrange
+ Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
+ Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
+ Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
+ Plane plane = Plane.fromPoints(p1, p2, p3, TEST_PRECISION);
Vector3D oldNormal = plane.getNormal();
+ // act/assert
plane = plane.rotate(p2, QuaternionRotation.fromAxisAngle(p2.subtract(p1), 1.7));
Assert.assertTrue(plane.contains(p1));
Assert.assertTrue(plane.contains(p2));
- Assert.assertTrue(! plane.contains(p3));
+ Assert.assertFalse(plane.contains(p3));
plane = plane.rotate(p2, QuaternionRotation.fromAxisAngle(oldNormal, 0.1));
- Assert.assertTrue(! plane.contains(p1));
+ Assert.assertFalse(plane.contains(p1));
Assert.assertTrue(plane.contains(p2));
- Assert.assertTrue(! plane.contains(p3));
+ Assert.assertFalse(plane.contains(p3));
plane = plane.rotate(p1, QuaternionRotation.fromAxisAngle(oldNormal, 0.1));
- Assert.assertTrue(! plane.contains(p1));
- Assert.assertTrue(! plane.contains(p2));
- Assert.assertTrue(! plane.contains(p3));
-
+ Assert.assertFalse(plane.contains(p1));
+ Assert.assertFalse(plane.contains(p2));
+ Assert.assertFalse(plane.contains(p3));
}
@Test
public void testTranslate() {
+ // arrange
Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
- Plane plane = Plane.fromPoints(p1, p2, p3, TEST_PRECISION);
+ Plane plane = Plane.fromPoints(p1, p2, p3, TEST_PRECISION);
+ // act/assert
plane = plane.translate(Vector3D.linearCombination(2.0, plane.getU(), -1.5, plane.getV()));
Assert.assertTrue(plane.contains(p1));
Assert.assertTrue(plane.contains(p2));
Assert.assertTrue(plane.contains(p3));
plane = plane.translate(Vector3D.linearCombination(-1.2, plane.getNormal()));
- Assert.assertTrue(! plane.contains(p1));
- Assert.assertTrue(! plane.contains(p2));
- Assert.assertTrue(! plane.contains(p3));
+ Assert.assertFalse(plane.contains(p1));
+ Assert.assertFalse(plane.contains(p2));
+ Assert.assertFalse(plane.contains(p3));
plane = plane.translate(Vector3D.linearCombination(+1.2, plane.getNormal()));
Assert.assertTrue(plane.contains(p1));
Assert.assertTrue(plane.contains(p2));
Assert.assertTrue(plane.contains(p3));
-
}
@Test
- public void testIntersection() {
+ public void testIntersection_withLine() {
+ // arrange
Plane plane = Plane.fromPointAndNormal(Vector3D.of(1, 2, 3), Vector3D.of(-4, 1, -5), TEST_PRECISION);
- Line line = new Line(Vector3D.of(0.2, -3.5, 0.7), Vector3D.of(1.2, -2.5, -0.3), TEST_PRECISION);
+ Line3D line = Line3D.fromPoints(Vector3D.of(0.2, -3.5, 0.7), Vector3D.of(1.2, -2.5, -0.3), TEST_PRECISION);
+
+ // act
Vector3D point = plane.intersection(line);
+
+ // assert
Assert.assertTrue(plane.contains(point));
Assert.assertTrue(line.contains(point));
- Assert.assertNull(plane.intersection(new Line(Vector3D.of(10, 10, 10),
+ Assert.assertNull(plane.intersection(Line3D.fromPoints(Vector3D.of(10, 10, 10),
Vector3D.of(10, 10, 10).add(plane.getNormal().orthogonal()),
TEST_PRECISION)));
}
@Test
- public void testIntersection2() {
- Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
- Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
- Plane planeA = Plane.fromPoints(p1, p2, Vector3D.of(-2.0, 4.3, 0.7), TEST_PRECISION);
- Plane planeB = Plane.fromPoints(p1, Vector3D.of(11.4, -3.8, 5.1), p2, TEST_PRECISION);
- Line line = planeA.intersection(planeB);
+ public void testIntersection_withLine_noIntersection() {
+ // arrange
+ Vector3D pt = Vector3D.of(1, 2, 3);
+ Vector3D normal = Vector3D.of(-4, 1, -5);
+
+ Plane plane = Plane.fromPointAndNormal(pt, normal, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertNull(plane.intersection(Line3D.fromPoints(pt, pt.add(plane.getU()), TEST_PRECISION)));
+
+ Vector3D offsetPt = pt.add(plane.getNormal());
+ Assert.assertNull(plane.intersection(Line3D.fromPoints(offsetPt, offsetPt.add(plane.getV()), TEST_PRECISION)));
+ }
+
+ @Test
+ public void testIntersection_withPlane() {
+ // arrange
+ Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
+ Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
+ Plane planeA = Plane.fromPoints(p1, p2, Vector3D.of(-2.0, 4.3, 0.7), TEST_PRECISION);
+ Plane planeB = Plane.fromPoints(p1, Vector3D.of(11.4, -3.8, 5.1), p2, TEST_PRECISION);
+
+ // act
+ Line3D line = planeA.intersection(planeB);
+
+ // assert
Assert.assertTrue(line.contains(p1));
Assert.assertTrue(line.contains(p2));
+ EuclideanTestUtils.assertCoordinatesEqual(planeA.getNormal().cross(planeB.getNormal()).normalize(),
+ line.getDirection(), TEST_EPS);
+
Assert.assertNull(planeA.intersection(planeA));
}
@Test
- public void testIntersection3() {
- Vector3D reference = Vector3D.of(1.2, 3.4, -5.8);
- Plane p1 = Plane.fromPointAndNormal(reference, Vector3D.of(1, 3, 3), TEST_PRECISION);
- Plane p2 = Plane.fromPointAndNormal(reference, Vector3D.of(-2, 4, 0), TEST_PRECISION);
- Plane p3 = Plane.fromPointAndNormal(reference, Vector3D.of(7, 0, -4), TEST_PRECISION);
- Vector3D plane = Plane.intersection(p1, p2, p3);
- Assert.assertEquals(reference.getX(), plane.getX(), TEST_EPS);
- Assert.assertEquals(reference.getY(), plane.getY(), TEST_EPS);
- Assert.assertEquals(reference.getZ(), plane.getZ(), TEST_EPS);
+ public void testIntersection_withPlane_noIntersection() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertNull(plane.intersection(plane));
+ Assert.assertNull(plane.intersection(plane.reverse()));
+
+ Assert.assertNull(plane.intersection(Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION)));
+ Assert.assertNull(plane.intersection(Plane.fromPointAndNormal(Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z, TEST_PRECISION)));
}
@Test
- public void testSimilar() {
- Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
- Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
- Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
- Plane planeA = Plane.fromPoints(p1, p2, p3, TEST_PRECISION);
- Plane planeB = Plane.fromPoints(p1, Vector3D.of(11.4, -3.8, 5.1), p2, TEST_PRECISION);
- Assert.assertTrue(! planeA.contains(planeB));
- Assert.assertTrue(planeA.contains(planeA));
- Assert.assertTrue(planeA.contains(Plane.fromPoints(p1, p3, p2, TEST_PRECISION)));
- Vector3D shift = Vector3D.linearCombination(0.3, planeA.getNormal());
- Assert.assertTrue(! planeA.contains(Plane.fromPoints(p1.add(shift),
- p3.add(shift),
- p2.add(shift),
- TEST_PRECISION)));
+ public void testIntersection_threePlanes() {
+ // arrange
+ Vector3D pt = Vector3D.of(1.2, 3.4, -5.8);
+ Plane a = Plane.fromPointAndNormal(pt, Vector3D.of(1, 3, 3), TEST_PRECISION);
+ Plane b = Plane.fromPointAndNormal(pt, Vector3D.of(-2, 4, 0), TEST_PRECISION);
+ Plane c = Plane.fromPointAndNormal(pt, Vector3D.of(7, 0, -4), TEST_PRECISION);
+
+ // act
+ Vector3D result = Plane.intersection(a, b, c);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(pt, result, TEST_EPS);
+ }
+
+ @Test
+ public void testIntersection_threePlanes_intersectInLine() {
+ // arrange
+ Plane a = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(1, 0, 0), TEST_PRECISION);
+ Plane b = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(1, 0.5, 0), TEST_PRECISION);
+ Plane c = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(1, 1, 0), TEST_PRECISION);
+
+ // act
+ Vector3D result = Plane.intersection(a, b, c);
+
+ // assert
+ Assert.assertNull(result);
+ }
+
+ @Test
+ public void testIntersection_threePlanes_twoParallel() {
+ // arrange
+ Plane a = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ Plane b = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+ Plane c = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ Vector3D result = Plane.intersection(a, b, c);
+
+ // assert
+ Assert.assertNull(result);
+ }
+
+ @Test
+ public void testIntersection_threePlanes_allParallel() {
+ // arrange
+ Plane a = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ Plane b = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ Plane c = Plane.fromPointAndNormal(Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ Vector3D result = Plane.intersection(a, b, c);
+
+ // assert
+ Assert.assertNull(result);
+ }
+
+ @Test
+ public void testIntersection_threePlanes_coincidentPlanes() {
+ // arrange
+ Plane a = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ Plane b = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ Plane c = b.reverse();
+
+ // act
+ Vector3D result = Plane.intersection(a, b, c);
+
+ // assert
+ Assert.assertNull(result);
+ }
+
+ @Test
+ public void testSpan() {
+ // arrange
+ Plane plane = Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ ConvexSubPlane sub = plane.span();
+
+ // assert
+ Assert.assertSame(plane, sub.getPlane());
+ Assert.assertTrue(sub.isFull());
+
+ Assert.assertTrue(sub.contains(Vector3D.ZERO));
+ Assert.assertTrue(sub.contains(Vector3D.of(1, 1, 0)));
+
+ Assert.assertFalse(sub.contains(Vector3D.of(0, 0, 1)));
+ }
+
+ @Test
+ public void testSimilarOrientation() {
+ // arrange
+ Plane plane = Plane.fromNormal(Vector3D.of(1, 0, 0), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(plane.similarOrientation(plane));
+ Assert.assertTrue(plane.similarOrientation(Plane.fromNormal(Vector3D.of(1, 1, 0), TEST_PRECISION)));
+ Assert.assertTrue(plane.similarOrientation(Plane.fromNormal(Vector3D.of(1, -1, 0), TEST_PRECISION)));
+
+ Assert.assertFalse(plane.similarOrientation(Plane.fromNormal(Vector3D.of(0, 1, 0), TEST_PRECISION)));
+ Assert.assertFalse(plane.similarOrientation(Plane.fromNormal(Vector3D.of(-1, 1, 0), TEST_PRECISION)));
+ Assert.assertFalse(plane.similarOrientation(Plane.fromNormal(Vector3D.of(-1, 1, 0), TEST_PRECISION)));
+ Assert.assertFalse(plane.similarOrientation(Plane.fromNormal(Vector3D.of(0, -1, 0), TEST_PRECISION)));
+ }
+
+ @Test
+ public void testEq() {
+ // arrange
+ double eps = 1e-3;
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
+
+ Vector3D pt = Vector3D.of(1, 2, 3);
+ Vector3D u = Vector3D.Unit.PLUS_X;
+ Vector3D v = Vector3D.Unit.PLUS_Y;
+
+ Vector3D ptPrime = Vector3D.of(1.0001, 2.0001, 3.0001);
+ Vector3D uPrime = Vector3D.Unit.of(1, 1e-4, 0);
+ Vector3D vPrime = Vector3D.Unit.of(0, 1, 1e-4);
+
+ Plane a = Plane.fromPointAndPlaneVectors(pt, u, v, precision);
+
+ Plane b = Plane.fromPointAndPlaneVectors(Vector3D.of(1, 2, 4), u, v, precision);
+ Plane c = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.MINUS_X, v, precision);
+ Plane d = Plane.fromPointAndPlaneVectors(pt, u, Vector3D.Unit.MINUS_Y, precision);
+ Plane e = Plane.fromPointAndPlaneVectors(pt, u, v, TEST_PRECISION);
+
+ Plane f = Plane.fromPointAndPlaneVectors(ptPrime, uPrime, vPrime, new EpsilonDoublePrecisionContext(eps));
+
+ // act/assert
+ Assert.assertTrue(a.eq(a));
+
+ Assert.assertFalse(a.eq(b));
+ Assert.assertFalse(a.eq(c));
+ Assert.assertFalse(a.eq(d));
+ Assert.assertFalse(a.eq(e));
+
+ Assert.assertTrue(a.eq(f));
+ Assert.assertTrue(f.eq(a));
+ }
+
+ @Test
+ public void testHashCode() {
+ // arrange
+ Vector3D pt = Vector3D.of(1, 2, 3);
+ Vector3D u = Vector3D.Unit.PLUS_X;
+ Vector3D v = Vector3D.Unit.PLUS_Y;
+
+ Plane a = Plane.fromPointAndPlaneVectors(pt, u, v, TEST_PRECISION);
+ Plane b = Plane.fromPointAndPlaneVectors(Vector3D.of(1, 2, 4), u, v, TEST_PRECISION);
+ Plane c = Plane.fromPointAndPlaneVectors(pt, Vector3D.of(1, 1, 0), v, TEST_PRECISION);
+ Plane d = Plane.fromPointAndPlaneVectors(pt, u, Vector3D.Unit.MINUS_Y, TEST_PRECISION);
+ Plane e = Plane.fromPointAndPlaneVectors(pt, u, v, new EpsilonDoublePrecisionContext(1e-8));
+ Plane f = Plane.fromPointAndPlaneVectors(pt, u, v, TEST_PRECISION);
+
+ // act/assert
+ int hash = a.hashCode();
+
+ Assert.assertEquals(hash, a.hashCode());
+
+ Assert.assertNotEquals(hash, b.hashCode());
+ Assert.assertNotEquals(hash, c.hashCode());
+ Assert.assertNotEquals(hash, d.hashCode());
+ Assert.assertNotEquals(hash, e.hashCode());
+
+ Assert.assertEquals(hash, f.hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // arrange
+ Vector3D pt = Vector3D.of(1, 2, 3);
+ Vector3D u = Vector3D.Unit.PLUS_X;
+ Vector3D v = Vector3D.Unit.PLUS_Y;
+
+ Plane a = Plane.fromPointAndPlaneVectors(pt, u, v, TEST_PRECISION);
+ Plane b = Plane.fromPointAndPlaneVectors(Vector3D.of(1, 2, 4), u, v, TEST_PRECISION);
+ Plane c = Plane.fromPointAndPlaneVectors(pt, Vector3D.Unit.MINUS_X, v, TEST_PRECISION);
+ Plane d = Plane.fromPointAndPlaneVectors(pt, u, Vector3D.Unit.MINUS_Y, TEST_PRECISION);
+ Plane e = Plane.fromPointAndPlaneVectors(pt, u, v, new EpsilonDoublePrecisionContext(1e-8));
+ Plane f = Plane.fromPointAndPlaneVectors(pt, u, v, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(a.equals(a));
+
+ Assert.assertFalse(a.equals(null));
+ Assert.assertFalse(a.equals(new Object()));
+
+ Assert.assertFalse(a.equals(b));
+ Assert.assertFalse(a.equals(c));
+ Assert.assertFalse(a.equals(d));
+ Assert.assertFalse(a.equals(e));
+
+ Assert.assertTrue(a.equals(f));
+ Assert.assertTrue(f.equals(a));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // act
+ String str = plane.toString();
+
+ // assert
+ Assert.assertTrue(str.startsWith("Plane["));
+ Assert.assertTrue(str.matches(".*origin= \\(0(\\.0)?, 0(\\.0)?\\, 0(\\.0)?\\).*"));
+ Assert.assertTrue(str.matches(".*u= \\(1(\\.0)?, 0(\\.0)?\\, 0(\\.0)?\\).*"));
+ Assert.assertTrue(str.matches(".*v= \\(0(\\.0)?, 1(\\.0)?\\, 0(\\.0)?\\).*"));
+ Assert.assertTrue(str.matches(".*w= \\(0(\\.0)?, 0(\\.0)?\\, 1(\\.0)?\\).*"));
}
private static void checkPlane(Plane plane, Vector3D origin, Vector3D u, Vector3D v) {
@@ -576,7 +1109,6 @@
EuclideanTestUtils.assertCoordinatesEqual(origin, plane.getNormal().multiply(-offset), TEST_EPS);
}
-
private static <T> List<T> rotate(List<T> list, int shift) {
int size = list.size();
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSetTest.java
deleted file mode 100644
index 228f3fc..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSetTest.java
+++ /dev/null
@@ -1,1655 +0,0 @@
-/*
- * 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.threed;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
-import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
-import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
-import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
-import org.apache.commons.geometry.euclidean.twod.SubLine;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
-import org.apache.commons.rng.UniformRandomProvider;
-import org.apache.commons.rng.simple.RandomSource;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class PolyhedronsSetTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testWholeSpace() {
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(polySet.getSize());
- Assert.assertEquals(0.0, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.NaN, polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertTrue(polySet.isFull());
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
- Vector3D.of(-100, -100, -100),
- Vector3D.of(0, 0, 0),
- Vector3D.of(100, 100, 100),
- Vector3D.of(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
- }
-
- @Test
- public void testEmptyRegion() {
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(new BSPTree<Vector3D>(Boolean.FALSE), TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- Assert.assertEquals(0.0, polySet.getSize(), TEST_EPS);
- Assert.assertEquals(0.0, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.NaN, polySet.getBarycenter(), TEST_EPS);
- Assert.assertTrue(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
- Vector3D.of(-100, -100, -100),
- Vector3D.of(0, 0, 0),
- Vector3D.of(100, 100, 100),
- Vector3D.of(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
- }
-
- @Test
- public void testHalfSpace() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = new ArrayList<>();
- boundaries.add(new SubPlane(Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, TEST_PRECISION),
- new PolygonsSet(TEST_PRECISION)));
-
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(polySet.getSize());
- EuclideanTestUtils.assertPositiveInfinity(polySet.getBoundarySize());
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.NaN, polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
- Vector3D.of(-100, -100, -100));
- checkPoints(Region.Location.BOUNDARY, polySet, Vector3D.of(0, 0, 0));
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(100, 100, 100),
- Vector3D.of(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
- }
-
- @Test
- public void testInvertedRegion() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = createBoxBoundaries(Vector3D.ZERO, 1.0, TEST_EPS);
- PolyhedronsSet box = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- // act
- PolyhedronsSet polySet = (PolyhedronsSet) new RegionFactory<Vector3D>().getComplement(box);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(polySet.getSize());
- Assert.assertEquals(6, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.NaN, polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
- Vector3D.of(-100, -100, -100),
- Vector3D.of(100, 100, 100),
- Vector3D.of(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(0, 0, 0));
- }
-
- @Test
- public void testCreateFromBoundaries_noBoundaries_treeRepresentsWholeSpace() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = new ArrayList<>();
-
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(polySet.getSize());
- Assert.assertEquals(0.0, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.NaN, polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertTrue(polySet.isFull());
- }
-
- @Test
- public void testCreateFromBoundaries_unitBox() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = createBoxBoundaries(Vector3D.ZERO, 1.0, TEST_EPS);
-
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- Assert.assertEquals(1.0, polySet.getSize(), TEST_EPS);
- Assert.assertEquals(6.0, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(-1, 0, 0),
- Vector3D.of(1, 0, 0),
- Vector3D.of(0, -1, 0),
- Vector3D.of(0, 1, 0),
- Vector3D.of(0, 0, -1),
- Vector3D.of(0, 0, 1),
-
- Vector3D.of(1, 1, 1),
- Vector3D.of(1, 1, -1),
- Vector3D.of(1, -1, 1),
- Vector3D.of(1, -1, -1),
- Vector3D.of(-1, 1, 1),
- Vector3D.of(-1, 1, -1),
- Vector3D.of(-1, -1, 1),
- Vector3D.of(-1, -1, -1));
-
- checkPoints(Region.Location.BOUNDARY, polySet,
- Vector3D.of(0.5, 0, 0),
- Vector3D.of(-0.5, 0, 0),
- Vector3D.of(0, 0.5, 0),
- Vector3D.of(0, -0.5, 0),
- Vector3D.of(0, 0, 0.5),
- Vector3D.of(0, 0, -0.5),
-
- Vector3D.of(0.5, 0.5, 0.5),
- Vector3D.of(0.5, 0.5, -0.5),
- Vector3D.of(0.5, -0.5, 0.5),
- Vector3D.of(0.5, -0.5, -0.5),
- Vector3D.of(-0.5, 0.5, 0.5),
- Vector3D.of(-0.5, 0.5, -0.5),
- Vector3D.of(-0.5, -0.5, 0.5),
- Vector3D.of(-0.5, -0.5, -0.5));
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(0, 0, 0),
-
- Vector3D.of(0.4, 0.4, 0.4),
- Vector3D.of(0.4, 0.4, -0.4),
- Vector3D.of(0.4, -0.4, 0.4),
- Vector3D.of(0.4, -0.4, -0.4),
- Vector3D.of(-0.4, 0.4, 0.4),
- Vector3D.of(-0.4, 0.4, -0.4),
- Vector3D.of(-0.4, -0.4, 0.4),
- Vector3D.of(-0.4, -0.4, -0.4));
- }
-
- @Test
- public void testCreateFromBoundaries_twoBoxes_disjoint() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = new ArrayList<>();
- boundaries.addAll(createBoxBoundaries(Vector3D.ZERO, 1.0, TEST_EPS));
- boundaries.addAll(createBoxBoundaries(Vector3D.of(2, 0, 0), 1.0, TEST_EPS));
-
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- Assert.assertEquals(2.0, polySet.getSize(), TEST_EPS);
- Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 0), polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(-1, 0, 0),
- Vector3D.of(1, 0, 0),
- Vector3D.of(3, 0, 0));
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(0, 0, 0),
- Vector3D.of(2, 0, 0));
- }
-
- @Test
- public void testCreateFromBoundaries_twoBoxes_sharedSide() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = new ArrayList<>();
- boundaries.addAll(createBoxBoundaries(Vector3D.of(0, 0, 0), 1.0, TEST_EPS));
- boundaries.addAll(createBoxBoundaries(Vector3D.of(1, 0, 0), 1.0, TEST_EPS));
-
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- Assert.assertEquals(2.0, polySet.getSize(), TEST_EPS);
- Assert.assertEquals(10.0, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0, 0), polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(-1, 0, 0),
- Vector3D.of(2, 0, 0));
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(0, 0, 0),
- Vector3D.of(1, 0, 0));
- }
-
- @Test
- public void testCreateFromBoundaries_twoBoxes_separationLessThanTolerance() {
- // arrange
- double eps = 1e-6;
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
- List<SubHyperplane<Vector3D>> boundaries = new ArrayList<>();
- boundaries.addAll(createBoxBoundaries(Vector3D.of(0, 0, 0), 1.0, eps));
- boundaries.addAll(createBoxBoundaries(Vector3D.of(1 + 1e-7, 0, 0), 1.0, eps));
-
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, precision);
-
- // assert
- Assert.assertSame(precision, polySet.getPrecision());
- Assert.assertEquals(2.0, polySet.getSize(), eps);
- Assert.assertEquals(10.0, polySet.getBoundarySize(), eps);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5 + 5e-8, 0, 0), polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(-1, 0, 0),
- Vector3D.of(2, 0, 0));
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(0, 0, 0),
- Vector3D.of(1, 0, 0));
- }
-
- @Test
- public void testCreateFromBoundaries_twoBoxes_sharedEdge() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = new ArrayList<>();
- boundaries.addAll(createBoxBoundaries(Vector3D.of(0, 0, 0), 1.0, TEST_EPS));
- boundaries.addAll(createBoxBoundaries(Vector3D.of(1, 1, 0), 1.0, TEST_EPS));
-
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- Assert.assertEquals(2.0, polySet.getSize(), TEST_EPS);
- Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0), polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(-1, 0, 0),
- Vector3D.of(1, 0, 0),
- Vector3D.of(0, 1, 0),
- Vector3D.of(2, 1, 0));
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(0, 0, 0),
- Vector3D.of(1, 1, 0));
- }
-
- @Test
- public void testCreateFromBoundaries_twoBoxes_sharedPoint() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = new ArrayList<>();
- boundaries.addAll(createBoxBoundaries(Vector3D.of(0, 0, 0), 1.0, TEST_EPS));
- boundaries.addAll(createBoxBoundaries(Vector3D.of(1, 1, 1), 1.0, TEST_EPS));
-
- // act
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, polySet.getPrecision());
- Assert.assertEquals(2.0, polySet.getSize(), TEST_EPS);
- Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(-1, 0, 0),
- Vector3D.of(1, 0, 0),
- Vector3D.of(0, 1, 1),
- Vector3D.of(2, 1, 1));
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(0, 0, 0),
- Vector3D.of(1, 1, 1));
- }
-
- @Test
- public void testCreateBox() {
- // act
- PolyhedronsSet tree = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_PRECISION);
-
- // assert
- Assert.assertEquals(1.0, tree.getSize(), TEST_EPS);
- Assert.assertEquals(6.0, tree.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
-
- for (double x = -0.25; x < 1.25; x += 0.1) {
- boolean xOK = (x >= 0.0) && (x <= 1.0);
- for (double y = -0.25; y < 1.25; y += 0.1) {
- boolean yOK = (y >= 0.0) && (y <= 1.0);
- for (double z = -0.25; z < 1.25; z += 0.1) {
- boolean zOK = (z >= 0.0) && (z <= 1.0);
- Region.Location expected =
- (xOK && yOK && zOK) ? Region.Location.INSIDE : Region.Location.OUTSIDE;
- Assert.assertEquals(expected, tree.checkPoint(Vector3D.of(x, y, z)));
- }
- }
- }
- checkPoints(Region.Location.BOUNDARY, tree, new Vector3D[] {
- Vector3D.of(0.0, 0.5, 0.5),
- Vector3D.of(1.0, 0.5, 0.5),
- Vector3D.of(0.5, 0.0, 0.5),
- Vector3D.of(0.5, 1.0, 0.5),
- Vector3D.of(0.5, 0.5, 0.0),
- Vector3D.of(0.5, 0.5, 1.0)
- });
- checkPoints(Region.Location.OUTSIDE, tree, new Vector3D[] {
- Vector3D.of(0.0, 1.2, 1.2),
- Vector3D.of(1.0, 1.2, 1.2),
- Vector3D.of(1.2, 0.0, 1.2),
- Vector3D.of(1.2, 1.0, 1.2),
- Vector3D.of(1.2, 1.2, 0.0),
- Vector3D.of(1.2, 1.2, 1.0)
- });
- }
-
- @Test
- public void testInvertedBox() {
- // arrange
- PolyhedronsSet tree = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_PRECISION);
-
- // act
- tree = (PolyhedronsSet) new RegionFactory<Vector3D>().getComplement(tree);
-
- // assert
- EuclideanTestUtils.assertPositiveInfinity(tree.getSize());
- Assert.assertEquals(6.0, tree.getBoundarySize(), 1.0e-10);
-
- Vector3D barycenter = tree.getBarycenter();
- Assert.assertTrue(Double.isNaN(barycenter.getX()));
- Assert.assertTrue(Double.isNaN(barycenter.getY()));
- Assert.assertTrue(Double.isNaN(barycenter.getZ()));
-
- for (double x = -0.25; x < 1.25; x += 0.1) {
- boolean xOK = (x < 0.0) || (x > 1.0);
- for (double y = -0.25; y < 1.25; y += 0.1) {
- boolean yOK = (y < 0.0) || (y > 1.0);
- for (double z = -0.25; z < 1.25; z += 0.1) {
- boolean zOK = (z < 0.0) || (z > 1.0);
- Region.Location expected =
- (xOK || yOK || zOK) ? Region.Location.INSIDE : Region.Location.OUTSIDE;
- Assert.assertEquals(expected, tree.checkPoint(Vector3D.of(x, y, z)));
- }
- }
- }
- checkPoints(Region.Location.BOUNDARY, tree, new Vector3D[] {
- Vector3D.of(0.0, 0.5, 0.5),
- Vector3D.of(1.0, 0.5, 0.5),
- Vector3D.of(0.5, 0.0, 0.5),
- Vector3D.of(0.5, 1.0, 0.5),
- Vector3D.of(0.5, 0.5, 0.0),
- Vector3D.of(0.5, 0.5, 1.0)
- });
- checkPoints(Region.Location.INSIDE, tree, new Vector3D[] {
- Vector3D.of(0.0, 1.2, 1.2),
- Vector3D.of(1.0, 1.2, 1.2),
- Vector3D.of(1.2, 0.0, 1.2),
- Vector3D.of(1.2, 1.0, 1.2),
- Vector3D.of(1.2, 1.2, 0.0),
- Vector3D.of(1.2, 1.2, 1.0)
- });
- }
-
- @Test
- public void testTetrahedron() {
- // arrange
- Vector3D vertex1 = Vector3D.of(1, 2, 3);
- Vector3D vertex2 = Vector3D.of(2, 2, 4);
- Vector3D vertex3 = Vector3D.of(2, 3, 3);
- Vector3D vertex4 = Vector3D.of(1, 3, 4);
-
- // act
- PolyhedronsSet tree =
- (PolyhedronsSet) new RegionFactory<Vector3D>().buildConvex(
- Plane.fromPoints(vertex3, vertex2, vertex1, TEST_PRECISION),
- Plane.fromPoints(vertex2, vertex3, vertex4, TEST_PRECISION),
- Plane.fromPoints(vertex4, vertex3, vertex1, TEST_PRECISION),
- Plane.fromPoints(vertex1, vertex2, vertex4, TEST_PRECISION));
-
- // assert
- Assert.assertEquals(1.0 / 3.0, tree.getSize(), TEST_EPS);
- Assert.assertEquals(2.0 * Math.sqrt(3.0), tree.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2.5, 3.5), tree.getBarycenter(), TEST_EPS);
-
- double third = 1.0 / 3.0;
- checkPoints(Region.Location.BOUNDARY, tree, new Vector3D[] {
- vertex1, vertex2, vertex3, vertex4,
- Vector3D.linearCombination(third, vertex1, third, vertex2, third, vertex3),
- Vector3D.linearCombination(third, vertex2, third, vertex3, third, vertex4),
- Vector3D.linearCombination(third, vertex3, third, vertex4, third, vertex1),
- Vector3D.linearCombination(third, vertex4, third, vertex1, third, vertex2)
- });
- checkPoints(Region.Location.OUTSIDE, tree, new Vector3D[] {
- Vector3D.of(1, 2, 4),
- Vector3D.of(2, 2, 3),
- Vector3D.of(2, 3, 4),
- Vector3D.of(1, 3, 3)
- });
- }
-
- @Test
- public void testSphere() {
- // arrange
- // (use a high tolerance value here since the sphere is only an approximation)
- double approximationTolerance = 0.2;
- double radius = 1.0;
-
- // act
- PolyhedronsSet polySet = createSphere(Vector3D.of(1, 2, 3), radius, 8, 16);
-
- // assert
- Assert.assertEquals(sphereVolume(radius), polySet.getSize(), approximationTolerance);
- Assert.assertEquals(sphereSurface(radius), polySet.getBoundarySize(), approximationTolerance);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), polySet.getBarycenter(), TEST_EPS);
- Assert.assertFalse(polySet.isEmpty());
- Assert.assertFalse(polySet.isFull());
-
- checkPoints(Region.Location.OUTSIDE, polySet,
- Vector3D.of(-0.1, 2, 3),
- Vector3D.of(2.1, 2, 3),
- Vector3D.of(1, 0.9, 3),
- Vector3D.of(1, 3.1, 3),
- Vector3D.of(1, 2, 1.9),
- Vector3D.of(1, 2, 4.1),
- Vector3D.of(1.6, 2.6, 3.6));
-
- checkPoints(Region.Location.INSIDE, polySet,
- Vector3D.of(1, 2, 3),
- Vector3D.of(0.1, 2, 3),
- Vector3D.of(1.9, 2, 3),
- Vector3D.of(1, 2.1, 3),
- Vector3D.of(1, 2.9, 3),
- Vector3D.of(1, 2, 2.1),
- Vector3D.of(1, 2, 3.9),
- Vector3D.of(1.5, 2.5, 3.5));
- }
-
- @Test
- public void testIsometry() {
- // arrange
- Vector3D vertex1 = Vector3D.of(1.1, 2.2, 3.3);
- Vector3D vertex2 = Vector3D.of(2.0, 2.4, 4.2);
- Vector3D vertex3 = Vector3D.of(2.8, 3.3, 3.7);
- Vector3D vertex4 = Vector3D.of(1.0, 3.6, 4.5);
-
- // act
- PolyhedronsSet tree =
- (PolyhedronsSet) new RegionFactory<Vector3D>().buildConvex(
- Plane.fromPoints(vertex3, vertex2, vertex1, TEST_PRECISION),
- Plane.fromPoints(vertex2, vertex3, vertex4, TEST_PRECISION),
- Plane.fromPoints(vertex4, vertex3, vertex1, TEST_PRECISION),
- Plane.fromPoints(vertex1, vertex2, vertex4, TEST_PRECISION));
-
- // assert
- Vector3D barycenter = tree.getBarycenter();
- Vector3D s = Vector3D.of(10.2, 4.3, -6.7);
- Vector3D c = Vector3D.of(-0.2, 2.1, -3.2);
- QuaternionRotation r = QuaternionRotation.fromAxisAngle(Vector3D.of(6.2, -4.4, 2.1), 0.12);
-
- tree = tree.rotate(c, r).translate(s);
-
- Vector3D newB =
- Vector3D.linearCombination(1.0, s,
- 1.0, c,
- 1.0, r.apply(barycenter.subtract(c)));
- Assert.assertEquals(0.0,
- newB.subtract(tree.getBarycenter()).norm(),
- TEST_EPS);
-
- final Vector3D[] expectedV = new Vector3D[] {
- Vector3D.linearCombination(1.0, s,
- 1.0, c,
- 1.0, r.apply(vertex1.subtract(c))),
- Vector3D.linearCombination(1.0, s,
- 1.0, c,
- 1.0, r.apply(vertex2.subtract(c))),
- Vector3D.linearCombination(1.0, s,
- 1.0, c,
- 1.0, r.apply(vertex3.subtract(c))),
- Vector3D.linearCombination(1.0, s,
- 1.0, c,
- 1.0, r.apply(vertex4.subtract(c)))
- };
- tree.getTree(true).visit(new BSPTreeVisitor<Vector3D>() {
-
- @Override
- public Order visitOrder(BSPTree<Vector3D> node) {
- return Order.MINUS_SUB_PLUS;
- }
-
- @Override
- public void visitInternalNode(BSPTree<Vector3D> node) {
- @SuppressWarnings("unchecked")
- BoundaryAttribute<Vector3D> attribute =
- (BoundaryAttribute<Vector3D>) node.getAttribute();
- if (attribute.getPlusOutside() != null) {
- checkFacet((SubPlane) attribute.getPlusOutside());
- }
- if (attribute.getPlusInside() != null) {
- checkFacet((SubPlane) attribute.getPlusInside());
- }
- }
-
- @Override
- public void visitLeafNode(BSPTree<Vector3D> node) {
- }
-
- private void checkFacet(SubPlane facet) {
- Plane plane = (Plane) facet.getHyperplane();
- Vector2D[][] vertices =
- ((PolygonsSet) facet.getRemainingRegion()).getVertices();
- Assert.assertEquals(1, vertices.length);
- for (int i = 0; i < vertices[0].length; ++i) {
- Vector3D v = plane.toSpace(vertices[0][i]);
- double d = Double.POSITIVE_INFINITY;
- for (int k = 0; k < expectedV.length; ++k) {
- d = Math.min(d, v.subtract(expectedV[k]).norm());
- }
- Assert.assertEquals(0, d, TEST_EPS);
- }
- }
-
- });
-
- }
-
- @Test
- public void testBuildBox() {
- // arrange
- double x = 1.0;
- double y = 2.0;
- double z = 3.0;
- double w = 0.1;
- double l = 1.0;
-
- // act
- PolyhedronsSet tree =
- new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w, TEST_PRECISION);
-
- // assert
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(x, y, z), tree.getBarycenter(), TEST_EPS);
- Assert.assertEquals(8 * l * w * w, tree.getSize(), TEST_EPS);
- Assert.assertEquals(8 * w * (2 * l + w), tree.getBoundarySize(), TEST_EPS);
- }
-
- @Test
- public void testCross() {
- // arrange
- double x = 1.0;
- double y = 2.0;
- double z = 3.0;
- double w = 0.1;
- double l = 1.0;
- PolyhedronsSet xBeam =
- new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w, TEST_PRECISION);
- PolyhedronsSet yBeam =
- new PolyhedronsSet(x - w, x + w, y - l, y + l, z - w, z + w, TEST_PRECISION);
- PolyhedronsSet zBeam =
- new PolyhedronsSet(x - w, x + w, y - w, y + w, z - l, z + l, TEST_PRECISION);
- RegionFactory<Vector3D> factory = new RegionFactory<>();
-
- // act
- PolyhedronsSet tree = (PolyhedronsSet) factory.union(xBeam, factory.union(yBeam, zBeam));
-
- // assert
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(x, y, z), tree.getBarycenter(), TEST_EPS);
- Assert.assertEquals(8 * w * w * (3 * l - 2 * w), tree.getSize(), TEST_EPS);
- Assert.assertEquals(24 * w * (2 * l - w), tree.getBoundarySize(), TEST_EPS);
- }
-
- // Issue MATH-780
- // See https://issues.apache.org/jira/browse/MATH-780
- @Test
- public void testCreateFromBoundaries_handlesSmallBoundariesCreatedDuringConstruction() {
- // arrange
- float[] coords = {
- 1.000000f, -1.000000f, -1.000000f,
- 1.000000f, -1.000000f, 1.000000f,
- -1.000000f, -1.000000f, 1.000000f,
- -1.000000f, -1.000000f, -1.000000f,
- 1.000000f, 1.000000f, -1f,
- 0.999999f, 1.000000f, 1.000000f, // 1.000000f, 1.000000f, 1.000000f,
- -1.000000f, 1.000000f, 1.000000f,
- -1.000000f, 1.000000f, -1.000000f};
- int[] indices = {
- 0, 1, 2, 0, 2, 3,
- 4, 7, 6, 4, 6, 5,
- 0, 4, 5, 0, 5, 1,
- 1, 5, 6, 1, 6, 2,
- 2, 6, 7, 2, 7, 3,
- 4, 0, 3, 4, 3, 7};
- ArrayList<SubHyperplane<Vector3D>> subHyperplaneList = new ArrayList<>();
- for (int idx = 0; idx < indices.length; idx += 3) {
- int idxA = indices[idx] * 3;
- int idxB = indices[idx + 1] * 3;
- int idxC = indices[idx + 2] * 3;
- Vector3D v_1 = Vector3D.of(coords[idxA], coords[idxA + 1], coords[idxA + 2]);
- Vector3D v_2 = Vector3D.of(coords[idxB], coords[idxB + 1], coords[idxB + 2]);
- Vector3D v_3 = Vector3D.of(coords[idxC], coords[idxC + 1], coords[idxC + 2]);
- Vector3D[] vertices = {v_1, v_2, v_3};
- Plane polyPlane = Plane.fromPoints(v_1, v_2, v_3, TEST_PRECISION);
- ArrayList<SubHyperplane<Vector2D>> lines = new ArrayList<>();
-
- Vector2D[] projPts = new Vector2D[vertices.length];
- for (int ptIdx = 0; ptIdx < projPts.length; ptIdx++) {
- projPts[ptIdx] = polyPlane.toSubSpace(vertices[ptIdx]);
- }
-
- SubLine lineInPlane = null;
- for (int ptIdx = 0; ptIdx < projPts.length; ptIdx++) {
- lineInPlane = new SubLine(projPts[ptIdx], projPts[(ptIdx + 1) % projPts.length], TEST_PRECISION);
- lines.add(lineInPlane);
- }
- Region<Vector2D> polyRegion = new PolygonsSet(lines, TEST_PRECISION);
- SubPlane polygon = new SubPlane(polyPlane, polyRegion);
- subHyperplaneList.add(polygon);
- }
-
- // act
- PolyhedronsSet polyhedronsSet = new PolyhedronsSet(subHyperplaneList, TEST_PRECISION);
-
- // assert
- Assert.assertEquals(8.0, polyhedronsSet.getSize(), 3.0e-6);
- Assert.assertEquals(24.0, polyhedronsSet.getBoundarySize(), 5.0e-6);
- }
-
- @Test
- public void testTooThinBox() {
- // act
- PolyhedronsSet polyhedronsSet = new PolyhedronsSet(0.0, 0.0, 0.0, 1.0, 0.0, 1.0, TEST_PRECISION);
-
- // assert
- Assert.assertEquals(0.0, polyhedronsSet.getSize(), TEST_EPS);
- }
-
- @Test
- public void testWrongUsage() {
- // the following is a wrong usage of the constructor.
- // as explained in the javadoc, the failure is NOT detected at construction
- // time but occurs later on
- PolyhedronsSet ps = new PolyhedronsSet(new BSPTree<Vector3D>(), TEST_PRECISION);
- Assert.assertNotNull(ps);
- try {
- ps.checkPoint(Vector3D.ZERO);
- Assert.fail("an exception should have been thrown");
- } catch (NullPointerException npe) {
- // this is expected
- }
- }
-
- @Test
- public void testDumpParse() throws IOException, ParseException {
- // arrange
- double eps = 1e-8;
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
-
- Vector3D[] verts=new Vector3D[8];
- double xmin=-1,xmax=1;
- double ymin=-1,ymax=1;
- double zmin=-1,zmax=1;
- verts[0]=Vector3D.of(xmin,ymin,zmin);
- verts[1]=Vector3D.of(xmax,ymin,zmin);
- verts[2]=Vector3D.of(xmax,ymax,zmin);
- verts[3]=Vector3D.of(xmin,ymax,zmin);
- verts[4]=Vector3D.of(xmin,ymin,zmax);
- verts[5]=Vector3D.of(xmax,ymin,zmax);
- verts[6]=Vector3D.of(xmax,ymax,zmax);
- verts[7]=Vector3D.of(xmin,ymax,zmax);
- //
- int[][] faces=new int[12][];
- faces[0]=new int[]{3,1,0}; // bottom (-z)
- faces[1]=new int[]{1,3,2}; // bottom (-z)
- faces[2]=new int[]{5,7,4}; // top (+z)
- faces[3]=new int[]{7,5,6}; // top (+z)
- faces[4]=new int[]{2,5,1}; // right (+x)
- faces[5]=new int[]{5,2,6}; // right (+x)
- faces[6]=new int[]{4,3,0}; // left (-x)
- faces[7]=new int[]{3,4,7}; // left (-x)
- faces[8]=new int[]{4,1,5}; // front (-y)
- faces[9]=new int[]{1,4,0}; // front (-y)
- faces[10]=new int[]{3,6,2}; // back (+y)
- faces[11]=new int[]{6,3,7}; // back (+y)
-
- PolyhedronsSet polyset = new PolyhedronsSet(Arrays.asList(verts), Arrays.asList(faces), precision);
-
- // act
- String dump = EuclideanTestUtils.dump(polyset);
- PolyhedronsSet parsed = EuclideanTestUtils.parsePolyhedronsSet(dump, precision);
-
- // assert
- Assert.assertEquals(8.0, polyset.getSize(), TEST_EPS);
- Assert.assertEquals(24.0, polyset.getBoundarySize(), TEST_EPS);
-
- Assert.assertEquals(8.0, parsed.getSize(), TEST_EPS);
- Assert.assertEquals(24.0, parsed.getBoundarySize(), TEST_EPS);
- Assert.assertTrue(new RegionFactory<Vector3D>().difference(polyset, parsed).isEmpty());
- }
-
- @Test
- public void testCreateFromBRep_connectedFacets() throws IOException, ParseException {
- InputStream stream = getClass().getResourceAsStream("pentomino-N.ply");
- PLYParser parser = new PLYParser(stream);
- stream.close();
- PolyhedronsSet polyhedron = new PolyhedronsSet(parser.getVertices(), parser.getFaces(), TEST_PRECISION);
- Assert.assertEquals( 5.0, polyhedron.getSize(), TEST_EPS);
- Assert.assertEquals(22.0, polyhedron.getBoundarySize(), TEST_EPS);
- }
-
- // GEOMETRY-59
- @Test
- public void testCreateFromBRep_slightlyConcavePrism() {
- // arrange
- Vector3D vertices[] = {
- Vector3D.of( 0, 0, 0 ),
- Vector3D.of( 2, 1e-7, 0 ),
- Vector3D.of( 4, 0, 0 ),
- Vector3D.of( 2, 2, 0 ),
- Vector3D.of( 0, 0, 2 ),
- Vector3D.of( 2, 1e-7, 2 ),
- Vector3D.of( 4, 0, 2 ),
- Vector3D.of( 2, 2, 2 )
- };
-
- int facets[][] = {
- { 4, 5, 6, 7 },
- { 3, 2, 1, 0 },
- { 0, 1, 5, 4 },
- { 1, 2, 6, 5 },
- { 2, 3, 7, 6 },
- { 3, 0, 4, 7 }
- };
-
- // act
- PolyhedronsSet prism = new PolyhedronsSet(
- Arrays.asList(vertices),
- Arrays.asList(facets),
- TEST_PRECISION);
-
-
- // assert
- Assert.assertTrue(Double.isFinite(prism.getSize()));
-
- checkPoints(Region.Location.INSIDE, prism, Vector3D.of(2, 1, 1));
- checkPoints(Region.Location.OUTSIDE, prism,
- Vector3D.of(2, 1, 3), Vector3D.of(2, 1, -3),
- Vector3D.of(2, -1, 1), Vector3D.of(2, 3, 1),
- Vector3D.of(-1, 1, 1), Vector3D.of(4, 1, 1));
- }
-
- @Test
- public void testCreateFromBRep_verticesTooClose() throws IOException, ParseException {
- checkError("pentomino-N-too-close.ply", "Vertices are too close");
- }
-
- @Test
- public void testCreateFromBRep_hole() throws IOException, ParseException {
- checkError("pentomino-N-hole.ply", "connected to one facet only");
- }
-
- @Test
- public void testCreateFromBRep_nonPlanar() throws IOException, ParseException {
- checkError("pentomino-N-out-of-plane.ply", "do not define a plane");
- }
-
- @Test
- public void testCreateFromBRep_badOrientation() throws IOException, ParseException {
- checkError("pentomino-N-bad-orientation.ply", "Facet orientation mismatch");
- }
-
- @Test
- public void testCreateFromBRep_wrongNumberOfPoints() throws IOException, ParseException {
- checkError(Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), Vector3D.of(0, 0, 1)),
- Arrays.asList(new int[] { 0, 1, 2 }, new int[] {2, 3}),
- "");
- }
-
- private void checkError(final String resourceName, final String expected) {
- try (InputStream stream = getClass().getResourceAsStream(resourceName)) {
- PLYParser parser = new PLYParser(stream);
- checkError(parser.getVertices(), parser.getFaces(), expected);
- } catch (IOException ioe) {
- Assert.fail(ioe.getLocalizedMessage());
- } catch (ParseException pe) {
- Assert.fail(pe.getLocalizedMessage());
- }
- }
-
- private void checkError(final List<Vector3D> vertices, final List<int[]> facets,
- final String expected) {
- try {
- new PolyhedronsSet(vertices, facets, TEST_PRECISION);
- Assert.fail("an exception should have been thrown");
- } catch (RuntimeException e) {
- String actual = e.getMessage();
- Assert.assertTrue("Expected string to contain \"" + expected + "\" but was \"" + actual + "\"",
- actual.contains(expected));
- }
- }
-
- @Test
- public void testFirstIntersection() {
- // arrange
- List<SubHyperplane<Vector3D>> boundaries = createBoxBoundaries(Vector3D.ZERO, 2.0, TEST_EPS);
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- Line xPlus = new Line(Vector3D.ZERO, Vector3D.of(1, 0, 0), TEST_PRECISION);
- Line xMinus = new Line(Vector3D.ZERO, Vector3D.of(-1, 0, 0), TEST_PRECISION);
-
- Line yPlus = new Line(Vector3D.ZERO, Vector3D.of(0, 1, 0), TEST_PRECISION);
- Line yMinus = new Line(Vector3D.ZERO, Vector3D.of(0, -1, 0), TEST_PRECISION);
-
- Line zPlus = new Line(Vector3D.ZERO, Vector3D.of(0, 0, 1), TEST_PRECISION);
- Line zMinus = new Line(Vector3D.ZERO, Vector3D.of(0, 0, -1), TEST_PRECISION);
-
- // act/assert
- assertSubPlaneNormal(Vector3D.of(-1, 0, 0), polySet.firstIntersection(Vector3D.of(-1.1, 0, 0), xPlus));
- assertSubPlaneNormal(Vector3D.of(-1, 0, 0), polySet.firstIntersection(Vector3D.of(-1, 0, 0), xPlus));
- assertSubPlaneNormal(Vector3D.of(1, 0, 0), polySet.firstIntersection(Vector3D.of(-0.9, 0, 0), xPlus));
- Assert.assertEquals(null, polySet.firstIntersection(Vector3D.of(1.1, 0, 0), xPlus));
-
- assertSubPlaneNormal(Vector3D.of(1, 0, 0), polySet.firstIntersection(Vector3D.of(1.1, 0, 0), xMinus));
- assertSubPlaneNormal(Vector3D.of(1, 0, 0), polySet.firstIntersection(Vector3D.of(1, 0, 0), xMinus));
- assertSubPlaneNormal(Vector3D.of(-1, 0, 0), polySet.firstIntersection(Vector3D.of(0.9, 0, 0), xMinus));
- Assert.assertEquals(null, polySet.firstIntersection(Vector3D.of(-1.1, 0, 0), xMinus));
-
- assertSubPlaneNormal(Vector3D.of(0, -1, 0), polySet.firstIntersection(Vector3D.of(0, -1.1, 0), yPlus));
- assertSubPlaneNormal(Vector3D.of(0, -1, 0), polySet.firstIntersection(Vector3D.of(0, -1, 0), yPlus));
- assertSubPlaneNormal(Vector3D.of(0, 1, 0), polySet.firstIntersection(Vector3D.of(0, -0.9, 0), yPlus));
- Assert.assertEquals(null, polySet.firstIntersection(Vector3D.of(0, 1.1, 0), yPlus));
-
- assertSubPlaneNormal(Vector3D.of(0, 1, 0), polySet.firstIntersection(Vector3D.of(0, 1.1, 0), yMinus));
- assertSubPlaneNormal(Vector3D.of(0, 1, 0), polySet.firstIntersection(Vector3D.of(0, 1, 0), yMinus));
- assertSubPlaneNormal(Vector3D.of(0, -1, 0), polySet.firstIntersection(Vector3D.of(0, 0.9, 0), yMinus));
- Assert.assertEquals(null, polySet.firstIntersection(Vector3D.of(0, -1.1, 0), yMinus));
-
- assertSubPlaneNormal(Vector3D.of(0, 0, -1), polySet.firstIntersection(Vector3D.of(0, 0, -1.1), zPlus));
- assertSubPlaneNormal(Vector3D.of(0, 0, -1), polySet.firstIntersection(Vector3D.of(0, 0, -1), zPlus));
- assertSubPlaneNormal(Vector3D.of(0, 0, 1), polySet.firstIntersection(Vector3D.of(0, 0, -0.9), zPlus));
- Assert.assertEquals(null, polySet.firstIntersection(Vector3D.of(0, 0, 1.1), zPlus));
-
- assertSubPlaneNormal(Vector3D.of(0, 0, 1), polySet.firstIntersection(Vector3D.of(0, 0, 1.1), zMinus));
- assertSubPlaneNormal(Vector3D.of(0, 0, 1), polySet.firstIntersection(Vector3D.of(0, 0, 1), zMinus));
- assertSubPlaneNormal(Vector3D.of(0, 0, -1), polySet.firstIntersection(Vector3D.of(0, 0, 0.9), zMinus));
- Assert.assertEquals(null, polySet.firstIntersection(Vector3D.of(0, 0, -1.1), zMinus));
- }
-
- // issue GEOMETRY-38
- @Test
- public void testFirstIntersection_linePassesThroughVertex() {
- // arrange
- Vector3D lowerCorner = Vector3D.ZERO;
- Vector3D upperCorner = Vector3D.of(1, 1, 1);
- Vector3D center = lowerCorner.lerp(upperCorner, 0.5);
-
- List<SubHyperplane<Vector3D>> boundaries = createBoxBoundaries(center, 1.0, TEST_EPS);
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- Line upDiagonal = new Line(lowerCorner, upperCorner, TEST_PRECISION);
- Line downDiagonal = upDiagonal.revert();
-
- // act/assert
- SubPlane upFromOutsideResult = (SubPlane) polySet.firstIntersection(Vector3D.of(-1, -1, -1), upDiagonal);
- Assert.assertNotNull(upFromOutsideResult);
- EuclideanTestUtils.assertCoordinatesEqual(lowerCorner,
- ((Plane) upFromOutsideResult.getHyperplane()).intersection(upDiagonal), TEST_EPS);
-
- SubPlane upFromCenterResult = (SubPlane) polySet.firstIntersection(center, upDiagonal);
- Assert.assertNotNull(upFromCenterResult);
- EuclideanTestUtils.assertCoordinatesEqual(upperCorner,
- ((Plane) upFromCenterResult.getHyperplane()).intersection(upDiagonal), TEST_EPS);
-
- SubPlane downFromOutsideResult = (SubPlane) polySet.firstIntersection(Vector3D.of(2, 2, 2), downDiagonal);
- Assert.assertNotNull(downFromOutsideResult);
- EuclideanTestUtils.assertCoordinatesEqual(upperCorner,
- ((Plane) downFromOutsideResult.getHyperplane()).intersection(downDiagonal), TEST_EPS);
-
- SubPlane downFromCenterResult = (SubPlane) polySet.firstIntersection(center, downDiagonal);
- Assert.assertNotNull(downFromCenterResult);
- EuclideanTestUtils.assertCoordinatesEqual(lowerCorner,
- ((Plane) downFromCenterResult.getHyperplane()).intersection(downDiagonal), TEST_EPS);
- }
-
- // Issue GEOMETRY-43
- @Test
- public void testFirstIntersection_lineParallelToFace() {
- // arrange - setup box
- Vector3D lowerCorner = Vector3D.ZERO;
- Vector3D upperCorner = Vector3D.of(1, 1, 1);
- Vector3D center = lowerCorner.lerp(upperCorner, 0.5);
- List<SubHyperplane<Vector3D>> boundaries = createBoxBoundaries(center, 1.0, TEST_EPS);
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- Vector3D firstPointOnLine = Vector3D.of(0.5, -1.0, 0);
- Vector3D secondPointOnLine = Vector3D.of(0.5, 2.0, 0);
- Line bottomLine = new Line(firstPointOnLine, secondPointOnLine, TEST_PRECISION);
-
- Vector3D expectedIntersection1 = Vector3D.of(0.5, 0, 0.0);
- Vector3D expectedIntersection2 = Vector3D.of(0.5, 1.0, 0.0);
-
- // act/assert
- SubPlane bottom = (SubPlane) polySet.firstIntersection(firstPointOnLine, bottomLine);
- Assert.assertNotNull(bottom);
- EuclideanTestUtils.assertCoordinatesEqual(expectedIntersection1,
- ((Plane) bottom.getHyperplane()).intersection(bottomLine), TEST_EPS);
-
- bottom = (SubPlane) polySet.firstIntersection(Vector3D.of(0.5, 0.1, 0.0), bottomLine);
- Assert.assertNotNull(bottom);
- Vector3D intersection = ((Plane) bottom.getHyperplane()).intersection(bottomLine);
- Assert.assertNotNull(intersection);
- EuclideanTestUtils.assertCoordinatesEqual(expectedIntersection2, intersection, TEST_EPS);
- }
-
- @Test
- public void testFirstIntersection_rayPointOnFace() {
- // arrange
- Vector3D lowerCorner = Vector3D.ZERO;
- Vector3D upperCorner = Vector3D.of(1, 1, 1);
- Vector3D center = lowerCorner.lerp(upperCorner, 0.5);
- List<SubHyperplane<Vector3D>> boundaries = createBoxBoundaries(center, 1.0, TEST_EPS);
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- Vector3D pt = Vector3D.of(0.5, 0.5, 0);
- Line intoBoxLine = new Line(pt, pt.add(Vector3D.Unit.PLUS_Z), TEST_PRECISION);
- Line outOfBoxLine = new Line(pt, pt.add(Vector3D.Unit.MINUS_Z), TEST_PRECISION);
-
- // act/assert
- SubPlane intoBoxResult = (SubPlane) polySet.firstIntersection(pt, intoBoxLine);
- Vector3D intoBoxPt = ((Plane) intoBoxResult.getHyperplane()).intersection(intoBoxLine);
- EuclideanTestUtils.assertCoordinatesEqual(pt, intoBoxPt, TEST_EPS);
-
- SubPlane outOfBoxResult = (SubPlane) polySet.firstIntersection(pt, outOfBoxLine);
- Vector3D outOfBoxPt = ((Plane) outOfBoxResult.getHyperplane()).intersection(outOfBoxLine);
- EuclideanTestUtils.assertCoordinatesEqual(pt, outOfBoxPt, TEST_EPS);
- }
-
- @Test
- public void testFirstIntersection_rayPointOnVertex() {
- // arrange
- Vector3D lowerCorner = Vector3D.ZERO;
- Vector3D upperCorner = Vector3D.of(1, 1, 1);
- Vector3D center = lowerCorner.lerp(upperCorner, 0.5);
-
- List<SubHyperplane<Vector3D>> boundaries = createBoxBoundaries(center, 1.0, TEST_EPS);
- PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_PRECISION);
-
- Line intoBoxLine = new Line(lowerCorner, upperCorner, TEST_PRECISION);
- Line outOfBoxLine = intoBoxLine.revert();
-
- // act/assert
- SubPlane intoBoxResult = (SubPlane) polySet.firstIntersection(lowerCorner, intoBoxLine);
- Vector3D intoBoxPt = ((Plane) intoBoxResult.getHyperplane()).intersection(intoBoxLine);
- EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, intoBoxPt, TEST_EPS);
-
- SubPlane outOfBoxResult = (SubPlane) polySet.firstIntersection(lowerCorner, outOfBoxLine);
- Vector3D outOfBoxPt = ((Plane) outOfBoxResult.getHyperplane()).intersection(outOfBoxLine);
- EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, outOfBoxPt, TEST_EPS);
- }
-
- // Issue 1211
- // See https://issues.apache.org/jira/browse/MATH-1211
- @Test
- public void testFirstIntersection_onlyReturnsPointsInDirectionOfRay() throws IOException, ParseException {
- // arrange
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-8);
- PolyhedronsSet polyset = EuclideanTestUtils.parsePolyhedronsSet(loadTestData("issue-1211.bsp"), precision);
- UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A, 0xb97c9d1ade21e40al);
-
- // act/assert
- int nrays = 1000;
- for (int i = 0; i < nrays; i++) {
- Vector3D origin = Vector3D.ZERO;
- Vector3D direction = Vector3D.of(2 * random.nextDouble() - 1,
- 2 * random.nextDouble() - 1,
- 2 * random.nextDouble() - 1).normalize();
- Line line = new Line(origin, origin.add(direction), polyset.getPrecision());
- SubHyperplane<Vector3D> plane = polyset.firstIntersection(origin, line);
- if (plane != null) {
- Vector3D intersectionPoint = ((Plane)plane.getHyperplane()).intersection(line);
- double dotProduct = direction.dot(intersectionPoint.subtract(origin));
- Assert.assertTrue(dotProduct > 0);
- }
- }
- }
-
- @Test
- public void testBoolean_union() throws IOException {
- // arrange
- double tolerance = 0.05;
- double size = 1.0;
- double radius = size * 0.5;
- PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_PRECISION);
- PolyhedronsSet sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().union(box, sphere);
-
- // OBJWriter.write("union.obj", result);
-
- // assert
- Assert.assertEquals(cubeVolume(size) + (sphereVolume(radius) * 0.5),
- result.getSize(), tolerance);
- Assert.assertEquals(cubeSurface(size) - circleSurface(radius) + (0.5 * sphereSurface(radius)),
- result.getBoundarySize(), tolerance);
- Assert.assertFalse(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-0.1, 0.5, 0.5),
- Vector3D.of(1.1, 0.5, 0.5),
- Vector3D.of(0.5, -0.1, 0.5),
- Vector3D.of(0.5, 1.1, 0.5),
- Vector3D.of(0.5, 0.5, -0.1),
- Vector3D.of(0.5, 0.5, 1.6));
-
- checkPoints(Region.Location.INSIDE, result,
- Vector3D.of(0.1, 0.5, 0.5),
- Vector3D.of(0.9, 0.5, 0.5),
- Vector3D.of(0.5, 0.1, 0.5),
- Vector3D.of(0.5, 0.9, 0.5),
- Vector3D.of(0.5, 0.5, 0.1),
- Vector3D.of(0.5, 0.5, 1.4));
- }
-
- @Test
- public void testUnion_self() {
- // arrange
- double tolerance = 0.2;
- double radius = 1.0;
-
- PolyhedronsSet sphere = createSphere(Vector3D.ZERO, radius, 8, 16);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().union(sphere, sphere.copySelf());
-
- // assert
- Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance);
- Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getBarycenter(), TEST_EPS);
- Assert.assertFalse(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-1.1, 0, 0),
- Vector3D.of(1.1, 0, 0),
- Vector3D.of(0, -1.1, 0),
- Vector3D.of(0, 1.1, 0),
- Vector3D.of(0, 0, -1.1),
- Vector3D.of(0, 0, 1.1));
-
- checkPoints(Region.Location.INSIDE, result,
- Vector3D.of(-0.9, 0, 0),
- Vector3D.of(0.9, 0, 0),
- Vector3D.of(0, -0.9, 0),
- Vector3D.of(0, 0.9, 0),
- Vector3D.of(0, 0, -0.9),
- Vector3D.of(0, 0, 0.9),
- Vector3D.ZERO);
- }
-
- @Test
- public void testBoolean_intersection() throws IOException {
- // arrange
- double tolerance = 0.05;
- double size = 1.0;
- double radius = size * 0.5;
- PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_PRECISION);
- PolyhedronsSet sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().intersection(box, sphere);
-
- // OBJWriter.write("intersection.obj", result);
-
- // assert
- Assert.assertEquals((sphereVolume(radius) * 0.5), result.getSize(), tolerance);
- Assert.assertEquals(circleSurface(radius) + (0.5 * sphereSurface(radius)),
- result.getBoundarySize(), tolerance);
- Assert.assertFalse(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-0.1, 0.5, 1.0),
- Vector3D.of(1.1, 0.5, 1.0),
- Vector3D.of(0.5, -0.1, 1.0),
- Vector3D.of(0.5, 1.1, 1.0),
- Vector3D.of(0.5, 0.5, 0.4),
- Vector3D.of(0.5, 0.5, 1.1));
-
- checkPoints(Region.Location.INSIDE, result,
- Vector3D.of(0.1, 0.5, 0.9),
- Vector3D.of(0.9, 0.5, 0.9),
- Vector3D.of(0.5, 0.1, 0.9),
- Vector3D.of(0.5, 0.9, 0.9),
- Vector3D.of(0.5, 0.5, 0.6),
- Vector3D.of(0.5, 0.5, 0.9));
- }
-
- @Test
- public void testIntersection_self() {
- // arrange
- double tolerance = 0.2;
- double radius = 1.0;
-
- PolyhedronsSet sphere = createSphere(Vector3D.ZERO, radius, 8, 16);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().intersection(sphere, sphere.copySelf());
-
- // assert
- Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance);
- Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getBarycenter(), TEST_EPS);
- Assert.assertFalse(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-1.1, 0, 0),
- Vector3D.of(1.1, 0, 0),
- Vector3D.of(0, -1.1, 0),
- Vector3D.of(0, 1.1, 0),
- Vector3D.of(0, 0, -1.1),
- Vector3D.of(0, 0, 1.1));
-
- checkPoints(Region.Location.INSIDE, result,
- Vector3D.of(-0.9, 0, 0),
- Vector3D.of(0.9, 0, 0),
- Vector3D.of(0, -0.9, 0),
- Vector3D.of(0, 0.9, 0),
- Vector3D.of(0, 0, -0.9),
- Vector3D.of(0, 0, 0.9),
- Vector3D.ZERO);
- }
-
- @Test
- public void testBoolean_xor_twoCubes() throws IOException {
- // arrange
- double size = 1.0;
- PolyhedronsSet box1 = new PolyhedronsSet(
- 0, size,
- 0, size,
- 0, size, TEST_PRECISION);
- PolyhedronsSet box2 = new PolyhedronsSet(
- 0.5, size + 0.5,
- 0.5, size + 0.5,
- 0.5, size + 0.5, TEST_PRECISION);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().xor(box1, box2);
-
- // OBJWriter.write("xor_twoCubes.obj", result);
-
- Assert.assertEquals((2 * cubeVolume(size)) - (2 * cubeVolume(size * 0.5)), result.getSize(), TEST_EPS);
-
- // assert
- Assert.assertEquals(2 * cubeSurface(size), result.getBoundarySize(), TEST_EPS);
- Assert.assertFalse(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-0.1, -0.1, -0.1),
- Vector3D.of(0.75, 0.75, 0.75),
- Vector3D.of(1.6, 1.6, 1.6));
-
- checkPoints(Region.Location.BOUNDARY, result,
- Vector3D.of(0, 0, 0),
- Vector3D.of(0.5, 0.5, 0.5),
- Vector3D.of(1, 1, 1),
- Vector3D.of(1.5, 1.5, 1.5));
-
- checkPoints(Region.Location.INSIDE, result,
- Vector3D.of(0.1, 0.1, 0.1),
- Vector3D.of(0.4, 0.4, 0.4),
- Vector3D.of(1.1, 1.1, 1.1),
- Vector3D.of(1.4, 1.4, 1.4));
- }
-
- @Test
- public void testBoolean_xor_cubeAndSphere() throws IOException {
- // arrange
- double tolerance = 0.05;
- double size = 1.0;
- double radius = size * 0.5;
- PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_PRECISION);
- PolyhedronsSet sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().xor(box, sphere);
-
- // OBJWriter.write("xor_cubeAndSphere.obj", result);
-
- Assert.assertEquals(cubeVolume(size), result.getSize(), tolerance);
-
- // assert
- Assert.assertEquals(cubeSurface(size) + (sphereSurface(radius)),
- result.getBoundarySize(), tolerance);
- Assert.assertFalse(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-0.1, 0.5, 0.5),
- Vector3D.of(1.1, 0.5, 0.5),
- Vector3D.of(0.5, -0.1, 0.5),
- Vector3D.of(0.5, 1.1, 0.5),
- Vector3D.of(0.5, 0.5, -0.1),
- Vector3D.of(0.5, 0.5, 1.6),
- Vector3D.of(0.5, 0.5, 0.9));
-
- checkPoints(Region.Location.INSIDE, result,
- Vector3D.of(0.1, 0.5, 0.5),
- Vector3D.of(0.9, 0.5, 0.5),
- Vector3D.of(0.5, 0.1, 0.5),
- Vector3D.of(0.5, 0.9, 0.5),
- Vector3D.of(0.5, 0.5, 0.1),
- Vector3D.of(0.5, 0.5, 1.4));
- }
-
- @Test
- public void testXor_self() {
- // arrange
- double radius = 1.0;
-
- PolyhedronsSet sphere = createSphere(Vector3D.ZERO, radius, 8, 16);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().xor(sphere, sphere.copySelf());
-
- // assert
- Assert.assertEquals(0.0, result.getSize(), TEST_EPS);
- Assert.assertEquals(0.0, result.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.NaN, result.getBarycenter(), TEST_EPS);
- Assert.assertTrue(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-1.1, 0, 0),
- Vector3D.of(1.1, 0, 0),
- Vector3D.of(0, -1.1, 0),
- Vector3D.of(0, 1.1, 0),
- Vector3D.of(0, 0, -1.1),
- Vector3D.of(0, 0, 1.1),
- Vector3D.of(-0.9, 0, 0),
- Vector3D.of(0.9, 0, 0),
- Vector3D.of(0, -0.9, 0),
- Vector3D.of(0, 0.9, 0),
- Vector3D.of(0, 0, -0.9),
- Vector3D.of(0, 0, 0.9),
- Vector3D.ZERO);
- }
-
- @Test
- public void testBoolean_difference() throws IOException {
- // arrange
- double tolerance = 0.05;
- double size = 1.0;
- double radius = size * 0.5;
- PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_PRECISION);
- PolyhedronsSet sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().difference(box, sphere);
-
- // OBJWriter.write("difference.obj", result);
-
- // assert
- Assert.assertEquals(cubeVolume(size) - (sphereVolume(radius) * 0.5), result.getSize(), tolerance);
- Assert.assertEquals(cubeSurface(size) - circleSurface(radius) + (0.5 * sphereSurface(radius)),
- result.getBoundarySize(), tolerance);
- Assert.assertFalse(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-0.1, 0.5, 1.0),
- Vector3D.of(1.1, 0.5, 1.0),
- Vector3D.of(0.5, -0.1, 1.0),
- Vector3D.of(0.5, 1.1, 1.0),
- Vector3D.of(0.5, 0.5, -0.1),
- Vector3D.of(0.5, 0.5, 0.6));
-
- checkPoints(Region.Location.INSIDE, result,
- Vector3D.of(0.1, 0.5, 0.4),
- Vector3D.of(0.9, 0.5, 0.4),
- Vector3D.of(0.5, 0.1, 0.4),
- Vector3D.of(0.5, 0.9, 0.4),
- Vector3D.of(0.5, 0.5, 0.1),
- Vector3D.of(0.5, 0.5, 0.4));
- }
-
- @Test
- public void testDifference_self() {
- // arrange
- double radius = 1.0;
-
- PolyhedronsSet sphere = createSphere(Vector3D.ZERO, radius, 8, 16);
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Vector3D>().difference(sphere, sphere.copySelf());
-
- // assert
- Assert.assertEquals(0.0, result.getSize(), TEST_EPS);
- Assert.assertEquals(0.0, result.getBoundarySize(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.NaN, result.getBarycenter(), TEST_EPS);
- Assert.assertTrue(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-1.1, 0, 0),
- Vector3D.of(1.1, 0, 0),
- Vector3D.of(0, -1.1, 0),
- Vector3D.of(0, 1.1, 0),
- Vector3D.of(0, 0, -1.1),
- Vector3D.of(0, 0, 1.1),
- Vector3D.of(-0.9, 0, 0),
- Vector3D.of(0.9, 0, 0),
- Vector3D.of(0, -0.9, 0),
- Vector3D.of(0, 0.9, 0),
- Vector3D.of(0, 0, -0.9),
- Vector3D.of(0, 0, 0.9),
- Vector3D.ZERO);
- }
-
- @Test
- public void testBoolean_multiple() throws IOException {
- // arrange
- double tolerance = 0.05;
- double size = 1.0;
- double radius = size * 0.5;
- PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_PRECISION);
- PolyhedronsSet sphereToAdd = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
- PolyhedronsSet sphereToRemove1 = createSphere(Vector3D.of(size * 0.5, 0, size * 0.5), radius, 8, 16);
- PolyhedronsSet sphereToRemove2 = createSphere(Vector3D.of(size * 0.5, 1, size * 0.5), radius, 8, 16);
-
- RegionFactory<Vector3D> factory = new RegionFactory<Vector3D>();
-
- // act
- PolyhedronsSet result = (PolyhedronsSet) factory.union(box, sphereToAdd);
- result = (PolyhedronsSet) factory.difference(result, sphereToRemove1);
- result = (PolyhedronsSet) factory.difference(result, sphereToRemove2);
-
- // OBJWriter.write("multiple.obj", result);
-
- // assert
- Assert.assertEquals(cubeVolume(size) - (sphereVolume(radius) * 0.5),
- result.getSize(), tolerance);
- Assert.assertEquals(cubeSurface(size) - (3.0 * circleSurface(radius)) + (1.5 * sphereSurface(radius)),
- result.getBoundarySize(), tolerance);
- Assert.assertFalse(result.isEmpty());
- Assert.assertFalse(result.isFull());
-
- checkPoints(Region.Location.OUTSIDE, result,
- Vector3D.of(-0.1, 0.5, 0.5),
- Vector3D.of(1.1, 0.5, 0.5),
- Vector3D.of(0.5, 0.4, 0.5),
- Vector3D.of(0.5, 0.6, 0.5),
- Vector3D.of(0.5, 0.5, -0.1),
- Vector3D.of(0.5, 0.5, 1.6));
-
- checkPoints(Region.Location.INSIDE, result,
- Vector3D.of(0.1, 0.5, 0.1),
- Vector3D.of(0.9, 0.5, 0.1),
- Vector3D.of(0.5, 0.4, 0.1),
- Vector3D.of(0.5, 0.6, 0.1),
- Vector3D.of(0.5, 0.5, 0.1),
- Vector3D.of(0.5, 0.5, 1.4));
- }
-
- @Test
- public void testProjectToBoundary() {
- // arrange
- PolyhedronsSet polySet = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_PRECISION);
-
- // act/assert
- checkProjectToBoundary(polySet, Vector3D.of(0.4, 0.5, 0.5),
- Vector3D.of(0, 0.5, 0.5), -0.4);
- checkProjectToBoundary(polySet, Vector3D.of(1.5, 0.5, 0.5),
- Vector3D.of(1, 0.5, 0.5), 0.5);
- checkProjectToBoundary(polySet, Vector3D.of(2, 2, 2),
- Vector3D.of(1, 1, 1), Math.sqrt(3));
- }
-
- @Test
- public void testProjectToBoundary_invertedRegion() {
- // arrange
- PolyhedronsSet polySet = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_PRECISION);
- polySet = (PolyhedronsSet) new RegionFactory<Vector3D>().getComplement(polySet);
-
- // act/assert
- checkProjectToBoundary(polySet, Vector3D.of(0.4, 0.5, 0.5),
- Vector3D.of(0, 0.5, 0.5), 0.4);
- checkProjectToBoundary(polySet, Vector3D.of(1.5, 0.5, 0.5),
- Vector3D.of(1, 0.5, 0.5), -0.5);
- checkProjectToBoundary(polySet, Vector3D.of(2, 2, 2),
- Vector3D.of(1, 1, 1), -Math.sqrt(3));
- }
-
- private void checkProjectToBoundary(PolyhedronsSet poly, Vector3D toProject,
- Vector3D expectedPoint, double expectedOffset) {
- BoundaryProjection<Vector3D> proj = poly.projectToBoundary(toProject);
-
- EuclideanTestUtils.assertCoordinatesEqual(toProject, proj.getOriginal(), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(expectedPoint, proj.getProjected(), TEST_EPS);
- Assert.assertEquals(expectedOffset, proj.getOffset(), TEST_EPS);
- }
-
- private String loadTestData(final String resourceName)
- throws IOException {
- try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8")) {
- StringBuilder builder = new StringBuilder();
- for (int c = reader.read(); c >= 0; c = reader.read()) {
- builder.append((char) c);
- }
- return builder.toString();
- }
- }
-
- private void checkPoints(Region.Location expected, PolyhedronsSet poly, Vector3D ... points) {
- for (int i = 0; i < points.length; ++i) {
- Assert.assertEquals("Incorrect location for " + points[i], expected, poly.checkPoint(points[i]));
- }
- }
-
- private List<SubHyperplane<Vector3D>> createBoxBoundaries(Vector3D center, double size, double eps) {
- List<SubHyperplane<Vector3D>> boundaries = new ArrayList<>();
-
- double offset = size * 0.5;
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
-
- Plane xMinus = Plane.fromPointAndNormal(center.add(Vector3D.of(-offset, 0, 0)), Vector3D.Unit.MINUS_X, precision);
- Plane xPlus = Plane.fromPointAndNormal(center.add(Vector3D.of(offset, 0, 0)), Vector3D.Unit.PLUS_X, precision);
- Plane yPlus = Plane.fromPointAndNormal(center.add(Vector3D.of(0, offset, 0)), Vector3D.Unit.PLUS_Y, precision);
- Plane yMinus = Plane.fromPointAndNormal(center.add(Vector3D.of(0, -offset, 0)), Vector3D.Unit.MINUS_Y, precision);
- Plane zPlus = Plane.fromPointAndNormal(center.add(Vector3D.of(0, 0, offset)), Vector3D.Unit.PLUS_Z, precision);
- Plane zMinus = Plane.fromPointAndNormal(center.add(Vector3D.of(0, 0, -offset)), Vector3D.Unit.MINUS_Z, precision);
-
- // +x
- boundaries.add(createSubPlane(xPlus,
- center.add(Vector3D.of(offset, offset, offset)),
- center.add(Vector3D.of(offset, -offset, offset)),
- center.add(Vector3D.of(offset, -offset, -offset)),
- center.add(Vector3D.of(offset, offset, -offset))));
-
- // -x
- boundaries.add(createSubPlane(xMinus,
- center.add(Vector3D.of(-offset, -offset, offset)),
- center.add(Vector3D.of(-offset, offset, offset)),
- center.add(Vector3D.of(-offset, offset, -offset)),
- center.add(Vector3D.of(-offset, -offset, -offset))));
-
- // +y
- boundaries.add(createSubPlane(yPlus,
- center.add(Vector3D.of(-offset, offset, offset)),
- center.add(Vector3D.of(offset, offset, offset)),
- center.add(Vector3D.of(offset, offset, -offset)),
- center.add(Vector3D.of(-offset, offset, -offset))));
-
- // -y
- boundaries.add(createSubPlane(yMinus,
- center.add(Vector3D.of(-offset, -offset, offset)),
- center.add(Vector3D.of(-offset, -offset, -offset)),
- center.add(Vector3D.of(offset, -offset, -offset)),
- center.add(Vector3D.of(offset, -offset, offset))));
-
- // +z
- boundaries.add(createSubPlane(zPlus,
- center.add(Vector3D.of(-offset, -offset, offset)),
- center.add(Vector3D.of(offset, -offset, offset)),
- center.add(Vector3D.of(offset, offset, offset)),
- center.add(Vector3D.of(-offset, offset, offset))));
-
- // -z
- boundaries.add(createSubPlane(zMinus,
- center.add(Vector3D.of(-offset, -offset, -offset)),
- center.add(Vector3D.of(-offset, offset, -offset)),
- center.add(Vector3D.of(offset, offset, -offset)),
- center.add(Vector3D.of(offset, -offset, -offset))));
-
- return boundaries;
- }
-
- private SubPlane createSubPlane(Plane plane, Vector3D...points) {
- Vector2D[] points2d = new Vector2D[points.length];
- for (int i=0; i<points.length; ++i) {
- points2d[i] = plane.toSubSpace(points[i]);
- }
-
- PolygonsSet polygon = new PolygonsSet(plane.getPrecision(), points2d);
-
- return new SubPlane(plane, polygon);
- }
-
- private PolyhedronsSet createSphere(Vector3D center, double radius, int stacks, int slices) {
- List<Plane> planes = new ArrayList<>();
-
- // add top and bottom planes (+/- z)
- Vector3D topZ = Vector3D.of(center.getX(), center.getY(), center.getZ() + radius);
- Vector3D bottomZ = Vector3D.of(center.getX(), center.getY(), center.getZ() - radius);
-
- planes.add(Plane.fromPointAndNormal(topZ, Vector3D.Unit.PLUS_Z, TEST_PRECISION));
- planes.add(Plane.fromPointAndNormal(bottomZ, Vector3D.Unit.MINUS_Z, TEST_PRECISION));
-
- // add the side planes
- double vDelta = Math.PI / stacks;
- double hDelta = Math.PI * 2 / slices;
-
- double adjustedRadius = (radius + (radius * Math.cos(vDelta * 0.5))) / 2.0;
-
- double vAngle;
- double hAngle;
- double stackRadius;
- double stackHeight;
- double x, y;
- Vector3D pt;
- Vector3D norm;
-
- vAngle = -0.5 * vDelta;
- for (int v=0; v<stacks; ++v) {
- vAngle += vDelta;
-
- stackRadius = Math.sin(vAngle) * adjustedRadius;
- stackHeight = Math.cos(vAngle) * adjustedRadius;
-
- hAngle = -0.5 * hDelta;
- for (int h=0; h<slices; ++h) {
- hAngle += hDelta;
-
- x = Math.cos(hAngle) * stackRadius;
- y = Math.sin(hAngle) * stackRadius;
-
- norm = Vector3D.of(x, y, stackHeight).normalize();
- pt = center.add(norm.multiply(adjustedRadius));
-
- planes.add(Plane.fromPointAndNormal(pt, norm, TEST_PRECISION));
- }
- }
-
- return (PolyhedronsSet) new RegionFactory<Vector3D>().buildConvex(planes.toArray(new Plane[0]));
- }
-
- private void assertSubPlaneNormal(Vector3D expectedNormal, SubHyperplane<Vector3D> sub) {
- Vector3D norm = ((Plane) sub.getHyperplane()).getNormal();
- EuclideanTestUtils.assertCoordinatesEqual(expectedNormal, norm, TEST_EPS);
- }
-
- private double cubeVolume(double size) {
- return size * size * size;
- }
-
- private double cubeSurface(double size) {
- return 6.0 * size * size;
- }
-
- private double sphereVolume(double radius) {
- return 4.0 * Math.PI * radius * radius * radius / 3.0;
- }
-
- private double sphereSurface(double radius) {
- return 4.0 * Math.PI * radius * radius;
- }
-
- private double circleSurface(double radius) {
- return Math.PI * radius * radius;
- }
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
new file mode 100644
index 0000000..17559ba
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
@@ -0,0 +1,1633 @@
+/*
+ * 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.threed;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D.RegionNode3D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RegionBSPTree3DTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testCtor_default() {
+ // act
+ RegionBSPTree3D tree = new RegionBSPTree3D();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ }
+
+ @Test
+ public void testCtor_boolean() {
+ // act
+ RegionBSPTree3D a = new RegionBSPTree3D(true);
+ RegionBSPTree3D b = new RegionBSPTree3D(false);
+
+ // assert
+ Assert.assertTrue(a.isFull());
+ Assert.assertFalse(a.isEmpty());
+
+ Assert.assertFalse(b.isFull());
+ Assert.assertTrue(b.isEmpty());
+ }
+
+ @Test
+ public void testEmpty() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.empty();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+
+ Assert.assertNull(tree.getBarycenter());
+ Assert.assertEquals(0.0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
+ Vector3D.of(-100, -100, -100),
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(100, 100, 100),
+ Vector3D.of(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
+ }
+
+ @Test
+ public void testFull() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.full();
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertNull(tree.getBarycenter());
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
+ Vector3D.of(-100, -100, -100),
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(100, 100, 100),
+ Vector3D.of(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
+ }
+
+ @Test
+ public void testCopy() {
+ // arrange
+ RegionBSPTree3D tree = new RegionBSPTree3D(true);
+ tree.getRoot().cut(Plane.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
+
+ // act
+ RegionBSPTree3D copy = tree.copy();
+
+ // assert
+ Assert.assertNotSame(tree, copy);
+ Assert.assertEquals(3, copy.count());
+ }
+
+ @Test
+ public void testBoundaries() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(Vector3D.ZERO, Vector3D.of(1, 1, 1))
+ .build();
+
+ // act
+ List<ConvexSubPlane> subplanes = new ArrayList<>();
+ tree.boundaries().forEach(subplanes::add);
+
+ // assert
+ Assert.assertEquals(6, subplanes.size());
+ }
+
+ @Test
+ public void testGetBoundaries() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(Vector3D.ZERO, Vector3D.of(1, 1, 1))
+ .build();
+
+ // act
+ List<ConvexSubPlane> subplanes = tree.getBoundaries();
+
+ // assert
+ Assert.assertEquals(6, subplanes.size());
+ }
+
+ @Test
+ public void testHalfSpace() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.empty();
+ tree.insert(Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, TEST_PRECISION).span());
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ EuclideanTestUtils.assertPositiveInfinity(tree.getSize());
+ EuclideanTestUtils.assertPositiveInfinity(tree.getBoundarySize());
+ Assert.assertNull(tree.getBarycenter());
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
+ Vector3D.of(-100, -100, -100));
+ checkClassify(tree, RegionLocation.BOUNDARY, Vector3D.of(0, 0, 0));
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(100, 100, 100),
+ Vector3D.of(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
+ }
+
+ @Test
+ public void testFromConvexVolume_full() {
+ // arrange
+ ConvexVolume volume = ConvexVolume.full();
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.from(volume);
+ Assert.assertNull(tree.getBarycenter());
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ }
+
+ @Test
+ public void testFromConvexVolume_infinite() {
+ // arrange
+ ConvexVolume volume = ConvexVolume.fromBounds(Plane.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.from(volume);
+
+ // assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
+ Assert.assertNull(tree.getBarycenter());
+
+ checkClassify(tree, RegionLocation.OUTSIDE, Vector3D.of(0, 0, 1));
+ checkClassify(tree, RegionLocation.BOUNDARY, Vector3D.ZERO);
+ checkClassify(tree, RegionLocation.INSIDE, Vector3D.of(0, 0, -1));
+ }
+
+ @Test
+ public void testFromConvexVolume_finite() {
+ // arrange
+ ConvexVolume volume = ConvexVolume.fromBounds(
+ Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_X, TEST_PRECISION),
+ Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Y, TEST_PRECISION),
+ Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Z, TEST_PRECISION),
+
+ Plane.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, TEST_PRECISION),
+ Plane.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y, TEST_PRECISION),
+ Plane.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION)
+ );
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.from(volume);
+
+ // assert
+ Assert.assertEquals(1, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0.5, 0.5), Vector3D.of(2, 0.5, 0.5),
+ Vector3D.of(0.5, -1, 0.5), Vector3D.of(0.5, 2, 0.5),
+ Vector3D.of(0.5, 0.5, -1), Vector3D.of(0.5, 0.5, 2));
+ checkClassify(tree, RegionLocation.BOUNDARY, Vector3D.ZERO);
+ checkClassify(tree, RegionLocation.INSIDE, Vector3D.of(0.5, 0.5, 0.5));
+ }
+
+ @Test
+ public void testRaycastFirstFace() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.ZERO, 2)
+ .build();
+
+ Line3D xPlus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(1, 0, 0), TEST_PRECISION);
+ Line3D xMinus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(-1, 0, 0), TEST_PRECISION);
+
+ Line3D yPlus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(0, 1, 0), TEST_PRECISION);
+ Line3D yMinus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(0, -1, 0), TEST_PRECISION);
+
+ Line3D zPlus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(0, 0, 1), TEST_PRECISION);
+ Line3D zMinus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(0, 0, -1), TEST_PRECISION);
+
+ // act/assert
+ assertSubPlaneNormal(Vector3D.of(-1, 0, 0), tree.raycastFirst(xPlus.segmentFrom(Vector3D.of(-1.1, 0, 0))));
+ assertSubPlaneNormal(Vector3D.of(-1, 0, 0), tree.raycastFirst(xPlus.segmentFrom(Vector3D.of(-1, 0, 0))));
+ assertSubPlaneNormal(Vector3D.of(1, 0, 0), tree.raycastFirst(xPlus.segmentFrom(Vector3D.of(-0.9, 0, 0))));
+ Assert.assertEquals(null, tree.raycastFirst(xPlus.segmentFrom(Vector3D.of(1.1, 0, 0))));
+
+ assertSubPlaneNormal(Vector3D.of(1, 0, 0), tree.raycastFirst(xMinus.segmentFrom(Vector3D.of(1.1, 0, 0))));
+ assertSubPlaneNormal(Vector3D.of(1, 0, 0), tree.raycastFirst(xMinus.segmentFrom(Vector3D.of(1, 0, 0))));
+ assertSubPlaneNormal(Vector3D.of(-1, 0, 0), tree.raycastFirst(xMinus.segmentFrom(Vector3D.of(0.9, 0, 0))));
+ Assert.assertEquals(null, tree.raycastFirst(xMinus.segmentFrom(Vector3D.of(-1.1, 0, 0))));
+
+ assertSubPlaneNormal(Vector3D.of(0, -1, 0), tree.raycastFirst(yPlus.segmentFrom(Vector3D.of(0, -1.1, 0))));
+ assertSubPlaneNormal(Vector3D.of(0, -1, 0), tree.raycastFirst(yPlus.segmentFrom(Vector3D.of(0, -1, 0))));
+ assertSubPlaneNormal(Vector3D.of(0, 1, 0), tree.raycastFirst(yPlus.segmentFrom(Vector3D.of(0, -0.9, 0))));
+ Assert.assertEquals(null, tree.raycastFirst(yPlus.segmentFrom(Vector3D.of(0, 1.1, 0))));
+
+ assertSubPlaneNormal(Vector3D.of(0, 1, 0), tree.raycastFirst(yMinus.segmentFrom(Vector3D.of(0, 1.1, 0))));
+ assertSubPlaneNormal(Vector3D.of(0, 1, 0), tree.raycastFirst(yMinus.segmentFrom(Vector3D.of(0, 1, 0))));
+ assertSubPlaneNormal(Vector3D.of(0, -1, 0), tree.raycastFirst(yMinus.segmentFrom(Vector3D.of(0, 0.9, 0))));
+ Assert.assertEquals(null, tree.raycastFirst(yMinus.segmentFrom(Vector3D.of(0, -1.1, 0))));
+
+ assertSubPlaneNormal(Vector3D.of(0, 0, -1), tree.raycastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -1.1))));
+ assertSubPlaneNormal(Vector3D.of(0, 0, -1), tree.raycastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -1))));
+ assertSubPlaneNormal(Vector3D.of(0, 0, 1), tree.raycastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -0.9))));
+ Assert.assertEquals(null, tree.raycastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, 1.1))));
+
+ assertSubPlaneNormal(Vector3D.of(0, 0, 1), tree.raycastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 1.1))));
+ assertSubPlaneNormal(Vector3D.of(0, 0, 1), tree.raycastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 1))));
+ assertSubPlaneNormal(Vector3D.of(0, 0, -1), tree.raycastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 0.9))));
+ Assert.assertEquals(null, tree.raycastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, -1.1))));
+ }
+
+ // issue GEOMETRY-38
+ @Test
+ public void testRaycastFirstFace_linePassesThroughVertex() {
+ // arrange
+ Vector3D lowerCorner = Vector3D.ZERO;
+ Vector3D upperCorner = Vector3D.of(1, 1, 1);
+ Vector3D center = lowerCorner.lerp(upperCorner, 0.5);
+
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(lowerCorner, upperCorner)
+ .build();
+
+ Line3D upDiagonal = Line3D.fromPoints(lowerCorner, upperCorner, TEST_PRECISION);
+ Line3D downDiagonal = upDiagonal.reverse();
+
+ // act/assert
+ ConvexSubPlane upFromOutsideResult = tree.raycastFirst(upDiagonal.segmentFrom(Vector3D.of(-1, -1, -1)));
+ Assert.assertNotNull(upFromOutsideResult);
+ EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, upFromOutsideResult.getPlane().intersection(upDiagonal), TEST_EPS);
+
+ ConvexSubPlane upFromCenterResult = tree.raycastFirst(upDiagonal.segmentFrom(center));
+ Assert.assertNotNull(upFromCenterResult);
+ EuclideanTestUtils.assertCoordinatesEqual(upperCorner, upFromCenterResult.getPlane().intersection(upDiagonal), TEST_EPS);
+
+ ConvexSubPlane downFromOutsideResult = tree.raycastFirst(downDiagonal.segmentFrom(Vector3D.of(2, 2, 2)));
+ Assert.assertNotNull(downFromOutsideResult);
+ EuclideanTestUtils.assertCoordinatesEqual(upperCorner, downFromOutsideResult.getPlane().intersection(downDiagonal), TEST_EPS);
+
+ ConvexSubPlane downFromCenterResult = tree.raycastFirst(downDiagonal.segmentFrom(center));
+ Assert.assertNotNull(downFromCenterResult);
+ EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, downFromCenterResult.getPlane().intersection(downDiagonal), TEST_EPS);
+ }
+
+ // Issue GEOMETRY-43
+ @Test
+ public void testFirstIntersection_lineParallelToFace() {
+ // arrange - setup box
+ Vector3D lowerCorner = Vector3D.ZERO;
+ Vector3D upperCorner = Vector3D.of(1, 1, 1);
+
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(lowerCorner, upperCorner)
+ .build();
+
+ Vector3D firstPointOnLine = Vector3D.of(0.5, -1.0, 0);
+ Vector3D secondPointOnLine = Vector3D.of(0.5, 2.0, 0);
+ Line3D bottomLine = Line3D.fromPoints(firstPointOnLine, secondPointOnLine, TEST_PRECISION);
+
+ Vector3D expectedIntersection1 = Vector3D.of(0.5, 0, 0.0);
+ Vector3D expectedIntersection2 = Vector3D.of(0.5, 1.0, 0.0);
+
+ // act/assert
+ ConvexSubPlane bottom = tree.raycastFirst(bottomLine.segmentFrom(firstPointOnLine));
+ Assert.assertNotNull(bottom);
+ EuclideanTestUtils.assertCoordinatesEqual(expectedIntersection1, bottom.getHyperplane().intersection(bottomLine), TEST_EPS);
+
+ bottom = tree.raycastFirst(bottomLine.segmentFrom(Vector3D.of(0.5, 0.1, 0.0)));
+ Assert.assertNotNull(bottom);
+ Vector3D intersection = bottom.getPlane().intersection(bottomLine);
+ Assert.assertNotNull(intersection);
+ EuclideanTestUtils.assertCoordinatesEqual(expectedIntersection2, intersection, TEST_EPS);
+ }
+
+ @Test
+ public void testRaycastFirstFace_rayPointOnFace() {
+ // arrange
+ Vector3D lowerCorner = Vector3D.ZERO;
+ Vector3D upperCorner = Vector3D.of(1, 1, 1);
+
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(lowerCorner, upperCorner)
+ .build();
+
+ Vector3D pt = Vector3D.of(0.5, 0.5, 0);
+ Line3D intoBoxLine = Line3D.fromPoints(pt, pt.add(Vector3D.Unit.PLUS_Z), TEST_PRECISION);
+ Line3D outOfBoxLine = Line3D.fromPoints(pt, pt.add(Vector3D.Unit.MINUS_Z), TEST_PRECISION);
+
+ // act/assert
+ ConvexSubPlane intoBoxResult = tree.raycastFirst(intoBoxLine.segmentFrom(pt));
+ Vector3D intoBoxPt = intoBoxResult.getPlane().intersection(intoBoxLine);
+ EuclideanTestUtils.assertCoordinatesEqual(pt, intoBoxPt, TEST_EPS);
+
+ ConvexSubPlane outOfBoxResult = tree.raycastFirst(outOfBoxLine.segmentFrom(pt));
+ Vector3D outOfBoxPt = outOfBoxResult.getPlane().intersection(outOfBoxLine);
+ EuclideanTestUtils.assertCoordinatesEqual(pt, outOfBoxPt, TEST_EPS);
+ }
+
+ @Test
+ public void testRaycastFirstFace_rayPointOnVertex() {
+ // arrange
+ Vector3D lowerCorner = Vector3D.ZERO;
+ Vector3D upperCorner = Vector3D.of(1, 1, 1);
+
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(lowerCorner, upperCorner)
+ .build();
+
+ Line3D intoBoxLine = Line3D.fromPoints(lowerCorner, upperCorner, TEST_PRECISION);
+ Line3D outOfBoxLine = intoBoxLine.reverse();
+
+ // act/assert
+ ConvexSubPlane intoBoxResult = tree.raycastFirst(intoBoxLine.segmentFrom(lowerCorner));
+ Vector3D intoBoxPt = intoBoxResult.getPlane().intersection(intoBoxLine);
+ EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, intoBoxPt, TEST_EPS);
+
+ ConvexSubPlane outOfBoxResult = tree.raycastFirst(outOfBoxLine.segmentFrom(lowerCorner));
+ Vector3D outOfBoxPt = outOfBoxResult.getPlane().intersection(outOfBoxLine);
+ EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, outOfBoxPt, TEST_EPS);
+ }
+
+ @Test
+ public void testRaycastFirstFace_onlyReturnsPointsWithinSegment() throws IOException, ParseException {
+ // arrange
+ Vector3D lowerCorner = Vector3D.ZERO;
+ Vector3D upperCorner = Vector3D.of(1, 1, 1);
+
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(lowerCorner, upperCorner)
+ .build();
+
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act/assert
+ assertSubPlaneNormal(Vector3D.Unit.MINUS_X, tree.raycastFirst(line.span()));
+ assertSubPlaneNormal(Vector3D.Unit.PLUS_X, tree.raycastFirst(line.reverse().span()));
+
+ assertSubPlaneNormal(Vector3D.Unit.MINUS_X, tree.raycastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(0.5, 0.5, 0.5))));
+ assertSubPlaneNormal(Vector3D.Unit.MINUS_X, tree.raycastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5))));
+
+ assertSubPlaneNormal(Vector3D.Unit.PLUS_X, tree.raycastFirst(line.segment(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(2, 0.5, 0.5))));
+ assertSubPlaneNormal(Vector3D.Unit.PLUS_X, tree.raycastFirst(line.segment(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1, 0.5, 0.5))));
+
+ Assert.assertNull(tree.raycastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(-1, 0.5, 0.5))));
+ Assert.assertNull(tree.raycastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(-1, 0.5, 0.5))));
+ Assert.assertNull(tree.raycastFirst(line.segment(Vector3D.of(0.25, 0.5, 0.5), Vector3D.of(0.75, 0.5, 0.5))));
+ }
+
+ @Test
+ public void testInvertedRegion() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.ZERO, 1)
+ .build();
+
+ // act
+ tree.complement();
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ EuclideanTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
+ Vector3D.of(-100, -100, -100),
+ Vector3D.of(100, 100, 100),
+ Vector3D.of(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(0, 0, 0));
+ }
+
+ @Test
+ public void testUnitBox() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.ZERO, 1)
+ .build();
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(1.0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(6.0, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0, 0),
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(0, -1, 0),
+ Vector3D.of(0, 1, 0),
+ Vector3D.of(0, 0, -1),
+ Vector3D.of(0, 0, 1),
+
+ Vector3D.of(1, 1, 1),
+ Vector3D.of(1, 1, -1),
+ Vector3D.of(1, -1, 1),
+ Vector3D.of(1, -1, -1),
+ Vector3D.of(-1, 1, 1),
+ Vector3D.of(-1, 1, -1),
+ Vector3D.of(-1, -1, 1),
+ Vector3D.of(-1, -1, -1));
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Vector3D.of(0.5, 0, 0),
+ Vector3D.of(-0.5, 0, 0),
+ Vector3D.of(0, 0.5, 0),
+ Vector3D.of(0, -0.5, 0),
+ Vector3D.of(0, 0, 0.5),
+ Vector3D.of(0, 0, -0.5),
+
+ Vector3D.of(0.5, 0.5, 0.5),
+ Vector3D.of(0.5, 0.5, -0.5),
+ Vector3D.of(0.5, -0.5, 0.5),
+ Vector3D.of(0.5, -0.5, -0.5),
+ Vector3D.of(-0.5, 0.5, 0.5),
+ Vector3D.of(-0.5, 0.5, -0.5),
+ Vector3D.of(-0.5, -0.5, 0.5),
+ Vector3D.of(-0.5, -0.5, -0.5));
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(0, 0, 0),
+
+ Vector3D.of(0.4, 0.4, 0.4),
+ Vector3D.of(0.4, 0.4, -0.4),
+ Vector3D.of(0.4, -0.4, 0.4),
+ Vector3D.of(0.4, -0.4, -0.4),
+ Vector3D.of(-0.4, 0.4, 0.4),
+ Vector3D.of(-0.4, 0.4, -0.4),
+ Vector3D.of(-0.4, -0.4, 0.4),
+ Vector3D.of(-0.4, -0.4, -0.4));
+ }
+
+ @Test
+ public void testTwoBoxes_disjoint() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.empty();
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.ZERO, 1)
+ .build());
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.of(2, 0, 0), 1)
+ .build());
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(2.0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(12.0, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 0), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0, 0),
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(3, 0, 0));
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(2, 0, 0));
+ }
+
+ @Test
+ public void testTwoBoxes_sharedSide() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.empty();
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.ZERO, 1)
+ .build());
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.of(1, 0, 0), 1)
+ .build());
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(2.0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(10.0, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0, 0), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0, 0),
+ Vector3D.of(2, 0, 0));
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(1, 0, 0));
+ }
+
+ @Test
+ public void testTwoBoxes_separationLessThanTolerance() {
+ // arrange
+ double eps = 1e-6;
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.empty();
+ tree.union(RegionBSPTree3D.builder(precision)
+ .addCenteredCube(Vector3D.ZERO, 1)
+ .build());
+ tree.union(RegionBSPTree3D.builder(precision)
+ .addCenteredCube(Vector3D.of(1 + 1e-7, 0, 0), 1)
+ .build());
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(2.0, tree.getSize(), eps);
+ Assert.assertEquals(10.0, tree.getBoundarySize(), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5 + 5.208e-8, 0, 0), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0, 0),
+ Vector3D.of(2, 0, 0));
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(1, 0, 0));
+ }
+
+ @Test
+ public void testTwoBoxes_sharedEdge() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.empty();
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.ZERO, 1)
+ .build());
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.of(1, 1, 0), 1)
+ .build());
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(2.0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(12.0, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0), tree.getBarycenter(), TEST_EPS);
+
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0, 0),
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(0, 1, 0),
+ Vector3D.of(2, 1, 0));
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(1, 1, 0));
+ }
+
+ @Test
+ public void testTwoBoxes_sharedPoint() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.empty();
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.ZERO, 1)
+ .build());
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCenteredCube(Vector3D.of(1, 1, 1), 1)
+ .build());
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(2.0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(12.0, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0, 0),
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(0, 1, 1),
+ Vector3D.of(2, 1, 1));
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(1, 1, 1));
+ }
+
+ @Test
+ public void testTetrahedron() {
+ // arrange
+ Vector3D vertex1 = Vector3D.of(1, 2, 3);
+ Vector3D vertex2 = Vector3D.of(2, 2, 4);
+ Vector3D vertex3 = Vector3D.of(2, 3, 3);
+ Vector3D vertex4 = Vector3D.of(1, 3, 4);
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addFacet(vertex3, vertex2, vertex1)
+ .addFacet(vertex2, vertex3, vertex4)
+ .addFacet(vertex4, vertex3, vertex1)
+ .addFacet(vertex1, vertex2, vertex4)
+ .build();
+
+ // assert
+ Assert.assertEquals(1.0 / 3.0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(2.0 * Math.sqrt(3.0), tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2.5, 3.5), tree.getBarycenter(), TEST_EPS);
+
+ double third = 1.0 / 3.0;
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ vertex1, vertex2, vertex3, vertex4,
+ Vector3D.linearCombination(third, vertex1, third, vertex2, third, vertex3),
+ Vector3D.linearCombination(third, vertex2, third, vertex3, third, vertex4),
+ Vector3D.linearCombination(third, vertex3, third, vertex4, third, vertex1),
+ Vector3D.linearCombination(third, vertex4, third, vertex1, third, vertex2)
+ );
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(1, 2, 4),
+ Vector3D.of(2, 2, 3),
+ Vector3D.of(2, 3, 4),
+ Vector3D.of(1, 3, 3)
+ );
+ }
+
+ @Test
+ public void testSphere() {
+ // arrange
+ // (use a high tolerance value here since the sphere is only an approximation)
+ double approximationTolerance = 0.2;
+ double radius = 1.0;
+
+ // act
+ RegionBSPTree3D tree = createSphere(Vector3D.of(1, 2, 3), radius, 8, 16);
+
+ // assert
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertFalse(tree.isFull());
+
+ Assert.assertEquals(sphereVolume(radius), tree.getSize(), approximationTolerance);
+ Assert.assertEquals(sphereSurface(radius), tree.getBoundarySize(), approximationTolerance);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(-0.1, 2, 3),
+ Vector3D.of(2.1, 2, 3),
+ Vector3D.of(1, 0.9, 3),
+ Vector3D.of(1, 3.1, 3),
+ Vector3D.of(1, 2, 1.9),
+ Vector3D.of(1, 2, 4.1),
+ Vector3D.of(1.6, 2.6, 3.6));
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(1, 2, 3),
+ Vector3D.of(0.1, 2, 3),
+ Vector3D.of(1.9, 2, 3),
+ Vector3D.of(1, 2.1, 3),
+ Vector3D.of(1, 2.9, 3),
+ Vector3D.of(1, 2, 2.1),
+ Vector3D.of(1, 2, 3.9),
+ Vector3D.of(1.5, 2.5, 3.5));
+ }
+
+ @Test
+ public void testProjectToBoundary() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.ZERO, 1)
+ .build();
+
+ // act/assert
+ checkProject(tree, Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5));
+ checkProject(tree, Vector3D.of(0.4, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5));
+ checkProject(tree, Vector3D.of(1.5, 0.5, 0.5), Vector3D.of(1, 0.5, 0.5));
+ checkProject(tree, Vector3D.of(2, 2, 2), Vector3D.of(1, 1, 1));
+ }
+
+ @Test
+ public void testProjectToBoundary_invertedRegion() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.ZERO, 1)
+ .build();
+
+ tree.complement();
+
+ // act/assert
+ checkProject(tree, Vector3D.of(0.4, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5));
+ checkProject(tree, Vector3D.of(1.5, 0.5, 0.5), Vector3D.of(1, 0.5, 0.5));
+ checkProject(tree, Vector3D.of(2, 2, 2), Vector3D.of(1, 1, 1));
+ }
+
+ private void checkProject(RegionBSPTree3D tree, Vector3D toProject, Vector3D expectedPoint) {
+ Vector3D proj = tree.project(toProject);
+
+ EuclideanTestUtils.assertCoordinatesEqual(expectedPoint, proj, TEST_EPS);
+ }
+
+ @Test
+ public void testBoolean_union() throws IOException {
+ // arrange
+ double tolerance = 0.05;
+ double size = 1.0;
+ double radius = size * 0.5;
+ RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.ZERO, size)
+ .build();
+ RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.union(box, sphere);
+
+ // assert
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals(cubeVolume(size) + (sphereVolume(radius) * 0.5),
+ result.getSize(), tolerance);
+ Assert.assertEquals(cubeSurface(size) - circleSurface(radius) + (0.5 * sphereSurface(radius)),
+ result.getBoundarySize(), tolerance);
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-0.1, 0.5, 0.5),
+ Vector3D.of(1.1, 0.5, 0.5),
+ Vector3D.of(0.5, -0.1, 0.5),
+ Vector3D.of(0.5, 1.1, 0.5),
+ Vector3D.of(0.5, 0.5, -0.1),
+ Vector3D.of(0.5, 0.5, 1.6));
+
+ checkClassify(result, RegionLocation.INSIDE,
+ Vector3D.of(0.1, 0.5, 0.5),
+ Vector3D.of(0.9, 0.5, 0.5),
+ Vector3D.of(0.5, 0.1, 0.5),
+ Vector3D.of(0.5, 0.9, 0.5),
+ Vector3D.of(0.5, 0.5, 0.1),
+ Vector3D.of(0.5, 0.5, 1.4));
+ }
+
+ @Test
+ public void testUnion_self() {
+ // arrange
+ double tolerance = 0.2;
+ double radius = 1.0;
+
+ RegionBSPTree3D sphere = createSphere(Vector3D.ZERO, radius, 8, 16);
+
+ RegionBSPTree3D copy = RegionBSPTree3D.empty();
+ copy.copy(sphere);
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.union(sphere, copy);
+
+ // assert
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance);
+ Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getBarycenter(), TEST_EPS);
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-1.1, 0, 0),
+ Vector3D.of(1.1, 0, 0),
+ Vector3D.of(0, -1.1, 0),
+ Vector3D.of(0, 1.1, 0),
+ Vector3D.of(0, 0, -1.1),
+ Vector3D.of(0, 0, 1.1));
+
+ checkClassify(result, RegionLocation.INSIDE,
+ Vector3D.of(-0.9, 0, 0),
+ Vector3D.of(0.9, 0, 0),
+ Vector3D.of(0, -0.9, 0),
+ Vector3D.of(0, 0.9, 0),
+ Vector3D.of(0, 0, -0.9),
+ Vector3D.of(0, 0, 0.9),
+ Vector3D.ZERO);
+ }
+
+ @Test
+ public void testBoolean_intersection() throws IOException {
+ // arrange
+ double tolerance = 0.05;
+ double size = 1.0;
+ double radius = size * 0.5;
+ RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.ZERO, size)
+ .build();
+ RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.intersection(box, sphere);
+
+ // assert
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals((sphereVolume(radius) * 0.5), result.getSize(), tolerance);
+ Assert.assertEquals(circleSurface(radius) + (0.5 * sphereSurface(radius)),
+ result.getBoundarySize(), tolerance);
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-0.1, 0.5, 1.0),
+ Vector3D.of(1.1, 0.5, 1.0),
+ Vector3D.of(0.5, -0.1, 1.0),
+ Vector3D.of(0.5, 1.1, 1.0),
+ Vector3D.of(0.5, 0.5, 0.4),
+ Vector3D.of(0.5, 0.5, 1.1));
+
+ checkClassify(result, RegionLocation.INSIDE,
+ Vector3D.of(0.1, 0.5, 0.9),
+ Vector3D.of(0.9, 0.5, 0.9),
+ Vector3D.of(0.5, 0.1, 0.9),
+ Vector3D.of(0.5, 0.9, 0.9),
+ Vector3D.of(0.5, 0.5, 0.6),
+ Vector3D.of(0.5, 0.5, 0.9));
+ }
+
+ @Test
+ public void testIntersection_self() {
+ // arrange
+ double tolerance = 0.2;
+ double radius = 1.0;
+
+ RegionBSPTree3D sphere = createSphere(Vector3D.ZERO, radius, 8, 16);
+ RegionBSPTree3D copy = RegionBSPTree3D.empty();
+ copy.copy(sphere);
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.intersection(sphere, copy);
+
+ // assert
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance);
+ Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getBarycenter(), TEST_EPS);
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-1.1, 0, 0),
+ Vector3D.of(1.1, 0, 0),
+ Vector3D.of(0, -1.1, 0),
+ Vector3D.of(0, 1.1, 0),
+ Vector3D.of(0, 0, -1.1),
+ Vector3D.of(0, 0, 1.1));
+
+ checkClassify(result, RegionLocation.INSIDE,
+ Vector3D.of(-0.9, 0, 0),
+ Vector3D.of(0.9, 0, 0),
+ Vector3D.of(0, -0.9, 0),
+ Vector3D.of(0, 0.9, 0),
+ Vector3D.of(0, 0, -0.9),
+ Vector3D.of(0, 0, 0.9),
+ Vector3D.ZERO);
+ }
+
+ @Test
+ public void testBoolean_xor_twoCubes() throws IOException {
+ // arrange
+ double size = 1.0;
+ RegionBSPTree3D box1 = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.ZERO, size)
+ .build();
+ RegionBSPTree3D box2 = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.of(0.5, 0.5, 0.5), size)
+ .build();
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.xor(box1, box2);
+
+ // assert
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals((2 * cubeVolume(size)) - (2 * cubeVolume(size * 0.5)), result.getSize(), TEST_EPS);
+ Assert.assertEquals(2 * cubeSurface(size), result.getBoundarySize(), TEST_EPS);
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-0.1, -0.1, -0.1),
+ Vector3D.of(0.75, 0.75, 0.75),
+ Vector3D.of(1.6, 1.6, 1.6));
+
+ checkClassify(result, RegionLocation.BOUNDARY,
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(0.5, 0.5, 0.5),
+ Vector3D.of(1, 1, 1),
+ Vector3D.of(1.5, 1.5, 1.5));
+
+ checkClassify(result, RegionLocation.INSIDE,
+ Vector3D.of(0.1, 0.1, 0.1),
+ Vector3D.of(0.4, 0.4, 0.4),
+ Vector3D.of(1.1, 1.1, 1.1),
+ Vector3D.of(1.4, 1.4, 1.4));
+ }
+
+ @Test
+ public void testBoolean_xor_cubeAndSphere() throws IOException {
+ // arrange
+ double tolerance = 0.05;
+ double size = 1.0;
+ double radius = size * 0.5;
+ RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.ZERO, size)
+ .build();
+ RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.xor(box, sphere);
+
+ // assert
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals(cubeVolume(size), result.getSize(), tolerance);
+ Assert.assertEquals(cubeSurface(size) + (sphereSurface(radius)),
+ result.getBoundarySize(), tolerance);
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-0.1, 0.5, 0.5),
+ Vector3D.of(1.1, 0.5, 0.5),
+ Vector3D.of(0.5, -0.1, 0.5),
+ Vector3D.of(0.5, 1.1, 0.5),
+ Vector3D.of(0.5, 0.5, -0.1),
+ Vector3D.of(0.5, 0.5, 1.6),
+ Vector3D.of(0.5, 0.5, 0.9));
+
+ checkClassify(result, RegionLocation.INSIDE,
+ Vector3D.of(0.1, 0.5, 0.5),
+ Vector3D.of(0.9, 0.5, 0.5),
+ Vector3D.of(0.5, 0.1, 0.5),
+ Vector3D.of(0.5, 0.9, 0.5),
+ Vector3D.of(0.5, 0.5, 0.1),
+ Vector3D.of(0.5, 0.5, 1.4));
+ }
+
+ @Test
+ public void testXor_self() {
+ // arrange
+ double radius = 1.0;
+
+ RegionBSPTree3D sphere = createSphere(Vector3D.ZERO, radius, 8, 16);
+ RegionBSPTree3D copy = RegionBSPTree3D.empty();
+ copy.copy(sphere);
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.xor(sphere, copy);
+
+ // assert
+ Assert.assertTrue(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals(0.0, result.getSize(), TEST_EPS);
+ Assert.assertEquals(0.0, result.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(result.getBarycenter());
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-1.1, 0, 0),
+ Vector3D.of(1.1, 0, 0),
+ Vector3D.of(0, -1.1, 0),
+ Vector3D.of(0, 1.1, 0),
+ Vector3D.of(0, 0, -1.1),
+ Vector3D.of(0, 0, 1.1),
+ Vector3D.of(-0.9, 0, 0),
+ Vector3D.of(0.9, 0, 0),
+ Vector3D.of(0, -0.9, 0),
+ Vector3D.of(0, 0.9, 0),
+ Vector3D.of(0, 0, -0.9),
+ Vector3D.of(0, 0, 0.9),
+ Vector3D.ZERO);
+ }
+
+ @Test
+ public void testBoolean_difference() throws IOException {
+ // arrange
+ double tolerance = 0.05;
+ double size = 1.0;
+ double radius = size * 0.5;
+ RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.ZERO, size)
+ .build();
+ RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.difference(box, sphere);
+
+ // assert
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals(cubeVolume(size) - (sphereVolume(radius) * 0.5), result.getSize(), tolerance);
+ Assert.assertEquals(cubeSurface(size) - circleSurface(radius) + (0.5 * sphereSurface(radius)),
+ result.getBoundarySize(), tolerance);
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-0.1, 0.5, 1.0),
+ Vector3D.of(1.1, 0.5, 1.0),
+ Vector3D.of(0.5, -0.1, 1.0),
+ Vector3D.of(0.5, 1.1, 1.0),
+ Vector3D.of(0.5, 0.5, -0.1),
+ Vector3D.of(0.5, 0.5, 0.6));
+
+ checkClassify(result, RegionLocation.INSIDE,
+ Vector3D.of(0.1, 0.5, 0.4),
+ Vector3D.of(0.9, 0.5, 0.4),
+ Vector3D.of(0.5, 0.1, 0.4),
+ Vector3D.of(0.5, 0.9, 0.4),
+ Vector3D.of(0.5, 0.5, 0.1),
+ Vector3D.of(0.5, 0.5, 0.4));
+ }
+
+ @Test
+ public void testDifference_self() {
+ // arrange
+ double radius = 1.0;
+
+ RegionBSPTree3D sphere = createSphere(Vector3D.ZERO, radius, 8, 16);
+ RegionBSPTree3D copy = sphere.copy();
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.difference(sphere, copy);
+
+ // assert
+ Assert.assertTrue(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals(0.0, result.getSize(), TEST_EPS);
+ Assert.assertEquals(0.0, result.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(result.getBarycenter());
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-1.1, 0, 0),
+ Vector3D.of(1.1, 0, 0),
+ Vector3D.of(0, -1.1, 0),
+ Vector3D.of(0, 1.1, 0),
+ Vector3D.of(0, 0, -1.1),
+ Vector3D.of(0, 0, 1.1),
+ Vector3D.of(-0.9, 0, 0),
+ Vector3D.of(0.9, 0, 0),
+ Vector3D.of(0, -0.9, 0),
+ Vector3D.of(0, 0.9, 0),
+ Vector3D.of(0, 0, -0.9),
+ Vector3D.of(0, 0, 0.9),
+ Vector3D.ZERO);
+ }
+
+ @Test
+ public void testBoolean_multiple() throws IOException {
+ // arrange
+ double tolerance = 0.05;
+ double size = 1.0;
+ double radius = size * 0.5;
+ RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.ZERO, size)
+ .build();
+ RegionBSPTree3D sphereToAdd = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
+ RegionBSPTree3D sphereToRemove1 = createSphere(Vector3D.of(size * 0.5, 0, size * 0.5), radius, 8, 16);
+ RegionBSPTree3D sphereToRemove2 = createSphere(Vector3D.of(size * 0.5, 1, size * 0.5), radius, 8, 16);
+
+ // act
+ RegionBSPTree3D result = RegionBSPTree3D.empty();
+ result.union(box, sphereToAdd);
+ result.difference(sphereToRemove1);
+ result.difference(sphereToRemove2);
+
+ // assert
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertFalse(result.isFull());
+
+ Assert.assertEquals(cubeVolume(size) - (sphereVolume(radius) * 0.5),
+ result.getSize(), tolerance);
+ Assert.assertEquals(cubeSurface(size) - (3.0 * circleSurface(radius)) + (1.5 * sphereSurface(radius)),
+ result.getBoundarySize(), tolerance);
+
+ checkClassify(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-0.1, 0.5, 0.5),
+ Vector3D.of(1.1, 0.5, 0.5),
+ Vector3D.of(0.5, 0.4, 0.5),
+ Vector3D.of(0.5, 0.6, 0.5),
+ Vector3D.of(0.5, 0.5, -0.1),
+ Vector3D.of(0.5, 0.5, 1.6));
+
+ checkClassify(result, RegionLocation.INSIDE,
+ Vector3D.of(0.1, 0.5, 0.1),
+ Vector3D.of(0.9, 0.5, 0.1),
+ Vector3D.of(0.5, 0.4, 0.1),
+ Vector3D.of(0.5, 0.6, 0.1),
+ Vector3D.of(0.5, 0.5, 0.1),
+ Vector3D.of(0.5, 0.5, 1.4));
+ }
+
+ @Test
+ public void testToConvex_empty() {
+ // act
+ List<ConvexVolume> result = RegionBSPTree3D.empty().toConvex();
+
+ // assert
+ Assert.assertEquals(0, result.size());
+ }
+
+ @Test
+ public void testToConvex_singleBox() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.of(1, 2, 3), 1)
+ .build();
+
+ // act
+ List<ConvexVolume> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+
+ ConvexVolume vol = result.get(0);
+ Assert.assertEquals(1, vol.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2.5, 3.5), vol.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testToConvex_multipleBoxes() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.of(4, 5, 6), 1)
+ .build();
+ tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(Vector3D.ZERO, 2, 1, 1)
+ .build());
+
+ // act
+ List<ConvexVolume> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(2, result.size());
+
+ boolean smallFirst = result.get(0).getSize() < result.get(1).getSize();
+
+ ConvexVolume small = smallFirst ? result.get(0) : result.get(1);
+ ConvexVolume large = smallFirst ? result.get(1) : result.get(0);
+
+ Assert.assertEquals(1, small.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4.5, 5.5, 6.5), small.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(2, large.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0.5, 0.5), large.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testSplit() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.of(-0.5, -0.5, -0.5), 1)
+ .build();
+
+ Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree3D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ RegionBSPTree3D minus = split.getMinus();
+ Assert.assertEquals(0.5, minus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.25, 0, 0), minus.getBarycenter(), TEST_EPS);
+
+ RegionBSPTree3D plus = split.getPlus();
+ Assert.assertEquals(0.5, plus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.25, 0, 0), plus.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetNodeRegion() {
+ // arrange
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addRect(Vector3D.ZERO, Vector3D.of(1, 1, 1))
+ .build();
+
+ // act/assert
+ ConvexVolume rootVol = tree.getRoot().getNodeRegion();
+ GeometryTestUtils.assertPositiveInfinity(rootVol.getSize());
+ Assert.assertNull(rootVol.getBarycenter());
+
+ ConvexVolume plusVol = tree.getRoot().getPlus().getNodeRegion();
+ GeometryTestUtils.assertPositiveInfinity(plusVol.getSize());
+ Assert.assertNull(plusVol.getBarycenter());
+
+ ConvexVolume centerVol = tree.findNode(Vector3D.of(0.5, 0.5, 0.5)).getNodeRegion();
+ Assert.assertEquals(1, centerVol.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), centerVol.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testBuilder_nothingAdded() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION).build();
+
+ // assert
+ Assert.assertTrue(tree.isEmpty());
+ }
+
+ @Test
+ public void testBuilder_rectMethods() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+
+ .addRect(Vector3D.ZERO, Vector3D.of(2, 1, 1))
+ .addRect(Vector3D.of(0, 0, -1), Vector3D.of(-1, -2, -2))
+
+ .addRect(Vector3D.of(0, 0, 5), 1, 2, 3)
+ .addRect(Vector3D.of(0, 0, 10), -3, -2, -1)
+
+ .addCenteredRect(Vector3D.of(0, 0, 15), 2, 3, 4)
+ .addCenteredRect(Vector3D.of(0, 0, 20), 4, 3, 2)
+
+ .build();
+
+ // act
+ Assert.assertEquals(2 + 2 + 6 + 6 + 24 + 24, tree.getSize(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(1, 0.5, 0.5),
+ Vector3D.of(-0.5, -1, -1.5),
+
+ Vector3D.of(0.5, 1, 6.5),
+ Vector3D.of(-1.5, -1, 9.5),
+
+ Vector3D.of(0, 0, 15),
+ Vector3D.of(0, 0, 20));
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Vector3D.of(-1, -1.5, 13),
+ Vector3D.of(1, 1.5, 17),
+
+ Vector3D.of(-2, -1.5, 19),
+ Vector3D.of(2, 1.5, 21));
+ }
+
+ @Test
+ public void testBuilder_rectMethods_invalidDimensions() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.ZERO, 1e-20, 1, 1);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.ZERO, 1, 1e-20, 1);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.ZERO, 1, 1, 1e-20);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.ZERO, 0, 0, 0);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.of(1, 2, 3), Vector3D.of(1, 2, 3));
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addCenteredRect(Vector3D.of(1, 2, 3), 0, 0, 0);
+ }, GeometryValueException.class);
+ }
+
+ @Test
+ public void testBuilder_cubeMethods() {
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .addCube(Vector3D.of(1, 0, 0), 2)
+ .addCenteredCube(Vector3D.of(-2, -3, -4), 3)
+ .build();
+
+ // assert
+ Assert.assertEquals(8 + 27, tree.getSize(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector3D.of(2, 1, 1),
+ Vector3D.of(-2, -3, -4));
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Vector3D.of(-3.5, -4.5, -5.5),
+ Vector3D.of(-0.5, -1.5, -2.5));
+ }
+
+ @Test
+ public void testBuilder_cubeMethods_invalidDimensions() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addCube(Vector3D.ZERO, 1e-20);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addCube(Vector3D.of(1, 2, 3), 1e-20);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ RegionBSPTree3D.builder(TEST_PRECISION).addCenteredCube(Vector3D.of(1, 2, 3), 0);
+ }, GeometryValueException.class);
+ }
+
+ @Test
+ public void testBuilder_addIndexedFacets_triangles() {
+ // arrange
+ Vector3D vertices[] = {
+ Vector3D.ZERO,
+ Vector3D.of(1, 0, 0),
+ Vector3D.of(1, 1, 0),
+ Vector3D.of(0, 1, 0),
+
+ Vector3D.of(0, 0, 1),
+ Vector3D.of(1, 0, 1),
+ Vector3D.of(1, 1, 1),
+ Vector3D.of(0, 1, 1)
+ };
+
+ int[][] facets = {
+ { 0, 3, 2 },
+ { 0, 2, 1 },
+
+ { 4, 5, 6 },
+ { 4, 6, 7 },
+
+ { 5, 1, 2 },
+ { 5, 2, 6 },
+
+ { 4, 7, 3 },
+ { 4, 3, 0 },
+
+ { 4, 0, 1 },
+ { 4, 1, 5 },
+
+ { 7, 6, 2 },
+ { 7, 2, 3 }
+ };
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .withVertexList(vertices)
+ .addIndexedFacets(facets)
+ .build();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(1, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testBuilder_addIndexedFacets_concaveFacets() {
+ // arrange
+ Vector3D[] vertices = {
+ Vector3D.of(-1, 0, 1),
+ Vector3D.of(-1, 0, 0),
+
+ Vector3D.of(0, 2, 1),
+ Vector3D.of(0, 2, 0),
+
+ Vector3D.of(1, 0, 1),
+ Vector3D.of(1, 0, 0),
+
+ Vector3D.of(0, 1, 1),
+ Vector3D.of(0, 1, 0)
+ };
+
+ int[][] facets = {
+ { 0, 2, 3, 1 },
+ { 4, 5, 3, 2 },
+ { 0, 1, 7, 6 },
+ { 4, 6, 7, 5 },
+ { 0, 6, 4, 2 },
+ { 1, 3, 5, 7 }
+ };
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .withVertexList(vertices)
+ .addIndexedFacets(facets)
+ .build();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertTrue(Double.isFinite(tree.getSize()));
+ Assert.assertTrue(Double.isFinite(tree.getBoundarySize()));
+ Assert.assertNotNull(tree.getBarycenter());
+
+ checkClassify(tree, RegionLocation.INSIDE, Vector3D.of(0, 1.5, 0.5));
+ checkClassify(tree, RegionLocation.OUTSIDE, Vector3D.of(0, 0.5, 0.5));
+ }
+
+ @Test
+ public void testBuilder_addIndexedFacets_multipleVertexLists() {
+ // arrange
+ Vector3D p0 = Vector3D.ZERO;
+ Vector3D p1 = Vector3D.Unit.PLUS_X;
+ Vector3D p2 = Vector3D.Unit.PLUS_Y;
+ Vector3D p3 = Vector3D.Unit.PLUS_Z;
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .withVertexList(p1, p2, p3)
+ .addIndexedFacet(Arrays.asList(2, 0, 1))
+
+ .withVertexList(p0, p1, p2, p3)
+ .addIndexedFacet(0, 2, 1)
+ .addIndexedFacets(new int[][] {
+ { 0, 1, 3 },
+ { 0, 3, 2 }
+ })
+ .build();
+
+ // assert
+ Assert.assertEquals(0.5 / 3.0, tree.getSize(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.INSIDE, Vector3D.of(0.25, 0.25, 0.25));
+ }
+
+ // GEOMETRY-59
+ @Test
+ public void testSlightlyConcavePrism() {
+ // arrange
+ Vector3D vertices[] = {
+ Vector3D.of(0, 0, 0),
+ Vector3D.of(2, 1e-7, 0),
+ Vector3D.of(4, 0, 0),
+ Vector3D.of(2, 2, 0),
+ Vector3D.of(0, 0, 2),
+ Vector3D.of(2, 1e-7, 2),
+ Vector3D.of(4, 0, 2),
+ Vector3D.of(2, 2, 2)
+ };
+
+ int facets[][] = {
+ { 4, 5, 6, 7 },
+ { 3, 2, 1, 0 },
+ { 0, 1, 5, 4 },
+ { 1, 2, 6, 5 },
+ { 2, 3, 7, 6 },
+ { 3, 0, 4, 7 }
+ };
+
+ // act
+ RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
+ .withVertexList(vertices)
+ .addIndexedFacets(facets)
+ .build();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ checkClassify(tree, RegionLocation.INSIDE, Vector3D.of(2, 1, 1));
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector3D.of(2, 1, 3), Vector3D.of(2, 1, -3),
+ Vector3D.of(2, -1, 1), Vector3D.of(2, 3, 1),
+ Vector3D.of(-1, 1, 1), Vector3D.of(4, 1, 1));
+ }
+
+ private static RegionBSPTree3D createSphere(final Vector3D center, final double radius, final int stacks, final int slices) {
+
+ final List<Plane> planes = new ArrayList<>();
+
+ // add top and bottom planes (+/- z)
+ final Vector3D topZ = Vector3D.of(center.getX(), center.getY(), center.getZ() + radius);
+ final Vector3D bottomZ = Vector3D.of(center.getX(), center.getY(), center.getZ() - radius);
+
+ planes.add(Plane.fromPointAndNormal(topZ, Vector3D.Unit.PLUS_Z, TEST_PRECISION));
+ planes.add(Plane.fromPointAndNormal(bottomZ, Vector3D.Unit.MINUS_Z, TEST_PRECISION));
+
+ // add the side planes
+ final double vDelta = Geometry.PI / stacks;
+ final double hDelta = Geometry.PI * 2 / slices;
+
+ final double adjustedRadius = (radius + (radius * Math.cos(vDelta * 0.5))) / 2.0;
+
+ double vAngle;
+ double hAngle;
+ double stackRadius;
+ double stackHeight;
+ double x;
+ double y;
+ Vector3D pt;
+ Vector3D norm;
+
+ vAngle = -0.5 * vDelta;
+ for (int v=0; v<stacks; ++v) {
+ vAngle += vDelta;
+
+ stackRadius = Math.sin(vAngle) * adjustedRadius;
+ stackHeight = Math.cos(vAngle) * adjustedRadius;
+
+ hAngle = -0.5 * hDelta;
+ for (int h=0; h<slices; ++h) {
+ hAngle += hDelta;
+
+ x = Math.cos(hAngle) * stackRadius;
+ y = Math.sin(hAngle) * stackRadius;
+
+ norm = Vector3D.of(x, y, stackHeight).normalize();
+ pt = center.add(norm.multiply(adjustedRadius));
+
+ planes.add(Plane.fromPointAndNormal(pt, norm, TEST_PRECISION));
+ }
+ }
+
+ RegionBSPTree3D tree = RegionBSPTree3D.full();
+ RegionNode3D node = tree.getRoot();
+
+ for (Plane plane : planes) {
+ node = node.cut(plane).getMinus();
+ }
+
+ return tree;
+ }
+
+ private static void assertSubPlaneNormal(Vector3D expectedNormal, ConvexSubPlane sub) {
+ EuclideanTestUtils.assertCoordinatesEqual(expectedNormal, sub.getPlane().getNormal(), TEST_EPS);
+ }
+
+ private static void checkClassify(Region<Vector3D> region, RegionLocation loc, Vector3D ... points) {
+ for (Vector3D point : points) {
+ String msg = "Unexpected location for point " + point;
+
+ Assert.assertEquals(msg, loc, region.classify(point));
+ }
+ }
+
+ private double cubeVolume(double size) {
+ return size * size * size;
+ }
+
+ private double cubeSurface(double size) {
+ return 6.0 * size * size;
+ }
+
+ private double sphereVolume(double radius) {
+ return 4.0 * Math.PI * radius * radius * radius / 3.0;
+ }
+
+ private double sphereSurface(double radius) {
+ return 4.0 * Math.PI * radius * radius;
+ }
+
+ private double circleSurface(double radius) {
+ return Math.PI * radius * radius;
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Segment3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Segment3DTest.java
new file mode 100644
index 0000000..4af514d
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Segment3DTest.java
@@ -0,0 +1,387 @@
+/*
+ * 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.threed;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Segment3DTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testFromPoints() {
+ // arrange
+ Vector3D p0 = Vector3D.of(1, 3, 2);
+ Vector3D p1 = Vector3D.of(1, 2, 3);
+ Vector3D p2 = Vector3D.of(-3, 4, 5);
+ Vector3D p3 = Vector3D.of(-5, -6, -8);
+
+ // act/assert
+
+ checkFiniteSegment(Segment3D.fromPoints(p0, p1, TEST_PRECISION), p0, p1);
+ checkFiniteSegment(Segment3D.fromPoints(p1, p0, TEST_PRECISION), p1, p0);
+
+ checkFiniteSegment(Segment3D.fromPoints(p0, p2, TEST_PRECISION), p0, p2);
+ checkFiniteSegment(Segment3D.fromPoints(p2, p0, TEST_PRECISION), p2, p0);
+
+ checkFiniteSegment(Segment3D.fromPoints(p0, p3, TEST_PRECISION), p0, p3);
+ checkFiniteSegment(Segment3D.fromPoints(p3, p0, TEST_PRECISION), p3, p0);
+ }
+
+ @Test
+ public void testFromPoints_invalidArgs() {
+ // arrange
+ Vector3D p0 = Vector3D.of(-1, 2, -3);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Segment3D.fromPoints(p0, p0, TEST_PRECISION);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Segment3D.fromPoints(p0, Vector3D.POSITIVE_INFINITY, TEST_PRECISION);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Segment3D.fromPoints(p0, Vector3D.NEGATIVE_INFINITY, TEST_PRECISION);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Segment3D.fromPoints(p0, Vector3D.NaN, TEST_PRECISION);
+ }, GeometryValueException.class);
+ }
+
+ @Test
+ public void testFromPointAndDirection() {
+ // act
+ Segment3D seg = Segment3D.fromPointAndDirection(Vector3D.of(1, 3, -2), Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 3, -2), seg.getStartPoint(), TEST_EPS);
+ Assert.assertNull(seg.getEndPoint());
+
+ Line3D line = seg.getLine();
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, -2), line.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Y, line.getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testFromInterval_intervalArg_finite() {
+ // arrange
+ DoublePrecisionContext intervalPrecision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(-1, 2, intervalPrecision);
+
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, interval);
+
+ // assert
+ double side = 1.0 / Math.sqrt(3);
+ checkFiniteSegment(segment, Vector3D.of(-side, -side, -side), Vector3D.of(2 * side, 2 * side, 2 * side));
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_intervalArg_full() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, Interval.full());
+
+ // assert
+ Assert.assertTrue(segment.isInfinite());
+ Assert.assertFalse(segment.isFinite());
+
+ GeometryTestUtils.assertNegativeInfinity(segment.getSubspaceStart());
+ GeometryTestUtils.assertPositiveInfinity(segment.getSubspaceEnd());
+
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertSame(Interval.full(), segment.getInterval());
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_intervalArg_positiveHalfSpace() {
+ // arrange
+ DoublePrecisionContext intervalPrecision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.min(-1, intervalPrecision);
+
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, interval);
+
+ // assert
+ Assert.assertTrue(segment.isInfinite());
+ Assert.assertFalse(segment.isFinite());
+
+ Assert.assertEquals(-1.0, segment.getSubspaceStart(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(segment.getSubspaceEnd());
+
+ double side = 1.0 / Math.sqrt(3);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-side, -side, -side), segment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertSame(interval, segment.getInterval());
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_intervalArg_negativeHalfSpace() {
+ // arrange
+ DoublePrecisionContext intervalPrecision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.max(2, intervalPrecision);
+
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, interval);
+
+ // assert
+ GeometryTestUtils.assertNegativeInfinity(segment.getSubspaceStart());
+ Assert.assertEquals(2, segment.getSubspaceEnd(), TEST_EPS);
+
+ double side = 1.0 / Math.sqrt(3);
+
+ Assert.assertNull(segment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2 * side, 2 * side, 2 * side), segment.getEndPoint(), TEST_EPS);
+
+ Assert.assertSame(interval, segment.getInterval());
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_doubleArgs_finite() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, -1, 2);
+
+ // assert
+ double side = 1.0 / Math.sqrt(3);
+ checkFiniteSegment(segment, Vector3D.of(-side, -side, -side), Vector3D.of(2 * side, 2 * side, 2 * side));
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_doubleArgs_full() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+ // assert
+ GeometryTestUtils.assertNegativeInfinity(segment.getSubspaceStart());
+ GeometryTestUtils.assertPositiveInfinity(segment.getSubspaceEnd());
+
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_doubleArgs_positiveHalfSpace() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, -1, Double.POSITIVE_INFINITY);
+
+ // assert
+ Assert.assertEquals(-1.0, segment.getSubspaceStart(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(segment.getSubspaceEnd());
+
+ double side = 1.0 / Math.sqrt(3);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-side, -side, -side), segment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_doubleArgs_negativeHalfSpace() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, 2, Double.NEGATIVE_INFINITY);
+
+ // assert
+ GeometryTestUtils.assertNegativeInfinity(segment.getSubspaceStart());
+ Assert.assertEquals(2, segment.getSubspaceEnd(), TEST_EPS);
+
+ double side = 1.0 / Math.sqrt(3);
+
+ Assert.assertNull(segment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2 * side, 2 * side, 2 * side), segment.getEndPoint(), TEST_EPS);
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_vectorArgs() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+ // act
+ Segment3D segment = Segment3D.fromInterval(line, Vector1D.of(-1), Vector1D.of(2));
+
+ // assert
+ double side = 1.0 / Math.sqrt(3);
+ checkFiniteSegment(segment, Vector3D.of(-side, -side, -side), Vector3D.of(2 * side, 2 * side, 2 * side));
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testGetSubspaceRegion() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+ Interval interval = Interval.full();
+
+ Segment3D segment = Segment3D.fromInterval(line, interval);
+
+ // act/assert
+ Assert.assertSame(interval, segment.getInterval());
+ Assert.assertSame(interval, segment.getSubspaceRegion());
+ }
+
+ @Test
+ public void testTransform_infinite() {
+ // arrange
+ Line3D line = Line3D.fromPointAndDirection(Vector3D.of(1, 0, 0), Vector3D.of(0, 1, -1), TEST_PRECISION);
+ Segment3D segment = Segment3D.fromInterval(line,
+ Interval.min(line.toSubspace(Vector3D.of(1, 0, 0)).getX(), TEST_PRECISION));
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.identity()
+ .scale(2, 1, 1)
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ Segment3D transformed = segment.transform(transform);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -2), transformed.getStartPoint(), TEST_EPS);
+ Assert.assertNull(transformed.getEndPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 1, 0).normalize(), transformed.getLine().getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_finite() {
+ // arrange
+ Segment3D segment = Segment3D.fromPoints(Vector3D.of(1, 0, 0), Vector3D.of(2, 1, 0), TEST_PRECISION);
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.identity()
+ .scale(2, 1, 1)
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ Segment3D transformed = segment.transform(transform);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -2), transformed.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, -4), transformed.getEndPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, -2).normalize(), transformed.getLine().getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testContains() {
+ // arrange
+ Vector3D p1 = Vector3D.of(1, 0, 0);
+ Vector3D p2 = Vector3D.of(3, 0, 2);
+ Segment3D segment = Segment3D.fromPoints(p1, p2, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(segment.contains(p1));
+ Assert.assertTrue(segment.contains(p2));
+ Assert.assertTrue(segment.contains(p1.lerp(p2, 0.5)));
+
+ Assert.assertFalse(segment.contains(p1.lerp(p2, -1)));
+ Assert.assertFalse(segment.contains(p1.lerp(p2, 2)));
+
+ Assert.assertFalse(segment.contains(Vector3D.ZERO));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Line3D line = Line3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ Segment3D full = Segment3D.fromInterval(line, Interval.full());
+ Segment3D startOnly = Segment3D.fromInterval(line, 0, Double.POSITIVE_INFINITY);
+ Segment3D endOnly = Segment3D.fromInterval(line, Double.NEGATIVE_INFINITY, 0);
+ Segment3D finite = Segment3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act/assert
+ String fullStr = full.toString();
+ Assert.assertTrue(fullStr.contains("lineOrigin=") && fullStr.contains("lineDirection="));
+
+ String startOnlyStr = startOnly.toString();
+ Assert.assertTrue(startOnlyStr.contains("start=") && startOnlyStr.contains("direction="));
+
+ String endOnlyStr = endOnly.toString();
+ Assert.assertTrue(endOnlyStr.contains("direction=") && endOnlyStr.contains("end="));
+
+ String finiteStr = finite.toString();
+ Assert.assertTrue(finiteStr.contains("start=") && finiteStr.contains("end="));
+ }
+
+ private static void checkFiniteSegment(Segment3D segment, Vector3D start, Vector3D end) {
+ checkFiniteSegment(segment, start, end, TEST_PRECISION);
+ }
+
+ private static void checkFiniteSegment(Segment3D segment, Vector3D start, Vector3D end, DoublePrecisionContext precision) {
+ Assert.assertFalse(segment.isInfinite());
+ Assert.assertTrue(segment.isFinite());
+
+ EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
+
+ Line3D line = segment.getLine();
+
+ Assert.assertEquals(line.toSubspace(segment.getStartPoint()).getX(), segment.getSubspaceStart(), TEST_EPS);
+ Assert.assertEquals(line.toSubspace(segment.getEndPoint()).getX(), segment.getSubspaceEnd(), TEST_EPS);
+
+ Assert.assertSame(precision, segment.getPrecision());
+ Assert.assertSame(precision, line.getPrecision());
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinatesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinatesTest.java
index 72a7c34..b98ac87 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinatesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinatesTest.java
@@ -237,6 +237,25 @@
}
@Test
+ public void testFinite() {
+ // act/assert
+ Assert.assertTrue(SphericalCoordinates.of(1, 1, 1).isFinite());
+
+ Assert.assertFalse(SphericalCoordinates.of(0, 0, Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(0, Double.NEGATIVE_INFINITY, 0).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(Double.NEGATIVE_INFINITY, 0, 0).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(0, 0, Double.POSITIVE_INFINITY).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(0, Double.POSITIVE_INFINITY, 0).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(Double.POSITIVE_INFINITY, 0, 0).isFinite());
+
+ Assert.assertFalse(SphericalCoordinates.of(0, 0, Double.NaN).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(0, Double.NEGATIVE_INFINITY, Double.NaN).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(Double.NaN, 0, Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(Double.POSITIVE_INFINITY, Double.NaN, 0).isFinite());
+ Assert.assertFalse(SphericalCoordinates.of(0, Double.NaN, Double.POSITIVE_INFINITY).isFinite());
+ }
+
+ @Test
public void testHashCode() {
// arrange
SphericalCoordinates a = SphericalCoordinates.of(1, 2, 3);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubLine3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubLine3DTest.java
new file mode 100644
index 0000000..19e939d
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubLine3DTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.threed;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SubLine3DTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 0), TEST_PRECISION);
+
+ @Test
+ public void testCtor_default() {
+ // act
+ SubLine3D sub = new SubLine3D(line);
+
+ // assert
+ Assert.assertSame(line, sub.getLine());
+ Assert.assertSame(TEST_PRECISION, sub.getPrecision());
+ Assert.assertTrue(sub.getSubspaceRegion().isEmpty());
+ }
+
+ @Test
+ public void testCtor_true() {
+ // act
+ SubLine3D sub = new SubLine3D(line, true);
+
+ // assert
+ Assert.assertSame(line, sub.getLine());
+ Assert.assertSame(TEST_PRECISION, sub.getPrecision());
+ Assert.assertTrue(sub.getSubspaceRegion().isFull());
+ }
+
+ @Test
+ public void testCtor_false() {
+ // act
+ SubLine3D sub = new SubLine3D(line, false);
+
+ // assert
+ Assert.assertSame(line, sub.getLine());
+ Assert.assertSame(TEST_PRECISION, sub.getPrecision());
+ Assert.assertTrue(sub.getSubspaceRegion().isEmpty());
+ }
+
+ @Test
+ public void testCtor_lineAndRegion() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+
+ // act
+ SubLine3D sub = new SubLine3D(line, tree);
+
+ // assert
+ Assert.assertSame(line, sub.getLine());
+ Assert.assertSame(TEST_PRECISION, sub.getPrecision());
+ Assert.assertSame(tree, sub.getSubspaceRegion());
+ }
+
+ @Test
+ public void testTransform_full() {
+ // arrange
+ SubLine3D sub = new SubLine3D(line, true);
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.identity()
+ .translate(Vector3D.of(1, 0, 0))
+ .scale(Vector3D.of(2, 1, 1))
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ SubLine3D result = sub.transform(transform);
+
+ // assert
+ Line3D resultLine = result.getLine();
+
+ Vector3D expectedOrigin = Line3D.fromPoints(Vector3D.of(0, 0, -2), Vector3D.of(0, 1, -4), TEST_PRECISION)
+ .getOrigin();
+
+ EuclideanTestUtils.assertCoordinatesEqual(expectedOrigin, resultLine.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, -2).normalize(), resultLine.getDirection(), TEST_EPS);
+
+ Assert.assertTrue(result.getSubspaceRegion().isFull());
+ }
+
+ @Test
+ public void testTransform_finite() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(
+ line.toSubspace(Vector3D.of(1, 1, 0)).getX(),
+ line.toSubspace(Vector3D.of(2, 2, 0)).getX(), TEST_PRECISION));
+
+ SubLine3D sub = new SubLine3D(line, tree);
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.identity()
+ .translate(Vector3D.of(1, 0, 0))
+ .scale(Vector3D.of(2, 1, 1))
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act
+ SubLine3D result = sub.transform(transform);
+
+ // assert
+ Line3D resultLine = result.getLine();
+
+ Vector3D expectedOrigin = Line3D.fromPoints(Vector3D.of(0, 0, -2), Vector3D.of(0, 1, -4), TEST_PRECISION)
+ .getOrigin();
+
+ EuclideanTestUtils.assertCoordinatesEqual(expectedOrigin, resultLine.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, -2).normalize(), resultLine.getDirection(), TEST_EPS);
+
+ Assert.assertFalse(result.getSubspaceRegion().isFull());
+
+ List<Interval> intervals = result.getSubspaceRegion().toIntervals();
+ Assert.assertEquals(1, intervals.size());
+
+ Interval resultInterval = intervals.get(0);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, -4),
+ resultLine.toSpace(resultInterval.getMin()), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 2, -6),
+ resultLine.toSpace(resultInterval.getMax()), TEST_EPS);
+ }
+
+ @Test
+ public void testToConvex_full() {
+ // arrange
+ SubLine3D sub = new SubLine3D(line, true);
+
+ // act
+ List<Segment3D> segments = sub.toConvex();
+
+ // assert
+ Assert.assertEquals(1, segments.size());
+ Assert.assertTrue(segments.get(0).getSubspaceRegion().isFull());
+ }
+
+ @Test
+ public void testToConvex_finite() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.empty();
+ tree.add(Interval.of(
+ line.toSubspace(Vector3D.of(1, 1, 0)).getX(),
+ line.toSubspace(Vector3D.of(2, 2, 0)).getX(), TEST_PRECISION));
+
+ SubLine3D sub = new SubLine3D(line, tree);
+
+ // act
+ List<Segment3D> segments = sub.toConvex();
+
+ // assert
+ Assert.assertEquals(1, segments.size());
+
+ Segment3D segment = segments.get(0);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 0), segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 0), segment.getEndPoint(), TEST_EPS);
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ SubLine3D sub = new SubLine3D(line);
+
+ // act
+ String str = sub.toString();
+
+ // assert
+ Assert.assertTrue(str.contains("SubLine3D[lineOrigin= "));
+ Assert.assertTrue(str.contains(", lineDirection= "));
+ Assert.assertTrue(str.contains(", region= "));
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubLineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubLineTest.java
deleted file mode 100644
index 38d2041..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubLineTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * 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.threed;
-
-import java.util.List;
-
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class SubLineTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testEndPoints() {
- Vector3D p1 = Vector3D.of(-1, -7, 2);
- Vector3D p2 = Vector3D.of(7, -1, 0);
- Segment segment = new Segment(p1, p2, new Line(p1, p2, TEST_PRECISION));
- SubLine sub = new SubLine(segment);
- List<Segment> segments = sub.getSegments();
- Assert.assertEquals(1, segments.size());
- Assert.assertEquals(0.0, Vector3D.of(-1, -7, 2).distance(segments.get(0).getStart()), TEST_EPS);
- Assert.assertEquals(0.0, Vector3D.of( 7, -1, 0).distance(segments.get(0).getEnd()), TEST_EPS);
- }
-
- @Test
- public void testNoEndPoints() {
- SubLine wholeLine = new Line(Vector3D.of(-1, 7, 2), Vector3D.of(7, 1, 0), TEST_PRECISION).wholeLine();
- List<Segment> segments = wholeLine.getSegments();
- Assert.assertEquals(1, segments.size());
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getX()) &&
- segments.get(0).getStart().getX() < 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getY()) &&
- segments.get(0).getStart().getY() > 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getZ()) &&
- segments.get(0).getStart().getZ() > 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getX()) &&
- segments.get(0).getEnd().getX() > 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getY()) &&
- segments.get(0).getEnd().getY() < 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getZ()) &&
- segments.get(0).getEnd().getZ() < 0);
- }
-
- @Test
- public void testNoSegments() {
- SubLine empty = new SubLine(new Line(Vector3D.of(-1, -7, 2), Vector3D.of(7, -1, 0), TEST_PRECISION),
- (IntervalsSet) new RegionFactory<Vector1D>().getComplement(new IntervalsSet(TEST_PRECISION)));
- List<Segment> segments = empty.getSegments();
- Assert.assertEquals(0, segments.size());
- }
-
- @Test
- public void testSeveralSegments() {
- SubLine twoSubs = new SubLine(new Line(Vector3D.of(-1, -7, 2), Vector3D.of(7, -1, 0), TEST_PRECISION),
- (IntervalsSet) new RegionFactory<Vector1D>().union(new IntervalsSet(1, 2, TEST_PRECISION),
- new IntervalsSet(3, 4, TEST_PRECISION)));
- List<Segment> segments = twoSubs.getSegments();
- Assert.assertEquals(2, segments.size());
- }
-
- @Test
- public void testHalfInfiniteNeg() {
- SubLine empty = new SubLine(new Line(Vector3D.of(-1, -7, 2), Vector3D.of(7, -1, -2), TEST_PRECISION),
- new IntervalsSet(Double.NEGATIVE_INFINITY, 0.0, TEST_PRECISION));
- List<Segment> segments = empty.getSegments();
- Assert.assertEquals(1, segments.size());
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getX()) &&
- segments.get(0).getStart().getX() < 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getY()) &&
- segments.get(0).getStart().getY() < 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getZ()) &&
- segments.get(0).getStart().getZ() > 0);
- Assert.assertEquals(0.0, Vector3D.of(3, -4, 0).distance(segments.get(0).getEnd()), TEST_EPS);
- }
-
- @Test
- public void testHalfInfinitePos() {
- SubLine empty = new SubLine(new Line(Vector3D.of(-1, -7, 2), Vector3D.of(7, -1, -2), TEST_PRECISION),
- new IntervalsSet(0.0, Double.POSITIVE_INFINITY, TEST_PRECISION));
- List<Segment> segments = empty.getSegments();
- Assert.assertEquals(1, segments.size());
- Assert.assertEquals(0.0, Vector3D.of(3, -4, 0).distance(segments.get(0).getStart()), 1.0e-10);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getX()) &&
- segments.get(0).getEnd().getX() > 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getY()) &&
- segments.get(0).getEnd().getY() > 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getZ()) &&
- segments.get(0).getEnd().getZ() < 0);
- }
-
- @Test
- public void testIntersectionInsideInside() {
- SubLine sub1 = new SubLine(Vector3D.of(1, 1, 1), Vector3D.of(3, 1, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector3D.of(2, 0, 0), Vector3D.of(2, 2, 2), TEST_PRECISION);
- Assert.assertEquals(0.0, Vector3D.of(2, 1, 1).distance(sub1.intersection(sub2, true)), TEST_EPS);
- Assert.assertEquals(0.0, Vector3D.of(2, 1, 1).distance(sub1.intersection(sub2, false)), TEST_EPS);
- }
-
- @Test
- public void testIntersectionInsideBoundary() {
- SubLine sub1 = new SubLine(Vector3D.of(1, 1, 1), Vector3D.of(3, 1, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector3D.of(2, 0, 0), Vector3D.of(2, 1, 1), TEST_PRECISION);
- Assert.assertEquals(0.0, Vector3D.of(2, 1, 1).distance(sub1.intersection(sub2, true)), TEST_EPS);
- Assert.assertNull(sub1.intersection(sub2, false));
- }
-
- @Test
- public void testIntersectionInsideOutside() {
- SubLine sub1 = new SubLine(Vector3D.of(1, 1, 1), Vector3D.of(3, 1, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector3D.of(2, 0, 0), Vector3D.of(2, 0.5, 0.5), TEST_PRECISION);
- Assert.assertNull(sub1.intersection(sub2, true));
- Assert.assertNull(sub1.intersection(sub2, false));
- }
-
- @Test
- public void testIntersectionBoundaryBoundary() {
- SubLine sub1 = new SubLine(Vector3D.of(1, 1, 1), Vector3D.of(2, 1, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector3D.of(2, 0, 0), Vector3D.of(2, 1, 1), TEST_PRECISION);
- Assert.assertEquals(0.0, Vector3D.of(2, 1, 1).distance(sub1.intersection(sub2, true)), TEST_EPS);
- Assert.assertNull(sub1.intersection(sub2, false));
- }
-
- @Test
- public void testIntersectionBoundaryOutside() {
- SubLine sub1 = new SubLine(Vector3D.of(1, 1, 1), Vector3D.of(2, 1, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector3D.of(2, 0, 0), Vector3D.of(2, 0.5, 0.5), TEST_PRECISION);
- Assert.assertNull(sub1.intersection(sub2, true));
- Assert.assertNull(sub1.intersection(sub2, false));
- }
-
- @Test
- public void testIntersectionOutsideOutside() {
- SubLine sub1 = new SubLine(Vector3D.of(1, 1, 1), Vector3D.of(1.5, 1, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector3D.of(2, 0, 0), Vector3D.of(2, 0.5, 0.5), TEST_PRECISION);
- Assert.assertNull(sub1.intersection(sub2, true));
- Assert.assertNull(sub1.intersection(sub2, false));
- }
-
- @Test
- public void testIntersectionNotIntersecting() {
- SubLine sub1 = new SubLine(Vector3D.of(1, 1, 1), Vector3D.of(1.5, 1, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector3D.of(2, 3, 0), Vector3D.of(2, 3, 0.5), TEST_PRECISION);
- Assert.assertNull(sub1.intersection(sub2, true));
- Assert.assertNull(sub1.intersection(sub2, false));
- }
-
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java
new file mode 100644
index 0000000..b0ff93e
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java
@@ -0,0 +1,496 @@
+/*
+ * 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.threed;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Line;
+import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SubPlaneTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final Plane XY_PLANE = Plane.fromPointAndPlaneVectors(Vector3D.ZERO,
+ Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ @Test
+ public void testCtor_plane() {
+ // act
+ SubPlane sp = new SubPlane(XY_PLANE);
+
+ // assert
+ Assert.assertFalse(sp.isFull());
+ Assert.assertTrue(sp.isEmpty());
+
+ Assert.assertEquals(0, sp.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testCtor_plane_booleanFalse() {
+ // act
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+
+ // assert
+ Assert.assertFalse(sp.isFull());
+ Assert.assertTrue(sp.isEmpty());
+
+ Assert.assertEquals(0, sp.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testCtor_plane_booleanTrue() {
+ // act
+ SubPlane sp = new SubPlane(XY_PLANE, true);
+
+ // assert
+ Assert.assertTrue(sp.isFull());
+ Assert.assertFalse(sp.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(sp.getSize());
+ }
+
+ @Test
+ public void testToConvex_full() {
+ // act
+ SubPlane sp = new SubPlane(XY_PLANE, true);
+
+ // act
+ List<ConvexSubPlane> convex = sp.toConvex();
+
+ // assert
+ Assert.assertEquals(1, convex.size());
+ Assert.assertTrue(convex.get(0).isFull());
+ }
+
+ @Test
+ public void testToConvex_empty() {
+ // act
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+
+ // act
+ List<ConvexSubPlane> convex = sp.toConvex();
+
+ // assert
+ Assert.assertEquals(0, convex.size());
+ }
+
+ @Test
+ public void testToConvex_nonConvexRegion() {
+ // act
+ ConvexArea a = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.of(0, 0), Vector2D.of(1, 0),
+ Vector2D.of(1, 1), Vector2D.of(0, 1)
+ ), TEST_PRECISION);
+ ConvexArea b = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.of(1, 0), Vector2D.of(2, 0),
+ Vector2D.of(2, 1), Vector2D.of(1, 1)
+ ), TEST_PRECISION);
+
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+ sp.add(ConvexSubPlane.fromConvexArea(XY_PLANE, a));
+ sp.add(ConvexSubPlane.fromConvexArea(XY_PLANE, b));
+
+ // act
+ List<ConvexSubPlane> convex = sp.toConvex();
+
+ // assert
+ Assert.assertEquals(2, convex.size());
+ Assert.assertEquals(1, convex.get(0).getSize(), TEST_EPS);
+ Assert.assertEquals(1, convex.get(1).getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testSplit_empty() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+
+ Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Split<SubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_halfSpace() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+ sp.getSubspaceRegion().getRoot().cut(
+ Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION));
+
+ Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Split<SubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ SubPlane minus = split.getMinus();
+ checkPoints(minus, RegionLocation.INSIDE, Vector3D.of(-1, 1, 0));
+ checkPoints(minus, RegionLocation.OUTSIDE, Vector3D.of(1, 1, 0), Vector3D.of(0, -1, 0));
+
+ SubPlane plus = split.getPlus();
+ checkPoints(plus, RegionLocation.OUTSIDE, Vector3D.of(-1, 1, 0), Vector3D.of(0, -1, 0));
+ checkPoints(plus, RegionLocation.INSIDE, Vector3D.of(1, 1, 0));
+ }
+
+ @Test
+ public void testSplit_both() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+ sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+
+ Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Split<SubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ SubPlane minus = split.getMinus();
+ checkPoints(minus, RegionLocation.INSIDE, Vector3D.of(-0.5, 0, 0));
+ checkPoints(minus, RegionLocation.OUTSIDE,
+ Vector3D.of(0.5, 0, 0), Vector3D.of(1.5, 0, 0),
+ Vector3D.of(0, 1.5, 0), Vector3D.of(0, -1.5, 0));
+
+ SubPlane plus = split.getPlus();
+ checkPoints(plus, RegionLocation.INSIDE, Vector3D.of(0.5, 0, 0));
+ checkPoints(plus, RegionLocation.OUTSIDE,
+ Vector3D.of(-0.5, 0, 0), Vector3D.of(1.5, 0, 0),
+ Vector3D.of(0, 1.5, 0), Vector3D.of(0, -1.5, 0));
+ }
+
+ @Test
+ public void testSplit_intersects_plusOnly() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+ sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+
+ Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0.1, 0, 1), TEST_PRECISION);
+
+ // act
+ Split<SubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(sp, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_intersects_minusOnly() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+ sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+
+ Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0.1, 0, -1), TEST_PRECISION);
+
+ // act
+ Split<SubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(sp, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_parallel_plusOnly() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+ sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+
+ Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ Split<SubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(sp, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_parallel_minusOnly() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+ sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+
+ Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.MINUS_Z, TEST_PRECISION);
+
+ // act
+ Split<SubPlane> split = sp.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(sp, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_coincident() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+ sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+
+ // act
+ Split<SubPlane> split = sp.split(sp.getPlane());
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testTransform_empty() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+
+ AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(Vector3D.Unit.PLUS_Z);
+
+ // act
+ SubPlane result = sp.transform(transform);
+
+ // assert
+ Assert.assertNotSame(sp, result);
+
+ Plane resultPlane = result.getPlane();
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), resultPlane.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z, resultPlane.getNormal(), TEST_EPS);
+
+ Assert.assertFalse(result.isFull());
+ Assert.assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void testTransform_full() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, true);
+
+ AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(Vector3D.Unit.PLUS_Z);
+
+ // act
+ SubPlane result = sp.transform(transform);
+
+ // assert
+ Assert.assertNotSame(sp, result);
+
+ Plane resultPlane = result.getPlane();
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), resultPlane.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z, resultPlane.getNormal(), TEST_EPS);
+
+ Assert.assertTrue(result.isFull());
+ Assert.assertFalse(result.isEmpty());
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ ConvexArea area = ConvexArea.fromVertexLoop(
+ Arrays.asList(Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_Y), TEST_PRECISION);
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ SubPlane sp = new SubPlane(plane, RegionBSPTree2D.from(area));
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.identity()
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI))
+ .translate(Vector3D.of(1, 0, 0));
+
+ // act
+ SubPlane result = sp.transform(transform);
+
+ // assert
+ Assert.assertNotSame(sp, result);
+
+ Plane resultPlane = result.getPlane();
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 0, 0), resultPlane.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_X, resultPlane.getNormal(), TEST_EPS);
+
+ checkPoints(result, RegionLocation.INSIDE, Vector3D.of(2, 0.25, -0.25));
+ checkPoints(result, RegionLocation.OUTSIDE, Vector3D.of(1, 0.25, -0.25), Vector3D.of(3, 0.25, -0.25));
+
+ checkPoints(result, RegionLocation.BOUNDARY,
+ Vector3D.of(2, 0, 0), Vector3D.of(2, 0, -1), Vector3D.of(2, 1, 0));
+ }
+
+ @Test
+ public void testTransform_reflection() {
+ // arrange
+ ConvexArea area = ConvexArea.fromVertexLoop(
+ Arrays.asList(Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_Y), TEST_PRECISION);
+ Plane plane = Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ SubPlane sp = new SubPlane(plane, RegionBSPTree2D.from(area));
+
+ Transform<Vector3D> transform = AffineTransformMatrix3D.createScale(-1, 1, 1);
+
+ // act
+ SubPlane result = sp.transform(transform);
+
+ // assert
+ Assert.assertNotSame(sp, result);
+
+ Plane resultPlane = result.getPlane();
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), resultPlane.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_Z, resultPlane.getNormal(), TEST_EPS);
+
+ checkPoints(result, RegionLocation.INSIDE, Vector3D.of(-0.25, 0.25, 1));
+ checkPoints(result, RegionLocation.OUTSIDE, Vector3D.of(0.25, 0.25, 0), Vector3D.of(0.25, 0.25, 2));
+
+ checkPoints(result, RegionLocation.BOUNDARY,
+ Vector3D.of(-1, 0, 1), Vector3D.of(0, 1, 1), Vector3D.of(0, 0, 1));
+ }
+
+ @Test
+ public void testBuilder() {
+ // arrange
+ Plane mainPlane = Plane.fromPointAndPlaneVectors(
+ Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ SubPlane.SubPlaneBuilder builder = new SubPlane.SubPlaneBuilder(mainPlane);
+
+ ConvexArea a = ConvexArea.fromVertexLoop(
+ Arrays.asList(Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_Y), TEST_PRECISION);
+ ConvexArea b = ConvexArea.fromVertexLoop(
+ Arrays.asList(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y), TEST_PRECISION);
+
+ Plane closePlane = Plane.fromPointAndPlaneVectors(
+ Vector3D.of(1e-16, 0, 1), Vector3D.of(1, 1e-16, 0), Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // act
+ builder.add(ConvexSubPlane.fromConvexArea(closePlane, a));
+ builder.add(new SubPlane(closePlane, RegionBSPTree2D.from(b)));
+
+ SubPlane result = builder.build();
+
+ // assert
+ Assert.assertFalse(result.isFull());
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertTrue(result.isFinite());
+ Assert.assertFalse(result.isInfinite());
+
+ checkPoints(result, RegionLocation.INSIDE, Vector3D.of(0.5, 0.5, 1));
+ checkPoints(result, RegionLocation.OUTSIDE,
+ Vector3D.of(-1, 0.5, 1), Vector3D.of(2, 0.5, 1),
+ Vector3D.of(0.5, -1, 1), Vector3D.of(0.5, 2, 1));
+ checkPoints(result, RegionLocation.BOUNDARY,
+ Vector3D.of(0, 0, 1), Vector3D.of(1, 0, 1),
+ Vector3D.of(1, 1, 1), Vector3D.of(0, 1, 1));
+ }
+
+ @Test
+ public void testSubPlaneAddMethods_validatesPlane() {
+ // arrange
+ SubPlane sp = new SubPlane(XY_PLANE, false);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ sp.add(ConvexSubPlane.fromConvexArea(
+ Plane.fromPointAndPlaneVectors(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X, TEST_PRECISION),
+ ConvexArea.full()));
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ sp.add(new SubPlane(
+ Plane.fromPointAndPlaneVectors(Vector3D.of(0, 0, -1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION),
+ false));
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testBuilder_addUnknownType() {
+ // arrange
+ SubPlane.SubPlaneBuilder sp = new SubPlane.SubPlaneBuilder(XY_PLANE);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ sp.add(new StubSubPlane(XY_PLANE));
+ }, IllegalArgumentException.class);
+ }
+
+ private static void checkPoints(SubPlane sp, RegionLocation loc, Vector3D ... pts) {
+ for (Vector3D pt : pts) {
+ Assert.assertEquals("Unexpected subplane location for point " + pt, loc, sp.classify(pt));
+ }
+ }
+
+ private static class StubSubPlane extends AbstractSubPlane<RegionBSPTree2D> implements SubHyperplane<Vector3D> {
+
+ private static final long serialVersionUID = 1L;
+
+ StubSubPlane(Plane plane) {
+ super(plane);
+ }
+
+ @Override
+ public Split<? extends SubHyperplane<Vector3D>> split(Hyperplane<Vector3D> splitter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public SubHyperplane<Vector3D> transform(Transform<Vector3D> transform) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<? extends ConvexSubHyperplane<Vector3D>> toConvex() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public HyperplaneBoundedRegion<Vector2D> getSubspaceRegion() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
index 9f55682..c67ce65 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
@@ -17,6 +17,7 @@
package org.apache.commons.geometry.euclidean.threed;
+import java.util.Comparator;
import java.util.regex.Pattern;
import org.apache.commons.geometry.core.Geometry;
@@ -77,6 +78,27 @@
}
@Test
+ public void testCoordinateAscendingOrder() {
+ // arrange
+ Comparator<Vector3D> cmp = Vector3D.COORDINATE_ASCENDING_ORDER;
+
+ // act/assert
+ Assert.assertEquals(0, cmp.compare(Vector3D.of(1, 2, 3), Vector3D.of(1, 2, 3)));
+
+ Assert.assertEquals(-1, cmp.compare(Vector3D.of(0, 2, 3), Vector3D.of(1, 2, 3)));
+ Assert.assertEquals(-1, cmp.compare(Vector3D.of(1, 1, 3), Vector3D.of(1, 2, 3)));
+ Assert.assertEquals(-1, cmp.compare(Vector3D.of(1, 2, 2), Vector3D.of(1, 2, 3)));
+
+ Assert.assertEquals(1, cmp.compare(Vector3D.of(2, 2, 3), Vector3D.of(1, 2, 3)));
+ Assert.assertEquals(1, cmp.compare(Vector3D.of(1, 3, 3), Vector3D.of(1, 2, 3)));
+ Assert.assertEquals(1, cmp.compare(Vector3D.of(1, 2, 4), Vector3D.of(1, 2, 3)));
+
+ Assert.assertEquals(-1, cmp.compare(Vector3D.of(1, 2, 3), null));
+ Assert.assertEquals(1, cmp.compare(null, Vector3D.of(1, 2, 3)));
+ Assert.assertEquals(0, cmp.compare(null, null));
+ }
+
+ @Test
public void testCoordinates() {
// arrange
Vector3D c = Vector3D.of(1, 2, 3);
@@ -143,6 +165,26 @@
}
@Test
+ public void testFinite() {
+ // act/assert
+ Assert.assertTrue(Vector3D.ZERO.isFinite());
+ Assert.assertTrue(Vector3D.of(1, 1, 1).isFinite());
+
+ Assert.assertFalse(Vector3D.of(0, 0, Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(Vector3D.of(0, Double.NEGATIVE_INFINITY, 0).isFinite());
+ Assert.assertFalse(Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0).isFinite());
+ Assert.assertFalse(Vector3D.of(0, 0, Double.POSITIVE_INFINITY).isFinite());
+ Assert.assertFalse(Vector3D.of(0, Double.POSITIVE_INFINITY, 0).isFinite());
+ Assert.assertFalse(Vector3D.of(Double.POSITIVE_INFINITY, 0, 0).isFinite());
+
+ Assert.assertFalse(Vector3D.of(0, 0, Double.NaN).isFinite());
+ Assert.assertFalse(Vector3D.of(0, Double.NEGATIVE_INFINITY, Double.NaN).isFinite());
+ Assert.assertFalse(Vector3D.of(Double.NaN, 0, Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(Vector3D.of(Double.POSITIVE_INFINITY, Double.NaN, 0).isFinite());
+ Assert.assertFalse(Vector3D.of(0, Double.NaN, Double.POSITIVE_INFINITY).isFinite());
+ }
+
+ @Test
public void testZero() {
// act
Vector3D zero = Vector3D.of(1, 2, 3).getZero();
@@ -973,26 +1015,26 @@
Vector3D vec = Vector3D.of(1, -2, 3);
// act/assert
- Assert.assertTrue(vec.equals(vec, smallEps));
- Assert.assertTrue(vec.equals(vec, largeEps));
+ Assert.assertTrue(vec.eq(vec, smallEps));
+ Assert.assertTrue(vec.eq(vec, largeEps));
- Assert.assertTrue(vec.equals(Vector3D.of(1.0000007, -2.0000009, 3.0000009), smallEps));
- Assert.assertTrue(vec.equals(Vector3D.of(1.0000007, -2.0000009, 3.0000009), largeEps));
+ Assert.assertTrue(vec.eq(Vector3D.of(1.0000007, -2.0000009, 3.0000009), smallEps));
+ Assert.assertTrue(vec.eq(Vector3D.of(1.0000007, -2.0000009, 3.0000009), largeEps));
- Assert.assertFalse(vec.equals(Vector3D.of(1.004, -2, 3), smallEps));
- Assert.assertFalse(vec.equals(Vector3D.of(1, -2.004, 3), smallEps));
- Assert.assertFalse(vec.equals(Vector3D.of(1, -2, 2.999), smallEps));
- Assert.assertTrue(vec.equals(Vector3D.of(1.004, -2.004, 2.999), largeEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(1.004, -2, 3), smallEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(1, -2.004, 3), smallEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(1, -2, 2.999), smallEps));
+ Assert.assertTrue(vec.eq(Vector3D.of(1.004, -2.004, 2.999), largeEps));
- Assert.assertFalse(vec.equals(Vector3D.of(2, -2, 3), smallEps));
- Assert.assertFalse(vec.equals(Vector3D.of(1, -3, 3), smallEps));
- Assert.assertFalse(vec.equals(Vector3D.of(1, -2, 4), smallEps));
- Assert.assertFalse(vec.equals(Vector3D.of(2, -3, 4), smallEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(2, -2, 3), smallEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(1, -3, 3), smallEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(1, -2, 4), smallEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(2, -3, 4), smallEps));
- Assert.assertFalse(vec.equals(Vector3D.of(2, -2, 3), largeEps));
- Assert.assertFalse(vec.equals(Vector3D.of(1, -3, 3), largeEps));
- Assert.assertFalse(vec.equals(Vector3D.of(1, -2, 4), largeEps));
- Assert.assertFalse(vec.equals(Vector3D.of(2, -3, 4), largeEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(2, -2, 3), largeEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(1, -3, 3), largeEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(1, -2, 4), largeEps));
+ Assert.assertFalse(vec.eq(Vector3D.of(2, -3, 4), largeEps));
}
@Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotationTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotationTest.java
index f4f40d0..5d25bde 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotationTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotationTest.java
@@ -23,6 +23,7 @@
import org.apache.commons.geometry.core.Geometry;
import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.exception.GeometryValueException;
import org.apache.commons.geometry.core.exception.IllegalNormException;
import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
@@ -266,15 +267,31 @@
@Test
public void testFromAxisAngle_invalidAngle() {
// act/assert
- GeometryTestUtils.assertThrows(() -> QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_X, Double.NaN), IllegalArgumentException.class,
+ GeometryTestUtils.assertThrows(() -> QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_X, Double.NaN), GeometryValueException.class,
"Invalid angle: NaN");
- GeometryTestUtils.assertThrows(() -> QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_X, Double.POSITIVE_INFINITY), IllegalArgumentException.class,
+ GeometryTestUtils.assertThrows(() -> QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_X, Double.POSITIVE_INFINITY), GeometryValueException.class,
"Invalid angle: Infinity");
- GeometryTestUtils.assertThrows(() -> QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_X, Double.NEGATIVE_INFINITY), IllegalArgumentException.class,
+ GeometryTestUtils.assertThrows(() -> QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_X, Double.NEGATIVE_INFINITY), GeometryValueException.class,
"Invalid angle: -Infinity");
}
@Test
+ public void testApplyVector() {
+ // arrange
+ QuaternionRotation q = QuaternionRotation.fromAxisAngle(Vector3D.of(1, 1, 1), Geometry.HALF_PI);
+
+ EuclideanTestUtils.permute(-2, 2, 0.2, (x, y, z) -> {
+ Vector3D input = Vector3D.of(x, y, z);
+
+ // act
+ Vector3D pt = q.apply(input);
+ Vector3D vec = q.applyVector(input);
+
+ EuclideanTestUtils.assertCoordinatesEqual(pt, vec, EPS);
+ });
+ }
+
+ @Test
public void testInverse() {
// arrange
QuaternionRotation rot = QuaternionRotation.of(0.5, 0.5, 0.5, 0.5);
@@ -692,50 +709,50 @@
}
@Test
- public void testToTransformMatrix() {
+ public void testToMatrix() {
// act/assert
// --- x axes
- assertTransformEquals(StandardRotations.IDENTITY, QuaternionRotation.fromAxisAngle(PLUS_X_DIR, Geometry.ZERO_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.IDENTITY, QuaternionRotation.fromAxisAngle(PLUS_X_DIR, Geometry.ZERO_PI).toMatrix());
- assertTransformEquals(StandardRotations.PLUS_X_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_X_DIR, Geometry.HALF_PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.PLUS_X_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_X_DIR, Geometry.MINUS_HALF_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.PLUS_X_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_X_DIR, Geometry.HALF_PI).toMatrix());
+ assertTransformEquals(StandardRotations.PLUS_X_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_X_DIR, Geometry.MINUS_HALF_PI).toMatrix());
- assertTransformEquals(StandardRotations.MINUS_X_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_X_DIR, Geometry.HALF_PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.MINUS_X_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_X_DIR, Geometry.MINUS_HALF_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.MINUS_X_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_X_DIR, Geometry.HALF_PI).toMatrix());
+ assertTransformEquals(StandardRotations.MINUS_X_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_X_DIR, Geometry.MINUS_HALF_PI).toMatrix());
- assertTransformEquals(StandardRotations.X_PI, QuaternionRotation.fromAxisAngle(PLUS_X_DIR, Geometry.PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.X_PI, QuaternionRotation.fromAxisAngle(MINUS_X_DIR, Geometry.PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.X_PI, QuaternionRotation.fromAxisAngle(PLUS_X_DIR, Geometry.PI).toMatrix());
+ assertTransformEquals(StandardRotations.X_PI, QuaternionRotation.fromAxisAngle(MINUS_X_DIR, Geometry.PI).toMatrix());
// --- y axes
- assertTransformEquals(StandardRotations.IDENTITY, QuaternionRotation.fromAxisAngle(PLUS_Y_DIR, Geometry.ZERO_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.IDENTITY, QuaternionRotation.fromAxisAngle(PLUS_Y_DIR, Geometry.ZERO_PI).toMatrix());
- assertTransformEquals(StandardRotations.PLUS_Y_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_Y_DIR, Geometry.HALF_PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.PLUS_Y_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_Y_DIR, Geometry.MINUS_HALF_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.PLUS_Y_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_Y_DIR, Geometry.HALF_PI).toMatrix());
+ assertTransformEquals(StandardRotations.PLUS_Y_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_Y_DIR, Geometry.MINUS_HALF_PI).toMatrix());
- assertTransformEquals(StandardRotations.MINUS_Y_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_Y_DIR, Geometry.HALF_PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.MINUS_Y_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_Y_DIR, Geometry.MINUS_HALF_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.MINUS_Y_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_Y_DIR, Geometry.HALF_PI).toMatrix());
+ assertTransformEquals(StandardRotations.MINUS_Y_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_Y_DIR, Geometry.MINUS_HALF_PI).toMatrix());
- assertTransformEquals(StandardRotations.Y_PI, QuaternionRotation.fromAxisAngle(PLUS_Y_DIR, Geometry.PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.Y_PI, QuaternionRotation.fromAxisAngle(MINUS_Y_DIR, Geometry.PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.Y_PI, QuaternionRotation.fromAxisAngle(PLUS_Y_DIR, Geometry.PI).toMatrix());
+ assertTransformEquals(StandardRotations.Y_PI, QuaternionRotation.fromAxisAngle(MINUS_Y_DIR, Geometry.PI).toMatrix());
// --- z axes
- assertTransformEquals(StandardRotations.IDENTITY, QuaternionRotation.fromAxisAngle(PLUS_Z_DIR, Geometry.ZERO_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.IDENTITY, QuaternionRotation.fromAxisAngle(PLUS_Z_DIR, Geometry.ZERO_PI).toMatrix());
- assertTransformEquals(StandardRotations.PLUS_Z_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_Z_DIR, Geometry.HALF_PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.PLUS_Z_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_Z_DIR, Geometry.MINUS_HALF_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.PLUS_Z_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_Z_DIR, Geometry.HALF_PI).toMatrix());
+ assertTransformEquals(StandardRotations.PLUS_Z_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_Z_DIR, Geometry.MINUS_HALF_PI).toMatrix());
- assertTransformEquals(StandardRotations.MINUS_Z_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_Z_DIR, Geometry.HALF_PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.MINUS_Z_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_Z_DIR, Geometry.MINUS_HALF_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.MINUS_Z_HALF_PI, QuaternionRotation.fromAxisAngle(MINUS_Z_DIR, Geometry.HALF_PI).toMatrix());
+ assertTransformEquals(StandardRotations.MINUS_Z_HALF_PI, QuaternionRotation.fromAxisAngle(PLUS_Z_DIR, Geometry.MINUS_HALF_PI).toMatrix());
- assertTransformEquals(StandardRotations.Z_PI, QuaternionRotation.fromAxisAngle(PLUS_Z_DIR, Geometry.PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.Z_PI, QuaternionRotation.fromAxisAngle(MINUS_Z_DIR, Geometry.PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.Z_PI, QuaternionRotation.fromAxisAngle(PLUS_Z_DIR, Geometry.PI).toMatrix());
+ assertTransformEquals(StandardRotations.Z_PI, QuaternionRotation.fromAxisAngle(MINUS_Z_DIR, Geometry.PI).toMatrix());
// --- diagonal
- assertTransformEquals(StandardRotations.PLUS_DIAGONAL_TWO_THIRDS_PI, QuaternionRotation.fromAxisAngle(PLUS_DIAGONAL, TWO_THIRDS_PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.PLUS_DIAGONAL_TWO_THIRDS_PI, QuaternionRotation.fromAxisAngle(MINUS_DIAGONAL, MINUS_TWO_THIRDS_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.PLUS_DIAGONAL_TWO_THIRDS_PI, QuaternionRotation.fromAxisAngle(PLUS_DIAGONAL, TWO_THIRDS_PI).toMatrix());
+ assertTransformEquals(StandardRotations.PLUS_DIAGONAL_TWO_THIRDS_PI, QuaternionRotation.fromAxisAngle(MINUS_DIAGONAL, MINUS_TWO_THIRDS_PI).toMatrix());
- assertTransformEquals(StandardRotations.MINUS_DIAGONAL_TWO_THIRDS_PI, QuaternionRotation.fromAxisAngle(MINUS_DIAGONAL, TWO_THIRDS_PI).toTransformMatrix());
- assertTransformEquals(StandardRotations.MINUS_DIAGONAL_TWO_THIRDS_PI, QuaternionRotation.fromAxisAngle(PLUS_DIAGONAL, MINUS_TWO_THIRDS_PI).toTransformMatrix());
+ assertTransformEquals(StandardRotations.MINUS_DIAGONAL_TWO_THIRDS_PI, QuaternionRotation.fromAxisAngle(MINUS_DIAGONAL, TWO_THIRDS_PI).toMatrix());
+ assertTransformEquals(StandardRotations.MINUS_DIAGONAL_TWO_THIRDS_PI, QuaternionRotation.fromAxisAngle(PLUS_DIAGONAL, MINUS_TWO_THIRDS_PI).toMatrix());
}
@Test
@@ -1571,6 +1588,8 @@
Assert.assertEquals(msg, x, q.getX(), EPS);
Assert.assertEquals(msg, y, q.getY(), EPS);
Assert.assertEquals(msg, z, q.getZ(), EPS);
+
+ Assert.assertTrue(qrot.preservesOrientation());
}
private static void checkVector(Vector3D v, double x, double y, double z) {
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AbstractSegmentConnectorTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AbstractSegmentConnectorTest.java
new file mode 100644
index 0000000..500d5f1
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AbstractSegmentConnectorTest.java
@@ -0,0 +1,525 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AbstractSegmentConnectorTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final Line Y_AXIS = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI,
+ TEST_PRECISION);
+
+ private TestConnector connector = new TestConnector();
+
+ @Test
+ public void testConnectAll_emptyCollection() {
+ // act
+ List<Polyline> paths = connector.connectAll(Collections.emptyList());
+
+ // assert
+ Assert.assertEquals(0, paths.size());
+ }
+
+ @Test
+ public void testConnectAll_singleInfiniteLine() {
+ // arrange
+ Segment segment = Y_AXIS.span();
+
+ // act
+ List<Polyline> paths = connector.connectAll(Arrays.asList(segment));
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(1, path.getSegments().size());
+ Assert.assertSame(segment, path.getStartSegment());
+ }
+
+ @Test
+ public void testConnectAll_singleHalfInfiniteLine_noEndPoint() {
+ // arrange
+ Segment segment = Y_AXIS.segmentFrom(Vector2D.ZERO);
+
+ // act
+ List<Polyline> paths = connector.connectAll(Arrays.asList(segment));
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(1, path.getSegments().size());
+ Assert.assertSame(segment, path.getStartSegment());
+ }
+
+ @Test
+ public void testConnectAll_singleHalfInfiniteLine_noStartPoint() {
+ // arrange
+ Segment segment = Y_AXIS.segmentTo(Vector2D.ZERO);
+
+ // act
+ List<Polyline> paths = connector.connectAll(Arrays.asList(segment));
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(1, path.getSegments().size());
+ Assert.assertSame(segment, path.getStartSegment());
+ }
+
+ @Test
+ public void testConnectAll_disjointSegments() {
+ // arrange
+ Segment a = Y_AXIS.segment(Vector2D.of(0, 1), Vector2D.of(0, 2));
+ Segment b = Y_AXIS.segment(Vector2D.of(0, -1), Vector2D.ZERO);
+
+ List<Segment> segments = Arrays.asList(a, b);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0), Vector2D.of(0, -1), Vector2D.ZERO);
+ assertFinitePath(paths.get(1), Vector2D.of(0, 1), Vector2D.of(0, 2));
+ }
+
+ @Test
+ public void testConnectAll_singleClosedPath() {
+ // arrange
+ Polyline input = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(1, 1), Vector2D.ZERO, Vector2D.of(1, 0))
+ .close();
+
+ List<Segment> segments = new ArrayList<>(input.getSegments());
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(1, 1), Vector2D.ZERO);
+ }
+
+ @Test
+ public void testConnectAll_multipleClosedPaths() {
+ // arrange
+ Polyline a = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(1, 1), Vector2D.ZERO, Vector2D.of(1, 0))
+ .close();
+
+ Polyline b = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(0, 1), Vector2D.of(-1, 0), Vector2D.of(-0.5, 0))
+ .close();
+
+ Polyline c = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(1, 3), Vector2D.of(0, 2), Vector2D.of(1, 2))
+ .close();
+
+ List<Segment> segments = new ArrayList<>();
+ segments.addAll(a.getSegments());
+ segments.addAll(b.getSegments());
+ segments.addAll(c.getSegments());
+
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(3, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.of(-1, 0), Vector2D.of(-0.5, 0), Vector2D.of(0, 1), Vector2D.of(-1, 0));
+
+ assertFinitePath(paths.get(1),
+ Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(1, 1), Vector2D.ZERO);
+
+ assertFinitePath(paths.get(2),
+ Vector2D.of(0, 2), Vector2D.of(1, 2), Vector2D.of(1, 3), Vector2D.of(0, 2));
+ }
+
+ @Test
+ public void testConnectAll_singleOpenPath() {
+ // arrange
+ Polyline input = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(1, 1), Vector2D.ZERO, Vector2D.of(1, 0))
+ .build();
+
+ List<Segment> segments = new ArrayList<>(input.getSegments());
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.of(1, 1), Vector2D.ZERO, Vector2D.of(1, 0));
+ }
+
+ @Test
+ public void testConnectAll_mixOfOpenConnectedAndInfinite() {
+ // arrange
+ Segment inputYInf = Y_AXIS.segmentTo(Vector2D.ZERO);
+ Segment inputXInf = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.MINUS_X, TEST_PRECISION)
+ .segmentFrom(Vector2D.ZERO);
+
+ Polyline closedPath = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(0, 2), Vector2D.of(1, 2), Vector2D.of(1, 3))
+ .close();
+
+ Polyline openPath = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(-1, 3), Vector2D.of(0, 1), Vector2D.of(1, 1))
+ .build();
+
+ List<Segment> segments = new ArrayList<>();
+ segments.add(inputYInf);
+ segments.add(inputXInf);
+ segments.addAll(closedPath.getSegments());
+ segments.addAll(openPath.getSegments());
+
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(3, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.of(-1, 3), Vector2D.of(0, 1), Vector2D.of(1, 1));
+
+ Polyline infPath = paths.get(1);
+ Assert.assertTrue(infPath.isInfinite());
+ Assert.assertEquals(2, infPath.getSegments().size());
+ Assert.assertSame(inputYInf, infPath.getSegments().get(0));
+ Assert.assertSame(inputXInf, infPath.getSegments().get(1));
+
+ assertFinitePath(paths.get(2),
+ Vector2D.of(0, 2), Vector2D.of(1, 2), Vector2D.of(1, 3), Vector2D.of(0, 2));
+ }
+
+ @Test
+ public void testConnectAll_pathWithSinglePoint() {
+ // arrange
+ Vector2D p0 = Vector2D.ZERO;
+
+ List<Segment> segments = Arrays.asList(Line.fromPointAndAngle(p0, 0, TEST_PRECISION).segment(p0, p0));
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertFinitePath(paths.get(0), p0, p0);
+ }
+
+ @Test
+ public void testConnectAll_pathWithPointLikeConnectedSegments() {
+ // arrange
+ Vector2D p0 = Vector2D.ZERO;
+ Vector2D p1 = Vector2D.of(1, 0);
+ Vector2D p2 = Vector2D.of(1, 1);
+
+ Vector2D almostP0 = Vector2D.of(-1e-20, -1e-20);
+ Vector2D almostP1 = Vector2D.of(1 - 1e-15, 0);
+
+ Polyline input = Polyline.builder(TEST_PRECISION)
+ .appendVertices(p0, p1)
+ .append(Line.fromPointAndAngle(p1, 0.25 * Geometry.PI, TEST_PRECISION).segment(p1, p1))
+ .append(Line.fromPointAndAngle(p1, -0.25 * Geometry.PI, TEST_PRECISION).segment(almostP1, almostP1))
+ .append(p2)
+ .append(p0)
+ .append(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.MINUS_HALF_PI, TEST_PRECISION)
+ .segment(almostP0, almostP0))
+ .build();
+
+ List<Segment> segments = new ArrayList<>(input.getSegments());
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertFinitePath(paths.get(0), p0, p1, almostP1, p1, p2, p0, almostP0);
+ }
+
+ @Test
+ public void testConnectAll_flatLineRegion() {
+ // arrange
+ Vector2D p0 = Vector2D.ZERO;
+ Vector2D p1 = Vector2D.of(1, 0);
+
+ Segment seg0 = Segment.fromPoints(p0, p1, TEST_PRECISION);
+ Segment seg1 = Segment.fromPoints(p1, p0, TEST_PRECISION);
+ Segment seg2 = Line.fromPointAndAngle(p1, Geometry.HALF_PI, TEST_PRECISION).segment(p1, p1);
+ Segment seg3 = Line.fromPointAndAngle(p0, Geometry.MINUS_HALF_PI, TEST_PRECISION).segment(p0, p0);
+
+ List<Segment> segments = new ArrayList<>(Arrays.asList(seg0, seg1, seg2, seg3));
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertSame(seg0, path.getSegments().get(0));
+ Assert.assertSame(seg2, path.getSegments().get(1));
+ Assert.assertSame(seg1, path.getSegments().get(2));
+ Assert.assertSame(seg3, path.getSegments().get(3));
+ }
+
+ @Test
+ public void testConnectAll_singlePointRegion() {
+ // arrange
+ Vector2D p0 = Vector2D.of(1, 0);
+
+ Segment seg0 = Line.fromPointAndAngle(p0, Geometry.ZERO_PI, TEST_PRECISION).segment(p0, p0);
+ Segment seg1 = Line.fromPointAndAngle(p0, Geometry.HALF_PI, TEST_PRECISION).segment(p0, p0);
+ Segment seg2 = Line.fromPointAndAngle(p0, Geometry.PI, TEST_PRECISION).segment(p0, p0);
+ Segment seg3 = Line.fromPointAndAngle(p0, Geometry.MINUS_HALF_PI, TEST_PRECISION).segment(p0, p0);
+
+ List<Segment> segments = new ArrayList<>(Arrays.asList(seg0, seg1, seg2, seg3));
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertSame(seg2, path.getSegments().get(0));
+ Assert.assertSame(seg3, path.getSegments().get(1));
+ Assert.assertSame(seg0, path.getSegments().get(2));
+ Assert.assertSame(seg1, path.getSegments().get(3));
+ }
+
+ @Test
+ public void testConnectAll_pathWithPointLikeUnconnectedSegments() {
+ // arrange
+ Vector2D p0 = Vector2D.ZERO;
+ Vector2D p1 = Vector2D.of(1, 0);
+
+ Segment seg0 = Line.fromPointAndAngle(p1, Geometry.ZERO_PI, TEST_PRECISION).segment(p1, p1);
+ Segment seg1 = Line.fromPointAndAngle(p1, 0.25 * Geometry.PI, TEST_PRECISION).segment(p1, p1);
+ Segment seg2 = Line.fromPointAndAngle(p0, 0, TEST_PRECISION).segment(p0, p0);
+
+ List<Segment> segments = new ArrayList<>(Arrays.asList(seg0, seg1, seg2));
+
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ Polyline path0 = paths.get(0);
+ Assert.assertEquals(1, path0.getSegments().size());
+ Assert.assertSame(seg2, path0.getSegments().get(0));
+
+ Polyline path1 = paths.get(1);
+ Assert.assertEquals(2, path1.getSegments().size());
+ Assert.assertSame(seg0, path1.getSegments().get(0));
+ Assert.assertSame(seg1, path1.getSegments().get(1));
+ }
+
+ @Test
+ public void testConnectAll_pathStartingWithPoint() {
+ // arrange
+ Vector2D p0 = Vector2D.ZERO;
+ Vector2D p1 = Vector2D.of(1, 0);
+ Vector2D p2 = Vector2D.of(1, 1);
+
+ Segment seg0 = Line.fromPointAndAngle(p0, Geometry.PI, TEST_PRECISION).segment(p0, p0);
+ Segment seg1 = Segment.fromPoints(p0, p1, TEST_PRECISION);
+ Segment seg2 = Segment.fromPoints(p1, p2, TEST_PRECISION);
+
+ List<Segment> segments = new ArrayList<>(Arrays.asList(seg0, seg1, seg2));
+
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertSame(seg0, path.getSegments().get(0));
+ Assert.assertSame(seg1, path.getSegments().get(1));
+ Assert.assertSame(seg2, path.getSegments().get(2));
+ }
+
+ @Test
+ public void testConnectAll_intersectingPaths() {
+ // arrange
+ Polyline a = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(-1, 1), Vector2D.of(0.5, 0), Vector2D.of(-1, -1))
+ .build();
+
+ Polyline b = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.of(1, 1), Vector2D.of(-0.5, 0), Vector2D.of(1, -1))
+ .build();
+
+ List<Segment> segments = new ArrayList<>();
+ segments.addAll(a.getSegments());
+ segments.addAll(b.getSegments());
+
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.of(-1, 1), Vector2D.of(0.5, 0), Vector2D.of(-1, -1));
+
+ assertFinitePath(paths.get(1),
+ Vector2D.of(1, 1), Vector2D.of(-0.5, 0), Vector2D.of(1, -1));
+ }
+
+ @Test
+ public void testInstancesCanBeReused() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+ Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // act
+ List<Polyline> firstPaths = connector.connectAll(Arrays.asList(a));
+ List<Polyline> secondPaths = connector.connectAll(Arrays.asList(b));
+
+ // assert
+ Assert.assertEquals(1, firstPaths.size());
+ Assert.assertEquals(1, secondPaths.size());
+
+ Assert.assertSame(a, firstPaths.get(0).getSegments().get(0));
+ Assert.assertSame(b, secondPaths.get(0).getSegments().get(0));
+ }
+
+ @Test
+ public void testAdd() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+ Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), TEST_PRECISION);
+ Segment c = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(2, 0), TEST_PRECISION);
+
+ // act
+ connector.add(Arrays.asList(a, b));
+ connector.add(Arrays.asList(c));
+
+ List<Polyline> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0), Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(2, 0));
+ assertFinitePath(paths.get(1), Vector2D.Unit.PLUS_X, Vector2D.of(1, 1));
+ }
+
+ @Test
+ public void testConnect() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+ Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), TEST_PRECISION);
+ Segment c = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(2, 0), TEST_PRECISION);
+
+ // act
+ connector.connect(Arrays.asList(a, b));
+ connector.connect(Arrays.asList(c));
+
+ List<Polyline> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0), Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1));
+ assertFinitePath(paths.get(1), Vector2D.Unit.PLUS_X, Vector2D.of(2, 0));
+ }
+
+ private static List<Segment> shuffle(final List<Segment> segments) {
+ return shuffle(segments, 1);
+ }
+
+ private static List<Segment> shuffle(final List<Segment> segments, final int seed) {
+ Collections.shuffle(segments, new Random(seed));
+
+ return segments;
+ }
+
+ private static void assertFinitePath(Polyline path, Vector2D ... vertices)
+ {
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+
+ assertPathVertices(path, vertices);
+ }
+
+ private static void assertPathVertices(Polyline path, Vector2D ... vertices) {
+ List<Vector2D> expectedVertices = Arrays.asList(vertices);
+ List<Vector2D> actualVertices = path.getVertices();
+
+ String msg = "Expected path vertices to equal " + expectedVertices + " but was " + actualVertices;
+ Assert.assertEquals(msg, expectedVertices.size(), actualVertices.size());
+
+ for (int i=0; i<expectedVertices.size(); ++i) {
+ EuclideanTestUtils.assertCoordinatesEqual(expectedVertices.get(i), actualVertices.get(i), TEST_EPS);
+ }
+ }
+
+ private static class TestConnector extends AbstractSegmentConnector {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected ConnectableSegment selectConnection(ConnectableSegment incoming, List<ConnectableSegment> outgoing) {
+ // just choose the first element
+ return outgoing.get(0);
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java
index fd819b4..76445e0 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java
@@ -632,6 +632,60 @@
}
@Test
+ public void testDeterminant() {
+ // act/assert
+ Assert.assertEquals(1.0, AffineTransformMatrix2D.identity().determinant(), EPS);
+ Assert.assertEquals(6.0, AffineTransformMatrix2D.of(
+ 2, 0, 4,
+ 0, 3, 5
+ ).determinant(), EPS);
+ Assert.assertEquals(-6.0, AffineTransformMatrix2D.of(
+ 2, 0, 4,
+ 0, -3, 5
+ ).determinant(), EPS);
+ Assert.assertEquals(-5.0, AffineTransformMatrix2D.of(
+ 1, 3, 0,
+ 2, 1, 0
+ ).determinant(), EPS);
+ Assert.assertEquals(-0.0, AffineTransformMatrix2D.of(
+ 0, 0, 1,
+ 0, 0, 2
+ ).determinant(), EPS);
+ }
+
+ @Test
+ public void testPreservesOrientation() {
+ // act/assert
+ Assert.assertTrue(AffineTransformMatrix2D.identity().preservesOrientation());
+ Assert.assertTrue(AffineTransformMatrix2D.of(
+ 2, 0, 4,
+ 0, 3, 5
+ ).preservesOrientation());
+
+ Assert.assertFalse(AffineTransformMatrix2D.of(
+ 2, 0, 4,
+ 0, -3, 5
+ ).preservesOrientation());
+ Assert.assertFalse(AffineTransformMatrix2D.of(
+ 1, 3, 0,
+ 2, 1, 0
+ ).preservesOrientation());
+ Assert.assertFalse(AffineTransformMatrix2D.of(
+ 0, 0, 1,
+ 0, 0, 2
+ ).preservesOrientation());
+ }
+
+ @Test
+ public void testToMatrix() {
+ // arrange
+ AffineTransformMatrix2D t = AffineTransformMatrix2D.createScale(2.0);
+
+ // act/assert
+ Assert.assertSame(t, t.toMatrix());
+ }
+
+ @Test
public void testMultiply() {
// arrange
AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java
new file mode 100644
index 0000000..b6666ab
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java
@@ -0,0 +1,1210 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ConvexAreaTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testFull() {
+ // act
+ ConvexArea area = ConvexArea.full();
+
+ // assert
+ Assert.assertTrue(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(0.0, area.getBoundarySize(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ Assert.assertNull(area.getBarycenter());
+ }
+
+ @Test
+ public void testToTree() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(
+ Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(1, 0), Geometry.HALF_PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(1, 1), Geometry.PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.MINUS_HALF_PI, TEST_PRECISION)
+ );
+
+ // act
+ RegionBSPTree2D tree = area.toTree();
+
+ // assert
+ Assert.assertEquals(1, tree.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_full() {
+ // arrange
+ Transform2D transform = FunctionTransform2D.from(v -> v.multiply(3));
+ ConvexArea area = ConvexArea.full();
+
+ // act
+ ConvexArea transformed = area.transform(transform);
+
+ // assert
+ Assert.assertSame(area, transformed);
+ }
+
+ @Test
+ public void testTransform_infinite() {
+ // arrange
+ AffineTransformMatrix2D mat = AffineTransformMatrix2D
+ .createRotation(Vector2D.of(0, 1), Geometry.HALF_PI)
+ .scale(Vector2D.of(3, 2));
+
+ ConvexArea area = ConvexArea.fromBounds(
+ Line.fromPointAndAngle(Vector2D.ZERO, 0.25 * Geometry.PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.ZERO, -0.25 * Geometry.PI, TEST_PRECISION));
+
+ // act
+ ConvexArea transformed = area.transform(mat);
+
+ // assert
+ Assert.assertNotSame(area, transformed);
+
+ List<Polyline> paths = transformed.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ List<Segment> segments = paths.get(0).getSegments();
+ Assert.assertEquals(2, segments.size());
+
+ Segment firstSegment = segments.get(0);
+ Assert.assertNull(firstSegment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 2), firstSegment.getEndPoint(), TEST_EPS);
+ Assert.assertEquals(Math.atan2(2, 3), firstSegment.getLine().getAngle(), TEST_EPS);
+
+ Segment secondSegment = segments.get(1);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 2), secondSegment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(secondSegment.getEndPoint());
+ Assert.assertEquals(Math.atan2(2, -3), secondSegment.getLine().getAngle(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_finite() {
+ // arrange
+ AffineTransformMatrix2D mat = AffineTransformMatrix2D.createScale(Vector2D.of(1, 2));
+
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.of(1, 1), Vector2D.of(2, 1),
+ Vector2D.of(2, 2), Vector2D.of(1, 2)
+ ), TEST_PRECISION);
+
+ // act
+ ConvexArea transformed = area.transform(mat);
+
+ // assert
+ Assert.assertNotSame(area, transformed);
+
+ List<Segment> segments = transformed.getBoundaries();
+ Assert.assertEquals(4, segments.size());
+
+ Assert.assertEquals(2, transformed.getSize(), TEST_EPS);
+ Assert.assertEquals(6, transformed.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 3), transformed.getBarycenter(), TEST_EPS);
+
+ checkRegion(transformed, RegionLocation.BOUNDARY,
+ Vector2D.of(1, 2), Vector2D.of(2, 2), Vector2D.of(2, 4), Vector2D.of(1, 4));
+ checkRegion(transformed, RegionLocation.INSIDE, transformed.getBarycenter());
+ }
+
+ @Test
+ public void testTransform_finite_withSingleReflection() {
+ // arrange
+ AffineTransformMatrix2D mat = AffineTransformMatrix2D.createScale(Vector2D.of(-1, 2));
+
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.of(1, 1), Vector2D.of(2, 1),
+ Vector2D.of(2, 2), Vector2D.of(1, 2)
+ ), TEST_PRECISION);
+
+ // act
+ ConvexArea transformed = area.transform(mat);
+
+ // assert
+ Assert.assertNotSame(area, transformed);
+
+ List<Segment> segments = transformed.getBoundaries();
+ Assert.assertEquals(4, segments.size());
+
+ Assert.assertEquals(2, transformed.getSize(), TEST_EPS);
+ Assert.assertEquals(6, transformed.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1.5, 3), transformed.getBarycenter(), TEST_EPS);
+
+ checkRegion(transformed, RegionLocation.BOUNDARY,
+ Vector2D.of(-1, 2), Vector2D.of(-2, 2), Vector2D.of(-2, 4), Vector2D.of(-1, 4));
+ checkRegion(transformed, RegionLocation.INSIDE, transformed.getBarycenter());
+ }
+
+ @Test
+ public void testTransform_finite_withDoubleReflection() {
+ // arrange
+ AffineTransformMatrix2D mat = AffineTransformMatrix2D.createScale(Vector2D.of(-1, -2));
+
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.of(1, 1), Vector2D.of(2, 1),
+ Vector2D.of(2, 2), Vector2D.of(1, 2)
+ ), TEST_PRECISION);
+
+ // act
+ ConvexArea transformed = area.transform(mat);
+
+ // assert
+ Assert.assertNotSame(area, transformed);
+
+ List<Segment> segments = transformed.getBoundaries();
+ Assert.assertEquals(4, segments.size());
+
+ Assert.assertEquals(2, transformed.getSize(), TEST_EPS);
+ Assert.assertEquals(6, transformed.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1.5, -3), transformed.getBarycenter(), TEST_EPS);
+
+ checkRegion(transformed, RegionLocation.BOUNDARY,
+ Vector2D.of(-1, -2), Vector2D.of(-2, -2), Vector2D.of(-2, -4), Vector2D.of(-1, -4));
+ checkRegion(transformed, RegionLocation.INSIDE, transformed.getBarycenter());
+ }
+
+ @Test
+ public void testGetVertices_full() {
+ // arrange
+ ConvexArea area = ConvexArea.full();
+
+ // act/assert
+ Assert.assertEquals(0, area.getVertices().size());
+ }
+
+ @Test
+ public void testGetVertices_twoParallelLines() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(
+ Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(0, -1), Geometry.ZERO_PI, TEST_PRECISION)
+ );
+
+ // act/assert
+ Assert.assertEquals(0, area.getVertices().size());
+ }
+
+ @Test
+ public void testGetVertices_infiniteWithVertices() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(
+ Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(0, -1), Geometry.ZERO_PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(1, 0), Geometry.HALF_PI, TEST_PRECISION)
+ );
+
+ // act
+ List<Vector2D> vertices = area.getVertices();
+
+ // assert
+ Assert.assertEquals(2, vertices.size());
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, -1), vertices.get(0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), vertices.get(1), TEST_EPS);
+ }
+
+ @Test
+ public void testGetVertices_finite() {
+ // arrange
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_X,
+ Vector2D.Unit.PLUS_Y
+ ), TEST_PRECISION);
+
+ // act
+ List<Vector2D> vertices = area.getVertices();
+
+ // assert
+ Assert.assertEquals(4, vertices.size());
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, vertices.get(0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_X, vertices.get(1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_Y, vertices.get(2), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, vertices.get(3), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_full() {
+ // arrange
+ ConvexArea area = ConvexArea.full();
+
+ // act/assert
+ Assert.assertNull(area.project(Vector2D.ZERO));
+ Assert.assertNull(area.project(Vector2D.Unit.PLUS_X));
+ }
+
+ @Test
+ public void testProject_halfSpace() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(
+ Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI, TEST_PRECISION));
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 1), area.project(Vector2D.of(1, 1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 2), area.project(Vector2D.of(-2, 2)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, -3), area.project(Vector2D.of(1, -3)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, -4), area.project(Vector2D.of(-2, -4)), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_square() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.ZERO, 1, 1));
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), area.project(Vector2D.of(1, 1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), area.project(Vector2D.of(2, 2)), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, area.project(Vector2D.ZERO), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, area.project(Vector2D.of(-1, -1)), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 0.5), area.project(Vector2D.of(0.1, 0.5)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.2, 1), area.project(Vector2D.of(0.2, 0.9)), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0), area.project(Vector2D.of(0.5, 0.5)), TEST_EPS);
+ }
+
+ @Test
+ public void testTrim_full() {
+ // arrange
+ ConvexArea area = ConvexArea.full();
+ Segment segment = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // act
+ Segment trimmed = area.trim(segment);
+
+ // assert
+ Assert.assertSame(segment, trimmed);
+ }
+
+ @Test
+ public void testTrim_halfSpace() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION));
+ Segment segment = Line.fromPoints(Vector2D.Unit.MINUS_Y, Vector2D.Unit.PLUS_Y, TEST_PRECISION).span();
+
+ // act
+ Segment trimmed = area.trim(segment);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, trimmed.getStartPoint(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(trimmed.getSubspaceEnd());
+ }
+
+ @Test
+ public void testTrim_square() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.ZERO, 1, 1));
+ Segment segment = Line.fromPoints(Vector2D.of(0.5, 0), Vector2D.of(0.5, 1), TEST_PRECISION).span();
+
+ // act
+ Segment trimmed = area.trim(segment);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0), trimmed.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 1), trimmed.getEndPoint(), TEST_EPS);
+ }
+
+ @Test
+ public void testTrim_segmentOutsideOfRegion() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.ZERO, 1, 1));
+ Segment segment = Line.fromPoints(Vector2D.of(-0.5, 0), Vector2D.of(-0.5, 1), TEST_PRECISION).span();
+
+ // act
+ Segment trimmed = area.trim(segment);
+
+ // assert
+ Assert.assertNull(trimmed);
+ }
+
+ @Test
+ public void testTrim_segmentDirectlyOnBoundaryOfRegion() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.ZERO, 1, 1));
+ Segment segment = Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(1, 1), TEST_PRECISION).span();
+
+ // act
+ Segment trimmed = area.trim(segment);
+
+ // assert
+ Assert.assertNull(trimmed);
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ ConvexArea input = ConvexArea.full();
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = input.split(splitter);
+
+ // act
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexArea minus = split.getMinus();
+ Assert.assertFalse(minus.isFull());
+ Assert.assertFalse(minus.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(minus.getBoundarySize());
+ GeometryTestUtils.assertPositiveInfinity(minus.getSize());
+ Assert.assertNull(minus.getBarycenter());
+
+ List<Segment> minusSegments = minus.getBoundaries();
+ Assert.assertEquals(1,minusSegments.size());
+ Assert.assertEquals(splitter, minusSegments.get(0).getLine());
+
+ ConvexArea plus = split.getPlus();
+ Assert.assertFalse(plus.isFull());
+ Assert.assertFalse(plus.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(plus.getBoundarySize());
+ GeometryTestUtils.assertPositiveInfinity(plus.getSize());
+ Assert.assertNull(plus.getBarycenter());
+
+ List<Segment> plusSegments = plus.getBoundaries();
+ Assert.assertEquals(1, plusSegments.size());
+ Assert.assertEquals(splitter, plusSegments.get(0).getLine().reverse());
+ }
+
+ @Test
+ public void testSplit_halfSpace_split() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+ Line splitter = Line.fromPointAndAngle(Vector2D.ZERO, 0.25 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexArea minus = split.getMinus();
+ Assert.assertFalse(minus.isFull());
+ Assert.assertFalse(minus.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(minus.getBoundarySize());
+ GeometryTestUtils.assertPositiveInfinity(minus.getSize());
+ Assert.assertNull(minus.getBarycenter());
+
+ Assert.assertEquals(2, minus.getBoundaries().size());
+
+ ConvexArea plus = split.getPlus();
+ Assert.assertFalse(plus.isFull());
+ Assert.assertFalse(plus.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(plus.getBoundarySize());
+ GeometryTestUtils.assertPositiveInfinity(plus.getSize());
+ Assert.assertNull(plus.getBarycenter());
+
+ Assert.assertEquals(2, plus.getBoundaries().size());
+ }
+
+ @Test
+ public void testSplit_halfSpace_splitOnBoundary() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+ Line splitter = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(area, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_halfSpace_splitOnBoundaryWithReversedSplitter() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+ Line splitter = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION).reverse();
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(area, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_square_split() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.of(1, 1), 2, 1));
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(2, 1), Geometry.HALF_PI, TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexArea minus = split.getMinus();
+ Assert.assertFalse(minus.isFull());
+ Assert.assertFalse(minus.isEmpty());
+
+ Assert.assertEquals(4, minus.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(1, minus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), minus.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(4, minus.getBoundaries().size());
+
+ ConvexArea plus = split.getPlus();
+ Assert.assertFalse(plus.isFull());
+ Assert.assertFalse(plus.isEmpty());
+
+ Assert.assertEquals(4, plus.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(1, plus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2.5, 1.5), plus.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(4, plus.getBoundaries().size());
+ }
+
+ @Test
+ public void testSplit_square_splitOnVertices() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.of(1, 1), 1, 1));
+ Line splitter = Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexArea minus = split.getMinus();
+ Assert.assertFalse(minus.isFull());
+ Assert.assertFalse(minus.isEmpty());
+
+ Assert.assertEquals(2 + Math.sqrt(2), minus.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.5, minus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4.0 / 3.0, 5.0 / 3.0), minus.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(3, minus.getBoundaries().size());
+
+ ConvexArea plus = split.getPlus();
+ Assert.assertFalse(plus.isFull());
+ Assert.assertFalse(plus.isEmpty());
+
+ Assert.assertEquals(2 + Math.sqrt(2), plus.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.5, plus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(5.0 / 3.0, 4.0 / 3.0), plus.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(3, plus.getBoundaries().size());
+ }
+
+ @Test
+ public void testSplit_square_splitOnVerticesWithReversedSplitter() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.of(1, 1), 1, 1));
+ Line splitter = Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).reverse();
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexArea minus = split.getMinus();
+ Assert.assertFalse(minus.isFull());
+ Assert.assertFalse(minus.isEmpty());
+
+ Assert.assertEquals(2 + Math.sqrt(2), minus.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.5, minus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(5.0 / 3.0, 4.0 / 3.0), minus.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(3, minus.getBoundaries().size());
+
+ ConvexArea plus = split.getPlus();
+ Assert.assertFalse(plus.isFull());
+ Assert.assertFalse(plus.isEmpty());
+
+ Assert.assertEquals(2 + Math.sqrt(2), plus.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.5, plus.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4.0 / 3.0, 5.0 / 3.0), plus.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(3, plus.getBoundaries().size());
+ }
+
+ @Test
+ public void testSplit_square_entirelyOnMinus() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.of(1, 1), 1, 1));
+ Line splitter = Line.fromPoints(Vector2D.of(3, 1), Vector2D.of(3, 2), TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+ Assert.assertSame(area, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_square_onMinusBoundary() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.of(1, 1), 1, 1));
+ Line splitter = Line.fromPoints(Vector2D.of(2, 1), Vector2D.of(2, 2), TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+ Assert.assertSame(area, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_square_entirelyOnPlus() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.of(1, 1), 1, 1));
+ Line splitter = Line.fromPoints(Vector2D.of(0, 1), Vector2D.of(0, 2), TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(area, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_square_onPlusBoundary() {
+ // arrange
+ ConvexArea area = ConvexArea.fromBounds(createSquareBoundingLines(Vector2D.of(1, 1), 1, 1));
+ Line splitter = Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(1, 2), TEST_PRECISION);
+
+ // act
+ Split<ConvexArea> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(area, split.getPlus());
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ ConvexArea area = ConvexArea.full();
+
+ // act
+ String str = area.toString();
+
+ // assert
+ Assert.assertTrue(str.contains("ConvexArea"));
+ Assert.assertTrue(str.contains("boundaries= "));
+ }
+
+ @Test
+ public void testFromVertices_noVertices() {
+ // act
+ ConvexArea area = ConvexArea.fromVertices(Arrays.asList(), TEST_PRECISION);
+
+ // assert
+ Assert.assertTrue(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(0, area.getBoundarySize(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ Assert.assertNull(area.getBarycenter());
+ }
+
+ @Test
+ public void testFromVertices_singleUniqueVertex() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromVertices(Arrays.asList(Vector2D.ZERO), precision);
+ }, IllegalStateException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1e-4, 1e-4)), precision);
+ }, IllegalStateException.class);
+ }
+
+ @Test
+ public void testFromVertices_twoVertices() {
+ // act
+ ConvexArea area = ConvexArea.fromVertices(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_X
+ ), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
+ Assert.assertNull(area.getBarycenter());
+
+ Assert.assertTrue(area.contains(Vector2D.Unit.PLUS_Y));
+ Assert.assertFalse(area.contains(Vector2D.Unit.MINUS_Y));
+ }
+
+ @Test
+ public void testFromVertices_threeVertices() {
+ // act
+ ConvexArea area = ConvexArea.fromVertices(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_X,
+ Vector2D.of(1, 1)
+ ), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
+ Assert.assertNull(area.getBarycenter());
+
+ Assert.assertTrue(area.contains(Vector2D.Unit.PLUS_Y));
+ Assert.assertFalse(area.contains(Vector2D.Unit.MINUS_Y));
+ Assert.assertFalse(area.contains(Vector2D.of(2, 2)));
+ }
+
+ @Test
+ public void testFromVertices_finite() {
+ // act
+ ConvexArea area = ConvexArea.fromVertices(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_X,
+ Vector2D.of(1, 1),
+ Vector2D.Unit.PLUS_Y,
+ Vector2D.ZERO
+ ), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(1, area.getSize(), TEST_EPS);
+ Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testFromVertices_handlesDuplicatePoints() {
+ // arrange
+ double eps = 1e-3;
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
+
+ // act
+ ConvexArea area = ConvexArea.fromVertices(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.of(1e-4, 1e-4),
+ Vector2D.Unit.PLUS_X,
+ Vector2D.of(1, 1e-4),
+ Vector2D.of(1, 1),
+ Vector2D.of(0, 1),
+ Vector2D.of(1e-4, 1),
+ Vector2D.of(1e-4, 1e-4)
+ ), precision);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(1, area.getSize(), eps);
+ Assert.assertEquals(4, area.getBoundarySize(), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), eps);
+ }
+
+ @Test
+ public void testFromVertices_clockwiseWinding() {
+ // act
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromVertices(
+ Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_Y,
+ Vector2D.of(1, 1),
+ Vector2D.Unit.PLUS_X,
+ Vector2D.ZERO
+ ),TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testFromVertexLoops_noVertices() {
+ // act
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(), TEST_PRECISION);
+
+ // assert
+ Assert.assertTrue(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(0, area.getBoundarySize(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ Assert.assertNull(area.getBarycenter());
+ }
+
+ @Test
+ public void testFromVertexLoop_singleUniqueVertex() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromVertexLoop(Arrays.asList(Vector2D.ZERO), precision);
+ }, IllegalStateException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromVertexLoop(Arrays.asList(Vector2D.ZERO, Vector2D.of(1e-4, 1e-4)), precision);
+ }, IllegalStateException.class);
+ }
+
+ @Test
+ public void testFromVertexLoop_twoVertices_fails() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromVertexLoop(Arrays.asList(Vector2D.ZERO, Vector2D.Unit.PLUS_X), TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testFromVertexLoop_square_closeRequired() {
+ // act
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_X,
+ Vector2D.of(1, 1),
+ Vector2D.of(0, 1)
+ ), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(1, area.getSize(), TEST_EPS);
+ Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testFromVertexLoop_square_closeNotRequired() {
+ // act
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_X,
+ Vector2D.of(1, 1),
+ Vector2D.of(0, 1),
+ Vector2D.ZERO
+ ), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(1, area.getSize(), TEST_EPS);
+ Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testFromVertexLoop_handlesDuplicatePoints() {
+ // arrange
+ double eps = 1e-3;
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
+
+ // act
+ ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.of(1e-4, 1e-4),
+ Vector2D.Unit.PLUS_X,
+ Vector2D.of(1, 1e-4),
+ Vector2D.of(1, 1),
+ Vector2D.of(0, 1),
+ Vector2D.of(1e-4, 1),
+ Vector2D.of(1e-4, 1e-4)
+ ), precision);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(1, area.getSize(), eps);
+ Assert.assertEquals(4, area.getBoundarySize(), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), eps);
+ }
+
+ @Test
+ public void testFromVertexLoop_clockwiseWinding() {
+ // act
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromVertexLoop(
+ Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_Y,
+ Vector2D.of(1, 1),
+ Vector2D.Unit.PLUS_X
+ ),TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testFromPath_empty() {
+ // act
+ ConvexArea area = ConvexArea.fromPath(Polyline.empty());
+
+ // assert
+ Assert.assertTrue(area.isFull());
+ }
+
+ @Test
+ public void testFromPath_infinite() {
+ // act
+ ConvexArea area = ConvexArea.fromPath(Polyline.fromVertices(
+ Arrays.asList(Vector2D.ZERO, Vector2D.Unit.PLUS_X),TEST_PRECISION));
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ Assert.assertNull(area.getBarycenter());
+
+ checkRegion(area, RegionLocation.INSIDE, Vector2D.Unit.PLUS_Y);
+ checkRegion(area, RegionLocation.BOUNDARY, Vector2D.ZERO);
+ checkRegion(area, RegionLocation.OUTSIDE, Vector2D.Unit.MINUS_Y);
+ }
+
+ @Test
+ public void testFromPath_finite() {
+ // act
+ ConvexArea area = ConvexArea.fromPath(Polyline.fromVertexLoop(
+ Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_X,
+ Vector2D.of(1, 1),
+ Vector2D.Unit.PLUS_Y
+ ),TEST_PRECISION));
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(1, area.getSize(), TEST_EPS);
+ Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testFromPath_clockwiseWinding() {
+ // act
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromPath(Polyline.fromVertexLoop(
+ Arrays.asList(
+ Vector2D.ZERO,
+ Vector2D.Unit.PLUS_Y,
+ Vector2D.of(1, 1),
+ Vector2D.Unit.PLUS_X
+ ),TEST_PRECISION));
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testFromBounds_noLines() {
+ // act
+ ConvexArea area = ConvexArea.fromBounds(Collections.emptyList());
+
+ // assert
+ Assert.assertSame(ConvexArea.full(), area);
+ }
+
+ @Test
+ public void testFromBounds_singleLine() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.of(0, 1), Vector2D.of(1, 3), TEST_PRECISION);
+
+ // act
+ ConvexArea area = ConvexArea.fromBounds(line);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ Assert.assertNull(area.getBarycenter());
+
+ List<Segment> segments = area.getBoundaries();
+ Assert.assertEquals(1, segments.size());
+ Assert.assertSame(line, segments.get(0).getLine());
+
+ checkRegion(area, RegionLocation.INSIDE, Vector2D.of(-1, 1), Vector2D.of(0, 2));
+ checkRegion(area, RegionLocation.BOUNDARY, Vector2D.of(0, 1), Vector2D.of(2, 5));
+ checkRegion(area, RegionLocation.OUTSIDE, Vector2D.ZERO, Vector2D.of(2, 3));
+ }
+
+ @Test
+ public void testFromBounds_twoLines() {
+ // arrange
+ Line a = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI, TEST_PRECISION);
+ Line b = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.PI, TEST_PRECISION);
+
+ // act
+ ConvexArea area = ConvexArea.fromBounds(a, b);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ Assert.assertNull(area.getBarycenter());
+
+ List<Segment> segments = area.getBoundaries();
+ Assert.assertEquals(2, segments.size());
+
+ checkRegion(area, RegionLocation.INSIDE, Vector2D.of(-1, -1));
+ checkRegion(area, RegionLocation.BOUNDARY,
+ Vector2D.ZERO, Vector2D.of(-1, 0), Vector2D.of(0, -1));
+ checkRegion(area, RegionLocation.OUTSIDE,
+ Vector2D.of(-1, 1), Vector2D.of(1, 1), Vector2D.of(1, -1));
+ }
+
+ @Test
+ public void testFromBounds_triangle() {
+ // arrange
+ Line a = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI, TEST_PRECISION);
+ Line b = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.PI, TEST_PRECISION);
+ Line c = Line.fromPointAndAngle(Vector2D.of(-2, 0), -0.25 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ ConvexArea area = ConvexArea.fromBounds(a, b, c);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(4 + (2 * Math.sqrt(2)), area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(2, area.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2.0 / 3.0, -2.0 / 3.0), area.getBarycenter(), TEST_EPS);
+
+ List<Segment> segments = area.getBoundaries();
+ Assert.assertEquals(3, segments.size());
+
+ checkRegion(area, RegionLocation.INSIDE, Vector2D.of(-0.5, -0.5));
+ checkRegion(area, RegionLocation.BOUNDARY,
+ Vector2D.ZERO, Vector2D.of(-1, 0), Vector2D.of(0, -1));
+ checkRegion(area, RegionLocation.OUTSIDE,
+ Vector2D.of(-1, 1), Vector2D.of(1, 1), Vector2D.of(1, -1), Vector2D.of(-2, -2));
+ }
+
+ @Test
+ public void testFromBounds_square() {
+ // arrange
+ List<Line> square = createSquareBoundingLines(Vector2D.ZERO, 1, 1);
+
+ // act
+ ConvexArea area = ConvexArea.fromBounds(square);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(1, area.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+
+ List<Segment> segments = area.getBoundaries();
+ Assert.assertEquals(4, segments.size());
+
+ checkRegion(area, RegionLocation.INSIDE, Vector2D.of(0.5, 0.5));
+ checkRegion(area, RegionLocation.BOUNDARY,
+ Vector2D.ZERO, Vector2D.of(1, 1),
+ Vector2D.of(0.5, 0), Vector2D.of(0.5, 1),
+ Vector2D.of(0, 0.5), Vector2D.of(1, 0.5));
+ checkRegion(area, RegionLocation.OUTSIDE,
+ Vector2D.of(-1, -1), Vector2D.of(2, 2));
+ }
+
+ @Test
+ public void testFromBounds_square_extraLines() {
+ // arrange
+ List<Line> extraLines = new ArrayList<>();
+ extraLines.add(Line.fromPoints(Vector2D.of(10, 10), Vector2D.of(10, 11), TEST_PRECISION));
+ extraLines.add(Line.fromPoints(Vector2D.of(-10, 10), Vector2D.of(-10, 9), TEST_PRECISION));
+ extraLines.add(Line.fromPoints(Vector2D.of(0, 10), Vector2D.of(-1, 11), TEST_PRECISION));
+ extraLines.addAll(createSquareBoundingLines(Vector2D.ZERO, 1, 1));
+
+ // act
+ ConvexArea area = ConvexArea.fromBounds(extraLines);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(1, area.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+
+ List<Segment> segments = area.getBoundaries();
+ Assert.assertEquals(4, segments.size());
+
+ checkRegion(area, RegionLocation.INSIDE, Vector2D.of(0.5, 0.5));
+ checkRegion(area, RegionLocation.BOUNDARY,
+ Vector2D.ZERO, Vector2D.of(1, 1),
+ Vector2D.of(0.5, 0), Vector2D.of(0.5, 1),
+ Vector2D.of(0, 0.5), Vector2D.of(1, 0.5));
+ checkRegion(area, RegionLocation.OUTSIDE,
+ Vector2D.of(-1, -1), Vector2D.of(2, 2));
+ }
+
+ @Test
+ public void testFromBounds_square_duplicateLines() {
+ // arrange
+ List<Line> duplicateLines = new ArrayList<>();
+ duplicateLines.addAll(createSquareBoundingLines(Vector2D.ZERO, 1, 1));
+ duplicateLines.addAll(createSquareBoundingLines(Vector2D.ZERO, 1, 1));
+
+ // act
+ ConvexArea area = ConvexArea.fromBounds(duplicateLines);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(1, area.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+
+ List<Segment> segments = area.getBoundaries();
+ Assert.assertEquals(4, segments.size());
+
+ checkRegion(area, RegionLocation.INSIDE, Vector2D.of(0.5, 0.5));
+ checkRegion(area, RegionLocation.BOUNDARY,
+ Vector2D.ZERO, Vector2D.of(1, 1),
+ Vector2D.of(0.5, 0), Vector2D.of(0.5, 1),
+ Vector2D.of(0, 0.5), Vector2D.of(1, 0.5));
+ checkRegion(area, RegionLocation.OUTSIDE,
+ Vector2D.of(-1, -1), Vector2D.of(2, 2));
+ }
+
+ @Test
+ public void testFromBounds_duplicateLines_similarOrientation() {
+ // arrange
+ Line a = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ Line b = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ Line c = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ ConvexArea area = ConvexArea.fromBounds(a, b, c);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
+ GeometryTestUtils.assertPositiveInfinity(area.getSize());
+ Assert.assertNull(area.getBarycenter());
+
+ List<Segment> segments = area.getBoundaries();
+ Assert.assertEquals(1, segments.size());
+
+ checkRegion(area, RegionLocation.BOUNDARY, Vector2D.of(0, 1), Vector2D.of(1, 1), Vector2D.of(-1, 1));
+ checkRegion(area, RegionLocation.INSIDE, Vector2D.of(0, 2), Vector2D.of(1, 2), Vector2D.of(-1, 2));
+ checkRegion(area, RegionLocation.OUTSIDE, Vector2D.of(0, 0), Vector2D.of(1, 0), Vector2D.of(-1, 0));
+ }
+
+ @Test
+ public void testFromBounds_duplicateLines_differentOrientation() {
+ // arrange
+ Line a = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ Line b = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION);
+ Line c = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromBounds(a, b, c);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testFromBounds_boundsDoNotProduceAConvexRegion() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea.fromBounds(Arrays.asList(
+ Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.of(0, -1), Geometry.PI, TEST_PRECISION),
+ Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI, TEST_PRECISION)
+ ));
+ }, GeometryException.class);
+ }
+
+ private static void checkRegion(ConvexArea area, RegionLocation loc, Vector2D ... pts) {
+ for (Vector2D pt : pts) {
+ Assert.assertEquals("Unexpected region location for point " + pt, loc, area.classify(pt));
+ }
+ }
+
+ private static List<Line> createSquareBoundingLines(final Vector2D lowerLeft, final double width, final double height) {
+ final Vector2D lowerRight = Vector2D.of(lowerLeft.getX() + width, lowerLeft.getY());
+ final Vector2D upperRight = Vector2D.of(lowerLeft.getX() + width, lowerLeft.getY() + height);
+ final Vector2D upperLeft = Vector2D.of(lowerLeft.getX(), lowerLeft.getY() + height);
+
+ return Arrays.asList(
+ Line.fromPoints(lowerLeft, lowerRight, TEST_PRECISION),
+ Line.fromPoints(upperRight, upperLeft, TEST_PRECISION),
+ Line.fromPoints(lowerRight, upperRight, TEST_PRECISION),
+ Line.fromPoints(upperLeft, lowerLeft, TEST_PRECISION)
+ );
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/FunctionTransform2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/FunctionTransform2DTest.java
new file mode 100644
index 0000000..845b187
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/FunctionTransform2DTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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;
+
+import java.util.function.Function;
+
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FunctionTransform2DTest {
+
+ private static final double TEST_EPS = 1e-15;
+
+ @Test
+ public void testIdentity() {
+ // arrange
+ Vector2D p0 = Vector2D.of(0, 0);
+ Vector2D p1 = Vector2D.of(1, 1);
+ Vector2D p2 = Vector2D.of(-1, -1);
+
+ // act
+ FunctionTransform2D t = FunctionTransform2D.identity();
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_identity() {
+ // arrange
+ Vector2D p0 = Vector2D.of(0, 0);
+ Vector2D p1 = Vector2D.of(1, 1);
+ Vector2D p2 = Vector2D.of(-1, -1);
+
+ // act
+ FunctionTransform2D t = FunctionTransform2D.from(Function.identity());
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_scaleAndTranslate() {
+ // arrange
+ Vector2D p0 = Vector2D.of(0, 0);
+ Vector2D p1 = Vector2D.of(1, 2);
+ Vector2D p2 = Vector2D.of(-1, -2);
+
+ // act
+ FunctionTransform2D t = FunctionTransform2D.from(v -> v.multiply(2).add(Vector2D.of(1, -1)));
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, -1), t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 3), t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, -5), t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_reflection_singleAxis() {
+ // arrange
+ Vector2D p0 = Vector2D.of(0, 0);
+ Vector2D p1 = Vector2D.of(1, 2);
+ Vector2D p2 = Vector2D.of(-1, -2);
+
+ // act
+ FunctionTransform2D t = FunctionTransform2D.from(v -> Vector2D.of(-v.getX(), v.getY()));
+
+ // assert
+ Assert.assertFalse(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 2), t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, -2), t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_reflection_bothAxes() {
+ // arrange
+ Vector2D p0 = Vector2D.of(0, 0);
+ Vector2D p1 = Vector2D.of(1, 2);
+ Vector2D p2 = Vector2D.of(-1, -2);
+
+ // act
+ FunctionTransform2D t = FunctionTransform2D.from(Vector2D::negate);
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ EuclideanTestUtils.assertCoordinatesEqual(p0, t.apply(p0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, -2), t.apply(p1), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), t.apply(p2), TEST_EPS);
+ }
+
+ @Test
+ public void testApplyVector() {
+ // arrange
+ Transform2D t = FunctionTransform2D.from(v -> {
+ return v.multiply(-2).add(Vector2D.of(4, 5));
+ });
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, t.applyVector(Vector2D.ZERO), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, 0), t.applyVector(Vector2D.Unit.PLUS_X), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-4, -4), t.applyVector(Vector2D.of(2, 2)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 0), t.applyVector(Vector2D.of(-1, 0)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4, 6), t.applyVector(Vector2D.of(-2, -3)), TEST_EPS);
+ }
+
+ @Test
+ public void testToMatrix() {
+ // act/assert
+ Assert.assertArrayEquals(new double[] {
+ 1, 0, 0,
+ 0, 1, 0
+ },
+ FunctionTransform2D.identity().toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] {
+ 1, 0, 2,
+ 0, 1, 3
+ },
+ FunctionTransform2D.from(v -> v.add(Vector2D.of(2, 3))).toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] {
+ 3, 0, 0,
+ 0, 3, 0
+ },
+ FunctionTransform2D.from(v -> v.multiply(3)).toMatrix().toArray(), TEST_EPS);
+ Assert.assertArrayEquals(new double[] {
+ 3, 0, 6,
+ 0, 3, 9
+ },
+ FunctionTransform2D.from(v -> v.add(Vector2D.of(2, 3)).multiply(3)).toMatrix().toArray(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransformRoundTrip() {
+ // arrange
+ double eps = 1e-8;
+ double delta = 0.11;
+
+ Vector2D p1 = Vector2D.of(1.1, -3);
+ Vector2D p2 = Vector2D.of(-5, 0.2);
+ Vector2D vec = p1.vectorTo(p2);
+
+ EuclideanTestUtils.permuteSkipZero(-2, 2, delta, (translate, scale) -> {
+
+ FunctionTransform2D t = FunctionTransform2D.from(v -> {
+ return v.multiply(scale * 0.5)
+ .add(Vector2D.of(translate, 0.5 * translate))
+ .multiply(scale * 1.5);
+ });
+
+ // act
+ Vector2D t1 = t.apply(p1);
+ Vector2D t2 = t.apply(p2);
+ Vector2D tvec = t.applyVector(vec);
+
+ Transform2D inverse = t.toMatrix().inverse();
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(tvec, t1.vectorTo(t2), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, inverse.apply(t1), eps);
+ EuclideanTestUtils.assertCoordinatesEqual(p2, inverse.apply(t2), eps);
+ });
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/InteriorAngleSegmentConnectorTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/InteriorAngleSegmentConnectorTest.java
new file mode 100644
index 0000000..1a18650
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/InteriorAngleSegmentConnectorTest.java
@@ -0,0 +1,342 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Consumer;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.twod.InteriorAngleSegmentConnector.Maximize;
+import org.apache.commons.geometry.euclidean.twod.InteriorAngleSegmentConnector.Minimize;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class InteriorAngleSegmentConnectorTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testConnectAll_noSegments() {
+ runWithMaxAndMin(connector -> {
+ // arrange
+ List<Segment> segments = new ArrayList<>();
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(0, paths.size());
+ });
+ }
+
+ @Test
+ public void testConnectAll_singleFiniteSegment() {
+ runWithMaxAndMin(connector -> {
+ // arrange
+ List<Segment> segments = Arrays.asList(
+ Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION)
+ );
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertFinitePath(paths.get(0), Vector2D.ZERO, Vector2D.Unit.PLUS_X);
+ });
+ }
+
+ @Test
+ public void testConnectAll_dualConnectedSegments() {
+ runWithMaxAndMin(connector -> {
+ // arrange
+ List<Segment> segments = Arrays.asList(
+ Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION),
+ Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.ZERO, TEST_PRECISION)
+ );
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ Assert.assertTrue(paths.get(0).isClosed());
+ assertFinitePath(paths.get(0), Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.ZERO);
+ });
+ }
+
+ @Test
+ public void testConnectAll_singleFiniteSegmentLoop() {
+ runWithMaxAndMin(connector -> {
+ // arrange
+ List<Segment> segments = shuffle(createSquare(Vector2D.ZERO, 1, 1));
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1),
+ Vector2D.of(0, 1), Vector2D.ZERO);
+ });
+ }
+
+ @Test
+ public void testConnectAll_disjointPaths() {
+ runWithMaxAndMin(connector -> {
+ // arrange
+ List<Segment> segments = new ArrayList<>();
+ segments.addAll(createSquare(Vector2D.ZERO, 1, 1));
+
+ Vector2D pt = Vector2D.of(0, 2);
+ Segment a = Line.fromPointAndAngle(pt, Geometry.ZERO_PI, TEST_PRECISION).segmentTo(pt);
+ Segment b = Line.fromPointAndAngle(pt, Geometry.HALF_PI, TEST_PRECISION).segmentFrom(pt);
+
+ segments.add(a);
+ segments.add(b);
+
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1),
+ Vector2D.of(0, 1), Vector2D.ZERO);
+
+ assertInfinitePath(paths.get(1), a, b, pt);
+ });
+ }
+
+ @Test
+ public void testConnectAll_squaresJoinedAtVertex_maximize() {
+ // arrange
+ Maximize connector = new Maximize();
+
+ List<Segment> segments = new ArrayList<>();
+ segments.addAll(createSquare(Vector2D.ZERO, 1, 1));
+ segments.addAll(createSquare(Vector2D.of(1, 1), 1, 1));
+
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1),
+ Vector2D.of(2, 1), Vector2D.of(2, 2),
+ Vector2D.of(1, 2), Vector2D.of(1, 1),
+ Vector2D.of(0, 1), Vector2D.ZERO);
+ }
+
+ @Test
+ public void testConnectAll_mutipleSegmentsAtVertex_maximize() {
+ // arrange
+ Maximize connector = new Maximize();
+
+ List<Segment> segments = new ArrayList<>();
+ segments.add(Segment.fromPoints(Vector2D.ZERO, Vector2D.of(2, 2), TEST_PRECISION));
+
+ segments.add(Segment.fromPoints(Vector2D.of(2, 2), Vector2D.of(2, 4), TEST_PRECISION));
+ segments.add(Segment.fromPoints(Vector2D.of(2, 2), Vector2D.of(1, 3), TEST_PRECISION));
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.of(2, 2), Vector2D.of(2, 4));
+
+ assertFinitePath(paths.get(1), Vector2D.of(2, 2), Vector2D.of(1, 3));
+ }
+
+ @Test
+ public void testConnectAll_squaresJoinedAtVertex_minimize() {
+ // arrange
+ Minimize connector = new Minimize();
+
+ List<Segment> segments = new ArrayList<>();
+ segments.addAll(createSquare(Vector2D.ZERO, 1, 1));
+ segments.addAll(createSquare(Vector2D.of(1, 1), 1, 1));
+
+ shuffle(segments);
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1),
+ Vector2D.of(0, 1), Vector2D.ZERO);
+
+ assertFinitePath(paths.get(1),
+ Vector2D.of(1, 1), Vector2D.of(2, 1), Vector2D.of(2, 2),
+ Vector2D.of(1, 2), Vector2D.of(1, 1));
+ }
+
+ @Test
+ public void testConnectAll_mutipleSegmentsAtVertex_minimize() {
+ // arrange
+ Minimize connector = new Minimize();
+
+ List<Segment> segments = new ArrayList<>();
+ segments.add(Segment.fromPoints(Vector2D.ZERO, Vector2D.of(2, 2), TEST_PRECISION));
+
+ segments.add(Segment.fromPoints(Vector2D.of(2, 2), Vector2D.of(2, 4), TEST_PRECISION));
+ segments.add(Segment.fromPoints(Vector2D.of(2, 2), Vector2D.of(1, 3), TEST_PRECISION));
+
+ // act
+ List<Polyline> paths = connector.connectAll(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.of(2, 2), Vector2D.of(1, 3));
+
+ assertFinitePath(paths.get(1), Vector2D.of(2, 2), Vector2D.of(2, 4));
+ }
+
+ @Test
+ public void testConnectMaximized() {
+ // arrange
+ List<Segment> segments = new ArrayList<>();
+ segments.add(Segment.fromPoints(Vector2D.ZERO, Vector2D.of(2, 2), TEST_PRECISION));
+
+ segments.add(Segment.fromPoints(Vector2D.of(2, 2), Vector2D.of(2, 4), TEST_PRECISION));
+ segments.add(Segment.fromPoints(Vector2D.of(2, 2), Vector2D.of(1, 3), TEST_PRECISION));
+
+ // act
+ List<Polyline> paths = InteriorAngleSegmentConnector.connectMaximized(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.of(2, 2), Vector2D.of(2, 4));
+
+ assertFinitePath(paths.get(1), Vector2D.of(2, 2), Vector2D.of(1, 3));
+ }
+
+ @Test
+ public void testConnectMinimized() {
+ // arrange
+ List<Segment> segments = new ArrayList<>();
+ segments.add(Segment.fromPoints(Vector2D.ZERO, Vector2D.of(2, 2), TEST_PRECISION));
+
+ segments.add(Segment.fromPoints(Vector2D.of(2, 2), Vector2D.of(2, 4), TEST_PRECISION));
+ segments.add(Segment.fromPoints(Vector2D.of(2, 2), Vector2D.of(1, 3), TEST_PRECISION));
+
+ // act
+ List<Polyline> paths = InteriorAngleSegmentConnector.connectMinimized(segments);
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertFinitePath(paths.get(0),
+ Vector2D.ZERO, Vector2D.of(2, 2), Vector2D.of(1, 3));
+
+ assertFinitePath(paths.get(1), Vector2D.of(2, 2), Vector2D.of(2, 4));
+ }
+
+ /**
+ * Run the given consumer function twice, once with a Maximize instance and once with
+ * a Minimize instance.
+ */
+ private static void runWithMaxAndMin(Consumer<InteriorAngleSegmentConnector> body) {
+ body.accept(new Maximize());
+ body.accept(new Minimize());
+ }
+
+ private static List<Segment> createSquare(final Vector2D lowerLeft, final double width, final double height) {
+ final Vector2D lowerRight = Vector2D.of(lowerLeft.getX() + width, lowerLeft.getY());
+ final Vector2D upperRight = Vector2D.of(lowerLeft.getX() + width, lowerLeft.getY() + height);
+ final Vector2D upperLeft = Vector2D.of(lowerLeft.getX(), lowerLeft.getY() + height);
+
+ return Arrays.asList(
+ Segment.fromPoints(lowerLeft, lowerRight, TEST_PRECISION),
+ Segment.fromPoints(lowerRight, upperRight, TEST_PRECISION),
+ Segment.fromPoints(upperRight, upperLeft, TEST_PRECISION),
+ Segment.fromPoints(upperLeft, lowerLeft, TEST_PRECISION)
+ );
+ }
+
+ private static List<Segment> shuffle(final List<Segment> segments) {
+ return shuffle(segments, 1);
+ }
+
+ private static List<Segment> shuffle(final List<Segment> segments, final int seed) {
+ Collections.shuffle(segments, new Random(seed));
+
+ return segments;
+ }
+
+ private static void assertInfinitePath(Polyline path, Segment start, Segment end,
+ Vector2D ... vertices) {
+ Assert.assertTrue(path.isInfinite());
+ Assert.assertFalse(path.isFinite());
+
+ Assert.assertEquals(start, path.getStartSegment());
+ Assert.assertEquals(end, path.getEndSegment());
+
+ assertPathVertices(path, vertices);
+ }
+
+ private static void assertFinitePath(Polyline path, Vector2D ... vertices)
+ {
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+
+ assertPathVertices(path, vertices);
+ }
+
+ private static void assertPathVertices(Polyline path, Vector2D ... vertices) {
+ List<Vector2D> expectedVertices = Arrays.asList(vertices);
+ List<Vector2D> actualVertices = path.getVertices();
+
+ String msg = "Expected path vertices to equal " + expectedVertices + " but was " + actualVertices;
+ Assert.assertEquals(msg, expectedVertices.size(), actualVertices.size());
+
+ for (int i=0; i<expectedVertices.size(); ++i) {
+ EuclideanTestUtils.assertCoordinatesEqual(expectedVertices.get(i), actualVertices.get(i), TEST_EPS);
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java
index 3907ec1..45e8701 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java
@@ -19,11 +19,13 @@
import org.apache.commons.geometry.core.Geometry;
import org.apache.commons.geometry.core.GeometryTestUtils;
import org.apache.commons.geometry.core.exception.GeometryValueException;
-import org.apache.commons.geometry.core.partitioning.Transform;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
+import org.apache.commons.geometry.euclidean.oned.Interval;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.twod.Line.SubspaceTransform;
import org.apache.commons.numbers.angle.PlaneAngleRadians;
import org.junit.Assert;
import org.junit.Test;
@@ -234,15 +236,6 @@
}
@Test
- public void testCopySelf() {
- // arrange
- Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
-
- // act/assert
- Assert.assertSame(line, line.copySelf());
- }
-
- @Test
public void testReverse() {
// arrange
Vector2D pt = Vector2D.of(0, 1);
@@ -262,15 +255,27 @@
}
@Test
- public void testToSubSpace() {
+ public void testAbscissa() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.of(-2, -2), Vector2D.of(2, 1), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(0.0, line.abscissa(Vector2D.of(-3, 4)), TEST_EPS);
+ Assert.assertEquals(0.0, line.abscissa(Vector2D.of( 3, -4)), TEST_EPS);
+ Assert.assertEquals(5.0, line.abscissa(Vector2D.of(7, -1)), TEST_EPS);
+ Assert.assertEquals(-5.0, line.abscissa(Vector2D.of(-1, -7)), TEST_EPS);
+ }
+
+ @Test
+ public void testToSubspace() {
// arrange
Line line = Line.fromPoints(Vector2D.of(2, 1), Vector2D.of(-2, -2), TEST_PRECISION);
// act/assert
- Assert.assertEquals(0.0, line.toSubSpace(Vector2D.of(-3, 4)).getX(), TEST_EPS);
- Assert.assertEquals(0.0, line.toSubSpace(Vector2D.of( 3, -4)).getX(), TEST_EPS);
- Assert.assertEquals(-5.0, line.toSubSpace(Vector2D.of(7, -1)).getX(), TEST_EPS);
- Assert.assertEquals(5.0, line.toSubSpace(Vector2D.of(-1, -7)).getX(), TEST_EPS);
+ Assert.assertEquals(0.0, line.toSubspace(Vector2D.of(-3, 4)).getX(), TEST_EPS);
+ Assert.assertEquals(0.0, line.toSubspace(Vector2D.of( 3, -4)).getX(), TEST_EPS);
+ Assert.assertEquals(-5.0, line.toSubspace(Vector2D.of(7, -1)).getX(), TEST_EPS);
+ Assert.assertEquals(5.0, line.toSubspace(Vector2D.of(-1, -7)).getX(), TEST_EPS);
}
@Test
@@ -380,6 +385,27 @@
}
@Test
+ public void testAngle() {
+ // arrange
+ Line a = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ Line b = Line.fromPointAndAngle(Vector2D.of(1, 4), Geometry.PI, TEST_PRECISION);
+ Line c = Line.fromPointAndDirection(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(Geometry.ZERO_PI, a.angle(a), TEST_EPS);
+ Assert.assertEquals(-Geometry.PI, a.angle(b), TEST_EPS);
+ Assert.assertEquals(0.25 * Geometry.PI, a.angle(c), TEST_EPS);
+
+ Assert.assertEquals(Geometry.ZERO_PI, b.angle(b), TEST_EPS);
+ Assert.assertEquals(-Geometry.PI, b.angle(a), TEST_EPS);
+ Assert.assertEquals(-0.75 * Geometry.PI, b.angle(c), TEST_EPS);
+
+ Assert.assertEquals(Geometry.ZERO_PI, c.angle(c), TEST_EPS);
+ Assert.assertEquals(-0.25 * Geometry.PI, c.angle(a), TEST_EPS);
+ Assert.assertEquals(0.75 * Geometry.PI, c.angle(b), TEST_EPS);
+ }
+
+ @Test
public void testProject() {
// --- arrange
Line xAxis = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
@@ -407,12 +433,12 @@
}
@Test
- public void testWholeHyperplane() {
+ public void testSpan() {
// arrange
Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
// act
- SubLine result = line.wholeHyperplane();
+ Segment result = line.span();
// assert
Assert.assertSame(line, result.getHyperplane());
@@ -420,20 +446,148 @@
}
@Test
- public void testWholeSpace() {
+ public void testSegment_interval() {
// arrange
- Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ Interval interval = Interval.of(1, 2, TEST_PRECISION);
// act
- PolygonsSet result = line.wholeSpace();
+ Segment segment = line.segment(interval);
// assert
- GeometryTestUtils.assertPositiveInfinity(result.getSize());
- Assert.assertSame(TEST_PRECISION, result.getPrecision());
+ Assert.assertSame(line, segment.getLine());
+ Assert.assertSame(interval, segment.getSubspaceRegion());
}
@Test
- public void testGetOffset_parallelLines() {
+ public void testSegment_doubles() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ Segment segment = line.segment(1, 2);
+
+ // assert
+ Assert.assertSame(line, segment.getLine());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), segment.getEndPoint(), TEST_EPS);
+ }
+
+ @Test
+ public void testSegment_pointsOnLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ Segment segment = line.segment(Vector2D.of(3, 1), Vector2D.of(2, 1));
+
+ // assert
+ Assert.assertSame(line, segment.getLine());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 1), segment.getEndPoint(), TEST_EPS);
+ }
+
+ @Test
+ public void testSegment_pointsProjectedOnLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ Segment segment = line.segment(Vector2D.of(-3, 2), Vector2D.of(2, -1));
+
+ // assert
+ Assert.assertSame(line, segment.getLine());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), segment.getEndPoint(), TEST_EPS);
+ }
+
+ @Test
+ public void testSegmentTo_pointOnLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION);
+
+ // act
+ Segment segment = line.segmentTo(Vector2D.of(-3, 1));
+
+ // assert
+ Assert.assertSame(line, segment.getLine());
+ Assert.assertTrue(segment.isInfinite());
+ Assert.assertNull(segment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), segment.getEndPoint(), TEST_EPS);
+
+ Assert.assertTrue(segment.contains(Vector2D.of(1, 1)));
+ Assert.assertFalse(segment.contains(Vector2D.of(-4, 1)));
+ }
+
+ @Test
+ public void testSegmentTo_pointProjectedOnLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION);
+
+ // act
+ Segment segment = line.segmentTo(Vector2D.of(-3, 5));
+
+ // assert
+ Assert.assertSame(line, segment.getLine());
+ Assert.assertTrue(segment.isInfinite());
+ Assert.assertNull(segment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), segment.getEndPoint(), TEST_EPS);
+
+ Assert.assertTrue(segment.contains(Vector2D.of(1, 1)));
+ Assert.assertFalse(segment.contains(Vector2D.of(-4, 1)));
+ }
+
+ @Test
+ public void testSegmentFrom_pointOnLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION);
+
+ // act
+ Segment segment = line.segmentFrom(Vector2D.of(-3, 1));
+
+ // assert
+ Assert.assertSame(line, segment.getLine());
+ Assert.assertTrue(segment.isInfinite());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), segment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertFalse(segment.contains(Vector2D.of(1, 1)));
+ Assert.assertTrue(segment.contains(Vector2D.of(-4, 1)));
+ }
+
+ @Test
+ public void testSegmentFrom_pointProjectedOnLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.PI, TEST_PRECISION);
+
+ // act
+ Segment segment = line.segmentFrom(Vector2D.of(-3, 5));
+
+ // assert
+ Assert.assertSame(line, segment.getLine());
+ Assert.assertTrue(segment.isInfinite());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 1), segment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertFalse(segment.contains(Vector2D.of(1, 1)));
+ Assert.assertTrue(segment.contains(Vector2D.of(-4, 1)));
+ }
+
+ @Test
+ public void testSubline() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ SubLine subline = line.subline();
+
+ // assert
+ Assert.assertSame(line, subline.getLine());
+ Assert.assertTrue(subline.isEmpty());
+ }
+
+ @Test
+ public void testOffset_parallelLines() {
// arrange
double dist = Math.sin(Math.atan2(2, 1));
@@ -443,35 +597,35 @@
Line d = Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, -2), TEST_PRECISION);
// act/assert
- Assert.assertEquals(-dist, a.getOffset(b), TEST_EPS);
- Assert.assertEquals(dist, b.getOffset(a), TEST_EPS);
+ Assert.assertEquals(-dist, a.offset(b), TEST_EPS);
+ Assert.assertEquals(dist, b.offset(a), TEST_EPS);
- Assert.assertEquals(dist, a.getOffset(c), TEST_EPS);
- Assert.assertEquals(-dist, c.getOffset(a), TEST_EPS);
+ Assert.assertEquals(dist, a.offset(c), TEST_EPS);
+ Assert.assertEquals(-dist, c.offset(a), TEST_EPS);
- Assert.assertEquals(3 * dist, a.getOffset(d), TEST_EPS);
- Assert.assertEquals(3 * dist, d.getOffset(a), TEST_EPS);
+ Assert.assertEquals(3 * dist, a.offset(d), TEST_EPS);
+ Assert.assertEquals(3 * dist, d.offset(a), TEST_EPS);
}
@Test
- public void testGetOffset_coincidentLines() {
+ public void testOffset_coincidentLines() {
// arrange
Line a = Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION);
Line b = Line.fromPoints(Vector2D.of(-2, 0), Vector2D.of(0, 4), TEST_PRECISION);
Line c = b.reverse();
// act/assert
- Assert.assertEquals(0, a.getOffset(a), TEST_EPS);
+ Assert.assertEquals(0, a.offset(a), TEST_EPS);
- Assert.assertEquals(0, a.getOffset(b), TEST_EPS);
- Assert.assertEquals(0, b.getOffset(a), TEST_EPS);
+ Assert.assertEquals(0, a.offset(b), TEST_EPS);
+ Assert.assertEquals(0, b.offset(a), TEST_EPS);
- Assert.assertEquals(0, a.getOffset(c), TEST_EPS);
- Assert.assertEquals(0, c.getOffset(a), TEST_EPS);
+ Assert.assertEquals(0, a.offset(c), TEST_EPS);
+ Assert.assertEquals(0, c.offset(a), TEST_EPS);
}
@Test
- public void testGetOffset_nonParallelLines() {
+ public void testOffset_nonParallelLines() {
// arrange
Line a = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
Line b = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
@@ -479,38 +633,38 @@
Line d = Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(0, 4), TEST_PRECISION);
// act/assert
- Assert.assertEquals(0, a.getOffset(b), TEST_EPS);
- Assert.assertEquals(0, b.getOffset(a), TEST_EPS);
+ Assert.assertEquals(0, a.offset(b), TEST_EPS);
+ Assert.assertEquals(0, b.offset(a), TEST_EPS);
- Assert.assertEquals(0, a.getOffset(c), TEST_EPS);
- Assert.assertEquals(0, c.getOffset(a), TEST_EPS);
+ Assert.assertEquals(0, a.offset(c), TEST_EPS);
+ Assert.assertEquals(0, c.offset(a), TEST_EPS);
- Assert.assertEquals(0, a.getOffset(d), TEST_EPS);
- Assert.assertEquals(0, d.getOffset(a), TEST_EPS);
+ Assert.assertEquals(0, a.offset(d), TEST_EPS);
+ Assert.assertEquals(0, d.offset(a), TEST_EPS);
}
@Test
- public void testGetOffset_point() {
+ public void testOffset_point() {
// arrange
Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
Line reversed = line.reverse();
// act/assert
- Assert.assertEquals(0.0, line.getOffset(Vector2D.of(-0.5, 1)), TEST_EPS);
- Assert.assertEquals(0.0, line.getOffset(Vector2D.of(-1.5, -1)), TEST_EPS);
- Assert.assertEquals(0.0, line.getOffset(Vector2D.of(0.5, 3)), TEST_EPS);
+ Assert.assertEquals(0.0, line.offset(Vector2D.of(-0.5, 1)), TEST_EPS);
+ Assert.assertEquals(0.0, line.offset(Vector2D.of(-1.5, -1)), TEST_EPS);
+ Assert.assertEquals(0.0, line.offset(Vector2D.of(0.5, 3)), TEST_EPS);
double d = Math.sin(Math.atan2(2, 1));
- Assert.assertEquals(d, line.getOffset(Vector2D.ZERO), TEST_EPS);
- Assert.assertEquals(-d, line.getOffset(Vector2D.of(-1, 2)), TEST_EPS);
+ Assert.assertEquals(d, line.offset(Vector2D.ZERO), TEST_EPS);
+ Assert.assertEquals(-d, line.offset(Vector2D.of(-1, 2)), TEST_EPS);
- Assert.assertEquals(-d, reversed.getOffset(Vector2D.ZERO), TEST_EPS);
- Assert.assertEquals(d, reversed.getOffset(Vector2D.of(-1, 2)), TEST_EPS);
+ Assert.assertEquals(-d, reversed.offset(Vector2D.ZERO), TEST_EPS);
+ Assert.assertEquals(d, reversed.offset(Vector2D.of(-1, 2)), TEST_EPS);
}
@Test
- public void testGetOffset_point_permute() {
+ public void testOffset_point_permute() {
// arrange
Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(0, 2), TEST_PRECISION);
Vector2D lineOrigin = line.getOrigin();
@@ -519,7 +673,7 @@
Vector2D pt = Vector2D.of(x, y);
// act
- double offset = line.getOffset(pt);
+ double offset = line.offset(pt);
// arrange
Vector2D vec = lineOrigin.vectorTo(pt).reject(line.getDirection());
@@ -531,7 +685,7 @@
}
@Test
- public void testSameOrientationAs() {
+ public void testSimilarOrientation() {
// arrange
Line a = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
Line b = Line.fromPointAndAngle(Vector2D.of(4, 5), Geometry.ZERO_PI, TEST_PRECISION);
@@ -543,38 +697,38 @@
Line g = Line.fromPointAndAngle(Vector2D.of(6, -3), -0.8 * Geometry.PI, TEST_PRECISION);
// act/assert
- Assert.assertTrue(a.sameOrientationAs(a));
- Assert.assertTrue(a.sameOrientationAs(b));
- Assert.assertTrue(b.sameOrientationAs(a));
- Assert.assertTrue(a.sameOrientationAs(c));
- Assert.assertTrue(c.sameOrientationAs(a));
- Assert.assertTrue(a.sameOrientationAs(d));
- Assert.assertTrue(d.sameOrientationAs(a));
+ Assert.assertTrue(a.similarOrientation(a));
+ Assert.assertTrue(a.similarOrientation(b));
+ Assert.assertTrue(b.similarOrientation(a));
+ Assert.assertTrue(a.similarOrientation(c));
+ Assert.assertTrue(c.similarOrientation(a));
+ Assert.assertTrue(a.similarOrientation(d));
+ Assert.assertTrue(d.similarOrientation(a));
- Assert.assertFalse(c.sameOrientationAs(d));
- Assert.assertFalse(d.sameOrientationAs(c));
+ Assert.assertFalse(c.similarOrientation(d));
+ Assert.assertFalse(d.similarOrientation(c));
- Assert.assertTrue(e.sameOrientationAs(f));
- Assert.assertTrue(f.sameOrientationAs(e));
- Assert.assertTrue(e.sameOrientationAs(g));
- Assert.assertTrue(g.sameOrientationAs(e));
+ Assert.assertTrue(e.similarOrientation(f));
+ Assert.assertTrue(f.similarOrientation(e));
+ Assert.assertTrue(e.similarOrientation(g));
+ Assert.assertTrue(g.similarOrientation(e));
- Assert.assertFalse(a.sameOrientationAs(e));
- Assert.assertFalse(e.sameOrientationAs(a));
+ Assert.assertFalse(a.similarOrientation(e));
+ Assert.assertFalse(e.similarOrientation(a));
}
@Test
- public void testSameOrientationAs_orthogonal() {
+ public void testSimilarOrientation_orthogonal() {
// arrange
Line a = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
Line b = Line.fromPointAndDirection(Vector2D.of(4, 5), Vector2D.Unit.PLUS_Y, TEST_PRECISION);
Line c = Line.fromPointAndDirection(Vector2D.of(-4, -5), Vector2D.Unit.MINUS_Y, TEST_PRECISION);
// act/assert
- Assert.assertTrue(a.sameOrientationAs(b));
- Assert.assertTrue(b.sameOrientationAs(a));
- Assert.assertTrue(a.sameOrientationAs(c));
- Assert.assertTrue(c.sameOrientationAs(a));
+ Assert.assertTrue(a.similarOrientation(b));
+ Assert.assertTrue(b.similarOrientation(a));
+ Assert.assertTrue(a.similarOrientation(c));
+ Assert.assertTrue(c.similarOrientation(a));
}
@Test
@@ -676,8 +830,8 @@
Vector2D point = line.pointAt(abscissa, offset);
// assert
- Assert.assertEquals(abscissa, line.toSubSpace(point).getX(), TEST_EPS);
- Assert.assertEquals(offset, line.getOffset(point), TEST_EPS);
+ Assert.assertEquals(abscissa, line.toSubspace(point).getX(), TEST_EPS);
+ Assert.assertEquals(offset, line.offset(point), TEST_EPS);
}
}
}
@@ -910,6 +1064,97 @@
}
@Test
+ public void testSubspaceTransform() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.of(1, 0), Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act/assert
+ checkSubspaceTransform(line.subspaceTransform(AffineTransformMatrix2D.createScale(2, 3)),
+ Vector2D.of(2, 0), Vector2D.Unit.PLUS_Y,
+ Vector2D.of(2, 0), Vector2D.of(2, 3));
+
+ checkSubspaceTransform(line.subspaceTransform(AffineTransformMatrix2D.createTranslation(2, 3)),
+ Vector2D.of(3, 0), Vector2D.Unit.PLUS_Y,
+ Vector2D.of(3, 3), Vector2D.of(3, 4));
+
+ checkSubspaceTransform(line.subspaceTransform(AffineTransformMatrix2D.createRotation(Geometry.HALF_PI)),
+ Vector2D.of(0, 1), Vector2D.Unit.MINUS_X,
+ Vector2D.of(0, 1), Vector2D.of(-1, 1));
+ }
+
+ private void checkSubspaceTransform(SubspaceTransform st, Vector2D origin, Vector2D dir, Vector2D tZero, Vector2D tOne) {
+
+ Line line = st.getLine();
+ AffineTransformMatrix1D transform = st.getTransform();
+
+ checkLine(line, origin, dir);
+
+ EuclideanTestUtils.assertCoordinatesEqual(tZero, line.toSpace(transform.apply(Vector1D.ZERO)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(tOne, line.toSpace(transform.apply(Vector1D.Unit.PLUS)), TEST_EPS);
+ }
+
+ @Test
+ public void testSubspaceTransform_transformsPointsCorrectly() {
+ // arrange
+ Line line = Line.fromPointAndDirection(Vector2D.of(1, 0), Vector2D.of(1, 1), TEST_PRECISION);
+
+ EuclideanTestUtils.permuteSkipZero(-2, 2, 0.5, (a, b) -> {
+ // create a somewhat complicate transform to try to hit all of the edge cases
+ AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(Vector2D.of(a, b))
+ .rotate(a * b)
+ .scale(0.1, 4);
+
+ // act
+ SubspaceTransform st = line.subspaceTransform(transform);
+
+ // assert
+ for (double x=-5.0; x<=5.0; x+=1) {
+ Vector1D subPt = Vector1D.of(x);
+ Vector2D expected = transform.apply(line.toSpace(subPt));
+ Vector2D actual = st.getLine().toSpace(
+ st.getTransform().apply(subPt));
+
+ EuclideanTestUtils.assertCoordinatesEqual(expected, actual, TEST_EPS);
+ };
+ });
+ }
+
+ @Test
+ public void testEq() {
+ // arrange
+ DoublePrecisionContext precision1 = new EpsilonDoublePrecisionContext(1e-3);
+ DoublePrecisionContext precision2 = new EpsilonDoublePrecisionContext(1e-2);
+
+ Vector2D p = Vector2D.of(1, 2);
+ double angle = 1.0;
+
+ Line a = Line.fromPointAndAngle(p, angle, precision1);
+ Line b = Line.fromPointAndAngle(Vector2D.ZERO, angle, precision1);
+ Line c = Line.fromPointAndAngle(p, angle + 1.0, precision1);
+ Line d = Line.fromPointAndAngle(p, angle, precision2);
+
+ Line e = Line.fromPointAndAngle(p, angle, precision1);
+ Line f = Line.fromPointAndAngle(p.add(Vector2D.of(1e-4, 1e-4)), angle, precision1);
+ Line g = Line.fromPointAndAngle(p, angle + 1e-4, precision1);
+
+ // act/assert
+ Assert.assertTrue(a.eq(a));
+
+ Assert.assertTrue(a.eq(e));
+ Assert.assertTrue(e.eq(a));
+
+ Assert.assertTrue(a.eq(f));
+ Assert.assertTrue(f.eq(a));
+
+ Assert.assertTrue(a.eq(g));
+ Assert.assertTrue(g.eq(a));
+
+ Assert.assertFalse(a.eq(b));
+ Assert.assertFalse(a.eq(c));
+ Assert.assertFalse(a.eq(d));
+ }
+
+ @Test
public void testHashCode() {
// arrange
DoublePrecisionContext precision1 = new EpsilonDoublePrecisionContext(1e-4);
@@ -977,25 +1222,6 @@
Assert.assertTrue(str.contains("direction= (1.0, 0.0)"));
}
- @Test
- public void testLineTransform() {
-
- Line l1 = Line.fromPoints(Vector2D.of(1.0 ,1.0), Vector2D.of(4.0 ,1.0), TEST_PRECISION);
- Transform<Vector2D, Vector1D> t1 =
- Line.getTransform(Vector2D.of(0.0, 0.5), Vector2D.of(-1.0, 0.0), Vector2D.of(1.0, 1.5));
- Assert.assertEquals(0.5 * Math.PI,
- ((Line) t1.apply(l1)).getAngle(),
- 1.0e-10);
-
- Line l2 = Line.fromPoints(Vector2D.of(0.0, 0.0), Vector2D.of(1.0, 1.0), TEST_PRECISION);
- Transform<Vector2D, Vector1D> t2 =
- Line.getTransform(Vector2D.of(0.0, 0.5), Vector2D.of(-1.0, 0.0), Vector2D.of(1.0, 1.5));
- Assert.assertEquals(Math.atan2(1.0, -2.0),
- ((Line) t2.apply(l2)).getAngle(),
- 1.0e-10);
-
- }
-
/**
* Check that the line has the given defining properties.
* @param line
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/NestedLoopsTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/NestedLoopsTest.java
deleted file mode 100644
index 05a0fb4..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/NestedLoopsTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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;
-
-import java.lang.reflect.Field;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class NestedLoopsTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @SuppressWarnings("unchecked")
- @Test
- public void testNestedLoops() throws Exception {
- Vector2D oneOne = Vector2D.of(1.0, 1.0);
- Vector2D oneNegativeOne = Vector2D.of(1.0, -1.0);
- Vector2D negativeOneNegativeOne = Vector2D.of(-1.0, -1.0);
- Vector2D negativeOneOne = Vector2D.of(-1.0, 1.0);
- Vector2D origin = Vector2D.of(0, 0);
-
- Vector2D [] vertices = new Vector2D[]{
- oneOne,
- oneNegativeOne,
- negativeOneNegativeOne,
- negativeOneOne,
- origin
- };
-
- NestedLoops nestedLoops = new NestedLoops(TEST_PRECISION);
- nestedLoops.add(vertices);
- nestedLoops.correctOrientation();
-
- Field surroundedField = nestedLoops.getClass().getDeclaredField("surrounded");
- Field loopField = nestedLoops.getClass().getDeclaredField("loop");
- surroundedField.setAccessible(Boolean.TRUE);
- loopField.setAccessible(Boolean.TRUE);
- List<NestedLoops> surrounded = (List<NestedLoops>) surroundedField.get(nestedLoops);
- Vector2D[] loop = (Vector2D []) loopField.get(surrounded.get(0));
- Set<Vector2D> vertexSet = new HashSet<>(Arrays.asList(loop));
- Assert.assertTrue(vertexSet.contains(oneOne));
- Assert.assertTrue(vertexSet.contains(oneNegativeOne));
- Assert.assertTrue(vertexSet.contains(negativeOneNegativeOne));
- Assert.assertTrue(vertexSet.contains(negativeOneOne));
- Assert.assertTrue(vertexSet.contains(origin));
- }
-
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinatesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinatesTest.java
index 81e94dc..4f22f1c 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinatesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinatesTest.java
@@ -173,6 +173,26 @@
}
@Test
+ public void testIsFinite() {
+ // act/assert
+ Assert.assertTrue(PolarCoordinates.of(1, 0).isFinite());
+ Assert.assertTrue(PolarCoordinates.of(1, Geometry.PI).isFinite());
+
+ Assert.assertFalse(PolarCoordinates.of(Double.NaN, Double.NaN).isFinite());
+
+ Assert.assertFalse(PolarCoordinates.of(Double.POSITIVE_INFINITY, 0).isFinite());
+ Assert.assertFalse(PolarCoordinates.of(Double.NEGATIVE_INFINITY, 0).isFinite());
+ Assert.assertFalse(PolarCoordinates.of(Double.NEGATIVE_INFINITY, Double.NaN).isFinite());
+
+ Assert.assertFalse(PolarCoordinates.of(0, Double.POSITIVE_INFINITY).isFinite());
+ Assert.assertFalse(PolarCoordinates.of(0, Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(PolarCoordinates.of(Double.NaN, Double.NEGATIVE_INFINITY).isFinite());
+
+ Assert.assertFalse(PolarCoordinates.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(PolarCoordinates.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY).isFinite());
+ }
+
+ @Test
public void testHashCode() {
// arrange
PolarCoordinates a = PolarCoordinates.of(1, 2);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolygonsSetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolygonsSetTest.java
deleted file mode 100644
index 53be220..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolygonsSetTest.java
+++ /dev/null
@@ -1,1849 +0,0 @@
-/*
- * 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;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
-import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
-import org.apache.commons.geometry.euclidean.oned.Interval;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
-import org.apache.commons.numbers.core.Precision;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class PolygonsSetTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testFull() {
- // act
- PolygonsSet poly = new PolygonsSet(TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, poly.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(poly.getSize());
- Assert.assertEquals(0.0, poly.getBoundarySize(), TEST_EPS);
- Assert.assertEquals(0, poly.getVertices().length);
- Assert.assertFalse(poly.isEmpty());
- Assert.assertTrue(poly.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, poly.getBarycenter(), TEST_EPS);
-
- checkPoints(Region.Location.INSIDE, poly,
- Vector2D.of(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY),
- Vector2D.ZERO,
- Vector2D.of(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY));
-
- for (double y = -1; y < 1; y += 0.1) {
- for (double x = -1; x < 1; x += 0.1) {
- EuclideanTestUtils.assertNegativeInfinity(poly.projectToBoundary(Vector2D.of(x, y)).getOffset());
- }
- }
- }
-
- @Test
- public void testEmpty() {
- // act
- PolygonsSet poly = (PolygonsSet) new RegionFactory<Vector2D>().getComplement(new PolygonsSet(TEST_PRECISION));
-
- // assert
- Assert.assertSame(TEST_PRECISION, poly.getPrecision());
- Assert.assertEquals(0.0, poly.getSize(), TEST_EPS);
- Assert.assertEquals(0.0, poly.getBoundarySize(), TEST_EPS);
- Assert.assertEquals(0, poly.getVertices().length);
- Assert.assertTrue(poly.isEmpty());
- Assert.assertFalse(poly.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, poly.getBarycenter(), TEST_EPS);
-
- checkPoints(Region.Location.OUTSIDE, poly,
- Vector2D.of(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY),
- Vector2D.ZERO,
- Vector2D.of(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY));
-
-
- for (double y = -1; y < 1; y += 0.1) {
- for (double x = -1; x < 1; x += 0.1) {
- EuclideanTestUtils.assertPositiveInfinity(poly.projectToBoundary(Vector2D.of(x, y)).getOffset());
- }
- }
- }
-
- @Test
- public void testInfiniteLines_single() {
- // arrange
- Line line = Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 1), TEST_PRECISION);
-
- List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>();
- boundaries.add(line.wholeHyperplane());
-
- // act
- PolygonsSet poly = new PolygonsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, poly.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(poly.getSize());
- EuclideanTestUtils.assertPositiveInfinity(poly.getBoundarySize());
- Assert.assertFalse(poly.isEmpty());
- Assert.assertFalse(poly.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, poly.getBarycenter(), TEST_EPS);
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- null,
- line.toSpace(Vector1D.of(-Float.MAX_VALUE)),
- line.toSpace(Vector1D.of(Float.MAX_VALUE))
- }
- }, poly.getVertices());
-
- checkPoints(Region.Location.OUTSIDE, poly,
- Vector2D.of(1, -1),
- Vector2D.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY));
- checkPoints(Region.Location.INSIDE, poly,
- Vector2D.of(-1, 1),
- Vector2D.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY));
- checkPoints(Region.Location.BOUNDARY, poly, Vector2D.ZERO);
- }
-
- @Test
- public void testInfiniteLines_twoIntersecting() {
- // arrange
- Line line1 = Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(1, 1), TEST_PRECISION);
- Line line2 = Line.fromPoints(Vector2D.of(1, -1), Vector2D.of(0, 0), TEST_PRECISION);
-
- List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>();
- boundaries.add(line1.wholeHyperplane());
- boundaries.add(line2.wholeHyperplane());
-
- // act
- PolygonsSet poly = new PolygonsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, poly.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(poly.getSize());
- EuclideanTestUtils.assertPositiveInfinity(poly.getBoundarySize());
- Assert.assertFalse(poly.isEmpty());
- Assert.assertFalse(poly.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, poly.getBarycenter(), TEST_EPS);
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- null,
- line2.toSpace(Vector1D.of(-Float.MAX_VALUE)),
- line2.toSpace(Vector1D.of(Float.MAX_VALUE))
- }
- }, poly.getVertices());
-
- checkPoints(Region.Location.INSIDE, poly,
- Vector2D.of(-1, 0),
- Vector2D.of(-Float.MAX_VALUE, Float.MAX_VALUE / 2.0));
- checkPoints(Region.Location.OUTSIDE, poly,
- Vector2D.of(1, 0),
- Vector2D.of(Float.MAX_VALUE, Float.MAX_VALUE / 2.0));
- checkPoints(Region.Location.BOUNDARY, poly, Vector2D.ZERO);
- }
-
- @Test
- public void testInfiniteLines_twoParallel_facingIn() {
- // arrange
- Line line1 = Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(0, 1), TEST_PRECISION);
- Line line2 = Line.fromPoints(Vector2D.of(0, -1), Vector2D.of(1, -1), TEST_PRECISION);
-
- List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>();
- boundaries.add(line1.wholeHyperplane());
- boundaries.add(line2.wholeHyperplane());
-
- // act
- PolygonsSet poly = new PolygonsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, poly.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(poly.getSize());
- EuclideanTestUtils.assertPositiveInfinity(poly.getBoundarySize());
- Assert.assertFalse(poly.isEmpty());
- Assert.assertFalse(poly.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, poly.getBarycenter(), TEST_EPS);
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- null,
- line1.toSpace(Vector1D.of(-Float.MAX_VALUE)),
- line1.toSpace(Vector1D.of(Float.MAX_VALUE))
- },
- {
- null,
- line2.toSpace(Vector1D.of(-Float.MAX_VALUE)),
- line2.toSpace(Vector1D.of(Float.MAX_VALUE))
- }
- }, poly.getVertices());
-
- checkPoints(Region.Location.INSIDE, poly,
- Vector2D.of(0, 0),
- Vector2D.of(0, 0.9),
- Vector2D.of(0, -0.9));
- checkPoints(Region.Location.OUTSIDE, poly,
- Vector2D.of(0, 1.1),
- Vector2D.of(0, -1.1));
- checkPoints(Region.Location.BOUNDARY, poly,
- Vector2D.of(0, 1),
- Vector2D.of(0, -1));
- }
-
- @Test
- public void testInfiniteLines_twoParallel_facingOut() {
- // arrange
- Line line1 = Line.fromPoints(Vector2D.of(0, 1), Vector2D.of(1, 1), TEST_PRECISION);
- Line line2 = Line.fromPoints(Vector2D.of(1, -1), Vector2D.of(0, -1), TEST_PRECISION);
-
- List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>();
- boundaries.add(line1.wholeHyperplane());
- boundaries.add(line2.wholeHyperplane());
-
- // act
- PolygonsSet poly = new PolygonsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, poly.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(poly.getSize());
- EuclideanTestUtils.assertPositiveInfinity(poly.getBoundarySize());
- Assert.assertFalse(poly.isEmpty());
- Assert.assertFalse(poly.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, poly.getBarycenter(), TEST_EPS);
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- null,
- line1.toSpace(Vector1D.of(-Float.MAX_VALUE)),
- line1.toSpace(Vector1D.of(Float.MAX_VALUE))
- },
- {
- null,
- line2.toSpace(Vector1D.of(-Float.MAX_VALUE)),
- line2.toSpace(Vector1D.of(Float.MAX_VALUE))
- }
- }, poly.getVertices());
-
- checkPoints(Region.Location.OUTSIDE, poly,
- Vector2D.of(0, 0),
- Vector2D.of(0, 0.9),
- Vector2D.of(0, -0.9));
- checkPoints(Region.Location.INSIDE, poly,
- Vector2D.of(0, 1.1),
- Vector2D.of(0, -1.1));
- checkPoints(Region.Location.BOUNDARY, poly,
- Vector2D.of(0, 1),
- Vector2D.of(0, -1));
- }
-
- @Test
- public void testMixedFiniteAndInfiniteLines_explicitInfiniteBoundaries() {
- // arrange
- Line line1 = Line.fromPoints(Vector2D.of(3, 3), Vector2D.of(0, 3), TEST_PRECISION);
- Line line2 = Line.fromPoints(Vector2D.of(0, -3), Vector2D.of(3, -3), TEST_PRECISION);
-
- List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>();
- boundaries.add(line1.wholeHyperplane());
- boundaries.add(line2.wholeHyperplane());
- boundaries.add(buildSegment(Vector2D.of(0, 3), Vector2D.of(0, -3)));
-
- // act
- PolygonsSet poly = new PolygonsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, poly.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(poly.getSize());
- EuclideanTestUtils.assertPositiveInfinity(poly.getBoundarySize());
- Assert.assertFalse(poly.isEmpty());
- Assert.assertFalse(poly.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, poly.getBarycenter(), TEST_EPS);
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- null,
- Vector2D.of(1, 3), // dummy point
- Vector2D.of(0, 3),
- Vector2D.of(0, -3),
- Vector2D.of(1, -3) // dummy point
- }
- }, poly.getVertices());
-
- checkPoints(Region.Location.INSIDE, poly,
- Vector2D.of(0.1, 2.9),
- Vector2D.of(0.1, 0),
- Vector2D.of(0.1, -2.9));
- checkPoints(Region.Location.OUTSIDE, poly,
- Vector2D.of(0, 3.1),
- Vector2D.of(-0.5, 0),
- Vector2D.of(0, -3.1));
- checkPoints(Region.Location.BOUNDARY, poly,
- Vector2D.of(3, 3),
- Vector2D.of(0, 0),
- Vector2D.of(3, -3));
- }
-
- // The polygon in this test is created from finite boundaries but the generated
- // loop still begins and ends with infinite lines. This is because the boundaries
- // used as input do not form a closed region, therefore the region itself is unclosed.
- // In other words, the boundaries used as input only define the region, not the points
- // returned from the getVertices() method.
- @Test
- public void testMixedFiniteAndInfiniteLines_impliedInfiniteBoundaries() {
- // arrange
- Line line = Line.fromPoints(Vector2D.of(3, 0), Vector2D.of(3, 3), TEST_PRECISION);
-
- List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>();
- boundaries.add(buildSegment(Vector2D.of(0, 3), Vector2D.of(0, 0)));
- boundaries.add(buildSegment(Vector2D.of(0, 0), Vector2D.of(3, 0)));
- boundaries.add(new SubLine(line, new IntervalsSet(0, Double.POSITIVE_INFINITY, TEST_PRECISION)));
-
- // act
- PolygonsSet poly = new PolygonsSet(boundaries, TEST_PRECISION);
-
- // assert
- Assert.assertSame(TEST_PRECISION, poly.getPrecision());
- EuclideanTestUtils.assertPositiveInfinity(poly.getSize());
- EuclideanTestUtils.assertPositiveInfinity(poly.getBoundarySize());
- Assert.assertFalse(poly.isEmpty());
- Assert.assertFalse(poly.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, poly.getBarycenter(), TEST_EPS);
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- null,
- Vector2D.of(0, 1), // dummy point
- Vector2D.of(0, 0),
- Vector2D.of(3, 0),
- Vector2D.of(3, 1) // dummy point
- }
- }, poly.getVertices());
-
- checkPoints(Region.Location.INSIDE, poly,
- Vector2D.of(0.1, Float.MAX_VALUE),
- Vector2D.of(0.1, 0.1),
- Vector2D.of(1.5, 0.1),
- Vector2D.of(2.9, 0.1),
- Vector2D.of(2.9, Float.MAX_VALUE));
- checkPoints(Region.Location.OUTSIDE, poly,
- Vector2D.of(-0.1, Float.MAX_VALUE),
- Vector2D.of(-0.1, 0.1),
- Vector2D.of(1.5, -0.1),
- Vector2D.of(3.1, 0.1),
- Vector2D.of(3.1, Float.MAX_VALUE));
- checkPoints(Region.Location.BOUNDARY, poly,
- Vector2D.of(0, 1),
- Vector2D.of(1, 0),
- Vector2D.of(3, 1));
- }
-
- @Test
- public void testBox() {
- // act
- PolygonsSet box = new PolygonsSet(0, 2, -1, 1, TEST_PRECISION);
-
- // assert
- Assert.assertEquals(4.0, box.getSize(), TEST_EPS);
- Assert.assertEquals(8.0, box.getBoundarySize(), TEST_EPS);
- Assert.assertFalse(box.isEmpty());
- Assert.assertFalse(box.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 0), box.getBarycenter(), TEST_EPS);
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- Vector2D.of(2, -1),
- Vector2D.of(2, 1),
- Vector2D.of(0, 1),
- Vector2D.of(0, -1)
- }
- }, box.getVertices());
-
- checkPoints(Region.Location.INSIDE, box,
- Vector2D.of(0.1, 0),
- Vector2D.of(1.9, 0),
- Vector2D.of(1, 0.9),
- Vector2D.of(1, -0.9));
- checkPoints(Region.Location.OUTSIDE, box,
- Vector2D.of(-0.1, 0),
- Vector2D.of(2.1, 0),
- Vector2D.of(1, -1.1),
- Vector2D.of(1, 1.1));
- checkPoints(Region.Location.BOUNDARY, box,
- Vector2D.of(0, 0),
- Vector2D.of(2, 0),
- Vector2D.of(1, 1),
- Vector2D.of(1, -1));
- }
-
- @Test
- public void testInvertedBox() {
- // arrange
- List<SubHyperplane<Vector2D>> boundaries = new ArrayList<SubHyperplane<Vector2D>>();
- boundaries.add(buildSegment(Vector2D.of(0, -1), Vector2D.of(0, 1)));
- boundaries.add(buildSegment(Vector2D.of(2, 1), Vector2D.of(2, -1)));
- boundaries.add(buildSegment(Vector2D.of(0, 1), Vector2D.of(2, 1)));
- boundaries.add(buildSegment(Vector2D.of(2, -1), Vector2D.of(0, -1)));
-
- // act
- PolygonsSet box = new PolygonsSet(boundaries, TEST_PRECISION);
-
- // assert
- EuclideanTestUtils.assertPositiveInfinity(box.getSize());
- Assert.assertEquals(8.0, box.getBoundarySize(), TEST_EPS);
- Assert.assertFalse(box.isEmpty());
- Assert.assertFalse(box.isFull());
- EuclideanTestUtils.assertCoordinatesEqual(Vector2D.NaN, box.getBarycenter(), TEST_EPS);
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- Vector2D.of(0, -1),
- Vector2D.of(0, 1),
- Vector2D.of(2, 1),
- Vector2D.of(2, -1)
- }
- }, box.getVertices());
-
- checkPoints(Region.Location.OUTSIDE, box,
- Vector2D.of(0.1, 0),
- Vector2D.of(1.9, 0),
- Vector2D.of(1, 0.9),
- Vector2D.of(1, -0.9));
- checkPoints(Region.Location.INSIDE, box,
- Vector2D.of(-0.1, 0),
- Vector2D.of(2.1, 0),
- Vector2D.of(1, -1.1),
- Vector2D.of(1, 1.1));
- checkPoints(Region.Location.BOUNDARY, box,
- Vector2D.of(0, 0),
- Vector2D.of(2, 0),
- Vector2D.of(1, 1),
- Vector2D.of(1, -1));
- }
-
- @Test
- public void testSimplyConnected() {
- // arrange
- Vector2D[][] vertices = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of(36.0, 22.0),
- Vector2D.of(39.0, 32.0),
- Vector2D.of(19.0, 32.0),
- Vector2D.of( 6.0, 16.0),
- Vector2D.of(31.0, 10.0),
- Vector2D.of(42.0, 16.0),
- Vector2D.of(34.0, 20.0),
- Vector2D.of(29.0, 19.0),
- Vector2D.of(23.0, 22.0),
- Vector2D.of(33.0, 25.0)
- }
- };
-
- // act
- PolygonsSet set = buildSet(vertices);
-
- // assert
- checkPoints(Region.Location.INSIDE, set,
- Vector2D.of(30.0, 15.0),
- Vector2D.of(15.0, 20.0),
- Vector2D.of(24.0, 25.0),
- Vector2D.of(35.0, 30.0),
- Vector2D.of(19.0, 17.0));
- checkPoints(Region.Location.OUTSIDE, set,
- Vector2D.of(50.0, 30.0),
- Vector2D.of(30.0, 35.0),
- Vector2D.of(10.0, 25.0),
- Vector2D.of(10.0, 10.0),
- Vector2D.of(40.0, 10.0),
- Vector2D.of(50.0, 15.0),
- Vector2D.of(30.0, 22.0));
- checkPoints(Region.Location.BOUNDARY, set,
- Vector2D.of(30.0, 32.0),
- Vector2D.of(34.0, 20.0));
-
- checkVertexLoopsEquivalent(vertices, set.getVertices());
- }
-
- @Test
- public void testStair() {
- // arrange
- Vector2D[][] vertices = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 0.0, 2.0),
- Vector2D.of(-0.1, 2.0),
- Vector2D.of(-0.1, 1.0),
- Vector2D.of(-0.3, 1.0),
- Vector2D.of(-0.3, 1.5),
- Vector2D.of(-1.3, 1.5),
- Vector2D.of(-1.3, 2.0),
- Vector2D.of(-1.8, 2.0),
- Vector2D.of(-1.8 - 1.0 / Math.sqrt(2.0),
- 2.0 - 1.0 / Math.sqrt(2.0))
- }
- };
-
- // act
- PolygonsSet set = buildSet(vertices);
-
- // assert
- checkVertexLoopsEquivalent(vertices, set.getVertices());
-
- Assert.assertEquals(1.1 + 0.95 * Math.sqrt(2.0), set.getSize(), TEST_EPS);
- }
-
- @Test
- public void testHole() {
- // arrange
- Vector2D[][] vertices = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of(0.0, 0.0),
- Vector2D.of(3.0, 0.0),
- Vector2D.of(3.0, 3.0),
- Vector2D.of(0.0, 3.0)
- }, new Vector2D[] {
- Vector2D.of(1.0, 2.0),
- Vector2D.of(2.0, 2.0),
- Vector2D.of(2.0, 1.0),
- Vector2D.of(1.0, 1.0)
- }
- };
-
- // act
- PolygonsSet set = buildSet(vertices);
-
- // assert
- checkPoints(Region.Location.INSIDE, set, new Vector2D[] {
- Vector2D.of(0.5, 0.5),
- Vector2D.of(1.5, 0.5),
- Vector2D.of(2.5, 0.5),
- Vector2D.of(0.5, 1.5),
- Vector2D.of(2.5, 1.5),
- Vector2D.of(0.5, 2.5),
- Vector2D.of(1.5, 2.5),
- Vector2D.of(2.5, 2.5),
- Vector2D.of(0.5, 1.0)
- });
- checkPoints(Region.Location.OUTSIDE, set, new Vector2D[] {
- Vector2D.of(1.5, 1.5),
- Vector2D.of(3.5, 1.0),
- Vector2D.of(4.0, 1.5),
- Vector2D.of(6.0, 6.0)
- });
- checkPoints(Region.Location.BOUNDARY, set, new Vector2D[] {
- Vector2D.of(1.0, 1.0),
- Vector2D.of(1.5, 0.0),
- Vector2D.of(1.5, 1.0),
- Vector2D.of(1.5, 2.0),
- Vector2D.of(1.5, 3.0),
- Vector2D.of(3.0, 3.0)
- });
- checkVertexLoopsEquivalent(vertices, set.getVertices());
-
- for (double x = -0.999; x < 3.999; x += 0.11) {
- Vector2D v = Vector2D.of(x, x + 0.5);
- BoundaryProjection<Vector2D> projection = set.projectToBoundary(v);
- Assert.assertTrue(projection.getOriginal() == v);
- Vector2D p = projection.getProjected();
- if (x < -0.5) {
- Assert.assertEquals(0.0, p.getX(), TEST_EPS);
- Assert.assertEquals(0.0, p.getY(), TEST_EPS);
- Assert.assertEquals(+v.distance(Vector2D.ZERO), projection.getOffset(), TEST_EPS);
- } else if (x < 0.5) {
- Assert.assertEquals(0.0, p.getX(), TEST_EPS);
- Assert.assertEquals(v.getY(), p.getY(), TEST_EPS);
- Assert.assertEquals(-v.getX(), projection.getOffset(), TEST_EPS);
- } else if (x < 1.25) {
- Assert.assertEquals(1.0, p.getX(), TEST_EPS);
- Assert.assertEquals(v.getY(), p.getY(), TEST_EPS);
- Assert.assertEquals(v.getX() - 1.0, projection.getOffset(), TEST_EPS);
- } else if (x < 2.0) {
- Assert.assertEquals(v.getX(), p.getX(), TEST_EPS);
- Assert.assertEquals(2.0, p.getY(), TEST_EPS);
- Assert.assertEquals(2.0 - v.getY(), projection.getOffset(), TEST_EPS);
- } else if (x < 3.0) {
- Assert.assertEquals(v.getX(), p.getX(), TEST_EPS);
- Assert.assertEquals(3.0, p.getY(), TEST_EPS);
- Assert.assertEquals(v.getY() - 3.0, projection.getOffset(), TEST_EPS);
- } else {
- Assert.assertEquals(3.0, p.getX(), TEST_EPS);
- Assert.assertEquals(3.0, p.getY(), TEST_EPS);
- Assert.assertEquals(+v.distance(Vector2D.of(3, 3)), projection.getOffset(), TEST_EPS);
- }
- }
- }
-
- @Test
- public void testDisjointPolygons() {
- // arrange
- Vector2D[][] vertices = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of(0.0, 1.0),
- Vector2D.of(2.0, 1.0),
- Vector2D.of(1.0, 2.0)
- }, new Vector2D[] {
- Vector2D.of(4.0, 0.0),
- Vector2D.of(5.0, 1.0),
- Vector2D.of(3.0, 1.0)
- }
- };
-
- // act
- PolygonsSet set = buildSet(vertices);
-
- // assert
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(Vector2D.of(1.0, 1.5)));
- checkPoints(Region.Location.INSIDE, set, new Vector2D[] {
- Vector2D.of(1.0, 1.5),
- Vector2D.of(4.5, 0.8)
- });
- checkPoints(Region.Location.OUTSIDE, set, new Vector2D[] {
- Vector2D.of(1.0, 0.0),
- Vector2D.of(3.5, 1.2),
- Vector2D.of(2.5, 1.0),
- Vector2D.of(3.0, 4.0)
- });
- checkPoints(Region.Location.BOUNDARY, set, new Vector2D[] {
- Vector2D.of(1.0, 1.0),
- Vector2D.of(3.5, 0.5),
- Vector2D.of(0.0, 1.0)
- });
- checkVertexLoopsEquivalent(vertices, set.getVertices());
- }
-
- @Test
- public void testOppositeHyperplanes() {
- // arrange
- Vector2D[][] vertices = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of(1.0, 0.0),
- Vector2D.of(2.0, 1.0),
- Vector2D.of(3.0, 1.0),
- Vector2D.of(2.0, 2.0),
- Vector2D.of(1.0, 1.0),
- Vector2D.of(0.0, 1.0)
- }
- };
-
- // act
- PolygonsSet set = buildSet(vertices);
-
- // assert
- checkVertexLoopsEquivalent(vertices, set.getVertices());
- }
-
- @Test
- public void testSingularPoint() {
- // arrange
- Vector2D[][] vertices = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 1.0, 0.0),
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 0.0, 1.0),
- Vector2D.of( 0.0, 0.0),
- Vector2D.of(-1.0, 0.0),
- Vector2D.of(-1.0, -1.0),
- Vector2D.of( 0.0, -1.0)
- }
- };
-
- // act
- PolygonsSet set = buildSet(vertices);
-
- // assert
- checkVertexLoopsEquivalent(new Vector2D[][] {
- {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 1.0, 0.0),
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 0.0, 1.0)
- },
- {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of(-1.0, 0.0),
- Vector2D.of(-1.0, -1.0),
- Vector2D.of( 0.0, -1.0)
- }
- }, set.getVertices());
- }
-
- @Test
- public void testLineIntersection() {
- // arrange
- Vector2D[][] vertices = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 2.0, 0.0),
- Vector2D.of( 2.0, 1.0),
- Vector2D.of( 3.0, 1.0),
- Vector2D.of( 3.0, 3.0),
- Vector2D.of( 1.0, 3.0),
- Vector2D.of( 1.0, 2.0),
- Vector2D.of( 0.0, 2.0)
- }
- };
-
- // act
- PolygonsSet set = buildSet(vertices);
-
- // assert
- Line l1 = Line.fromPointAndAngle(Vector2D.of(-1.5, 0.0), Math.PI / 4, TEST_PRECISION);
- SubLine s1 = (SubLine) set.intersection(l1.wholeHyperplane());
- List<Interval> i1 = ((IntervalsSet) s1.getRemainingRegion()).asList();
- Assert.assertEquals(2, i1.size());
- Interval v10 = i1.get(0);
- Vector2D p10Lower = l1.toSpace(Vector1D.of(v10.getInf()));
- Assert.assertEquals(0.0, p10Lower.getX(), TEST_EPS);
- Assert.assertEquals(1.5, p10Lower.getY(), TEST_EPS);
- Vector2D p10Upper = l1.toSpace(Vector1D.of(v10.getSup()));
- Assert.assertEquals(0.5, p10Upper.getX(), TEST_EPS);
- Assert.assertEquals(2.0, p10Upper.getY(), TEST_EPS);
- Interval v11 = i1.get(1);
- Vector2D p11Lower = l1.toSpace(Vector1D.of(v11.getInf()));
- Assert.assertEquals(1.0, p11Lower.getX(), TEST_EPS);
- Assert.assertEquals(2.5, p11Lower.getY(), TEST_EPS);
- Vector2D p11Upper = l1.toSpace(Vector1D.of(v11.getSup()));
- Assert.assertEquals(1.5, p11Upper.getX(), TEST_EPS);
- Assert.assertEquals(3.0, p11Upper.getY(), TEST_EPS);
-
- Line l2 = Line.fromPointAndAngle(Vector2D.of(-1.0, 2.0), 0, TEST_PRECISION);
- SubLine s2 = (SubLine) set.intersection(l2.wholeHyperplane());
- List<Interval> i2 = ((IntervalsSet) s2.getRemainingRegion()).asList();
- Assert.assertEquals(1, i2.size());
- Interval v20 = i2.get(0);
- Vector2D p20Lower = l2.toSpace(Vector1D.of(v20.getInf()));
- Assert.assertEquals(1.0, p20Lower.getX(), TEST_EPS);
- Assert.assertEquals(2.0, p20Lower.getY(), TEST_EPS);
- Vector2D p20Upper = l2.toSpace(Vector1D.of(v20.getSup()));
- Assert.assertEquals(3.0, p20Upper.getX(), TEST_EPS);
- Assert.assertEquals(2.0, p20Upper.getY(), TEST_EPS);
- }
-
- @Test
- public void testUnlimitedSubHyperplane() {
- // arrange
- Vector2D[][] vertices1 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of(0.0, 0.0),
- Vector2D.of(4.0, 0.0),
- Vector2D.of(1.4, 1.5),
- Vector2D.of(0.0, 3.5)
- }
- };
- PolygonsSet set1 = buildSet(vertices1);
- Vector2D[][] vertices2 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of(1.4, 0.2),
- Vector2D.of(2.8, -1.2),
- Vector2D.of(2.5, 0.6)
- }
- };
- PolygonsSet set2 = buildSet(vertices2);
-
- // act
- PolygonsSet set =
- (PolygonsSet) new RegionFactory<Vector2D>().union(set1.copySelf(),
- set2.copySelf());
-
- // assert
- checkVertexLoopsEquivalent(vertices1, set1.getVertices());
- checkVertexLoopsEquivalent(vertices2, set2.getVertices());
- checkVertexLoopsEquivalent(new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of(0.0, 0.0),
- Vector2D.of(1.6, 0.0),
- Vector2D.of(2.8, -1.2),
- Vector2D.of(2.6, 0.0),
- Vector2D.of(4.0, 0.0),
- Vector2D.of(1.4, 1.5),
- Vector2D.of(0.0, 3.5)
- }
- }, set.getVertices());
- }
-
- @Test
- public void testUnion() {
- // arrange
- Vector2D[][] vertices1 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 2.0, 0.0),
- Vector2D.of( 2.0, 2.0),
- Vector2D.of( 0.0, 2.0)
- }
- };
- PolygonsSet set1 = buildSet(vertices1);
- Vector2D[][] vertices2 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 3.0, 1.0),
- Vector2D.of( 3.0, 3.0),
- Vector2D.of( 1.0, 3.0)
- }
- };
- PolygonsSet set2 = buildSet(vertices2);
-
- // act
- PolygonsSet set = (PolygonsSet) new RegionFactory<Vector2D>().union(set1.copySelf(),
- set2.copySelf());
-
- // assert
- checkVertexLoopsEquivalent(vertices1, set1.getVertices());
- checkVertexLoopsEquivalent(vertices2, set2.getVertices());
- checkVertexLoopsEquivalent(new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 2.0, 0.0),
- Vector2D.of( 2.0, 1.0),
- Vector2D.of( 3.0, 1.0),
- Vector2D.of( 3.0, 3.0),
- Vector2D.of( 1.0, 3.0),
- Vector2D.of( 1.0, 2.0),
- Vector2D.of( 0.0, 2.0)
- }
- }, set.getVertices());
-
- checkPoints(Region.Location.INSIDE, set, new Vector2D[] {
- Vector2D.of(1.0, 1.0),
- Vector2D.of(0.5, 0.5),
- Vector2D.of(2.0, 2.0),
- Vector2D.of(2.5, 2.5),
- Vector2D.of(0.5, 1.5),
- Vector2D.of(1.5, 1.5),
- Vector2D.of(1.5, 0.5),
- Vector2D.of(1.5, 2.5),
- Vector2D.of(2.5, 1.5),
- Vector2D.of(2.5, 2.5)
- });
- checkPoints(Region.Location.OUTSIDE, set, new Vector2D[] {
- Vector2D.of(-0.5, 0.5),
- Vector2D.of( 0.5, 2.5),
- Vector2D.of( 2.5, 0.5),
- Vector2D.of( 3.5, 2.5)
- });
- checkPoints(Region.Location.BOUNDARY, set, new Vector2D[] {
- Vector2D.of(0.0, 0.0),
- Vector2D.of(0.5, 2.0),
- Vector2D.of(2.0, 0.5),
- Vector2D.of(2.5, 1.0),
- Vector2D.of(3.0, 2.5)
- });
- }
-
- @Test
- public void testIntersection() {
- // arrange
- Vector2D[][] vertices1 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 2.0, 0.0),
- Vector2D.of( 2.0, 2.0),
- Vector2D.of( 0.0, 2.0)
- }
- };
- PolygonsSet set1 = buildSet(vertices1);
- Vector2D[][] vertices2 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 3.0, 1.0),
- Vector2D.of( 3.0, 3.0),
- Vector2D.of( 1.0, 3.0)
- }
- };
- PolygonsSet set2 = buildSet(vertices2);
-
- // act
- PolygonsSet set = (PolygonsSet) new RegionFactory<Vector2D>().intersection(set1.copySelf(),
- set2.copySelf());
-
- // assert
- checkVertexLoopsEquivalent(vertices1, set1.getVertices());
- checkVertexLoopsEquivalent(vertices2, set2.getVertices());
- checkVertexLoopsEquivalent(new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 2.0, 1.0),
- Vector2D.of( 2.0, 2.0),
- Vector2D.of( 1.0, 2.0)
- }
- }, set.getVertices());
-
- checkPoints(Region.Location.INSIDE, set, new Vector2D[] {
- Vector2D.of(1.5, 1.5)
- });
- checkPoints(Region.Location.OUTSIDE, set, new Vector2D[] {
- Vector2D.of(0.5, 1.5),
- Vector2D.of(2.5, 1.5),
- Vector2D.of(1.5, 0.5),
- Vector2D.of(0.5, 0.5)
- });
- checkPoints(Region.Location.BOUNDARY, set, new Vector2D[] {
- Vector2D.of(1.0, 1.0),
- Vector2D.of(2.0, 2.0),
- Vector2D.of(1.0, 1.5),
- Vector2D.of(1.5, 2.0)
- });
- }
-
- @Test
- public void testXor() {
- // arrange
- Vector2D[][] vertices1 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 2.0, 0.0),
- Vector2D.of( 2.0, 2.0),
- Vector2D.of( 0.0, 2.0)
- }
- };
- PolygonsSet set1 = buildSet(vertices1);
- Vector2D[][] vertices2 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 3.0, 1.0),
- Vector2D.of( 3.0, 3.0),
- Vector2D.of( 1.0, 3.0)
- }
- };
- PolygonsSet set2 = buildSet(vertices2);
-
- // act
- PolygonsSet set = (PolygonsSet) new RegionFactory<Vector2D>().xor(set1.copySelf(),
- set2.copySelf());
-
- // assert
- checkVertexLoopsEquivalent(vertices1, set1.getVertices());
- checkVertexLoopsEquivalent(vertices2, set2.getVertices());
- checkVertexLoopsEquivalent(new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 2.0, 0.0),
- Vector2D.of( 2.0, 1.0),
- Vector2D.of( 3.0, 1.0),
- Vector2D.of( 3.0, 3.0),
- Vector2D.of( 1.0, 3.0),
- Vector2D.of( 1.0, 2.0),
- Vector2D.of( 0.0, 2.0)
- },
- new Vector2D[] {
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 1.0, 2.0),
- Vector2D.of( 2.0, 2.0),
- Vector2D.of( 2.0, 1.0)
- }
- }, set.getVertices());
-
- checkPoints(Region.Location.INSIDE, set, new Vector2D[] {
- Vector2D.of(0.5, 0.5),
- Vector2D.of(2.5, 2.5),
- Vector2D.of(0.5, 1.5),
- Vector2D.of(1.5, 0.5),
- Vector2D.of(1.5, 2.5),
- Vector2D.of(2.5, 1.5),
- Vector2D.of(2.5, 2.5)
- });
- checkPoints(Region.Location.OUTSIDE, set, new Vector2D[] {
- Vector2D.of(-0.5, 0.5),
- Vector2D.of( 0.5, 2.5),
- Vector2D.of( 2.5, 0.5),
- Vector2D.of( 1.5, 1.5),
- Vector2D.of( 3.5, 2.5)
- });
- checkPoints(Region.Location.BOUNDARY, set, new Vector2D[] {
- Vector2D.of(1.0, 1.0),
- Vector2D.of(2.0, 2.0),
- Vector2D.of(1.5, 1.0),
- Vector2D.of(2.0, 1.5),
- Vector2D.of(0.0, 0.0),
- Vector2D.of(0.5, 2.0),
- Vector2D.of(2.0, 0.5),
- Vector2D.of(2.5, 1.0),
- Vector2D.of(3.0, 2.5)
- });
- }
-
- @Test
- public void testDifference() {
- // arrange
- Vector2D[][] vertices1 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 2.0, 0.0),
- Vector2D.of( 2.0, 2.0),
- Vector2D.of( 0.0, 2.0)
- }
- };
- PolygonsSet set1 = buildSet(vertices1);
- Vector2D[][] vertices2 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 3.0, 1.0),
- Vector2D.of( 3.0, 3.0),
- Vector2D.of( 1.0, 3.0)
- }
- };
- PolygonsSet set2 = buildSet(vertices2);
-
- // act
- PolygonsSet set = (PolygonsSet) new RegionFactory<Vector2D>().difference(set1.copySelf(),
- set2.copySelf());
-
- // assert
- checkVertexLoopsEquivalent(vertices1, set1.getVertices());
- checkVertexLoopsEquivalent(vertices2, set2.getVertices());
- checkVertexLoopsEquivalent(new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.0, 0.0),
- Vector2D.of( 2.0, 0.0),
- Vector2D.of( 2.0, 1.0),
- Vector2D.of( 1.0, 1.0),
- Vector2D.of( 1.0, 2.0),
- Vector2D.of( 0.0, 2.0)
- }
- }, set.getVertices());
-
- checkPoints(Region.Location.INSIDE, set, new Vector2D[] {
- Vector2D.of(0.5, 0.5),
- Vector2D.of(0.5, 1.5),
- Vector2D.of(1.5, 0.5)
- });
- checkPoints(Region.Location.OUTSIDE, set, new Vector2D[] {
- Vector2D.of( 2.5, 2.5),
- Vector2D.of(-0.5, 0.5),
- Vector2D.of( 0.5, 2.5),
- Vector2D.of( 2.5, 0.5),
- Vector2D.of( 1.5, 1.5),
- Vector2D.of( 3.5, 2.5),
- Vector2D.of( 1.5, 2.5),
- Vector2D.of( 2.5, 1.5),
- Vector2D.of( 2.0, 1.5),
- Vector2D.of( 2.0, 2.0),
- Vector2D.of( 2.5, 1.0),
- Vector2D.of( 2.5, 2.5),
- Vector2D.of( 3.0, 2.5)
- });
- checkPoints(Region.Location.BOUNDARY, set, new Vector2D[] {
- Vector2D.of(1.0, 1.0),
- Vector2D.of(1.5, 1.0),
- Vector2D.of(0.0, 0.0),
- Vector2D.of(0.5, 2.0),
- Vector2D.of(2.0, 0.5)
- });
- }
-
- @Test
- public void testEmptyDifference() {
- // arrange
- Vector2D[][] vertices1 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.5, 3.5),
- Vector2D.of( 0.5, 4.5),
- Vector2D.of(-0.5, 4.5),
- Vector2D.of(-0.5, 3.5)
- }
- };
- PolygonsSet set1 = buildSet(vertices1);
- Vector2D[][] vertices2 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 1.0, 2.0),
- Vector2D.of( 1.0, 8.0),
- Vector2D.of(-1.0, 8.0),
- Vector2D.of(-1.0, 2.0)
- }
- };
- PolygonsSet set2 = buildSet(vertices2);
-
- // act
- PolygonsSet diff = (PolygonsSet) new RegionFactory<Vector2D>().difference(set1.copySelf(), set2.copySelf());
-
- // assert
- Assert.assertEquals(0.0, diff.getSize(), TEST_EPS);
- Assert.assertTrue(diff.isEmpty());
- }
-
- @Test
- public void testChoppedHexagon() {
- // arrange
- double pi6 = Math.PI / 6.0;
- double sqrt3 = Math.sqrt(3.0);
- SubLine[] hyp = {
- Line.fromPointAndAngle(Vector2D.of( 0.0, 1.0), 5 * pi6, TEST_PRECISION).wholeHyperplane(),
- Line.fromPointAndAngle(Vector2D.of(-sqrt3, 1.0), 7 * pi6, TEST_PRECISION).wholeHyperplane(),
- Line.fromPointAndAngle(Vector2D.of(-sqrt3, 1.0), 9 * pi6, TEST_PRECISION).wholeHyperplane(),
- Line.fromPointAndAngle(Vector2D.of(-sqrt3, 0.0), 11 * pi6, TEST_PRECISION).wholeHyperplane(),
- Line.fromPointAndAngle(Vector2D.of( 0.0, 0.0), 13 * pi6, TEST_PRECISION).wholeHyperplane(),
- Line.fromPointAndAngle(Vector2D.of( 0.0, 1.0), 3 * pi6, TEST_PRECISION).wholeHyperplane(),
- Line.fromPointAndAngle(Vector2D.of(-5.0 * sqrt3 / 6.0, 0.0), 9 * pi6, TEST_PRECISION).wholeHyperplane()
- };
- hyp[1] = (SubLine) hyp[1].split(hyp[0].getHyperplane()).getMinus();
- hyp[2] = (SubLine) hyp[2].split(hyp[1].getHyperplane()).getMinus();
- hyp[3] = (SubLine) hyp[3].split(hyp[2].getHyperplane()).getMinus();
- hyp[4] = (SubLine) hyp[4].split(hyp[3].getHyperplane()).getMinus().split(hyp[0].getHyperplane()).getMinus();
- hyp[5] = (SubLine) hyp[5].split(hyp[4].getHyperplane()).getMinus().split(hyp[0].getHyperplane()).getMinus();
- hyp[6] = (SubLine) hyp[6].split(hyp[3].getHyperplane()).getMinus().split(hyp[1].getHyperplane()).getMinus();
- BSPTree<Vector2D> tree = new BSPTree<>(Boolean.TRUE);
- for (int i = hyp.length - 1; i >= 0; --i) {
- tree = new BSPTree<>(hyp[i], new BSPTree<Vector2D>(Boolean.FALSE), tree, null);
- }
- PolygonsSet set = new PolygonsSet(tree, TEST_PRECISION);
- SubLine splitter =
- Line.fromPointAndAngle(Vector2D.of(-2.0 * sqrt3 / 3.0, 0.0), 9 * pi6, TEST_PRECISION).wholeHyperplane();
-
- // act
- PolygonsSet slice =
- new PolygonsSet(new BSPTree<>(splitter,
- set.getTree(false).split(splitter).getPlus(),
- new BSPTree<Vector2D>(Boolean.FALSE), null),
- TEST_PRECISION);
-
- // assert
- Assert.assertEquals(Region.Location.OUTSIDE,
- slice.checkPoint(Vector2D.of(0.1, 0.5)));
- Assert.assertEquals(11.0 / 3.0, slice.getBoundarySize(), TEST_EPS);
- }
-
- @Test
- public void testConcentric() {
- // arrange
- double h = Math.sqrt(3.0) / 2.0;
- Vector2D[][] vertices1 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.00, 0.1 * h),
- Vector2D.of( 0.05, 0.1 * h),
- Vector2D.of( 0.10, 0.2 * h),
- Vector2D.of( 0.05, 0.3 * h),
- Vector2D.of(-0.05, 0.3 * h),
- Vector2D.of(-0.10, 0.2 * h),
- Vector2D.of(-0.05, 0.1 * h)
- }
- };
- PolygonsSet set1 = buildSet(vertices1);
- Vector2D[][] vertices2 = new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of( 0.00, 0.0 * h),
- Vector2D.of( 0.10, 0.0 * h),
- Vector2D.of( 0.20, 0.2 * h),
- Vector2D.of( 0.10, 0.4 * h),
- Vector2D.of(-0.10, 0.4 * h),
- Vector2D.of(-0.20, 0.2 * h),
- Vector2D.of(-0.10, 0.0 * h)
- }
- };
- PolygonsSet set2 = buildSet(vertices2);
-
- // act/assert
- Assert.assertTrue(set2.contains(set1));
- }
-
- @Test
- public void testBug20040520() {
- // arrange
- BSPTree<Vector2D> a0 =
- new BSPTree<>(buildSegment(Vector2D.of(0.85, -0.05),
- Vector2D.of(0.90, -0.10)),
- new BSPTree<Vector2D>(Boolean.FALSE),
- new BSPTree<Vector2D>(Boolean.TRUE),
- null);
- BSPTree<Vector2D> a1 =
- new BSPTree<>(buildSegment(Vector2D.of(0.85, -0.10),
- Vector2D.of(0.90, -0.10)),
- new BSPTree<Vector2D>(Boolean.FALSE), a0, null);
- BSPTree<Vector2D> a2 =
- new BSPTree<>(buildSegment(Vector2D.of(0.90, -0.05),
- Vector2D.of(0.85, -0.05)),
- new BSPTree<Vector2D>(Boolean.FALSE), a1, null);
- BSPTree<Vector2D> a3 =
- new BSPTree<>(buildSegment(Vector2D.of(0.82, -0.05),
- Vector2D.of(0.82, -0.08)),
- new BSPTree<Vector2D>(Boolean.FALSE),
- new BSPTree<Vector2D>(Boolean.TRUE),
- null);
- BSPTree<Vector2D> a4 =
- new BSPTree<>(buildHalfLine(Vector2D.of(0.85, -0.05),
- Vector2D.of(0.80, -0.05),
- false),
- new BSPTree<Vector2D>(Boolean.FALSE), a3, null);
- BSPTree<Vector2D> a5 =
- new BSPTree<>(buildSegment(Vector2D.of(0.82, -0.08),
- Vector2D.of(0.82, -0.18)),
- new BSPTree<Vector2D>(Boolean.FALSE),
- new BSPTree<Vector2D>(Boolean.TRUE),
- null);
- BSPTree<Vector2D> a6 =
- new BSPTree<>(buildHalfLine(Vector2D.of(0.82, -0.18),
- Vector2D.of(0.85, -0.15),
- true),
- new BSPTree<Vector2D>(Boolean.FALSE), a5, null);
- BSPTree<Vector2D> a7 =
- new BSPTree<>(buildHalfLine(Vector2D.of(0.85, -0.05),
- Vector2D.of(0.82, -0.08),
- false),
- a4, a6, null);
- BSPTree<Vector2D> a8 =
- new BSPTree<>(buildLine(Vector2D.of(0.85, -0.25),
- Vector2D.of(0.85, 0.05)),
- a2, a7, null);
- BSPTree<Vector2D> a9 =
- new BSPTree<>(buildLine(Vector2D.of(0.90, 0.05),
- Vector2D.of(0.90, -0.50)),
- a8, new BSPTree<Vector2D>(Boolean.FALSE), null);
-
- BSPTree<Vector2D> b0 =
- new BSPTree<>(buildSegment(Vector2D.of(0.92, -0.12),
- Vector2D.of(0.92, -0.08)),
- new BSPTree<Vector2D>(Boolean.FALSE), new BSPTree<Vector2D>(Boolean.TRUE),
- null);
- BSPTree<Vector2D> b1 =
- new BSPTree<>(buildHalfLine(Vector2D.of(0.92, -0.08),
- Vector2D.of(0.90, -0.10),
- true),
- new BSPTree<Vector2D>(Boolean.FALSE), b0, null);
- BSPTree<Vector2D> b2 =
- new BSPTree<>(buildSegment(Vector2D.of(0.92, -0.18),
- Vector2D.of(0.92, -0.12)),
- new BSPTree<Vector2D>(Boolean.FALSE), new BSPTree<Vector2D>(Boolean.TRUE),
- null);
- BSPTree<Vector2D> b3 =
- new BSPTree<>(buildSegment(Vector2D.of(0.85, -0.15),
- Vector2D.of(0.90, -0.20)),
- new BSPTree<Vector2D>(Boolean.FALSE), b2, null);
- BSPTree<Vector2D> b4 =
- new BSPTree<>(buildSegment(Vector2D.of(0.95, -0.15),
- Vector2D.of(0.85, -0.05)),
- b1, b3, null);
- BSPTree<Vector2D> b5 =
- new BSPTree<>(buildHalfLine(Vector2D.of(0.85, -0.05),
- Vector2D.of(0.85, -0.25),
- true),
- new BSPTree<Vector2D>(Boolean.FALSE), b4, null);
- BSPTree<Vector2D> b6 =
- new BSPTree<>(buildLine(Vector2D.of(0.0, -1.10),
- Vector2D.of(1.0, -0.10)),
- new BSPTree<Vector2D>(Boolean.FALSE), b5, null);
-
- // act
- PolygonsSet c =
- (PolygonsSet) new RegionFactory<Vector2D>().union(new PolygonsSet(a9, TEST_PRECISION),
- new PolygonsSet(b6, TEST_PRECISION));
-
- // assert
- checkPoints(Region.Location.INSIDE, c, new Vector2D[] {
- Vector2D.of(0.83, -0.06),
- Vector2D.of(0.83, -0.15),
- Vector2D.of(0.88, -0.15),
- Vector2D.of(0.88, -0.09),
- Vector2D.of(0.88, -0.07),
- Vector2D.of(0.91, -0.18),
- Vector2D.of(0.91, -0.10)
- });
-
- checkPoints(Region.Location.OUTSIDE, c, new Vector2D[] {
- Vector2D.of(0.80, -0.10),
- Vector2D.of(0.83, -0.50),
- Vector2D.of(0.83, -0.20),
- Vector2D.of(0.83, -0.02),
- Vector2D.of(0.87, -0.50),
- Vector2D.of(0.87, -0.20),
- Vector2D.of(0.87, -0.02),
- Vector2D.of(0.91, -0.20),
- Vector2D.of(0.91, -0.08),
- Vector2D.of(0.93, -0.15)
- });
-
- checkVertexLoopsEquivalent(new Vector2D[][] {
- new Vector2D[] {
- Vector2D.of(0.85, -0.15),
- Vector2D.of(0.90, -0.20),
- Vector2D.of(0.92, -0.18),
- Vector2D.of(0.92, -0.08),
- Vector2D.of(0.90, -0.10),
- Vector2D.of(0.90, -0.05),
- Vector2D.of(0.82, -0.05),
- Vector2D.of(0.82, -0.18),
- }
- }, c.getVertices());
- }
-
- @Test
- public void testBug20041003() {
- // arrange
- Line[] l = {
- Line.fromPoints(Vector2D.of(0.0, 0.625000007541172),
- Vector2D.of(1.0, 0.625000007541172), TEST_PRECISION),
- Line.fromPoints(Vector2D.of(-0.19204433621902645, 0.0),
- Vector2D.of(-0.19204433621902645, 1.0), TEST_PRECISION),
- Line.fromPoints(Vector2D.of(-0.40303524786887, 0.4248364535319128),
- Vector2D.of(-1.12851149797877, -0.2634107480798909), TEST_PRECISION),
- Line.fromPoints(Vector2D.of(0.0, 2.0),
- Vector2D.of(1.0, 2.0), TEST_PRECISION)
- };
-
- BSPTree<Vector2D> node1 =
- new BSPTree<>(new SubLine(l[0],
- new IntervalsSet(intersectionAbscissa(l[0], l[1]),
- intersectionAbscissa(l[0], l[2]),
- TEST_PRECISION)),
- new BSPTree<Vector2D>(Boolean.TRUE),
- new BSPTree<Vector2D>(Boolean.FALSE),
- null);
- BSPTree<Vector2D> node2 =
- new BSPTree<>(new SubLine(l[1],
- new IntervalsSet(intersectionAbscissa(l[1], l[2]),
- intersectionAbscissa(l[1], l[3]),
- TEST_PRECISION)),
- node1,
- new BSPTree<Vector2D>(Boolean.FALSE),
- null);
- BSPTree<Vector2D> node3 =
- new BSPTree<>(new SubLine(l[2],
- new IntervalsSet(intersectionAbscissa(l[2], l[3]),
- Double.POSITIVE_INFINITY, TEST_PRECISION)),
- node2,
- new BSPTree<Vector2D>(Boolean.FALSE),
- null);
- BSPTree<Vector2D> node4 =
- new BSPTree<>(l[3].wholeHyperplane(),
- node3,
- new BSPTree<Vector2D>(Boolean.FALSE),
- null);
-
- // act
- PolygonsSet set = new PolygonsSet(node4, TEST_PRECISION);
-
- // assert
- Assert.assertEquals(0, set.getVertices().length);
- }
-
- @Test
- public void testSqueezedHexa() {
- // act
- PolygonsSet set = new PolygonsSet(TEST_PRECISION,
- Vector2D.of(-6, -4), Vector2D.of(-8, -8), Vector2D.of( 8, -8),
- Vector2D.of( 6, -4), Vector2D.of(10, 4), Vector2D.of(-10, 4));
-
- // assert
- Assert.assertEquals(Location.OUTSIDE, set.checkPoint(Vector2D.of(0, 6)));
- }
-
- @Test
- public void testIssue880Simplified() {
- // arrange
- Vector2D[] vertices1 = new Vector2D[] {
- Vector2D.of( 90.13595870833188, 38.33604606376991),
- Vector2D.of( 90.14047850603913, 38.34600084496253),
- Vector2D.of( 90.11045289492762, 38.36801537312368),
- Vector2D.of( 90.10871471476526, 38.36878044144294),
- Vector2D.of( 90.10424901707671, 38.374300101757),
- Vector2D.of( 90.0979455456843, 38.373578376172475),
- Vector2D.of( 90.09081227075944, 38.37526295920463),
- Vector2D.of( 90.09081378927135, 38.375193883266434)
- };
-
- // act
- PolygonsSet set1 = new PolygonsSet(TEST_PRECISION, vertices1);
-
- // assert
- Assert.assertEquals(Location.OUTSIDE, set1.checkPoint(Vector2D.of(90.12, 38.32)));
- Assert.assertEquals(Location.OUTSIDE, set1.checkPoint(Vector2D.of(90.135, 38.355)));
-
- }
-
- @Test
- public void testIssue880Complete() {
- Vector2D[] vertices1 = new Vector2D[] {
- Vector2D.of( 90.08714908223715, 38.370299337260235),
- Vector2D.of( 90.08709517675004, 38.3702895991413),
- Vector2D.of( 90.08401538704919, 38.368849330127944),
- Vector2D.of( 90.08258210430711, 38.367634558585564),
- Vector2D.of( 90.08251455106665, 38.36763409247078),
- Vector2D.of( 90.08106599752608, 38.36761621664249),
- Vector2D.of( 90.08249585300035, 38.36753627557965),
- Vector2D.of( 90.09075743352184, 38.35914647644972),
- Vector2D.of( 90.09099945896571, 38.35896264724079),
- Vector2D.of( 90.09269383800086, 38.34595756121246),
- Vector2D.of( 90.09638631543191, 38.3457988093121),
- Vector2D.of( 90.09666417351019, 38.34523360999418),
- Vector2D.of( 90.1297082145872, 38.337670454923625),
- Vector2D.of( 90.12971687748956, 38.337669827794684),
- Vector2D.of( 90.1240820219179, 38.34328502001131),
- Vector2D.of( 90.13084259656404, 38.34017811765017),
- Vector2D.of( 90.13378567942857, 38.33860579180606),
- Vector2D.of( 90.13519557833206, 38.33621054663689),
- Vector2D.of( 90.13545616732307, 38.33614965452864),
- Vector2D.of( 90.13553111202748, 38.33613962818305),
- Vector2D.of( 90.1356903436448, 38.33610227127048),
- Vector2D.of( 90.13576283227428, 38.33609255422783),
- Vector2D.of( 90.13595870833188, 38.33604606376991),
- Vector2D.of( 90.1361556630693, 38.3360024198866),
- Vector2D.of( 90.13622408795709, 38.335987048115726),
- Vector2D.of( 90.13696189099994, 38.33581914328681),
- Vector2D.of( 90.13746655304897, 38.33616706665265),
- Vector2D.of( 90.13845973716064, 38.33650776167099),
- Vector2D.of( 90.13950901827667, 38.3368469456463),
- Vector2D.of( 90.14393814424852, 38.337591835857495),
- Vector2D.of( 90.14483839716831, 38.337076122362475),
- Vector2D.of( 90.14565474433601, 38.33769000964429),
- Vector2D.of( 90.14569421179482, 38.3377117256905),
- Vector2D.of( 90.14577067124333, 38.33770883625908),
- Vector2D.of( 90.14600350631684, 38.337714326520995),
- Vector2D.of( 90.14600355139731, 38.33771435193319),
- Vector2D.of( 90.14600369112401, 38.33771443882085),
- Vector2D.of( 90.14600382486884, 38.33771453466096),
- Vector2D.of( 90.14600395205912, 38.33771463904344),
- Vector2D.of( 90.14600407214999, 38.337714751520764),
- Vector2D.of( 90.14600418462749, 38.337714871611695),
- Vector2D.of( 90.14600422249327, 38.337714915811034),
- Vector2D.of( 90.14867838361471, 38.34113888210675),
- Vector2D.of( 90.14923750157374, 38.341582537502575),
- Vector2D.of( 90.14877083250991, 38.34160685841391),
- Vector2D.of( 90.14816667319519, 38.34244232585684),
- Vector2D.of( 90.14797696744586, 38.34248455284745),
- Vector2D.of( 90.14484318014337, 38.34385573215269),
- Vector2D.of( 90.14477919958296, 38.3453797747614),
- Vector2D.of( 90.14202393306448, 38.34464324839456),
- Vector2D.of( 90.14198920640195, 38.344651155237216),
- Vector2D.of( 90.14155207025175, 38.34486424263724),
- Vector2D.of( 90.1415196143314, 38.344871730519),
- Vector2D.of( 90.14128611910814, 38.34500196593859),
- Vector2D.of( 90.14047850603913, 38.34600084496253),
- Vector2D.of( 90.14045907000337, 38.34601860032171),
- Vector2D.of( 90.14039496493928, 38.346223030432384),
- Vector2D.of( 90.14037626063737, 38.346240203360026),
- Vector2D.of( 90.14030005823724, 38.34646920000705),
- Vector2D.of( 90.13799164754806, 38.34903093011013),
- Vector2D.of( 90.11045289492762, 38.36801537312368),
- Vector2D.of( 90.10871471476526, 38.36878044144294),
- Vector2D.of( 90.10424901707671, 38.374300101757),
- Vector2D.of( 90.10263482039932, 38.37310041316073),
- Vector2D.of( 90.09834601753448, 38.373615053823414),
- Vector2D.of( 90.0979455456843, 38.373578376172475),
- Vector2D.of( 90.09086514328669, 38.37527884194668),
- Vector2D.of( 90.09084931407364, 38.37590801712463),
- Vector2D.of( 90.09081227075944, 38.37526295920463),
- Vector2D.of( 90.09081378927135, 38.375193883266434)
- };
- PolygonsSet set1 = new PolygonsSet(TEST_PRECISION, vertices1);
- Assert.assertEquals(Location.OUTSIDE, set1.checkPoint(Vector2D.of(90.0905, 38.3755)));
- Assert.assertEquals(Location.INSIDE, set1.checkPoint(Vector2D.of(90.09084, 38.3755)));
- Assert.assertEquals(Location.OUTSIDE, set1.checkPoint(Vector2D.of(90.0913, 38.3755)));
- Assert.assertEquals(Location.INSIDE, set1.checkPoint(Vector2D.of(90.1042, 38.3739)));
- Assert.assertEquals(Location.INSIDE, set1.checkPoint(Vector2D.of(90.1111, 38.3673)));
- Assert.assertEquals(Location.OUTSIDE, set1.checkPoint(Vector2D.of(90.0959, 38.3457)));
-
- Vector2D[] vertices2 = new Vector2D[] {
- Vector2D.of( 90.13067558880044, 38.36977255037573),
- Vector2D.of( 90.12907570488, 38.36817308242706),
- Vector2D.of( 90.1342774136516, 38.356886880294724),
- Vector2D.of( 90.13090330629757, 38.34664392676211),
- Vector2D.of( 90.13078571364593, 38.344904617518466),
- Vector2D.of( 90.1315602208914, 38.3447185040846),
- Vector2D.of( 90.1316336226821, 38.34470643148342),
- Vector2D.of( 90.134020944832, 38.340936644972885),
- Vector2D.of( 90.13912536387306, 38.335497255122334),
- Vector2D.of( 90.1396178806582, 38.334878075552126),
- Vector2D.of( 90.14083049696671, 38.33316530644106),
- Vector2D.of( 90.14145252901329, 38.33152722916191),
- Vector2D.of( 90.1404779335565, 38.32863516047786),
- Vector2D.of( 90.14282712131586, 38.327504432532066),
- Vector2D.of( 90.14616669875488, 38.3237354115015),
- Vector2D.of( 90.14860976050608, 38.315714862457924),
- Vector2D.of( 90.14999277782437, 38.3164932507504),
- Vector2D.of( 90.15005207194997, 38.316534677663356),
- Vector2D.of( 90.15508513859612, 38.31878731691609),
- Vector2D.of( 90.15919938519221, 38.31852743183782),
- Vector2D.of( 90.16093758658837, 38.31880662005153),
- Vector2D.of( 90.16099420184912, 38.318825953291594),
- Vector2D.of( 90.1665411125756, 38.31859497874757),
- Vector2D.of( 90.16999653861313, 38.32505772048029),
- Vector2D.of( 90.17475243391698, 38.32594398441148),
- Vector2D.of( 90.17940844844992, 38.327427213761325),
- Vector2D.of( 90.20951909541378, 38.330616833491774),
- Vector2D.of( 90.2155400467941, 38.331746223670336),
- Vector2D.of( 90.21559881391778, 38.33175551425302),
- Vector2D.of( 90.21916646426041, 38.332584299620805),
- Vector2D.of( 90.23863749852285, 38.34778978875795),
- Vector2D.of( 90.25459855175802, 38.357790570608984),
- Vector2D.of( 90.25964298227257, 38.356918010203174),
- Vector2D.of( 90.26024593994703, 38.361692743151366),
- Vector2D.of( 90.26146187570015, 38.36311080550837),
- Vector2D.of( 90.26614159359622, 38.36510808579902),
- Vector2D.of( 90.26621342936448, 38.36507942500333),
- Vector2D.of( 90.26652190211962, 38.36494042196722),
- Vector2D.of( 90.26621240678867, 38.365113172030874),
- Vector2D.of( 90.26614057102057, 38.365141832826794),
- Vector2D.of( 90.26380080055299, 38.3660381760273),
- Vector2D.of( 90.26315345241, 38.36670658276421),
- Vector2D.of( 90.26251574942881, 38.367490323488084),
- Vector2D.of( 90.26247873448426, 38.36755266444749),
- Vector2D.of( 90.26234628016698, 38.36787989125406),
- Vector2D.of( 90.26214559424784, 38.36945909356126),
- Vector2D.of( 90.25861728442555, 38.37200753430875),
- Vector2D.of( 90.23905557537864, 38.375405314295904),
- Vector2D.of( 90.22517251874075, 38.38984691662256),
- Vector2D.of( 90.22549955153215, 38.3911564273979),
- Vector2D.of( 90.22434386063355, 38.391476432092134),
- Vector2D.of( 90.22147729457276, 38.39134652252034),
- Vector2D.of( 90.22142070120117, 38.391349167741964),
- Vector2D.of( 90.20665060751588, 38.39475580900313),
- Vector2D.of( 90.20042268367109, 38.39842558622888),
- Vector2D.of( 90.17423771242085, 38.402727751805344),
- Vector2D.of( 90.16756796257476, 38.40913898597597),
- Vector2D.of( 90.16728283954308, 38.411255399912875),
- Vector2D.of( 90.16703538220418, 38.41136059866693),
- Vector2D.of( 90.16725865657685, 38.41013618805954),
- Vector2D.of( 90.16746107640665, 38.40902614307544),
- Vector2D.of( 90.16122795307462, 38.39773101873203)
- };
- PolygonsSet set2 = new PolygonsSet(TEST_PRECISION, vertices2);
- PolygonsSet set = (PolygonsSet) new
- RegionFactory<Vector2D>().difference(set1.copySelf(),
- set2.copySelf());
-
- Vector2D[][] vertices = set.getVertices();
- Assert.assertTrue(vertices[0][0] != null);
- Assert.assertEquals(1, vertices.length);
- }
-
- @Test
- public void testTooThinBox() {
- // act/assert
- Assert.assertEquals(0.0,
- new PolygonsSet(0.0, 0.0, 0.0, 10.3206397147574, TEST_PRECISION).getSize(),
- TEST_EPS);
- }
-
- @Test
- public void testWrongUsage() {
- // the following is a wrong usage of the constructor.
- // as explained in the javadoc, the failure is NOT detected at construction
- // time but occurs later on
- PolygonsSet ps = new PolygonsSet(new BSPTree<Vector2D>(), TEST_PRECISION);
- Assert.assertNotNull(ps);
- try {
- ps.getSize();
- Assert.fail("an exception should have been thrown");
- } catch (NullPointerException npe) {
- // this is expected
- }
- }
-
- @Test
- public void testIssue1162() {
- // arrange
- PolygonsSet p = new PolygonsSet(TEST_PRECISION,
- Vector2D.of(4.267199999996532, -11.928637756014894),
- Vector2D.of(4.267200000026445, -14.12360595809307),
- Vector2D.of(9.144000000273694, -14.12360595809307),
- Vector2D.of(9.144000000233383, -11.928637756020067));
-
- PolygonsSet w = new PolygonsSet(TEST_PRECISION,
- Vector2D.of(2.56735636510452512E-9, -11.933116461089332),
- Vector2D.of(2.56735636510452512E-9, -12.393225665247766),
- Vector2D.of(2.56735636510452512E-9, -27.785625665247778),
- Vector2D.of(4.267200000030211, -27.785625665247778),
- Vector2D.of(4.267200000030211, -11.933116461089332));
-
- // act/assert
- Assert.assertFalse(p.contains(w));
- }
-
- @Test
- public void testThinRectangle_toleranceLessThanWidth_resultIsAccurate() {
- // if tolerance is smaller than rectangle width, the rectangle is computed accurately
-
- // arrange
- RegionFactory<Vector2D> factory = new RegionFactory<>();
- Vector2D pA = Vector2D.of(0.0, 1.0);
- Vector2D pB = Vector2D.of(0.0, 0.0);
- Vector2D pC = Vector2D.of(1.0 / 64.0, 0.0);
- Vector2D pD = Vector2D.of(1.0 / 64.0, 1.0);
-
- // if tolerance is smaller than rectangle width, the rectangle is computed accurately
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1.0 / 256);
- Hyperplane<Vector2D>[] h1 = new Line[] {
- Line.fromPoints(pA, pB, precision),
- Line.fromPoints(pB, pC, precision),
- Line.fromPoints(pC, pD, precision),
- Line.fromPoints(pD, pA, precision)
- };
-
- // act
- Region<Vector2D> accuratePolygon = factory.buildConvex(h1);
-
- // assert
- Assert.assertEquals(1.0 / 64.0, accuratePolygon.getSize(), TEST_EPS);
- EuclideanTestUtils.assertPositiveInfinity(new RegionFactory<Vector2D>().getComplement(accuratePolygon).getSize());
- Assert.assertEquals(2 * (1.0 + 1.0 / 64.0), accuratePolygon.getBoundarySize(), TEST_EPS);
- }
-
- @Test
- public void testThinRectangle_toleranceGreaterThanWidth_resultIsDegenerate() {
- // if tolerance is larger than rectangle width, the rectangle degenerates
- // as of 3.3, its two long edges cannot be distinguished anymore and this part of the test did fail
- // this has been fixed in 3.4 (issue MATH-1174)
-
- // arrange
- RegionFactory<Vector2D> factory = new RegionFactory<>();
- Vector2D pA = Vector2D.of(0.0, 1.0);
- Vector2D pB = Vector2D.of(0.0, 0.0);
- Vector2D pC = Vector2D.of(1.0 / 64.0, 0.0);
- Vector2D pD = Vector2D.of(1.0 / 64.0, 1.0);
-
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1.0 / 16);
-
- Hyperplane<Vector2D>[] h2 = new Line[] {
- Line.fromPointAndDirection(pA, Vector2D.Unit.MINUS_Y, precision),
- Line.fromPointAndDirection(pB, Vector2D.Unit.PLUS_X, precision),
- Line.fromPointAndDirection(pC, Vector2D.Unit.PLUS_Y, precision),
- Line.fromPointAndDirection(pD, Vector2D.Unit.MINUS_X, precision)
- };
-
- // act
- Region<Vector2D> degeneratedPolygon = factory.buildConvex(h2);
-
- // assert
- Assert.assertEquals(0.0, degeneratedPolygon.getSize(), TEST_EPS);
- Assert.assertTrue(degeneratedPolygon.isEmpty());
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testInconsistentHyperplanes() {
- // act
- new RegionFactory<Vector2D>().buildConvex(Line.fromPoints(Vector2D.of(0, 0), Vector2D.of(0, 1), TEST_PRECISION),
- Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(1, 0), TEST_PRECISION));
- }
-
- @Test
- public void testBoundarySimplification() {
- // a simple square will result in a 4 cuts and 5 leafs tree
- PolygonsSet square = new PolygonsSet(TEST_PRECISION,
- Vector2D.of(0, 0),
- Vector2D.of(1, 0),
- Vector2D.of(1, 1),
- Vector2D.of(0, 1));
- Vector2D[][] squareBoundary = square.getVertices();
- Assert.assertEquals(1, squareBoundary.length);
- Assert.assertEquals(4, squareBoundary[0].length);
- Counter squareCount = new Counter();
- squareCount.count(square);
- Assert.assertEquals(4, squareCount.getInternalNodes());
- Assert.assertEquals(5, squareCount.getLeafNodes());
-
- // splitting the square in two halves increases the BSP tree
- // with 3 more cuts and 3 more leaf nodes
- SubLine cut = Line.fromPointAndAngle(Vector2D.of(0.5, 0.5), 0.0, square.getPrecision()).wholeHyperplane();
- PolygonsSet splitSquare = new PolygonsSet(square.getTree(false).split(cut),
- square.getPrecision());
- Counter splitSquareCount = new Counter();
- splitSquareCount.count(splitSquare);
- Assert.assertEquals(squareCount.getInternalNodes() + 3, splitSquareCount.getInternalNodes());
- Assert.assertEquals(squareCount.getLeafNodes() + 3, splitSquareCount.getLeafNodes());
-
- // the number of vertices should not change, as the intermediate vertices
- // at (0.0, 0.5) and (1.0, 0.5) induced by the top level horizontal split
- // should be removed during the boundary extraction process
- Vector2D[][] splitBoundary = splitSquare.getVertices();
- Assert.assertEquals(1, splitBoundary.length);
- Assert.assertEquals(4, splitBoundary[0].length);
- }
-
- private static class Counter {
-
- private int internalNodes;
- private int leafNodes;
-
- public void count(PolygonsSet polygonsSet) {
- leafNodes = 0;
- internalNodes = 0;
- polygonsSet.getTree(false).visit(new BSPTreeVisitor<Vector2D>() {
- @Override
- public Order visitOrder(BSPTree<Vector2D> node) {
- return Order.SUB_PLUS_MINUS;
- }
- @Override
- public void visitInternalNode(BSPTree<Vector2D> node) {
- ++internalNodes;
- }
- @Override
- public void visitLeafNode(BSPTree<Vector2D> node) {
- ++leafNodes;
- }
-
- });
- }
-
- public int getInternalNodes() {
- return internalNodes;
- }
-
- public int getLeafNodes() {
- return leafNodes;
- }
- }
-
- private PolygonsSet buildSet(Vector2D[][] vertices) {
- ArrayList<SubHyperplane<Vector2D>> edges = new ArrayList<>();
- for (int i = 0; i < vertices.length; ++i) {
- int l = vertices[i].length;
- for (int j = 0; j < l; ++j) {
- edges.add(buildSegment(vertices[i][j], vertices[i][(j + 1) % l]));
- }
- }
- return new PolygonsSet(edges, TEST_PRECISION);
- }
-
- private SubHyperplane<Vector2D> buildLine(Vector2D start, Vector2D end) {
- return Line.fromPoints(start, end, TEST_PRECISION).wholeHyperplane();
- }
-
- private double intersectionAbscissa(Line l0, Line l1) {
- Vector2D p = l0.intersection(l1);
- return (l0.toSubSpace(p)).getX();
- }
-
- private SubHyperplane<Vector2D> buildHalfLine(Vector2D start, Vector2D end,
- boolean startIsVirtual) {
- Line line = Line.fromPoints(start, end, TEST_PRECISION);
- double lower = startIsVirtual ? Double.NEGATIVE_INFINITY : (line.toSubSpace(start)).getX();
- double upper = startIsVirtual ? (line.toSubSpace(end)).getX() : Double.POSITIVE_INFINITY;
- return new SubLine(line, new IntervalsSet(lower, upper, TEST_PRECISION));
- }
-
- private SubHyperplane<Vector2D> buildSegment(Vector2D start, Vector2D end) {
- Line line = Line.fromPoints(start, end, TEST_PRECISION);
- double lower = (line.toSubSpace(start)).getX();
- double upper = (line.toSubSpace(end)).getX();
- return new SubLine(line, new IntervalsSet(lower, upper, TEST_PRECISION));
- }
-
- private void checkPoints(Region.Location expected, PolygonsSet poly, Vector2D ... points) {
- for (int i = 0; i < points.length; ++i) {
- Assert.assertEquals("Incorrect location for " + points[i], expected, poly.checkPoint(points[i]));
- }
- }
-
- /** Asserts that the two arrays of vertex loops have equivalent content.
- * @param expectedLoops
- * @param actualLoops
- */
- private void checkVertexLoopsEquivalent(Vector2D[][] expectedLoops, Vector2D[][] actualLoops) {
- Assert.assertEquals("Expected vertices array to have length of " + expectedLoops.length + " but was " + actualLoops.length,
- expectedLoops.length, actualLoops.length);
-
- // go through each loop in the expected array and try to find a match in the actual array
- for (Vector2D[] expectedLoop : expectedLoops) {
- boolean foundMatch = false;
- for (Vector2D[] actualLoop : actualLoops) {
- if (vertexLoopsEquivalent(expectedLoop, actualLoop, TEST_EPS)) {
- foundMatch = true;
- break;
- }
- }
-
- if (!foundMatch) {
- StringBuilder sb = new StringBuilder();
- for (Vector2D[] actualLoop : actualLoops) {
- sb.append(Arrays.toString(actualLoop));
- sb.append(", ");
- }
- if (sb.length() > 0) {
- sb.delete(sb.length() - 2, sb.length());
- }
- Assert.fail("Failed to find vertex loop " + Arrays.toString(expectedLoop) + " in loop array [" +
- sb.toString() + "].");
- }
- }
- }
-
- /** Returns true if the two sets of vertices can be considered equivalent using the given
- * tolerance. For open loops, (i.e. ones that start with null) this means that the two loops
- * must have the exact same elements in the exact same order. For closed loops, equivalent
- * means that one of the loops can be rotated to match the other (e.g. [3, 1, 2] is equivalent
- * to [1, 2, 3]).
- * @param a
- * @param b
- * @param tolerance
- * @return
- */
- private boolean vertexLoopsEquivalent(Vector2D[] a, Vector2D[] b, double tolerance) {
- if (a.length == b.length) {
- if (a.length < 1) {
- // the loops are empty
- return true;
- }
- if (a[0] == null || b[0] == null) {
- // at least one of the loops is unclosed, so there is only one
- // possible sequence that could match
- return vertexLoopsEqual(a, 0, b, 0, tolerance);
- }
- else {
- // the loops are closed so they could be equivalent but
- // start at different vertices
- for (int i=0; i<a.length; ++i) {
- if (vertexLoopsEqual(a, 0, b, i, tolerance)) {
- return true;
- }
- }
- }
- }
-
- return false;
- }
-
- /** Returns true if the two vertex loops have the same elements, starting
- * from the given indices and allowing loop-around.
- * @param a
- * @param aStartIdx
- * @param b
- * @param bStartIdx
- * @param tolerance
- * @return
- */
- private boolean vertexLoopsEqual(Vector2D[] a, int aStartIdx,
- Vector2D[] b, int bStartIdx, double tolerance) {
-
- int len = a.length;
-
- Vector2D ptA;
- Vector2D ptB;
- for (int i=0; i<len; ++i) {
- ptA = a[(i + aStartIdx) % len];
- ptB = b[(i + bStartIdx) % len];
-
- if (!((ptA == null && ptB == null) ||
- (Precision.equals(ptA.getX(), ptB.getX(), tolerance) &&
- Precision.equals(ptA.getY(), ptB.getY(), tolerance)))) {
- return false;
- }
- }
-
- return true;
- }
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java
new file mode 100644
index 0000000..f95495b
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java
@@ -0,0 +1,1306 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.twod.Polyline.Builder;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PolylineTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testFromSegments_empty() {
+ // act
+ Polyline path = Polyline.fromSegments(new ArrayList<>());
+
+ // assert
+ Assert.assertTrue(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertNull(path.getStartSegment());
+ Assert.assertNull(path.getStartVertex());
+
+ Assert.assertNull(path.getEndSegment());
+ Assert.assertNull(path.getEndVertex());
+
+ Assert.assertEquals(0, path.getSegments().size());
+
+ Assert.assertEquals(0, path.getVertices().size());
+ }
+
+ @Test
+ public void testFromSegments_singleFiniteSegment() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION);
+
+ // act
+ Polyline path = Polyline.fromSegments(a);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertSame(a, path.getStartSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, path.getStartVertex(), TEST_EPS);
+
+ Assert.assertSame(a, path.getEndSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 0), path.getEndVertex(), TEST_EPS);
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(1, segments.size());
+ Assert.assertSame(a, segments.get(0));
+
+ Assert.assertEquals(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0)), path.getVertices());
+ }
+
+ @Test
+ public void testFromSegments_singleInfiniteSegment() {
+ // arrange
+ Segment a = Line.fromPoints(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION).span();
+
+ // act
+ Polyline path = Polyline.fromSegments(a);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isInfinite());
+ Assert.assertFalse(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertSame(a, path.getStartSegment());
+ Assert.assertNull(path.getStartVertex());
+
+ Assert.assertSame(a, path.getEndSegment());
+ Assert.assertNull(path.getEndVertex());
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(1, segments.size());
+ Assert.assertSame(a, segments.get(0));
+
+ Assert.assertEquals(0, path.getVertices().size());
+ }
+
+ @Test
+ public void testFromSegments_finiteSegments_notClosed() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+
+ Segment a = Segment.fromPoints(p1, p2, TEST_PRECISION);
+ Segment b = Segment.fromPoints(p2, p3, TEST_PRECISION);
+
+ // act
+ Polyline path = Polyline.fromSegments(a, b);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertSame(a, path.getStartSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(p1, path.getStartVertex(), TEST_EPS);
+
+ Assert.assertSame(b, path.getEndSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(p3, path.getEndVertex(), TEST_EPS);
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(2, segments.size());
+ Assert.assertSame(a, segments.get(0));
+ Assert.assertSame(b, segments.get(1));
+
+ Assert.assertEquals(Arrays.asList(p1, p2, p3), path.getVertices());
+ }
+
+ @Test
+ public void testFromSegments_finiteSegments_closed() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+
+ Segment a = Segment.fromPoints(p1, p2, TEST_PRECISION);
+ Segment b = Segment.fromPoints(p2, p3, TEST_PRECISION);
+ Segment c = Segment.fromPoints(p3, p1, TEST_PRECISION);
+
+ // act
+ Polyline path = Polyline.fromSegments(Arrays.asList(a, b, c));
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertTrue(path.isClosed());
+
+ Assert.assertSame(a, path.getStartSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(p1, path.getStartVertex(), TEST_EPS);
+
+ Assert.assertSame(c, path.getEndSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(p1, path.getEndVertex(), TEST_EPS);
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(3, segments.size());
+ Assert.assertSame(a, segments.get(0));
+ Assert.assertSame(b, segments.get(1));
+ Assert.assertSame(c, segments.get(2));
+
+ Assert.assertEquals(Arrays.asList(p1, p2, p3, p1), path.getVertices());
+ }
+
+ @Test
+ public void testFromSegments_infiniteSegments() {
+ // arrange
+ Segment a = Line.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION).segment(Double.NEGATIVE_INFINITY, 1.0);
+ Segment b = Line.fromPointAndAngle(Vector2D.of(1, 0), Geometry.HALF_PI, TEST_PRECISION).segment(0.0, Double.POSITIVE_INFINITY);
+
+ // act
+ Polyline path = Polyline.fromSegments(Arrays.asList(a, b));
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isInfinite());
+ Assert.assertFalse(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertSame(a, path.getStartSegment());
+ Assert.assertNull(path.getStartVertex());
+
+ Assert.assertSame(b, path.getEndSegment());
+ Assert.assertNull(path.getEndVertex());
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(2, segments.size());
+ Assert.assertSame(a, segments.get(0));
+ Assert.assertSame(b, segments.get(1));
+
+ Assert.assertEquals(Arrays.asList(Vector2D.of(1, 0)), path.getVertices());
+ }
+
+ @Test
+ public void testFromSegments_finiteAndInfiniteSegments_startInfinite() {
+ // arrange
+ Segment a = Line.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION).segment(Double.NEGATIVE_INFINITY, 1.0);
+ Segment b = Segment.fromPoints(Vector2D.of(1, 0), Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Polyline path = Polyline.fromSegments(Arrays.asList(a, b));
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isInfinite());
+ Assert.assertFalse(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertSame(a, path.getStartSegment());
+ Assert.assertNull(path.getStartVertex());
+
+ Assert.assertSame(b, path.getEndSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), path.getEndVertex(), TEST_EPS);
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(2, segments.size());
+ Assert.assertSame(a, segments.get(0));
+ Assert.assertSame(b, segments.get(1));
+
+ Assert.assertEquals(Arrays.asList(Vector2D.of(1, 0), Vector2D.of(1, 1)), path.getVertices());
+ }
+
+ @Test
+ public void testFromSegments_finiteAndInfiniteSegments_endInfinite() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION);
+ Segment b = Line.fromPointAndAngle(Vector2D.of(1, 0), Geometry.HALF_PI, TEST_PRECISION).segment(0.0, Double.POSITIVE_INFINITY);
+
+ // act
+ Polyline path = Polyline.fromSegments(Arrays.asList(a, b));
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isInfinite());
+ Assert.assertFalse(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertSame(a, path.getStartSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, path.getStartVertex(), TEST_EPS);
+
+ Assert.assertSame(b, path.getEndSegment());
+ Assert.assertNull(path.getEndVertex());
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(2, segments.size());
+ Assert.assertSame(a, segments.get(0));
+ Assert.assertSame(b, segments.get(1));
+
+ Assert.assertEquals(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0)), path.getVertices());
+ }
+
+ @Test
+ public void testFromSegments_segmentsNotConnected() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION);
+ Segment b = Segment.fromPoints(Vector2D.of(1.01, 0), Vector2D.of(1, 0), TEST_PRECISION);
+
+ Segment c = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION).span();
+ Segment d = Line.fromPointAndAngle(Vector2D.of(1, 0), Geometry.HALF_PI, TEST_PRECISION).span();
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Polyline.fromSegments(a, b);
+ }, IllegalStateException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Polyline.fromSegments(c, b);
+ }, IllegalStateException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Polyline.fromSegments(a, d);
+ }, IllegalStateException.class);
+ }
+
+ @Test
+ public void testFromVertices_empty() {
+ // act
+ Polyline path = Polyline.fromVertices(new ArrayList<>(), TEST_PRECISION);
+
+ // assert
+ Assert.assertTrue(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertNull(path.getStartSegment());
+ Assert.assertNull(path.getStartVertex());
+
+ Assert.assertNull(path.getEndSegment());
+ Assert.assertNull(path.getEndVertex());
+
+ Assert.assertEquals(0, path.getSegments().size());
+
+ Assert.assertEquals(0, path.getVertices().size());
+ }
+
+ @Test
+ public void testFromVertices_singleVertex_failsToCreatePath() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Polyline.fromVertices(Arrays.asList(Vector2D.ZERO), TEST_PRECISION);
+ }, IllegalStateException.class);
+ }
+
+ @Test
+ public void testFromVertices_twoVertices() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+
+ // act
+ Polyline path = Polyline.fromVertices(Arrays.asList(p1, p2), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ assertFiniteSegment(path.getStartSegment(), p1, p2);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, path.getStartVertex(), TEST_EPS);
+
+ Assert.assertSame(path.getStartSegment(), path.getEndSegment());
+ EuclideanTestUtils.assertCoordinatesEqual(p2, path.getEndVertex(), TEST_EPS);
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(1, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+
+ Assert.assertEquals(Arrays.asList(p1, p2), path.getVertices());
+ }
+
+ @Test
+ public void testFromVertices_multipleVertices_notClosed() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+ Vector2D p4 = Vector2D.of(0, 1);
+
+ // act
+ Polyline path = Polyline.fromVertices(Arrays.asList(p1, p2, p3, p4), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ assertFiniteSegment(path.getStartSegment(), p1, p2);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, path.getStartVertex(), TEST_EPS);
+
+ assertFiniteSegment(path.getEndSegment(), p3, p4);
+ EuclideanTestUtils.assertCoordinatesEqual(p4, path.getEndVertex(), TEST_EPS);
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(3, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p4);
+
+ Assert.assertEquals(Arrays.asList(p1, p2, p3, p4), path.getVertices());
+ }
+
+ @Test
+ public void testFromVertices_multipleVertices_closed() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+ Vector2D p4 = Vector2D.of(0, 1);
+
+ // act
+ Polyline path = Polyline.fromVertices(Arrays.asList(p1, p2, p3, p4, p1), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertTrue(path.isClosed());
+
+ assertFiniteSegment(path.getStartSegment(), p1, p2);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, path.getStartVertex(), TEST_EPS);
+
+ assertFiniteSegment(path.getEndSegment(), p4, p1);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, path.getEndVertex(), TEST_EPS);
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(4, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p4);
+ assertFiniteSegment(segments.get(3), p4, p1);
+
+ Assert.assertEquals(Arrays.asList(p1, p2, p3, p4, p1), path.getVertices());
+ }
+
+ @Test
+ public void testFromVertexLoop_empty() {
+ // act
+ Polyline path = Polyline.fromVertexLoop(new ArrayList<>(), TEST_PRECISION);
+
+ // assert
+ Assert.assertTrue(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertNull(path.getStartSegment());
+ Assert.assertNull(path.getStartVertex());
+
+ Assert.assertNull(path.getEndSegment());
+ Assert.assertNull(path.getEndVertex());
+
+ Assert.assertEquals(0, path.getSegments().size());
+
+ Assert.assertEquals(0, path.getVertices().size());
+ }
+
+ @Test
+ public void testFromVertexLoop_singleVertex_failsToCreatePath() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Polyline.fromVertexLoop(Arrays.asList(Vector2D.ZERO), TEST_PRECISION);
+ }, IllegalStateException.class);
+ }
+
+ @Test
+ public void testFromVertexLoop_closeRequired() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+
+ // act
+ Polyline path = Polyline.fromVertexLoop(Arrays.asList(p1, p2, p3), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertTrue(path.isClosed());
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(3, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p1);
+
+ Assert.assertEquals(Arrays.asList(p1, p2, p3, p1), path.getVertices());
+ }
+
+ @Test
+ public void testFromVertexLoop_closeNotRequired() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+
+ // act
+ Polyline path = Polyline.fromVertexLoop(Arrays.asList(p1, p2, p3, Vector2D.of(0, 0)), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isInfinite());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertTrue(path.isClosed());
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(3, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p1);
+
+ Assert.assertEquals(Arrays.asList(p1, p2, p3, p1), path.getVertices());
+ }
+
+ @Test
+ public void testFromVertices_booleanArg() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(0, 1);
+
+ // act
+ Polyline open = Polyline.fromVertices(Arrays.asList(p1, p2, p3), false, TEST_PRECISION);
+ Polyline closed = Polyline.fromVertices(Arrays.asList(p1, p2, p3), true, TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(open.isClosed());
+
+ List<Segment> openSegments = open.getSegments();
+ Assert.assertEquals(2, openSegments.size());
+ assertFiniteSegment(openSegments.get(0), p1, p2);
+ assertFiniteSegment(openSegments.get(1), p2, p3);
+
+ Assert.assertTrue(closed.isClosed());
+
+ List<Segment> closedSegments = closed.getSegments();
+ Assert.assertEquals(3, closedSegments.size());
+ assertFiniteSegment(closedSegments.get(0), p1, p2);
+ assertFiniteSegment(closedSegments.get(1), p2, p3);
+ assertFiniteSegment(closedSegments.get(2), p3, p1);
+ }
+
+ @Test
+ public void testGetSegments_listIsNotModifiable() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION);
+ List<Segment> inputSegments = new ArrayList<>(Arrays.asList(a));
+
+ // act
+ Polyline path = Polyline.fromSegments(inputSegments);
+
+ inputSegments.clear();
+
+ // assert
+ Assert.assertNotSame(inputSegments, path.getSegments());
+ Assert.assertEquals(1, path.getSegments().size());
+
+ GeometryTestUtils.assertThrows(() -> {
+ path.getSegments().add(a);
+ }, UnsupportedOperationException.class);
+ }
+
+ @Test
+ public void testIterable() {
+ // arrange
+ Polyline path = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1)).build();
+
+ // act
+ List<Segment> segments = new ArrayList<>();
+ for (Segment segment : path) {
+ segments.add(segment);
+ }
+
+ // assert
+ Assert.assertEquals(2, segments.size());
+ assertFiniteSegment(segments.get(0), Vector2D.Unit.ZERO, Vector2D.Unit.PLUS_X);
+ assertFiniteSegment(segments.get(1), Vector2D.Unit.PLUS_X, Vector2D.of(1, 1));
+ }
+
+ @Test
+ public void testTransform_empty() {
+ // arrange
+ Polyline path = Polyline.empty();
+ FunctionTransform2D t = FunctionTransform2D.from(v -> v.add(Vector2D.Unit.PLUS_X));
+
+ // act/assert
+ Assert.assertSame(path, path.transform(t));
+ }
+
+ @Test
+ public void testTransform_finite() {
+ // arrange
+ Polyline path = Polyline.builder(TEST_PRECISION)
+ .append(Vector2D.Unit.ZERO)
+ .append(Vector2D.Unit.PLUS_X)
+ .append(Vector2D.Unit.PLUS_Y)
+ .close();
+
+ AffineTransformMatrix2D t =
+ AffineTransformMatrix2D.createRotation(Vector2D.of(1, 1), Geometry.HALF_PI);
+
+ // act
+ Polyline result = path.transform(t);
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertTrue(result.isClosed());
+ Assert.assertTrue(result.isFinite());
+
+ List<Segment> segments = result.getSegments();
+
+ Assert.assertEquals(3, segments.size());
+ assertFiniteSegment(segments.get(0), Vector2D.of(2, 0), Vector2D.of(2, 1));
+ assertFiniteSegment(segments.get(1), Vector2D.of(2, 1), Vector2D.Unit.PLUS_X);
+ assertFiniteSegment(segments.get(2), Vector2D.Unit.PLUS_X, Vector2D.of(2, 0));
+ }
+
+ @Test
+ public void testTransform_infinite() {
+ // arrange
+ Polyline path = Polyline.fromSegments(
+ Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION).span());
+
+ FunctionTransform2D t = FunctionTransform2D.from(v -> v.add(Vector2D.Unit.PLUS_X));
+
+ // act
+ Polyline result = path.transform(t);
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertFalse(result.isClosed());
+ Assert.assertFalse(result.isFinite());
+
+ List<Segment> segments = result.getSegments();
+
+ Assert.assertEquals(1, segments.size());
+ Segment segment = segments.get(0);
+ Assert.assertTrue(segment.isInfinite());
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_X, segment.getLine().getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_Y, segment.getLine().getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testReverse_empty() {
+ // arrange
+ Polyline path = Polyline.empty();
+
+ // act/assert
+ Assert.assertSame(path, path.reverse());
+ }
+
+ @Test
+ public void testReverse() {
+ // arrange
+ Polyline path = Polyline.builder(TEST_PRECISION)
+ .append(Vector2D.Unit.ZERO)
+ .append(Vector2D.Unit.PLUS_X)
+ .append(Vector2D.Unit.PLUS_Y)
+ .close();
+
+ // act
+ Polyline result = path.reverse();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertTrue(result.isClosed());
+ Assert.assertTrue(result.isFinite());
+
+ List<Segment> segments = result.getSegments();
+
+ Assert.assertEquals(3, segments.size());
+ assertFiniteSegment(segments.get(0), Vector2D.Unit.ZERO, Vector2D.Unit.PLUS_Y);
+ assertFiniteSegment(segments.get(1), Vector2D.Unit.PLUS_Y, Vector2D.Unit.PLUS_X);
+ assertFiniteSegment(segments.get(2), Vector2D.Unit.PLUS_X, Vector2D.Unit.ZERO);
+ }
+
+ @Test
+ public void testReverse_singleInfinite() {
+ // arrange
+ Polyline path = Polyline.fromSegments(
+ Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION).span());
+
+ // act
+ Polyline result = path.reverse();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertFalse(result.isClosed());
+ Assert.assertFalse(result.isFinite());
+
+ List<Segment> segments = result.getSegments();
+
+ Assert.assertEquals(1, segments.size());
+ Segment segment = segments.get(0);
+ Assert.assertTrue(segment.isInfinite());
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.ZERO, segment.getLine().getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_Y, segment.getLine().getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testReverse_doubleInfinite() {
+ // arrange
+ Segment a = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION).segmentTo(Vector2D.ZERO);
+ Segment b = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION).segmentFrom(Vector2D.ZERO);
+
+ Polyline path = Polyline.fromSegments(a, b);
+
+ // act
+ Polyline result = path.reverse();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertFalse(result.isClosed());
+ Assert.assertFalse(result.isFinite());
+
+ List<Segment> segments = result.getSegments();
+ Assert.assertEquals(2, segments.size());
+
+ Segment bResult = segments.get(0);
+ Assert.assertTrue(bResult.isInfinite());
+ Assert.assertNull(bResult.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, bResult.getEndPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, bResult.getLine().getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_X, bResult.getLine().getDirection(), TEST_EPS);
+
+ Segment aResult = segments.get(1);
+ Assert.assertTrue(aResult.isInfinite());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, aResult.getStartPoint(), TEST_EPS);
+ Assert.assertNull(aResult.getEndPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, aResult.getLine().getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_Y, aResult.getLine().getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testToTree() {
+ // arrange
+ Polyline path = Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), Vector2D.of(0, 1))
+ .close();
+
+ // act
+ RegionBSPTree2D tree = path.toTree();
+
+ // assert
+ Assert.assertEquals(1, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(4, tree.getBoundarySize(), TEST_EPS);
+
+ Assert.assertEquals(RegionLocation.INSIDE, tree.classify(Vector2D.of(0.5, 0.5)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(Vector2D.of(0.5, -1)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(Vector2D.of(0.5, 2)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(Vector2D.of(-1, 0.5)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, tree.classify(Vector2D.of(2, 0.5)));
+ }
+
+ @Test
+ public void testSimplify() {
+ // arrange
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ Polyline path = builder.appendVertices(
+ Vector2D.of(-1, 0),
+ Vector2D.ZERO,
+ Vector2D.of(1, 0),
+ Vector2D.of(1, 1),
+ Vector2D.of(1, 2))
+ .build();
+
+ // act
+ Polyline result = path.simplify();
+
+ // assert
+ List<Segment> segments = result.getSegments();
+ Assert.assertEquals(2, segments.size());
+ assertFiniteSegment(segments.get(0), Vector2D.of(-1, 0), Vector2D.of(1, 0));
+ assertFiniteSegment(segments.get(1), Vector2D.of(1, 0), Vector2D.of(1, 2));
+ }
+
+ @Test
+ public void testSimplify_startAndEndCombined() {
+ // arrange
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ Polyline path = builder.appendVertices(
+ Vector2D.ZERO,
+ Vector2D.of(1, 0),
+ Vector2D.of(0, 1),
+ Vector2D.of(-1, 0))
+ .close();
+
+ // act
+ Polyline result = path.simplify();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertTrue(result.isClosed());
+ Assert.assertFalse(result.isInfinite());
+
+ List<Segment> segments = result.getSegments();
+ Assert.assertEquals(3, segments.size());
+ assertFiniteSegment(segments.get(0), Vector2D.of(-1, 0), Vector2D.of(1, 0));
+ assertFiniteSegment(segments.get(1), Vector2D.of(1, 0), Vector2D.of(0, 1));
+ assertFiniteSegment(segments.get(2), Vector2D.of(0, 1), Vector2D.of(-1, 0));
+ }
+
+ @Test
+ public void testSimplify_empty() {
+ // arrange
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ Polyline path = builder.build();
+
+ // act
+ Polyline result = path.simplify();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertFalse(result.isClosed());
+ Assert.assertFalse(result.isInfinite());
+
+ List<Segment> segments = result.getSegments();
+ Assert.assertEquals(0, segments.size());
+ }
+
+ @Test
+ public void testSimplify_infiniteSegment() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+ Polyline path = builder
+ .append(line.span())
+ .build();
+
+ // act
+ Polyline result = path.simplify();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertFalse(result.isClosed());
+ Assert.assertTrue(result.isInfinite());
+
+ Assert.assertNull(result.getStartVertex());
+ Assert.assertNull(result.getEndVertex());
+
+ List<Segment> segments = result.getSegments();
+ Assert.assertEquals(1, segments.size());
+ Assert.assertSame(line, segments.get(0).getLine());
+ }
+
+ @Test
+ public void testSimplify_combinedInfiniteSegment() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ Split<Segment> split = line.span().split(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI, TEST_PRECISION));
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+ Polyline path = builder
+ .append(split.getMinus())
+ .append(split.getPlus())
+ .build();
+
+ // act
+ Polyline result = path.simplify();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertFalse(result.isClosed());
+ Assert.assertTrue(result.isInfinite());
+
+ Assert.assertNull(result.getStartVertex());
+ Assert.assertNull(result.getEndVertex());
+
+ List<Segment> segments = result.getSegments();
+ Assert.assertEquals(1, segments.size());
+ Assert.assertSame(line, segments.get(0).getLine());
+ }
+
+ @Test
+ public void testSimplify_startAndEndNotCombinedWhenNotClosed() {
+ // arrange
+ Line xAxis = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ Polyline path = builder
+ .append(xAxis.segment(0, 1))
+ .appendVertices(
+ Vector2D.of(2, 1),
+ Vector2D.of(3, 0))
+ .append(xAxis.segment(3, 4))
+ .build();
+
+ // act
+ Polyline result = path.simplify();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertFalse(result.isClosed());
+ Assert.assertFalse(result.isInfinite());
+
+ List<Segment> segments = result.getSegments();
+ Assert.assertEquals(4, segments.size());
+ assertFiniteSegment(segments.get(0), Vector2D.ZERO, Vector2D.of(1, 0));
+ assertFiniteSegment(segments.get(1), Vector2D.of(1, 0), Vector2D.of(2, 1));
+ assertFiniteSegment(segments.get(2), Vector2D.of(2, 1), Vector2D.of(3, 0));
+ assertFiniteSegment(segments.get(3), Vector2D.of(3, 0), Vector2D.of(4, 0));
+ }
+
+ @Test
+ public void testSimplify_subsequentCallsToReturnedObjectReturnSameObject() {
+ // arrange
+ Builder builder = Polyline.builder(TEST_PRECISION);
+ Polyline path = builder.appendVertices(
+ Vector2D.ZERO,
+ Vector2D.of(1, 0),
+ Vector2D.of(2, 0))
+ .build();
+
+ // act
+ Polyline result = path.simplify();
+
+ // assert
+ Assert.assertNotSame(path, result);
+ Assert.assertSame(result, result.simplify());
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Line yAxis = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
+ Line xAxis = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+ Polyline empty = Polyline.empty();
+
+ Polyline singleFullSegment = Polyline.fromSegments(xAxis.span());
+ Polyline singleFiniteSegment = Polyline.fromSegments(
+ Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+ Polyline startOpenPath = Polyline.builder(TEST_PRECISION)
+ .append(xAxis.segmentTo(Vector2D.Unit.PLUS_X))
+ .append(Vector2D.of(1, 1))
+ .build();
+
+ Polyline endOpenPath = Polyline.builder(TEST_PRECISION)
+ .append(Vector2D.of(0, 1))
+ .append(Vector2D.ZERO)
+ .append(xAxis.segmentFrom(Vector2D.ZERO))
+ .build();
+
+ Polyline doubleOpenPath = Polyline.fromSegments(yAxis.segmentTo(Vector2D.ZERO),
+ xAxis.segmentFrom(Vector2D.ZERO));
+
+ Polyline nonOpenPath = Polyline.builder(TEST_PRECISION)
+ .append(Vector2D.ZERO)
+ .append(Vector2D.Unit.PLUS_X)
+ .append(Vector2D.of(1, 1))
+ .build();
+
+ // act/assert
+ String emptyStr = empty.toString();
+ Assert.assertTrue(emptyStr.contains("empty= true"));
+
+ String singleFullStr = singleFullSegment.toString();
+ Assert.assertTrue(singleFullStr.contains("segment= Segment["));
+
+ String singleFiniteStr = singleFiniteSegment.toString();
+ Assert.assertTrue(singleFiniteStr.contains("segment= Segment["));
+
+ String startOpenStr = startOpenPath.toString();
+ Assert.assertTrue(startOpenStr.contains("startDirection= ") && startOpenStr.contains("vertices= "));
+
+ String endOpenStr = endOpenPath.toString();
+ Assert.assertTrue(endOpenStr.contains("vertices= ") && endOpenStr.contains("endDirection= "));
+
+ String doubleOpenStr = doubleOpenPath.toString();
+ Assert.assertTrue(doubleOpenStr.contains("startDirection= ") && doubleOpenStr.contains("vertices= ") &&
+ doubleOpenStr.contains("endDirection= "));
+
+ String nonOpenStr = nonOpenPath.toString();
+ Assert.assertTrue(nonOpenStr.contains("vertices= "));
+ }
+
+ @Test
+ public void testBuilder_prependAndAppend_segments() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+ Vector2D p4 = Vector2D.of(1, 0);
+
+ Segment a = Segment.fromPoints(p1, p2, TEST_PRECISION);
+ Segment b = Segment.fromPoints(p2, p3, TEST_PRECISION);
+ Segment c = Segment.fromPoints(p3, p4, TEST_PRECISION);
+ Segment d = Segment.fromPoints(p4, p1, TEST_PRECISION);
+
+ Builder builder = Polyline.builder(null);
+
+ // act
+ builder.prepend(b)
+ .append(c)
+ .prepend(a)
+ .append(d);
+
+ Polyline path = builder.build();
+
+ // assert
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(4, segments.size());
+ Assert.assertSame(a, segments.get(0));
+ Assert.assertSame(b, segments.get(1));
+ Assert.assertSame(c, segments.get(2));
+ Assert.assertSame(d, segments.get(3));
+ }
+
+ @Test
+ public void testBuilder_prependAndAppend_disconnectedSegments() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION);
+
+ Builder builder = Polyline.builder(null);
+ builder.append(a);
+
+ // act
+ GeometryTestUtils.assertThrows(() -> {
+ builder.append(a);
+ }, IllegalStateException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.prepend(a);
+ }, IllegalStateException.class);
+ }
+
+ @Test
+ public void testBuilder_prependAndAppend_vertices() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+ Vector2D p4 = Vector2D.of(1, 0);
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ // act
+ builder.prepend(p2)
+ .append(p3)
+ .prepend(p1)
+ .append(p4)
+ .append(p1);
+
+ Polyline path = builder.build();
+
+ // assert
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(4, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p4);
+ assertFiniteSegment(segments.get(3), p4, p1);
+ }
+
+ @Test
+ public void testBuilder_prependAndAppend_noPrecisionSpecified() {
+ // arrange
+ Vector2D p = Vector2D.ZERO;
+ Builder builder = Polyline.builder(null);
+
+ String msg = "Unable to create line segment: no vertex precision specified";
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ builder.append(p);
+ }, IllegalStateException.class, msg);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.prepend(p);
+ }, IllegalStateException.class, msg);
+ }
+
+ @Test
+ public void testBuilder_prependAndAppend_addingToInfinitePath() {
+ // arrange
+ Vector2D p = Vector2D.Unit.PLUS_X;
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ builder.append(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION).span());
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ builder.append(p);
+ }, IllegalStateException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.prepend(p);
+ }, IllegalStateException.class);
+ }
+
+ @Test
+ public void testBuilder_prependAndAppend_ignoresEquivalentVertices() {
+ // arrange
+ Vector2D p = Vector2D.ZERO;
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+ builder.append(p);
+
+ // act
+ builder.append(p)
+ .prepend(p)
+ .append(Vector2D.of(0, 1e-20))
+ .prepend(Vector2D.of(1e-20, 0));
+
+ builder.append(Vector2D.Unit.PLUS_X);
+
+ // assert
+ Polyline path = builder.build();
+
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(1, segments.size());
+ assertFiniteSegment(segments.get(0), p, Vector2D.Unit.PLUS_X);
+ }
+
+ @Test
+ public void testBuilder_prependAndAppend_mixedVerticesAndSegments() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+ Vector2D p4 = Vector2D.of(0, 1);
+
+ Segment a = Segment.fromPoints(p1, p2, TEST_PRECISION);
+ Segment c = Segment.fromPoints(p3, p4, TEST_PRECISION);
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ // act
+ builder.prepend(p2)
+ .append(p3)
+ .append(c)
+ .prepend(a)
+ .append(p1);
+
+ Polyline path = builder.build();
+
+ // assert
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(4, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p4);
+ assertFiniteSegment(segments.get(3), p4, p1);
+ }
+
+ @Test
+ public void testBuilder_appendVertices() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+ Vector2D p4 = Vector2D.of(0, 1);
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ // act
+ builder.appendVertices(p1, p2)
+ .appendVertices(Arrays.asList(p3, p4, p1));
+
+ Polyline path = builder.build();
+
+ // assert
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(4, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p4);
+ assertFiniteSegment(segments.get(3), p4, p1);
+ }
+
+ @Test
+ public void testBuilder_prependVertices() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+ Vector2D p4 = Vector2D.of(0, 1);
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ // act
+ builder.prependVertices(p3, p4, p1)
+ .prependVertices(Arrays.asList(p1, p2));
+
+ Polyline path = builder.build();
+
+ // assert
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(4, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p4);
+ assertFiniteSegment(segments.get(3), p4, p1);
+ }
+
+ @Test
+ public void testBuilder_close_notYetClosed() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ // act
+ builder.append(p1)
+ .append(p2)
+ .append(p3);
+
+ Polyline path = builder.close();
+
+ // assert
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(3, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p1);
+ }
+
+ @Test
+ public void testBuilder_close_alreadyClosed() {
+ // arrange
+ Vector2D p1 = Vector2D.ZERO;
+ Vector2D p2 = Vector2D.of(1, 0);
+ Vector2D p3 = Vector2D.of(1, 1);
+
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ // act
+ builder.append(p1)
+ .append(p2)
+ .append(p3)
+ .append(p1);
+
+ Polyline path = builder.close();
+
+ // assert
+ List<Segment> segments = path.getSegments();
+ Assert.assertEquals(3, segments.size());
+ assertFiniteSegment(segments.get(0), p1, p2);
+ assertFiniteSegment(segments.get(1), p2, p3);
+ assertFiniteSegment(segments.get(2), p3, p1);
+ }
+
+ @Test
+ public void testBuilder_close_infiniteSegmentAtStart() {
+ // arrange
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ builder.append(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION)
+ .segment(Double.NEGATIVE_INFINITY, 1))
+ .append(Vector2D.of(1, 1));
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ builder.close();
+ }, IllegalStateException.class, "Unable to close polyline: polyline is infinite");
+ }
+
+ @Test
+ public void testBuilder_close_infiniteSegmentAtEnd() {
+ // arrange
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ builder
+ .append(Vector2D.ZERO)
+ .append(Vector2D.Unit.PLUS_X)
+ .append(Line.fromPointAndAngle(Vector2D.Unit.PLUS_X, Geometry.HALF_PI, TEST_PRECISION)
+ .segment(0, Double.POSITIVE_INFINITY));
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ builder.close();
+ }, IllegalStateException.class, "Unable to close polyline: polyline is infinite");
+ }
+
+ @Test
+ public void testBuilder_close_emptyPath() {
+ // arrange
+ Builder builder = Polyline.builder(TEST_PRECISION);
+
+ // act
+ Polyline path = builder.close();
+
+ // assert
+ Assert.assertEquals(0, path.getSegments().size());
+ }
+
+ @Test
+ public void testBuilder_close_obtuseTriangle() {
+ // arrange
+ Builder builder = Polyline.builder(TEST_PRECISION);
+ builder.appendVertices(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(2, 1));
+
+ // act
+ Polyline path = builder.close();
+
+ // assert
+ Assert.assertEquals(3, path.getSegments().size());
+ assertFiniteSegment(path.getSegments().get(0), Vector2D.ZERO, Vector2D.of(1, 0));
+ assertFiniteSegment(path.getSegments().get(1), Vector2D.of(1, 0), Vector2D.of(2, 1));
+ assertFiniteSegment(path.getSegments().get(2), Vector2D.of(2, 1), Vector2D.ZERO);
+ }
+
+ private static void assertFiniteSegment(Segment segment, Vector2D start, Vector2D end) {
+ Assert.assertFalse(segment.isInfinite());
+ Assert.assertTrue(segment.isFinite());
+
+ EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
new file mode 100644
index 0000000..e84f271
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
@@ -0,0 +1,1237 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D.RegionNode2D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RegionBSPTree2DTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final Comparator<Segment> SEGMENT_COMPARATOR =
+ (a, b) -> Vector2D.COORDINATE_ASCENDING_ORDER.compare(a.getStartPoint(), b.getStartPoint());
+
+ private static final Line X_AXIS = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+ private static final Line Y_AXIS = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
+
+ @Test
+ public void testCtor_booleanArg_true() {
+ // act
+ RegionBSPTree2D tree = new RegionBSPTree2D(true);
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testCtor_booleanArg_false() {
+ // act
+ RegionBSPTree2D tree = new RegionBSPTree2D(false);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testCtor_default() {
+ // act
+ RegionBSPTree2D tree = new RegionBSPTree2D();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testFull_factoryMethod() {
+ // act
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testEmpty_factoryMethod() {
+ // act
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testCopy() {
+ // arrange
+ RegionBSPTree2D tree = new RegionBSPTree2D(true);
+ tree.getRoot().cut(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION));
+
+ // act
+ RegionBSPTree2D copy = tree.copy();
+
+ // assert
+ Assert.assertNotSame(tree, copy);
+ Assert.assertEquals(3, copy.count());
+ }
+
+ @Test
+ public void testBoundaries() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.ZERO, Vector2D.of(1, 1))
+ .build();
+
+ // act
+ List<Segment> segments = new ArrayList<>();
+ tree.boundaries().forEach(segments::add);
+
+ // assert
+ Assert.assertEquals(4, segments.size());
+ }
+
+ @Test
+ public void testGetBoundaries() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.ZERO, Vector2D.of(1, 1))
+ .build();
+
+ // act
+ List<Segment> segments = tree.getBoundaries();
+
+ // assert
+ Assert.assertEquals(4, segments.size());
+ }
+
+ @Test
+ public void testGetBoundaryPaths_cachesResult() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.insert(Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+ // act
+ List<Polyline> a = tree.getBoundaryPaths();
+ List<Polyline> b = tree.getBoundaryPaths();
+
+ // assert
+ Assert.assertSame(a, b);
+ }
+
+ @Test
+ public void testGetBoundaryPaths_recomputesResultOnChange() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.insert(Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+ // act
+ List<Polyline> a = tree.getBoundaryPaths();
+ tree.insert(Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION));
+ List<Polyline> b = tree.getBoundaryPaths();
+
+ // assert
+ Assert.assertNotSame(a, b);
+ }
+
+ @Test
+ public void testGetBoundaryPaths_isUnmodifiable() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.insert(Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ tree.getBoundaryPaths().add(Polyline.builder(null).build());
+ }, UnsupportedOperationException.class);
+ }
+
+ @Test
+ public void testAdd_convexArea() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+
+ // act
+ tree.add(ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.ZERO, Vector2D.of(2, 0),
+ Vector2D.of(2, 2), Vector2D.of(0, 2)
+ ), TEST_PRECISION));
+ tree.add(ConvexArea.fromVertexLoop(Arrays.asList(
+ Vector2D.of(1, 1), Vector2D.of(3, 1),
+ Vector2D.of(3, 3), Vector2D.of(1, 3)
+ ), TEST_PRECISION));
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(7, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(12, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector2D.of(1, 1), Vector2D.of(1.5, 1.5), Vector2D.of(2, 2));
+ }
+
+ @Test
+ public void testToConvex_full() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+
+ // act
+ List<ConvexArea> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+ Assert.assertTrue(result.get(0).isFull());
+ }
+
+ @Test
+ public void testToConvex_empty() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+
+ // act
+ List<ConvexArea> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(0, result.size());
+ }
+
+ @Test
+ public void testToConvex_halfSpace() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+ tree.getRoot().insertCut(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION));
+
+ // act
+ List<ConvexArea> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+
+ ConvexArea area = result.get(0);
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ checkClassify(area, RegionLocation.INSIDE, Vector2D.of(0, 1));
+ checkClassify(area, RegionLocation.BOUNDARY, Vector2D.ZERO);
+ checkClassify(area, RegionLocation.OUTSIDE, Vector2D.of(0, -1));
+ }
+
+ @Test
+ public void testToConvex_quadrantComplement() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+ tree.getRoot().cut(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.PI, TEST_PRECISION))
+ .getPlus().cut(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI, TEST_PRECISION));
+
+ tree.complement();
+
+ // act
+ List<ConvexArea> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+
+ ConvexArea area = result.get(0);
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ checkClassify(area, RegionLocation.INSIDE, Vector2D.of(1, 1));
+ checkClassify(area, RegionLocation.BOUNDARY, Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1));
+ checkClassify(area, RegionLocation.OUTSIDE, Vector2D.of(1, -1), Vector2D.of(-1, -1), Vector2D.of(-1, 1));
+ }
+
+ @Test
+ public void testToConvex_square() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addSquare(Vector2D.ZERO, 1)
+ .build();
+
+ // act
+ List<ConvexArea> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+
+ ConvexArea area = result.get(0);
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+
+ Assert.assertEquals(1, area.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+
+ checkClassify(area, RegionLocation.INSIDE, Vector2D.of(0.5, 0.5));
+ checkClassify(area, RegionLocation.BOUNDARY, Vector2D.ZERO, Vector2D.of(1, 1));
+ checkClassify(area, RegionLocation.OUTSIDE,
+ Vector2D.of(0.5, -1), Vector2D.of(0.5, 2),
+ Vector2D.of(-1, 0.5), Vector2D.of(2, 0.5));
+ }
+
+ @Test
+ public void testToConvex_multipleConvexAreas() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.insert(Arrays.asList(
+ Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION),
+
+ Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(0, 1), TEST_PRECISION),
+ Segment.fromPoints(Vector2D.of(0, 1), Vector2D.ZERO, TEST_PRECISION),
+
+ Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION),
+ Segment.fromPoints(Vector2D.of(1, 0), Vector2D.of(1, 1), TEST_PRECISION)
+ ));
+
+ // act
+ List<ConvexArea> result = tree.toConvex();
+
+ // assert
+ Collections.sort(result, (a, b) ->
+ Vector2D.COORDINATE_ASCENDING_ORDER.compare(a.getBarycenter(), b.getBarycenter()));
+
+ Assert.assertEquals(2, result.size());
+
+ ConvexArea firstArea = result.get(0);
+ Assert.assertFalse(firstArea.isFull());
+ Assert.assertFalse(firstArea.isEmpty());
+
+ Assert.assertEquals(0.5, firstArea.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.0 / 3.0, 2.0 / 3.0), firstArea.getBarycenter(), TEST_EPS);
+
+ checkClassify(firstArea, RegionLocation.INSIDE, Vector2D.of(1.0 / 3.0, 2.0 / 3.0));
+ checkClassify(firstArea, RegionLocation.BOUNDARY, Vector2D.ZERO, Vector2D.of(1, 1), Vector2D.of(0.5, 0.5));
+ checkClassify(firstArea, RegionLocation.OUTSIDE,
+ Vector2D.of(0.25, -1), Vector2D.of(0.25, 2),
+ Vector2D.of(-1, 0.5), Vector2D.of(0.75, 0.5));
+
+ ConvexArea secondArea = result.get(1);
+ Assert.assertFalse(secondArea.isFull());
+ Assert.assertFalse(secondArea.isEmpty());
+
+ Assert.assertEquals(0.5, secondArea.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2.0 / 3.0, 1.0 / 3.0), secondArea.getBarycenter(), TEST_EPS);
+
+ checkClassify(secondArea, RegionLocation.INSIDE, Vector2D.of(2.0 / 3.0, 1.0 / 3.0));
+ checkClassify(secondArea, RegionLocation.BOUNDARY, Vector2D.ZERO, Vector2D.of(1, 1), Vector2D.of(0.5, 0.5));
+ checkClassify(secondArea, RegionLocation.OUTSIDE,
+ Vector2D.of(0.75, -1), Vector2D.of(0.75, 2),
+ Vector2D.of(2, 0.5), Vector2D.of(0.25, 0.5));
+ }
+
+ @Test
+ public void testGetNodeRegion() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+
+ RegionNode2D root = tree.getRoot();
+ root.cut(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION));
+
+ RegionNode2D minus = root.getMinus();
+ minus.cut(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI, TEST_PRECISION));
+
+ Vector2D origin = Vector2D.ZERO;
+
+ Vector2D a = Vector2D.of(1, 0);
+ Vector2D b = Vector2D.of(1, 1);
+ Vector2D c = Vector2D.of(0, 1);
+ Vector2D d = Vector2D.of(-1, 1);
+ Vector2D e = Vector2D.of(-1, 0);
+ Vector2D f = Vector2D.of(-1, -1);
+ Vector2D g = Vector2D.of(0, -1);
+ Vector2D h = Vector2D.of(1, -1);
+
+ // act/assert
+ checkConvexArea(root.getNodeRegion(), Arrays.asList(origin, a, b, c, d, e, f, g, h), Arrays.asList());
+
+ checkConvexArea(minus.getNodeRegion(), Arrays.asList(b, c, d), Arrays.asList(f, g, h));
+ checkConvexArea(root.getPlus().getNodeRegion(), Arrays.asList(f, g, h), Arrays.asList(b, c, d));
+
+ checkConvexArea(minus.getMinus().getNodeRegion(), Arrays.asList(d), Arrays.asList(a, b, f, g, h));
+ checkConvexArea(minus.getPlus().getNodeRegion(), Arrays.asList(b), Arrays.asList(d, e, f, g, h));
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(1, 0), 0.25 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree2D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkClassify(split.getMinus(), RegionLocation.INSIDE, Vector2D.of(0, 1));
+ checkClassify(split.getMinus(), RegionLocation.OUTSIDE, Vector2D.of(1, -1));
+
+ List<Polyline> minusBoundaryList = split.getMinus().getBoundaryPaths();
+ Assert.assertEquals(1, minusBoundaryList.size());
+
+ Polyline minusBoundary = minusBoundaryList.get(0);
+ Assert.assertEquals(1, minusBoundary.getSegments().size());
+ Assert.assertTrue(minusBoundary.isInfinite());
+ Assert.assertSame(splitter, minusBoundary.getStartSegment().getLine());
+
+ checkClassify(split.getPlus(), RegionLocation.OUTSIDE, Vector2D.of(0, 1));
+ checkClassify(split.getPlus(), RegionLocation.INSIDE, Vector2D.of(1, -1));
+
+ List<Polyline> plusBoundaryList = split.getPlus().getBoundaryPaths();
+ Assert.assertEquals(1, plusBoundaryList.size());
+
+ Polyline plusBoundary = minusBoundaryList.get(0);
+ Assert.assertEquals(1, plusBoundary.getSegments().size());
+ Assert.assertTrue(plusBoundary.isInfinite());
+ Assert.assertSame(splitter, plusBoundary.getStartSegment().getLine());
+ }
+
+ @Test
+ public void testSplit_empty() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(1, 0), 0.25 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree2D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_bothSides() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.ZERO, 2, 1)
+ .build();
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.ZERO, 0.25 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree2D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ List<Polyline> minusPath = split.getMinus().getBoundaryPaths();
+ Assert.assertEquals(1, minusPath.size());
+ checkVertices(minusPath.get(0), Vector2D.ZERO, Vector2D.of(1, 1),
+ Vector2D.of(0, 1), Vector2D.ZERO);
+
+ List<Polyline> plusPath = split.getPlus().getBoundaryPaths();
+ Assert.assertEquals(1, plusPath.size());
+ checkVertices(plusPath.get(0), Vector2D.ZERO, Vector2D.of(2, 0),
+ Vector2D.of(2, 1), Vector2D.of(1, 1), Vector2D.ZERO);
+ }
+
+ @Test
+ public void testSplit_plusSideOnly() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.ZERO, 2, 1)
+ .build();
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(0, 1), 0.25 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree2D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+
+ List<Polyline> plusPath = split.getPlus().getBoundaryPaths();
+ Assert.assertEquals(1, plusPath.size());
+ checkVertices(plusPath.get(0), Vector2D.ZERO, Vector2D.of(2, 0),
+ Vector2D.of(2, 1), Vector2D.of(0, 1), Vector2D.ZERO);
+ }
+
+ @Test
+ public void testSplit_minusSideOnly() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.ZERO, 2, 1)
+ .build();
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(0, 1), 0.25 * Geometry.PI, TEST_PRECISION)
+ .reverse();
+
+ // act
+ Split<RegionBSPTree2D> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ List<Polyline> minusPath = split.getMinus().getBoundaryPaths();
+ Assert.assertEquals(1, minusPath.size());
+ checkVertices(minusPath.get(0), Vector2D.ZERO, Vector2D.of(2, 0),
+ Vector2D.of(2, 1), Vector2D.of(0, 1), Vector2D.ZERO);
+
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testGeometricProperties_full() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertNull(tree.getBarycenter());
+
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+
+ Assert.assertEquals(0, tree.getBoundaries().size());
+ Assert.assertEquals(0, tree.getBoundaryPaths().size());
+ }
+
+ @Test
+ public void testGeometricProperties_empty() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+
+ // act/assert
+ Assert.assertEquals(0, tree.getSize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+
+ Assert.assertEquals(0, tree.getBoundaries().size());
+ Assert.assertEquals(0, tree.getBoundaryPaths().size());
+ }
+
+ @Test
+ public void testGeometricProperties_halfSpace() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+ tree.getRoot().cut(X_AXIS);
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertNull(tree.getBarycenter());
+
+ GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
+
+ List<Segment> segments = tree.getBoundaries();
+ Assert.assertEquals(1, segments.size());
+
+ Segment segment = segments.get(0);
+ Assert.assertSame(X_AXIS, segment.getLine());
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(1, path.getSegments().size());
+ assertSegmentsEqual(segment, path.getStartSegment());
+ }
+
+ @Test
+ public void testGeometricProperties_complementedHalfSpace() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+ tree.getRoot().cut(X_AXIS);
+
+ tree.complement();
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertNull(tree.getBarycenter());
+
+ GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
+
+ List<Segment> segments = tree.getBoundaries();
+ Assert.assertEquals(1, segments.size());
+
+ Segment segment = segments.get(0);
+ Assert.assertEquals(X_AXIS.reverse(), segment.getLine());
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(1, path.getSegments().size());
+ assertSegmentsEqual(segment, path.getSegments().get(0));
+ }
+
+ @Test
+ public void testGeometricProperties_quadrant() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.getRoot().cut(X_AXIS)
+ .getMinus().cut(Y_AXIS);
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertNull(tree.getBarycenter());
+
+ GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
+
+ List<Segment> segments = new ArrayList<>(tree.getBoundaries());
+ Assert.assertEquals(2, segments.size());
+
+ Collections.sort(segments, SEGMENT_COMPARATOR);
+
+ Segment firstSegment = segments.get(0);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, firstSegment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(firstSegment.getEndPoint());
+ Assert.assertSame(Y_AXIS, firstSegment.getLine());
+
+ Segment secondSegment = segments.get(1);
+ Assert.assertNull(secondSegment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, secondSegment.getEndPoint(), TEST_EPS);
+ Assert.assertSame(X_AXIS, secondSegment.getLine());
+
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(2, path.getSegments().size());
+ assertSegmentsEqual(secondSegment, path.getSegments().get(0));
+ assertSegmentsEqual(firstSegment, path.getSegments().get(1));
+ }
+
+ @Test
+ public void testGeometricProperties_complementedQuadrant() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.getRoot().cut(X_AXIS)
+ .getMinus().cut(Y_AXIS);
+
+ tree.complement();
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertNull(tree.getBarycenter());
+
+ GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
+
+ List<Segment> segments = new ArrayList<>(tree.getBoundaries());
+ Assert.assertEquals(2, segments.size());
+
+ Collections.sort(segments, SEGMENT_COMPARATOR);
+
+ Segment firstSegment = segments.get(0);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, firstSegment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(firstSegment.getEndPoint());
+ Assert.assertEquals(X_AXIS.reverse(), firstSegment.getLine());
+
+ Segment secondSegment = segments.get(1);
+ Assert.assertNull(secondSegment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, secondSegment.getEndPoint(), TEST_EPS);
+ Assert.assertEquals(Y_AXIS.reverse(), secondSegment.getLine());
+
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(2, path.getSegments().size());
+ assertSegmentsEqual(secondSegment, path.getSegments().get(0));
+ assertSegmentsEqual(firstSegment, path.getSegments().get(1));
+ }
+
+ @Test
+ public void testGeometricProperties_closedRegion() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.insert(Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(2, 1))
+ .close());
+
+ // act/assert
+ Assert.assertEquals(0.5, tree.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1.0 / 3.0), tree.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(1.0 + Math.sqrt(2) + Math.sqrt(5), tree.getBoundarySize(), TEST_EPS);
+
+ List<Segment> segments = new ArrayList<>(tree.getBoundaries());
+ Collections.sort(segments, SEGMENT_COMPARATOR);
+
+ Assert.assertEquals(3, segments.size());
+
+ checkFiniteSegment(segments.get(0), Vector2D.ZERO, Vector2D.of(1, 0));
+ checkFiniteSegment(segments.get(1), Vector2D.of(1, 0), Vector2D.of(2, 1));
+ checkFiniteSegment(segments.get(2), Vector2D.of(2, 1), Vector2D.ZERO);
+
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ checkVertices(paths.get(0), Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(2, 1), Vector2D.ZERO);
+ }
+
+ @Test
+ public void testGeometricProperties_complementedClosedRegion() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.insert(Polyline.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(2, 1))
+ .close());
+
+ tree.complement();
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertNull(tree.getBarycenter());
+
+ Assert.assertEquals(1.0 + Math.sqrt(2) + Math.sqrt(5), tree.getBoundarySize(), TEST_EPS);
+
+ List<Segment> segments = new ArrayList<>(tree.getBoundaries());
+ Collections.sort(segments, SEGMENT_COMPARATOR);
+
+ Assert.assertEquals(3, segments.size());
+
+ checkFiniteSegment(segments.get(0), Vector2D.ZERO, Vector2D.of(2, 1));
+ checkFiniteSegment(segments.get(1), Vector2D.of(1, 0), Vector2D.ZERO);
+ checkFiniteSegment(segments.get(2), Vector2D.of(2, 1), Vector2D.of(1, 0));
+
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ checkVertices(paths.get(0), Vector2D.ZERO, Vector2D.of(2, 1), Vector2D.of(1, 0), Vector2D.ZERO);
+ }
+
+ @Test
+ public void testGeometricProperties_regionWithHole() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.ZERO, Vector2D.of(3, 3))
+ .build();
+ RegionBSPTree2D inner = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.of(1, 1), Vector2D.of(2, 2))
+ .build();
+
+ tree.difference(inner);
+
+ // act/assert
+ Assert.assertEquals(8, tree.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), tree.getBarycenter(), TEST_EPS);
+
+ Assert.assertEquals(16, tree.getBoundarySize(), TEST_EPS);
+
+ List<Segment> segments = new ArrayList<>(tree.getBoundaries());
+ Collections.sort(segments, SEGMENT_COMPARATOR);
+
+ Assert.assertEquals(8, segments.size());
+
+ checkFiniteSegment(segments.get(0), Vector2D.ZERO, Vector2D.of(3, 0));
+ checkFiniteSegment(segments.get(1), Vector2D.of(0, 3), Vector2D.ZERO);
+ checkFiniteSegment(segments.get(2), Vector2D.of(1, 1), Vector2D.of(1, 2));
+ checkFiniteSegment(segments.get(3), Vector2D.of(1, 2), Vector2D.of(2, 2));
+ checkFiniteSegment(segments.get(4), Vector2D.of(2, 1), Vector2D.of(1, 1));
+ checkFiniteSegment(segments.get(5), Vector2D.of(2, 2), Vector2D.of(2, 1));
+ checkFiniteSegment(segments.get(6), Vector2D.of(3, 0), Vector2D.of(3, 3));
+ checkFiniteSegment(segments.get(7), Vector2D.of(3, 3), Vector2D.of(0, 3));
+
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(2, paths.size());
+
+ checkVertices(paths.get(0), Vector2D.ZERO, Vector2D.of(3, 0), Vector2D.of(3, 3),
+ Vector2D.of(0, 3), Vector2D.ZERO);
+ checkVertices(paths.get(1), Vector2D.of(1, 1), Vector2D.of(1, 2), Vector2D.of(2, 2),
+ Vector2D.of(2, 1), Vector2D.of(1, 1));
+ }
+
+ @Test
+ public void testGeometricProperties_complementedRegionWithHole() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.ZERO, Vector2D.of(3, 3))
+ .build();
+ RegionBSPTree2D inner = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.of(1, 1), Vector2D.of(2, 2))
+ .build();
+
+ tree.difference(inner);
+
+ tree.complement();
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ Assert.assertNull(tree.getBarycenter());
+
+ Assert.assertEquals(16, tree.getBoundarySize(), TEST_EPS);
+
+ List<Segment> segments = new ArrayList<>(tree.getBoundaries());
+ Collections.sort(segments, SEGMENT_COMPARATOR);
+
+ Assert.assertEquals(8, segments.size());
+
+ checkFiniteSegment(segments.get(0), Vector2D.ZERO, Vector2D.of(0, 3));
+ checkFiniteSegment(segments.get(1), Vector2D.of(0, 3), Vector2D.of(3, 3));
+ checkFiniteSegment(segments.get(2), Vector2D.of(1, 1), Vector2D.of(2, 1));
+ checkFiniteSegment(segments.get(3), Vector2D.of(1, 2), Vector2D.of(1, 1));
+ checkFiniteSegment(segments.get(4), Vector2D.of(2, 1), Vector2D.of(2, 2));
+ checkFiniteSegment(segments.get(5), Vector2D.of(2, 2), Vector2D.of(1, 2));
+ checkFiniteSegment(segments.get(6), Vector2D.of(3, 0), Vector2D.ZERO);
+ checkFiniteSegment(segments.get(7), Vector2D.of(3, 3), Vector2D.of(3, 0));
+
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(2, paths.size());
+
+ checkVertices(paths.get(0), Vector2D.ZERO, Vector2D.of(0, 3), Vector2D.of(3, 3),
+ Vector2D.of(3, 0), Vector2D.ZERO);
+ checkVertices(paths.get(1), Vector2D.of(1, 1), Vector2D.of(2, 1), Vector2D.of(2, 2),
+ Vector2D.of(1, 2), Vector2D.of(1, 1));
+ }
+
+ @Test
+ public void testFromConvexArea_full() {
+ // arrange
+ ConvexArea area = ConvexArea.full();
+
+ // act
+ RegionBSPTree2D tree = RegionBSPTree2D.from(area);
+ Assert.assertNull(tree.getBarycenter());
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ }
+
+ @Test
+ public void testFromConvexArea_infinite() {
+ // arrange
+ ConvexArea area = ConvexArea.fromVertices(
+ Arrays.asList(Vector2D.ZERO, Vector2D.Unit.PLUS_Y), TEST_PRECISION);
+
+ // act
+ RegionBSPTree2D tree = RegionBSPTree2D.from(area);
+
+ // assert
+ GeometryTestUtils.assertPositiveInfinity(tree.getSize());
+ GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
+ Assert.assertNull(tree.getBarycenter());
+
+ checkClassify(tree, RegionLocation.OUTSIDE, Vector2D.of(1, 0));
+ checkClassify(tree, RegionLocation.BOUNDARY, Vector2D.ZERO);
+ checkClassify(tree, RegionLocation.INSIDE, Vector2D.of(-1, 0));
+ }
+
+ @Test
+ public void testFromConvexArea_finite() {
+ // arrange
+ ConvexArea area = ConvexArea.fromVertexLoop(
+ Arrays.asList(Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y), TEST_PRECISION);
+
+ // act
+ RegionBSPTree2D tree = RegionBSPTree2D.from(area);
+
+ // assert
+ Assert.assertEquals(1, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(4, tree.getBoundarySize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Vector2D.of(-1, 0.5), Vector2D.of(2, 0.5),
+ Vector2D.of(0.5, -1), Vector2D.of(0.5, 2));
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Vector2D.of(0, 0.5), Vector2D.of(1, 0.5),
+ Vector2D.of(0.5, 0), Vector2D.of(0.5, 1));
+ checkClassify(tree, RegionLocation.INSIDE, Vector2D.of(0.5, 0.5));
+ }
+
+ @Test
+ public void testBuilder_rectMethods() {
+ // act
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addCenteredRect(Vector2D.ZERO, 1, 2)
+ .addCenteredSquare(Vector2D.of(5, 0), -2)
+
+ .addRect(Vector2D.of(10, 5), -2, -3)
+ .addRect(Vector2D.of(15, 5), Vector2D.of(16, 4))
+
+ .addSquare(Vector2D.of(20, 1), 3)
+
+ .build();
+
+ // assert
+ Assert.assertEquals(2 + 4 + 6 + 1 + 9, tree.getSize(), TEST_EPS);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Vector2D.ZERO,
+ Vector2D.of(5, 0),
+ Vector2D.of(9, 3.5),
+ Vector2D.of(15.5, 4.5),
+ Vector2D.of(21.5, 2.5));
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Vector2D.of(-0.5, -1), Vector2D.of(0.5, 1),
+ Vector2D.of(4, -1), Vector2D.of(6, 1));
+ }
+
+ @Test
+ public void testBuilder_rectMethods_zeroSize() {
+ // arrange
+ final RegionBSPTree2D.Builder builder = RegionBSPTree2D.builder(TEST_PRECISION);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addRect(Vector2D.of(1, 1), 0, 2);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addRect(Vector2D.of(1, 1), 2, 0);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addRect(Vector2D.of(2, 3), 0, 0);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addRect(Vector2D.of(1, 1), Vector2D.of(1, 3));
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addRect(Vector2D.of(1, 1), Vector2D.of(3, 1));
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addRect(Vector2D.of(2, 3), Vector2D.of(2, 3));
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addCenteredRect(Vector2D.of(2, 3), 0, 1e-20);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addSquare(Vector2D.of(2, 3), 1e-20);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.addCenteredSquare(Vector2D.of(2, 3), 0);
+ }, GeometryValueException.class);
+ }
+
+ @Test
+ public void testBuilder_mixedArguments() {
+ // arrange
+ Line minusYAxis = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.MINUS_HALF_PI, TEST_PRECISION);
+
+ Polyline path = Polyline.builder(TEST_PRECISION)
+ .append(Vector2D.Unit.PLUS_X)
+ .append(Vector2D.of(1, 1))
+ .build();
+
+ // act
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .add(Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION))
+ .add(path)
+ .addSegment(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+ .add(new SubLine(minusYAxis, Interval.of(-1, 0, TEST_PRECISION).toTree()))
+ .build();
+
+ // assert
+ Assert.assertEquals(1, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(4, tree.getBoundarySize(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_fullAndEmpty() {
+ // act/assert
+ Assert.assertNull(RegionBSPTree2D.full().project(Vector2D.ZERO));
+ Assert.assertNull(RegionBSPTree2D.empty().project(Vector2D.of(1, 2)));
+ }
+
+ @Test
+ public void testProject_halfSpace() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.full();
+ tree.getRoot().cut(X_AXIS);
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, tree.project(Vector2D.ZERO), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 0), tree.project(Vector2D.of(-1, 0)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 0),
+ tree.project(Vector2D.of(2, -1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-3, 0),
+ tree.project(Vector2D.of(-3, 1)), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_rect() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addSquare(Vector2D.of(1, 1), 1)
+ .build();
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), tree.project(Vector2D.ZERO), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), tree.project(Vector2D.of(1, 0)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1), tree.project(Vector2D.of(1.5, 0)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), tree.project(Vector2D.of(2, 0)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), tree.project(Vector2D.of(3, 0)), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), tree.project(Vector2D.of(1, 3)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), tree.project(Vector2D.of(1, 3)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 2), tree.project(Vector2D.of(1.5, 3)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 2), tree.project(Vector2D.of(2, 3)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 2), tree.project(Vector2D.of(3, 3)), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1.5), tree.project(Vector2D.of(0, 1.5)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1.5), tree.project(Vector2D.of(1.5, 1.5)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1.5), tree.project(Vector2D.of(3, 1.5)), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addRect(Vector2D.of(1, 1), 2, 1)
+ .build();
+
+ AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(0.5, 2)
+ .rotate(Geometry.HALF_PI)
+ .translate(Vector2D.of(0, -1));
+
+ // act
+ tree.transform(transform);
+
+ // assert
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(4, path.getSegments().size());
+ checkFiniteSegment(path.getSegments().get(0), Vector2D.of(-4, -0.5), Vector2D.of(-2, -0.5));
+ checkFiniteSegment(path.getSegments().get(1), Vector2D.of(-2, -0.5), Vector2D.of(-2, 0.5));
+ checkFiniteSegment(path.getSegments().get(2), Vector2D.of(-2, 0.5), Vector2D.of(-4, 0.5));
+ checkFiniteSegment(path.getSegments().get(3), Vector2D.of(-4, 0.5), Vector2D.of(-4, -0.5));
+ }
+
+ @Test
+ public void testTransform_halfSpace() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.getRoot().insertCut(Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION));
+
+ AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(0.5, 2)
+ .rotate(Geometry.HALF_PI)
+ .translate(Vector2D.of(1, 0));
+
+ // act
+ tree.transform(transform);
+
+ // assert
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(1, path.getSegments().size());
+ Segment segment = path.getStartSegment();
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+
+ Line expectedLine = Line.fromPointAndAngle(Vector2D.of(-1, 0), Geometry.HALF_PI, TEST_PRECISION);
+ Assert.assertTrue(expectedLine.eq(segment.getLine()));
+ }
+
+ @Test
+ public void testTransform_fullAndEmpty() {
+ // arrange
+ RegionBSPTree2D full = RegionBSPTree2D.full();
+ RegionBSPTree2D empty = RegionBSPTree2D.empty();
+
+ AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(Geometry.HALF_PI);
+
+ // act
+ full.transform(transform);
+ empty.transform(transform);
+
+ // assert
+ Assert.assertTrue(full.isFull());
+ Assert.assertTrue(empty.isEmpty());
+ }
+
+ @Test
+ public void testTransform_reflection() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addSquare(Vector2D.of(1, 1), 1)
+ .build();
+
+ Transform2D transform = FunctionTransform2D.from(v -> Vector2D.of(-v.getX(), v.getY()));
+
+ // act
+ tree.transform(transform);
+
+ // assert
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(4, path.getSegments().size());
+ checkFiniteSegment(path.getSegments().get(0), Vector2D.of(-2, 1), Vector2D.of(-1, 1));
+ checkFiniteSegment(path.getSegments().get(1), Vector2D.of(-1, 1), Vector2D.of(-1, 2));
+ checkFiniteSegment(path.getSegments().get(2), Vector2D.of(-1, 2), Vector2D.of(-2, 2));
+ checkFiniteSegment(path.getSegments().get(3), Vector2D.of(-2, 2), Vector2D.of(-2, 1));
+ }
+
+ @Test
+ public void testTransform_doubleReflection() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addSquare(Vector2D.of(1, 1), 1)
+ .build();
+
+ Transform2D transform = FunctionTransform2D.from(Vector2D::negate);
+
+ // act
+ tree.transform(transform);
+
+ // assert
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ Polyline path = paths.get(0);
+ Assert.assertEquals(4, path.getSegments().size());
+ checkFiniteSegment(path.getSegments().get(0), Vector2D.of(-2, -2), Vector2D.of(-1, -2));
+ checkFiniteSegment(path.getSegments().get(1), Vector2D.of(-1, -2), Vector2D.of(-1, -1));
+ checkFiniteSegment(path.getSegments().get(2), Vector2D.of(-1, -1), Vector2D.of(-2, -1));
+ checkFiniteSegment(path.getSegments().get(3), Vector2D.of(-2, -1), Vector2D.of(-2, -2));
+ }
+
+ @Test
+ public void testBooleanOperations() {
+ // arrange
+ RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
+ .addSquare(Vector2D.ZERO, 3)
+ .build();
+ RegionBSPTree2D temp;
+
+ // act
+ temp = RegionBSPTree2D.builder(TEST_PRECISION).addSquare(Vector2D.of(1, 1), 1).build();
+ temp.complement();
+ tree.intersection(temp);
+
+ temp = RegionBSPTree2D.builder(TEST_PRECISION).addSquare(Vector2D.of(3, 0), 3).build();
+ tree.union(temp);
+
+ temp = RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(2, 1), 3, 1).build();
+ tree.difference(temp);
+
+ temp.setFull();
+ tree.xor(temp);
+
+ // assert
+ List<Polyline> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(2, paths.size());
+
+ checkVertices(paths.get(0), Vector2D.ZERO, Vector2D.of(0, 3), Vector2D.of(6, 3),
+ Vector2D.of(6, 0), Vector2D.ZERO);
+
+ checkVertices(paths.get(1), Vector2D.of(1, 1), Vector2D.of(5, 1), Vector2D.of(5, 2),
+ Vector2D.of(1, 2), Vector2D.of(1, 1));
+ }
+
+ private static void assertSegmentsEqual(Segment expected, Segment actual) {
+ Assert.assertEquals(expected.getLine(), actual.getLine());
+
+ Vector2D expectedStart = expected.getStartPoint();
+ Vector2D expectedEnd = expected.getEndPoint();
+
+ if (expectedStart != null) {
+ EuclideanTestUtils.assertCoordinatesEqual(expectedStart, actual.getStartPoint(), TEST_EPS);
+ }
+ else {
+ Assert.assertNull(actual.getStartPoint());
+ }
+
+ if (expectedEnd != null) {
+ EuclideanTestUtils.assertCoordinatesEqual(expectedEnd, actual.getEndPoint(), TEST_EPS);
+ }
+ else {
+ Assert.assertNull(actual.getEndPoint());
+ }
+ }
+
+ private static void checkFiniteSegment(Segment segment, Vector2D start, Vector2D end) {
+ EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
+ }
+
+ private static void checkClassify(Region<Vector2D> region, RegionLocation loc, Vector2D ... points) {
+ for (Vector2D point : points) {
+ String msg = "Unexpected location for point " + point;
+
+ Assert.assertEquals(msg, loc, region.classify(point));
+ }
+ }
+
+ private static void checkConvexArea(final ConvexArea area, final List<Vector2D> inside, final List<Vector2D> outside) {
+ checkClassify(area, RegionLocation.INSIDE, inside.toArray(new Vector2D[0]));
+ checkClassify(area, RegionLocation.OUTSIDE, outside.toArray(new Vector2D[0]));
+ }
+
+ /** Assert that the given path is finite and contains the given vertices.
+ * @param path
+ * @param vertices
+ */
+ private static void checkVertices(Polyline path, Vector2D ... vertices) {
+ Assert.assertTrue("Line segment path is not finite", path.isFinite());
+
+ List<Vector2D> actual = path.getVertices();
+
+ Assert.assertEquals("Vertex lists have different lengths", vertices.length, actual.size());
+
+ for (int i=0; i<vertices.length; ++i) {
+ EuclideanTestUtils.assertCoordinatesEqual(vertices[i], actual.get(i), TEST_EPS);
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java
index d6085b6..704795b 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java
@@ -16,8 +16,19 @@
*/
package org.apache.commons.geometry.euclidean.twod;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.junit.Assert;
import org.junit.Test;
@@ -29,21 +40,861 @@
new EpsilonDoublePrecisionContext(TEST_EPS);
@Test
- public void testDistance() {
- Vector2D start = Vector2D.of(2, 2);
- Vector2D end = Vector2D.of(-2, -2);
- Segment segment = new Segment(start, end, Line.fromPoints(start, end, TEST_PRECISION));
+ public void testFromPoints() {
+ // arrange
+ Vector2D p0 = Vector2D.of(1, 3);
+ Vector2D p1 = Vector2D.of(1, 2);
+ Vector2D p2 = Vector2D.of(-3, 4);
+ Vector2D p3 = Vector2D.of(-5, -6);
- // distance to center of segment
- Assert.assertEquals(Math.sqrt(2), segment.distance(Vector2D.of(1, -1)), TEST_EPS);
+ // act/assert
- // distance a point on segment
- Assert.assertEquals(Math.sin(Math.PI / 4.0), segment.distance(Vector2D.of(0, -1)), TEST_EPS);
+ checkFiniteSegment(Segment.fromPoints(p0, p1, TEST_PRECISION), p0, p1);
+ checkFiniteSegment(Segment.fromPoints(p1, p0, TEST_PRECISION), p1, p0);
- // distance to end point
- Assert.assertEquals(Math.sqrt(8), segment.distance(Vector2D.of(0, 4)), TEST_EPS);
+ checkFiniteSegment(Segment.fromPoints(p0, p2, TEST_PRECISION), p0, p2);
+ checkFiniteSegment(Segment.fromPoints(p2, p0, TEST_PRECISION), p2, p0);
- // distance to start point
- Assert.assertEquals(Math.sqrt(8), segment.distance(Vector2D.of(0, -4)), TEST_EPS);
+ checkFiniteSegment(Segment.fromPoints(p0, p3, TEST_PRECISION), p0, p3);
+ checkFiniteSegment(Segment.fromPoints(p3, p0, TEST_PRECISION), p3, p0);
+ }
+
+ @Test
+ public void testFromPoints_invalidArgs() {
+ // arrange
+ Vector2D p0 = Vector2D.of(-1, 2);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Segment.fromPoints(p0, p0, TEST_PRECISION);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Segment.fromPoints(p0, Vector2D.POSITIVE_INFINITY, TEST_PRECISION);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Segment.fromPoints(p0, Vector2D.NEGATIVE_INFINITY, TEST_PRECISION);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Segment.fromPoints(p0, Vector2D.NaN, TEST_PRECISION);
+ }, GeometryValueException.class);
+ }
+
+ @Test
+ public void testFromPointAndDirection() {
+ // act
+ Segment seg = Segment.fromPointAndDirection(Vector2D.of(1, 3), Vector2D.Unit.PLUS_Y, TEST_PRECISION);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 3), seg.getStartPoint(), TEST_EPS);
+ Assert.assertNull(seg.getEndPoint());
+
+ Line line = seg.getLine();
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 0), line.getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.PLUS_Y, line.getDirection(), TEST_EPS);
+ }
+
+ @Test
+ public void testFromInterval_intervalArg_finite() {
+ // arrange
+ DoublePrecisionContext intervalPrecision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.of(-1, 2, intervalPrecision);
+
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, interval);
+
+ // assert
+ double side = 1.0 / Math.sqrt(2);
+ checkFiniteSegment(segment, Vector2D.of(-side, -side), Vector2D.of(2 * side, 2 * side));
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_intervalArg_full() {
+ // arrange
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, Interval.full());
+
+ // assert
+ GeometryTestUtils.assertNegativeInfinity(segment.getSubspaceStart());
+ GeometryTestUtils.assertPositiveInfinity(segment.getSubspaceEnd());
+
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertSame(Interval.full(), segment.getInterval());
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_intervalArg_positiveHalfSpace() {
+ // arrange
+ DoublePrecisionContext intervalPrecision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.min(-1, intervalPrecision);
+
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, interval);
+
+ // assert
+ Assert.assertEquals(-1.0, segment.getSubspaceStart(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(segment.getSubspaceEnd());
+
+ double side = 1.0 / Math.sqrt(2);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-side, -side), segment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertSame(interval, segment.getInterval());
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_intervalArg_negativeHalfSpace() {
+ // arrange
+ DoublePrecisionContext intervalPrecision = new EpsilonDoublePrecisionContext(1e-2);
+ Interval interval = Interval.max(2, intervalPrecision);
+
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, interval);
+
+ // assert
+ GeometryTestUtils.assertNegativeInfinity(segment.getSubspaceStart());
+ Assert.assertEquals(2, segment.getSubspaceEnd(), TEST_EPS);
+
+ double side = 1.0 / Math.sqrt(2);
+
+ Assert.assertNull(segment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2 * side, 2 * side), segment.getEndPoint(), TEST_EPS);
+
+ Assert.assertSame(interval, segment.getInterval());
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_doubleArgs_finite() {
+ // arrange
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, -1, 2);
+
+ // assert
+ double side = 1.0 / Math.sqrt(2);
+ checkFiniteSegment(segment, Vector2D.of(-side, -side), Vector2D.of(2 * side, 2 * side));
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_doubleArgs_full() {
+ // arrange
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+ // assert
+ GeometryTestUtils.assertNegativeInfinity(segment.getSubspaceStart());
+ GeometryTestUtils.assertPositiveInfinity(segment.getSubspaceEnd());
+
+ Assert.assertNull(segment.getStartPoint());
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_doubleArgs_positiveHalfSpace() {
+ // arrange
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, -1, Double.POSITIVE_INFINITY);
+
+ // assert
+ Assert.assertEquals(-1.0, segment.getSubspaceStart(), TEST_EPS);
+ GeometryTestUtils.assertPositiveInfinity(segment.getSubspaceEnd());
+
+ double side = 1.0 / Math.sqrt(2);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-side, -side), segment.getStartPoint(), TEST_EPS);
+ Assert.assertNull(segment.getEndPoint());
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_doubleArgs_negativeHalfSpace() {
+ // arrange
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, 2, Double.NEGATIVE_INFINITY);
+
+ // assert
+ GeometryTestUtils.assertNegativeInfinity(segment.getSubspaceStart());
+ Assert.assertEquals(2, segment.getSubspaceEnd(), TEST_EPS);
+
+ double side = 1.0 / Math.sqrt(2);
+
+ Assert.assertNull(segment.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2 * side, 2 * side), segment.getEndPoint(), TEST_EPS);
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testFromInterval_vectorArgs() {
+ // arrange
+ Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ Segment segment = Segment.fromInterval(line, Vector1D.of(-1), Vector1D.of(2));
+
+ // assert
+ double side = 1.0 / Math.sqrt(2);
+ checkFiniteSegment(segment, Vector2D.of(-side, -side), Vector2D.of(2 * side, 2 * side));
+
+ Assert.assertSame(TEST_PRECISION, segment.getPrecision());
+ }
+
+ @Test
+ public void testIsFull() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(4, 5), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(Segment.fromInterval(line, Interval.full()).isFull());
+ Assert.assertTrue(Segment.fromInterval(line, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).isFull());
+
+ Assert.assertFalse(Segment.fromInterval(line, Interval.min(0, TEST_PRECISION)).isFull());
+ Assert.assertFalse(Segment.fromInterval(line, Interval.max(0, TEST_PRECISION)).isFull());
+
+ Assert.assertFalse(Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).isFull());
+
+ Assert.assertFalse(Segment.fromInterval(line, Interval.point(1, TEST_PRECISION)).isEmpty());
+ }
+
+ @Test
+ public void testIsInfinite() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(4, 5), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(Segment.fromInterval(line, Interval.full()).isInfinite());
+ Assert.assertTrue(Segment.fromInterval(line, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).isInfinite());
+
+ Assert.assertTrue(Segment.fromInterval(line, Interval.min(0, TEST_PRECISION)).isInfinite());
+ Assert.assertTrue(Segment.fromInterval(line, Interval.max(0, TEST_PRECISION)).isInfinite());
+
+ Assert.assertFalse(Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).isInfinite());
+ Assert.assertFalse(Segment.fromInterval(line, Interval.point(1, TEST_PRECISION)).isInfinite());
+ }
+
+ @Test
+ public void testIsFinite() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(4, 5), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).isFinite());
+ Assert.assertTrue(Segment.fromInterval(line, Interval.point(1, TEST_PRECISION)).isFinite());
+
+ Assert.assertFalse(Segment.fromInterval(line, Interval.full()).isFinite());
+ Assert.assertFalse(Segment.fromInterval(line, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).isFinite());
+
+ Assert.assertFalse(Segment.fromInterval(line, Interval.min(0, TEST_PRECISION)).isFinite());
+ Assert.assertFalse(Segment.fromInterval(line, Interval.max(0, TEST_PRECISION)).isFinite());
+ }
+
+ @Test
+ public void testIsEmpty_alwaysReturnsFalse() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(4, 5), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertFalse(Segment.fromInterval(line, Interval.full()).isEmpty());
+ Assert.assertFalse(Segment.fromInterval(line, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).isEmpty());
+
+ Assert.assertFalse(Segment.fromInterval(line, Interval.min(0, TEST_PRECISION)).isEmpty());
+ Assert.assertFalse(Segment.fromInterval(line, Interval.max(0, TEST_PRECISION)).isEmpty());
+
+ Assert.assertFalse(Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).isEmpty());
+
+ Assert.assertFalse(Segment.fromInterval(line, Interval.point(1, TEST_PRECISION)).isEmpty());
+ }
+
+ @Test
+ public void testGetSize() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.of(-1, 0), Vector2D.of(4, 5), TEST_PRECISION);
+
+ // act/assert
+ GeometryTestUtils.assertPositiveInfinity(Segment.fromInterval(line, Interval.full()).getSize());
+ GeometryTestUtils.assertPositiveInfinity(Segment.fromInterval(line, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).getSize());
+
+ GeometryTestUtils.assertPositiveInfinity(Segment.fromInterval(line, Interval.min(0, TEST_PRECISION)).getSize());
+ GeometryTestUtils.assertPositiveInfinity(Segment.fromInterval(line, Interval.max(0, TEST_PRECISION)).getSize());
+
+ Assert.assertEquals(Math.sqrt(2), Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).getSize(), TEST_EPS);
+ Assert.assertEquals(9.0, Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(1, 10), TEST_PRECISION).getSize(), TEST_EPS);
+
+ Assert.assertEquals(0.0, Segment.fromInterval(line, Interval.point(1, TEST_PRECISION)).getSize(), TEST_EPS);
+ Assert.assertEquals(1.0, Segment.fromInterval(line, Interval.of(1, 2, TEST_PRECISION)).getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+ Segment segment = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 2), precision);
+
+ // act/assert
+ checkClassify(segment, RegionLocation.OUTSIDE,
+ Vector2D.of(0.25, 1), Vector2D.of(0.75, 1),
+ Vector2D.of(-1, -2), Vector2D.of(-0.1, 0),
+ Vector2D.of(1.1, 2), Vector2D.of(2, 4));
+
+ checkClassify(segment, RegionLocation.BOUNDARY,
+ Vector2D.ZERO, Vector2D.of(0.005, 0),
+ Vector2D.of(1, 2), Vector2D.of(1, 1.995));
+
+ checkClassify(segment, RegionLocation.INSIDE,
+ Vector2D.of(0.25, 0.5), Vector2D.of(0.495, 1),
+ Vector2D.of(0.75, 1.5));
+ }
+
+ @Test
+ public void testClosest() {
+ // arrange
+ Segment segment = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, segment.closest(Vector2D.of(-1, -1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, segment.closest(Vector2D.of(-2, 2)), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), segment.closest(Vector2D.of(0.5, 0.5)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), segment.closest(Vector2D.of(0, 1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), segment.closest(Vector2D.of(1, 0)), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), segment.closest(Vector2D.of(2, 2)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), segment.closest(Vector2D.of(5, 10)), TEST_EPS);
+ }
+
+ @Test
+ public void testToConvex() {
+ // arrange
+ Segment segment = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+ // act
+ List<Segment> segments = segment.toConvex();
+
+ // assert
+ Assert.assertEquals(1, segments.size());
+ Assert.assertSame(segment, segments.get(0));
+ }
+
+ @Test
+ public void testTransform_finite() {
+ // arrange
+ Segment segment = Segment.fromPoints(Vector2D.of(0, 1), Vector2D.of(2, 3), TEST_PRECISION);
+
+ Transform2D translation = AffineTransformMatrix2D.createTranslation(-1, 1);
+ Transform2D rotation = AffineTransformMatrix2D.createRotation(Geometry.HALF_PI);
+ Transform2D scale = AffineTransformMatrix2D.createScale(2, 3);
+ Transform2D reflect = FunctionTransform2D.from((pt) -> Vector2D.of(pt.getX(), -pt.getY()));
+
+ // act/assert
+ checkFiniteSegment(segment.transform(translation), Vector2D.of(-1, 2), Vector2D.of(1, 4));
+ checkFiniteSegment(segment.transform(rotation), Vector2D.of(-1, 0), Vector2D.of(-3, 2));
+ checkFiniteSegment(segment.transform(scale), Vector2D.of(0, 3), Vector2D.of(4, 9));
+ checkFiniteSegment(segment.transform(reflect), Vector2D.of(0, -1), Vector2D.of(2, -3));
+ }
+
+ @Test
+ public void testTransform_singlePoint() {
+ // arrange
+ Segment segment = Segment.fromInterval(Line.fromPoints(Vector2D.of(0, 1), Vector2D.of(1, 1), TEST_PRECISION),
+ Interval.point(0, TEST_PRECISION));
+
+ Transform2D translation = AffineTransformMatrix2D.createTranslation(-1, 1);
+ Transform2D rotation = AffineTransformMatrix2D.createRotation(Geometry.HALF_PI);
+ Transform2D scale = AffineTransformMatrix2D.createScale(2, 3);
+ Transform2D reflect = FunctionTransform2D.from((pt) -> Vector2D.of(pt.getX(), -pt.getY()));
+
+ // act/assert
+ checkFiniteSegment(segment.transform(translation), Vector2D.of(-1, 2), Vector2D.of(-1, 2));
+ checkFiniteSegment(segment.transform(rotation), Vector2D.of(-1, 0), Vector2D.of(-1, 0));
+ checkFiniteSegment(segment.transform(scale), Vector2D.of(0, 3), Vector2D.of(0, 3));
+ checkFiniteSegment(segment.transform(reflect), Vector2D.of(0, -1), Vector2D.of(0, -1));
+ }
+
+ @Test
+ public void testTransform_full() {
+ // arrange
+ Segment segment = Segment.fromInterval(Line.fromPoints(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+ Transform2D translation = AffineTransformMatrix2D.createTranslation(-1, 1);
+ Transform2D rotation = AffineTransformMatrix2D.createRotation(Geometry.HALF_PI);
+ Transform2D scale = AffineTransformMatrix2D.createScale(2, 3);
+ Transform2D reflect = FunctionTransform2D.from((pt) -> Vector2D.of(pt.getX(), -pt.getY()));
+
+ // act/assert
+ Segment translated = segment.transform(translation);
+ Assert.assertTrue(translated.isFull());
+ Assert.assertTrue(translated.contains(Vector2D.of(-1, 1)));
+ Assert.assertTrue(translated.contains(Vector2D.of(1, 2)));
+ Assert.assertNull(translated.getStartPoint());
+ Assert.assertNull(translated.getEndPoint());
+
+ Segment rotated = segment.transform(rotation);
+ Assert.assertTrue(rotated.isFull());
+ Assert.assertTrue(rotated.contains(Vector2D.ZERO));
+ Assert.assertTrue(rotated.contains(Vector2D.of(-1, 2)));
+ Assert.assertNull(rotated.getStartPoint());
+ Assert.assertNull(rotated.getEndPoint());
+
+ Segment scaled = segment.transform(scale);
+ Assert.assertTrue(scaled.isFull());
+ Assert.assertTrue(scaled.contains(Vector2D.ZERO));
+ Assert.assertTrue(scaled.contains(Vector2D.of(4, 3)));
+ Assert.assertNull(scaled.getStartPoint());
+ Assert.assertNull(scaled.getEndPoint());
+
+ Segment reflected = segment.transform(reflect);
+ Assert.assertTrue(reflected.isFull());
+ Assert.assertTrue(reflected.contains(Vector2D.ZERO));
+ Assert.assertTrue(reflected.contains(Vector2D.of(2, -1)));
+ Assert.assertNull(reflected.getStartPoint());
+ Assert.assertNull(reflected.getEndPoint());
+ }
+
+ @Test
+ public void testTransform_positiveHalfspace() {
+ // arrange
+ Segment segment = Segment.fromInterval(Line.fromPoints(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION),
+ 0.0, Double.POSITIVE_INFINITY);
+
+ Transform2D translation = AffineTransformMatrix2D.createTranslation(-1, 1);
+ Transform2D rotation = AffineTransformMatrix2D.createRotation(Geometry.HALF_PI);
+ Transform2D scale = AffineTransformMatrix2D.createScale(2, 3);
+ Transform2D reflect = FunctionTransform2D.from((pt) -> Vector2D.of(pt.getX(), -pt.getY()));
+
+ // act/assert
+ Segment translated = segment.transform(translation);
+ Assert.assertTrue(translated.isInfinite());
+ Assert.assertTrue(translated.contains(Vector2D.of(-1, 1)));
+ Assert.assertTrue(translated.contains(Vector2D.of(1, 2)));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), translated.getStartPoint(), TEST_EPS);
+ Assert.assertNull(translated.getEndPoint());
+
+ Segment rotated = segment.transform(rotation);
+ Assert.assertTrue(rotated.isInfinite());
+ Assert.assertTrue(rotated.contains(Vector2D.ZERO));
+ Assert.assertTrue(rotated.contains(Vector2D.of(-1, 2)));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, rotated.getStartPoint(), TEST_EPS);
+ Assert.assertNull(rotated.getEndPoint());
+
+ Segment scaled = segment.transform(scale);
+ Assert.assertTrue(scaled.isInfinite());
+ Assert.assertTrue(scaled.contains(Vector2D.ZERO));
+ Assert.assertTrue(scaled.contains(Vector2D.of(4, 3)));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, scaled.getStartPoint(), TEST_EPS);
+ Assert.assertNull(scaled.getEndPoint());
+
+ Segment reflected = segment.transform(reflect);
+ Assert.assertTrue(reflected.isInfinite());
+ Assert.assertTrue(reflected.contains(Vector2D.ZERO));
+ Assert.assertTrue(reflected.contains(Vector2D.of(2, -1)));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, reflected.getStartPoint(), TEST_EPS);
+ Assert.assertNull(reflected.getEndPoint());
+ }
+
+ @Test
+ public void testTransform_negativeHalfspace() {
+ // arrange
+ Segment segment = Segment.fromInterval(Line.fromPoints(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION),
+ Double.NEGATIVE_INFINITY, 0.0);
+
+ Transform2D translation = AffineTransformMatrix2D.createTranslation(-1, 1);
+ Transform2D rotation = AffineTransformMatrix2D.createRotation(Geometry.HALF_PI);
+ Transform2D scale = AffineTransformMatrix2D.createScale(2, 3);
+ Transform2D reflect = FunctionTransform2D.from((pt) -> Vector2D.of(pt.getX(), -pt.getY()));
+
+ // act/assert
+ Segment translated = segment.transform(translation);
+ Assert.assertTrue(translated.isInfinite());
+ Assert.assertTrue(translated.contains(Vector2D.of(-1, 1)));
+ Assert.assertTrue(translated.contains(Vector2D.of(-3, 0)));
+ Assert.assertNull(translated.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), translated.getEndPoint(), TEST_EPS);
+
+ Segment rotated = segment.transform(rotation);
+ Assert.assertTrue(rotated.isInfinite());
+ Assert.assertTrue(rotated.contains(Vector2D.ZERO));
+ Assert.assertTrue(rotated.contains(Vector2D.of(1, -2)));
+ Assert.assertNull(rotated.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, rotated.getEndPoint(), TEST_EPS);
+
+ Segment scaled = segment.transform(scale);
+ Assert.assertTrue(scaled.isInfinite());
+ Assert.assertTrue(scaled.contains(Vector2D.ZERO));
+ Assert.assertTrue(scaled.contains(Vector2D.of(-4, -3)));
+ Assert.assertNull(scaled.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, scaled.getEndPoint(), TEST_EPS);
+
+ Segment reflected = segment.transform(reflect);
+ Assert.assertTrue(reflected.isInfinite());
+ Assert.assertTrue(reflected.contains(Vector2D.ZERO));
+ Assert.assertTrue(reflected.contains(Vector2D.of(-2, 1)));
+ Assert.assertNull(reflected.getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, reflected.getEndPoint(), TEST_EPS);
+ }
+
+ @Test
+ public void testIntersection_line() {
+ // arrange
+ Segment aSeg = Segment.fromPoints(Vector2D.of(1, 0), Vector2D.of(2, 0), TEST_PRECISION);
+ Segment bSeg = Segment.fromPoints(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION);
+
+ Line xAxis = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ Line yAxis = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.HALF_PI, TEST_PRECISION);
+ Line angledLine = Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 0), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertNull(aSeg.intersection(xAxis));
+ Assert.assertNull(aSeg.intersection(yAxis));
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, bSeg.intersection(xAxis), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, bSeg.intersection(yAxis), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), bSeg.intersection(angledLine), TEST_EPS);
+ }
+
+ @Test
+ public void testIntersection_lineSegment() {
+ // arrange
+ Segment a = Segment.fromPoints(Vector2D.of(1, 0), Vector2D.of(2, 0), TEST_PRECISION);
+ Segment b = Segment.fromPoints(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION);
+ Segment c = Segment.fromPoints(Vector2D.of(-1, 0), Vector2D.ZERO, TEST_PRECISION);
+ Segment d = Segment.fromPoints(Vector2D.of(0, 3), Vector2D.of(3, 0), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertNull(a.intersection(a));
+ Assert.assertNull(a.intersection(c));
+ Assert.assertNull(a.intersection(b));
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, b.intersection(c), TEST_EPS);
+
+ Assert.assertNull(b.intersection(d));
+ Assert.assertNull(d.intersection(b));
+ }
+
+ @Test
+ public void testReverse_full() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+ Segment segment = line.span();
+
+ // act
+ Segment result = segment.reverse();
+
+ // assert
+ checkInfiniteSegment(result, segment.getLine().reverse(), null, null);
+ }
+
+ @Test
+ public void testReverse_positiveHalfSpace() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+ Segment segment = line.segment(1, Double.POSITIVE_INFINITY);
+
+ // act
+ Segment result = segment.reverse();
+
+ // assert
+ checkInfiniteSegment(result, segment.getLine().reverse(), null, Vector2D.of(1, 0));
+ }
+
+ @Test
+ public void testReverse_negativeHalfSpace() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+ Segment segment = line.segment(Double.NEGATIVE_INFINITY, 1);
+
+ // act
+ Segment result = segment.reverse();
+
+ // assert
+ checkInfiniteSegment(result, segment.getLine().reverse(), Vector2D.of(1, 0), null);
+ }
+
+ @Test
+ public void testReverse_finiteSegment() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+ Segment segment = line.segment(3, 4);
+
+ // act
+ Segment result = segment.reverse();
+
+ // assert
+ checkFiniteSegment(result, Vector2D.of(4, 0), Vector2D.of(3, 0));
+ }
+
+ @Test
+ public void testSplit_finite() {
+ // arrange
+ Vector2D start = Vector2D.of(1, 1);
+ Vector2D end = Vector2D.of(3, 2);
+ Vector2D middle = start.lerp(end, 0.5);
+
+ Segment seg = Segment.fromPoints(start, end, TEST_PRECISION);
+
+ // act/assert
+ Split<Segment> both = seg.split(Line.fromPointAndDirection(middle, Vector2D.of(1, -2), TEST_PRECISION));
+ checkFiniteSegment(both.getMinus(), middle, end);
+ checkFiniteSegment(both.getPlus(), start, middle);
+
+ Split<Segment> bothReversed = seg.split(Line.fromPointAndDirection(middle, Vector2D.of(-1, 2), TEST_PRECISION));
+ checkFiniteSegment(bothReversed.getMinus(), start, middle);
+ checkFiniteSegment(bothReversed.getPlus(), middle, end);
+
+ Split<Segment> minusOnlyOrthogonal = seg.split(Line.fromPointAndDirection(start, Vector2D.of(1, -2), TEST_PRECISION));
+ Assert.assertSame(seg, minusOnlyOrthogonal.getMinus());
+ Assert.assertNull(minusOnlyOrthogonal.getPlus());
+
+ Split<Segment> minusOnlyParallel = seg.split(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION));
+ Assert.assertSame(seg, minusOnlyParallel.getMinus());
+ Assert.assertNull(minusOnlyParallel.getPlus());
+
+ Split<Segment> plusOnlyOrthogonal = seg.split(Line.fromPointAndDirection(end, Vector2D.of(1, -2), TEST_PRECISION));
+ Assert.assertNull(plusOnlyOrthogonal.getMinus());
+ Assert.assertSame(seg, plusOnlyOrthogonal.getPlus());
+
+ Split<Segment> plusOnlyParallel = seg.split(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(-2, -1), TEST_PRECISION));
+ Assert.assertNull(plusOnlyParallel.getMinus());
+ Assert.assertSame(seg, plusOnlyParallel.getPlus());
+
+ Split<Segment> hyper = seg.split(Line.fromPointAndDirection(start, Vector2D.of(2, 1), TEST_PRECISION));
+ Assert.assertNull(hyper.getMinus());
+ Assert.assertNull(hyper.getPlus());
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ Vector2D p1 = Vector2D.of(1, 1);
+ Vector2D p2 = Vector2D.of(3, 2);
+ Vector2D middle = p1.lerp(p2, 0.5);
+
+ Line line = Line.fromPoints(p1, p2, TEST_PRECISION);
+
+ Segment seg = Segment.fromInterval(line, Interval.full());
+
+ // act/assert
+ Split<Segment> both = seg.split(Line.fromPointAndDirection(middle, Vector2D.of(1, -2), TEST_PRECISION));
+ checkInfiniteSegment(both.getMinus(), line, middle, null);
+ checkInfiniteSegment(both.getPlus(), line, null, middle);
+
+ Split<Segment> bothReversed = seg.split(Line.fromPointAndDirection(middle, Vector2D.of(-1, 2), TEST_PRECISION));
+ checkInfiniteSegment(bothReversed.getMinus(), line, null, middle);
+ checkInfiniteSegment(bothReversed.getPlus(), line, middle, null);
+
+ Split<Segment> minusOnlyParallel = seg.split(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION));
+ Assert.assertSame(seg, minusOnlyParallel.getMinus());
+ Assert.assertNull(minusOnlyParallel.getPlus());
+
+ Split<Segment> plusOnlyParallel = seg.split(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(-2, -1), TEST_PRECISION));
+ Assert.assertNull(plusOnlyParallel.getMinus());
+ Assert.assertSame(seg, plusOnlyParallel.getPlus());
+
+ Split<Segment> hyper = seg.split(Line.fromPointAndDirection(p1, Vector2D.of(2, 1), TEST_PRECISION));
+ Assert.assertNull(hyper.getMinus());
+ Assert.assertNull(hyper.getPlus());
+ }
+
+ @Test
+ public void testSplit_positiveHalfSpace() {
+ // arrange
+ Vector2D p1 = Vector2D.of(1, 1);
+ Vector2D p2 = Vector2D.of(3, 2);
+ Vector2D middle = p1.lerp(p2, 0.5);
+
+ Line line = Line.fromPoints(p1, p2, TEST_PRECISION);
+
+ Segment seg = Segment.fromInterval(line, Interval.min(line.toSubspace(p1).getX(), TEST_PRECISION));
+
+ // act/assert
+ Split<Segment> both = seg.split(Line.fromPointAndDirection(middle, Vector2D.of(1, -2), TEST_PRECISION));
+ checkInfiniteSegment(both.getMinus(), line, middle, null);
+ checkFiniteSegment(both.getPlus(), p1, middle);
+
+ Split<Segment> bothReversed = seg.split(Line.fromPointAndDirection(middle, Vector2D.of(-1, 2), TEST_PRECISION));
+ checkFiniteSegment(bothReversed.getMinus(), p1, middle);
+ checkInfiniteSegment(bothReversed.getPlus(), line, middle, null);
+
+ Split<Segment> minusOnlyParallel = seg.split(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION));
+ Assert.assertSame(seg, minusOnlyParallel.getMinus());
+ Assert.assertNull(minusOnlyParallel.getPlus());
+
+ Split<Segment> minusOnlyOrthogonal = seg.split(Line.fromPointAndDirection(p1, Vector2D.of(1, -2), TEST_PRECISION));
+ Assert.assertSame(seg, minusOnlyOrthogonal.getMinus());
+ Assert.assertNull(minusOnlyOrthogonal.getPlus());
+
+ Split<Segment> plusOnlyParallel = seg.split(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(-2, -1), TEST_PRECISION));
+ Assert.assertNull(plusOnlyParallel.getMinus());
+ Assert.assertSame(seg, plusOnlyParallel.getPlus());
+
+ Split<Segment> hyper = seg.split(Line.fromPointAndDirection(p1, Vector2D.of(2, 1), TEST_PRECISION));
+ Assert.assertNull(hyper.getMinus());
+ Assert.assertNull(hyper.getPlus());
+ }
+
+ @Test
+ public void testSplit_negativeHalfSpace() {
+ // arrange
+ Vector2D p1 = Vector2D.of(1, 1);
+ Vector2D p2 = Vector2D.of(3, 2);
+ Vector2D middle = p1.lerp(p2, 0.5);
+
+ Line line = Line.fromPoints(p1, p2, TEST_PRECISION);
+
+ Segment seg = Segment.fromInterval(line, Interval.max(line.toSubspace(p2).getX(), TEST_PRECISION));
+
+ // act/assert
+ Split<Segment> both = seg.split(Line.fromPointAndDirection(middle, Vector2D.of(1, -2), TEST_PRECISION));
+ checkFiniteSegment(both.getMinus(), middle, p2);
+ checkInfiniteSegment(both.getPlus(), line, null, middle);
+
+ Split<Segment> bothReversed = seg.split(Line.fromPointAndDirection(middle, Vector2D.of(-1, 2), TEST_PRECISION));
+ checkInfiniteSegment(bothReversed.getMinus(), line, null, middle);
+ checkFiniteSegment(bothReversed.getPlus(), middle, p2);
+
+ Split<Segment> minusOnlyParallel = seg.split(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION));
+ Assert.assertSame(seg, minusOnlyParallel.getMinus());
+ Assert.assertNull(minusOnlyParallel.getPlus());
+
+ Split<Segment> plusOnlyParallel = seg.split(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(-2, -1), TEST_PRECISION));
+ Assert.assertNull(plusOnlyParallel.getMinus());
+ Assert.assertSame(seg, plusOnlyParallel.getPlus());
+
+ Split<Segment> plusOnlyOrthogonal = seg.split(Line.fromPointAndDirection(p2, Vector2D.of(1, -2), TEST_PRECISION));
+ Assert.assertNull(plusOnlyOrthogonal.getMinus());
+ Assert.assertSame(seg, plusOnlyOrthogonal.getPlus());
+
+ Split<Segment> hyper = seg.split(Line.fromPointAndDirection(p1, Vector2D.of(2, 1), TEST_PRECISION));
+ Assert.assertNull(hyper.getMinus());
+ Assert.assertNull(hyper.getPlus());
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Line line = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+ Segment full = Segment.fromInterval(line, Interval.full());
+ Segment startOnly = Segment.fromInterval(line, 0, Double.POSITIVE_INFINITY);
+ Segment endOnly = Segment.fromInterval(line, Double.NEGATIVE_INFINITY, 0);
+ Segment finite = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act/assert
+ String fullStr = full.toString();
+ Assert.assertTrue(fullStr.contains("lineOrigin=") && fullStr.contains("lineDirection="));
+
+ String startOnlyStr = startOnly.toString();
+ Assert.assertTrue(startOnlyStr.contains("start=") && startOnlyStr.contains("direction="));
+
+ String endOnlyStr = endOnly.toString();
+ Assert.assertTrue(endOnlyStr.contains("direction=") && endOnlyStr.contains("end="));
+
+ String finiteStr = finite.toString();
+ Assert.assertTrue(finiteStr.contains("start=") && finiteStr.contains("end="));
+ }
+
+ @Test
+ public void testEmpty() {
+ // act
+ Polyline path = Polyline.empty();
+
+ // assert
+ Assert.assertTrue(path.isEmpty());
+ Assert.assertFalse(path.isClosed());
+ Assert.assertTrue(path.isFinite());
+ Assert.assertFalse(path.isInfinite());
+
+ Assert.assertEquals(0, path.getSegments().size());
+ }
+
+ private static void checkClassify(Segment segment, RegionLocation loc, Vector2D ... points) {
+ for (Vector2D pt : points) {
+ String msg = "Unexpected location for point " + pt;
+
+ Assert.assertEquals(msg, loc, segment.classify(pt));
+ }
+ }
+
+ private static void checkFiniteSegment(Segment segment, Vector2D start, Vector2D end) {
+ checkFiniteSegment(segment, start, end, TEST_PRECISION);
+ }
+
+ private static void checkFiniteSegment(Segment segment, Vector2D start, Vector2D end, DoublePrecisionContext precision) {
+ Assert.assertFalse(segment.isInfinite());
+
+ EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
+
+ Line line = segment.getLine();
+ Assert.assertEquals(HyperplaneLocation.ON, line.classify(segment.getStartPoint()));
+ Assert.assertEquals(HyperplaneLocation.ON, line.classify(segment.getEndPoint()));
+
+ Assert.assertEquals(line.toSubspace(segment.getStartPoint()).getX(), segment.getSubspaceStart(), TEST_EPS);
+ Assert.assertEquals(line.toSubspace(segment.getEndPoint()).getX(), segment.getSubspaceEnd(), TEST_EPS);
+
+ Assert.assertSame(precision, segment.getPrecision());
+ Assert.assertSame(precision, line.getPrecision());
+ }
+
+ private static void checkInfiniteSegment(Segment segment, Line line, Vector2D start, Vector2D end) {
+ checkInfiniteSegment(segment, line, start, end, TEST_PRECISION);
+ }
+
+ private static void checkInfiniteSegment(Segment segment, Line line, Vector2D start, Vector2D end,
+ DoublePrecisionContext precision) {
+
+ Assert.assertTrue(segment.isInfinite());
+
+ Assert.assertEquals(line, segment.getLine());
+
+ if (start == null) {
+ Assert.assertNull(segment.getStartPoint());
+ }
+ else {
+ EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
+ Assert.assertEquals(line.toSubspace(segment.getStartPoint()).getX(), segment.getSubspaceStart(), TEST_EPS);
+ }
+
+ if (end == null) {
+ Assert.assertNull(segment.getEndPoint());
+ }
+ else {
+ EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
+ Assert.assertEquals(line.toSubspace(segment.getEndPoint()).getX(), segment.getSubspaceEnd(), TEST_EPS);
+ }
+
+ Assert.assertSame(precision, segment.getPrecision());
+ Assert.assertSame(precision, line.getPrecision());
}
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SubLineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SubLineTest.java
index 2e086bc..f663ebe 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SubLineTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SubLineTest.java
@@ -18,11 +18,23 @@
import java.util.List;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.twod.SubLine.SubLineBuilder;
import org.junit.Assert;
import org.junit.Test;
@@ -33,130 +45,628 @@
private static final DoublePrecisionContext TEST_PRECISION =
new EpsilonDoublePrecisionContext(TEST_EPS);
+ private Line line = Line.fromPointAndDirection(Vector2D.of(0, 1), Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
@Test
- public void testEndPoints() {
- Vector2D p1 = Vector2D.of(-1, -7);
- Vector2D p2 = Vector2D.of(7, -1);
- Segment segment = new Segment(p1, p2, Line.fromPoints(p1, p2, TEST_PRECISION));
- SubLine sub = new SubLine(segment);
- List<Segment> segments = sub.getSegments();
- Assert.assertEquals(1, segments.size());
- Assert.assertEquals(0.0, Vector2D.of(-1, -7).distance(segments.get(0).getStart()), TEST_EPS);
- Assert.assertEquals(0.0, Vector2D.of( 7, -1).distance(segments.get(0).getEnd()), TEST_EPS);
+ public void testCtor_lineOnly() {
+ // act
+ SubLine sub = new SubLine(line);
+
+ // assert
+ Assert.assertSame(line, sub.getLine());
+ Assert.assertSame(TEST_PRECISION, sub.getPrecision());
+
+ Assert.assertFalse(sub.isFull());
+ Assert.assertTrue(sub.isEmpty());
+ Assert.assertFalse(sub.isInfinite());
+ Assert.assertTrue(sub.isFinite());
}
@Test
- public void testNoEndPoints() {
- SubLine wholeLine = Line.fromPoints(Vector2D.of(-1, 7), Vector2D.of(7, 1), TEST_PRECISION).wholeHyperplane();
- List<Segment> segments = wholeLine.getSegments();
- Assert.assertEquals(1, segments.size());
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getX()) &&
- segments.get(0).getStart().getX() < 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getY()) &&
- segments.get(0).getStart().getY() > 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getX()) &&
- segments.get(0).getEnd().getX() > 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getY()) &&
- segments.get(0).getEnd().getY() < 0);
+ public void testCtor_lineAndBoolean() {
+ // act
+ SubLine sub = new SubLine(line, true);
+
+ // assert
+ Assert.assertSame(line, sub.getLine());
+ Assert.assertSame(TEST_PRECISION, sub.getPrecision());
+
+ Assert.assertTrue(sub.isFull());
+ Assert.assertFalse(sub.isEmpty());
+ Assert.assertTrue(sub.isInfinite());
+ Assert.assertFalse(sub.isFinite());
}
@Test
- public void testNoSegments() {
- SubLine empty = new SubLine(Line.fromPoints(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION),
- new RegionFactory<Vector1D>().getComplement(new IntervalsSet(TEST_PRECISION)));
- List<Segment> segments = empty.getSegments();
+ public void testCtor_lineAndRegion() {
+ // arrange
+ RegionBSPTree1D tree = RegionBSPTree1D.full();
+
+ // act
+ SubLine sub = new SubLine(line, tree);
+
+ // assert
+ Assert.assertSame(line, sub.getLine());
+ Assert.assertSame(tree, sub.getSubspaceRegion());
+ Assert.assertSame(TEST_PRECISION, sub.getPrecision());
+
+ Assert.assertTrue(sub.isFull());
+ Assert.assertFalse(sub.isEmpty());
+ Assert.assertTrue(sub.isInfinite());
+ Assert.assertFalse(sub.isFinite());
+ }
+
+ @Test
+ public void testToConvex_full() {
+ // arrange
+ SubLine sub = new SubLine(line, true);
+
+ // act
+ List<Segment> segments = sub.toConvex();
+
+ // assert
+ Assert.assertEquals(1, segments.size());
+
+ Segment seg = segments.get(0);
+ Assert.assertTrue(seg.isFull());
+ }
+
+ @Test
+ public void testToConvex_empty() {
+ // arrange
+ SubLine sub = new SubLine(line, false);
+
+ // act
+ List<Segment> segments = sub.toConvex();
+
+ // assert
Assert.assertEquals(0, segments.size());
}
@Test
- public void testSeveralSegments() {
- SubLine twoSubs = new SubLine(Line.fromPoints(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION),
- new RegionFactory<Vector1D>().union(new IntervalsSet(1, 2, TEST_PRECISION),
- new IntervalsSet(3, 4, TEST_PRECISION)));
- List<Segment> segments = twoSubs.getSegments();
+ public void testToConvex_finiteAndInfiniteSegments() {
+ // arrange
+ SubLine sub = new SubLine(line, false);
+ RegionBSPTree1D tree = sub.getSubspaceRegion();
+ tree.add(Interval.max(-2.0, TEST_PRECISION));
+ tree.add(Interval.of(-1, 2, TEST_PRECISION));
+
+ // act
+ List<Segment> segments = sub.toConvex();
+
+ // assert
Assert.assertEquals(2, segments.size());
+
+ Assert.assertNull(segments.get(0).getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, 1), segments.get(0).getEndPoint(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), segments.get(1).getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 1), segments.get(1).getEndPoint(), TEST_EPS);
}
@Test
- public void testHalfInfiniteNeg() {
- SubLine empty = new SubLine(Line.fromPoints(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION),
- new IntervalsSet(Double.NEGATIVE_INFINITY, 0.0, TEST_PRECISION));
- List<Segment> segments = empty.getSegments();
- Assert.assertEquals(1, segments.size());
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getX()) &&
- segments.get(0).getStart().getX() < 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getStart().getY()) &&
- segments.get(0).getStart().getY() < 0);
- Assert.assertEquals(0.0, Vector2D.of(3, -4).distance(segments.get(0).getEnd()), TEST_EPS);
+ public void testAdd_lineSegment() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ Line otherLine = Line.fromPointAndAngle(Vector2D.of(0, 1), 1e-11, TEST_PRECISION);
+
+ SubLine subline = new SubLine(line);
+
+ // act
+ subline.add(Segment.fromInterval(line, 2, 4));
+ subline.add(Segment.fromInterval(otherLine, 1, 3));
+ subline.add(Segment.fromPoints(Vector2D.of(-4, 1), Vector2D.of(-1, 1), TEST_PRECISION));
+
+ // assert
+ Assert.assertFalse(subline.isFull());
+ Assert.assertFalse(subline.isEmpty());
+
+ List<Segment> segments = subline.toConvex();
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-4, 1), segments.get(0).getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), segments.get(0).getEndPoint(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), segments.get(1).getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4, 1), segments.get(1).getEndPoint(), TEST_EPS);
}
@Test
- public void testHalfInfinitePos() {
- SubLine empty = new SubLine(Line.fromPoints(Vector2D.of(-1, -7), Vector2D.of(7, -1), TEST_PRECISION),
- new IntervalsSet(0.0, Double.POSITIVE_INFINITY, TEST_PRECISION));
- List<Segment> segments = empty.getSegments();
- Assert.assertEquals(1, segments.size());
- Assert.assertEquals(0.0, Vector2D.of(3, -4).distance(segments.get(0).getStart()), TEST_EPS);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getX()) &&
- segments.get(0).getEnd().getX() > 0);
- Assert.assertTrue(Double.isInfinite(segments.get(0).getEnd().getY()) &&
- segments.get(0).getEnd().getY() > 0);
+ public void testAdd_subLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ SubLine a = new SubLine(line);
+ RegionBSPTree1D aTree = a.getSubspaceRegion();
+ aTree.add(Interval.max(-3, TEST_PRECISION));
+ aTree.add(Interval.of(1, 2, TEST_PRECISION));
+
+ SubLine b = new SubLine(line);
+ RegionBSPTree1D bTree = b.getSubspaceRegion();
+ bTree.add(Interval.of(2, 4, TEST_PRECISION));
+ bTree.add(Interval.of(-4, -2, TEST_PRECISION));
+
+ SubLine subline = new SubLine(line);
+
+ int aTreeCount = aTree.count();
+ int bTreeCount = bTree.count();
+
+ // act
+ subline.add(a);
+ subline.add(b);
+
+ // assert
+ Assert.assertFalse(subline.isFull());
+ Assert.assertFalse(subline.isEmpty());
+
+ List<Segment> segments = subline.toConvex();
+
+ Assert.assertEquals(2, segments.size());
+
+ Assert.assertNull(segments.get(0).getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, 1), segments.get(0).getEndPoint(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), segments.get(1).getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4, 1), segments.get(1).getEndPoint(), TEST_EPS);
+
+ Assert.assertEquals(aTreeCount, aTree.count());
+ Assert.assertEquals(bTreeCount, bTree.count());
}
@Test
- public void testIntersectionInsideInside() {
- SubLine sub1 = new SubLine(Vector2D.of(1, 1), Vector2D.of(3, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector2D.of(2, 0), Vector2D.of(2, 2), TEST_PRECISION);
- Assert.assertEquals(0.0, Vector2D.of(2, 1).distance(sub1.intersection(sub2, true)), TEST_EPS);
- Assert.assertEquals(0.0, Vector2D.of(2, 1).distance(sub1.intersection(sub2, false)), TEST_EPS);
+ public void testAdd_argumentsFromDifferentLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ Line otherLine = Line.fromPointAndAngle(Vector2D.of(0, 1), 1e-2, TEST_PRECISION);
+
+ SubLine subline = new SubLine(line);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ subline.add(Segment.fromInterval(otherLine, 0, 1));
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ subline.add(new SubLine(otherLine));
+ }, GeometryException.class);
}
@Test
- public void testIntersectionInsideBoundary() {
- SubLine sub1 = new SubLine(Vector2D.of(1, 1), Vector2D.of(3, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector2D.of(2, 0), Vector2D.of(2, 1), TEST_PRECISION);
- Assert.assertEquals(0.0, Vector2D.of(2, 1).distance(sub1.intersection(sub2, true)), TEST_EPS);
- Assert.assertNull(sub1.intersection(sub2, false));
+ public void testSplit_both_anglePositive() {
+ // arrange
+ RegionBSPTree1D subRegion = RegionBSPTree1D.empty();
+ subRegion.add(Interval.of(0, 2, TEST_PRECISION));
+ subRegion.add(Interval.of(3, 4, TEST_PRECISION));
+
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ SubLine subline = new SubLine(line, subRegion);
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(1, 0), 0.1 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<SubLine> split = subline.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ List<Segment> minusSegments = split.getMinus().toConvex();
+ Assert.assertEquals(1, minusSegments.size());
+ checkFiniteSegment(minusSegments.get(0), Vector2D.ZERO, Vector2D.of(1, 0));
+
+ List<Segment> plusSegments = split.getPlus().toConvex();
+ Assert.assertEquals(2, plusSegments.size());
+ checkFiniteSegment(plusSegments.get(0), Vector2D.of(1, 0), Vector2D.of(2, 0));
+ checkFiniteSegment(plusSegments.get(1), Vector2D.of(3, 0), Vector2D.of(4, 0));
}
@Test
- public void testIntersectionInsideOutside() {
- SubLine sub1 = new SubLine(Vector2D.of(1, 1), Vector2D.of(3, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector2D.of(2, 0), Vector2D.of(2, 0.5), TEST_PRECISION);
- Assert.assertNull(sub1.intersection(sub2, true));
- Assert.assertNull(sub1.intersection(sub2, false));
+ public void testSplit_both_angleNegative() {
+ // arrange
+ RegionBSPTree1D subRegion = RegionBSPTree1D.empty();
+ subRegion.add(Interval.of(0, 2, TEST_PRECISION));
+ subRegion.add(Interval.of(3, 4, TEST_PRECISION));
+
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ SubLine subline = new SubLine(line, subRegion);
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(1, 0), -0.9 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<SubLine> split = subline.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ List<Segment> minusSegments = split.getMinus().toConvex();
+ Assert.assertEquals(2, minusSegments.size());
+ checkFiniteSegment(minusSegments.get(0), Vector2D.of(1, 0), Vector2D.of(2, 0));
+ checkFiniteSegment(minusSegments.get(1), Vector2D.of(3, 0), Vector2D.of(4, 0));
+
+ List<Segment> plusSegments = split.getPlus().toConvex();
+ Assert.assertEquals(1, plusSegments.size());
+ checkFiniteSegment(plusSegments.get(0), Vector2D.ZERO, Vector2D.of(1, 0));
}
@Test
- public void testIntersectionBoundaryBoundary() {
- SubLine sub1 = new SubLine(Vector2D.of(1, 1), Vector2D.of(2, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector2D.of(2, 0), Vector2D.of(2, 1), TEST_PRECISION);
- Assert.assertEquals(0.0, Vector2D.of(2, 1).distance(sub1.intersection(sub2, true)), TEST_EPS);
- Assert.assertNull(sub1.intersection(sub2, false));
+ public void testSplit_intersection_plusOnly() {
+ // arrange
+ RegionBSPTree1D subRegion = RegionBSPTree1D.empty();
+ subRegion.add(Interval.of(0, 2, TEST_PRECISION));
+ subRegion.add(Interval.of(3, 4, TEST_PRECISION));
+
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ SubLine subline = new SubLine(line, subRegion);
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(-1, 0), 0.1 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<SubLine> split = subline.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(subline, split.getPlus());
}
@Test
- public void testIntersectionBoundaryOutside() {
- SubLine sub1 = new SubLine(Vector2D.of(1, 1), Vector2D.of(2, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector2D.of(2, 0), Vector2D.of(2, 0.5), TEST_PRECISION);
- Assert.assertNull(sub1.intersection(sub2, true));
- Assert.assertNull(sub1.intersection(sub2, false));
+ public void testSplit_intersection_minusOnly() {
+ // arrange
+ RegionBSPTree1D subRegion = RegionBSPTree1D.empty();
+ subRegion.add(Interval.of(0, 2, TEST_PRECISION));
+ subRegion.add(Interval.of(3, 4, TEST_PRECISION));
+
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ SubLine subline = new SubLine(line, subRegion);
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(10, 0), 0.1 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<SubLine> split = subline.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(subline, split.getMinus());
+ Assert.assertNull(split.getPlus());
}
@Test
- public void testIntersectionOutsideOutside() {
- SubLine sub1 = new SubLine(Vector2D.of(1, 1), Vector2D.of(1.5, 1), TEST_PRECISION);
- SubLine sub2 = new SubLine(Vector2D.of(2, 0), Vector2D.of(2, 0.5), TEST_PRECISION);
- Assert.assertNull(sub1.intersection(sub2, true));
- Assert.assertNull(sub1.intersection(sub2, false));
+ public void testSplit_parallel_plus() {
+ // arrange
+ RegionBSPTree1D subRegion = RegionBSPTree1D.empty();
+ subRegion.add(Interval.of(0, 2, TEST_PRECISION));
+ subRegion.add(Interval.of(3, 4, TEST_PRECISION));
+
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ SubLine subline = new SubLine(line, subRegion);
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ Split<SubLine> split = subline.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(subline, split.getPlus());
}
@Test
- public void testIntersectionParallel() {
- final SubLine sub1 = new SubLine(Vector2D.of(0, 1), Vector2D.of(0, 2), TEST_PRECISION);
- final SubLine sub2 = new SubLine(Vector2D.of(66, 3), Vector2D.of(66, 4), TEST_PRECISION);
- Assert.assertNull(sub1.intersection(sub2, true));
- Assert.assertNull(sub1.intersection(sub2, false));
+ public void testSplit_parallel_minus() {
+ // arrange
+ RegionBSPTree1D subRegion = RegionBSPTree1D.empty();
+ subRegion.add(Interval.of(0, 2, TEST_PRECISION));
+ subRegion.add(Interval.of(3, 4, TEST_PRECISION));
+
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ SubLine subline = new SubLine(line, subRegion);
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.of(0, -1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ Split<SubLine> split = subline.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(subline, split.getMinus());
+ Assert.assertNull(split.getPlus());
}
+ @Test
+ public void testSplit_coincident_sameDirection() {
+ // arrange
+ RegionBSPTree1D subRegion = RegionBSPTree1D.empty();
+ subRegion.add(Interval.of(0, 2, TEST_PRECISION));
+ subRegion.add(Interval.of(3, 4, TEST_PRECISION));
+
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ SubLine subline = new SubLine(line, subRegion);
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ Split<SubLine> split = subline.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_coincident_oppositeDirection() {
+ // arrange
+ RegionBSPTree1D subRegion = RegionBSPTree1D.empty();
+ subRegion.add(Interval.of(0, 2, TEST_PRECISION));
+ subRegion.add(Interval.of(3, 4, TEST_PRECISION));
+
+ Line line = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION);
+ SubLine subline = new SubLine(line, subRegion);
+
+ Line splitter = Line.fromPointAndAngle(Vector2D.ZERO, Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<SubLine> split = subline.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ AffineTransformMatrix2D mat = AffineTransformMatrix2D
+ .createRotation(Vector2D.of(0, 1), Geometry.HALF_PI)
+ .scale(Vector2D.of(3, 2));
+
+ SubLine subline = new SubLine(Line.fromPointAndAngle(Vector2D.ZERO, Geometry.ZERO_PI, TEST_PRECISION));
+ subline.getSubspaceRegion().add(Interval.of(0, 1, TEST_PRECISION));
+ subline.getSubspaceRegion().add(Interval.min(3, TEST_PRECISION));
+
+ // act
+ SubLine transformed = subline.transform(mat);
+
+ // assert
+ Assert.assertNotSame(subline, transformed);
+
+ List<Segment> originalSegments = subline.toConvex();
+ Assert.assertEquals(2, originalSegments.size());
+ checkFiniteSegment(originalSegments.get(0), Vector2D.ZERO, Vector2D.Unit.PLUS_X);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 0), originalSegments.get(1).getStartPoint(), TEST_EPS);
+ Assert.assertNull(originalSegments.get(1).getEndPoint());
+
+ List<Segment> transformedSegments = transformed.toConvex();
+ Assert.assertEquals(2, transformedSegments.size());
+ checkFiniteSegment(transformedSegments.get(0), Vector2D.of(3, 2), Vector2D.of(3, 4));
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 8), transformedSegments.get(1).getStartPoint(), TEST_EPS);
+ Assert.assertNull(transformedSegments.get(1).getEndPoint());
+ }
+
+ @Test
+ public void testTransform_reflection() {
+ // arrange
+ AffineTransformMatrix2D mat = AffineTransformMatrix2D.createScale(Vector2D.of(-1, 2));
+
+ SubLine subline = new SubLine(Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION));
+ subline.getSubspaceRegion().add(Interval.of(0, 1, TEST_PRECISION));
+
+ // act
+ SubLine transformed = subline.transform(mat);
+
+ // assert
+ Assert.assertNotSame(subline, transformed);
+
+ List<Segment> originalSegments = subline.toConvex();
+ Assert.assertEquals(1, originalSegments.size());
+ checkFiniteSegment(originalSegments.get(0), Vector2D.of(0, 1), Vector2D.of(1, 1));
+
+ List<Segment> transformedSegments = transformed.toConvex();
+ Assert.assertEquals(1, transformedSegments.size());
+ checkFiniteSegment(transformedSegments.get(0), Vector2D.of(0, 2), Vector2D.of(-1, 2));
+ }
+
+ @Test
+ public void testBuilder_instanceMethod() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ SubLineBuilder builder = new SubLine(line).builder();
+
+ // act
+ SubLine subline = builder.build();
+
+ // assert
+ Assert.assertFalse(subline.isFull());
+ Assert.assertTrue(subline.isEmpty());
+
+ List<Segment> segments = subline.toConvex();
+ Assert.assertEquals(0, segments.size());
+
+ Assert.assertSame(line, subline.getLine());
+ Assert.assertSame(line, subline.getHyperplane());
+ Assert.assertSame(TEST_PRECISION, subline.getPrecision());
+ }
+
+ @Test
+ public void testBuilder_createEmpty() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ SubLineBuilder builder = new SubLineBuilder(line);
+
+ // act
+ SubLine subline = builder.build();
+
+ // assert
+ Assert.assertFalse(subline.isFull());
+ Assert.assertTrue(subline.isEmpty());
+
+ List<Segment> segments = subline.toConvex();
+ Assert.assertEquals(0, segments.size());
+ }
+
+ @Test
+ public void testBuilder_addConvex() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ Line otherLine = Line.fromPointAndAngle(Vector2D.of(0, 1), 1e-11, TEST_PRECISION);
+
+ SubLineBuilder builder = new SubLineBuilder(line);
+
+ // act
+ builder.add(Segment.fromInterval(line, 2, 4));
+ builder.add(Segment.fromInterval(otherLine, 1, 3));
+ builder.add(Segment.fromPoints(Vector2D.of(-4, 1), Vector2D.of(-1, 1), TEST_PRECISION));
+
+ SubLine subline = builder.build();
+
+ // assert
+ Assert.assertFalse(subline.isFull());
+ Assert.assertFalse(subline.isEmpty());
+
+ List<Segment> segments = subline.toConvex();
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-4, 1), segments.get(0).getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), segments.get(0).getEndPoint(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), segments.get(1).getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4, 1), segments.get(1).getEndPoint(), TEST_EPS);
+ }
+
+ @Test
+ public void testBuilder_addNonConvex() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ SubLine a = new SubLine(line);
+ RegionBSPTree1D aTree = a.getSubspaceRegion();
+ aTree.add(Interval.max(-3, TEST_PRECISION));
+ aTree.add(Interval.of(1, 2, TEST_PRECISION));
+
+ SubLine b = new SubLine(line);
+ RegionBSPTree1D bTree = b.getSubspaceRegion();
+ bTree.add(Interval.of(2, 4, TEST_PRECISION));
+ bTree.add(Interval.of(-4, -2, TEST_PRECISION));
+
+ SubLineBuilder builder = new SubLineBuilder(line);
+
+ int aTreeCount = aTree.count();
+ int bTreeCount = bTree.count();
+
+ // act
+ builder.add(a);
+ builder.add(b);
+
+ SubLine subline = builder.build();
+
+ // assert
+ Assert.assertFalse(subline.isFull());
+ Assert.assertFalse(subline.isEmpty());
+
+ List<Segment> segments = subline.toConvex();
+
+ Assert.assertEquals(2, segments.size());
+
+ Assert.assertNull(segments.get(0).getStartPoint());
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, 1), segments.get(0).getEndPoint(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), segments.get(1).getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4, 1), segments.get(1).getEndPoint(), TEST_EPS);
+
+ Assert.assertEquals(aTreeCount, aTree.count());
+ Assert.assertEquals(bTreeCount, bTree.count());
+ }
+
+ @Test
+ public void testBuilder_argumentsFromDifferentLine() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+ Line otherLine = Line.fromPointAndAngle(Vector2D.of(0, 1), 1e-2, TEST_PRECISION);
+
+ SubLineBuilder builder = new SubLineBuilder(line);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ builder.add(Segment.fromInterval(otherLine, 0, 1));
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.add(new SubLine(otherLine));
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testBuilder_unknownSubLineType() {
+ // arrange
+ Line line = Line.fromPointAndAngle(Vector2D.of(0, 1), Geometry.ZERO_PI, TEST_PRECISION);
+
+ AbstractSubLine unknownType = new AbstractSubLine(line) {
+
+ /** Serializable UID */
+ private static final long serialVersionUID = 20190729L;
+
+ @Override
+ public boolean isInfinite() {
+ return false;
+ }
+
+ @Override
+ public boolean isFinite() {
+ return true;
+ }
+
+ @Override
+ public List<? extends ConvexSubHyperplane<Vector2D>> toConvex() {
+ return null;
+ }
+
+ @Override
+ public HyperplaneBoundedRegion<Vector1D> getSubspaceRegion() {
+ return null;
+ }
+
+ @Override
+ public Split<? extends SubHyperplane<Vector2D>> split(Hyperplane<Vector2D> splitter) {
+ return null;
+ }
+
+ @Override
+ public SubHyperplane<Vector2D> transform(Transform<Vector2D> transform) {
+ return null;
+ }
+ };
+
+ SubLineBuilder builder = new SubLineBuilder(line);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ builder.add(unknownType);
+ }, IllegalArgumentException.class);
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ SubLine sub = new SubLine(line);
+
+ // act
+ String str = sub.toString();
+
+ // assert
+ Assert.assertTrue(str.contains("SubLine[lineOrigin= "));
+ Assert.assertTrue(str.contains(", lineDirection= "));
+ Assert.assertTrue(str.contains(", region= "));
+ }
+
+ private static void checkFiniteSegment(Segment segment, Vector2D start, Vector2D end) {
+ Assert.assertFalse(segment.isInfinite());
+
+ EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
+ }
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
index 20c3faf..0bd84fb 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.commons.geometry.euclidean.twod;
+import java.util.Comparator;
import java.util.regex.Pattern;
import org.apache.commons.geometry.core.Geometry;
@@ -65,6 +66,25 @@
}
@Test
+ public void testCoordinateAscendingOrder() {
+ // arrange
+ Comparator<Vector2D> cmp = Vector2D.COORDINATE_ASCENDING_ORDER;
+
+ // act/assert
+ Assert.assertEquals(0, cmp.compare(Vector2D.of(1, 2), Vector2D.of(1, 2)));
+
+ Assert.assertEquals(-1, cmp.compare(Vector2D.of(0, 2), Vector2D.of(1, 2)));
+ Assert.assertEquals(-1, cmp.compare(Vector2D.of(1, 1), Vector2D.of(1, 2)));
+
+ Assert.assertEquals(1, cmp.compare(Vector2D.of(2, 2), Vector2D.of(1, 2)));
+ Assert.assertEquals(1, cmp.compare(Vector2D.of(1, 3), Vector2D.of(1, 2)));
+
+ Assert.assertEquals(-1, cmp.compare(Vector2D.of(1, 3), null));
+ Assert.assertEquals(1, cmp.compare(null, Vector2D.of(1, 2)));
+ Assert.assertEquals(0, cmp.compare(null, null));
+ }
+
+ @Test
public void testCoordinates() {
// arrange
Vector2D v = Vector2D.of(1, 2);
@@ -125,6 +145,24 @@
}
@Test
+ public void testFinite() {
+ // act/assert
+ Assert.assertTrue(Vector2D.ZERO.isFinite());
+ Assert.assertTrue(Vector2D.of(1, 1).isFinite());
+
+ Assert.assertFalse(Vector2D.of(0, Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(Vector2D.of(Double.NEGATIVE_INFINITY, 0).isFinite());
+ Assert.assertFalse(Vector2D.of(0, Double.POSITIVE_INFINITY).isFinite());
+ Assert.assertFalse(Vector2D.of(Double.POSITIVE_INFINITY, 0).isFinite());
+
+ Assert.assertFalse(Vector2D.of(0, Double.NaN).isFinite());
+ Assert.assertFalse(Vector2D.of(Double.NEGATIVE_INFINITY, Double.NaN).isFinite());
+ Assert.assertFalse(Vector2D.of(Double.NaN, Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(Vector2D.of(Double.POSITIVE_INFINITY, Double.NaN).isFinite());
+ Assert.assertFalse(Vector2D.of(Double.NaN, Double.POSITIVE_INFINITY).isFinite());
+ }
+
+ @Test
public void testGetZero() {
// act/assert
checkVector(Vector2D.of(1.0, 1.0).getZero(), 0, 0);
@@ -809,20 +847,20 @@
Vector2D vec = Vector2D.of(1, -2);
// act/assert
- Assert.assertTrue(vec.equals(vec, smallEps));
- Assert.assertTrue(vec.equals(vec, largeEps));
+ Assert.assertTrue(vec.eq(vec, smallEps));
+ Assert.assertTrue(vec.eq(vec, largeEps));
- Assert.assertTrue(vec.equals(Vector2D.of(1.0000007, -2.0000009), smallEps));
- Assert.assertTrue(vec.equals(Vector2D.of(1.0000007, -2.0000009), largeEps));
+ Assert.assertTrue(vec.eq(Vector2D.of(1.0000007, -2.0000009), smallEps));
+ Assert.assertTrue(vec.eq(Vector2D.of(1.0000007, -2.0000009), largeEps));
- Assert.assertFalse(vec.equals(Vector2D.of(1.004, -2), smallEps));
- Assert.assertFalse(vec.equals(Vector2D.of(1, -2.004), smallEps));
- Assert.assertTrue(vec.equals(Vector2D.of(1.004, -2.004), largeEps));
+ Assert.assertFalse(vec.eq(Vector2D.of(1.004, -2), smallEps));
+ Assert.assertFalse(vec.eq(Vector2D.of(1, -2.004), smallEps));
+ Assert.assertTrue(vec.eq(Vector2D.of(1.004, -2.004), largeEps));
- Assert.assertFalse(vec.equals(Vector2D.of(1, -3), smallEps));
- Assert.assertFalse(vec.equals(Vector2D.of(2, -2), smallEps));
- Assert.assertFalse(vec.equals(Vector2D.of(1, -3), largeEps));
- Assert.assertFalse(vec.equals(Vector2D.of(2, -2), largeEps));
+ Assert.assertFalse(vec.eq(Vector2D.of(1, -3), smallEps));
+ Assert.assertFalse(vec.eq(Vector2D.of(2, -2), smallEps));
+ Assert.assertFalse(vec.eq(Vector2D.of(1, -3), largeEps));
+ Assert.assertFalse(vec.eq(Vector2D.of(2, -2), largeEps));
}
@Test
diff --git a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/issue-1211.bsp b/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/issue-1211.bsp
deleted file mode 100644
index 2803ac6..0000000
--- a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/issue-1211.bsp
+++ /dev/null
@@ -1,14 +0,0 @@
-PolyhedronsSet
- plus internal -1.0 0.0 0.0 -1.0 0.0 0.0
- minus internal 0.0 1.0 0.0 0.0 1.0 0.0
- minus internal 0.0 0.0 1.0 0.0 0.0 1.0
- minus internal 0.0 0.0 -1.0 0.0 0.0 -1.0
- minus internal 0.0 -1.0 0.0 0.0 -1.0 0.0
- minus internal 1.0 0.0 0.0 1.0 0.0 0.0
- minus leaf true
- plus leaf false
- plus leaf false
- plus leaf false
- plus leaf false
- plus leaf false
- plus leaf false
diff --git a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-bad-orientation.ply b/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-bad-orientation.ply
deleted file mode 100644
index 4109576..0000000
--- a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-bad-orientation.ply
+++ /dev/null
@@ -1,40 +0,0 @@
-ply
-format ascii 1.0
-comment this file represents the 'N' pentomino
-comment it has been created manually
-comment the shape has a reversed orientation for facet 3
-element vertex 16
-property double x
-property double y
-property double z
-element face 12
-property list uchar uint vertex_indices
-end_header
-0.0 0.0 0.0
-1.0 0.0 0.0
-1.0 1.0 0.0
-2.0 1.0 0.0
-2.0 4.0 0.0
-1.0 4.0 0.0
-1.0 2.0 0.0
-0.0 2.0 0.0
-0.0 0.0 1.0
-1.0 0.0 1.0
-1.0 1.0 1.0
-2.0 1.0 1.0
-2.0 4.0 1.0
-1.0 4.0 1.0
-1.0 2.0 1.0
-0.0 2.0 1.0
-5 8 9 10 14 15
-5 10 11 12 13 14
-5 7 6 2 1 0
-5 2 3 4 5 6
-4 0 1 9 8
-4 1 2 10 9
-4 2 3 11 10
-4 3 4 12 11
-4 4 5 13 12
-4 5 6 14 13
-4 6 7 15 14
-4 7 0 8 15
\ No newline at end of file
diff --git a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-hole.ply b/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-hole.ply
deleted file mode 100644
index e40a025..0000000
--- a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-hole.ply
+++ /dev/null
@@ -1,39 +0,0 @@
-ply
-format ascii 1.0
-comment this file represents the 'N' pentomino
-comment it has been created manually
-comment the shape has a missing face between vertices 0, 1, 9, 8
-element vertex 16
-property double x
-property double y
-property double z
-element face 11
-property list uchar uint vertex_indices
-end_header
-0.0 0.0 0.0
-1.0 0.0 0.0
-1.0 1.0 0.0
-2.0 1.0 0.0
-2.0 4.0 0.0
-1.0 4.0 0.0
-1.0 2.0 0.0
-0.0 2.0 0.0
-0.0 0.0 1.0
-1.0 0.0 1.0
-1.0 1.0 1.0
-2.0 1.0 1.0
-2.0 4.0 1.0
-1.0 4.0 1.0
-1.0 2.0 1.0
-0.0 2.0 1.0
-5 8 9 10 14 15
-5 10 11 12 13 14
-5 7 6 2 1 0
-5 6 5 4 3 2
-4 1 2 10 9
-4 2 3 11 10
-4 3 4 12 11
-4 4 5 13 12
-4 5 6 14 13
-4 6 7 15 14
-4 7 0 8 15
\ No newline at end of file
diff --git a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-out-of-plane.ply b/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-out-of-plane.ply
deleted file mode 100644
index c345eda..0000000
--- a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-out-of-plane.ply
+++ /dev/null
@@ -1,40 +0,0 @@
-ply
-format ascii 1.0
-comment this file represents the 'N' pentomino
-comment it has been created manually
-comment the shape is distorted with edge 7 moved, so associated facets are not planar
-element vertex 16
-property double x
-property double y
-property double z
-element face 12
-property list uchar uint vertex_indices
-end_header
-0.0 0.0 0.0
-1.0 0.0 0.0
-1.0 1.0 0.0
-2.0 1.0 0.0
-2.0 4.0 0.0
-1.0 4.0 0.0
-1.0 2.0 0.0
-0.0 2.0 0.5
-0.0 0.0 1.0
-1.0 0.0 1.0
-1.0 1.0 1.0
-2.0 1.0 1.0
-2.0 4.0 1.0
-1.0 4.0 1.0
-1.0 2.0 1.0
-0.0 2.0 1.0
-5 8 9 10 14 15
-5 10 11 12 13 14
-5 7 6 2 1 0
-5 6 5 4 3 2
-4 0 1 9 8
-4 1 2 10 9
-4 2 3 11 10
-4 3 4 12 11
-4 4 5 13 12
-4 5 6 14 13
-4 6 7 15 14
-4 7 0 8 15
\ No newline at end of file
diff --git a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-too-close.ply b/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-too-close.ply
deleted file mode 100644
index 1701540..0000000
--- a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N-too-close.ply
+++ /dev/null
@@ -1,86 +0,0 @@
-ply
-format ascii 1.0
-comment this file should trigger an error as it contains several vertices at the same location
-comment the file was originally created using blender http://www.blender.org
-element vertex 52
-property float x
-property float y
-property float z
-property float nx
-property float ny
-property float nz
-element face 20
-property list uchar uint vertex_indices
-end_header
-0.000000 0.000000 0.000000 1.000000 0.000000 0.000000
-0.000000 1.000000 0.000000 1.000000 0.000000 0.000000
-0.000000 1.000000 1.000000 1.000000 0.000000 0.000000
-0.000000 0.000000 1.000000 1.000000 0.000000 0.000000
-0.000000 1.000000 0.000000 0.000000 1.000000 0.000000
--2.000000 1.000000 0.000000 0.000000 1.000000 0.000000
--2.000000 1.000000 1.000000 0.000000 1.000000 0.000000
-0.000000 1.000000 1.000000 0.000000 1.000000 0.000000
--2.000000 1.000000 1.000000 0.000000 0.000000 0.000000
-1.000000 1.000000 1.000000 0.000000 0.000000 0.000000
-0.000000 1.000000 1.000000 0.000000 0.000000 0.000000
--2.000000 2.000000 1.000000 0.000000 0.000000 -1.000000
-1.000000 2.000000 1.000000 0.000000 0.000000 -1.000000
-1.000000 1.000000 1.000000 0.000000 0.000000 -1.000000
--2.000000 1.000000 1.000000 -0.000000 -0.000000 -1.000000
--2.000000 2.000000 0.000000 0.000000 -1.000000 -0.000000
-1.000000 2.000000 0.000000 0.000000 -1.000000 -0.000000
-1.000000 2.000000 1.000000 0.000000 -1.000000 -0.000000
--2.000000 2.000000 1.000000 0.000000 -1.000000 -0.000000
-0.000000 0.000000 0.000000 -0.000000 0.000000 1.000000
-1.000000 1.000000 0.000000 -0.000000 0.000000 1.000000
-0.000000 1.000000 0.000000 -0.000000 0.000000 1.000000
-2.000000 0.000000 0.000000 -0.000000 0.000000 1.000000
-2.000000 1.000000 0.000000 -0.000000 0.000000 1.000000
-2.000000 1.000000 0.000000 -1.000000 0.000000 -0.000000
-2.000000 0.000000 0.000000 -1.000000 0.000000 -0.000000
-2.000000 0.000000 1.000000 -1.000000 0.000000 -0.000000
-2.000000 1.000000 1.000000 -1.000000 0.000000 -0.000000
-2.000000 0.000000 0.000000 0.000000 1.000000 0.000000
-0.000000 0.000000 0.000000 0.000000 1.000000 0.000000
-0.000000 0.000000 1.000000 0.000000 1.000000 0.000000
-2.000000 0.000000 1.000000 0.000000 1.000000 0.000000
--2.000000 1.000000 0.000000 1.000000 0.000000 0.000000
--2.000000 2.000000 0.000000 1.000000 0.000000 0.000000
--2.000000 2.000000 1.000000 1.000000 0.000000 0.000000
--2.000000 1.000000 1.000000 1.000000 0.000000 0.000000
-1.000000 1.000000 0.000000 0.000000 -1.000000 -0.000000
-2.000000 1.000000 0.000000 0.000000 -1.000000 -0.000000
-2.000000 1.000000 1.000000 0.000000 -1.000000 -0.000000
-1.000000 1.000000 1.000000 0.000000 -1.000000 -0.000000
-1.000000 2.000000 0.000000 -1.000000 0.000000 -0.000000
-1.000000 1.000000 0.000000 -1.000000 0.000000 -0.000000
-1.000000 1.000000 1.000000 -1.000000 0.000000 -0.000000
-1.000000 2.000000 1.000000 -1.000000 0.000000 -0.000000
--2.000000 1.000000 0.000000 -0.000000 0.000000 1.000000
-1.000000 2.000000 0.000000 -0.000000 0.000000 1.000000
--2.000000 2.000000 0.000000 -0.000000 0.000000 1.000000
-0.000000 0.000000 1.000000 -0.000000 0.000000 -1.000000
-2.000000 1.000000 1.000000 -0.000000 0.000000 -1.000000
-2.000000 0.000000 1.000000 -0.000000 0.000000 -1.000000
-2.000000 1.000000 1.000000 0.000000 0.000000 0.000000
-0.000000 1.000000 1.000000 -0.000000 -0.000000 -1.000000
-4 0 1 2 3
-4 4 5 6 7
-3 8 9 10
-3 11 12 13
-3 14 11 13
-4 15 16 17 18
-3 19 20 21
-3 22 23 20
-3 19 22 20
-4 24 25 26 27
-4 28 29 30 31
-4 32 33 34 35
-4 36 37 38 39
-4 40 41 42 43
-3 44 45 46
-3 21 20 45
-3 44 21 45
-3 47 48 49
-3 10 9 50
-3 47 51 48
diff --git a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N.ply b/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N.ply
deleted file mode 100644
index 4efbf20..0000000
--- a/commons-geometry-euclidean/src/test/resources/org/apache/commons/geometry/euclidean/threed/pentomino-N.ply
+++ /dev/null
@@ -1,39 +0,0 @@
-ply
-format ascii 1.0
-comment this file represents the 'N' pentomino
-comment it has been created manually
-element vertex 16
-property double x
-property double y
-property double z
-element face 12
-property list uchar uint vertex_indices
-end_header
-0.0 0.0 0.0
-1.0 0.0 0.0
-1.0 1.0 0.0
-2.0 1.0 0.0
-2.0 4.0 0.0
-1.0 4.0 0.0
-1.0 2.0 0.0
-0.0 2.0 0.0
-0.0 0.0 1.0
-1.0 0.0 1.0
-1.0 1.0 1.0
-2.0 1.0 1.0
-2.0 4.0 1.0
-1.0 4.0 1.0
-1.0 2.0 1.0
-0.0 2.0 1.0
-5 8 9 10 14 15
-5 10 11 12 13 14
-5 7 6 2 1 0
-5 6 5 4 3 2
-4 0 1 9 8
-4 1 2 10 9
-4 2 3 11 10
-4 3 4 12 11
-4 4 5 13 12
-4 5 6 14 13
-4 6 7 15 14
-4 7 0 8 15
\ No newline at end of file
diff --git a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/AbstractConvexHullGenerator2D.java b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/AbstractConvexHullGenerator2D.java
index 3ec1621..1f5d3f2 100644
--- a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/AbstractConvexHullGenerator2D.java
+++ b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/AbstractConvexHullGenerator2D.java
@@ -27,7 +27,7 @@
*/
abstract class AbstractConvexHullGenerator2D implements ConvexHullGenerator2D {
- /** Default epsilon vlaue. */
+ /** Default epsilon value. */
private static final double DEFAULT_EPSILON = 1e-10;
/** Precision context used to compare floating point numbers. */
@@ -58,7 +58,8 @@
* added as hull vertices
* @param precision precision context used to compare floating point numbers
*/
- protected AbstractConvexHullGenerator2D(final boolean includeCollinearPoints, final DoublePrecisionContext precision) {
+ protected AbstractConvexHullGenerator2D(final boolean includeCollinearPoints,
+ final DoublePrecisionContext precision) {
this.includeCollinearPoints = includeCollinearPoints;
this.precision = precision;
}
diff --git a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java
index 7dc7b26..a0656db 100644
--- a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java
+++ b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHull2D.java
@@ -17,10 +17,12 @@
package org.apache.commons.geometry.euclidean.twod.hull;
import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
import org.apache.commons.geometry.euclidean.twod.Line;
import org.apache.commons.geometry.euclidean.twod.Segment;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
@@ -127,7 +129,7 @@
this.lineSegments = new Segment[1];
final Vector2D p1 = vertices[0];
final Vector2D p2 = vertices[1];
- this.lineSegments[0] = new Segment(p1, p2, Line.fromPoints(p1, p2, precision));
+ this.lineSegments[0] = Segment.fromPoints(p1, p2, precision);
} else {
this.lineSegments = new Segment[size];
Vector2D firstPoint = null;
@@ -138,13 +140,11 @@
firstPoint = point;
lastPoint = point;
} else {
- this.lineSegments[index++] =
- new Segment(lastPoint, point, Line.fromPoints(lastPoint, point, precision));
+ this.lineSegments[index++] = Segment.fromPoints(lastPoint, point, precision);
lastPoint = point;
}
}
- this.lineSegments[index] =
- new Segment(lastPoint, firstPoint, Line.fromPoints(lastPoint, firstPoint, precision));
+ this.lineSegments[index] = Segment.fromPoints(lastPoint, firstPoint, precision);
}
}
return lineSegments;
@@ -152,16 +152,15 @@
/** {@inheritDoc} */
@Override
- public Region<Vector2D> createRegion() {
+ public ConvexArea createRegion() {
if (vertices.length < 3) {
- throw new IllegalStateException("Region generation requires at least 3 vertices but found only " + vertices.length);
+ throw new IllegalStateException("Region generation requires at least 3 vertices but found only " +
+ vertices.length);
}
- final RegionFactory<Vector2D> factory = new RegionFactory<>();
- final Segment[] segments = retrieveLineSegments();
- final Line[] lineArray = new Line[segments.length];
- for (int i = 0; i < segments.length; i++) {
- lineArray[i] = segments[i].getLine();
- }
- return factory.buildConvex(lineArray);
+
+ List<Line> bounds = Arrays.asList(retrieveLineSegments()).stream()
+ .map(Segment::getLine).collect(Collectors.toList());
+
+ return ConvexArea.fromBounds(bounds);
}
}
diff --git a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChain.java b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChain.java
index 98d019d..9a45066 100644
--- a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChain.java
+++ b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/euclidean/twod/hull/MonotoneChain.java
@@ -118,7 +118,7 @@
}
// special case: if the lower and upper hull may contain only 1 point if all are identical
- if (hullVertices.isEmpty() && ! lowerHull.isEmpty()) {
+ if (hullVertices.isEmpty() && !lowerHull.isEmpty()) {
hullVertices.add(lowerHull.get(0));
}
@@ -147,7 +147,7 @@
final Vector2D p1 = hull.get(size - 2);
final Vector2D p2 = hull.get(size - 1);
- final double offset = Line.fromPoints(p1, p2, precision).getOffset(point);
+ final double offset = Line.fromPoints(p1, p2, precision).offset(point);
if (precision.eqZero(offset)) {
// the point is collinear to the line (p1, p2)
diff --git a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/hull/ConvexHull.java b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/hull/ConvexHull.java
index 118a640..0d4ed2a 100644
--- a/commons-geometry-hull/src/main/java/org/apache/commons/geometry/hull/ConvexHull.java
+++ b/commons-geometry-hull/src/main/java/org/apache/commons/geometry/hull/ConvexHull.java
@@ -19,7 +19,7 @@
import java.io.Serializable;
import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.Region;
/**
* This class represents a convex hull.
diff --git a/commons-geometry-hull/src/test/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHullGenerator2DAbstractTest.java b/commons-geometry-hull/src/test/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHullGenerator2DAbstractTest.java
index fff9971..1ced9c1 100644
--- a/commons-geometry-hull/src/test/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHullGenerator2DAbstractTest.java
+++ b/commons-geometry-hull/src/test/java/org/apache/commons/geometry/euclidean/twod/hull/ConvexHullGenerator2DAbstractTest.java
@@ -22,8 +22,8 @@
import java.util.Collections;
import java.util.List;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.arrays.LinearCombination;
import org.apache.commons.numbers.core.Precision;
@@ -348,7 +348,7 @@
Assert.assertEquals(perimeter, hullRegion.getBoundarySize(), 1.0e-12);
for (int i = 0; i < referenceHull.length; ++i) {
- Assert.assertEquals(Location.BOUNDARY, hullRegion.checkPoint(referenceHull[i]));
+ Assert.assertEquals(RegionLocation.BOUNDARY, hullRegion.classify(referenceHull[i]));
}
}
@@ -425,10 +425,10 @@
final Region<Vector2D> region = hull.createRegion();
for (final Vector2D p : points) {
- Location location = region.checkPoint(p);
- Assert.assertTrue(location != Location.OUTSIDE);
+ RegionLocation location = region.classify(p);
+ Assert.assertTrue(location != RegionLocation.OUTSIDE);
- if (location == Location.BOUNDARY && includesCollinearPoints) {
+ if (location == RegionLocation.BOUNDARY && includesCollinearPoints) {
Assert.assertTrue(hullVertices.contains(p));
}
}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
new file mode 100644
index 0000000..c77630b
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
@@ -0,0 +1,630 @@
+/*
+ * 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.oned;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Class representing an angular interval of size greater than zero to {@code 2pi}. The interval is
+ * defined by two azimuth angles: a min and a max. The interval starts at the min azimuth angle and
+ * contains all points in the direction of increasing azimuth angles up to max.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public class AngularInterval implements HyperplaneBoundedRegion<Point1S>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190817L;
+
+ /** The minimum boundary of the interval. */
+ private final CutAngle minBoundary;
+
+ /** The maximum boundary of the interval. */
+ private final CutAngle maxBoundary;
+
+ /** Point halfway between the min and max boundaries. */
+ private final Point1S midpoint;
+
+ /** Construct a new instance representing the angular region between the given
+ * min and max azimuth boundaries. The arguments must be either all finite or all
+ * null (to indicate the full space). If the boundaries are finite, then the min
+ * boundary azimuth value must be numerically less than the max boundary. Callers are
+ * responsible for enforcing these constraints. No validation is performed.
+ * @param minBoundary minimum boundary for the interval
+ * @param maxBoundary maximum boundary for the interval
+ */
+ private AngularInterval(final CutAngle minBoundary, final CutAngle maxBoundary) {
+
+ this.minBoundary = minBoundary;
+ this.maxBoundary = maxBoundary;
+ this.midpoint = (minBoundary != null && maxBoundary != null) ?
+ Point1S.of(0.5 * (minBoundary.getAzimuth() + maxBoundary.getAzimuth())) :
+ null;
+ }
+
+ /** Get the minimum azimuth angle for the interval, or {@code 0}
+ * if the interval is full.
+ * @return the minimum azimuth angle for the interval or {@code 0}
+ * if the interval represents the full space.
+ */
+ public double getMin() {
+ return (minBoundary != null) ?
+ minBoundary.getAzimuth() :
+ Geometry.ZERO_PI;
+ }
+
+ /** Get the minimum boundary for the interval, or null if the
+ * interval represents the full space.
+ * @return the minimum point for the interval or null if
+ * the interval represents the full space
+ */
+ public CutAngle getMinBoundary() {
+ return minBoundary;
+ }
+
+ /** Get the maximum azimuth angle for the interval, or {@code 2pi} if
+ * the interval represents the full space.
+ * @return the maximum azimuth angle for the interval or {@code 2pi} if
+ * the interval represents the full space.
+ */
+ public double getMax() {
+ return (maxBoundary != null) ?
+ maxBoundary.getAzimuth() :
+ Geometry.TWO_PI;
+ }
+
+ /** Get the maximum point for the interval. This will be null if the
+ * interval represents the full space.
+ * @return the maximum point for the interval or null if
+ * the interval represents the full space
+ */
+ public CutAngle getMaxBoundary() {
+ return maxBoundary;
+ }
+
+ /** Get the midpoint of the interval or null if the interval represents
+ * the full space.
+ * @return the midpoint of the interval or null if the interval represents
+ * the full space
+ * @see #getBarycenter()
+ */
+ public Point1S getMidPoint() {
+ return midpoint;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFull() {
+ // minBoundary and maxBoundary are either both null or both not null
+ return minBoundary == null;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns false.</p>
+ */
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return getMax() - getMin();
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns 0 because boundaries in one dimension do not
+ * have any size.</p>
+ */
+ @Override
+ public double getBoundarySize() {
+ return 0;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method is an alias for {@link #getMidPoint()}.</p>
+ * @see #getMidPoint()
+ */
+ @Override
+ public Point1S getBarycenter() {
+ return getMidPoint();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionLocation classify(final Point1S pt) {
+ if (!isFull()) {
+ final HyperplaneLocation minLoc = minBoundary.classify(pt);
+ final HyperplaneLocation maxLoc = maxBoundary.classify(pt);
+
+ final boolean wraps = wrapsZero();
+
+ if ((!wraps && (minLoc == HyperplaneLocation.PLUS || maxLoc == HyperplaneLocation.PLUS)) ||
+ (wraps && minLoc == HyperplaneLocation.PLUS && maxLoc == HyperplaneLocation.PLUS)) {
+ return RegionLocation.OUTSIDE;
+ } else if (minLoc == HyperplaneLocation.ON || maxLoc == HyperplaneLocation.ON) {
+ return RegionLocation.BOUNDARY;
+ }
+ }
+ return RegionLocation.INSIDE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point1S project(final Point1S pt) {
+ if (!isFull()) {
+ final double minDist = minBoundary.getPoint().distance(pt);
+ final double maxDist = maxBoundary.getPoint().distance(pt);
+
+ return (minDist <= maxDist) ?
+ minBoundary.getPoint() :
+ maxBoundary.getPoint();
+ }
+ return null;
+ }
+
+ /** Return true if the interval wraps around the zero/{@code 2pi} point. In this
+ * case, the max boundary azimuth is less than that of the min boundary when both
+ * values are normalized to the range {@code [0, 2pi)}.
+ * @return true if the interval wraps around the zero/{@code 2pi} point
+ */
+ public boolean wrapsZero() {
+ if (!isFull()) {
+ final double minNormAz = minBoundary.getPoint().getNormalizedAzimuth();
+ final double maxNormAz = maxBoundary.getPoint().getNormalizedAzimuth();
+
+ return maxNormAz < minNormAz;
+ }
+ return false;
+ }
+
+ /** Return a new instance transformed by the argument. If the transformed size
+ * of the interval is greater than or equal to 2pi, then an interval representing
+ * the full space is returned.
+ * @param transform transform to apply
+ * @return a new instance transformed by the argument
+ */
+ public AngularInterval transform(final Transform<Point1S> transform) {
+ return AngularInterval.transform(this, transform, AngularInterval::of);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method returns instances of {@link RegionBSPTree1S} instead of
+ * {@link AngularInterval} since it is possible for a convex angular interval
+ * to be split into disjoint regions by a single hyperplane. These disjoint
+ * regions cannot be represented by this class and require the use of a BSP
+ * tree.</p>
+ *
+ * @see RegionBSPTree1S#split(Hyperplane)
+ */
+ @Override
+ public Split<RegionBSPTree1S> split(final Hyperplane<Point1S> splitter) {
+ return toTree().split(splitter);
+ }
+
+ /** Return a {@link RegionBSPTree1S} instance representing the same region
+ * as this instance.
+ * @return a BSP tree representing the same region as this instance
+ */
+ public RegionBSPTree1S toTree() {
+ return RegionBSPTree1S.fromInterval(this);
+ }
+
+ /** Return a list of convex intervals comprising this region.
+ * @return a list of convex intervals comprising this region
+ * @see Convex
+ */
+ public List<AngularInterval.Convex> toConvex() {
+ if (isConvex(minBoundary, maxBoundary)) {
+ return Collections.singletonList(new Convex(minBoundary, maxBoundary));
+ }
+
+ final CutAngle midPos = CutAngle.createPositiveFacing(midpoint, minBoundary.getPrecision());
+ final CutAngle midNeg = CutAngle.createNegativeFacing(midpoint, maxBoundary.getPrecision());
+
+ return Arrays.asList(
+ new Convex(minBoundary, midPos),
+ new Convex(midNeg, maxBoundary)
+ );
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[min= ")
+ .append(getMin())
+ .append(", max= ")
+ .append(getMax())
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Return an instance representing the full space. The returned instance contains all
+ * possible azimuth angles.
+ * @return an interval representing the full space
+ */
+ public static AngularInterval.Convex full() {
+ return Convex.FULL;
+ }
+
+ /** Return an instance representing the angular interval between the given min and max azimuth
+ * values. The max value is adjusted to be numerically above the min value, even if the resulting
+ * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
+ * is returned if either point is infinite or min and max are equivalent as evaluated by the
+ * given precision context.
+ * @param min min azimuth value
+ * @param max max azimuth value
+ * @param precision precision precision context used to compare floating point values
+ * @return a new instance resulting the angular region between the given min and max azimuths
+ * @throws GeometryException if either azimuth is infinite or NaN
+ */
+ public static AngularInterval of(final double min, final double max, final DoublePrecisionContext precision) {
+ return of(Point1S.of(min), Point1S.of(max), precision);
+ }
+
+ /** Return an instance representing the angular interval between the given min and max azimuth
+ * points. The max point is adjusted to be numerically above the min point, even if the resulting
+ * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
+ * is returned if either point is infinite or min and max are equivalent as evaluated by the
+ * given precision context.
+ * @param min min azimuth value
+ * @param max max azimuth value
+ * @param precision precision precision context used to compare floating point values
+ * @return a new instance resulting the angular region between the given min and max points
+ * @throws GeometryException if either azimuth is infinite or NaN
+ */
+ public static AngularInterval of(final Point1S min, final Point1S max, final DoublePrecisionContext precision) {
+ return createInterval(min, max, precision, AngularInterval::new, Convex.FULL);
+ }
+
+ /** Return an instance representing the angular interval between the given oriented points.
+ * The negative-facing point is used as the minimum boundary and the positive-facing point is
+ * adjusted to be above the minimum. The arguments can be given in any order. The full space
+ * is returned if the points are equivalent or are oriented in the same direction.
+ * @param a first oriented point
+ * @param b second oriented point
+ * @return an instance representing the angular interval between the given oriented points
+ * @throws GeometryException if either argument is infinite or NaN
+ */
+ public static AngularInterval of(final CutAngle a, final CutAngle b) {
+ return createInterval(a, b, AngularInterval::new, Convex.FULL);
+ }
+
+ /** Internal method to create an interval between the given min and max points. The max point
+ * is adjusted to be numerically above the min point, even if the resulting
+ * azimuth value is greater than or equal to {@code 2pi}. The full instance argument
+ * is returned if either point is infinite or min and max are equivalent as evaluated by the
+ * given precision context.
+ * @param min min azimuth value
+ * @param max max azimuth value
+ * @param precision precision precision context used to compare floating point values
+ * @param factory factory object used to create new instances; this object is passed the validated
+ * min (negative-facing) cut and the max (positive-facing) cut, in that order
+ * @param <T> Angular interval implementation type
+ * @param fullSpace instance returned if the interval should represent the full space
+ * @return a new instance resulting the angular region between the given min and max points
+ * @throws GeometryException if either azimuth is infinite or NaN
+ */
+ private static <T extends AngularInterval> T createInterval(final Point1S min, final Point1S max,
+ final DoublePrecisionContext precision, final BiFunction<CutAngle, CutAngle, T> factory,
+ final T fullSpace) {
+
+ validateIntervalValues(min, max);
+
+ // return the full space if either point is infinite or the points are equivalent
+ if (min.eq(max, precision)) {
+ return fullSpace;
+ }
+
+ final Point1S adjustedMax = max.above(min);
+
+ return factory.apply(
+ CutAngle.createNegativeFacing(min, precision),
+ CutAngle.createPositiveFacing(adjustedMax, precision)
+ );
+ }
+
+ /** Internal method to create a new interval instance from the given cut angles.
+ * The negative-facing point is used as the minimum boundary and the positive-facing point is
+ * adjusted to be above the minimum. The arguments can be given in any order. The full space
+ * argument is returned if the points are equivalent or are oriented in the same direction.
+ * @param a first cut point
+ * @param b second cut point
+ * @param factory factory object used to create new instances; this object is passed the validated
+ * min (negative-facing) cut and the max (positive-facing) cut, in that order
+ * @param fullSpace instance returned if the interval should represent the full space
+ * @param <T> Angular interval implementation type
+ * @return a new interval instance created from the given cut angles
+ * @throws GeometryException if either argument is infinite or NaN
+ */
+ private static <T extends AngularInterval> T createInterval(final CutAngle a, final CutAngle b,
+ final BiFunction<CutAngle, CutAngle, T> factory, final T fullSpace) {
+
+ final Point1S aPoint = a.getPoint();
+ final Point1S bPoint = b.getPoint();
+
+ validateIntervalValues(aPoint, bPoint);
+
+ if (a.isPositiveFacing() == b.isPositiveFacing() ||
+ aPoint.eq(bPoint, a.getPrecision()) ||
+ bPoint.eq(aPoint, b.getPrecision())) {
+ // points are equivalent or facing in the same direction
+ return fullSpace;
+ }
+
+ final CutAngle min = a.isPositiveFacing() ? b : a;
+ final CutAngle max = a.isPositiveFacing() ? a : b;
+ final CutAngle adjustedMax = CutAngle.createPositiveFacing(
+ max.getPoint().above(min.getPoint()),
+ max.getPrecision());
+
+ return factory.apply(min, adjustedMax);
+ }
+
+ /** Validate that the given points can be used to specify an angular interval.
+ * @param a first point
+ * @param b second point
+ * @throws GeometryException if either point is infinite NaN
+ */
+ private static void validateIntervalValues(final Point1S a, final Point1S b) {
+ if (!a.isFinite() || !b.isFinite()) {
+ throw new GeometryException(MessageFormat.format("Invalid angular interval: [{0}, {1}]",
+ a.getAzimuth(), b.getAzimuth()));
+ }
+ }
+
+ /** Return true if the given cut angles define a convex region. By convention, the
+ * precision context from the min cut is used for the floating point comparison.
+ * @param min min (negative-facing) cut angle
+ * @param max max (positive-facing) cut angle
+ * @return true if the given cut angles define a convex region
+ */
+ private static boolean isConvex(final CutAngle min, final CutAngle max) {
+ if (min != null && max != null) {
+ final double dist = max.getAzimuth() - min.getAzimuth();
+ final DoublePrecisionContext precision = min.getPrecision();
+ return precision.lte(dist, Geometry.PI);
+ }
+
+ return true;
+ }
+
+ /** Internal transform method that transforms the given instance, using the factory
+ * method to create a new instance if needed.
+ * @param interval interval to transform
+ * @param transform transform to apply
+ * @param factory object used to create new instances
+ * @param <T> Angular interval implementation type
+ * @return a transformed instance
+ */
+ private static <T extends AngularInterval> T transform(final T interval,
+ final Transform<Point1S> transform,
+ final BiFunction<CutAngle, CutAngle, T> factory) {
+
+ if (!interval.isFull()) {
+ final CutAngle tMin = interval.getMinBoundary().transform(transform);
+ final CutAngle tMax = interval.getMaxBoundary().transform(transform);
+
+ return factory.apply(tMin, tMax);
+ }
+
+ return interval;
+ }
+
+ /** Class representing an angular interval with the additional property that the
+ * region is convex. By convex, it is meant that the shortest path between any
+ * two points in the region is also contained entirely in the region. If there is
+ * a tie for shortest path, then it is sufficient that at least one lie entirely
+ * within the region. For spherical 1D space, this means that the angular interval
+ * is either completely full or has a length less than or equal to {@code pi}.
+ */
+ public static final class Convex extends AngularInterval {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191012L;
+
+ /** Interval instance representing the full space. */
+ private static final Convex FULL = new Convex(null, null);
+
+ /** Construct a new convex instance from its boundaries and midpoint. No validation
+ * of the argument is performed. Callers are responsible for ensuring that the size
+ * of interval is less than or equal to {@code pi}.
+ * @param minBoundary minimum boundary for the interval
+ * @param maxBoundary maximum boundary for the interval
+ * @throws GeometryException if the interval is not convex
+ */
+ private Convex(final CutAngle minBoundary, final CutAngle maxBoundary) {
+ super(minBoundary, maxBoundary);
+
+ if (!isConvex(minBoundary, maxBoundary)) {
+ throw new GeometryException(MessageFormat.format("Interval is not convex: [{0}, {1}]",
+ minBoundary.getAzimuth(), maxBoundary.getAzimuth()));
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<AngularInterval.Convex> toConvex() {
+ return Collections.singletonList(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Convex transform(final Transform<Point1S> transform) {
+ return AngularInterval.transform(this, transform, Convex::of);
+ }
+
+ /** Split the instance along a circle diameter.The diameter is defined by the given split point and
+ * its reversed antipodal point.
+ * @param splitter split point defining one side of the split diameter
+ * @return result of the split operation
+ */
+ public Split<Convex> splitDiameter(final CutAngle splitter) {
+
+ final CutAngle opposite = CutAngle.fromPointAndDirection(
+ splitter.getPoint().antipodal(),
+ !splitter.isPositiveFacing(),
+ splitter.getPrecision());
+
+ if (isFull()) {
+ final Convex minus = Convex.of(splitter, opposite);
+ final Convex plus = Convex.of(splitter.reverse(), opposite.reverse());
+
+ return new Split<>(minus, plus);
+ }
+
+ final CutAngle minBoundary = getMinBoundary();
+ final CutAngle maxBoundary = getMaxBoundary();
+
+ final Point1S posPole = Point1S.of(splitter.getPoint().getAzimuth() + Geometry.HALF_PI);
+
+ final int minLoc = minBoundary.getPrecision().compare(Geometry.HALF_PI,
+ posPole.distance(minBoundary.getPoint()));
+ final int maxLoc = maxBoundary.getPrecision().compare(Geometry.HALF_PI,
+ posPole.distance(maxBoundary.getPoint()));
+
+ final boolean positiveFacingSplit = splitter.isPositiveFacing();
+
+ // assume a positive orientation of the splitter for region location
+ // purposes and adjust later
+ Convex pos = null;
+ Convex neg = null;
+
+ if (minLoc > 0) {
+ // min is on the pos side
+
+ if (maxLoc >= 0) {
+ // max is directly on the splitter or on the pos side
+ pos = this;
+ } else {
+ // min is on the pos side and max is on the neg side
+ final CutAngle posCut = positiveFacingSplit ?
+ opposite.reverse() :
+ opposite;
+ pos = Convex.of(minBoundary, posCut);
+
+ final CutAngle negCut = positiveFacingSplit ?
+ opposite :
+ opposite.reverse();
+ neg = Convex.of(negCut, maxBoundary);
+ }
+ } else if (minLoc < 0) {
+ // min is on the neg side
+
+ if (maxLoc <= 0) {
+ // max is directly on the splitter or on the neg side
+ neg = this;
+ } else {
+ // min is on the neg side and max is on the pos side
+ final CutAngle posCut = positiveFacingSplit ?
+ splitter.reverse() :
+ splitter;
+ pos = Convex.of(maxBoundary, posCut);
+
+ final CutAngle negCut = positiveFacingSplit ?
+ splitter :
+ splitter.reverse();
+ neg = Convex.of(negCut, minBoundary);
+ }
+ } else {
+ // min is directly on the splitter; determine whether it was on the main split
+ // point or its antipodal point
+ if (splitter.getPoint().distance(minBoundary.getPoint()) < Geometry.HALF_PI) {
+ // on main splitter; interval will be located on pos side of split
+ pos = this;
+ } else {
+ // on antipodal point; interval will be located on neg side of split
+ neg = this;
+ }
+ }
+
+ // adjust for the actual orientation of the splitter
+ final Convex minus = positiveFacingSplit ? neg : pos;
+ final Convex plus = positiveFacingSplit ? pos : neg;
+
+ return new Split<>(minus, plus);
+ }
+
+ /** Return an instance representing the convex angular interval between the given min and max azimuth
+ * values. The max value is adjusted to be numerically above the min value, even if the resulting
+ * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
+ * is returned if either point is infinite or min and max are equivalent as evaluated by the
+ * given precision context.
+ * @param min min azimuth value
+ * @param max max azimuth value
+ * @param precision precision precision context used to compare floating point values
+ * @return a new instance resulting the angular region between the given min and max azimuths
+ * @throws GeometryException if either azimuth is infinite or NaN, or the given angular
+ * interval is not convex (meaning it has a size of greater than {@code pi})
+ */
+ public static Convex of(final double min, final double max, final DoublePrecisionContext precision) {
+ return of(Point1S.of(min), Point1S.of(max), precision);
+ }
+
+ /** Return an instance representing the convex angular interval between the given min and max azimuth
+ * points. The max point is adjusted to be numerically above the min point, even if the resulting
+ * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
+ * is returned if either point is infinite or min and max are equivalent as evaluated by the
+ * given precision context.
+ * @param min min azimuth value
+ * @param max max azimuth value
+ * @param precision precision precision context used to compare floating point values
+ * @return a new instance resulting the angular region between the given min and max points
+ * @throws GeometryException if either azimuth is infinite or NaN, or the given angular
+ * interval is not convex (meaning it has a size of greater than {@code pi})
+ */
+ public static Convex of(final Point1S min, final Point1S max, final DoublePrecisionContext precision) {
+ return createInterval(min, max, precision, Convex::new, Convex.FULL);
+ }
+
+ /** Return an instance representing the convex angular interval between the given oriented points.
+ * The negative-facing point is used as the minimum boundary and the positive-facing point is
+ * adjusted to be above the minimum. The arguments can be given in any order. The full space
+ * is returned if the points are equivalent or are oriented in the same direction.
+ * @param a first oriented point
+ * @param b second oriented point
+ * @return an instance representing the angular interval between the given oriented points
+ * @throws GeometryException if either azimuth is infinite or NaN, or the given angular
+ * interval is not convex (meaning it has a size of greater than {@code pi})
+ */
+ public static Convex of(final CutAngle a, final CutAngle b) {
+ return createInterval(a, b, Convex::new, Convex.FULL);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Arc.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Arc.java
deleted file mode 100644
index ba2b34d..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Arc.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * 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.oned;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.numbers.angle.PlaneAngleRadians;
-import org.apache.commons.numbers.core.Precision;
-
-
-/** This class represents an arc on a circle.
- * @see ArcsSet
- */
-public class Arc {
-
- /** The lower angular bound of the arc. */
- private final double lower;
-
- /** The upper angular bound of the arc. */
- private final double upper;
-
- /** Middle point of the arc. */
- private final double middle;
-
- /** Precision context used to determine floating point equality. */
- private final DoublePrecisionContext precision;
-
- /** Simple constructor.
- * <p>
- * If either {@code lower} is equals to {@code upper} or
- * the interval exceeds \( 2 \pi \), the arc is considered
- * to be the full circle and its initial defining boundaries
- * will be forgotten. {@code lower} is not allowed to be
- * greater than {@code upper} (an exception is thrown in this case).
- * {@code lower} will be canonicalized between 0 and \( 2 \pi \), and
- * upper shifted accordingly, so the {@link #getInf()} and {@link #getSup()}
- * may not return the value used at instance construction.
- * </p>
- * @param lower lower angular bound of the arc
- * @param upper upper angular bound of the arc
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if lower is greater than upper
- */
- public Arc(final double lower, final double upper, final DoublePrecisionContext precision)
- throws IllegalArgumentException {
- this.precision = precision;
- if (Precision.equals(lower, upper, 0) || (upper - lower) >= Geometry.TWO_PI) {
- // the arc must cover the whole circle
- this.lower = 0;
- this.upper = Geometry.TWO_PI;
- this.middle = Math.PI;
- } else if (lower <= upper) {
- this.lower = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(lower);
- this.upper = this.lower + (upper - lower);
- this.middle = 0.5 * (this.lower + this.upper);
- } else {
- throw new IllegalArgumentException("Endpoints do not specify an interval: [" + lower + ", " + upper + "]");
- }
- }
-
- /** Get the lower angular bound of the arc.
- * @return lower angular bound of the arc,
- * always between 0 and \( 2 \pi \)
- */
- public double getInf() {
- return lower;
- }
-
- /** Get the upper angular bound of the arc.
- * @return upper angular bound of the arc,
- * always between {@link #getInf()} and {@link #getInf()} \( + 2 \pi \)
- */
- public double getSup() {
- return upper;
- }
-
- /** Get the angular size of the arc.
- * @return angular size of the arc
- */
- public double getSize() {
- return upper - lower;
- }
-
- /** Get the barycenter of the arc.
- * @return barycenter of the arc
- */
- public double getBarycenter() {
- return middle;
- }
-
- /** Get the object used to determine floating point equality for this region.
- * @return the floating point precision context for the instance
- */
- public DoublePrecisionContext getPrecision() {
- return precision;
- }
-
- /** Check a point with respect to the arc.
- * @param point point to check
- * @return a code representing the point status: either {@link
- * Location#INSIDE}, {@link Location#OUTSIDE} or {@link Location#BOUNDARY}
- */
- public Location checkPoint(final double point) {
- final double normalizedPoint = PlaneAngleRadians.normalize(point, middle);
-
- final int lowerCmp = precision.compare(normalizedPoint, lower);
- final int upperCmp = precision.compare(normalizedPoint, upper);
-
- if (lowerCmp < 0 || upperCmp > 0) {
- return Location.OUTSIDE;
- } else if (lowerCmp > 0 && upperCmp < 0) {
- return Location.INSIDE;
- } else {
- return (precision.compare(getSize(), Geometry.TWO_PI) >= 0) ? Location.INSIDE : Location.BOUNDARY;
- }
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/ArcsSet.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/ArcsSet.java
deleted file mode 100644
index 2e8cc23..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/ArcsSet.java
+++ /dev/null
@@ -1,925 +0,0 @@
-/*
- * 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.oned;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.internal.GeometryInternalError;
-import org.apache.commons.geometry.core.partitioning.AbstractRegion;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
-import org.apache.commons.geometry.core.partitioning.Side;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.numbers.angle.PlaneAngleRadians;
-import org.apache.commons.numbers.core.Precision;
-
-/** This class represents a region of a circle: a set of arcs.
- * <p>
- * Note that due to the wrapping around \(2 \pi\), barycenter is
- * ill-defined here. It was defined only in order to fulfill
- * the requirements of the {@link
- * org.apache.commons.geometry.core.partitioning.Region Region}
- * interface, but its use is discouraged.
- * </p>
- */
-public class ArcsSet extends AbstractRegion<S1Point, S1Point> implements Iterable<double[]> {
-
- /** Build an arcs set representing the whole circle.
- * @param precision precision context used to compare floating point values
- */
- public ArcsSet(final DoublePrecisionContext precision) {
- super(precision);
- }
-
- /** Build an arcs set corresponding to a single arc.
- * <p>
- * If either {@code lower} is equals to {@code upper} or
- * the interval exceeds \( 2 \pi \), the arc is considered
- * to be the full circle and its initial defining boundaries
- * will be forgotten. {@code lower} is not allowed to be greater
- * than {@code upper} (an exception is thrown in this case).
- * </p>
- * @param lower lower bound of the arc
- * @param upper upper bound of the arc
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if lower is greater than upper
- */
- public ArcsSet(final double lower, final double upper, final DoublePrecisionContext precision) {
- super(buildTree(lower, upper, precision), precision);
- }
-
- /** Build an arcs set from an inside/outside BSP tree.
- * <p>The leaf nodes of the BSP tree <em>must</em> have a
- * {@code Boolean} attribute representing the inside status of
- * the corresponding cell (true for inside cells, false for outside
- * cells). In order to avoid building too many small objects, it is
- * recommended to use the predefined constants
- * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
- * @param tree inside/outside BSP tree representing the arcs set
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if the tree leaf nodes are not
- * consistent across the \( 0, 2 \pi \) crossing
- */
- public ArcsSet(final BSPTree<S1Point> tree, final DoublePrecisionContext precision) {
- super(tree, precision);
- check2PiConsistency();
- }
-
- /** Build an arcs set from a Boundary REPresentation (B-rep).
- * <p>The boundary is provided as a collection of {@link
- * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
- * interior part of the region on its minus side and the exterior on
- * its plus side.</p>
- * <p>The boundary elements can be in any order, and can form
- * several non-connected sets (like for example polygons with holes
- * or a set of disjoints polyhedrons considered as a whole). In
- * fact, the elements do not even need to be connected together
- * (their topological connections are not used here). However, if the
- * boundary does not really separate an inside open from an outside
- * open (open having here its topological meaning), then subsequent
- * calls to the {@link
- * org.apache.commons.geometry.core.partitioning.Region#checkPoint(org.apache.commons.geometry.core.Point)
- * checkPoint} method will not be meaningful anymore.</p>
- * <p>If the boundary is empty, the region will represent the whole
- * space.</p>
- * @param boundary collection of boundary elements
- * @param precision precision context used to compare floating point values
- * @exception IllegalArgumentException if the tree leaf nodes are not
- * consistent across the \( 0, 2 \pi \) crossing
- */
- public ArcsSet(final Collection<SubHyperplane<S1Point>> boundary, final DoublePrecisionContext precision) {
- super(boundary, precision);
- check2PiConsistency();
- }
-
- /** Build an inside/outside tree representing a single arc.
- * @param lower lower angular bound of the arc
- * @param upper upper angular bound of the arc
- * @param precision precision context used to compare floating point values
- * @return the built tree
- * @exception IllegalArgumentException if lower is greater than upper
- */
- private static BSPTree<S1Point> buildTree(final double lower, final double upper,
- final DoublePrecisionContext precision) {
-
- if (Precision.equals(lower, upper, 0) || (upper - lower) >= Geometry.TWO_PI) {
- // the tree must cover the whole circle
- return new BSPTree<>(Boolean.TRUE);
- } else if (lower > upper) {
- throw new IllegalArgumentException("Endpoints do not specify an interval: [" + lower + ", " + upper + "]");
- }
-
- // this is a regular arc, covering only part of the circle
- final double normalizedLower = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(lower);
- final double normalizedUpper = normalizedLower + (upper - lower);
- final SubHyperplane<S1Point> lowerCut =
- new LimitAngle(S1Point.of(normalizedLower), false, precision).wholeHyperplane();
-
- if (normalizedUpper <= Geometry.TWO_PI) {
- // simple arc starting after 0 and ending before 2 \pi
- final SubHyperplane<S1Point> upperCut =
- new LimitAngle(S1Point.of(normalizedUpper), true, precision).wholeHyperplane();
- return new BSPTree<>(lowerCut,
- new BSPTree<S1Point>(Boolean.FALSE),
- new BSPTree<>(upperCut,
- new BSPTree<S1Point>(Boolean.FALSE),
- new BSPTree<S1Point>(Boolean.TRUE),
- null),
- null);
- } else {
- // arc wrapping around 2 \pi
- final SubHyperplane<S1Point> upperCut =
- new LimitAngle(S1Point.of(normalizedUpper - Geometry.TWO_PI), true, precision).wholeHyperplane();
- return new BSPTree<>(lowerCut,
- new BSPTree<>(upperCut,
- new BSPTree<S1Point>(Boolean.FALSE),
- new BSPTree<S1Point>(Boolean.TRUE),
- null),
- new BSPTree<S1Point>(Boolean.TRUE),
- null);
- }
-
- }
-
- /** Check consistency.
- * @exception IllegalArgumentException if the tree leaf nodes are not
- * consistent across the \( 0, 2 \pi \) crossing
- */
- private void check2PiConsistency() {
-
- // start search at the tree root
- BSPTree<S1Point> root = getTree(false);
- if (root.getCut() == null) {
- return;
- }
-
- // find the inside/outside state before the smallest internal node
- final Boolean stateBefore = (Boolean) getFirstLeaf(root).getAttribute();
-
- // find the inside/outside state after the largest internal node
- final Boolean stateAfter = (Boolean) getLastLeaf(root).getAttribute();
-
- if (stateBefore ^ stateAfter) {
- throw new IllegalArgumentException("Inconsistent state at 2\\u03c0 wrapping");
- }
-
- }
-
- /** Get the first leaf node of a tree.
- * @param root tree root
- * @return first leaf node (i.e. node corresponding to the region just after 0.0 radians)
- */
- private BSPTree<S1Point> getFirstLeaf(final BSPTree<S1Point> root) {
-
- if (root.getCut() == null) {
- return root;
- }
-
- // find the smallest internal node
- BSPTree<S1Point> smallest = null;
- for (BSPTree<S1Point> n = root; n != null; n = previousInternalNode(n)) {
- smallest = n;
- }
-
- return leafBefore(smallest);
-
- }
-
- /** Get the last leaf node of a tree.
- * @param root tree root
- * @return last leaf node (i.e. node corresponding to the region just before \( 2 \pi \) radians)
- */
- private BSPTree<S1Point> getLastLeaf(final BSPTree<S1Point> root) {
-
- if (root.getCut() == null) {
- return root;
- }
-
- // find the largest internal node
- BSPTree<S1Point> largest = null;
- for (BSPTree<S1Point> n = root; n != null; n = nextInternalNode(n)) {
- largest = n;
- }
-
- return leafAfter(largest);
-
- }
-
- /** Get the node corresponding to the first arc start.
- * @return smallest internal node (i.e. first after 0.0 radians, in trigonometric direction),
- * or null if there are no internal nodes (i.e. the set is either empty or covers the full circle)
- */
- private BSPTree<S1Point> getFirstArcStart() {
-
- // start search at the tree root
- BSPTree<S1Point> node = getTree(false);
- if (node.getCut() == null) {
- return null;
- }
-
- // walk tree until we find the smallest internal node
- node = getFirstLeaf(node).getParent();
-
- // walk tree until we find an arc start
- while (node != null && !isArcStart(node)) {
- node = nextInternalNode(node);
- }
-
- return node;
-
- }
-
- /** Check if an internal node corresponds to the start angle of an arc.
- * @param node internal node to check
- * @return true if the node corresponds to the start angle of an arc
- */
- private boolean isArcStart(final BSPTree<S1Point> node) {
-
- if ((Boolean) leafBefore(node).getAttribute()) {
- // it has an inside cell before it, it may end an arc but not start it
- return false;
- }
-
- if (!(Boolean) leafAfter(node).getAttribute()) {
- // it has an outside cell after it, it is a dummy cut away from real arcs
- return false;
- }
-
- // the cell has an outside before and an inside after it
- // it is the start of an arc
- return true;
-
- }
-
- /** Check if an internal node corresponds to the end angle of an arc.
- * @param node internal node to check
- * @return true if the node corresponds to the end angle of an arc
- */
- private boolean isArcEnd(final BSPTree<S1Point> node) {
-
- if (!(Boolean) leafBefore(node).getAttribute()) {
- // it has an outside cell before it, it may start an arc but not end it
- return false;
- }
-
- if ((Boolean) leafAfter(node).getAttribute()) {
- // it has an inside cell after it, it is a dummy cut in the middle of an arc
- return false;
- }
-
- // the cell has an inside before and an outside after it
- // it is the end of an arc
- return true;
-
- }
-
- /** Get the next internal node.
- * @param node current internal node
- * @return next internal node in trigonometric order, or null
- * if this is the last internal node
- */
- private BSPTree<S1Point> nextInternalNode(BSPTree<S1Point> node) {
-
- if (childAfter(node).getCut() != null) {
- // the next node is in the sub-tree
- return leafAfter(node).getParent();
- }
-
- // there is nothing left deeper in the tree, we backtrack
- while (isAfterParent(node)) {
- node = node.getParent();
- }
- return node.getParent();
-
- }
-
- /** Get the previous internal node.
- * @param node current internal node
- * @return previous internal node in trigonometric order, or null
- * if this is the first internal node
- */
- private BSPTree<S1Point> previousInternalNode(BSPTree<S1Point> node) {
-
- if (childBefore(node).getCut() != null) {
- // the next node is in the sub-tree
- return leafBefore(node).getParent();
- }
-
- // there is nothing left deeper in the tree, we backtrack
- while (isBeforeParent(node)) {
- node = node.getParent();
- }
- return node.getParent();
-
- }
-
- /** Find the leaf node just before an internal node.
- * @param node internal node at which the sub-tree starts
- * @return leaf node just before the internal node
- */
- private BSPTree<S1Point> leafBefore(BSPTree<S1Point> node) {
-
- node = childBefore(node);
- while (node.getCut() != null) {
- node = childAfter(node);
- }
-
- return node;
-
- }
-
- /** Find the leaf node just after an internal node.
- * @param node internal node at which the sub-tree starts
- * @return leaf node just after the internal node
- */
- private BSPTree<S1Point> leafAfter(BSPTree<S1Point> node) {
-
- node = childAfter(node);
- while (node.getCut() != null) {
- node = childBefore(node);
- }
-
- return node;
-
- }
-
- /** Check if a node is the child before its parent in trigonometric order.
- * @param node child node considered
- * @return true is the node has a parent end is before it in trigonometric order
- */
- private boolean isBeforeParent(final BSPTree<S1Point> node) {
- final BSPTree<S1Point> parent = node.getParent();
- if (parent == null) {
- return false;
- } else {
- return node == childBefore(parent);
- }
- }
-
- /** Check if a node is the child after its parent in trigonometric order.
- * @param node child node considered
- * @return true is the node has a parent end is after it in trigonometric order
- */
- private boolean isAfterParent(final BSPTree<S1Point> node) {
- final BSPTree<S1Point> parent = node.getParent();
- if (parent == null) {
- return false;
- } else {
- return node == childAfter(parent);
- }
- }
-
- /** Find the child node just before an internal node.
- * @param node internal node at which the sub-tree starts
- * @return child node just before the internal node
- */
- private BSPTree<S1Point> childBefore(BSPTree<S1Point> node) {
- if (isDirect(node)) {
- // smaller angles are on minus side, larger angles are on plus side
- return node.getMinus();
- } else {
- // smaller angles are on plus side, larger angles are on minus side
- return node.getPlus();
- }
- }
-
- /** Find the child node just after an internal node.
- * @param node internal node at which the sub-tree starts
- * @return child node just after the internal node
- */
- private BSPTree<S1Point> childAfter(BSPTree<S1Point> node) {
- if (isDirect(node)) {
- // smaller angles are on minus side, larger angles are on plus side
- return node.getPlus();
- } else {
- // smaller angles are on plus side, larger angles are on minus side
- return node.getMinus();
- }
- }
-
- /** Check if an internal node has a direct limit angle.
- * @param node internal node to check
- * @return true if the limit angle is direct
- */
- private boolean isDirect(final BSPTree<S1Point> node) {
- return ((LimitAngle) node.getCut().getHyperplane()).isDirect();
- }
-
- /** Get the limit angle of an internal node.
- * @param node internal node to check
- * @return limit angle
- */
- private double getAngle(final BSPTree<S1Point> node) {
- return ((LimitAngle) node.getCut().getHyperplane()).getLocation().getAzimuth();
- }
-
- /** {@inheritDoc} */
- @Override
- public ArcsSet buildNew(final BSPTree<S1Point> tree) {
- return new ArcsSet(tree, getPrecision());
- }
-
- /** {@inheritDoc} */
- @Override
- protected void computeGeometricalProperties() {
- if (getTree(false).getCut() == null) {
- setBarycenter(S1Point.NaN);
- setSize(((Boolean) getTree(false).getAttribute()) ? Geometry.TWO_PI : 0);
- } else {
- double size = 0.0;
- double sum = 0.0;
- for (final double[] a : this) {
- final double length = a[1] - a[0];
- size += length;
- sum += length * (a[0] + a[1]);
- }
- setSize(size);
- if (Precision.equals(size, Geometry.TWO_PI, 0)) {
- setBarycenter(S1Point.NaN);
- } else if (size >= Precision.SAFE_MIN) {
- setBarycenter(S1Point.of(sum / (2 * size)));
- } else {
- final LimitAngle limit = (LimitAngle) getTree(false).getCut().getHyperplane();
- setBarycenter(limit.getLocation());
- }
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public BoundaryProjection<S1Point> projectToBoundary(final S1Point point) {
-
- // get position of test point
- final double alpha = point.getAzimuth();
-
- boolean wrapFirst = false;
- double first = Double.NaN;
- double previous = Double.NaN;
- for (final double[] a : this) {
-
- if (Double.isNaN(first)) {
- // remember the first angle in case we need it later
- first = a[0];
- }
-
- if (!wrapFirst) {
- if (alpha < a[0]) {
- // the test point lies between the previous and the current arcs
- // offset will be positive
- if (Double.isNaN(previous)) {
- // we need to wrap around the circle
- wrapFirst = true;
- } else {
- final double previousOffset = alpha - previous;
- final double currentOffset = a[0] - alpha;
- if (previousOffset < currentOffset) {
- return new BoundaryProjection<>(point, S1Point.of(previous), previousOffset);
- } else {
- return new BoundaryProjection<>(point, S1Point.of(a[0]), currentOffset);
- }
- }
- } else if (alpha <= a[1]) {
- // the test point lies within the current arc
- // offset will be negative
- final double offset0 = a[0] - alpha;
- final double offset1 = alpha - a[1];
- if (offset0 < offset1) {
- return new BoundaryProjection<>(point, S1Point.of(a[1]), offset1);
- } else {
- return new BoundaryProjection<>(point, S1Point.of(a[0]), offset0);
- }
- }
- }
- previous = a[1];
- }
-
- if (Double.isNaN(previous)) {
-
- // there are no points at all in the arcs set
- return new BoundaryProjection<>(point, null, Geometry.TWO_PI);
-
- } else {
-
- // the test point if before first arc and after last arc,
- // somewhere around the 0/2 \pi crossing
- if (wrapFirst) {
- // the test point is between 0 and first
- final double previousOffset = alpha - (previous - Geometry.TWO_PI);
- final double currentOffset = first - alpha;
- if (previousOffset < currentOffset) {
- return new BoundaryProjection<>(point, S1Point.of(previous), previousOffset);
- } else {
- return new BoundaryProjection<>(point, S1Point.of(first), currentOffset);
- }
- } else {
- // the test point is between last and 2\pi
- final double previousOffset = alpha - previous;
- final double currentOffset = first + Geometry.TWO_PI - alpha;
- if (previousOffset < currentOffset) {
- return new BoundaryProjection<>(point, S1Point.of(previous), previousOffset);
- } else {
- return new BoundaryProjection<>(point, S1Point.of(first), currentOffset);
- }
- }
-
- }
-
- }
-
- /** Build an ordered list of arcs representing the instance.
- * <p>This method builds this arcs set as an ordered list of
- * {@link Arc Arc} elements. An empty tree will build an empty list
- * while a tree representing the whole circle will build a one
- * element list with bounds set to \( 0 and 2 \pi \).</p>
- * @return a new ordered list containing {@link Arc Arc} elements
- */
- public List<Arc> asList() {
- final List<Arc> list = new ArrayList<>();
- for (final double[] a : this) {
- list.add(new Arc(a[0], a[1], getPrecision()));
- }
- return list;
- }
-
- /** {@inheritDoc}
- * <p>
- * The iterator returns the limit angles pairs of sub-arcs in trigonometric order.
- * </p>
- * <p>
- * The iterator does <em>not</em> support the optional {@code remove} operation.
- * </p>
- */
- @Override
- public Iterator<double[]> iterator() {
- return new SubArcsIterator();
- }
-
- /** Local iterator for sub-arcs. */
- private class SubArcsIterator implements Iterator<double[]> {
-
- /** Start of the first arc. */
- private final BSPTree<S1Point> firstStart;
-
- /** Current node. */
- private BSPTree<S1Point> current;
-
- /** Sub-arc no yet returned. */
- private double[] pending;
-
- /** Simple constructor.
- */
- SubArcsIterator() {
-
- firstStart = getFirstArcStart();
- current = firstStart;
-
- if (firstStart == null) {
- // all the leaf tree nodes share the same inside/outside status
- if ((Boolean) getFirstLeaf(getTree(false)).getAttribute()) {
- // it is an inside node, it represents the full circle
- pending = new double[] {
- 0, Geometry.TWO_PI
- };
- } else {
- pending = null;
- }
- } else {
- selectPending();
- }
- }
-
- /** Walk the tree to select the pending sub-arc.
- */
- private void selectPending() {
-
- // look for the start of the arc
- BSPTree<S1Point> start = current;
- while (start != null && !isArcStart(start)) {
- start = nextInternalNode(start);
- }
-
- if (start == null) {
- // we have exhausted the iterator
- current = null;
- pending = null;
- return;
- }
-
- // look for the end of the arc
- BSPTree<S1Point> end = start;
- while (end != null && !isArcEnd(end)) {
- end = nextInternalNode(end);
- }
-
- if (end != null) {
-
- // we have identified the arc
- pending = new double[] {
- getAngle(start), getAngle(end)
- };
-
- // prepare search for next arc
- current = end;
-
- } else {
-
- // the final arc wraps around 2\pi, its end is before the first start
- end = firstStart;
- while (end != null && !isArcEnd(end)) {
- end = previousInternalNode(end);
- }
- if (end == null) {
- // this should never happen
- throw new GeometryInternalError();
- }
-
- // we have identified the last arc
- pending = new double[] {
- getAngle(start), getAngle(end) + Geometry.TWO_PI
- };
-
- // there won't be any other arcs
- current = null;
-
- }
-
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean hasNext() {
- return pending != null;
- }
-
- /** {@inheritDoc} */
- @Override
- public double[] next() {
- if (pending == null) {
- throw new NoSuchElementException();
- }
- final double[] next = pending;
- selectPending();
- return next;
- }
-
- /** {@inheritDoc} */
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
-
- }
-
- /** Compute the relative position of the instance with respect
- * to an arc.
- * <p>
- * The {@link Side#MINUS} side of the arc is the one covered by the arc.
- * </p>
- * @param arc arc to check instance against
- * @return one of {@link Side#PLUS}, {@link Side#MINUS}, {@link Side#BOTH}
- * or {@link Side#HYPER}
- * @deprecated as of 3.6, replaced with {@link #split(Arc)}.{@link Split#getSide()}
- */
- @Deprecated
- public Side side(final Arc arc) {
- return split(arc).getSide();
- }
-
- /** Split the instance in two parts by an arc.
- * @param arc splitting arc
- * @return an object containing both the part of the instance
- * on the plus side of the arc and the part of the
- * instance on the minus side of the arc
- */
- public Split split(final Arc arc) {
-
- final List<Double> minus = new ArrayList<>();
- final List<Double> plus = new ArrayList<>();
-
- final double reference = Geometry.PI + arc.getInf();
- final double arcLength = arc.getSup() - arc.getInf();
-
- for (final double[] a : this) {
- final double syncedStart = PlaneAngleRadians.normalize(a[0], reference) - arc.getInf();
- final double arcOffset = a[0] - syncedStart;
- final double syncedEnd = a[1] - arcOffset;
- if (syncedStart < arcLength) {
- // the start point a[0] is in the minus part of the arc
- minus.add(a[0]);
- if (syncedEnd > arcLength) {
- // the end point a[1] is past the end of the arc
- // so we leave the minus part and enter the plus part
- final double minusToPlus = arcLength + arcOffset;
- minus.add(minusToPlus);
- plus.add(minusToPlus);
- if (syncedEnd > Geometry.TWO_PI) {
- // in fact the end point a[1] goes far enough that we
- // leave the plus part of the arc and enter the minus part again
- final double plusToMinus = Geometry.TWO_PI + arcOffset;
- plus.add(plusToMinus);
- minus.add(plusToMinus);
- minus.add(a[1]);
- } else {
- // the end point a[1] is in the plus part of the arc
- plus.add(a[1]);
- }
- } else {
- // the end point a[1] is in the minus part of the arc
- minus.add(a[1]);
- }
- } else {
- // the start point a[0] is in the plus part of the arc
- plus.add(a[0]);
- if (syncedEnd > Geometry.TWO_PI) {
- // the end point a[1] wraps around to the start of the arc
- // so we leave the plus part and enter the minus part
- final double plusToMinus = Geometry.TWO_PI + arcOffset;
- plus.add(plusToMinus);
- minus.add(plusToMinus);
- if (syncedEnd > Geometry.TWO_PI + arcLength) {
- // in fact the end point a[1] goes far enough that we
- // leave the minus part of the arc and enter the plus part again
- final double minusToPlus = Geometry.TWO_PI + arcLength + arcOffset;
- minus.add(minusToPlus);
- plus.add(minusToPlus);
- plus.add(a[1]);
- } else {
- // the end point a[1] is in the minus part of the arc
- minus.add(a[1]);
- }
- } else {
- // the end point a[1] is in the plus part of the arc
- plus.add(a[1]);
- }
- }
- }
-
- return new Split(createSplitPart(plus), createSplitPart(minus));
-
- }
-
- /** Add an arc limit to a BSP tree under construction.
- * @param tree BSP tree under construction
- * @param alpha arc limit
- * @param isStart if true, the limit is the start of an arc
- */
- private void addArcLimit(final BSPTree<S1Point> tree, final double alpha, final boolean isStart) {
-
- final LimitAngle limit = new LimitAngle(S1Point.of(alpha), !isStart, getPrecision());
- final BSPTree<S1Point> node = tree.getCell(limit.getLocation(), getPrecision());
- if (node.getCut() != null) {
- // this should never happen
- throw new GeometryInternalError();
- }
-
- node.insertCut(limit);
- node.setAttribute(null);
- node.getPlus().setAttribute(Boolean.FALSE);
- node.getMinus().setAttribute(Boolean.TRUE);
-
- }
-
- /** Create a split part.
- * <p>
- * As per construction, the list of limit angles is known to have
- * an even number of entries, with start angles at even indices and
- * end angles at odd indices.
- * </p>
- * @param limits limit angles of the split part
- * @return split part (may be null)
- */
- private ArcsSet createSplitPart(final List<Double> limits) {
- if (limits.isEmpty()) {
- return null;
- } else {
-
- // collapse close limit angles
- for (int i = 0; i < limits.size(); ++i) {
- final int j = (i + 1) % limits.size();
- final double lA = limits.get(i);
- final double lB = PlaneAngleRadians.normalize(limits.get(j), lA);
- if (getPrecision().eq(lB, lA)) {
- // the two limits are too close to each other, we remove both of them
- if (j > 0) {
- // regular case, the two entries are consecutive ones
- limits.remove(j);
- limits.remove(i);
- i = i - 1;
- } else {
- // special case, i the the last entry and j is the first entry
- // we have wrapped around list end
- final double lEnd = limits.remove(limits.size() - 1);
- final double lStart = limits.remove(0);
- if (limits.isEmpty()) {
- // the ends were the only limits, is it a full circle or an empty circle?
- if (lEnd - lStart > Geometry.PI) {
- // it was full circle
- return new ArcsSet(new BSPTree<S1Point>(Boolean.TRUE), getPrecision());
- } else {
- // it was an empty circle
- return null;
- }
- } else {
- // we have removed the first interval start, so our list
- // currently starts with an interval end, which is wrong
- // we need to move this interval end to the end of the list
- limits.add(limits.remove(0) + Geometry.TWO_PI);
- }
- }
- }
- }
-
- // build the tree by adding all angular sectors
- BSPTree<S1Point> tree = new BSPTree<>(Boolean.FALSE);
- for (int i = 0; i < limits.size() - 1; i += 2) {
- addArcLimit(tree, limits.get(i), true);
- addArcLimit(tree, limits.get(i + 1), false);
- }
-
- if (tree.getCut() == null) {
- // we did not insert anything
- return null;
- }
-
- return new ArcsSet(tree, getPrecision());
-
- }
- }
-
- /** Class holding the results of the {@link #split split} method.
- */
- public static class Split {
-
- /** Part of the arcs set on the plus side of the splitting arc. */
- private final ArcsSet plus;
-
- /** Part of the arcs set on the minus side of the splitting arc. */
- private final ArcsSet minus;
-
- /** Build a Split from its parts.
- * @param plus part of the arcs set on the plus side of the
- * splitting arc
- * @param minus part of the arcs set on the minus side of the
- * splitting arc
- */
- private Split(final ArcsSet plus, final ArcsSet minus) {
- this.plus = plus;
- this.minus = minus;
- }
-
- /** Get the part of the arcs set on the plus side of the splitting arc.
- * @return part of the arcs set on the plus side of the splitting arc
- */
- public ArcsSet getPlus() {
- return plus;
- }
-
- /** Get the part of the arcs set on the minus side of the splitting arc.
- * @return part of the arcs set on the minus side of the splitting arc
- */
- public ArcsSet getMinus() {
- return minus;
- }
-
- /** Get the side of the split arc with respect to its splitter.
- * @return {@link Side#PLUS} if only {@link #getPlus()} returns non-null,
- * {@link Side#MINUS} if only {@link #getMinus()} returns non-null,
- * {@link Side#BOTH} if both {@link #getPlus()} and {@link #getMinus()}
- * return non-null or {@link Side#HYPER} if both {@link #getPlus()} and
- * {@link #getMinus()} return null
- */
- public Side getSide() {
- if (plus != null) {
- if (minus != null) {
- return Side.BOTH;
- } else {
- return Side.PLUS;
- }
- } else if (minus != null) {
- return Side.MINUS;
- } else {
- return Side.HYPER;
- }
- }
- }
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
new file mode 100644
index 0000000..40b538e
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
@@ -0,0 +1,518 @@
+/*
+ * 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.oned;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Class representing an oriented point in 1-dimensional spherical space,
+ * meaning an azimuth angle and a direction (increasing or decreasing angles)
+ * along the circle.
+ *
+ * <p>Hyperplanes split the spaces they are embedded in into three distinct parts:
+ * the hyperplane itself, a plus side and a minus side. However, since spherical
+ * space wraps around, a single oriented point is not sufficient to partition the space;
+ * any point could be classified as being on the plus or minus side of a hyperplane
+ * depending on the direction that the circle is traversed. The approach taken in this
+ * class to address this issue is to (1) define a second, implicit cut point at {@code 0pi} and
+ * (2) define the domain of hyperplane points (for partitioning purposes) to be the
+ * range {@code [0, 2pi)}. Each hyperplane then splits the space into the intervals
+ * {@code [0, x]} and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane.
+ * One way to visualize this is to picture the circle as a cake that has already been
+ * cut at {@code 0pi}. Each hyperplane then specifies the location of the second
+ * cut of the cake, with the plus and minus sides being the pieces thus cut.
+ * </p>
+ *
+ * <p>Note that with the hyperplane partitioning rules described above, the hyperplane
+ * at {@code 0pi} is unique in that it has the entire space on one side (minus the hyperplane
+ * itself) and no points whatsoever on the other. This is very different from hyperplanes in
+ * Euclidean space, which always have infinitely many points on both sides.</p>
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class CutAngle extends AbstractHyperplane<Point1S>
+ implements Equivalency<CutAngle>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190817L;
+
+ /** Hyperplane location as a point. */
+ private final Point1S point;
+
+ /** Hyperplane direction. */
+ private final boolean positiveFacing;
+
+ /** Simple constructor.
+ * @param point location of the hyperplane
+ * @param positiveFacing if true, the hyperplane will point in a positive angular
+ * direction; otherwise, it will point in a negative direction
+ * @param precision precision context used to compare floating point values
+ */
+ private CutAngle(final Point1S point, final boolean positiveFacing,
+ final DoublePrecisionContext precision) {
+ super(precision);
+
+ this.point = point;
+ this.positiveFacing = positiveFacing;
+ }
+
+ /** Get the location of the hyperplane as a point.
+ * @return the hyperplane location as a point
+ * @see #getAzimuth()
+ */
+ public Point1S getPoint() {
+ return point;
+ }
+
+ /** Get the location of the hyperplane as a single value. This is
+ * equivalent to {@code cutAngle.getPoint().getAzimuth()}.
+ * @return the location of the hyperplane as a single value.
+ * @see #getPoint()
+ * @see Point1S#getAzimuth()
+ */
+ public double getAzimuth() {
+ return point.getAzimuth();
+ }
+
+ /** Get the location of the hyperplane as a single value, normalized
+ * to the range {@code [0, 2pi)}. This is equivalent to
+ * {@code cutAngle.getPoint().getNormalizedAzimuth()}.
+ * @return the location of the hyperplane, normalized to the range
+ * {@code [0, 2pi)}
+ * @see #getPoint()
+ * @see Point1S#getNormalizedAzimuth()
+ */
+ public double getNormalizedAzimuth() {
+ return point.getNormalizedAzimuth();
+ }
+
+ /** Return true if the hyperplane is oriented with its plus
+ * side pointing toward increasing angles.
+ * @return true if the hyperplane is facing in the direction
+ * of increasing angles
+ */
+ public boolean isPositiveFacing() {
+ return positiveFacing;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>The instances are considered equivalent if they
+ * <ol>
+ * <li>have equal precision contexts,</li>
+ * <li>have equivalent point locations as evaluated by the precision
+ * context (points separated by multiples of 2pi are considered equivalent), and
+ * <li>point in the same direction.</li>
+ * </ol>
+ * @see Point1S#eq(Point1S, DoublePrecisionContext)
+ */
+ @Override
+ public boolean eq(final CutAngle other) {
+ if (this == other) {
+ return true;
+ }
+
+ final DoublePrecisionContext precision = getPrecision();
+
+ return precision.equals(other.getPrecision()) &&
+ point.eq(other.point, precision) &&
+ positiveFacing == other.positiveFacing;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double offset(final Point1S pt) {
+ final double dist = pt.getNormalizedAzimuth() - this.point.getNormalizedAzimuth();
+ return positiveFacing ? +dist : -dist;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public HyperplaneLocation classify(final Point1S pt) {
+ final DoublePrecisionContext precision = getPrecision();
+
+ final Point1S compPt = Point1S.ZERO.eq(pt, precision) ?
+ Point1S.ZERO :
+ pt;
+
+ final double offsetValue = offset(compPt);
+ final int cmp = precision.sign(offsetValue);
+
+ if (cmp > 0) {
+ return HyperplaneLocation.PLUS;
+ } else if (cmp < 0) {
+ return HyperplaneLocation.MINUS;
+ }
+
+ return HyperplaneLocation.ON;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point1S project(final Point1S pt) {
+ return this.point;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CutAngle reverse() {
+ return new CutAngle(point, !positiveFacing, getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CutAngle transform(final Transform<Point1S> transform) {
+ final Point1S tPoint = transform.apply(point);
+ final boolean tPositiveFacing = transform.preservesOrientation() == positiveFacing;
+
+ return CutAngle.fromPointAndDirection(tPoint, tPositiveFacing, getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean similarOrientation(final Hyperplane<Point1S> other) {
+ return positiveFacing == ((CutAngle) other).positiveFacing;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubCutAngle span() {
+ return new SubCutAngle(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(point, positiveFacing, getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof CutAngle)) {
+ return false;
+ }
+
+ final CutAngle other = (CutAngle) obj;
+ return Objects.equals(getPrecision(), other.getPrecision()) &&
+ Objects.equals(point, other.point) &&
+ positiveFacing == other.positiveFacing;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[point= ")
+ .append(point)
+ .append(", positiveFacing= ")
+ .append(isPositiveFacing())
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Create a new instance from the given azimuth and direction.
+ * @param azimuth azimuth value in radians
+ * @param positiveFacing if true, the instance's plus side will be oriented to point toward increasing
+ * angular values; if false, it will point toward decreasing angular value
+ * @param precision precision context used to determine floating point equality
+ * @return a new instance
+ */
+ public static CutAngle fromAzimuthAndDirection(final double azimuth, final boolean positiveFacing,
+ final DoublePrecisionContext precision) {
+ return fromPointAndDirection(Point1S.of(azimuth), positiveFacing, precision);
+ }
+
+ /** Create a new instance from the given point and direction.
+ * @param point point representing the location of the hyperplane
+ * @param positiveFacing if true, the instance's plus side will be oriented to point toward increasing
+ * angular values; if false, it will point toward decreasing angular value
+ * @param precision precision context used to determine floating point equality
+ * @return a new instance
+ */
+ public static CutAngle fromPointAndDirection(final Point1S point, final boolean positiveFacing,
+ final DoublePrecisionContext precision) {
+ return new CutAngle(point, positiveFacing, precision);
+ }
+
+ /** Create a new instance at the given azimuth, oriented so that the plus side of the hyperplane points
+ * toward increasing angular values.
+ * @param azimuth azimuth value in radians
+ * @param precision precision precision context used to determine floating point equality
+ * @return a new instance
+ */
+ public static CutAngle createPositiveFacing(final double azimuth, final DoublePrecisionContext precision) {
+ return createPositiveFacing(Point1S.of(azimuth), precision);
+ }
+
+ /** Create a new instance at the given point, oriented so that the plus side of the hyperplane points
+ * toward increasing angular values.
+ * @param point point representing the location of the hyperplane
+ * @param precision precision precision context used to determine floating point equality
+ * @return a new instance
+ */
+ public static CutAngle createPositiveFacing(final Point1S point, final DoublePrecisionContext precision) {
+ return fromPointAndDirection(point, true, precision);
+ }
+
+ /** Create a new instance at the given azimuth, oriented so that the plus side of the hyperplane points
+ * toward decreasing angular values.
+ * @param azimuth azimuth value in radians
+ * @param precision precision precision context used to determine floating point equality
+ * @return a new instance
+ */
+ public static CutAngle createNegativeFacing(final double azimuth, final DoublePrecisionContext precision) {
+ return createNegativeFacing(Point1S.of(azimuth), precision);
+ }
+
+ /** Create a new instance at the given point, oriented so that the plus side of the hyperplane points
+ * toward decreasing angular values.
+ * @param point point representing the location of the hyperplane
+ * @param precision precision precision context used to determine floating point equality
+ * @return a new instance
+ */
+ public static CutAngle createNegativeFacing(final Point1S point, final DoublePrecisionContext precision) {
+ return fromPointAndDirection(point, false, precision);
+ }
+
+ /** {@link ConvexSubHyperplane} implementation for spherical 1D space. Since there are no subspaces in 1D,
+ * this is effectively a stub implementation, its main use being to allow for the correct functioning of
+ * partitioning code.
+ */
+ public static class SubCutAngle implements ConvexSubHyperplane<Point1S>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190825L;
+
+ /** The underlying hyperplane for this instance. */
+ private final CutAngle hyperplane;
+
+ /** Simple constructor.
+ * @param hyperplane underlying hyperplane instance
+ */
+ public SubCutAngle(final CutAngle hyperplane) {
+ this.hyperplane = hyperplane;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CutAngle getHyperplane() {
+ return hyperplane;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns false.</p>
+ */
+ @Override
+ public boolean isFull() {
+ return false;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns false.</p>
+ */
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns false.</p>
+ */
+ @Override
+ public boolean isInfinite() {
+ return false;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns true.</p>
+ */
+ @Override
+ public boolean isFinite() {
+ return true;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns {@code 0}.</p>
+ */
+ @Override
+ public double getSize() {
+ return 0;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method returns {@link RegionLocation#BOUNDARY} if the
+ * point is on the hyperplane and {@link RegionLocation#OUTSIDE}
+ * otherwise.</p>
+ */
+ @Override
+ public RegionLocation classify(Point1S point) {
+ if (hyperplane.contains(point)) {
+ return RegionLocation.BOUNDARY;
+ }
+
+ return RegionLocation.OUTSIDE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point1S closest(Point1S point) {
+ return hyperplane.project(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<SubCutAngle> split(final Hyperplane<Point1S> splitter) {
+ final HyperplaneLocation side = splitter.classify(hyperplane.getPoint());
+
+ SubCutAngle minus = null;
+ SubCutAngle plus = null;
+
+ if (side == HyperplaneLocation.MINUS) {
+ minus = this;
+ } else if (side == HyperplaneLocation.PLUS) {
+ plus = this;
+ }
+
+ return new Split<>(minus, plus);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<SubCutAngle> toConvex() {
+ return Arrays.asList(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubCutAngle transform(final Transform<Point1S> transform) {
+ return getHyperplane().transform(transform).span();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubCutAngleBuilder builder() {
+ return new SubCutAngleBuilder(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubCutAngle reverse() {
+ return new SubCutAngle(hyperplane.reverse());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[hyperplane= ")
+ .append(hyperplane)
+ .append(']');
+
+ return sb.toString();
+ }
+ }
+
+ /** {@link SubHyperplane.Builder} implementation for spherical 1D space. This is effectively
+ * a stub implementation since there are no subspaces of 1D space. Its primary use is to allow
+ * for the correct functioning of partitioning code.
+ */
+ public static final class SubCutAngleBuilder implements SubHyperplane.Builder<Point1S>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190825L;
+
+ /** Base subhyperplane for the builder. */
+ private final SubCutAngle base;
+
+ /** Construct a new instance using the given base subhyperplane.
+ * @param base base subhyperplane for the instance
+ */
+ private SubCutAngleBuilder(final SubCutAngle base) {
+ this.base = base;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final SubHyperplane<Point1S> sub) {
+ validateHyperplane(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final ConvexSubHyperplane<Point1S> sub) {
+ validateHyperplane(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubCutAngle build() {
+ return base;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[base= ")
+ .append(base)
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Validate the given subhyperplane lies on the same hyperplane.
+ * @param sub subhyperplane to validate
+ */
+ private void validateHyperplane(final SubHyperplane<Point1S> sub) {
+ final CutAngle baseHyper = base.getHyperplane();
+ final CutAngle inputHyper = (CutAngle) sub.getHyperplane();
+
+ if (!baseHyper.eq(inputHyper)) {
+ throw new GeometryException("Argument is not on the same " +
+ "hyperplane. Expected " + baseHyper + " but was " +
+ inputHyper);
+ }
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/LimitAngle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/LimitAngle.java
deleted file mode 100644
index 554c483..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/LimitAngle.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * 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.oned;
-
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-
-/** This class represents a 1D oriented hyperplane on the circle.
- * <p>An hyperplane on the 1-sphere is an angle with an orientation.</p>
- * <p>Instances of this class are guaranteed to be immutable.</p>
- */
-public class LimitAngle implements Hyperplane<S1Point> {
-
- /** Angle location. */
- private final S1Point location;
-
- /** Orientation. */
- private final boolean direct;
-
- /** Precision context used to compare floating point numbers. */
- private final DoublePrecisionContext precision;
-
- /** Simple constructor.
- * @param location location of the hyperplane
- * @param direct if true, the plus side of the hyperplane is towards
- * angles greater than {@code location}
- * @param precision precision context used to compare floating point values
- */
- public LimitAngle(final S1Point location, final boolean direct, final DoublePrecisionContext precision) {
- this.location = location;
- this.direct = direct;
- this.precision = precision;
- }
-
- /** Copy the instance.
- * <p>Since instances are immutable, this method directly returns
- * the instance.</p>
- * @return the instance itself
- */
- @Override
- public LimitAngle copySelf() {
- return this;
- }
-
- /** {@inheritDoc} */
- @Override
- public double getOffset(final S1Point point) {
- final double delta = point.getAzimuth() - location.getAzimuth();
- return direct ? delta : -delta;
- }
-
- /** Check if the hyperplane orientation is direct.
- * @return true if the plus side of the hyperplane is towards
- * angles greater than hyperplane location
- */
- public boolean isDirect() {
- return direct;
- }
-
- /** Get the reverse of the instance.
- * <p>Get a limit angle with reversed orientation with respect to the
- * instance. A new object is built, the instance is untouched.</p>
- * @return a new limit angle, with orientation opposite to the instance orientation
- */
- public LimitAngle getReverse() {
- return new LimitAngle(location, !direct, precision);
- }
-
- /** Build a region covering the whole hyperplane.
- * <p>Since this class represent zero dimension spaces which does
- * not have lower dimension sub-spaces, this method returns a dummy
- * implementation of a {@link
- * org.apache.commons.geometry.core.partitioning.SubHyperplane SubHyperplane}.
- * This implementation is only used to allow the {@link
- * org.apache.commons.geometry.core.partitioning.SubHyperplane
- * SubHyperplane} class implementation to work properly, it should
- * <em>not</em> be used otherwise.</p>
- * @return a dummy sub hyperplane
- */
- @Override
- public SubLimitAngle wholeHyperplane() {
- return new SubLimitAngle(this, null);
- }
-
- /** Build a region covering the whole space.
- * @return a region containing the instance (really an {@link
- * ArcsSet IntervalsSet} instance)
- */
- @Override
- public ArcsSet wholeSpace() {
- return new ArcsSet(precision);
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean sameOrientationAs(final Hyperplane<S1Point> other) {
- return !(direct ^ ((LimitAngle) other).direct);
- }
-
- /** Get the hyperplane location on the circle.
- * @return the hyperplane location
- */
- public S1Point getLocation() {
- return location;
- }
-
- /** {@inheritDoc} */
- @Override
- public S1Point project(S1Point point) {
- return location;
- }
-
- /** {@inheritDoc} */
- @Override
- public DoublePrecisionContext getPrecision() {
- return precision;
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Point1S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Point1S.java
new file mode 100644
index 0000000..4af43fa
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Point1S.java
@@ -0,0 +1,393 @@
+/*
+ * 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.oned;
+
+import java.io.Serializable;
+import java.util.Comparator;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.numbers.angle.PlaneAngle;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
+
+/** This class represents a point on the 1-sphere, or in other words, an
+ * azimuth angle on a circle. The value of the azimuth angle is not normalized
+ * by default, meaning that instances can be constructed representing negative
+ * values or values greater than {@code 2pi}. However, instances separated by a
+ * multiple of {@code 2pi} are considered equivalent for most methods, with the
+ * exceptions being {@link #equals(Object)} and {@link #hashCode()}, where the
+ * azimuth values must match exactly in order for instances to be considered
+ * equal.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Point1S implements Point<Point1S>, Serializable {
+
+ /** A point with coordinates set to {@code 0*pi}. */
+ public static final Point1S ZERO = Point1S.of(Geometry.ZERO_PI);
+
+ /** A point with coordinates set to {@code pi}. */
+ public static final Point1S PI = Point1S.of(Geometry.PI);
+
+ // CHECKSTYLE: stop ConstantName
+ /** A point with all coordinates set to NaN. */
+ public static final Point1S NaN = Point1S.of(Double.NaN);
+ // CHECKSTYLE: resume ConstantName
+
+ /** Comparator that sorts points by normalized azimuth in ascending order.
+ * Points are only considered equal if their normalized azimuths match exactly.
+ * Null arguments are evaluated as being greater than non-null arguments.
+ * @see #getNormalizedAzimuth()
+ */
+ public static final Comparator<Point1S> NORMALIZED_AZIMUTH_ASCENDING_ORDER = (a, b) -> {
+ int cmp = 0;
+
+ if (a != null && b != null) {
+ cmp = Double.compare(a.getNormalizedAzimuth(), b.getNormalizedAzimuth());
+ } else if (a != null) {
+ cmp = -1;
+ } else if (b != null) {
+ cmp = 1;
+ }
+
+ return cmp;
+ };
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20180710L;
+
+ /** Azimuthal angle in radians. */
+ private final double azimuth;
+
+ /** Normalized azimuth value in the range {@code [0, 2pi)}. */
+ private final double normalizedAzimuth;
+
+ /** Build a point from its internal components.
+ * @param azimuth azimuth angle
+ * @param normalizedAzimuth azimuth angle normalized to the range {@code [0, 2pi)}
+ */
+ private Point1S(final double azimuth, final double normalizedAzimuth) {
+ this.azimuth = azimuth;
+ this.normalizedAzimuth = normalizedAzimuth;
+ }
+
+ /** Get the azimuth angle in radians. This value is not normalized and
+ * can be any floating point number.
+ * @return azimuth angle
+ * @see Point1S#of(double)
+ */
+ public double getAzimuth() {
+ return azimuth;
+ }
+
+ /** Get the azimuth angle normalized to the range {@code [0, 2pi)}.
+ * @return the azimuth angle normalized to the range {@code [0, 2pi)}.
+ */
+ public double getNormalizedAzimuth() {
+ return normalizedAzimuth;
+ }
+
+ /** Get the normalized vector corresponding to this azimuth angle in 2D Euclidean space.
+ * @return normalized vector
+ */
+ public Vector2D getVector() {
+ if (isFinite()) {
+ return PolarCoordinates.toCartesian(1, azimuth);
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getDimension() {
+ return 1;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isNaN() {
+ return Double.isNaN(azimuth);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return !isNaN() && Double.isInfinite(azimuth);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return Double.isFinite(azimuth);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>The returned value is the shortest angular distance between
+ * the two points, in the range {@code [0, pi]}.</p>
+ */
+ @Override
+ public double distance(final Point1S point) {
+ return distance(this, point);
+ }
+
+ /** Return the signed distance (angular separation) between this instance and the
+ * given point in the range {@code [-pi, pi)}. If {@code p1} is the current instance,
+ * {@code p2} the given point, and {@code d} the signed distance, then
+ * {@code p1.getAzimuth() + d} is an angle equivalent to {@code p2.getAzimuth()}.
+ * @param point point to compute the signed distance to
+ * @return the signed distance between this instance and the given point in the range
+ * {@code [-pi, pi)}
+ */
+ public double signedDistance(final Point1S point) {
+ return signedDistance(this, point);
+ }
+
+ /** Return an equivalent point with an azimuth value at or above the given base.
+ * The returned point has an azimuth value in the range {@code [base, base + 2pi)}.
+ * @param base point to place this instance's azimuth value above
+ * @return a point equivalent to the current instance but with an azimuth
+ * value in the range {@code [base, base + 2pi)}
+ * @throws GeometryValueException if the azimuth value is NaN or infinite and
+ * cannot be normalized
+ */
+ public Point1S above(final Point1S base) {
+ return normalize(base.getAzimuth() + Geometry.PI);
+ }
+
+ /** Return an equivalent point with an azimuth value strictly below the given base.
+ * The returned point has an azimuth value in the range {@code [base - 2pi, base)}.
+ * @param base point to place this instance's azimuth value below
+ * @return a point equivalent to the current instance but with an azimuth
+ * value in the range {@code [base - 2pi, base)}
+ * @throws GeometryValueException if the azimuth value is NaN or infinite and
+ * cannot be normalized
+ */
+ public Point1S below(final Point1S base) {
+ return normalize(base.getAzimuth() - Geometry.PI);
+ }
+
+ /** Normalize this point around the given center point. The azimuth value of
+ * the returned point is in the range {@code [center - pi, center + pi)}.
+ * @param center point to center this instance around
+ * @return a point equivalent to this instance but with an azimuth value
+ * in the range {@code [center - pi, center + pi)}.
+ * @throws GeometryValueException if the azimuth value is NaN or infinite and
+ * cannot be normalized
+ */
+ public Point1S normalize(final Point1S center) {
+ return normalize(center.getAzimuth());
+ }
+
+ /** Return an equivalent point with an azimuth value normalized around the given center
+ * angle. The azimuth value of the returned point is in the range
+ * {@code [center - pi, center + pi)}.
+ * @param center angle to center this instance around
+ * @return a point equivalent to this instance but with an azimuth value
+ * in the range {@code [center - pi, center + pi)}.
+ * @throws GeometryValueException if the azimuth value is NaN or infinite and
+ * cannot be normalized
+ */
+ public Point1S normalize(final double center) {
+ if (isFinite()) {
+ final double az = PlaneAngleRadians.normalize(azimuth, center);
+ return new Point1S(az, normalizedAzimuth);
+ }
+ throw new GeometryValueException("Cannot normalize azimuth value: " + azimuth);
+ }
+
+ /** Get the point exactly opposite this point on the circle, {@code pi} distance away.
+ * The azimuth of the antipodal point is in the range {@code [0, 2pi)}.
+ * @return the point exactly opposite this point on the circle
+ */
+ public Point1S antipodal() {
+ double az = normalizedAzimuth + Geometry.PI;
+ if (az >= Geometry.TWO_PI) {
+ az -= Geometry.TWO_PI;
+ }
+
+ return Point1S.of(az);
+ }
+
+ /** Return true if this instance is equivalent to the argument. The points are
+ * considered equivalent if the shortest angular distance between them is equal to
+ * zero as evaluated by the given precision context. This means that points that differ
+ * in azimuth by multiples of {@code 2pi} are considered equivalent.
+ * @param other point to compare with
+ * @param precision precision context used for floating point comparisons
+ * @return true if this instance is equivalent to the argument
+ */
+ public boolean eq(final Point1S other, final DoublePrecisionContext precision) {
+ final double dist = signedDistance(other);
+ return precision.eqZero(dist);
+ }
+
+ /**
+ * Get a hashCode for the point. Points normally must have exactly the
+ * same azimuth angles in order to have the same hash code. Points
+ * will angles that differ by multiples of {@code 2pi} will not
+ * necessarily have the same hash code.
+ *
+ * <p>All NaN values have the same hash code.</p>
+ *
+ * @return a hash code value for this object
+ */
+ @Override
+ public int hashCode() {
+ if (isNaN()) {
+ return 542;
+ }
+ return 1759 * Objects.hash(azimuth, normalizedAzimuth);
+ }
+
+ /** Test for the exact equality of two points on the 1-sphere.
+ *
+ * <p>If all coordinates of the given points are exactly the same, and none are
+ * <code>Double.NaN</code>, the points are considered to be equal. Points with
+ * azimuth values separated by multiples of {@code 2pi} are <em>not</em> considered
+ * equal.</p>
+ *
+ * <p><code>NaN</code> coordinates are considered to affect globally the vector
+ * and be equals to each other - i.e, if either (or all) coordinates of the
+ * point are equal to <code>Double.NaN</code>, the point is equal to
+ * {@link #NaN}.</p>
+ *
+ * @param other Object to test for equality to this
+ * @return true if two points on the 1-sphere objects are exactly equal, false if
+ * object is null, not an instance of Point1S, or
+ * not equal to this Point1S instance
+ *
+ */
+ @Override
+ public boolean equals(final Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (other instanceof Point1S) {
+ final Point1S rhs = (Point1S) other;
+
+ if (rhs.isNaN()) {
+ return this.isNaN();
+ }
+
+ return Double.compare(azimuth, rhs.azimuth) == 0 &&
+ Double.compare(normalizedAzimuth, rhs.normalizedAzimuth) == 0;
+ }
+
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return SimpleTupleFormat.getDefault().format(getAzimuth());
+ }
+
+ /** Create a new point instance from the given azimuth angle.
+ * @param azimuth azimuth angle in radians
+ * @return point instance with the given azimuth angle
+ * @see #getAzimuth()
+ */
+ public static Point1S of(final double azimuth) {
+ final double normalizedAzimuth = PolarCoordinates.normalizeAzimuth(azimuth);
+
+ return new Point1S(azimuth, normalizedAzimuth);
+ }
+
+ /** Create a new point instance from the given azimuth angle.
+ * @param azimuth azimuth azimuth angle in radians
+ * @return point instance with the given azimuth angle
+ * @see #getAzimuth()
+ */
+ public static Point1S of(final PlaneAngle azimuth) {
+ return of(azimuth.toRadians());
+ }
+
+ /** Create a new point instance from the given Euclidean 2D vector. The returned point
+ * will have an azimuth value equal to the angle between the positive x-axis and the
+ * given vector, measured in a counter-clockwise direction.
+ * @param vector 3D vector to create the point from
+ * @return a new point instance with an azimuth value equal to the angle between the given
+ * vector and the positive x-axis, measured in a counter-clockwise direction
+ */
+ public static Point1S from(final Vector2D vector) {
+ final PolarCoordinates polar = PolarCoordinates.fromCartesian(vector);
+ final double az = polar.getAzimuth();
+
+ return new Point1S(az, az);
+ }
+
+ /** Create a new point instance containing an azimuth value equal to that of the
+ * given set of polar coordinates.
+ * @param polar polar coordinates to convert to a point
+ * @return a new point instance containing an azimuth value equal to that of
+ * the given set of polar coordinates.
+ */
+ public static Point1S from(final PolarCoordinates polar) {
+ final double az = polar.getAzimuth();
+
+ return new Point1S(az, az);
+ }
+
+ /** Parse the given string and returns a new point instance. The expected string
+ * format is the same as that returned by {@link #toString()}.
+ * @param str the string to parse
+ * @return point instance represented by the string
+ * @throws IllegalArgumentException if the given string has an invalid format
+ */
+ public static Point1S parse(final String str) {
+ return SimpleTupleFormat.getDefault().parse(str, az -> Point1S.of(az));
+ }
+
+ /** Compute the signed shortest distance (angular separation) between two points. The return
+ * value is in the range {@code [-pi, pi)} and is such that {@code p1.getAzimuth() + d}
+ * (where {@code d} is the signed distance) is an angle equivalent to {@code p2.getAzimuth()}.
+ * @param p1 first point
+ * @param p2 second point
+ * @return the signed angular separation between p1 and p2, in the range {@code [-pi, pi)}.
+ */
+ public static double signedDistance(final Point1S p1, final Point1S p2) {
+ double dist = p2.normalizedAzimuth - p1.normalizedAzimuth;
+ if (dist < -Geometry.PI) {
+ dist += Geometry.TWO_PI;
+ }
+ if (dist >= Geometry.PI) {
+ dist -= Geometry.TWO_PI;
+ }
+ return dist;
+ }
+
+ /** Compute the shortest distance (angular separation) between two points. The returned
+ * value is in the range {@code [0, pi]}. This method is equal to the absolute value of
+ * the {@link #signedDistance(Point1S, Point1S) signed distance}.
+ * @param p1 first point
+ * @param p2 second point
+ * @return the angular separation between p1 and p2, in the range {@code [0, pi]}.
+ * @see #signedDistance(Point1S, Point1S)
+ */
+ public static double distance(final Point1S p1, final Point1S p2) {
+ return Math.abs(signedDistance(p1, p2));
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java
new file mode 100644
index 0000000..2c17465
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java
@@ -0,0 +1,511 @@
+/*
+ * 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.oned;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** BSP tree representing regions in 1D spherical space.
+ */
+public class RegionBSPTree1S extends AbstractRegionBSPTree<Point1S, RegionBSPTree1S.RegionNode1S> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190817L;
+
+ /** Comparator used to sort BoundaryPairs by ascending azimuth. */
+ private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR = (BoundaryPair a, BoundaryPair b) -> {
+ return Double.compare(a.getMinValue(), b.getMinValue());
+ };
+
+ /** Create a new, empty instance.
+ */
+ public RegionBSPTree1S() {
+ this(false);
+ }
+
+ /** Create a new region. If {@code full} is true, then the region will
+ * represent the entire circle. Otherwise, it will be empty.
+ * @param full whether or not the region should contain the entire
+ * circle or be empty
+ */
+ public RegionBSPTree1S(boolean full) {
+ super(full);
+ }
+
+ /** Return a deep copy of this instance.
+ * @return a deep copy of this instance.
+ * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
+ */
+ public RegionBSPTree1S copy() {
+ RegionBSPTree1S result = RegionBSPTree1S.empty();
+ result.copy(this);
+
+ return result;
+ }
+
+ /** Add an interval to this region. The resulting region will be the
+ * union of the interval and the region represented by this instance.
+ * @param interval the interval to add
+ */
+ public void add(final AngularInterval interval) {
+ union(fromInterval(interval));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point1S project(final Point1S pt) {
+ final BoundaryProjector1S projector = new BoundaryProjector1S(pt);
+ accept(projector);
+
+ return projector.getProjected();
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>Each interval of the region is transformed individually and the
+ * results are unioned. If the size of any transformed interval is greater
+ * than or equal to 2pi, then the region is set to the full space.</p>
+ */
+ @Override
+ public void transform(final Transform<Point1S> transform) {
+ if (!isFull() && !isEmpty()) {
+ // transform each interval individually to handle wrap-around
+ final List<AngularInterval> intervals = toIntervals();
+
+ setEmpty();
+
+ for (AngularInterval interval : intervals) {
+ union(interval.transform(transform).toTree());
+ }
+ }
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>It is important to note that split operations occur according to the rules of the
+ * {@link CutAngle} hyperplane class. In this class, the continuous circle is viewed
+ * as a non-circular segment of the number line in the range {@code [0, 2pi)}. Hyperplanes
+ * are placed along this line and partition it into the segments {@code [0, x]}
+ * and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane. For example,
+ * a positive-facing {@link CutAngle} instance with an azimuth of {@code 0.5pi} has
+ * a minus side consisting of the angles {@code [0, 0.5pi]} and a plus side consisting of
+ * the angles {@code [0.5pi, 2pi)}. Similarly, a positive-facing {@link CutAngle} with
+ * an azimuth of {@code 0pi} has a plus side of {@code [0, 2pi)} (the full space) and
+ * a minus side that is completely empty (since no points exist in our domain that are
+ * less than zero). These rules can result in somewhat non-intuitive behavior at times.
+ * For example, splitting a non-empty region with a hyperplane at {@code 0pi} is
+ * essentially a no-op, since the region will either lie entirely on the plus or minus
+ * side of the hyperplane (depending on the hyperplane's orientation) regardless of the actual
+ * content of the region. In these situations, a copy of the tree is returned on the
+ * appropriate side of the split.</p>
+ *
+ * @see CutAngle
+ * @see #splitDiameter(CutAngle)
+ */
+ @Override
+ public Split<RegionBSPTree1S> split(final Hyperplane<Point1S> splitter) {
+ // Handle the special case where the cut is on the azimuth equivalent to zero;
+ // In this case, it is not possible for any points to lie between it and zero.
+ if (!isEmpty() && splitter.classify(Point1S.ZERO) == HyperplaneLocation.ON) {
+ CutAngle cut = (CutAngle) splitter;
+ if (cut.isPositiveFacing()) {
+ return new Split<>(null, copy());
+ } else {
+ return new Split<>(copy(), null);
+ }
+ }
+
+ return split(splitter, RegionBSPTree1S.empty(), RegionBSPTree1S.empty());
+ }
+
+ /** Split the instance along a circle diameter.The diameter is defined by the given
+ * split point and its reversed antipodal point.
+ * @param splitter split point defining one side of the split diameter
+ * @return result of the split operation
+ */
+ public Split<RegionBSPTree1S> splitDiameter(final CutAngle splitter) {
+
+ final CutAngle opposite = CutAngle.fromPointAndDirection(
+ splitter.getPoint().antipodal(),
+ !splitter.isPositiveFacing(),
+ splitter.getPrecision());
+
+ final double plusPoleOffset = splitter.isPositiveFacing() ?
+ +Geometry.HALF_PI :
+ -Geometry.HALF_PI;
+ final Point1S plusPole = Point1S.of(splitter.getAzimuth() + plusPoleOffset);
+
+ final boolean zeroOnPlusSide = splitter.getPrecision()
+ .lte(plusPole.distance(Point1S.ZERO), Geometry.HALF_PI);
+
+ Split<RegionBSPTree1S> firstSplit = split(splitter);
+ Split<RegionBSPTree1S> secondSplit = split(opposite);
+
+ RegionBSPTree1S minus = RegionBSPTree1S.empty();
+ RegionBSPTree1S plus = RegionBSPTree1S.empty();
+
+ if (zeroOnPlusSide) {
+ // zero wrap-around needs to be handled on the plus side of the split
+ safeUnion(plus, firstSplit.getPlus());
+ safeUnion(plus, secondSplit.getPlus());
+
+ minus = firstSplit.getMinus();
+ if (minus != null) {
+ minus = minus.split(opposite).getMinus();
+ }
+ } else {
+ // zero wrap-around needs to be handled on the minus side of the split
+ safeUnion(minus, firstSplit.getMinus());
+ safeUnion(minus, secondSplit.getMinus());
+
+ plus = firstSplit.getPlus();
+ if (plus != null) {
+ plus = plus.split(opposite).getPlus();
+ }
+ }
+
+ return new Split<>(
+ (minus != null && !minus.isEmpty()) ? minus : null,
+ (plus != null && !plus.isEmpty()) ? plus : null);
+ }
+
+
+ /** Convert the region represented by this tree into a list of separate
+ * {@link AngularInterval}s, arranged in order of ascending min value.
+ * @return list of {@link AngularInterval}s representing this region in order of
+ * ascending min value
+ */
+ public List<AngularInterval> toIntervals() {
+ if (isFull()) {
+ return Collections.singletonList(AngularInterval.full());
+ }
+
+ final List<BoundaryPair> insideBoundaryPairs = new ArrayList<>();
+ for (RegionNode1S node : this) {
+ if (node.isInside()) {
+ insideBoundaryPairs.add(getNodeBoundaryPair(node));
+ }
+ }
+
+ insideBoundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR);
+
+ int boundaryPairCount = insideBoundaryPairs.size();
+
+ // Find the index of the first boundary pair that is not connected to pair before it.
+ // This will be our start point for merging intervals together.
+ int startOffset = 0;
+ if (boundaryPairCount > 1) {
+ BoundaryPair current = null;
+ BoundaryPair previous = insideBoundaryPairs.get(boundaryPairCount - 1);
+
+ for (int i = 0; i < boundaryPairCount; ++i, previous = current) {
+ current = insideBoundaryPairs.get(i);
+
+ if (!Objects.equals(current.getMin(), previous.getMax())) {
+ startOffset = i;
+ break;
+ }
+ }
+ }
+
+ // Go through the pairs starting at the start offset and create intervals
+ // for each set of adjacent pairs.
+ final List<AngularInterval> intervals = new ArrayList<>();
+
+ BoundaryPair start = null;
+ BoundaryPair end = null;
+ BoundaryPair current = null;
+
+ for (int i = 0; i < boundaryPairCount; ++i) {
+ current = insideBoundaryPairs.get((i + startOffset) % boundaryPairCount);
+
+ if (start == null) {
+ start = current;
+ end = current;
+ } else if (Objects.equals(end.getMax(), current.getMin())) {
+ // these intervals should be merged
+ end = current;
+ } else {
+ // these intervals should be separate
+ intervals.add(createInterval(start, end));
+
+ // queue up the next pair
+ start = current;
+ end = current;
+ }
+ }
+
+ if (start != null && end != null) {
+ intervals.add(createInterval(start, end));
+ }
+
+ return intervals;
+ }
+
+ /** Create an interval instance from the min boundary from the start boundary pair and
+ * the max boundary from the end boundary pair. The hyperplane directions are adjusted
+ * as needed.
+ * @param start starting boundary pair
+ * @param end ending boundary pair
+ * @return an interval created from the min boundary of the given start pair and the
+ * max boundary from the given end pair
+ */
+ private AngularInterval createInterval(final BoundaryPair start, final BoundaryPair end) {
+ CutAngle min = start.getMin();
+ CutAngle max = end.getMax();
+
+ DoublePrecisionContext precision = (min != null) ? min.getPrecision() : max.getPrecision();
+
+ // flip the hyperplanes if needed since there's no
+ // guarantee that the inside will be on the minus side
+ // of the hyperplane (for example, if the region is complemented)
+
+ if (min != null) {
+ if (min.isPositiveFacing()) {
+ min = min.reverse();
+ }
+ } else {
+ min = CutAngle.createNegativeFacing(Geometry.ZERO_PI, precision);
+ }
+
+ if (max != null) {
+ if (!max.isPositiveFacing()) {
+ max = max.reverse();
+ }
+ } else {
+ max = CutAngle.createPositiveFacing(Geometry.TWO_PI, precision);
+ }
+
+ return AngularInterval.of(min, max);
+ }
+
+ /** Return the min/max boundary pair for the convex region represented by the given node.
+ * @param node the node to compute the interval for
+ * @return the min/max boundary pair for the convex region represented by the given node
+ */
+ private BoundaryPair getNodeBoundaryPair(final RegionNode1S node) {
+ CutAngle min = null;
+ CutAngle max = null;
+
+ CutAngle pt;
+ RegionNode1S child = node;
+ RegionNode1S parent;
+
+ while ((min == null || max == null) && (parent = child.getParent()) != null) {
+ pt = (CutAngle) parent.getCutHyperplane();
+
+ if ((pt.isPositiveFacing() && child.isMinus()) ||
+ (!pt.isPositiveFacing() && child.isPlus())) {
+
+ if (max == null) {
+ max = pt;
+ }
+ } else if (min == null) {
+ min = pt;
+ }
+
+ child = parent;
+ }
+
+ return new BoundaryPair(min, max);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionSizeProperties<Point1S> computeRegionSizeProperties() {
+ if (isFull()) {
+ return new RegionSizeProperties<>(Geometry.TWO_PI, null);
+ }
+
+ double size = 0;
+ double scaledBarycenterSum = 0;
+
+ double intervalSize;
+
+ for (AngularInterval interval : toIntervals()) {
+ intervalSize = interval.getSize();
+
+ size += intervalSize;
+ scaledBarycenterSum += intervalSize * interval.getBarycenter().getNormalizedAzimuth();
+ }
+
+ final Point1S barycenter = size > 0 ?
+ Point1S.of(scaledBarycenterSum / size) :
+ null;
+
+ return new RegionSizeProperties<>(size, barycenter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode1S createNode() {
+ return new RegionNode1S(this);
+ }
+
+ /** Return a new, empty BSP tree.
+ * @return a new, empty BSP tree.
+ */
+ public static RegionBSPTree1S empty() {
+ return new RegionBSPTree1S(false);
+ }
+
+ /** Return a new, full BSP tree. The returned tree represents the
+ * full space.
+ * @return a new, full BSP tree.
+ */
+ public static RegionBSPTree1S full() {
+ return new RegionBSPTree1S(true);
+ }
+
+ /** Return a new BSP tree representing the same region as the given angular interval.
+ * @param interval the input interval
+ * @return a new BSP tree representing the same region as the given angular interval
+ */
+ public static RegionBSPTree1S fromInterval(final AngularInterval interval) {
+ final CutAngle minBoundary = interval.getMinBoundary();
+ final CutAngle maxBoundary = interval.getMaxBoundary();
+
+ final RegionBSPTree1S tree = full();
+
+ if (minBoundary != null) {
+ tree.insert(minBoundary.span());
+ }
+
+ if (maxBoundary != null) {
+ tree.insert(maxBoundary.span());
+ }
+
+ return tree;
+ }
+
+ /** Perform a union operation with {@code target} and {@code input}, storing the result
+ * in {@code target}; does nothing if {@code input} is null.
+ * @param target target tree
+ * @param input input tree
+ */
+ private static void safeUnion(final RegionBSPTree1S target, final RegionBSPTree1S input) {
+ if (input != null) {
+ target.union(input);
+ }
+ }
+
+ /** BSP tree node for one dimensional spherical space.
+ */
+ public static final class RegionNode1S extends AbstractRegionBSPTree.AbstractRegionNode<Point1S, RegionNode1S> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190922L;
+
+ /** Simple constructor.
+ * @param tree the owning tree instance
+ */
+ protected RegionNode1S(final AbstractBSPTree<Point1S, RegionNode1S> tree) {
+ super(tree);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode1S getSelf() {
+ return this;
+ }
+ }
+
+ /** Internal class containing pairs of interval boundaries.
+ */
+ private static final class BoundaryPair {
+
+ /** The min boundary. */
+ private final CutAngle min;
+
+ /** The max boundary. */
+ private final CutAngle max;
+
+ /** Simple constructor.
+ * @param min min boundary hyperplane
+ * @param max max boundary hyperplane
+ */
+ BoundaryPair(final CutAngle min, final CutAngle max) {
+ this.min = min;
+ this.max = max;
+ }
+
+ /** Get the minimum boundary hyperplane.
+ * @return the minimum boundary hyperplane.
+ */
+ public CutAngle getMin() {
+ return min;
+ }
+
+ /** Get the maximum boundary hyperplane.
+ * @return the maximum boundary hyperplane.
+ */
+ public CutAngle getMax() {
+ return max;
+ }
+
+ /** Get the minumum value of the interval or zero if no minimum value exists.
+ * @return the minumum value of the interval or zero
+ * if no minimum value exists.
+ */
+ public double getMinValue() {
+ return (min != null) ? min.getNormalizedAzimuth() : 0;
+ }
+ }
+
+ /** Class used to project points onto the region boundary.
+ */
+ private static final class BoundaryProjector1S extends BoundaryProjector<Point1S, RegionNode1S> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190926L;
+
+ /** Simple constructor.
+ * @param point the point to project onto the region's boundary
+ */
+ BoundaryProjector1S(final Point1S point) {
+ super(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean isPossibleClosestCut(final SubHyperplane<Point1S> cut, final Point1S target,
+ final double minDist) {
+ // since the space wraps around, consider any cut as possibly being the closest
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Point1S disambiguateClosestPoint(final Point1S target, final Point1S a, final Point1S b) {
+ // prefer the point with the smaller normalize azimuth value
+ return a.getNormalizedAzimuth() < b.getNormalizedAzimuth() ? a : b;
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/S1Point.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/S1Point.java
deleted file mode 100644
index c13083d..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/S1Point.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * 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.oned;
-
-import java.io.Serializable;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
-import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
-
-/** This class represents a point on the 1-sphere.
- * <p>Instances of this class are guaranteed to be immutable.</p>
- */
-public final class S1Point implements Point<S1Point>, Serializable {
-
- // CHECKSTYLE: stop ConstantName
- /** A point with all coordinates set to NaN. */
- public static final S1Point NaN = new S1Point(Double.NaN);
- // CHECKSTYLE: resume ConstantName
-
- /** Serializable UID. */
- private static final long serialVersionUID = 20180710L;
-
- /** Azimuthal angle in radians. */
- private final double azimuth;
-
- /** Corresponding 2D normalized vector. */
- private final Vector2D vector;
-
- /** Build a point from its internal components.
- * @param azimuth azimuthal angle
- */
- private S1Point(final double azimuth) {
- this.azimuth = PolarCoordinates.normalizeAzimuth(azimuth);
- this.vector = Double.isFinite(azimuth) ? PolarCoordinates.toCartesian(1.0, azimuth) : Vector2D.NaN;
- }
-
- /** Get the azimuthal angle in radians.
- * @return azimuthal angle
- * @see S1Point#of(double)
- */
- public double getAzimuth() {
- return azimuth;
- }
-
- /** Get the corresponding normalized vector in the 2D Euclidean space.
- * @return normalized vector
- */
- public Vector2D getVector() {
- return vector;
- }
-
- /** {@inheritDoc} */
- @Override
- public int getDimension() {
- return 1;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isNaN() {
- return Double.isNaN(azimuth);
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isInfinite() {
- return !isNaN() && Double.isInfinite(azimuth);
- }
-
- /** {@inheritDoc} */
- @Override
- public double distance(final S1Point point) {
- return distance(this, point);
- }
-
- /** Compute the distance (angular separation) between two points.
- * @param p1 first vector
- * @param p2 second vector
- * @return the angular separation between p1 and p2
- */
- public static double distance(S1Point p1, S1Point p2) {
- return p1.vector.angle(p2.vector);
- }
-
- /**
- * Test for the equality of two points on the 2-sphere.
- * <p>
- * If all coordinates of two points are exactly the same, and none are
- * <code>Double.NaN</code>, the two points are considered to be equal.
- * </p>
- * <p>
- * <code>NaN</code> coordinates are considered to affect globally the vector
- * and be equals to each other - i.e, if either (or all) coordinates of the
- * 2D vector are equal to <code>Double.NaN</code>, the 2D vector is equal to
- * {@link #NaN}.
- * </p>
- *
- * @param other Object to test for equality to this
- * @return true if two points on the 2-sphere objects are equal, false if
- * object is null, not an instance of S2Point, or
- * not equal to this S2Point instance
- *
- */
- @Override
- public boolean equals(Object other) {
- if (this == other) {
- return true;
- }
-
- if (other instanceof S1Point) {
- final S1Point rhs = (S1Point) other;
- if (rhs.isNaN()) {
- return this.isNaN();
- }
-
- return azimuth == rhs.azimuth;
- }
-
- return false;
- }
-
- /**
- * Get a hashCode for the 2D vector.
- * <p>
- * All NaN values have the same hash code.</p>
- *
- * @return a hash code value for this object
- */
- @Override
- public int hashCode() {
- if (isNaN()) {
- return 542;
- }
- return 1759 * Double.hashCode(azimuth);
- }
-
- /** {@inheritDoc} */
- @Override
- public String toString() {
- return SimpleTupleFormat.getDefault().format(getAzimuth());
- }
-
- /** Creates a new point instance from the given azimuthal coordinate value.
- * @param azimuth azimuthal angle in radians
- * @return point instance with the given azimuth coordinate value
- * @see #getAzimuth()
- */
- public static S1Point of(double azimuth) {
- return new S1Point(azimuth);
- }
-
- /** Parses the given string and returns a new point instance. The expected string
- * format is the same as that returned by {@link #toString()}.
- * @param str the string to parse
- * @return point instance represented by the string
- * @throws IllegalArgumentException if the given string has an invalid format
- */
- public static S1Point parse(String str) {
- return SimpleTupleFormat.getDefault().parse(str, S1Point::new);
- }
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/SubLimitAngle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/SubLimitAngle.java
deleted file mode 100644
index b51a36f..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/SubLimitAngle.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.oned;
-
-import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Region;
-
-/** This class represents sub-hyperplane for {@link LimitAngle}.
- * <p>Instances of this class are guaranteed to be immutable.</p>
- */
-public class SubLimitAngle extends AbstractSubHyperplane<S1Point, S1Point> {
-
- /** Simple constructor.
- * @param hyperplane underlying hyperplane
- * @param remainingRegion remaining region of the hyperplane
- */
- public SubLimitAngle(final Hyperplane<S1Point> hyperplane,
- final Region<S1Point> remainingRegion) {
- super(hyperplane, remainingRegion);
- }
-
- /** {@inheritDoc} */
- @Override
- public double getSize() {
- return 0;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isEmpty() {
- return false;
- }
-
- /** {@inheritDoc} */
- @Override
- protected AbstractSubHyperplane<S1Point, S1Point> buildNew(final Hyperplane<S1Point> hyperplane,
- final Region<S1Point> remainingRegion) {
- return new SubLimitAngle(hyperplane, remainingRegion);
- }
-
- /** {@inheritDoc} */
- @Override
- public SplitSubHyperplane<S1Point> split(final Hyperplane<S1Point> hyperplane) {
- final double global = hyperplane.getOffset(((LimitAngle) getHyperplane()).getLocation());
- return (global < -1.0e-10) ?
- new SplitSubHyperplane<>(null, this) :
- new SplitSubHyperplane<>(this, null);
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Transform1S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Transform1S.java
new file mode 100644
index 0000000..dd9ac00
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Transform1S.java
@@ -0,0 +1,236 @@
+/*
+ * 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.oned;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Implementation of the {@link Transform} interface for spherical 1D points.
+ *
+ * <p>Similar to the Euclidean 1D
+ * {@link org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D AffineTransformMatrix1D},
+ * this class performs transformations using an internal 1D affine transformation matrix. In the
+ * Euclidean case, the matrix contains a scale factor and a translation. Here, the matrix contains
+ * a scale/negation factor that takes the values -1 or +1, and a rotation value. This restriction on
+ * the allowed values in the matrix is required in order to fulfill the geometric requirements
+ * of the {@link Transform} interface. For example, if arbitrary scaling is allowed, the point {@code 0.5pi}
+ * could be scaled by 4 to {@code 2pi}, which is equivalent to {@code 0pi}. However, if the inverse scaling
+ * of {@code 1/4} is applied to {@code 0pi}, the result is {@code 0pi} and not {@code 0.5pi}. This breaks
+ * the {@link Transform} requirement that transforms be inversible.
+ * </p>
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Transform1S implements Transform<Point1S>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191001L;
+
+ /** Static instance representing the identity transform. */
+ private static final Transform1S IDENTITY = new Transform1S(1, 0);
+
+ /** Static instance that negates azimuth values. */
+ private static final Transform1S NEGATION = new Transform1S(-1, 0);
+
+ /** Value to scale the point azimuth by. This will only be +1/-1. */
+ private final double scale;
+
+ /** Value to rotate the point azimuth by. */
+ private final double rotate;
+
+ /** Construct a new instance from its transform components.
+ * @param scale scale value for the transform; must only be +1 or -1
+ * @param rotate rotation value
+ */
+ private Transform1S(final double scale, final double rotate) {
+ this.scale = scale;
+ this.rotate = rotate;
+ }
+
+ /** Return true if the transform negates the azimuth values of transformed
+ * points, regardless of any rotation applied subsequently.
+ * @return true if the transform negates the azimuth values of transformed
+ * points
+ * @see #preservesOrientation()
+ */
+ public boolean isNegation() {
+ return scale <= 0;
+ }
+
+ /** Get the rotation value applied by this instance, in radians.
+ * @return the rotation value applied by this instance, in radians.
+ */
+ public double getRotation() {
+ return rotate;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point1S apply(final Point1S pt) {
+ final double az = pt.getAzimuth();
+ final double resultAz = (az * scale) + rotate;
+
+ return Point1S.of(resultAz);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean preservesOrientation() {
+ return !isNegation();
+ }
+
+ /** Return a new transform created by pre-multiplying this instance by a transform
+ * producing a rotation with the given angle.
+ * @param angle angle to rotate, in radians
+ * @return a new transform created by pre-multiplying this instance by a transform
+ * producing a rotation with the given angle
+ * @see #createRotation(double)
+ */
+ public Transform1S rotate(final double angle) {
+ return premultiply(createRotation(angle));
+ }
+
+ /** Return a new transform created by pre-multiplying this instance by a transform
+ * that negates azimuth values.
+ * @return a new transform created by pre-multiplying this instance by a transform
+ * that negates azimuth values
+ */
+ public Transform1S negate() {
+ return premultiply(createNegation());
+ }
+
+ /** Multiply the underlying matrix of this instance by that of the argument, eg,
+ * {@code other * this}. The returned transform performs the equivalent of
+ * {@code other} followed by {@code this}.
+ * @param other transform to multiply with
+ * @return a new transform computed by multiplying the matrix of this
+ * instance by that of the argument
+ */
+ public Transform1S multiply(final Transform1S other) {
+ return multiply(this, other);
+ }
+
+ /** Multiply the underlying matrix of the argument by that of this instance, eg,
+ * {@code this * other}. The returned transform performs the equivalent of {@code this}
+ * followed by {@code other}.
+ * @param other transform to multiply with
+ * @return a new transform computed by multiplying the matrix of the
+ * argument by that of this instance
+ */
+ public Transform1S premultiply(final Transform1S other) {
+ return multiply(other, this);
+ }
+
+ /** Return a transform that is the inverse of the current instance. The returned transform
+ * will undo changes applied by this instance.
+ * @return a transform that is the inverse of the current instance
+ */
+ public Transform1S inverse() {
+ final double invScale = 1.0 / scale;
+
+ final double resultScale = invScale;
+ final double resultRotate = -(rotate * invScale);
+
+ return new Transform1S(resultScale, resultRotate);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = (result * prime) + Double.hashCode(scale);
+ result = (result * prime) + Double.hashCode(rotate);
+
+ return result;
+ }
+
+ /**
+ * Return true if the given object is an instance of {@link Transform1S}
+ * and all transform element values are exactly equal.
+ * @param obj object to test for equality with the current instance
+ * @return true if all transform elements are exactly equal; otherwise false
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Transform1S)) {
+ return false;
+ }
+ final Transform1S other = (Transform1S) obj;
+
+ return Double.compare(scale, other.scale) == 0 &&
+ Double.compare(rotate, other.rotate) == 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+
+ sb.append(this.getClass().getSimpleName())
+ .append("[negate= ")
+ .append(isNegation())
+ .append(", rotate= ")
+ .append(getRotation())
+ .append("]");
+
+ return sb.toString();
+ }
+
+ /** Return a transform instance representing the identity transform.
+ * @return a transform instance representing the identity transform
+ */
+ public static Transform1S identity() {
+ return IDENTITY;
+ }
+
+ /** Return a transform instance that negates azimuth values.
+ * @return a transform instance that negates azimuth values.
+ */
+ public static Transform1S createNegation() {
+ return NEGATION;
+ }
+
+ /** Return a transform instance that performs a rotation with the given
+ * angle.
+ * @param angle angle of the rotation, in radians
+ * @return a transform instance that performs a rotation with the given
+ * angle
+ */
+ public static Transform1S createRotation(final double angle) {
+ return new Transform1S(1, angle);
+ }
+
+ /** Multiply two transforms together as matrices.
+ * @param a first transform
+ * @param b second transform
+ * @return the transform computed as {@code a x b}
+ */
+ private static Transform1S multiply(final Transform1S a, final Transform1S b) {
+
+ // calculate the matrix elements
+ final double resultScale = a.scale * b.scale;
+ final double resultRotate = (a.scale * b.rotate) + a.rotate;
+
+ return new Transform1S(resultScale, resultRotate);
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/AbstractGreatArcConnector.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/AbstractGreatArcConnector.java
new file mode 100644
index 0000000..c6fd050
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/AbstractGreatArcConnector.java
@@ -0,0 +1,303 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.geometry.euclidean.internal.AbstractPathConnector;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+
+/** Abstract class for joining collections of great arcs into connected
+ * paths. This class is not thread-safe.
+ */
+public abstract class AbstractGreatArcConnector
+ extends AbstractPathConnector<AbstractGreatArcConnector.ConnectableGreatArc> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191107L;
+
+ /** Add an arc to the connector, leaving it unconnected until a later call to
+ * to {@link #connect(Iterable)} or {@link #connectAll()}.
+ * @param arc arc to add
+ * @see #connect(Iterable)
+ * @see #connectAll()
+ */
+ public void add(final GreatArc arc) {
+ addPathElement(new ConnectableGreatArc(arc));
+ }
+
+ /** Add a collection of arcs to the connector, leaving them unconnected
+ * until a later call to {@link #connect(Iterable)} or
+ * {@link #connectAll()}.
+ * @param arcs arcs to add
+ * @see #connect(Iterable)
+ * @see #connectAll()
+ * @see #add(GreatArc)
+ */
+ public void add(final Iterable<GreatArc> arcs) {
+ for (GreatArc segment : arcs) {
+ add(segment);
+ }
+ }
+
+ /** Add a collection of arcs to the connector and attempt to connect each new
+ * arc with existing ones. Connections made at this time will not be
+ * overwritten by subsequent calls to this or other connection methods,
+ * (eg, {@link #connectAll()}).
+ *
+ * <p>The connector is not reset by this call. Additional arc can still be added
+ * to the current set of paths.</p>
+ * @param arcs arcs to connect
+ * @see #connectAll()
+ */
+ public void connect(final Iterable<GreatArc> arcs) {
+ List<ConnectableGreatArc> newEntries = new ArrayList<>();
+
+ for (GreatArc segment : arcs) {
+ newEntries.add(new ConnectableGreatArc(segment));
+ }
+
+ connectPathElements(newEntries);
+ }
+
+ /** Add the given arcs to this instance and connect all current
+ * arc into paths. This call is equivalent to
+ * <pre>
+ * connector.add(arcs);
+ * List<GreatArcPath> result = connector.connectAll();
+ * </pre>
+ *
+ * <p>The connector is reset after this call. Further calls to
+ * add or connect arcs will result in new paths being generated.</p>
+ * @param arcs arcs to add
+ * @return the connected arc paths
+ * @see #add(Iterable)
+ * @see #connectAll()
+ */
+ public List<GreatArcPath> connectAll(final Iterable<GreatArc> arcs) {
+ add(arcs);
+ return connectAll();
+ }
+
+ /** Connect all current arcs into connected paths, returning the result as a
+ * list of arc paths.
+ *
+ * <p>The connector is reset after this call. Further calls to
+ * add or connect arcs will result in new paths being generated.</p>
+ * @return the connected line segments paths
+ */
+ public List<GreatArcPath> connectAll() {
+ final List<ConnectableGreatArc> roots = computePathRoots();
+ final List<GreatArcPath> paths = new ArrayList<>(roots.size());
+
+ for (ConnectableGreatArc root : roots) {
+ paths.add(toPath(root));
+ }
+
+ return paths;
+ }
+
+ /** Convert the linked list of path elements starting at the argument
+ * into a {@link GreatArcPath}.
+ * @param root root of a connected path linked list
+ * @return a great arc path representing the linked list path
+ */
+ private GreatArcPath toPath(final ConnectableGreatArc root) {
+ final GreatArcPath.Builder builder = GreatArcPath.builder(null);
+
+ builder.append(root.getArc());
+
+ ConnectableGreatArc current = root.getNext();
+
+ while (current != null && current != root) {
+ builder.append(current.getArc());
+ current = current.getNext();
+ }
+
+ return builder.build();
+ }
+
+ /** Internal class for connecting {@link GreatArc}s into {@link GreatArcPath}s.
+ */
+ protected static class ConnectableGreatArc extends AbstractPathConnector.ConnectableElement<ConnectableGreatArc> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191107L;
+
+ /** Segment start point. This will be used to connect to other path elements. */
+ private final Point2S start;
+
+ /** Great arc for this instance. */
+ private final GreatArc arc;
+
+ /** Create a new instance with the given start point. This constructor is
+ * intended only for performing searches for other path elements.
+ * @param start start point
+ */
+ public ConnectableGreatArc(final Point2S start) {
+ this(start, null);
+ }
+
+ /** Create a new instance from the given arc.
+ * @param arc arc for the instance
+ */
+ public ConnectableGreatArc(final GreatArc arc) {
+ this(arc.getStartPoint(), arc);
+ }
+
+ /** Create a new instance with the given start point and arc.
+ * @param start start point
+ * @param arc arc for the instance
+ */
+ private ConnectableGreatArc(final Point2S start, final GreatArc arc) {
+ this.start = start;
+ this.arc = arc;
+ }
+
+ /** Get the arc for the instance.
+ * @return the arc for the instance
+ */
+ public GreatArc getArc() {
+ return arc;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasStart() {
+ return start != null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasEnd() {
+ return arc.getEndPoint() != null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean endPointsEq(final ConnectableGreatArc other) {
+ if (hasEnd() && other.hasEnd()) {
+ return arc.getEndPoint()
+ .eq(other.arc.getEndPoint(), arc.getPrecision());
+ }
+
+ return false;
+ }
+
+ /** Return true if this instance has a size equivalent to zero.
+ * @return true if this instance has a size equivalent to zero.
+ */
+ public boolean hasZeroSize() {
+ return arc != null && arc.getPrecision().eqZero(arc.getSize());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean canConnectTo(final ConnectableGreatArc next) {
+ final Point2S end = arc.getEndPoint();
+ final Point2S nextStart = next.start;
+
+ return end != null && nextStart != null &&
+ end.eq(nextStart, arc.getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getRelativeAngle(final ConnectableGreatArc other) {
+ return arc.getCircle().angle(other.getArc().getCircle());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConnectableGreatArc getConnectionSearchKey() {
+ return new ConnectableGreatArc(arc.getEndPoint());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean shouldContinueConnectionSearch(final ConnectableGreatArc candidate,
+ final boolean ascending) {
+
+ if (candidate.hasStart()) {
+ final double candidatePolar = candidate.getArc().getStartPoint().getPolar();
+ final double thisPolar = arc.getEndPoint().getPolar();
+ final int cmp = arc.getPrecision().compare(candidatePolar, thisPolar);
+
+ return ascending ? cmp <= 0 : cmp >= 0;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int compareTo(final ConnectableGreatArc other) {
+ int cmp = Point2S.POLAR_AZIMUTH_ASCENDING_ORDER.compare(start, other.start);
+
+ if (cmp == 0) {
+ // sort entries without arcs before ones with arcs
+ final boolean thisHasArc = arc != null;
+ final boolean otherHasArc = other.arc != null;
+
+ cmp = Boolean.compare(thisHasArc, otherHasArc);
+
+ if (cmp == 0 && thisHasArc) {
+ // place point-like segments before ones with non-zero length
+ cmp = Boolean.compare(this.hasZeroSize(), other.hasZeroSize());
+
+ if (cmp == 0) {
+ // sort by circle pole
+ cmp = Vector3D.COORDINATE_ASCENDING_ORDER.compare(
+ arc.getCircle().getPole(),
+ other.arc.getCircle().getPole());
+ }
+ }
+ }
+
+ return cmp;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(start, arc);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !this.getClass().equals(obj.getClass())) {
+ return false;
+ }
+
+ final ConnectableGreatArc other = (ConnectableGreatArc) obj;
+ return Objects.equals(this.start, other.start) &&
+ Objects.equals(this.arc, other.arc);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected ConnectableGreatArc getSelf() {
+ return this;
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/AbstractSubGreatCircle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/AbstractSubGreatCircle.java
new file mode 100644
index 0000000..05b02b4
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/AbstractSubGreatCircle.java
@@ -0,0 +1,69 @@
+/*
+ * 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 org.apache.commons.geometry.core.partitioning.AbstractEmbeddingSubHyperplane;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.apache.commons.geometry.spherical.twod.SubGreatCircle.SubGreatCircleBuilder;
+
+/** Abstract base class for great circle subhyperplane implementations.
+ */
+abstract class AbstractSubGreatCircle
+ extends AbstractEmbeddingSubHyperplane<Point2S, Point1S, GreatCircle> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191005L;
+
+ /** The great circle defining this instance. */
+ private final GreatCircle circle;
+
+ /** Simple constructor.
+ * @param circle great circle defining this instance
+ */
+ AbstractSubGreatCircle(final GreatCircle circle) {
+ this.circle = circle;
+ }
+
+ /** Get the great circle defining this instance.
+ * @return the great circle defining this instance
+ * @see #getHyperplane()
+ */
+ public GreatCircle getCircle() {
+ return circle;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public GreatCircle getHyperplane() {
+ return circle;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubGreatCircleBuilder builder() {
+ return new SubGreatCircleBuilder(circle);
+ }
+
+ /** Return the object used to perform floating point comparisons, which is the
+ * same object used by the underlying {@link GreatCircle}.
+ * @return precision object used to perform floating point comparisons.
+ */
+ public DoublePrecisionContext getPrecision() {
+ return circle.getPrecision();
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Circle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Circle.java
deleted file mode 100644
index dd3dda7..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Circle.java
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * 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 org.apache.commons.geometry.core.partitioning.Embedding;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.partitioning.Transform;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.spherical.oned.Arc;
-import org.apache.commons.geometry.spherical.oned.ArcsSet;
-import org.apache.commons.geometry.spherical.oned.S1Point;
-
-/** This class represents an oriented great circle on the 2-sphere.
-
- * <p>An oriented circle can be defined by a center point. The circle
- * is the the set of points that are in the normal plan the center.</p>
-
- * <p>Since it is oriented the two spherical caps at its two sides are
- * unambiguously identified as a left cap and a right cap. This can be
- * used to identify the interior and the exterior in a simple way by
- * local properties only when part of a line is used to define part of
- * a spherical polygon boundary.</p>
- */
-public class Circle implements Hyperplane<S2Point>, Embedding<S2Point, S1Point> {
-
- /** Pole or circle center. */
- private Vector3D pole;
-
- /** First axis in the equator plane, origin of the phase angles. */
- private Vector3D x;
-
- /** Second axis in the equator plane, in quadrature with respect to x. */
- private Vector3D y;
-
- /** Precision context used to determine floating point equality. */
- private final DoublePrecisionContext precision;
-
- /** Build a great circle from its pole.
- * <p>The circle is oriented in the trigonometric direction around pole.</p>
- * @param pole circle pole
- * @param precision precision context used to compare floating point values
- */
- public Circle(final Vector3D pole, final DoublePrecisionContext precision) {
- reset(pole);
- this.precision = precision;
- }
-
- /** Build a great circle from two non-aligned points.
- * <p>The circle is oriented from first to second point using the path smaller than \( \pi \).</p>
- * @param first first point contained in the great circle
- * @param second second point contained in the great circle
- * @param precision precision context used to compare floating point values
- */
- public Circle(final S2Point first, final S2Point second, final DoublePrecisionContext precision) {
- reset(first.getVector().cross(second.getVector()));
- this.precision = precision;
- }
-
- /** Build a circle from its internal components.
- * <p>The circle is oriented in the trigonometric direction around center.</p>
- * @param pole circle pole
- * @param x first axis in the equator plane
- * @param y second axis in the equator plane
- * @param precision precision context used to compare floating point values
- */
- private Circle(final Vector3D pole, final Vector3D x, final Vector3D y,
- final DoublePrecisionContext precision) {
- this.pole = pole;
- this.x = x;
- this.y = y;
- this.precision = precision;
- }
-
- /** Copy constructor.
- * <p>The created instance is completely independent from the
- * original instance, it is a deep copy.</p>
- * @param circle circle to copy
- */
- public Circle(final Circle circle) {
- this(circle.pole, circle.x, circle.y, circle.precision);
- }
-
- /** {@inheritDoc} */
- @Override
- public Circle copySelf() {
- return new Circle(this);
- }
-
- /** Reset the instance as if built from a pole.
- * <p>The circle is oriented in the trigonometric direction around pole.</p>
- * @param newPole circle pole
- */
- public void reset(final Vector3D newPole) {
- this.pole = newPole.normalize();
- this.x = newPole.orthogonal();
- this.y = newPole.cross(x).normalize();
- }
-
- /** Revert the instance.
- */
- public void revertSelf() {
- // x remains the same
- y = y.negate();
- pole = pole.negate();
- }
-
- /** Get the reverse of the instance.
- * <p>Get a circle with reversed orientation with respect to the
- * instance. A new object is built, the instance is untouched.</p>
- * @return a new circle, with orientation opposite to the instance orientation
- */
- public Circle getReverse() {
- return new Circle(pole.negate(), x, y.negate(), precision);
- }
-
- /** {@inheritDoc} */
- @Override
- public S2Point project(S2Point point) {
- return toSpace(toSubSpace(point));
- }
-
- /** Get the object used to determine floating point equality for this region.
- * @return the floating point precision context for the instance
- */
- @Override
- public DoublePrecisionContext getPrecision() {
- return precision;
- }
-
- /** {@inheritDoc}
- * @see #getPhase(Vector3D)
- */
- @Override
- public S1Point toSubSpace(final S2Point point) {
- return S1Point.of(getPhase(point.getVector()));
- }
-
- /** Get the phase angle of a direction.
- * <p>
- * The direction may not belong to the circle as the
- * phase is computed for the meridian plane between the circle
- * pole and the direction.
- * </p>
- * @param direction direction for which phase is requested
- * @return phase angle of the direction around the circle
- * @see #toSubSpace(Point)
- */
- public double getPhase(final Vector3D direction) {
- return Math.PI + Math.atan2(-direction.dot(y), -direction.dot(x));
- }
-
- /** {@inheritDoc}
- * @see #getPointAt(double)
- */
- @Override
- public S2Point toSpace(final S1Point point) {
- return S2Point.ofVector(getPointAt(point.getAzimuth()));
- }
-
- /** Get a circle point from its phase around the circle.
- * @param alpha phase around the circle
- * @return circle point on the sphere
- * @see #toSpace(Point)
- * @see #getXAxis()
- * @see #getYAxis()
- */
- public Vector3D getPointAt(final double alpha) {
- return Vector3D.linearCombination(Math.cos(alpha), x, Math.sin(alpha), y);
- }
-
- /** Get the X axis of the circle.
- * <p>
- * This method returns the same value as {@link #getPointAt(double)
- * getPointAt(0.0)} but it does not do any computation and always
- * return the same instance.
- * </p>
- * @return an arbitrary x axis on the circle
- * @see #getPointAt(double)
- * @see #getYAxis()
- * @see #getPole()
- */
- public Vector3D getXAxis() {
- return x;
- }
-
- /** Get the Y axis of the circle.
- * <p>
- * This method returns the same value as {@link #getPointAt(double)
- * getPointAt(0.5 * Math.PI)} but it does not do any computation and always
- * return the same instance.
- * </p>
- * @return an arbitrary y axis point on the circle
- * @see #getPointAt(double)
- * @see #getXAxis()
- * @see #getPole()
- */
- public Vector3D getYAxis() {
- return y;
- }
-
- /** Get the pole of the circle.
- * <p>
- * As the circle is a great circle, the pole does <em>not</em>
- * belong to it.
- * </p>
- * @return pole of the circle
- * @see #getXAxis()
- * @see #getYAxis()
- */
- public Vector3D getPole() {
- return pole;
- }
-
- /** Get the arc of the instance that lies inside the other circle.
- * @param other other circle
- * @return arc of the instance that lies inside the other circle
- */
- public Arc getInsideArc(final Circle other) {
- final double alpha = getPhase(other.pole);
- final double halfPi = 0.5 * Math.PI;
- return new Arc(alpha - halfPi, alpha + halfPi, precision);
- }
-
- /** {@inheritDoc} */
- @Override
- public SubCircle wholeHyperplane() {
- return new SubCircle(this, new ArcsSet(precision));
- }
-
- /** Build a region covering the whole space.
- * @return a region containing the instance (really a {@link
- * SphericalPolygonsSet SphericalPolygonsSet} instance)
- */
- @Override
- public SphericalPolygonsSet wholeSpace() {
- return new SphericalPolygonsSet(precision);
- }
-
- /** {@inheritDoc}
- * @see #getOffset(Vector3D)
- */
- @Override
- public double getOffset(final S2Point point) {
- return getOffset(point.getVector());
- }
-
- /** Get the offset (oriented distance) of a direction.
- * <p>The offset is defined as the angular distance between the
- * circle center and the direction minus the circle radius. It
- * is therefore 0 on the circle, positive for directions outside of
- * the cone delimited by the circle, and negative inside the cone.</p>
- * @param direction direction to check
- * @return offset of the direction
- * @see #getOffset(Point)
- */
- public double getOffset(final Vector3D direction) {
- return pole.angle(direction) - 0.5 * Math.PI;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean sameOrientationAs(final Hyperplane<S2Point> other) {
- final Circle otherC = (Circle) other;
- return pole.dot(otherC.pole) >= 0.0;
- }
-
- /** Get a {@link org.apache.commons.geometry.core.partitioning.Transform
- * Transform} embedding a 3D rotation.
- * @param rotation rotation to use
- * @return a new transform that can be applied to either {@link
- * S2Point Point}, {@link Circle Line} or {@link
- * org.apache.commons.geometry.core.partitioning.SubHyperplane
- * SubHyperplane} instances
- */
- public static Transform<S2Point, S1Point> getTransform(final QuaternionRotation rotation) {
- return new CircleTransform(rotation);
- }
-
- /** Class embedding a 3D rotation. */
- private static class CircleTransform implements Transform<S2Point, S1Point> {
-
- /** Underlying rotation. */
- private final QuaternionRotation rotation;
-
- /** Build a transform from a {@code Rotation}.
- * @param rotation rotation to use
- */
- CircleTransform(final QuaternionRotation rotation) {
- this.rotation = rotation;
- }
-
- /** {@inheritDoc} */
- @Override
- public S2Point apply(final S2Point point) {
- return S2Point.ofVector(rotation.apply(point.getVector()));
- }
-
- /** {@inheritDoc} */
- @Override
- public Circle apply(final Hyperplane<S2Point> hyperplane) {
- final Circle circle = (Circle) hyperplane;
- return new Circle(rotation.apply(circle.pole),
- rotation.apply(circle.x),
- rotation.apply(circle.y),
- circle.precision);
- }
-
- /** {@inheritDoc} */
- @Override
- public SubHyperplane<S1Point> apply(final SubHyperplane<S1Point> sub,
- final Hyperplane<S2Point> original,
- final Hyperplane<S2Point> transformed) {
- // as the circle is rotated, the limit angles are rotated too
- return sub;
- }
-
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java
new file mode 100644
index 0000000..763244c
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java
@@ -0,0 +1,305 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+
+/** Class representing a convex area in 2D spherical space. The boundaries of this
+ * area, if any, are composed of convex great circle arcs.
+ */
+public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Point2S, GreatArc> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191021L;
+
+ /** Instance representing the full spherical area. */
+ private static final ConvexArea2S FULL = new ConvexArea2S(Collections.emptyList());
+
+ /** Constant containing the area of the full spherical space. */
+ private static final double FULL_SIZE = 4 * Geometry.PI;
+
+ /** Constant containing the area of half of the spherical space. */
+ private static final double HALF_SIZE = Geometry.TWO_PI;
+
+ /** Construct an instance from its boundaries. Callers are responsible for ensuring
+ * that the given path represents the boundary of a convex area. No validation is
+ * performed.
+ * @param boundaries the boundaries of the convex area
+ */
+ private ConvexArea2S(final List<GreatArc> boundaries) {
+ super(boundaries);
+ }
+
+ /** Get a path instance representing the boundary of the area. The path is oriented
+ * so that the minus sides of the arcs lie on the inside of the area.
+ * @return the boundary path of the area
+ */
+ public GreatArcPath getBoundaryPath() {
+ final List<GreatArcPath> paths = InteriorAngleGreatArcConnector.connectMinimized(getBoundaries());
+ if (paths.isEmpty()) {
+ return GreatArcPath.empty();
+ }
+
+ return paths.get(0);
+ }
+
+ /** Get an array of interior angles for the area. An empty array is returned if there
+ * are no boundary intersections (ie, it has only one boundary or no boundaries at all).
+ *
+ * <p>The order of the angles corresponds with the order of the boundaries returned
+ * by {@link #getBoundaries()}: if {@code i} is an index into the boundaries list,
+ * then {@code angles[i]} is the angle between boundaries {@code i} and {@code (i+1) % boundariesSize}.</p>
+ * @return an array of interior angles for the area
+ */
+ public double[] getInteriorAngles() {
+ final List<GreatArc> arcs = getBoundaryPath().getArcs();
+ final int numSides = arcs.size();
+
+ if (numSides < 2) {
+ return new double[0];
+ }
+
+ final double[] angles = new double[numSides];
+
+ GreatArc current;
+ GreatArc next;
+ for (int i = 0; i < numSides; ++i) {
+ current = arcs.get(i);
+ next = arcs.get((i + 1) % numSides);
+
+ angles[i] = Geometry.PI - current.getCircle()
+ .angle(next.getCircle(), current.getEndPoint());
+ }
+
+ return angles;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ final int numSides = getBoundaries().size();
+
+ if (numSides == 0) {
+ return FULL_SIZE;
+ } else if (numSides == 1) {
+ return HALF_SIZE;
+ } else {
+ // use the extended version of Girard's theorem
+ // https://en.wikipedia.org/wiki/Spherical_trigonometry#Girard's_theorem
+ final double[] angles = getInteriorAngles();
+ final double sum = Arrays.stream(angles).sum();
+
+ return sum - ((angles.length - 2) * Geometry.PI);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point2S getBarycenter() {
+ List<GreatArc> arcs = getBoundaries();
+ int numSides = arcs.size();
+
+ if (numSides == 0) {
+ // full space; no barycenter
+ return null;
+ } else if (numSides == 1) {
+ // hemisphere; barycenter is the pole of the hemisphere
+ return arcs.get(0).getCircle().getPolePoint();
+ } else {
+ // 2 or more sides; use an extension of the approach outlined here:
+ // https://archive.org/details/centroidinertiat00broc
+ // In short, the barycenter is the sum of the pole vectors of each side
+ // multiplied by their arc lengths.
+ Vector3D barycenter = Vector3D.ZERO;
+
+ for (GreatArc arc : getBoundaries()) {
+ barycenter = Vector3D.linearCombination(
+ 1, barycenter,
+ arc.getSize(), arc.getCircle().getPole());
+ }
+
+ return Point2S.from(barycenter);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<ConvexArea2S> split(final Hyperplane<Point2S> splitter) {
+ return splitInternal(splitter, this, GreatArc.class, ConvexArea2S::new);
+ }
+
+ /** Return a new instance transformed by the argument.
+ * @param transform transform to apply
+ * @return a new instance transformed by the argument
+ */
+ public ConvexArea2S transform(final Transform<Point2S> transform) {
+ return transformInternal(transform, this, GreatArc.class, ConvexArea2S::new);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public GreatArc trim(final ConvexSubHyperplane<Point2S> convexSubHyperplane) {
+ return (GreatArc) super.trim(convexSubHyperplane);
+ }
+
+ /** Return a BSP tree instance representing the same region as the current instance.
+ * @return a BSP tree instance representing the same region as the current instance
+ */
+ public RegionBSPTree2S toTree() {
+ return RegionBSPTree2S.from(this);
+ }
+
+ /** Return an instance representing the full spherical 2D space.
+ * @return an instance representing the full spherical 2D space.
+ */
+ public static ConvexArea2S full() {
+ return FULL;
+ }
+
+ /** Construct a convex area by creating great circles between adjacent vertices. The vertices must be given
+ * in a counter-clockwise around order the interior of the shape. If the area is intended to be closed, the
+ * beginning point must be repeated at the end of the path.
+ * @param vertices vertices to use to construct the area
+ * @param precision precision context used to create new great circle instances
+ * @return a convex area constructed using great circles between adjacent vertices
+ * @see #fromVertexLoop(Collection, DoublePrecisionContext)
+ */
+ public static ConvexArea2S fromVertices(final Collection<Point2S> vertices,
+ final DoublePrecisionContext precision) {
+ return fromVertices(vertices, false, precision);
+ }
+
+ /** Construct a convex area by creating great circles between adjacent vertices. An implicit great circle is
+ * created between the last vertex given and the first one, if needed. The vertices must be given in a
+ * counter-clockwise around order the interior of the shape.
+ * @param vertices vertices to use to construct the area
+ * @param precision precision context used to create new great circles instances
+ * @return a convex area constructed using great circles between adjacent vertices
+ * @see #fromVertices(Collection, DoublePrecisionContext)
+ */
+ public static ConvexArea2S fromVertexLoop(final Collection<Point2S> vertices,
+ final DoublePrecisionContext precision) {
+ return fromVertices(vertices, true, precision);
+ }
+
+ /** Construct a convex area from great circles between adjacent vertices.
+ * @param vertices vertices to use to construct the area
+ * @param close if true, an additional great circle will be created between the last and first vertex
+ * @param precision precision context used to create new great circle instances
+ * @return a convex area constructed using great circles between adjacent vertices
+ */
+ public static ConvexArea2S fromVertices(final Collection<Point2S> vertices, final boolean close,
+ final DoublePrecisionContext precision) {
+
+ if (vertices.isEmpty()) {
+ return full();
+ }
+
+ final List<GreatCircle> circles = new ArrayList<>();
+
+ Point2S first = null;
+ Point2S prev = null;
+ Point2S cur = null;
+
+ for (Point2S vertex : vertices) {
+ cur = vertex;
+
+ if (first == null) {
+ first = cur;
+ }
+
+ if (prev != null && !cur.eq(prev, precision)) {
+ circles.add(GreatCircle.fromPoints(prev, cur, precision));
+ }
+
+ prev = cur;
+ }
+
+ if (close && cur != null && !cur.eq(first, precision)) {
+ circles.add(GreatCircle.fromPoints(cur, first, precision));
+ }
+
+ if (!vertices.isEmpty() && circles.isEmpty()) {
+ throw new IllegalStateException("Unable to create convex area: only a single unique vertex provided");
+ }
+
+ return fromBounds(circles);
+ }
+
+ /** Construct a convex area from an arc path. The area represents the intersection of all of the negative
+ * half-spaces of the great circles in the path. The boundaries of the returned area may therefore not match
+ * the arcs in the path.
+ * @param path path to construct the area from
+ * @return a convex area constructed from the great circles in the given path
+ */
+ public static ConvexArea2S fromPath(final GreatArcPath path) {
+ final List<GreatCircle> bounds = path.getArcs().stream()
+ .map(a -> a.getCircle())
+ .collect(Collectors.toList());
+
+ return fromBounds(bounds);
+ }
+
+ /** Create a convex area formed by the intersection of the negative half-spaces of the
+ * given bounding great circles. The returned instance represents the area that is on the
+ * minus side of all of the given circles. Note that this method does not support areas
+ * of zero size (ie, infinitely thin areas or points.)
+ * @param bounds great circles used to define the convex area
+ * @return a new convex area instance representing the area on the minus side of all
+ * of the bounding great circles or an instance representing the full area if no
+ * circles are given
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if the given set of bounding great
+ * circles do not form a convex area, meaning that there is no region that is on the minus side of all
+ * of the bounding circles.
+ */
+ public static ConvexArea2S fromBounds(final GreatCircle... bounds) {
+ return fromBounds(Arrays.asList(bounds));
+ }
+
+ /** Create a convex area formed by the intersection of the negative half-spaces of the
+ * given bounding great circles. The returned instance represents the area that is on the
+ * minus side of all of the given circles. Note that this method does not support areas
+ * of zero size (ie, infinitely thin areas or points.)
+ * @param bounds great circles used to define the convex area
+ * @return a new convex area instance representing the area on the minus side of all
+ * of the bounding great circles or an instance representing the full area if no
+ * circles are given
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if the given set of bounding great
+ * circles do not form a convex area, meaning that there is no region that is on the minus side of all
+ * of the bounding circles.
+ */
+ public static ConvexArea2S fromBounds(final Iterable<GreatCircle> bounds) {
+ final List<GreatArc> arcs = new ConvexRegionBoundaryBuilder<>(GreatArc.class).build(bounds);
+ return arcs.isEmpty() ?
+ full() :
+ new ConvexArea2S(arcs);
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Edge.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Edge.java
deleted file mode 100644
index fd0181e..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Edge.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * 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.util.List;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.spherical.oned.Arc;
-import org.apache.commons.numbers.angle.PlaneAngleRadians;
-
-/** Spherical polygons boundary edge.
- * @see SphericalPolygonsSet#getBoundaryLoops()
- * @see Vertex
- */
-public class Edge {
-
- /** Start vertex. */
- private final Vertex start;
-
- /** End vertex. */
- private Vertex end;
-
- /** Length of the arc. */
- private final double length;
-
- /** Circle supporting the edge. */
- private final Circle circle;
-
- /** Build an edge not contained in any node yet.
- * @param start start vertex
- * @param end end vertex
- * @param length length of the arc (it can be greater than \( \pi \))
- * @param circle circle supporting the edge
- */
- Edge(final Vertex start, final Vertex end, final double length, final Circle circle) {
-
- this.start = start;
- this.end = end;
- this.length = length;
- this.circle = circle;
-
- // connect the vertices back to the edge
- start.setOutgoing(this);
- end.setIncoming(this);
-
- }
-
- /** Get start vertex.
- * @return start vertex
- */
- public Vertex getStart() {
- return start;
- }
-
- /** Get end vertex.
- * @return end vertex
- */
- public Vertex getEnd() {
- return end;
- }
-
- /** Get the length of the arc.
- * @return length of the arc (can be greater than \( \pi \))
- */
- public double getLength() {
- return length;
- }
-
- /** Get the circle supporting this edge.
- * @return circle supporting this edge
- */
- public Circle getCircle() {
- return circle;
- }
-
- /** Get an intermediate point.
- * <p>
- * The angle along the edge should normally be between 0 and {@link #getLength()}
- * in order to remain within edge limits. However, there are no checks on the
- * value of the angle, so user can rebuild the full circle on which an edge is
- * defined if they want.
- * </p>
- * @param alpha angle along the edge, counted from {@link #getStart()}
- * @return an intermediate point
- */
- public Vector3D getPointAt(final double alpha) {
- return circle.getPointAt(alpha + circle.getPhase(start.getLocation().getVector()));
- }
-
- /** Connect the instance with a following edge.
- * @param next edge following the instance
- */
- void setNextEdge(final Edge next) {
- end = next.getStart();
- end.setIncoming(this);
- end.bindWith(getCircle());
- }
-
- /** Split the edge.
- * <p>
- * Once split, this edge is not referenced anymore by the vertices,
- * it is replaced by the two or three sub-edges and intermediate splitting
- * vertices are introduced to connect these sub-edges together.
- * </p>
- * @param splitCircle circle splitting the edge in several parts
- * @param outsideList list where to put parts that are outside of the split circle
- * @param insideList list where to put parts that are inside the split circle
- */
- void split(final Circle splitCircle,
- final List<Edge> outsideList, final List<Edge> insideList) {
-
- // get the inside arc, synchronizing its phase with the edge itself
- final double edgeStart = circle.getPhase(start.getLocation().getVector());
- final Arc arc = circle.getInsideArc(splitCircle);
- final double arcRelativeStart = PlaneAngleRadians.normalize(arc.getInf(), edgeStart + Math.PI) - edgeStart;
- final double arcRelativeEnd = arcRelativeStart + arc.getSize();
- final double unwrappedEnd = arcRelativeEnd - Geometry.TWO_PI;
-
- // build the sub-edges
- final DoublePrecisionContext precision = circle.getPrecision();
- Vertex previousVertex = start;
- if (precision.compare(unwrappedEnd, length) >= 0) {
-
- // the edge is entirely contained inside the circle
- // we don't split anything
- insideList.add(this);
-
- } else {
-
- // there are at least some parts of the edge that should be outside
- // (even is they are later be filtered out as being too small)
- double alreadyManagedLength = 0;
- if (unwrappedEnd >= 0) {
- // the start of the edge is inside the circle
- previousVertex = addSubEdge(previousVertex,
- new Vertex(S2Point.ofVector(circle.getPointAt(edgeStart + unwrappedEnd))),
- unwrappedEnd, insideList, splitCircle);
- alreadyManagedLength = unwrappedEnd;
- }
-
- if (precision.compare(arcRelativeStart, length) >= 0) {
- // the edge ends while still outside of the circle
- if (unwrappedEnd >= 0) {
- previousVertex = addSubEdge(previousVertex, end,
- length - alreadyManagedLength, outsideList, splitCircle);
- } else {
- // the edge is entirely outside of the circle
- // we don't split anything
- outsideList.add(this);
- }
- } else {
- // the edge is long enough to enter inside the circle
- previousVertex = addSubEdge(previousVertex,
- new Vertex(S2Point.ofVector(circle.getPointAt(edgeStart + arcRelativeStart))),
- arcRelativeStart - alreadyManagedLength, outsideList, splitCircle);
- alreadyManagedLength = arcRelativeStart;
-
- if (precision.compare(arcRelativeEnd, length) >= 0) {
- // the edge ends while still inside of the circle
- previousVertex = addSubEdge(previousVertex, end,
- length - alreadyManagedLength, insideList, splitCircle);
- } else {
- // the edge is long enough to exit outside of the circle
- previousVertex = addSubEdge(previousVertex,
- new Vertex(S2Point.ofVector(circle.getPointAt(edgeStart + arcRelativeStart))),
- arcRelativeStart - alreadyManagedLength, insideList, splitCircle);
- alreadyManagedLength = arcRelativeStart;
- previousVertex = addSubEdge(previousVertex, end,
- length - alreadyManagedLength, outsideList, splitCircle);
- }
- }
-
- }
-
- }
-
- /** Add a sub-edge to a list if long enough.
- * <p>
- * If the length of the sub-edge to add is smaller than the {@link Circle#getTolerance()}
- * tolerance of the support circle, it will be ignored.
- * </p>
- * @param subStart start of the sub-edge
- * @param subEnd end of the sub-edge
- * @param subLength length of the sub-edge
- * @param splitCircle circle splitting the edge in several parts
- * @param list list where to put the sub-edge
- * @return end vertex of the edge ({@code subEnd} if the edge was long enough and really
- * added, {@code subStart} if the edge was too small and therefore ignored)
- */
- private Vertex addSubEdge(final Vertex subStart, final Vertex subEnd, final double subLength,
- final List<Edge> list, final Circle splitCircle) {
-
- if (circle.getPrecision().eqZero(subLength)) {
- // the edge is too short, we ignore it
- return subStart;
- }
-
- // really add the edge
- subEnd.bindWith(splitCircle);
- final Edge edge = new Edge(subStart, subEnd, subLength, circle);
- list.add(edge);
- return subEnd;
-
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/EdgesBuilder.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/EdgesBuilder.java
deleted file mode 100644
index 6247eef..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/EdgesBuilder.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * 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.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
-import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.spherical.oned.Arc;
-import org.apache.commons.geometry.spherical.oned.ArcsSet;
-import org.apache.commons.geometry.spherical.oned.S1Point;
-
-/** Visitor building edges.
- */
-class EdgesBuilder implements BSPTreeVisitor<S2Point> {
-
- /** Root of the tree. */
- private final BSPTree<S2Point> root;
-
- /** Precision context used to determine floating point equality. */
- private final DoublePrecisionContext precision;
-
- /** Built edges and their associated nodes. */
- private final Map<Edge, BSPTree<S2Point>> edgeToNode;
-
- /** Reversed map. */
- private final Map<BSPTree<S2Point>, List<Edge>> nodeToEdgesList;
-
- /** Simple constructor.
- * @param root tree root
- * @param precision precision context used to compare floating point values
- */
- EdgesBuilder(final BSPTree<S2Point> root, final DoublePrecisionContext precision) {
- this.root = root;
- this.precision = precision;
- this.edgeToNode = new IdentityHashMap<>();
- this.nodeToEdgesList = new IdentityHashMap<>();
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<S2Point> node) {
- return Order.MINUS_SUB_PLUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<S2Point> node) {
- nodeToEdgesList.put(node, new ArrayList<Edge>());
- @SuppressWarnings("unchecked")
- final BoundaryAttribute<S2Point> attribute = (BoundaryAttribute<S2Point>) node.getAttribute();
- if (attribute.getPlusOutside() != null) {
- addContribution((SubCircle) attribute.getPlusOutside(), false, node);
- }
- if (attribute.getPlusInside() != null) {
- addContribution((SubCircle) attribute.getPlusInside(), true, node);
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<S2Point> node) {
- }
-
- /** Add the contribution of a boundary edge.
- * @param sub boundary facet
- * @param reversed if true, the facet has the inside on its plus side
- * @param node node to which the edge belongs
- */
- private void addContribution(final SubCircle sub, final boolean reversed,
- final BSPTree<S2Point> node) {
- final Circle circle = (Circle) sub.getHyperplane();
- final List<Arc> arcs = ((ArcsSet) sub.getRemainingRegion()).asList();
- for (final Arc a : arcs) {
- final Vertex start = new Vertex(circle.toSpace(S1Point.of(a.getInf())));
- final Vertex end = new Vertex(circle.toSpace(S1Point.of(a.getSup())));
- start.bindWith(circle);
- end.bindWith(circle);
- final Edge edge;
- if (reversed) {
- edge = new Edge(end, start, a.getSize(), circle.getReverse());
- } else {
- edge = new Edge(start, end, a.getSize(), circle);
- }
- edgeToNode.put(edge, node);
- nodeToEdgesList.get(node).add(edge);
- }
- }
-
- /** Get the edge that should naturally follow another one.
- * @param previous edge to be continued
- * @return other edge, starting where the previous one ends (they
- * have not been connected yet)
- * @exception IllegalStateException if there is not a single other edge
- */
- private Edge getFollowingEdge(final Edge previous)
- throws IllegalStateException {
-
- // get the candidate nodes
- final S2Point point = previous.getEnd().getLocation();
- final List<BSPTree<S2Point>> candidates = root.getCloseCuts(point, precision.getMaxZero());
-
- // the following edge we are looking for must start from one of the candidates nodes
- double closest = precision.getMaxZero();
- Edge following = null;
- for (final BSPTree<S2Point> node : candidates) {
- for (final Edge edge : nodeToEdgesList.get(node)) {
- if (edge != previous && edge.getStart().getIncoming() == null) {
- final Vector3D edgeStart = edge.getStart().getLocation().getVector();
- final double gap = point.getVector().angle(edgeStart);
- if (gap <= closest) {
- closest = gap;
- following = edge;
- }
- }
- }
- }
-
- if (following == null) {
- final Vector3D previousStart = previous.getStart().getLocation().getVector();
- if (precision.eqZero(point.getVector().angle(previousStart))) {
- // the edge connects back to itself
- return previous;
- }
-
- // this should never happen
- throw new IllegalStateException("An outline boundary loop is open");
-
- }
-
- return following;
-
- }
-
- /** Get the boundary edges.
- * @return boundary edges
- * @exception IllegalStateException if there is not a single other edge
- */
- public List<Edge> getEdges() {
-
- // connect the edges
- for (final Edge previous : edgeToNode.keySet()) {
- previous.setNextEdge(getFollowingEdge(previous));
- }
-
- return new ArrayList<>(edgeToNode.keySet());
-
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArc.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArc.java
new file mode 100644
index 0000000..faee91c
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArc.java
@@ -0,0 +1,227 @@
+/*
+ * 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.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.spherical.oned.AngularInterval;
+import org.apache.commons.geometry.spherical.oned.CutAngle;
+import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.apache.commons.geometry.spherical.oned.Transform1S;
+
+/** Class representing a single, <em>convex</em> angular interval in a {@link GreatCircle}. Convex
+ * angular intervals are those where the shortest path between all pairs of points in the
+ * interval are completely contained in the interval. In the case of paths that tie for the
+ * shortest length, it is sufficient that one of the paths is completely contained in the
+ * interval. In spherical 2D space, convex arcs either fill the entire great circle or have
+ * an angular size of less than or equal to {@code pi} radians.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class GreatArc extends AbstractSubGreatCircle implements ConvexSubHyperplane<Point2S> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191005L;
+
+ /** The interval representing the region of the great circle contained in the arc.
+ */
+ private final AngularInterval.Convex interval;
+
+ /** Create a new instance from a great circle and the interval embedded in it.
+ * @param circle defining great circle instance
+ * @param interval convex angular interval embedded in the great circle
+ */
+ private GreatArc(final GreatCircle circle, final AngularInterval.Convex interval) {
+ super(circle);
+
+ this.interval = interval;
+ }
+
+ /** Return the start point of the arc, or null if the arc represents the full space.
+ * @return the start point of the arc, or null if the arc represents the full space.
+ */
+ public Point2S getStartPoint() {
+ if (!interval.isFull()) {
+ return getCircle().toSpace(interval.getMinBoundary().getPoint());
+ }
+
+ return null;
+ }
+
+ /** Return the end point of the arc, or null if the arc represents the full space.
+ * @return the end point of the arc, or null if the arc represents the full space.
+ */
+ public Point2S getEndPoint() {
+ if (!interval.isFull()) {
+ return getCircle().toSpace(interval.getMaxBoundary().getPoint());
+ }
+
+ return null;
+ }
+
+ /** Return the midpoint of the arc, or null if the arc represents the full space.
+ * @return the midpoint of the arc, or null if the arc represents the full space.
+ */
+ public Point2S getMidPoint() {
+ if (!interval.isFull()) {
+ return getCircle().toSpace(interval.getMidPoint());
+ }
+
+ return null;
+ }
+
+ /** Get the angular interval for the arc.
+ * @return the angular interval for the arc
+ * @see #getSubspaceRegion()
+ */
+ public AngularInterval.Convex getInterval() {
+ return interval;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AngularInterval.Convex getSubspaceRegion() {
+ return getInterval();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<GreatArc> toConvex() {
+ return Collections.singletonList(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<GreatArc> split(final Hyperplane<Point2S> splitter) {
+ final GreatCircle splitterCircle = (GreatCircle) splitter;
+ final GreatCircle thisCircle = getCircle();
+
+ final Point2S intersection = splitterCircle.intersection(thisCircle);
+
+ GreatArc minus = null;
+ GreatArc plus = null;
+
+ if (intersection != null) {
+ // use a negative-facing cut angle to account for the fact that the great circle
+ // poles point to the minus side of the circle
+ final CutAngle subSplitter = CutAngle.createNegativeFacing(
+ thisCircle.toSubspace(intersection), splitterCircle.getPrecision());
+
+ final Split<AngularInterval.Convex> subSplit = interval.splitDiameter(subSplitter);
+ final SplitLocation subLoc = subSplit.getLocation();
+
+ if (subLoc == SplitLocation.MINUS) {
+ minus = this;
+ } else if (subLoc == SplitLocation.PLUS) {
+ plus = this;
+ } else if (subLoc == SplitLocation.BOTH) {
+ minus = GreatArc.fromInterval(thisCircle, subSplit.getMinus());
+ plus = GreatArc.fromInterval(thisCircle, subSplit.getPlus());
+ }
+ }
+
+ return new Split<>(minus, plus);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public GreatArc transform(final Transform<Point2S> transform) {
+ return new GreatArc(getCircle().transform(transform), interval);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public GreatArc reverse() {
+ return new GreatArc(
+ getCircle().reverse(),
+ interval.transform(Transform1S.createNegation()));
+ }
+
+ /** Return a string representation of this great arc.
+ *
+ * <p>In order to keep the string representation short but useful, the exact format of the return
+ * value depends on the properties of the arc. See below for examples.
+ *
+ * <ul>
+ * <li>Full arc
+ * <ul>
+ * <li>{@code GreatArc[full= true, circle= GreatCircle[pole= (0.0, 0.0, 1.0), x= (1.0, 0.0, 0.0), y= (0.0, 1.0, 0.0)]}</li>
+ * </ul>
+ * </li>
+ * <li>Non-full arc
+ * <ul>
+ * <li>{@code GreatArc[start= (1.0, 1.5707963267948966), end= (2.0, 1.5707963267948966)}</li>
+ * </ul>
+ * </li>
+ * </ul>
+ */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append("[");
+
+ if (isFull()) {
+ sb.append("full= true, circle= ")
+ .append(getCircle());
+ } else {
+ sb.append("start= ")
+ .append(getStartPoint())
+ .append(", end= ")
+ .append(getEndPoint());
+ }
+
+ return sb.toString();
+ }
+
+ /** Construct an arc along the shortest path between the given points. The underlying
+ * great circle is oriented in the direction from {@code start} to {@code end}.
+ * @param start start point for the interval
+ * @param end end point point for the interval
+ * @param precision precision context used to compare floating point numbers
+ * @return an arc representing the shortest path between the given points
+ * @throws org.apache.commons.geometry.core.exception.GeometryException if either of the given points is
+ * NaN or infinite, or if the given points are equal or antipodal as evaluated by the given precision context
+ * @see GreatCircle#fromPoints(Point2S, Point2S, org.apache.commons.geometry.core.precision.DoublePrecisionContext)
+ */
+ public static GreatArc fromPoints(final Point2S start, final Point2S end, final DoublePrecisionContext precision) {
+ final GreatCircle circle = GreatCircle.fromPoints(start, end, precision);
+
+ final Point1S subspaceStart = circle.toSubspace(start);
+ final Point1S subspaceEnd = circle.toSubspace(end);
+ final AngularInterval.Convex interval = AngularInterval.Convex.of(subspaceStart, subspaceEnd, precision);
+
+ return fromInterval(circle, interval);
+ }
+
+ /** Construct an arc from a great circle and an angular interval.
+ * @param circle circle defining the arc
+ * @param interval interval representing the portion of the circle contained
+ * in the arc
+ * @return an arc created from the given great circle and interval
+ */
+ public static GreatArc fromInterval(final GreatCircle circle, final AngularInterval.Convex interval) {
+ return new GreatArc(circle, interval);
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArcPath.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArcPath.java
new file mode 100644
index 0000000..87cbd3b
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArcPath.java
@@ -0,0 +1,688 @@
+/*
+ * 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.io.Serializable;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Class representing a connected sequence of {@link GreatArc} instances.
+ */
+public final class GreatArcPath implements Iterable<GreatArc>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191028L;
+
+ /** 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);
+ }
+
+ /** 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 (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;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterator<GreatArc> iterator() {
+ return arcs.iterator();
+ }
+
+ /** Construct a {@link RegionBSPTree2S} from the arcs in this instance.
+ * @return a bsp tree constructed from the arcs in this instance
+ */
+ public RegionBSPTree2S toTree() {
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+ tree.insert(this);
+
+ return tree;
+ }
+
+ /** 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 (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, DoublePrecisionContext)
+ * 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, DoublePrecisionContext)
+ */
+ public static GreatArcPath fromVertexLoop(final Collection<Point2S> vertices,
+ final DoublePrecisionContext 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, DoublePrecisionContext)
+ * 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, DoublePrecisionContext)
+ */
+ public static GreatArcPath fromVertices(final Collection<Point2S> vertices,
+ final DoublePrecisionContext 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 DoublePrecisionContext 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 DoublePrecisionContext 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 implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191031L;
+
+ /** Arcs appended to the path. */
+ private List<GreatArc> appendedArcs = null;
+
+ /** Arcs prepended to the path. */
+ private List<GreatArc> prependedArcs = null;
+
+ /** Precision context used when creating arcs directly from points. */
+ private DoublePrecisionContext 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 DoublePrecisionContext 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 DoublePrecisionContext 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 DoublePrecisionContext 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(DoublePrecisionContext)
+ */
+ public Builder append(final Point2S vertex) {
+ final DoublePrecisionContext 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(GreatArc.fromPoints(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 (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(DoublePrecisionContext)
+ */
+ public Builder prepend(final Point2S vertex) {
+ final DoublePrecisionContext 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(GreatArc.fromPoints(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(GreatArc.fromPoints(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 DoublePrecisionContext 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 DoublePrecisionContext 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.size() > 0) {
+ 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.size() > 0) {
+ return list.get(list.size() - 1);
+ }
+ return null;
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java
new file mode 100644
index 0000000..47f4dbd
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java
@@ -0,0 +1,446 @@
+/*
+ * 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.util.Objects;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.oned.AngularInterval;
+import org.apache.commons.geometry.spherical.oned.Point1S;
+
+/** Class representing a great circle on the 2-sphere. A great circle is the
+ * intersection of a sphere with a plane that passes through its center. It is
+ * the largest diameter circle that can be drawn on the sphere and partitions the
+ * sphere into two hemispheres. The vectors {@code u} and {@code v} lie in the great
+ * circle plane, while the vector {@code w} (the pole) is perpendicular to it.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class GreatCircle extends AbstractHyperplane<Point2S>
+ implements EmbeddingHyperplane<Point2S, Point1S>, Equivalency<GreatCircle> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20190928L;
+
+ /** Pole or circle center. */
+ private final Vector3D.Unit pole;
+
+ /** First axis in the equator plane, origin of the azimuth angles. */
+ private final Vector3D.Unit u;
+
+ /** Second axis in the equator plane, in quadrature with respect to u. */
+ private final Vector3D.Unit v;
+
+ /** Simple constructor. Callers are responsible for ensuring the inputs are valid.
+ * @param pole pole vector of the great circle
+ * @param u u axis in the equator plane
+ * @param v v axis in the equator plane
+ * @param precision precision context used for floating point comparisons
+ */
+ private GreatCircle(final Vector3D.Unit pole, final Vector3D.Unit u, final Vector3D.Unit v,
+ final DoublePrecisionContext precision) {
+ super(precision);
+
+ this.pole = pole;
+ this.u = u;
+ this.v = v;
+ }
+
+ /** Get the pole of the great circle. This vector is perpendicular to the
+ * equator plane of the instance.
+ * @return pole of the great circle
+ */
+ public Vector3D.Unit getPole() {
+ return pole;
+ }
+
+ /** Get the spherical point located at the positive pole of the instance.
+ * @return the spherical point located at the positive pole of the instance
+ */
+ public Point2S getPolePoint() {
+ return Point2S.from(pole);
+ }
+
+ /** Get the u axis of the great circle. This vector is located in the equator plane and defines
+ * the {@code 0pi} location of the embedded subspace.
+ * @return u axis of the great circle
+ */
+ public Vector3D.Unit getU() {
+ return u;
+ }
+
+ /** Get the v axis of the great circle. This vector lies in the equator plane,
+ * perpendicular to the u-axis.
+ * @return v axis of the great circle
+ */
+ public Vector3D.Unit getV() {
+ return v;
+ }
+
+ /** Get the w (pole) axis of the great circle. The method is equivalent to {@code #getPole()}.
+ * @return the w (pole) axis of the great circle.
+ * @see #getPole()
+ */
+ public Vector3D.Unit getW() {
+ return getPole();
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>The returned offset values are in the range {@code [-pi/2, +pi/2]},
+ * with a point directly on the circle's pole vector having an offset of
+ * {@code -pi/2} and its antipodal point having an offset of {@code +pi/2}.
+ * Thus, the circle's pole vector points toward the <em>minus</em> side of
+ * the hyperplane.</p>
+ *
+ * @see #offset(Vector3D)
+ */
+ @Override
+ public double offset(final Point2S point) {
+ return offset(point.getVector());
+ }
+
+ /** Get the offset (oriented distance) of a direction.
+ *
+ * <p>The offset computed here is equal to the angle between the circle's
+ * pole and the given vector minus {@code pi/2}. Thus, the pole vector
+ * has an offset of {@code -pi/2}, a point on the circle itself has an
+ * offset of {@code 0}, and the negation of the pole vector has an offset
+ * of {@code +pi/2}.</p>
+ * @param vec vector to compute the offset for
+ * @return the offset (oriented distance) of a direction
+ */
+ public double offset(final Vector3D vec) {
+ return pole.angle(vec) - Geometry.HALF_PI;
+ }
+
+ /** Get the azimuth angle of a point relative to this great circle instance,
+ * in the range {@code [0, 2pi)}.
+ * @param pt point to compute the azimuth for
+ * @return azimuth angle of the point in the range {@code [0, 2pi)}
+ */
+ public double azimuth(final Point2S pt) {
+ return azimuth(pt.getVector());
+ }
+
+ /** Get the azimuth angle of a vector in the range {@code [0, 2pi)}.
+ * The azimuth angle is the angle of the projection of the argument on the
+ * equator plane relative to the plane's u-axis. Since the vector is
+ * projected onto the equator plane, it does not need to belong to the circle.
+ * Vectors parallel to the great circle's pole do not have a defined azimuth angle.
+ * In these cases, the method follows the rules of the
+ * {@code Math#atan2(double, double)} method and returns {@code 0}.
+ * @param vector vector to compute the great circle azimuth of
+ * @return azimuth angle of the vector around the great circle in the range
+ * {@code [0, 2pi)}
+ * @see #toSubspace(Point2S)
+ */
+ public double azimuth(final Vector3D vector) {
+ double az = Math.atan2(vector.dot(v), vector.dot(u));
+
+ // adjust range
+ if (az < 0) {
+ az += Geometry.TWO_PI;
+ }
+
+ return az;
+ }
+
+ /** Get the vector on the great circle with the given azimuth angle.
+ * @param azimuth azimuth angle in radians
+ * @return the point on the great circle with the given phase angle
+ */
+ public Vector3D vectorAt(final double azimuth) {
+ return Vector3D.linearCombination(Math.cos(azimuth), u, Math.sin(azimuth), v);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point2S project(final Point2S point) {
+ final double az = azimuth(point.getVector());
+ return Point2S.from(vectorAt(az));
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>The returned instance has the same u-axis but opposite pole and v-axis
+ * as this instance.</p>
+ */
+ @Override
+ public GreatCircle reverse() {
+ return new GreatCircle(pole.negate(), u, v.negate(), getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public GreatCircle transform(final Transform<Point2S> transform) {
+ final Point2S tu = transform.apply(Point2S.from(u));
+ final Point2S tv = transform.apply(Point2S.from(v));
+
+ return fromPoints(tu, tv, getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean similarOrientation(final Hyperplane<Point2S> other) {
+ final GreatCircle otherCircle = (GreatCircle) other;
+ return pole.dot(otherCircle.pole) > 0.0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public GreatArc span() {
+ return GreatArc.fromInterval(this, AngularInterval.full());
+ }
+
+ /** Create an arc on this circle between the given points.
+ * @param start start point
+ * @param end end point
+ * @return an arc on this circle between the given points
+ * @throws GeometryException if the specified interval is not
+ * convex (ie, the angle between the points is greater than {@code pi}
+ */
+ public GreatArc arc(final Point2S start, final Point2S end) {
+ return arc(toSubspace(start), toSubspace(end));
+ }
+
+ /** Create an arc on this circle between the given subspace points.
+ * @param start start subspace point
+ * @param end end subspace point
+ * @return an arc on this circle between the given subspace points
+ * @throws GeometryException if the specified interval is not
+ * convex (ie, the angle between the points is greater than {@code pi}
+ */
+ public GreatArc arc(final Point1S start, final Point1S end) {
+ return arc(start.getAzimuth(), end.getAzimuth());
+ }
+
+ /** Create an arc on this circle between the given subspace azimuth values.
+ * @param start start subspace azimuth
+ * @param end end subspace azimuth
+ * @return an arc on this circle between the given subspace azimuths
+ * @throws GeometryException if the specified interval is not
+ * convex (ie, the angle between the points is greater than {@code pi}
+ */
+ public GreatArc arc(final double start, final double end) {
+ return arc(AngularInterval.Convex.of(start, end, getPrecision()));
+ }
+
+ /** Create an arc on this circle consisting of the given subspace interval.
+ * @param interval subspace interval
+ * @return an arc on this circle consisting of the given subspace interval
+ */
+ public GreatArc arc(final AngularInterval.Convex interval) {
+ return GreatArc.fromInterval(this, interval);
+ }
+
+ /** Return one of the two intersection points between this instance and the argument.
+ * If the circles occupy the same space (ie, their poles are parallel or anti-parallel),
+ * then null is returned. Otherwise, the intersection located at the cross product of
+ * the pole of this instance and that of the argument is returned (ie, {@code thisPole.cross(otherPole)}.
+ * The other intersection point of the pair is antipodal to this point.
+ * @param other circle to intersect with
+ * @return one of the two intersection points between this instance and the argument
+ */
+ public Point2S intersection(final GreatCircle other) {
+ final Vector3D cross = pole.cross(other.pole);
+ if (!cross.eq(Vector3D.ZERO, getPrecision())) {
+ return Point2S.from(cross);
+ }
+
+ return null;
+ }
+
+ /** Compute the angle between this great circle and the argument.
+ * The return value is the angle between the poles of the two circles,
+ * in the range {@code [0, pi]}.
+ * @param other great circle to compute the angle with
+ * @return the angle between this great circle and the argument in the
+ * range {@code [0, pi]}
+ * @see #angle(GreatCircle, Point2S)
+ */
+ public double angle(final GreatCircle other) {
+ return pole.angle(other.pole);
+ }
+
+ /** Compute the angle between this great circle and the argument, measured
+ * at the intersection point closest to the given point. The value is computed
+ * as if a tangent line was drawn from each great circle at the intersection
+ * point closest to {@code pt}, and the angle required to rotate the tangent
+ * line representing the current instance to align with that of the given
+ * instance was measured. The return value lies in the range {@code [-pi, pi)} and
+ * has an absolute value equal to that returned by {@link #angle(GreatCircle)}, but
+ * possibly a different sign. If the given point is equidistant from both intersection
+ * points (as evaluated by this instance's precision context), then the point is assumed
+ * to be closest to the point opposite the cross product of the two poles.
+ * @param other great circle to compute the angle with
+ * @param pt point determining the circle intersection to compute the angle at
+ * @return the angle between this great circle and the argument as measured at the
+ * intersection point closest to the given point; the value is in the range
+ * {@code [-pi, pi)}
+ * @see #angle(GreatCircle)
+ */
+ public double angle(final GreatCircle other, final Point2S pt) {
+ final double theta = angle(other);
+ final Vector3D cross = pole.cross(other.pole);
+
+ return getPrecision().gt(pt.getVector().dot(cross), 0) ?
+ theta :
+ -theta;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point1S toSubspace(final Point2S point) {
+ return Point1S.of(azimuth(point.getVector()));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point2S toSpace(final Point1S point) {
+ return Point2S.from(vectorAt(point.getAzimuth()));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean eq(final GreatCircle other) {
+ if (this == other) {
+ return true;
+ }
+
+ final DoublePrecisionContext precision = getPrecision();
+
+ return precision.equals(other.getPrecision()) &&
+ pole.eq(other.pole, precision) &&
+ u.eq(other.u, precision) &&
+ v.eq(other.v, precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(pole, u, v, getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof GreatCircle)) {
+ return false;
+ }
+
+ GreatCircle other = (GreatCircle) obj;
+
+ return Objects.equals(this.pole, other.pole) &&
+ Objects.equals(this.u, other.u) &&
+ Objects.equals(this.v, other.v) &&
+ Objects.equals(this.getPrecision(), other.getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName())
+ .append("[pole= ")
+ .append(pole)
+ .append(", u= ")
+ .append(u)
+ .append(", v= ")
+ .append(v)
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Create a great circle instance from its pole vector. An arbitrary u-axis is chosen.
+ * @param pole pole vector for the great circle
+ * @param precision precision context used to compare floating point values
+ * @return a great circle defined by the given pole vector
+ */
+ public static GreatCircle fromPole(final Vector3D pole, final DoublePrecisionContext precision) {
+ final Vector3D.Unit u = pole.orthogonal();
+ final Vector3D.Unit v = pole.cross(u).normalize();
+ return new GreatCircle(pole.normalize(), u, v, precision);
+ }
+
+ /** Create a great circle instance from its pole vector and a vector representing the u-axis
+ * in the equator plane. The u-axis vector defines the {@code 0pi} location for the embedded
+ * subspace.
+ * @param pole pole vector for the great circle
+ * @param u u-axis direction for the equator plane
+ * @param precision precision context used to compare floating point values
+ * @return a great circle defined by the given pole vector and u-axis direction
+ */
+ public static GreatCircle fromPoleAndU(final Vector3D pole, final Vector3D u,
+ final DoublePrecisionContext precision) {
+
+ final Vector3D.Unit unitPole = pole.normalize();
+ final Vector3D.Unit unitX = pole.orthogonal(u);
+ final Vector3D.Unit unitY = pole.cross(u).normalize();
+
+ return new GreatCircle(unitPole, unitX, unitY, precision);
+ }
+
+ /** Create a great circle instance from two points on the circle. The u-axis of the
+ * instance points to the location of the first point. The orientation of the circle
+ * is along the shortest path between the two points.
+ * @param a first point on the great circle
+ * @param b second point on the great circle
+ * @param precision precision context used to compare floating point values
+ * @return great circle instance containing the given points
+ * @throws GeometryException if either of the given points is NaN or infinite, or if the given points are
+ * equal or antipodal as evaluated by the given precision context
+ */
+ public static GreatCircle fromPoints(final Point2S a, final Point2S b,
+ final DoublePrecisionContext precision) {
+
+ if (!a.isFinite() || !b.isFinite()) {
+ throw new GeometryException("Invalid points for great circle: " + a + ", " + b);
+ }
+
+ String err = null;
+
+ final double dist = a.distance(b);
+ if (precision.eqZero(dist)) {
+ err = "equal";
+ } else if (precision.eq(dist, Geometry.PI)) {
+ err = "antipodal";
+ }
+
+ if (err != null) {
+ throw new GeometryException("Cannot create great circle from points " + a + " and " + b +
+ ": points are " + err);
+ }
+
+ final Vector3D.Unit u = a.getVector().normalize();
+ final Vector3D.Unit pole = u.cross(b.getVector()).normalize();
+ final Vector3D.Unit v = pole.cross(u).normalize();
+
+ return new GreatCircle(pole, u, v, precision);
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/InteriorAngleGreatArcConnector.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/InteriorAngleGreatArcConnector.java
new file mode 100644
index 0000000..3d3dec0
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/InteriorAngleGreatArcConnector.java
@@ -0,0 +1,127 @@
+/*
+ * 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.util.Collection;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+
+/** Great arc connector that selects between multiple connection options
+ * based on the resulting interior angle. An interior angle in this
+ * case is the angle created between an incoming arc and an outgoing arc
+ * as measured on the minus (interior) side of the incoming arc. If looking
+ * along the direction of the incoming arc, smaller interior angles
+ * point more to the left and larger ones point more to the right.
+ *
+ * <p>This class provides two concrete implementations: {@link Maximize} and
+ * {@link Minimize}, which choose connections with the largest or smallest interior
+ * angles respectively.
+ * </p>
+ */
+public abstract class InteriorAngleGreatArcConnector extends AbstractGreatArcConnector {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191107L;
+
+ /** {@inheritDoc} */
+ @Override
+ protected ConnectableGreatArc selectConnection(final ConnectableGreatArc incoming,
+ final List<ConnectableGreatArc> outgoing) {
+
+ // search for the best connection
+ final GreatCircle circle = incoming.getArc().getCircle();
+
+ double selectedInteriorAngle = Double.POSITIVE_INFINITY;
+ ConnectableGreatArc selected = null;
+
+ for (ConnectableGreatArc candidate : outgoing) {
+ double interiorAngle = Geometry.PI - circle.angle(candidate.getArc().getCircle(),
+ incoming.getArc().getEndPoint());
+
+ if (selected == null || isBetterAngle(interiorAngle, selectedInteriorAngle)) {
+ selectedInteriorAngle = interiorAngle;
+ selected = candidate;
+ }
+ }
+
+ return selected;
+ }
+
+ /** Return true if {@code newAngle} represents a better interior angle than {@code previousAngle}.
+ * @param newAngle the new angle under consideration
+ * @param previousAngle the previous best angle
+ * @return true if {@code newAngle} represents a better interior angle than {@code previousAngle}
+ */
+ protected abstract boolean isBetterAngle(double newAngle, double previousAngle);
+
+ /** Convenience method for connecting a set of arcs with interior angles maximized
+ * when possible. This method is equivalent to {@code new Maximize().connect(segments)}.
+ * @param arcs arcs to connect
+ * @return a list of connected arc paths
+ * @see Maximize
+ */
+ public static List<GreatArcPath> connectMaximized(final Collection<GreatArc> arcs) {
+ return new Maximize().connectAll(arcs);
+ }
+
+ /** Convenience method for connecting a set of line segments with interior angles minimized
+ * when possible. This method is equivalent to {@code new Minimize().connect(segments)}.
+ * @param arcs arcs to connect
+ * @return a list of connected arc paths
+ * @see Minimize
+ */
+ public static List<GreatArcPath> connectMinimized(final Collection<GreatArc> arcs) {
+ return new Minimize().connectAll(arcs);
+ }
+
+ /** Implementation of {@link InteriorAngleGreatArcConnector} that chooses arc
+ * connections that produce the largest interior angles. Another way to visualize this is
+ * that when presented multiple connection options for a given arc, this class will
+ * choose the option that points most to the right when viewed in the direction of the incoming
+ * arc.
+ */
+ public static class Maximize extends InteriorAngleGreatArcConnector {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191107L;
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean isBetterAngle(double newAngle, double previousAngle) {
+ return newAngle > previousAngle;
+ }
+ }
+
+ /** Implementation of {@link InteriorAngleGreatArcConnector} that chooses arc
+ * connections that produce the smallest interior angles. Another way to visualize this is
+ * that when presented multiple connection options for a given arc, this class will
+ * choose the option that points most to the left when viewed in the direction of the incoming
+ * arc.
+ */
+ public static class Minimize extends InteriorAngleGreatArcConnector {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191107L;
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean isBetterAngle(double newAngle, double previousAngle) {
+ return newAngle < previousAngle;
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Point2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Point2S.java
new file mode 100644
index 0000000..e741e5d
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Point2S.java
@@ -0,0 +1,317 @@
+/*
+ * 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.io.Serializable;
+import java.util.Comparator;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.SphericalCoordinates;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+
+/** This class represents a point on the 2-sphere.
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Point2S implements Point<Point2S>, Serializable {
+
+ /** +I (coordinates: ( azimuth = 0, polar = pi/2 )). */
+ public static final Point2S PLUS_I = new Point2S(0, 0.5 * Math.PI, Vector3D.Unit.PLUS_X);
+
+ /** +J (coordinates: ( azimuth = pi/2, polar = pi/2 ))). */
+ public static final Point2S PLUS_J = new Point2S(0.5 * Math.PI, 0.5 * Math.PI, Vector3D.Unit.PLUS_Y);
+
+ /** +K (coordinates: ( azimuth = any angle, polar = 0 )). */
+ public static final Point2S PLUS_K = new Point2S(0, 0, Vector3D.Unit.PLUS_Z);
+
+ /** -I (coordinates: ( azimuth = pi, polar = pi/2 )). */
+ public static final Point2S MINUS_I = new Point2S(Math.PI, 0.5 * Math.PI, Vector3D.Unit.MINUS_X);
+
+ /** -J (coordinates: ( azimuth = 3pi/2, polar = pi/2 )). */
+ public static final Point2S MINUS_J = new Point2S(1.5 * Math.PI, 0.5 * Math.PI, Vector3D.Unit.MINUS_Y);
+
+ /** -K (coordinates: ( azimuth = any angle, polar = pi )). */
+ public static final Point2S MINUS_K = new Point2S(0, Math.PI, Vector3D.Unit.MINUS_Z);
+
+ // CHECKSTYLE: stop ConstantName
+ /** A point with all coordinates set to NaN. */
+ public static final Point2S NaN = new Point2S(Double.NaN, Double.NaN, null);
+ // CHECKSTYLE: resume ConstantName
+
+ /** Comparator that sorts points in component-wise ascending order, first sorting
+ * by polar value and then by azimuth value. Points are only considered equal if
+ * their components match exactly. Null arguments are evaluated as being greater
+ * than non-null arguments.
+ */
+ public static final Comparator<Point2S> POLAR_AZIMUTH_ASCENDING_ORDER = (a, b) -> {
+ int cmp = 0;
+
+ if (a != null && b != null) {
+ cmp = Double.compare(a.getPolar(), b.getPolar());
+
+ if (cmp == 0) {
+ cmp = Double.compare(a.getAzimuth(), b.getAzimuth());
+ }
+ } else if (a != null) {
+ cmp = -1;
+ } else if (b != null) {
+ cmp = 1;
+ }
+
+ return cmp;
+ };
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20180710L;
+
+ /** Azimuthal angle in the x-y plane. */
+ private final double azimuth;
+
+ /** Polar angle. */
+ private final double polar;
+
+ /** Corresponding 3D normalized vector. */
+ private final Vector3D.Unit vector;
+
+ /** Build a point from its internal components.
+ * @param azimuth azimuthal angle in the x-y plane
+ * @param polar polar angle
+ * @param vector corresponding vector; if null, the vector is computed
+ */
+ private Point2S(final double azimuth, final double polar, final Vector3D.Unit vector) {
+ this.azimuth = SphericalCoordinates.normalizeAzimuth(azimuth);
+ this.polar = SphericalCoordinates.normalizePolar(polar);
+ this.vector = (vector != null) ?
+ vector :
+ computeVector(azimuth, polar);
+ }
+
+ /** Get the azimuth angle in the x-y plane in the range {@code [0, 2pi)}.
+ * @return azimuth angle in the x-y plane in the range {@code [0, 2pi)}.
+ * @see Point2S#of(double, double)
+ */
+ public double getAzimuth() {
+ return azimuth;
+ }
+
+ /** Get the polar angle in the range {@code [0, pi)}.
+ * @return polar angle in the range {@code [0, pi)}.
+ * @see Point2S#of(double, double)
+ */
+ public double getPolar() {
+ return polar;
+ }
+
+ /** Get the corresponding normalized vector in 3D Euclidean space.
+ * This value will be null if the spherical coordinates of the point
+ * are infinite or NaN.
+ * @return normalized vector
+ */
+ public Vector3D.Unit getVector() {
+ return vector;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getDimension() {
+ return 2;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isNaN() {
+ return Double.isNaN(azimuth) || Double.isNaN(polar);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return !isNaN() && (Double.isInfinite(azimuth) || Double.isInfinite(polar));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinite() {
+ return Double.isFinite(azimuth) && Double.isFinite(polar);
+ }
+
+ /** Get the point exactly opposite this point on the sphere. The returned
+ * point is {@code pi} distance away from the current instance.
+ * @return the point exactly opposite this point on the sphere
+ */
+ public Point2S antipodal() {
+ return new Point2S(-azimuth, Geometry.PI - polar, vector.negate());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance(final Point2S point) {
+ return distance(this, point);
+ }
+
+ /** Spherically interpolate a point along the shortest arc between this point and
+ * the given point. The parameter {@code t} controls the interpolation and is expected
+ * to be in the range {@code [0, 1]}, with {@code 0} returning a point equivalent to the
+ * current instance {@code 1} returning a point equivalent to the given instance. If the
+ * points are antipodal, then an arbitrary arc is chosen from the infinite number available.
+ * @param other other point to interpolate with
+ * @param t interpolation parameter
+ * @return spherically interpolated point
+ * @see QuaternionRotation#slerp(QuaternionRotation)
+ * @see QuaternionRotation#createVectorRotation(Vector3D, Vector3D)
+ */
+ public Point2S slerp(final Point2S other, final double t) {
+ final QuaternionRotation start = QuaternionRotation.identity();
+ final QuaternionRotation end = QuaternionRotation.createVectorRotation(getVector(), other.getVector());
+
+ final QuaternionRotation quat = QuaternionRotation.of(start.slerp(end).apply(t));
+
+ return Point2S.from(quat.apply(getVector()));
+ }
+
+ /** Return true if this point should be considered equivalent to the argument using the
+ * given precision context. This will be true if the distance between the points is
+ * equivalent to zero as evaluated by the precision context.
+ * @param point point to compare with
+ * @param precision precision context used to perform floating point comparisons
+ * @return true if this point should be considered equivalent to the argument using the
+ * given precision context
+ */
+ public boolean eq(final Point2S point, final DoublePrecisionContext precision) {
+ return precision.eqZero(distance(point));
+ }
+
+ /** Get a hashCode for the point.
+ * .
+ * <p>All NaN values have the same hash code.</p>
+ *
+ * @return a hash code value for this object
+ */
+ @Override
+ public int hashCode() {
+ if (isNaN()) {
+ return 542;
+ }
+ return 134 * (37 * Double.hashCode(azimuth) + Double.hashCode(polar));
+ }
+
+ /** Test for the equality of two points.
+ *
+ * <p>If all spherical coordinates of two points are exactly the same, and none are
+ * <code>Double.NaN</code>, the two points are considered to be equal. Note
+ * that the comparison is made using the azimuth and polar coordinates only; the
+ * corresponding 3D vectors are not compared. This is significant at the poles,
+ * where an infinite number of points share the same underlying 3D vector but may
+ * have different spherical coordinates. For example, the points {@code (0, 0)}
+ * and {@code (1, 0)} (both located at a pole but with different azimuths) will
+ * <em>not</em> be considered equal by this method, even though they share the
+ * exact same underlying 3D vector.</p>
+ *
+ * <p>
+ * <code>NaN</code> coordinates are considered to affect the point globally
+ * and be equals to each other - i.e, if either (or all) coordinates of the
+ * point are equal to <code>Double.NaN</code>, the point is equal to
+ * {@link #NaN}.
+ * </p>
+ *
+ * @param other Object to test for equality to this
+ * @return true if two points on the 2-sphere objects are exactly equal, false if
+ * object is null, not an instance of Point2S, or
+ * not equal to this Point2S instance
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Point2S)) {
+ return false;
+ }
+
+ final Point2S rhs = (Point2S) other;
+ if (rhs.isNaN()) {
+ return this.isNaN();
+ }
+
+ return Double.compare(azimuth, rhs.azimuth) == 0 &&
+ Double.compare(polar, rhs.polar) == 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return SimpleTupleFormat.getDefault().format(getAzimuth(), getPolar());
+ }
+
+ /** Build a vector from its spherical coordinates.
+ * @param azimuth azimuthal angle in the x-y plane
+ * @param polar polar angle
+ * @return point instance with the given coordinates
+ * @see #getAzimuth()
+ * @see #getPolar()
+ */
+ public static Point2S of(final double azimuth, final double polar) {
+ return new Point2S(azimuth, polar, null);
+ }
+
+ /** Build a point from its underlying 3D vector.
+ * @param vector 3D vector
+ * @return point instance with the coordinates determined by the given 3D vector
+ * @exception IllegalStateException if vector norm is zero
+ */
+ public static Point2S from(final Vector3D vector) {
+ final SphericalCoordinates coords = SphericalCoordinates.fromCartesian(vector);
+
+ return new Point2S(coords.getAzimuth(), coords.getPolar(), vector.normalize());
+ }
+
+ /** Parses the given string and returns a new point instance. The expected string
+ * format is the same as that returned by {@link #toString()}.
+ * @param str the string to parse
+ * @return point instance represented by the string
+ * @throws IllegalArgumentException if the given string has an invalid format
+ */
+ public static Point2S parse(final String str) {
+ return SimpleTupleFormat.getDefault().parse(str, Point2S::of);
+ }
+
+ /** Compute the distance (angular separation) between two points.
+ * @param p1 first vector
+ * @param p2 second vector
+ * @return the angular separation between p1 and p2
+ */
+ public static double distance(final Point2S p1, final Point2S p2) {
+ return p1.vector.angle(p2.vector);
+ }
+
+ /** Compute the 3D Euclidean vector associated with the given spherical coordinates.
+ * Null is returned if the coordinates are infinite or NaN.
+ * @param azimuth azimuth value
+ * @param polar polar value
+ * @return the 3D Euclidean vector associated with the given spherical coordinates
+ * or null if either of the arguments are infinite or NaN.
+ */
+ private static Vector3D.Unit computeVector(final double azimuth, final double polar) {
+ if (Double.isFinite(azimuth) && Double.isFinite(polar)) {
+ return SphericalCoordinates.toCartesian(1, azimuth, polar).normalize();
+ }
+ return null;
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/PropertiesComputer.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/PropertiesComputer.java
deleted file mode 100644
index 4793547..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/PropertiesComputer.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * 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.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.internal.GeometryInternalError;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-
-/** Visitor computing geometrical properties.
- */
-class PropertiesComputer implements BSPTreeVisitor<S2Point> {
-
- /** Precision context used to determine floating point equality. */
- private final DoublePrecisionContext precision;
-
- /** Summed area. */
- private double summedArea;
-
- /** Summed barycenter. */
- private Vector3D summedBarycenter;
-
- /** List of points strictly inside convex cells. */
- private final List<Vector3D> convexCellsInsidePoints;
-
- /** Simple constructor.
- * @param precision precision context used to compare floating point values
- */
- PropertiesComputer(final DoublePrecisionContext precision) {
- this.precision = precision;
- this.summedArea = 0;
- this.summedBarycenter = Vector3D.ZERO;
- this.convexCellsInsidePoints = new ArrayList<>();
- }
-
- /** {@inheritDoc} */
- @Override
- public Order visitOrder(final BSPTree<S2Point> node) {
- return Order.MINUS_SUB_PLUS;
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitInternalNode(final BSPTree<S2Point> node) {
- // nothing to do here
- }
-
- /** {@inheritDoc} */
- @Override
- public void visitLeafNode(final BSPTree<S2Point> node) {
- if ((Boolean) node.getAttribute()) {
-
- // transform this inside leaf cell into a simple convex polygon
- final SphericalPolygonsSet convex =
- new SphericalPolygonsSet(node.pruneAroundConvexCell(Boolean.TRUE,
- Boolean.FALSE,
- null),
- precision);
-
- // extract the start of the single loop boundary of the convex cell
- final List<Vertex> boundary = convex.getBoundaryLoops();
- if (boundary.size() != 1) {
- // this should never happen
- throw new GeometryInternalError();
- }
-
- // compute the geometrical properties of the convex cell
- final double area = convexCellArea(boundary.get(0));
- final Vector3D barycenter = convexCellBarycenter(boundary.get(0));
- convexCellsInsidePoints.add(barycenter);
-
- // add the cell contribution to the global properties
- summedArea += area;
- summedBarycenter = Vector3D.linearCombination(1, summedBarycenter, area, barycenter);
-
- }
- }
-
- /** Compute convex cell area.
- * @param start start vertex of the convex cell boundary
- * @return area
- */
- private double convexCellArea(final Vertex start) {
-
- int n = 0;
- double sum = 0;
-
- // loop around the cell
- for (Edge e = start.getOutgoing(); n == 0 || e.getStart() != start; e = e.getEnd().getOutgoing()) {
-
- // find path interior angle at vertex
- final Vector3D previousPole = e.getCircle().getPole();
- final Vector3D nextPole = e.getEnd().getOutgoing().getCircle().getPole();
- final Vector3D point = e.getEnd().getLocation().getVector();
- double alpha = Math.atan2(nextPole.dot(point.cross(previousPole)),
- - nextPole.dot(previousPole));
- if (alpha < 0) {
- alpha += Geometry.TWO_PI;
- }
- sum += alpha;
- n++;
- }
-
- // compute area using extended Girard theorem
- // see Spherical Trigonometry: For the Use of Colleges and Schools by I. Todhunter
- // article 99 in chapter VIII Area Of a Spherical Triangle. Spherical Excess.
- // book available from project Gutenberg at http://www.gutenberg.org/ebooks/19770
- return sum - (n - 2) * Math.PI;
-
- }
-
- /** Compute convex cell barycenter.
- * @param start start vertex of the convex cell boundary
- * @return barycenter
- */
- private Vector3D convexCellBarycenter(final Vertex start) {
-
- int n = 0;
- Vector3D sumB = Vector3D.ZERO;
-
- // loop around the cell
- for (Edge e = start.getOutgoing(); n == 0 || e.getStart() != start; e = e.getEnd().getOutgoing()) {
- sumB = Vector3D.linearCombination(1, sumB, e.getLength(), e.getCircle().getPole());
- n++;
- }
-
- return sumB.normalize();
-
- }
-
- /** Get the area.
- * @return area
- */
- public double getArea() {
- return summedArea;
- }
-
- /** Get the barycenter.
- * @return barycenter
- */
- public S2Point getBarycenter() {
- if (summedBarycenter.normSq() == 0) {
- return S2Point.NaN;
- } else {
- return S2Point.ofVector(summedBarycenter);
- }
- }
-
- /** Get the points strictly inside convex cells.
- * @return points strictly inside convex cells
- */
- public List<Vector3D> getConvexCellsInsidePoints() {
- return convexCellsInsidePoints;
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java
new file mode 100644
index 0000000..a9640e1
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java
@@ -0,0 +1,303 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+
+/** BSP tree representing regions in 2D spherical space.
+ */
+public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTree2S.RegionNode2S> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191005L;
+
+ /** Constant containing the area of the full spherical space. */
+ private static final double FULL_SIZE = 4 * Geometry.PI;
+
+ /** List of great arc path comprising the region boundary. */
+ private List<GreatArcPath> boundaryPaths;
+
+ /** Create a new, empty instance.
+ */
+ public RegionBSPTree2S() {
+ this(false);
+ }
+
+ /** Create a new region. If {@code full} is true, then the region will
+ * represent the entire 2-sphere. Otherwise, it will be empty.
+ * @param full whether or not the region should contain the entire
+ * 2-sphere or be empty
+ */
+ public RegionBSPTree2S(boolean full) {
+ super(full);
+ }
+
+ /** Return a deep copy of this instance.
+ * @return a deep copy of this instance.
+ * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
+ */
+ public RegionBSPTree2S copy() {
+ RegionBSPTree2S result = RegionBSPTree2S.empty();
+ result.copy(this);
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable<GreatArc> boundaries() {
+ return createBoundaryIterable(b -> (GreatArc) b);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<GreatArc> getBoundaries() {
+ return createBoundaryList(b -> (GreatArc) b);
+ }
+
+ /** Get the boundary of the region as a list of connected great arc paths. The
+ * arcs are oriented such that their minus (left) side lies on the interior of
+ * the region.
+ * @return great arc paths representing the region boundary
+ */
+ public List<GreatArcPath> getBoundaryPaths() {
+ if (boundaryPaths == null) {
+ boundaryPaths = Collections.unmodifiableList(computeBoundaryPaths());
+ }
+ return boundaryPaths;
+ }
+
+ /** Return a list of {@link ConvexArea2S}s representing the same region
+ * as this instance. One convex area is returned for each interior leaf
+ * node in the tree.
+ * @return a list of convex areas representing the same region as this
+ * instance
+ */
+ public List<ConvexArea2S> toConvex() {
+ final List<ConvexArea2S> result = new ArrayList<>();
+
+ toConvexRecursive(getRoot(), ConvexArea2S.full(), result);
+
+ return result;
+ }
+
+ /** Recursive method to compute the convex areas of all inside leaf nodes in the subtree rooted at the given
+ * node. The computed convex areas are added to the given list.
+ * @param node root of the subtree to compute the convex areas for
+ * @param nodeArea the convex area for the current node; this will be split by the node's cut hyperplane to
+ * form the convex areas for any child nodes
+ * @param result list containing the results of the computation
+ */
+ private void toConvexRecursive(final RegionNode2S node, final ConvexArea2S nodeArea,
+ final List<ConvexArea2S> result) {
+ if (node.isLeaf()) {
+ // base case; only add to the result list if the node is inside
+ if (node.isInside()) {
+ result.add(nodeArea);
+ }
+ } else {
+ // recurse
+ Split<ConvexArea2S> split = nodeArea.split(node.getCutHyperplane());
+
+ toConvexRecursive(node.getMinus(), split.getMinus(), result);
+ toConvexRecursive(node.getPlus(), split.getPlus(), result);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<RegionBSPTree2S> split(final Hyperplane<Point2S> splitter) {
+ return split(splitter, empty(), empty());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point2S project(final Point2S pt) {
+ // use our custom projector so that we can disambiguate points that are
+ // actually equidistant from the target point
+ final BoundaryProjector2S projector = new BoundaryProjector2S(pt);
+ accept(projector);
+
+ return projector.getProjected();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionSizeProperties<Point2S> computeRegionSizeProperties() {
+ // handle simple cases
+ if (isFull()) {
+ return new RegionSizeProperties<>(FULL_SIZE, null);
+ } else if (isEmpty()) {
+ return new RegionSizeProperties<>(0, null);
+ }
+
+ List<ConvexArea2S> areas = toConvex();
+ DoublePrecisionContext precision = ((GreatArc) getRoot().getCut()).getPrecision();
+
+ double sizeSum = 0;
+ Vector3D barycenterVector = Vector3D.ZERO;
+
+ double size;
+ for (ConvexArea2S area : areas) {
+ size = area.getSize();
+
+ sizeSum += size;
+ barycenterVector = Vector3D.linearCombination(
+ 1, barycenterVector,
+ size, area.getBarycenter().getVector());
+ }
+
+ Point2S barycenter = barycenterVector.eq(Vector3D.ZERO, precision) ?
+ null :
+ Point2S.from(barycenterVector);
+
+ return new RegionSizeProperties<>(sizeSum, barycenter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode2S createNode() {
+ return new RegionNode2S(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void invalidate() {
+ super.invalidate();
+
+ boundaryPaths = null;
+ }
+
+ /** Compute the region represented by the given node.
+ * @param node the node to compute the region for
+ * @return the region represented by the given node
+ */
+ private ConvexArea2S computeNodeRegion(final RegionNode2S node) {
+ ConvexArea2S area = ConvexArea2S.full();
+
+ RegionNode2S child = node;
+ RegionNode2S parent;
+
+ while ((parent = child.getParent()) != null) {
+ Split<ConvexArea2S> split = area.split(parent.getCutHyperplane());
+
+ area = child.isMinus() ? split.getMinus() : split.getPlus();
+
+ child = parent;
+ }
+
+ return area;
+ }
+
+ /** Compute the great arc paths comprising the region boundary.
+ * @return the great arc paths comprising the region boundary
+ */
+ private List<GreatArcPath> computeBoundaryPaths() {
+ final InteriorAngleGreatArcConnector connector = new InteriorAngleGreatArcConnector.Minimize();
+ return connector.connectAll(boundaries());
+ }
+
+ /** Return a new, empty BSP tree.
+ * @return a new, empty BSP tree.
+ */
+ public static RegionBSPTree2S empty() {
+ return new RegionBSPTree2S(false);
+ }
+
+ /** Return a new, full BSP tree. The returned tree represents the
+ * full space.
+ * @return a new, full BSP tree.
+ */
+ public static RegionBSPTree2S full() {
+ return new RegionBSPTree2S(true);
+ }
+
+ /** Construct a tree from a convex area.
+ * @param area the area to construct a tree from
+ * @return tree instance representing the same area as the given
+ * convex area
+ */
+ public static RegionBSPTree2S from(final ConvexArea2S area) {
+ final RegionBSPTree2S tree = RegionBSPTree2S.full();
+ tree.insert(area.getBoundaries());
+
+ return tree;
+ }
+
+ /** BSP tree node for two dimensional spherical space.
+ */
+ public static final class RegionNode2S extends AbstractRegionBSPTree.AbstractRegionNode<Point2S, RegionNode2S> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191005L;
+
+ /** Simple constructor.
+ * @param tree tree owning the instance.
+ */
+ protected RegionNode2S(final AbstractBSPTree<Point2S, RegionNode2S> tree) {
+ super(tree);
+ }
+
+ /** Get the region represented by this node. The returned region contains
+ * the entire area contained in this node, regardless of the attributes of
+ * any child nodes.
+ * @return the region represented by this node
+ */
+ public ConvexArea2S getNodeRegion() {
+ return ((RegionBSPTree2S) getTree()).computeNodeRegion(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected RegionNode2S getSelf() {
+ return this;
+ }
+ }
+
+ /** Class used to project points onto the region boundary.
+ */
+ private static final class BoundaryProjector2S extends BoundaryProjector<Point2S, RegionNode2S> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191120L;
+
+ /** Simple constructor.
+ * @param point the point to project onto the region's boundary
+ */
+ BoundaryProjector2S(Point2S point) {
+ super(point);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Point2S disambiguateClosestPoint(final Point2S target, final Point2S a, final Point2S b) {
+ // return the point with the smallest coordinate values
+ final int cmp = Point2S.POLAR_AZIMUTH_ASCENDING_ORDER.compare(a, b);
+ return cmp < 0 ? a : b;
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/S2Point.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/S2Point.java
deleted file mode 100644
index ca3bf3f..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/S2Point.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- * 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.io.Serializable;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
-import org.apache.commons.geometry.euclidean.threed.SphericalCoordinates;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-
-/** This class represents a point on the 2-sphere.
- * <p>Instances of this class are guaranteed to be immutable.</p>
- */
-public final class S2Point implements Point<S2Point>, Serializable {
-
- /** +I (coordinates: ( azimuth = 0, polar = pi/2 )). */
- public static final S2Point PLUS_I = new S2Point(0, 0.5 * Math.PI, Vector3D.Unit.PLUS_X);
-
- /** +J (coordinates: ( azimuth = pi/2, polar = pi/2 ))). */
- public static final S2Point PLUS_J = new S2Point(0.5 * Math.PI, 0.5 * Math.PI, Vector3D.Unit.PLUS_Y);
-
- /** +K (coordinates: ( azimuth = any angle, polar = 0 )). */
- public static final S2Point PLUS_K = new S2Point(0, 0, Vector3D.Unit.PLUS_Z);
-
- /** -I (coordinates: ( azimuth = pi, polar = pi/2 )). */
- public static final S2Point MINUS_I = new S2Point(Math.PI, 0.5 * Math.PI, Vector3D.Unit.MINUS_X);
-
- /** -J (coordinates: ( azimuth = 3pi/2, polar = pi/2 )). */
- public static final S2Point MINUS_J = new S2Point(1.5 * Math.PI, 0.5 * Math.PI, Vector3D.Unit.MINUS_Y);
-
- /** -K (coordinates: ( azimuth = any angle, polar = pi )). */
- public static final S2Point MINUS_K = new S2Point(0, Math.PI, Vector3D.Unit.MINUS_Z);
-
- // CHECKSTYLE: stop ConstantName
- /** A point with all coordinates set to NaN. */
- public static final S2Point NaN = new S2Point(Double.NaN, Double.NaN, Vector3D.NaN);
- // CHECKSTYLE: resume ConstantName
-
- /** Serializable UID. */
- private static final long serialVersionUID = 20180710L;
-
- /** Azimuthal angle in the x-y plane. */
- private final double azimuth;
-
- /** Polar angle. */
- private final double polar;
-
- /** Corresponding 3D normalized vector. */
- private final Vector3D vector;
-
- /** Build a point from its internal components.
- * @param azimuth azimuthal angle in the x-y plane
- * @param polar polar angle
- * @param vector corresponding vector; if null, the vector is computed
- */
- private S2Point(final double azimuth, final double polar, final Vector3D vector) {
- this.azimuth = SphericalCoordinates.normalizeAzimuth(azimuth);
- this.polar = SphericalCoordinates.normalizePolar(polar);
- this.vector = (vector != null) ? vector : SphericalCoordinates.toCartesian(1.0, azimuth, polar);
- }
-
- /** Get the azimuthal angle in the x-y plane in radians.
- * @return azimuthal angle in the x-y plane
- * @see S2Point#of(double, double)
- */
- public double getAzimuth() {
- return azimuth;
- }
-
- /** Get the polar angle in radians.
- * @return polar angle
- * @see S2Point#of(double, double)
- */
- public double getPolar() {
- return polar;
- }
-
- /** Get the corresponding normalized vector in the 3D Euclidean space.
- * @return normalized vector
- */
- public Vector3D getVector() {
- return vector;
- }
-
- /** {@inheritDoc} */
- @Override
- public int getDimension() {
- return 2;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isNaN() {
- return Double.isNaN(azimuth) || Double.isNaN(polar);
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isInfinite() {
- return !isNaN() && (Double.isInfinite(azimuth) || Double.isInfinite(polar));
- }
-
- /** Get the opposite of the instance.
- * @return a new vector which is opposite to the instance
- */
- public S2Point negate() {
- return new S2Point(-azimuth, Math.PI - polar, vector.negate());
- }
-
- /** {@inheritDoc} */
- @Override
- public double distance(final S2Point point) {
- return distance(this, point);
- }
-
- /** Compute the distance (angular separation) between two points.
- * @param p1 first vector
- * @param p2 second vector
- * @return the angular separation between p1 and p2
- */
- public static double distance(S2Point p1, S2Point p2) {
- return p1.vector.angle(p2.vector);
- }
-
- /**
- * Test for the equality of two points on the 2-sphere.
- * <p>
- * If all coordinates of two points are exactly the same, and none are
- * <code>Double.NaN</code>, the two points are considered to be equal.
- * </p>
- * <p>
- * <code>NaN</code> coordinates are considered to affect globally the vector
- * and be equals to each other - i.e, if either (or all) coordinates of the
- * 2D vector are equal to <code>Double.NaN</code>, the 2D vector is equal to
- * {@link #NaN}.
- * </p>
- *
- * @param other Object to test for equality to this
- * @return true if two points on the 2-sphere objects are equal, false if
- * object is null, not an instance of S2Point, or
- * not equal to this S2Point instance
- *
- */
- @Override
- public boolean equals(Object other) {
- if (this == other) {
- return true;
- }
-
- if (other instanceof S2Point) {
- final S2Point rhs = (S2Point) other;
- if (rhs.isNaN()) {
- return this.isNaN();
- }
-
- return (azimuth == rhs.azimuth) && (polar == rhs.polar);
- }
- return false;
- }
-
- /**
- * Get a hashCode for the 2D vector.
- * <p>
- * All NaN values have the same hash code.</p>
- *
- * @return a hash code value for this object
- */
- @Override
- public int hashCode() {
- if (isNaN()) {
- return 542;
- }
- return 134 * (37 * Double.hashCode(azimuth) + Double.hashCode(polar));
- }
-
- /** {@inheritDoc} */
- @Override
- public String toString() {
- return SimpleTupleFormat.getDefault().format(getAzimuth(), getPolar());
- }
-
- /** Build a vector from its spherical coordinates
- * @param azimuth azimuthal angle in the x-y plane
- * @param polar polar angle
- * @return point instance with the given coordinates
- * @see #getAzimuth()
- * @see #getPolar()
- */
- public static S2Point of(final double azimuth, final double polar) {
- return new S2Point(azimuth, polar, null);
- }
-
- /** Build a point from its underlying 3D vector
- * @param vector 3D vector
- * @return point instance with the coordinates determined by the given 3D vector
- * @exception IllegalStateException if vector norm is zero
- */
- public static S2Point ofVector(final Vector3D vector) {
- SphericalCoordinates coords = SphericalCoordinates.fromCartesian(vector);
-
- return new S2Point(coords.getAzimuth(), coords.getPolar(), vector.normalize());
- }
-
- /** Parses the given string and returns a new point instance. The expected string
- * format is the same as that returned by {@link #toString()}.
- * @param str the string to parse
- * @return point instance represented by the string
- * @throws IllegalArgumentException if the given string has an invalid format
- */
- public static S2Point parse(String str) {
- return SimpleTupleFormat.getDefault().parse(str, S2Point::of);
- }
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSet.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSet.java
deleted file mode 100644
index 83df5a6..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSet.java
+++ /dev/null
@@ -1,558 +0,0 @@
-/*
- * 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.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.partitioning.AbstractRegion;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.enclosing.EnclosingBall;
-import org.apache.commons.geometry.enclosing.WelzlEncloser;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.euclidean.threed.enclosing.SphereGenerator;
-import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.spherical.oned.S1Point;
-
-/** This class represents a region on the 2-sphere: a set of spherical polygons.
- */
-public class SphericalPolygonsSet extends AbstractRegion<S2Point, S1Point> {
-
- /** Boundary defined as an array of closed loops start vertices. */
- private List<Vertex> loops;
-
- /** Build a polygons set representing the whole real 2-sphere.
- * @param precision precision context used to compare floating point values
- */
- public SphericalPolygonsSet(final DoublePrecisionContext precision) {
- super(precision);
- }
-
- /** Build a polygons set representing a hemisphere.
- * @param pole pole of the hemisphere (the pole is in the inside half)
- * @param precision precision context used to compare floating point values
- */
- public SphericalPolygonsSet(final Vector3D pole, final DoublePrecisionContext precision) {
- super(new BSPTree<>(new Circle(pole, precision).wholeHyperplane(),
- new BSPTree<S2Point>(Boolean.FALSE),
- new BSPTree<S2Point>(Boolean.TRUE),
- null),
- precision);
- }
-
- /** Build a polygons set representing a regular polygon.
- * @param center center of the polygon (the center is in the inside half)
- * @param meridian point defining the reference meridian for first polygon vertex
- * @param outsideRadius distance of the vertices to the center
- * @param n number of sides of the polygon
- * @param precision precision context used to compare floating point values
- */
- public SphericalPolygonsSet(final Vector3D center, final Vector3D meridian,
- final double outsideRadius, final int n,
- final DoublePrecisionContext precision) {
- this(precision, createRegularPolygonVertices(center, meridian, outsideRadius, n));
- }
-
- /** Build a polygons set from a BSP tree.
- * <p>The leaf nodes of the BSP tree <em>must</em> have a
- * {@code Boolean} attribute representing the inside status of
- * the corresponding cell (true for inside cells, false for outside
- * cells). In order to avoid building too many small objects, it is
- * recommended to use the predefined constants
- * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
- * @param tree inside/outside BSP tree representing the region
- * @param precision precision context used to compare floating point values
- */
- public SphericalPolygonsSet(final BSPTree<S2Point> tree, final DoublePrecisionContext precision) {
- super(tree, precision);
- }
-
- /** Build a polygons set from a Boundary REPresentation (B-rep).
- * <p>The boundary is provided as a collection of {@link
- * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
- * interior part of the region on its minus side and the exterior on
- * its plus side.</p>
- * <p>The boundary elements can be in any order, and can form
- * several non-connected sets (like for example polygons with holes
- * or a set of disjoint polygons considered as a whole). In
- * fact, the elements do not even need to be connected together
- * (their topological connections are not used here). However, if the
- * boundary does not really separate an inside open from an outside
- * open (open having here its topological meaning), then subsequent
- * calls to the {@link
- * org.apache.commons.geometry.core.partitioning.Region#checkPoint(org.apache.commons.geometry.core.Point)
- * checkPoint} method will not be meaningful anymore.</p>
- * <p>If the boundary is empty, the region will represent the whole
- * space.</p>
- * @param boundary collection of boundary elements, as a
- * collection of {@link SubHyperplane SubHyperplane} objects
- * @param precision precision context used to compare floating point values
- */
- public SphericalPolygonsSet(final Collection<SubHyperplane<S2Point>> boundary, final DoublePrecisionContext precision) {
- super(boundary, precision);
- }
-
- /** Build a polygon from a simple list of vertices.
- * <p>The boundary is provided as a list of points considering to
- * represent the vertices of a simple loop. The interior part of the
- * region is on the left side of this path and the exterior is on its
- * right side.</p>
- * <p>This constructor does not handle polygons with a boundary
- * forming several disconnected paths (such as polygons with holes).</p>
- * <p>For cases where this simple constructor applies, it is expected to
- * be numerically more robust than the {@link #SphericalPolygonsSet(Collection, DoublePrecisionContext)
- * general constructor} using {@link SubHyperplane subhyperplanes}.</p>
- * <p>If the list is empty, the region will represent the whole
- * space.</p>
- * <p>
- * Polygons with thin pikes or dents are inherently difficult to handle because
- * they involve circles with almost opposite directions at some vertices. Polygons
- * whose vertices come from some physical measurement with noise are also
- * difficult because an edge that should be straight may be broken in lots of
- * different pieces with almost equal directions. In both cases, computing the
- * circles intersections is not numerically robust due to the almost 0 or almost
- * π angle. Such cases need to carefully adjust the {@code hyperplaneThickness}
- * parameter. A too small value would often lead to completely wrong polygons
- * with large area wrongly identified as inside or outside. Large values are
- * often much safer. As a rule of thumb, a value slightly below the size of the
- * most accurate detail needed is a good value for the {@code hyperplaneThickness}
- * parameter.
- * </p>
- * @param precision precision context used to compare floating point values
- * @param vertices vertices of the simple loop boundary
- */
- public SphericalPolygonsSet(final DoublePrecisionContext precision, final S2Point ... vertices) {
- super(verticesToTree(precision, vertices), precision);
- }
-
- /** Build the vertices representing a regular polygon.
- * @param center center of the polygon (the center is in the inside half)
- * @param meridian point defining the reference meridian for first polygon vertex
- * @param outsideRadius distance of the vertices to the center
- * @param n number of sides of the polygon
- * @return vertices array
- */
- private static S2Point[] createRegularPolygonVertices(final Vector3D center, final Vector3D meridian,
- final double outsideRadius, final int n) {
- final S2Point[] array = new S2Point[n];
- final QuaternionRotation r0 = QuaternionRotation.fromAxisAngle(center.cross(meridian),
- outsideRadius);
- array[0] = S2Point.ofVector(r0.apply(center));
-
- final QuaternionRotation r = QuaternionRotation.fromAxisAngle(center, Geometry.TWO_PI / n);
- for (int i = 1; i < n; ++i) {
- array[i] = S2Point.ofVector(r.apply(array[i - 1].getVector()));
- }
-
- return array;
- }
-
- /** Build the BSP tree of a polygons set from a simple list of vertices.
- * <p>The boundary is provided as a list of points considering to
- * represent the vertices of a simple loop. The interior part of the
- * region is on the left side of this path and the exterior is on its
- * right side.</p>
- * <p>This constructor does not handle polygons with a boundary
- * forming several disconnected paths (such as polygons with holes).</p>
- * <p>This constructor handles only polygons with edges strictly shorter
- * than \( \pi \). If longer edges are needed, they need to be broken up
- * in smaller sub-edges so this constraint holds.</p>
- * <p>For cases where this simple constructor applies, it is expected to
- * be numerically more robust than the {@link #PolygonsSet(Collection) general
- * constructor} using {@link SubHyperplane subhyperplanes}.</p>
- * @param precision precision context used to compare floating point values
- * @param vertices vertices of the simple loop boundary
- * @return the BSP tree of the input vertices
- */
- private static BSPTree<S2Point> verticesToTree(final DoublePrecisionContext precision,
- final S2Point ... vertices) {
-
- final int n = vertices.length;
- if (n == 0) {
- // the tree represents the whole space
- return new BSPTree<>(Boolean.TRUE);
- }
-
- // build the vertices
- final Vertex[] vArray = new Vertex[n];
- for (int i = 0; i < n; ++i) {
- vArray[i] = new Vertex(vertices[i]);
- }
-
- // build the edges
- List<Edge> edges = new ArrayList<>(n);
- Vertex end = vArray[n - 1];
- for (int i = 0; i < n; ++i) {
-
- // get the endpoints of the edge
- final Vertex start = end;
- end = vArray[i];
-
- // get the circle supporting the edge, taking care not to recreate it
- // if it was already created earlier due to another edge being aligned
- // with the current one
- Circle circle = start.sharedCircleWith(end);
- if (circle == null) {
- circle = new Circle(start.getLocation(), end.getLocation(), precision);
- }
-
- // create the edge and store it
- edges.add(new Edge(start, end,
- start.getLocation().getVector().angle(
- end.getLocation().getVector()),
- circle));
-
- // check if another vertex also happens to be on this circle
- for (final Vertex vertex : vArray) {
- if (vertex != start && vertex != end &&
- precision.eqZero(circle.getOffset(vertex.getLocation()))) {
- vertex.bindWith(circle);
- }
- }
-
- }
-
- // build the tree top-down
- final BSPTree<S2Point> tree = new BSPTree<>();
- insertEdges(precision, tree, edges);
-
- return tree;
-
- }
-
- /** Recursively build a tree by inserting cut sub-hyperplanes.
- * @param precision precision context used to compare floating point values
- * @param node current tree node (it is a leaf node at the beginning
- * of the call)
- * @param edges list of edges to insert in the cell defined by this node
- * (excluding edges not belonging to the cell defined by this node)
- */
- private static void insertEdges(final DoublePrecisionContext precision,
- final BSPTree<S2Point> node,
- final List<Edge> edges) {
-
- // find an edge with an hyperplane that can be inserted in the node
- int index = 0;
- Edge inserted = null;
- while (inserted == null && index < edges.size()) {
- inserted = edges.get(index++);
- if (!node.insertCut(inserted.getCircle())) {
- inserted = null;
- }
- }
-
- if (inserted == null) {
- // no suitable edge was found, the node remains a leaf node
- // we need to set its inside/outside boolean indicator
- final BSPTree<S2Point> parent = node.getParent();
- if (parent == null || node == parent.getMinus()) {
- node.setAttribute(Boolean.TRUE);
- } else {
- node.setAttribute(Boolean.FALSE);
- }
- return;
- }
-
- // we have split the node by inserting an edge as a cut sub-hyperplane
- // distribute the remaining edges in the two sub-trees
- final List<Edge> outsideList = new ArrayList<>();
- final List<Edge> insideList = new ArrayList<>();
- for (final Edge edge : edges) {
- if (edge != inserted) {
- edge.split(inserted.getCircle(), outsideList, insideList);
- }
- }
-
- // recurse through lower levels
- if (!outsideList.isEmpty()) {
- insertEdges(precision, node.getPlus(), outsideList);
- } else {
- node.getPlus().setAttribute(Boolean.FALSE);
- }
- if (!insideList.isEmpty()) {
- insertEdges(precision, node.getMinus(), insideList);
- } else {
- node.getMinus().setAttribute(Boolean.TRUE);
- }
-
- }
-
- /** {@inheritDoc} */
- @Override
- public SphericalPolygonsSet buildNew(final BSPTree<S2Point> tree) {
- return new SphericalPolygonsSet(tree, getPrecision());
- }
-
- /** {@inheritDoc}
- * @exception IllegalStateException if the tolerance setting does not allow to build
- * a clean non-ambiguous boundary
- */
- @Override
- protected void computeGeometricalProperties() {
-
- final BSPTree<S2Point> tree = getTree(true);
-
- if (tree.getCut() == null) {
-
- // the instance has a single cell without any boundaries
-
- if (tree.getCut() == null && (Boolean) tree.getAttribute()) {
- // the instance covers the whole space
- setSize(4 * Math.PI);
- setBarycenter(S2Point.of(0, 0));
- } else {
- setSize(0);
- setBarycenter(S2Point.NaN);
- }
-
- } else {
-
- // the instance has a boundary
- final PropertiesComputer pc = new PropertiesComputer(getPrecision());
- tree.visit(pc);
- setSize(pc.getArea());
- setBarycenter(pc.getBarycenter());
-
- }
-
- }
-
- /** Get the boundary loops of the polygon.
- * <p>The polygon boundary can be represented as a list of closed loops,
- * each loop being given by exactly one of its vertices. From each loop
- * start vertex, one can follow the loop by finding the outgoing edge,
- * then the end vertex, then the next outgoing edge ... until the start
- * vertex of the loop (exactly the same instance) is found again once
- * the full loop has been visited.</p>
- * <p>If the polygon has no boundary at all, a zero length loop
- * array will be returned.</p>
- * <p>If the polygon is a simple one-piece polygon, then the returned
- * array will contain a single vertex.
- * </p>
- * <p>All edges in the various loops have the inside of the region on
- * their left side (i.e. toward their pole) and the outside on their
- * right side (i.e. away from their pole) when moving in the underlying
- * circle direction. This means that the closed loops obey the direct
- * trigonometric orientation.</p>
- * @return boundary of the polygon, organized as an unmodifiable list of loops start vertices.
- * @exception IllegalStateException if the tolerance setting does not allow to build
- * a clean non-ambiguous boundary
- * @see Vertex
- * @see Edge
- */
- public List<Vertex> getBoundaryLoops() {
-
- if (loops == null) {
- if (getTree(false).getCut() == null) {
- loops = Collections.emptyList();
- } else {
-
- // sort the arcs according to their start point
- final BSPTree<S2Point> root = getTree(true);
- final EdgesBuilder visitor = new EdgesBuilder(root, getPrecision());
- root.visit(visitor);
- final List<Edge> edges = visitor.getEdges();
-
-
- // convert the list of all edges into a list of start vertices
- loops = new ArrayList<>();
- while (!edges.isEmpty()) {
-
- // this is an edge belonging to a new loop, store it
- Edge edge = edges.get(0);
- final Vertex startVertex = edge.getStart();
- loops.add(startVertex);
-
- // remove all remaining edges in the same loop
- do {
-
- // remove one edge
- for (final Iterator<Edge> iterator = edges.iterator(); iterator.hasNext();) {
- if (iterator.next() == edge) {
- iterator.remove();
- break;
- }
- }
-
- // go to next edge following the boundary loop
- edge = edge.getEnd().getOutgoing();
-
- } while (edge.getStart() != startVertex);
-
- }
-
- }
- }
-
- return Collections.unmodifiableList(loops);
-
- }
-
- /** Get a spherical cap enclosing the polygon.
- * <p>
- * This method is intended as a first test to quickly identify points
- * that are guaranteed to be outside of the region, hence performing a full
- * {@link #checkPoint(org.apache.commons.geometry.core.Point) checkPoint}
- * only if the point status remains undecided after the quick check. It is
- * is therefore mostly useful to speed up computation for small polygons with
- * complex shapes (say a country boundary on Earth), as the spherical cap will
- * be small and hence will reliably identify a large part of the sphere as outside,
- * whereas the full check can be more computing intensive. A typical use case is
- * therefore:
- * </p>
- * <pre>{@code
- * // compute region, plus an enclosing spherical cap
- * SphericalPolygonsSet complexShape = ...;
- * EnclosingBall<S2Point, S2Point> cap = complexShape.getEnclosingCap();
- *
- * // check lots of points
- * for (Vector3D p : points) {
- *
- * final Location l;
- * if (cap.contains(p)) {
- * // we cannot be sure where the point is
- * // we need to perform the full computation
- * l = complexShape.checkPoint(v);
- * } else {
- * // no need to do further computation,
- * // we already know the point is outside
- * l = Location.OUTSIDE;
- * }
- *
- * // use l ...
- *
- * }
- * }</pre>
- * <p>
- * In the special cases of empty or whole sphere polygons, special
- * spherical caps are returned, with angular radius set to negative
- * or positive infinity so the {@link
- * EnclosingBall#contains(org.apache.commons.geometry.core.Point) ball.contains(point)}
- * method return always false or true.
- * </p>
- * <p>
- * This method is <em>not</em> guaranteed to return the smallest enclosing cap.
- * </p>
- * @return a spherical cap enclosing the polygon
- */
- public EnclosingBall<S2Point> getEnclosingCap() {
-
- // handle special cases first
- if (isEmpty()) {
- return new EnclosingBall<>(S2Point.PLUS_K, Double.NEGATIVE_INFINITY);
- }
- if (isFull()) {
- return new EnclosingBall<>(S2Point.PLUS_K, Double.POSITIVE_INFINITY);
- }
-
- // as the polygons is neither empty nor full, it has some boundaries and cut hyperplanes
- final BSPTree<S2Point> root = getTree(false);
- if (isEmpty(root.getMinus()) && isFull(root.getPlus())) {
- // the polygon covers an hemisphere, and its boundary is one 2π long edge
- final Circle circle = (Circle) root.getCut().getHyperplane();
- return new EnclosingBall<>(S2Point.ofVector(circle.getPole()).negate(),
- 0.5 * Math.PI);
- }
- if (isFull(root.getMinus()) && isEmpty(root.getPlus())) {
- // the polygon covers an hemisphere, and its boundary is one 2π long edge
- final Circle circle = (Circle) root.getCut().getHyperplane();
- return new EnclosingBall<>(S2Point.ofVector(circle.getPole()),
- 0.5 * Math.PI);
- }
-
- // gather some inside points, to be used by the encloser
- final List<Vector3D> points = getInsidePoints();
-
- // extract points from the boundary loops, to be used by the encloser as well
- final List<Vertex> boundary = getBoundaryLoops();
- for (final Vertex loopStart : boundary) {
- int count = 0;
- for (Vertex v = loopStart; count == 0 || v != loopStart; v = v.getOutgoing().getEnd()) {
- ++count;
- points.add(v.getLocation().getVector());
- }
- }
-
- // find the smallest enclosing 3D sphere
- final SphereGenerator generator = new SphereGenerator();
- final WelzlEncloser<Vector3D> encloser =
- new WelzlEncloser<>(getPrecision(), generator);
- EnclosingBall<Vector3D> enclosing3D = encloser.enclose(points);
- final Vector3D[] support3D = enclosing3D.getSupport();
-
- // convert to 3D sphere to spherical cap
- final double r = enclosing3D.getRadius();
- final double h = enclosing3D.getCenter().norm();
- if (getPrecision().eqZero(h)) {
- // the 3D sphere is centered on the unit sphere and covers it
- // fall back to a crude approximation, based only on outside convex cells
- EnclosingBall<S2Point> enclosingS2 =
- new EnclosingBall<>(S2Point.PLUS_K, Double.POSITIVE_INFINITY);
- for (Vector3D outsidePoint : getOutsidePoints()) {
- final S2Point outsideS2 = S2Point.ofVector(outsidePoint);
- final BoundaryProjection<S2Point> projection = projectToBoundary(outsideS2);
- if (Math.PI - projection.getOffset() < enclosingS2.getRadius()) {
- enclosingS2 = new EnclosingBall<>(outsideS2.negate(),
- Math.PI - projection.getOffset(),
- projection.getProjected());
- }
- }
- return enclosingS2;
- }
- final S2Point[] support = new S2Point[support3D.length];
- for (int i = 0; i < support3D.length; ++i) {
- support[i] = S2Point.ofVector(support3D[i]);
- }
-
- final EnclosingBall<S2Point> enclosingS2 =
- new EnclosingBall<>(S2Point.ofVector(enclosing3D.getCenter()),
- Math.acos((1 + h * h - r * r) / (2 * h)),
- support);
-
- return enclosingS2;
-
- }
-
- /** Gather some inside points.
- * @return list of points known to be strictly in all inside convex cells
- */
- private List<Vector3D> getInsidePoints() {
- final PropertiesComputer pc = new PropertiesComputer(getPrecision());
- getTree(true).visit(pc);
- return pc.getConvexCellsInsidePoints();
- }
-
- /** Gather some outside points.
- * @return list of points known to be strictly in all outside convex cells
- */
- private List<Vector3D> getOutsidePoints() {
- final SphericalPolygonsSet complement =
- (SphericalPolygonsSet) new RegionFactory<S2Point>().getComplement(this);
- final PropertiesComputer pc = new PropertiesComputer(getPrecision());
- complement.getTree(true).visit(pc);
- return pc.getConvexCellsInsidePoints();
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubCircle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubCircle.java
deleted file mode 100644
index bc8edca..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubCircle.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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 org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.spherical.oned.Arc;
-import org.apache.commons.geometry.spherical.oned.ArcsSet;
-import org.apache.commons.geometry.spherical.oned.S1Point;
-
-/** This class represents a sub-hyperplane for {@link Circle}.
- */
-public class SubCircle extends AbstractSubHyperplane<S2Point, S1Point> {
-
- /** Simple constructor.
- * @param hyperplane underlying hyperplane
- * @param remainingRegion remaining region of the hyperplane
- */
- public SubCircle(final Hyperplane<S2Point> hyperplane,
- final Region<S1Point> remainingRegion) {
- super(hyperplane, remainingRegion);
- }
-
- /** {@inheritDoc} */
- @Override
- protected AbstractSubHyperplane<S2Point, S1Point> buildNew(final Hyperplane<S2Point> hyperplane,
- final Region<S1Point> remainingRegion) {
- return new SubCircle(hyperplane, remainingRegion);
- }
-
- /** {@inheritDoc} */
- @Override
- public SplitSubHyperplane<S2Point> split(final Hyperplane<S2Point> hyperplane) {
-
- final Circle thisCircle = (Circle) getHyperplane();
- final Circle otherCircle = (Circle) hyperplane;
- final double angle = thisCircle.getPole().angle(otherCircle.getPole());
- final DoublePrecisionContext precision = thisCircle.getPrecision();
-
- if (precision.eqZero(angle) || precision.compare(angle, Math.PI) >= 0) {
- // the two circles are aligned or opposite
- return new SplitSubHyperplane<>(null, null);
- } else {
- // the two circles intersect each other
- final Arc arc = thisCircle.getInsideArc(otherCircle);
- final ArcsSet.Split split = ((ArcsSet) getRemainingRegion()).split(arc);
- final ArcsSet plus = split.getPlus();
- final ArcsSet minus = split.getMinus();
- return new SplitSubHyperplane<>(plus == null ? null : new SubCircle(thisCircle.copySelf(), plus),
- minus == null ? null : new SubCircle(thisCircle.copySelf(), minus));
- }
-
- }
-
-}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubGreatCircle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubGreatCircle.java
new file mode 100644
index 0000000..2b873b4
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubGreatCircle.java
@@ -0,0 +1,233 @@
+/*
+ * 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.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.spherical.oned.CutAngle;
+import org.apache.commons.geometry.spherical.oned.RegionBSPTree1S;
+
+/** Class representing an arbitrary region of a great circle.
+ */
+public final class SubGreatCircle extends AbstractSubGreatCircle {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20191005L;
+
+ /** The 1D region on the great circle. */
+ private final RegionBSPTree1S region;
+
+ /** Construct a new, empty subhyperplane for the given great circle.
+ * @param greatCircle great circle defining this instance
+ */
+ public SubGreatCircle(final GreatCircle greatCircle) {
+ this(greatCircle, false);
+ }
+
+ /** Construct a new sub-region for the given great circle. If {@code full}
+ * is true, then the region will cover the entire circle; otherwise,
+ * it will be empty.
+ * @param circle great circle that the sub-region will belong to
+ * @param full if true, the sub-region will cover the entire circle;
+ * otherwise it will be empty
+ */
+ public SubGreatCircle(final GreatCircle circle, final boolean full) {
+ this(circle, new RegionBSPTree1S(full));
+ }
+
+ /** Construct a new instance from its defining great circle and subspace region.
+ * @param circle great circle that the sub-region will belong to
+ * @param region subspace region
+ */
+ public SubGreatCircle(final GreatCircle circle, final RegionBSPTree1S region) {
+ super(circle);
+
+ this.region = region;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionBSPTree1S getSubspaceRegion() {
+ return region;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubGreatCircle transform(final Transform<Point2S> transform) {
+ final GreatCircle circle = getCircle().transform(transform);
+
+ return new SubGreatCircle(circle, region.copy());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<GreatArc> toConvex() {
+ return region.toIntervals().stream()
+ .flatMap(i -> i.toConvex().stream())
+ .map(i -> GreatArc.fromInterval(getCircle(), i))
+ .collect(Collectors.toList());
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>In all cases, the current instance is not modified. However, In order to avoid
+ * unnecessary copying, this method will use the current instance as the split value when
+ * the instance lies entirely on the plus or minus side of the splitter. For example, if
+ * this instance lies entirely on the minus side of the splitter, the subplane
+ * returned by {@link Split#getMinus()} will be this instance. Similarly, {@link Split#getPlus()}
+ * will return the current instance if it lies entirely on the plus side. Callers need to make
+ * special note of this, since this class is mutable.</p>
+ */
+ @Override
+ public Split<SubGreatCircle> split(final Hyperplane<Point2S> splitter) {
+
+ final GreatCircle splitterCircle = (GreatCircle) splitter;
+ final GreatCircle thisCircle = getCircle();
+
+ final Point2S intersection = splitterCircle.intersection(thisCircle);
+
+ SubGreatCircle minus = null;
+ SubGreatCircle plus = null;
+
+ if (intersection != null) {
+ final CutAngle subSplitter = CutAngle.createPositiveFacing(
+ thisCircle.toSubspace(intersection), splitterCircle.getPrecision());
+
+ final Split<RegionBSPTree1S> subSplit = region.splitDiameter(subSplitter);
+ final SplitLocation subLoc = subSplit.getLocation();
+
+ if (subLoc == SplitLocation.MINUS) {
+ minus = this;
+ } else if (subLoc == SplitLocation.PLUS) {
+ plus = this;
+ } else if (subLoc == SplitLocation.BOTH) {
+ minus = new SubGreatCircle(thisCircle, subSplit.getMinus());
+ plus = new SubGreatCircle(thisCircle, subSplit.getPlus());
+ }
+ }
+
+ return new Split<>(minus, plus);
+ }
+
+ /** Add an arc to this instance.
+ * @param arc arc to add
+ * @throws GeometryException if the given arc is not from
+ * a great circle equivalent to this instance
+ */
+ public void add(final GreatArc arc) {
+ validateGreatCircle(arc.getCircle());
+
+ region.add(arc.getSubspaceRegion());
+ }
+
+ /** Add the region represented by the given subcircle to this instance.
+ * The argument is not modified.
+ * @param subcircle subcircle to add
+ * @throws GeometryException if the given subcircle is not from
+ * a great circle equivalent to this instance
+ */
+ public void add(final SubGreatCircle subcircle) {
+ validateGreatCircle(subcircle.getCircle());
+
+ region.union(subcircle.getSubspaceRegion());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName())
+ .append('[')
+ .append("circle= ")
+ .append(getCircle())
+ .append(", region= ")
+ .append(region)
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Validate that the given great circle is equivalent to the circle
+ * defining this instance.
+ * @param inputCircle the great circle to validate
+ * @throws GeometryException if the argument is not equivalent
+ * to the great circle for this instance
+ */
+ private void validateGreatCircle(final GreatCircle inputCircle) {
+ final GreatCircle circle = getCircle();
+
+ if (!circle.eq(inputCircle)) {
+ throw new GeometryException("Argument is not on the same " +
+ "great circle. Expected " + circle + " but was " +
+ inputCircle);
+ }
+ }
+
+ /** {@link Builder} implementation for subcircles.
+ */
+ public static final class SubGreatCircleBuilder implements SubHyperplane.Builder<Point2S> {
+
+ /** SubGreatCircle instance created by this builder. */
+ private final SubGreatCircle subcircle;
+
+ /** Construct a new instance for building regions for the given great circle.
+ * @param circle the underlying great circle for the region
+ */
+ public SubGreatCircleBuilder(final GreatCircle circle) {
+ this.subcircle = new SubGreatCircle(circle);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final SubHyperplane<Point2S> sub) {
+ addInternal(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void add(final ConvexSubHyperplane<Point2S> sub) {
+ addInternal(sub);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SubGreatCircle build() {
+ return subcircle;
+ }
+
+ /** Internal method for adding subhyperplanes to this builder.
+ * @param sub the subhyperplane to add; either convex or non-convex
+ */
+ private void addInternal(final SubHyperplane<Point2S> sub) {
+ if (sub instanceof GreatArc) {
+ subcircle.add((GreatArc) sub);
+ } else if (sub instanceof SubGreatCircle) {
+ subcircle.add((SubGreatCircle) sub);
+ } else {
+ throw new IllegalArgumentException("Unsupported subhyperplane type: " + sub.getClass().getName());
+ }
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Transform2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Transform2S.java
new file mode 100644
index 0000000..e6abe83
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Transform2S.java
@@ -0,0 +1,264 @@
+/*
+ * 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.io.Serializable;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+
+/** Implementation of the {@link Transform} interface for spherical 2D points.
+ *
+ * <p>This class uses an {@link AffineTransformMatrix3D} to perform spherical point transforms
+ * in Euclidean 3D space.</p>
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Transform2S implements Transform<Point2S>, Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 1L;
+
+ /** Static instance representing the identity transform. */
+ private static final Transform2S IDENTITY = new Transform2S(AffineTransformMatrix3D.identity());
+
+ /** Static transform instance that reflects across the x-y plane. */
+ private static final AffineTransformMatrix3D XY_PLANE_REFLECTION = AffineTransformMatrix3D.createScale(1, 1, -1);
+
+ /** Euclidean transform matrix underlying the spherical transform. */
+ private final AffineTransformMatrix3D euclideanTransform;
+
+ /** Construct a new instance from its underlying Euclidean transform.
+ * @param euclideanTransform underlying Euclidean transform
+ */
+ private Transform2S(final AffineTransformMatrix3D euclideanTransform) {
+ this.euclideanTransform = euclideanTransform;
+ }
+
+ /** Get the Euclidean transform matrix underlying the spherical transform.
+ * @return the Euclidean transform matrix underlying the spherical transform
+ */
+ public AffineTransformMatrix3D getEuclideanTransform() {
+ return euclideanTransform;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point2S apply(final Point2S pt) {
+ final Vector3D vec = pt.getVector();
+ return Point2S.from(euclideanTransform.apply(vec));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean preservesOrientation() {
+ return euclideanTransform.preservesOrientation();
+ }
+
+ /** Return a new instance representing the inverse transform operation
+ * of this instance.
+ * @return a transform representing the inverse of this instance
+ */
+ public Transform2S inverse() {
+ return new Transform2S(euclideanTransform.inverse());
+ }
+
+ /** Apply a rotation of {@code angle} radians around the given point to this instance.
+ * @param pt point to rotate around
+ * @param angle rotation angle in radians
+ * @return transform resulting from applying the specified rotation to this instance
+ */
+ public Transform2S rotate(final Point2S pt, final double angle) {
+ return premultiply(createRotation(pt, angle));
+ }
+
+ /** Apply a rotation of {@code angle} radians around the given 3D axis to this instance.
+ * @param axis 3D axis of rotation
+ * @param angle rotation angle in radians
+ * @return transform resulting from applying the specified rotation to this instance
+ */
+ public Transform2S rotate(final Vector3D axis, final double angle) {
+ return premultiply(createRotation(axis, angle));
+ }
+
+ /** Apply the given quaternion rotation to this instance.
+ * @param quaternion quaternion rotation to apply
+ * @return transform resulting from applying the specified rotation to this instance
+ */
+ public Transform2S rotate(final QuaternionRotation quaternion) {
+ return premultiply(createRotation(quaternion));
+ }
+
+ /** Apply a reflection across the equatorial plane defined by the given pole point
+ * to this instance.
+ * @param pole pole point defining the equatorial reflection plane
+ * @return transform resulting from applying the specified reflection to this instance
+ */
+ public Transform2S reflect(final Point2S pole) {
+ return premultiply(createReflection(pole));
+ }
+
+ /** Apply a reflection across the equatorial plane defined by the given pole vector
+ * to this instance.
+ * @param poleVector pole vector defining the equatorial reflection plane
+ * @return transform resulting from applying the specified reflection to this instance
+ */
+ public Transform2S reflect(final Vector3D poleVector) {
+ return premultiply(createReflection(poleVector));
+ }
+
+ /** Multiply the underlying Euclidean transform of this instance by that of the argument, eg,
+ * {@code other * this}. The returned transform performs the equivalent of
+ * {@code other} followed by {@code this}.
+ * @param other transform to multiply with
+ * @return a new transform computed by multiplying the matrix of this
+ * instance by that of the argument
+ * @see AffineTransformMatrix3D#multiply(AffineTransformMatrix3D)
+ */
+ public Transform2S multiply(final Transform2S other) {
+ return multiply(this, other);
+ }
+
+ /** Multiply the underlying Euclidean transform matrix of the argument by that of this instance, eg,
+ * {@code this * other}. The returned transform performs the equivalent of {@code this}
+ * followed by {@code other}.
+ * @param other transform to multiply with
+ * @return a new transform computed by multiplying the matrix of the
+ * argument by that of this instance
+ * @see AffineTransformMatrix3D#premultiply(AffineTransformMatrix3D)
+ */
+ public Transform2S premultiply(final Transform2S other) {
+ return multiply(other, this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return euclideanTransform.hashCode();
+ }
+
+ /**
+ * Return true if the given object is an instance of {@link Transform2S}
+ * and the underlying Euclidean transform matrices are exactly equal.
+ * @param obj object to test for equality with the current instance
+ * @return true if the underlying transform matrices are exactly equal
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Transform2S)) {
+ return false;
+ }
+ final Transform2S other = (Transform2S) obj;
+
+ return euclideanTransform.equals(other.euclideanTransform);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+
+ sb.append(this.getClass().getSimpleName())
+ .append("[euclideanTransform= ")
+ .append(getEuclideanTransform())
+ .append("]");
+
+ return sb.toString();
+ }
+
+ /** Return an instance representing the identity transform. This transform is guaranteed
+ * to return an <em>equivalent</em> (ie, co-located) point for any input point. However, the
+ * points are not guaranteed to contain exactly equal coordinates. For example, at the poles, an
+ * infinite number of points exist that vary only in the azimuth coordinate. When one of these
+ * points is transformed by this identity transform, the returned point may contain a different
+ * azimuth value from the input, but it will still represent the same location in space.
+ * @return an instance representing the identity transform
+ */
+ public static Transform2S identity() {
+ return IDENTITY;
+ }
+
+ /** Create a transform that rotates the given angle around {@code pt}.
+ * @param pt point to rotate around
+ * @param angle angle of rotation in radians
+ * @return a transform that rotates the given angle around {@code pt}
+ */
+ public static Transform2S createRotation(final Point2S pt, final double angle) {
+ return createRotation(pt.getVector(), angle);
+ }
+
+ /** Create a transform that rotates the given angle around {@code axis}.
+ * @param axis 3D axis of rotation
+ * @param angle angle of rotation in radians
+ * @return a transform that rotates the given angle {@code axis}
+ */
+ public static Transform2S createRotation(final Vector3D axis, final double angle) {
+ return createRotation(QuaternionRotation.fromAxisAngle(axis, angle));
+ }
+
+ /** Create a transform that performs the given 3D rotation.
+ * @param quaternion quaternion instance representing the 3D rotation
+ * @return a transform that performs the given 3D rotation
+ */
+ public static Transform2S createRotation(final QuaternionRotation quaternion) {
+ return new Transform2S(quaternion.toMatrix());
+ }
+
+ /** Create a transform that performs a reflection across the equatorial plane
+ * defined by the given pole point.
+ * @param pole pole point defining the equatorial reflection plane
+ * @return a transform that performs a reflection across the equatorial plane
+ * defined by the given pole point
+ */
+ public static Transform2S createReflection(final Point2S pole) {
+ return createReflection(pole.getVector());
+ }
+
+ /** Create a transform that performs a reflection across the equatorial plane
+ * defined by the given pole point.
+ * @param poleVector pole vector defining the equatorial reflection plane
+ * @return a transform that performs a reflection across the equatorial plane
+ * defined by the given pole point
+ */
+ public static Transform2S createReflection(final Vector3D poleVector) {
+ final QuaternionRotation quat = QuaternionRotation.createVectorRotation(poleVector, Vector3D.Unit.PLUS_Z);
+
+ final AffineTransformMatrix3D matrix = quat.toMatrix()
+ .premultiply(XY_PLANE_REFLECTION)
+ .premultiply(quat.inverse().toMatrix());
+
+ return new Transform2S(matrix);
+ }
+
+ /** Multiply the Euclidean transform matrices of the arguments together.
+ * @param a first transform
+ * @param b second transform
+ * @return the transform computed as {@code a x b}
+ */
+ private static Transform2S multiply(final Transform2S a, final Transform2S b) {
+
+ final AffineTransformMatrix3D aMat = a.euclideanTransform;
+ final AffineTransformMatrix3D bMat = b.euclideanTransform;
+
+ return new Transform2S(aMat.multiply(bMat));
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Vertex.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Vertex.java
deleted file mode 100644
index cfa1840..0000000
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Vertex.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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.util.ArrayList;
-import java.util.List;
-
-/** Spherical polygons boundary vertex.
- * @see SphericalPolygonsSet#getBoundaryLoops()
- * @see Edge
- */
-public class Vertex {
-
- /** Vertex location. */
- private final S2Point location;
-
- /** Incoming edge. */
- private Edge incoming;
-
- /** Outgoing edge. */
- private Edge outgoing;
-
- /** Circles bound with this vertex. */
- private final List<Circle> circles;
-
- /** Build a non-processed vertex not owned by any node yet.
- * @param location vertex location
- */
- Vertex(final S2Point location) {
- this.location = location;
- this.incoming = null;
- this.outgoing = null;
- this.circles = new ArrayList<>();
- }
-
- /** Get Vertex location.
- * @return vertex location
- */
- public S2Point getLocation() {
- return location;
- }
-
- /** Bind a circle considered to contain this vertex.
- * @param circle circle to bind with this vertex
- */
- void bindWith(final Circle circle) {
- circles.add(circle);
- }
-
- /** Get the common circle bound with both the instance and another vertex, if any.
- * <p>
- * When two vertices are both bound to the same circle, this means they are
- * already handled by node associated with this circle, so there is no need
- * to create a cut hyperplane for them.
- * </p>
- * @param vertex other vertex to check instance against
- * @return circle bound with both the instance and another vertex, or null if the
- * two vertices do not share a circle yet
- */
- Circle sharedCircleWith(final Vertex vertex) {
- for (final Circle circle1 : circles) {
- for (final Circle circle2 : vertex.circles) {
- if (circle1 == circle2) {
- return circle1;
- }
- }
- }
- return null;
- }
-
- /** Set incoming edge.
- * <p>
- * The circle supporting the incoming edge is automatically bound
- * with the instance.
- * </p>
- * @param incoming incoming edge
- */
- void setIncoming(final Edge incoming) {
- this.incoming = incoming;
- bindWith(incoming.getCircle());
- }
-
- /** Get incoming edge.
- * @return incoming edge
- */
- public Edge getIncoming() {
- return incoming;
- }
-
- /** Set outgoing edge.
- * <p>
- * The circle supporting the outgoing edge is automatically bound
- * with the instance.
- * </p>
- * @param outgoing outgoing edge
- */
- void setOutgoing(final Edge outgoing) {
- this.outgoing = outgoing;
- bindWith(outgoing.getCircle());
- }
-
- /** Get outgoing edge.
- * @return outgoing edge
- */
- public Edge getOutgoing() {
- return outgoing;
- }
-
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalTestUtils.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalTestUtils.java
index dcb2571..a1aac54 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalTestUtils.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalTestUtils.java
@@ -16,105 +16,69 @@
*/
package org.apache.commons.geometry.spherical;
-import java.io.IOException;
-import java.text.ParseException;
-
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.TreeBuilder;
-import org.apache.commons.geometry.core.partitioning.TreeDumper;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.spherical.oned.ArcsSet;
-import org.apache.commons.geometry.spherical.oned.LimitAngle;
-import org.apache.commons.geometry.spherical.oned.S1Point;
-import org.apache.commons.geometry.spherical.twod.Circle;
-import org.apache.commons.geometry.spherical.twod.S2Point;
-import org.apache.commons.geometry.spherical.twod.SphericalPolygonsSet;
+import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.apache.commons.geometry.spherical.twod.Point2S;
+import org.junit.Assert;
-/** Test utilities for spherical spaces.
+/** Class containing various test utilities for spherical space.
*/
public class SphericalTestUtils {
- /** Get a string representation of an {@link ArcsSet}.
- * @param arcsSet region to dump
- * @return string representation of the region
+ /** Assert that the given points are equal, using the specified tolerance value.
+ * @param expected
+ * @param actual
+ * @param tolerance
*/
- public static String dump(final ArcsSet arcsSet) {
- final TreeDumper<S1Point> visitor = new TreeDumper<S1Point>("ArcsSet") {
-
- /** {@inheritDoc} */
- @Override
- protected void formatHyperplane(final Hyperplane<S1Point> hyperplane) {
- final LimitAngle h = (LimitAngle) hyperplane;
- getFormatter().format("%22.15e %b %s",
- h.getLocation().getAzimuth(), h.isDirect(), h.getPrecision().toString());
- }
-
- };
- arcsSet.getTree(false).visit(visitor);
- return visitor.getDump();
+ public static void assertPointsEqual(Point1S expected, Point1S actual, double tolerance) {
+ String msg = "Expected point to equal " + expected + " but was " + actual + ";";
+ Assert.assertEquals(msg, expected.getAzimuth(), actual.getAzimuth(), tolerance);
}
- /** Get a string representation of a {@link SphericalPolygonsSet}.
- * @param sphericalPolygonsSet region to dump
- * @return string representation of the region
+ /** Assert that the given points are equal, using the specified tolerance value.
+ * @param expected
+ * @param actual
+ * @param tolerance
*/
- public static String dump(final SphericalPolygonsSet sphericalPolygonsSet) {
- final TreeDumper<S2Point> visitor = new TreeDumper<S2Point>("SphericalPolygonsSet") {
-
- /** {@inheritDoc} */
- @Override
- protected void formatHyperplane(final Hyperplane<S2Point> hyperplane) {
- final Circle h = (Circle) hyperplane;
- getFormatter().format("%22.15e %22.15e %22.15e %s",
- h.getPole().getX(), h.getPole().getY(), h.getPole().getZ(),
- h.getPrecision().toString());
- }
-
- };
- sphericalPolygonsSet.getTree(false).visit(visitor);
- return visitor.getDump();
+ public static void assertPointsEqual(Point2S expected, Point2S actual, double tolerance) {
+ String msg = "Expected point to equal " + expected + " but was " + actual + ";";
+ Assert.assertEquals(msg, expected.getAzimuth(), actual.getAzimuth(), tolerance);
+ Assert.assertEquals(msg, expected.getPolar(), actual.getPolar(), tolerance);
}
- /** Parse a string representation of an {@link ArcsSet}.
- * @param s string to parse
- * @return parsed region
- * @exception IOException if the string cannot be read
- * @exception ParseException if the string cannot be parsed
+ /** Assert that the given points are equalivalent, using the specified tolerance value.
+ * @param expected
+ * @param actual
+ * @param tolerance
*/
- public static ArcsSet parseArcsSet(final String s)
- throws IOException, ParseException {
- final TreeBuilder<S1Point> builder = new TreeBuilder<S1Point>("ArcsSet", s) {
-
- /** {@inheritDoc} */
- @Override
- protected LimitAngle parseHyperplane()
- throws ParseException {
- return new LimitAngle(S1Point.of(getNumber()), getBoolean(), getPrecision());
- }
-
- };
- return new ArcsSet(builder.getTree(), builder.getPrecision());
+ public static void assertPointsEq(Point2S expected, Point2S actual, double tolerance) {
+ String msg = "Expected point to be equivalent to " + expected + " but was " + actual + ";";
+ Assert.assertTrue(msg, expected.eq(actual, new EpsilonDoublePrecisionContext(tolerance)));
}
- /** Parse a string representation of a {@link SphericalPolygonsSet}.
- * @param s string to parse
- * @return parsed region
- * @exception IOException if the string cannot be read
- * @exception ParseException if the string cannot be parsed
+ /** Assert that the given vectors are equal, using the specified tolerance value.
+ * @param expected
+ * @param actual
+ * @param tolerance
*/
- public static SphericalPolygonsSet parseSphericalPolygonsSet(final String s)
- throws IOException, ParseException {
- final TreeBuilder<S2Point> builder = new TreeBuilder<S2Point>("SphericalPolygonsSet", s) {
-
- /** {@inheritDoc} */
- @Override
- public Circle parseHyperplane()
- throws ParseException {
- return new Circle(Vector3D.of(getNumber(), getNumber(), getNumber()), getPrecision());
- }
-
- };
- return new SphericalPolygonsSet(builder.getTree(), builder.getPrecision());
+ public static void assertVectorsEqual(Vector3D expected, Vector3D actual, double tolerance) {
+ String msg = "Expected vector to equal " + expected + " but was " + actual + ";";
+ Assert.assertEquals(msg, expected.getX(), actual.getX(), tolerance);
+ Assert.assertEquals(msg, expected.getY(), actual.getY(), tolerance);
+ Assert.assertEquals(msg, expected.getZ(), actual.getZ(), tolerance);
}
+ /** Assert that the given points lie in the specified location relative to the region.
+ * @param region region to test
+ * @param loc expected location of the given points
+ * @param pts points to test
+ */
+ public static void checkClassify(Region<Point2S> region, RegionLocation loc, Point2S ... pts) {
+ for (Point2S pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, loc, region.classify(pt));
+ }
+ }
}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
new file mode 100644
index 0000000..853b297
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
@@ -0,0 +1,894 @@
+/*
+ * 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.oned;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AngularIntervalTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testOf_doubles() {
+ // act/assert
+ checkInterval(AngularInterval.of(0, 1, TEST_PRECISION), 0, 1);
+ checkInterval(AngularInterval.of(1, 0, TEST_PRECISION), 1, Geometry.TWO_PI);
+ checkInterval(AngularInterval.of(-2, -1.5, TEST_PRECISION), -2, -1.5);
+ checkInterval(AngularInterval.of(-2, -2.5, TEST_PRECISION), -2, Geometry.TWO_PI - 2.5);
+
+ checkFull(AngularInterval.of(1, 1, TEST_PRECISION));
+ checkFull(AngularInterval.of(0, 1e-11, TEST_PRECISION));
+ checkFull(AngularInterval.of(0, -1e-11, TEST_PRECISION));
+ checkFull(AngularInterval.of(0, Geometry.TWO_PI, TEST_PRECISION));
+ }
+
+ @Test
+ public void testOf_doubles_invalidArgs() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Double.NEGATIVE_INFINITY, 0, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(0, Double.POSITIVE_INFINITY, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Double.NaN, 0, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(0, Double.NaN, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Double.NaN, Double.NaN, TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testOf_points() {
+ // act/assert
+ checkInterval(AngularInterval.of(Point1S.of(0), Point1S.of(1), TEST_PRECISION), 0, 1);
+ checkInterval(AngularInterval.of(Point1S.of(1), Point1S.of(0), TEST_PRECISION), 1, Geometry.TWO_PI);
+ checkInterval(AngularInterval.of(Point1S.of(-2), Point1S.of(-1.5), TEST_PRECISION), -2, -1.5);
+ checkInterval(AngularInterval.of(Point1S.of(-2), Point1S.of(-2.5), TEST_PRECISION), -2, Geometry.TWO_PI - 2.5);
+
+ checkFull(AngularInterval.of(Point1S.of(1), Point1S.of(1), TEST_PRECISION));
+ checkFull(AngularInterval.of(Point1S.of(0), Point1S.of(1e-11), TEST_PRECISION));
+ checkFull(AngularInterval.of(Point1S.of(0), Point1S.of(-1e-11), TEST_PRECISION));
+ }
+
+ @Test
+ public void testOf_points_invalidArgs() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Point1S.of(Double.NEGATIVE_INFINITY), Point1S.ZERO, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Point1S.ZERO, Point1S.of(Double.POSITIVE_INFINITY), TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Point1S.of(Double.POSITIVE_INFINITY), Point1S.of(Double.NEGATIVE_INFINITY), TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Point1S.NaN, Point1S.ZERO, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Point1S.ZERO, Point1S.NaN, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(Point1S.NaN, Point1S.NaN, TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testOf_orientedPoints() {
+ // arrange
+ DoublePrecisionContext precisionA = new EpsilonDoublePrecisionContext(1e-3);
+ DoublePrecisionContext precisionB = new EpsilonDoublePrecisionContext(1e-2);
+
+ CutAngle zeroPos = CutAngle.createPositiveFacing(Point1S.ZERO, precisionA);
+ CutAngle zeroNeg = CutAngle.createNegativeFacing(Point1S.ZERO, precisionA);
+
+ CutAngle piPos = CutAngle.createPositiveFacing(Point1S.PI, precisionA);
+ CutAngle piNeg = CutAngle.createNegativeFacing(Point1S.PI, precisionA);
+
+ CutAngle almostPiPos = CutAngle.createPositiveFacing(Point1S.of(Geometry.PI + 5e-3), precisionB);
+
+ // act/assert
+ checkInterval(AngularInterval.of(zeroNeg, piPos), 0, Geometry.PI);
+ checkInterval(AngularInterval.of(zeroPos, piNeg), Geometry.PI, Geometry.TWO_PI);
+
+ checkFull(AngularInterval.of(zeroPos, zeroNeg));
+ checkFull(AngularInterval.of(zeroPos, piPos));
+ checkFull(AngularInterval.of(piNeg, zeroNeg));
+
+ checkFull(AngularInterval.of(almostPiPos, piNeg));
+ checkFull(AngularInterval.of(piNeg, almostPiPos));
+ }
+
+ @Test
+ public void testOf_orientedPoints_invalidArgs() {
+ // arrange
+ CutAngle pt = CutAngle.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
+ CutAngle nan = CutAngle.createPositiveFacing(Point1S.NaN, TEST_PRECISION);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(pt, nan);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(nan, pt);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.of(nan, nan);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testFull() {
+ // act
+ AngularInterval.Convex interval = AngularInterval.full();
+
+ // assert
+ checkFull(interval);
+ }
+
+ @Test
+ public void testClassify_full() {
+ // arrange
+ AngularInterval interval = AngularInterval.full();
+
+ // act/assert
+ for (double a = -2 * Geometry.PI; a >= 4 * Geometry.PI; a += 0.5) {
+ checkClassify(interval, RegionLocation.INSIDE, Point1S.of(a));
+ }
+ }
+
+ @Test
+ public void testClassify_almostFull() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(1 + 2e-10, 1, TEST_PRECISION);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ Point1S.of(1 + 2e-10), Point1S.of(1 + 6e-11), Point1S.of(1));
+
+ checkClassify(interval, RegionLocation.INSIDE, Point1S.of(1 + 6e-11 + Geometry.PI));
+
+ for (double a = 1 + 1e-9; a >= 1 - 1e-9 + Geometry.TWO_PI; a += 0.5) {
+ checkClassify(interval, RegionLocation.INSIDE, Point1S.of(a));
+ }
+ }
+
+ @Test
+ public void testClassify_sizeableGap() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(0.25, -0.25, TEST_PRECISION);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.OUTSIDE,
+ Point1S.ZERO, Point1S.of(-0.2), Point1S.of(0.2));
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ Point1S.of(-0.25), Point1S.of(0.2499999999999));
+ checkClassify(interval, RegionLocation.INSIDE,
+ Point1S.of(1), Point1S.PI, Point1S.of(-1));
+ }
+
+ @Test
+ public void testClassify_halfPi() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.HALF_PI, Geometry.MINUS_HALF_PI, TEST_PRECISION);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.OUTSIDE,
+ Point1S.ZERO, Point1S.of(Geometry.HALF_PI - 0.1), Point1S.of(Geometry.MINUS_HALF_PI + 0.1));
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ Point1S.of(Geometry.HALF_PI), Point1S.of(1.5 * Geometry.PI));
+ checkClassify(interval, RegionLocation.INSIDE,
+ Point1S.PI, Point1S.of(Geometry.HALF_PI + 0.1), Point1S.of(Geometry.MINUS_HALF_PI - 0.1));
+ }
+
+ @Test
+ public void testClassify_almostEmpty() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(1, 1 + 2e-10, TEST_PRECISION);
+
+ // act/assert
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ Point1S.of(1 + 2e-10), Point1S.of(1 + 6e-11), Point1S.of(1));
+
+ checkClassify(interval, RegionLocation.OUTSIDE, Point1S.of(1 + 6e-11 + Geometry.PI));
+
+ for (double a = 1 + 1e-9; a >= 1 - 1e-9 + Geometry.TWO_PI; a += 0.5) {
+ checkClassify(interval, RegionLocation.OUTSIDE, Point1S.of(a));
+ }
+ }
+
+ @Test
+ public void testProject_full() {
+ // arrange
+ AngularInterval interval = AngularInterval.full();
+
+ // act/assert
+ Assert.assertNull(interval.project(Point1S.ZERO));
+ Assert.assertNull(interval.project(Point1S.PI));
+ }
+
+ @Test
+ public void testProject() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(1, 2, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(1, interval.project(Point1S.ZERO).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(1, interval.project(Point1S.of(1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(1, interval.project(Point1S.of(1.5)).getAzimuth(), TEST_EPS);
+
+ Assert.assertEquals(2, interval.project(Point1S.of(2)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(2, interval.project(Point1S.PI).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(2, interval.project(Point1S.of(1.4 + Geometry.PI)).getAzimuth(), TEST_EPS);
+
+ Assert.assertEquals(1, interval.project(Point1S.of(1.5 + Geometry.PI)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(1, interval.project(Point1S.of(1.6 + Geometry.PI)).getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_full() {
+ // arrange
+ AngularInterval interval = AngularInterval.full();
+
+ Transform1S rotate = Transform1S.createRotation(Geometry.HALF_PI);
+ Transform1S invert = Transform1S.createNegation().rotate(Geometry.HALF_PI);
+
+ // act/assert
+ checkFull(interval.transform(rotate));
+ checkFull(interval.transform(invert));
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION);
+
+ Transform1S rotate = Transform1S.createRotation(Geometry.HALF_PI);
+ Transform1S invert = Transform1S.createNegation().rotate(Geometry.HALF_PI);
+
+ // act/assert
+ checkInterval(interval.transform(rotate), Geometry.PI, 1.5 * Geometry.PI);
+ checkInterval(interval.transform(invert), -0.5 * Geometry.PI, Geometry.ZERO_PI);
+ }
+
+ @Test
+ public void testWrapsZero() {
+ // act/assert
+ Assert.assertFalse(AngularInterval.full().wrapsZero());
+ Assert.assertFalse(AngularInterval.of(0, Geometry.HALF_PI, TEST_PRECISION).wrapsZero());
+ Assert.assertFalse(AngularInterval.of(Geometry.HALF_PI, Geometry.PI , TEST_PRECISION).wrapsZero());
+ Assert.assertFalse(AngularInterval.of(Geometry.PI, 1.5 * Geometry.PI , TEST_PRECISION).wrapsZero());
+ Assert.assertFalse(AngularInterval.of(1.5 * Geometry.PI, Geometry.TWO_PI - 1e-5, TEST_PRECISION).wrapsZero());
+
+ Assert.assertTrue(AngularInterval.of(1.5 * Geometry.PI, Geometry.TWO_PI, TEST_PRECISION).wrapsZero());
+ Assert.assertTrue(AngularInterval.of(1.5 * Geometry.PI, 2.5 * Geometry.PI, TEST_PRECISION).wrapsZero());
+ Assert.assertTrue(AngularInterval.of(-2.5 * Geometry.PI, -1.5 * Geometry.PI, TEST_PRECISION).wrapsZero());
+ }
+
+ @Test
+ public void testToTree_full() {
+ // arrange
+ AngularInterval interval = AngularInterval.full();
+
+ // act
+ RegionBSPTree1S tree = interval.toTree();
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Point1S.ZERO, Point1S.of(Geometry.HALF_PI),
+ Point1S.PI, Point1S.of(Geometry.MINUS_HALF_PI));
+ }
+
+ @Test
+ public void testToTree_intervalEqualToPi() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.ZERO_PI, Geometry.PI, TEST_PRECISION);
+
+ // act
+ RegionBSPTree1S tree = interval.toTree();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Point1S.ZERO, Point1S.PI);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Point1S.of(1e-4), Point1S.of(0.25 * Geometry.PI),
+ Point1S.of(-1.25 * Geometry.PI), Point1S.of(Geometry.PI - 1e-4));
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Point1S.of(-1e-4), Point1S.of(-0.25 * Geometry.PI),
+ Point1S.of(1.25 * Geometry.PI), Point1S.of(-Geometry.PI + 1e-4));
+ }
+
+ @Test
+ public void testToTree_intervalLessThanPi() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION);
+
+ // act
+ RegionBSPTree1S tree = interval.toTree();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Point1S.of(Geometry.HALF_PI), Point1S.PI);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Point1S.of(0.51 * Geometry.PI), Point1S.of(0.75 * Geometry.PI),
+ Point1S.of(0.99 * Geometry.PI));
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Point1S.ZERO, Point1S.of(0.25 * Geometry.PI),
+ Point1S.of(1.25 * Geometry.PI), Point1S.of(1.75 * Geometry.PI));
+ }
+
+ @Test
+ public void testToTree_intervalGreaterThanPi() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.PI, Geometry.HALF_PI, TEST_PRECISION);
+
+ // act
+ RegionBSPTree1S tree = interval.toTree();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Point1S.of(Geometry.HALF_PI), Point1S.PI);
+
+ checkClassify(tree, RegionLocation.INSIDE,
+ Point1S.ZERO, Point1S.of(0.25 * Geometry.PI),
+ Point1S.of(1.25 * Geometry.PI), Point1S.of(1.75 * Geometry.PI));
+
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Point1S.of(0.51 * Geometry.PI), Point1S.of(0.75 * Geometry.PI),
+ Point1S.of(0.99 * Geometry.PI));
+ }
+
+ @Test
+ public void testToConvex_lessThanPi() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(0, Geometry.HALF_PI, TEST_PRECISION);
+
+ //act
+ List<AngularInterval.Convex> result = interval.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+ checkInterval(interval, 0, Geometry.HALF_PI);
+ }
+
+ @Test
+ public void testToConvex_equalToPi() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.PI, Geometry.TWO_PI, TEST_PRECISION);
+
+ //act
+ List<AngularInterval.Convex> result = interval.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+ checkInterval(interval, Geometry.PI, Geometry.TWO_PI);
+ }
+
+ @Test
+ public void testToConvex_overPi() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.PI, Geometry.HALF_PI, TEST_PRECISION);
+
+ // act
+ List<AngularInterval.Convex> result = interval.toConvex();
+
+ // assert
+ Assert.assertEquals(2, result.size());
+ checkInterval(result.get(0), Geometry.PI, 1.75 * Geometry.PI);
+ checkInterval(result.get(1), 1.75 * Geometry.PI, 2.5 * Geometry.PI);
+ }
+
+ @Test
+ public void testToConvex_overPi_splitAtZero() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(1.25 * Geometry.PI, 2.75 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ List<AngularInterval.Convex> result = interval.toConvex();
+
+ // assert
+ Assert.assertEquals(2, result.size());
+ checkInterval(result.get(0), 1.25 * Geometry.PI, Geometry.TWO_PI);
+ checkInterval(result.get(1), Geometry.TWO_PI, 2.75 * Geometry.PI);
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ AngularInterval interval = AngularInterval.full();
+ CutAngle pt = CutAngle.createNegativeFacing(Geometry.HALF_PI, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S> split = interval.split(pt);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ checkClassify(minus, RegionLocation.BOUNDARY, Point1S.of(Geometry.HALF_PI));
+ checkClassify(minus, RegionLocation.INSIDE,
+ Point1S.PI, Point1S.of(Geometry.MINUS_HALF_PI), Point1S.of(-0.25 * Geometry.PI));
+ checkClassify(minus, RegionLocation.OUTSIDE,
+ Point1S.ZERO, Point1S.of(0.25 * Geometry.PI));
+
+ RegionBSPTree1S plus = split.getPlus();
+ checkClassify(plus, RegionLocation.BOUNDARY, Point1S.of(Geometry.HALF_PI));
+ checkClassify(plus, RegionLocation.INSIDE,
+ Point1S.ZERO, Point1S.of(0.25 * Geometry.PI));
+ checkClassify(plus, RegionLocation.OUTSIDE,
+ Point1S.PI, Point1S.of(Geometry.MINUS_HALF_PI), Point1S.of(-0.25 * Geometry.PI));
+ }
+
+ @Test
+ public void testSplit_interval_both() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION);
+ CutAngle cut = CutAngle.createNegativeFacing(0.75 * Geometry.PI, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S> split = interval.split(cut);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ checkClassify(minus, RegionLocation.BOUNDARY, Point1S.of(Geometry.PI), cut.getPoint());
+ checkClassify(minus, RegionLocation.INSIDE, Point1S.of(0.8 * Geometry.PI));
+ checkClassify(minus, RegionLocation.OUTSIDE,
+ Point1S.ZERO, Point1S.of(Geometry.TWO_PI), Point1S.of(Geometry.MINUS_HALF_PI),
+ Point1S.of(0.7 * Geometry.PI));
+
+ RegionBSPTree1S plus = split.getPlus();
+ checkClassify(plus, RegionLocation.BOUNDARY, Point1S.of(Geometry.HALF_PI), cut.getPoint());
+ checkClassify(plus, RegionLocation.INSIDE, Point1S.of(0.6 * Geometry.PI));
+ checkClassify(plus, RegionLocation.OUTSIDE,
+ Point1S.ZERO, Point1S.of(Geometry.TWO_PI), Point1S.of(Geometry.MINUS_HALF_PI),
+ Point1S.of(0.8 * Geometry.PI));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(1, 2, TEST_PRECISION);
+
+ // act
+ String str = interval.toString();
+
+ // assert
+ Assert.assertTrue(str.contains("AngularInterval"));
+ Assert.assertTrue(str.contains("min= 1.0"));
+ Assert.assertTrue(str.contains("max= 2.0"));
+ }
+
+ @Test
+ public void testConvex_of_doubles() {
+ // act/assert
+ checkInterval(AngularInterval.Convex.of(0, 1, TEST_PRECISION), 0, 1);
+ checkInterval(AngularInterval.Convex.of(0, Geometry.PI, TEST_PRECISION), 0, Geometry.PI);
+ checkInterval(AngularInterval.Convex.of(Geometry.PI + 2, 1, TEST_PRECISION), Geometry.PI + 2, Geometry.TWO_PI + 1);
+ checkInterval(AngularInterval.Convex.of(-2, -1.5, TEST_PRECISION), -2, -1.5);
+
+ checkFull(AngularInterval.Convex.of(1, 1, TEST_PRECISION));
+ checkFull(AngularInterval.Convex.of(0, 1e-11, TEST_PRECISION));
+ checkFull(AngularInterval.Convex.of(0, -1e-11, TEST_PRECISION));
+ checkFull(AngularInterval.Convex.of(0, Geometry.TWO_PI, TEST_PRECISION));
+ }
+
+ @Test
+ public void testConvex_of_doubles_invalidArgs() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(0, Geometry.PI + 1e-1, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(Geometry.HALF_PI, Geometry.MINUS_HALF_PI + 1, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(0, -0.5, TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testConvex_of_points() {
+ // act/assert
+ checkInterval(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(1), TEST_PRECISION), 0, 1);
+ checkInterval(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(Geometry.PI), TEST_PRECISION),
+ 0, Geometry.PI);
+ checkInterval(AngularInterval.Convex.of(Point1S.of(Geometry.PI + 2), Point1S.of(1), TEST_PRECISION),
+ Geometry.PI + 2, Geometry.TWO_PI + 1);
+ checkInterval(AngularInterval.Convex.of(Point1S.of(-2), Point1S.of(-1.5), TEST_PRECISION), -2, -1.5);
+
+ checkFull(AngularInterval.Convex.of(Point1S.of(1), Point1S.of(1), TEST_PRECISION));
+ checkFull(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(1e-11), TEST_PRECISION));
+ checkFull(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(-1e-11), TEST_PRECISION));
+ checkFull(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(Geometry.TWO_PI), TEST_PRECISION));
+ }
+
+ @Test
+ public void testConvex_of_points_invalidArgs() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(Point1S.of(Double.NEGATIVE_INFINITY),
+ Point1S.of(Double.POSITIVE_INFINITY), TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(Point1S.of(0), Point1S.of(Geometry.PI + 1e-1), TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(Point1S.of(Geometry.HALF_PI),
+ Point1S.of(Geometry.MINUS_HALF_PI + 1), TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(Point1S.of(0), Point1S.of(-0.5), TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testConvex_of_cutAngles() {
+ // arrange
+ DoublePrecisionContext precisionA = new EpsilonDoublePrecisionContext(1e-3);
+ DoublePrecisionContext precisionB = new EpsilonDoublePrecisionContext(1e-2);
+
+ CutAngle zeroPos = CutAngle.createPositiveFacing(Point1S.ZERO, precisionA);
+ CutAngle zeroNeg = CutAngle.createNegativeFacing(Point1S.ZERO, precisionA);
+
+ CutAngle piPos = CutAngle.createPositiveFacing(Point1S.PI, precisionA);
+ CutAngle piNeg = CutAngle.createNegativeFacing(Point1S.PI, precisionA);
+
+ CutAngle almostPiPos = CutAngle.createPositiveFacing(Point1S.of(Geometry.PI + 5e-3), precisionB);
+
+ // act/assert
+ checkInterval(AngularInterval.Convex.of(zeroNeg, piPos), 0, Geometry.PI);
+ checkInterval(AngularInterval.Convex.of(zeroPos, piNeg), Geometry.PI, Geometry.TWO_PI);
+
+ checkFull(AngularInterval.Convex.of(zeroPos, zeroNeg));
+ checkFull(AngularInterval.Convex.of(zeroPos, piPos));
+ checkFull(AngularInterval.Convex.of(piNeg, zeroNeg));
+
+ checkFull(AngularInterval.Convex.of(almostPiPos, piNeg));
+ checkFull(AngularInterval.Convex.of(piNeg, almostPiPos));
+ }
+
+ @Test
+ public void testConvex_of_cutAngles_invalidArgs() {
+ // arrange
+ CutAngle pt = CutAngle.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
+ CutAngle nan = CutAngle.createPositiveFacing(Point1S.NaN, TEST_PRECISION);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(pt, nan);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(nan, pt);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(nan, nan);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ AngularInterval.Convex.of(
+ CutAngle.createNegativeFacing(1, TEST_PRECISION),
+ CutAngle.createPositiveFacing(0.5, TEST_PRECISION));
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testConvex_toConvex() {
+ // arrange
+ AngularInterval.Convex full = AngularInterval.full();
+ AngularInterval.Convex interval = AngularInterval.Convex.of(0, 1, TEST_PRECISION);
+
+ List<AngularInterval.Convex> result;
+
+ // act/assert
+ result = full.toConvex();
+ Assert.assertEquals(1, result.size());
+ Assert.assertSame(full, result.get(0));
+
+ result = interval.toConvex();
+ Assert.assertEquals(1, result.size());
+ Assert.assertSame(interval, result.get(0));
+ }
+
+ @Test
+ public void testSplitDiameter_full() {
+ // arrange
+ AngularInterval.Convex full = AngularInterval.full();
+ CutAngle splitter = CutAngle.createPositiveFacing(Point1S.of(Geometry.HALF_PI), TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = full.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), 1.5 * Geometry.PI, 2.5 * Geometry.PI);
+ checkInterval(split.getPlus(), 0.5 * Geometry.PI, 1.5 * Geometry.PI);
+ }
+
+ @Test
+ public void testSplitDiameter_full_splitOnZero() {
+ // arrange
+ AngularInterval.Convex full = AngularInterval.full();
+ CutAngle splitter = CutAngle.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = full.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), 0, Geometry.PI);
+ checkInterval(split.getPlus(), Geometry.PI, Geometry.TWO_PI);
+ }
+
+ @Test
+ public void testSplitDiameter_minus() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(0.1, Geometry.HALF_PI, TEST_PRECISION);
+ CutAngle splitter = CutAngle.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(interval, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplitDiameter_plus() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(-0.4 * Geometry.PI, 0.4 * Geometry.PI, TEST_PRECISION);
+ CutAngle splitter = CutAngle.createNegativeFacing(Point1S.of(Geometry.HALF_PI), TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(interval, split.getPlus());
+ }
+
+ @Test
+ public void testSplitDiameter_both_negativeFacingSplitter() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(Geometry.HALF_PI, Geometry.MINUS_HALF_PI, TEST_PRECISION);
+ CutAngle splitter = CutAngle.createNegativeFacing(Point1S.of(Geometry.PI), TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), Geometry.PI, 1.5 * Geometry.PI);
+ checkInterval(split.getPlus(), Geometry.HALF_PI, Geometry.PI);
+ }
+
+ @Test
+ public void testSplitDiameter_both_positiveFacingSplitter() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(Geometry.HALF_PI, Geometry.MINUS_HALF_PI, TEST_PRECISION);
+ CutAngle splitter = CutAngle.createPositiveFacing(Point1S.of(Geometry.PI), TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), Geometry.HALF_PI, Geometry.PI);
+ checkInterval(split.getPlus(), Geometry.PI, 1.5 * Geometry.PI);
+ }
+
+ @Test
+ public void testSplitDiameter_both_antipodal_negativeFacingSplitter() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(Geometry.HALF_PI, Geometry.MINUS_HALF_PI, TEST_PRECISION);
+ CutAngle splitter = CutAngle.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), Geometry.HALF_PI, Geometry.PI);
+ checkInterval(split.getPlus(), Geometry.PI, 1.5 * Geometry.PI);
+ }
+
+ @Test
+ public void testSplitDiameter_both_antipodal_positiveFacingSplitter() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(Geometry.HALF_PI, Geometry.MINUS_HALF_PI, TEST_PRECISION);
+ CutAngle splitter = CutAngle.createPositiveFacing(Point1S.ZERO, TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ checkInterval(split.getMinus(), Geometry.PI, 1.5 * Geometry.PI);
+ checkInterval(split.getPlus(), Geometry.HALF_PI, Geometry.PI);
+ }
+
+ @Test
+ public void testSplitDiameter_splitOnBoundary_negativeFacing() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(Geometry.HALF_PI, Geometry.MINUS_HALF_PI, TEST_PRECISION);
+ CutAngle splitter = CutAngle.createNegativeFacing(Point1S.of(Geometry.HALF_PI), TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(interval, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplitDiameter_splitOnBoundary_positiveFacing() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(0, Geometry.PI, TEST_PRECISION);
+ CutAngle splitter = CutAngle.createPositiveFacing(Point1S.of(Geometry.PI), TEST_PRECISION);
+
+ // act
+ Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(interval, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testConvex_transform() {
+ // arrange
+ AngularInterval.Convex interval = AngularInterval.Convex.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION);
+
+ Transform1S rotate = Transform1S.createRotation(Geometry.HALF_PI);
+ Transform1S invert = Transform1S.createNegation().rotate(Geometry.HALF_PI);
+
+ // act/assert
+ checkInterval(interval.transform(rotate), Geometry.PI, 1.5 * Geometry.PI);
+ checkInterval(interval.transform(invert), -0.5 * Geometry.PI, Geometry.ZERO_PI);
+ }
+
+ private static void checkFull(AngularInterval interval) {
+ Assert.assertTrue(interval.isFull());
+ Assert.assertFalse(interval.isEmpty());
+
+ Assert.assertNull(interval.getMinBoundary());
+ Assert.assertEquals(0, interval.getMin(), TEST_EPS);
+ Assert.assertNull(interval.getMaxBoundary());
+ Assert.assertEquals(Geometry.TWO_PI, interval.getMax(), TEST_EPS);
+
+ Assert.assertNull(interval.getBarycenter());
+ Assert.assertNull(interval.getMidPoint());
+
+ Assert.assertEquals(Geometry.TWO_PI, interval.getSize(), TEST_EPS);
+ Assert.assertEquals(0, interval.getBoundarySize(), TEST_EPS);
+
+ checkClassify(interval, RegionLocation.INSIDE, Point1S.ZERO, Point1S.of(Geometry.PI));
+ }
+
+ private static void checkInterval(AngularInterval interval, double min, double max) {
+
+ Assert.assertFalse(interval.isFull());
+ Assert.assertFalse(interval.isEmpty());
+
+ CutAngle minBoundary = interval.getMinBoundary();
+ Assert.assertEquals(min, minBoundary.getAzimuth(), TEST_EPS);
+ Assert.assertFalse(minBoundary.isPositiveFacing());
+
+ CutAngle maxBoundary = interval.getMaxBoundary();
+ Assert.assertEquals(max, maxBoundary.getAzimuth(), TEST_EPS);
+ Assert.assertTrue(maxBoundary.isPositiveFacing());
+
+ Assert.assertEquals(min, interval.getMin(), TEST_EPS);
+ Assert.assertEquals(max, interval.getMax(), TEST_EPS);
+
+ Assert.assertEquals(0.5 * (max + min), interval.getMidPoint().getAzimuth(), TEST_EPS);
+ Assert.assertSame(interval.getMidPoint(), interval.getBarycenter());
+
+ Assert.assertEquals(0, interval.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(max - min, interval.getSize(), TEST_EPS);
+
+ checkClassify(interval, RegionLocation.INSIDE, interval.getMidPoint());
+ checkClassify(interval, RegionLocation.BOUNDARY,
+ interval.getMinBoundary().getPoint(), interval.getMaxBoundary().getPoint());
+ checkClassify(interval, RegionLocation.OUTSIDE, Point1S.of(interval.getMidPoint().getAzimuth() + Geometry.PI));
+ }
+
+ private static void checkClassify(Region<Point1S> region, RegionLocation loc, Point1S ... pts) {
+ for (Point1S pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, loc, region.classify(pt));
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/ArcTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/ArcTest.java
deleted file mode 100644
index 358eac3..0000000
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/ArcTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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.oned;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.numbers.core.Precision;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class ArcTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testArc() {
- Arc arc = new Arc(2.3, 5.7, TEST_PRECISION);
- Assert.assertEquals(3.4, arc.getSize(), TEST_EPS);
- Assert.assertEquals(4.0, arc.getBarycenter(), TEST_EPS);
- Assert.assertEquals(Region.Location.BOUNDARY, arc.checkPoint(2.3));
- Assert.assertEquals(Region.Location.BOUNDARY, arc.checkPoint(5.7));
- Assert.assertEquals(Region.Location.OUTSIDE, arc.checkPoint(1.2));
- Assert.assertEquals(Region.Location.OUTSIDE, arc.checkPoint(8.5));
- Assert.assertEquals(Region.Location.INSIDE, arc.checkPoint(8.7));
- Assert.assertEquals(Region.Location.INSIDE, arc.checkPoint(3.0));
- Assert.assertEquals(2.3, arc.getInf(), TEST_EPS);
- Assert.assertEquals(5.7, arc.getSup(), TEST_EPS);
- Assert.assertEquals(4.0, arc.getBarycenter(), TEST_EPS);
- Assert.assertEquals(3.4, arc.getSize(), TEST_EPS);
- }
-
- @Test(expected=IllegalArgumentException.class)
- public void testWrongInterval() {
- new Arc(1.2, 0.0, TEST_PRECISION);
- }
-
- @Test
- public void testTolerance() {
- Assert.assertEquals(Region.Location.OUTSIDE, new Arc(2.3, 5.7, createPrecision(1.0)).checkPoint(1.2));
- Assert.assertEquals(Region.Location.BOUNDARY, new Arc(2.3, 5.7, createPrecision(1.2)).checkPoint(1.2));
- Assert.assertEquals(Region.Location.OUTSIDE, new Arc(2.3, 5.7, createPrecision(0.7)).checkPoint(6.5));
- Assert.assertEquals(Region.Location.BOUNDARY, new Arc(2.3, 5.7, createPrecision(0.9)).checkPoint(6.5));
- Assert.assertEquals(Region.Location.INSIDE, new Arc(2.3, 5.7, createPrecision(0.6)).checkPoint(3.0));
- Assert.assertEquals(Region.Location.BOUNDARY, new Arc(2.3, 5.7, createPrecision(0.8)).checkPoint(3.0));
- }
-
- @Test
- public void testFullCircle() {
- Arc arc = new Arc(9.0, 9.0, TEST_PRECISION);
- // no boundaries on a full circle
- Assert.assertEquals(Region.Location.INSIDE, arc.checkPoint(9.0));
- Assert.assertEquals(.0, arc.getInf(), TEST_EPS);
- Assert.assertEquals(Geometry.TWO_PI, arc.getSup(), TEST_EPS);
- Assert.assertEquals(2.0 * Math.PI, arc.getSize(), TEST_EPS);
- for (double alpha = -20.0; alpha <= 20.0; alpha += 0.1) {
- Assert.assertEquals(Region.Location.INSIDE, arc.checkPoint(alpha));
- }
- }
-
- @Test
- public void testSmall() {
- Arc arc = new Arc(1.0, Math.nextAfter(1.0, Double.POSITIVE_INFINITY), createPrecision(Precision.EPSILON));
- Assert.assertEquals(2 * Precision.EPSILON, arc.getSize(), Precision.SAFE_MIN);
- Assert.assertEquals(1.0, arc.getBarycenter(), Precision.EPSILON);
- }
-
- /** Create a {@link DoublePrecisionContext} with the given epsilon value.
- * @param eps epsilon value
- * @return new precision context
- */
- private static DoublePrecisionContext createPrecision(final double eps) {
- return new EpsilonDoublePrecisionContext(eps);
- }
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/ArcsSetTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/ArcsSetTest.java
deleted file mode 100644
index 1171665..0000000
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/ArcsSetTest.java
+++ /dev/null
@@ -1,599 +0,0 @@
-/*
- * 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.oned;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.partitioning.BSPTree;
-import org.apache.commons.geometry.core.partitioning.Region;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.Side;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.numbers.core.Precision;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class ArcsSetTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testArc() {
- ArcsSet set = new ArcsSet(2.3, 5.7, TEST_PRECISION);
- Assert.assertEquals(3.4, set.getSize(), TEST_EPS);
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(S1Point.of(2.3)));
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(S1Point.of(5.7)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(S1Point.of(1.2)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(S1Point.of(8.5)));
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(8.7)));
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(3.0)));
- Assert.assertEquals(1, set.asList().size());
- Assert.assertEquals(2.3, set.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.7, set.asList().get(0).getSup(), TEST_EPS);
- }
-
- @Test
- public void testWrapAround2PiArc() {
- ArcsSet set = new ArcsSet(5.7 - Geometry.TWO_PI, 2.3, TEST_PRECISION);
- Assert.assertEquals(Geometry.TWO_PI - 3.4, set.getSize(), TEST_EPS);
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(S1Point.of(2.3)));
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(S1Point.of(5.7)));
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(1.2)));
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(8.5)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(S1Point.of(8.7)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(S1Point.of(3.0)));
- Assert.assertEquals(1, set.asList().size());
- Assert.assertEquals(5.7, set.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(2.3 + Geometry.TWO_PI, set.asList().get(0).getSup(), TEST_EPS);
- }
-
- @Test
- public void testSplitOver2Pi() {
- ArcsSet set = new ArcsSet(TEST_PRECISION);
- Arc arc = new Arc(1.5 * Math.PI, 2.5 * Math.PI, TEST_PRECISION);
- ArcsSet.Split split = set.split(arc);
- for (double alpha = 0.0; alpha <= Geometry.TWO_PI; alpha += 0.01) {
- S1Point p = S1Point.of(alpha);
- if (alpha < 0.5 * Math.PI || alpha > 1.5 * Math.PI) {
- Assert.assertEquals(Location.OUTSIDE, split.getPlus().checkPoint(p));
- Assert.assertEquals(Location.INSIDE, split.getMinus().checkPoint(p));
- } else {
- Assert.assertEquals(Location.INSIDE, split.getPlus().checkPoint(p));
- Assert.assertEquals(Location.OUTSIDE, split.getMinus().checkPoint(p));
- }
- }
- }
-
- @Test
- public void testSplitAtEnd() {
- ArcsSet set = new ArcsSet(TEST_PRECISION);
- Arc arc = new Arc(Math.PI, Geometry.TWO_PI, TEST_PRECISION);
- ArcsSet.Split split = set.split(arc);
- for (double alpha = 0.01; alpha < Geometry.TWO_PI; alpha += 0.01) {
- S1Point p = S1Point.of(alpha);
- if (alpha > Math.PI) {
- Assert.assertEquals(Location.OUTSIDE, split.getPlus().checkPoint(p));
- Assert.assertEquals(Location.INSIDE, split.getMinus().checkPoint(p));
- } else {
- Assert.assertEquals(Location.INSIDE, split.getPlus().checkPoint(p));
- Assert.assertEquals(Location.OUTSIDE, split.getMinus().checkPoint(p));
- }
- }
-
- S1Point zero = S1Point.of(0.0);
- Assert.assertEquals(Location.BOUNDARY, split.getPlus().checkPoint(zero));
- Assert.assertEquals(Location.BOUNDARY, split.getMinus().checkPoint(zero));
-
- S1Point pi = S1Point.of(Math.PI);
- Assert.assertEquals(Location.BOUNDARY, split.getPlus().checkPoint(pi));
- Assert.assertEquals(Location.BOUNDARY, split.getMinus().checkPoint(pi));
-
- }
-
- @Test(expected=IllegalArgumentException.class)
- public void testWrongInterval() {
- new ArcsSet(1.2, 0.0, TEST_PRECISION);
- }
-
- @Test
- public void testFullEqualEndPoints() {
- ArcsSet set = new ArcsSet(1.0, 1.0, TEST_PRECISION);
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(9.0)));
- for (double alpha = -20.0; alpha <= 20.0; alpha += 0.1) {
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(alpha)));
- }
- Assert.assertEquals(1, set.asList().size());
- Assert.assertEquals(0.0, set.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(2 * Math.PI, set.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(2 * Math.PI, set.getSize(), TEST_EPS);
- }
-
- @Test
- public void testFullCircle() {
- ArcsSet set = new ArcsSet(TEST_PRECISION);
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(9.0)));
- for (double alpha = -20.0; alpha <= 20.0; alpha += 0.1) {
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(alpha)));
- }
- Assert.assertEquals(1, set.asList().size());
- Assert.assertEquals(0.0, set.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(2 * Math.PI, set.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(2 * Math.PI, set.getSize(), TEST_EPS);
- }
-
- @Test
- public void testEmpty() {
- ArcsSet empty = (ArcsSet) new RegionFactory<S1Point>().getComplement(new ArcsSet(TEST_PRECISION));
- Assert.assertSame(TEST_PRECISION, empty.getPrecision());
- Assert.assertEquals(0.0, empty.getSize(), TEST_EPS);
- Assert.assertTrue(empty.asList().isEmpty());
- }
-
- @Test
- public void testTiny() {
- ArcsSet tiny = new ArcsSet(0.0, Precision.SAFE_MIN / 2, TEST_PRECISION);
- Assert.assertSame(TEST_PRECISION, tiny.getPrecision());
- Assert.assertEquals(Precision.SAFE_MIN / 2, tiny.getSize(), TEST_EPS);
- Assert.assertEquals(1, tiny.asList().size());
- Assert.assertEquals(0.0, tiny.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(Precision.SAFE_MIN / 2, tiny.asList().get(0).getSup(), TEST_EPS);
- }
-
- @Test
- public void testSpecialConstruction() {
- List<SubHyperplane<S1Point>> boundary = new ArrayList<>();
- boundary.add(new LimitAngle(S1Point.of(0.0), false, TEST_PRECISION).wholeHyperplane());
- boundary.add(new LimitAngle(S1Point.of(Geometry.TWO_PI - 1.0e-11), true, TEST_PRECISION).wholeHyperplane());
- ArcsSet set = new ArcsSet(boundary, TEST_PRECISION);
- Assert.assertEquals(Geometry.TWO_PI, set.getSize(), TEST_EPS);
- Assert.assertSame(TEST_PRECISION, set.getPrecision());
- Assert.assertEquals(1, set.asList().size());
- Assert.assertEquals(0.0, set.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(Geometry.TWO_PI, set.asList().get(0).getSup(), TEST_EPS);
- }
-
- @Test
- public void testDifference() {
-
- ArcsSet a = new ArcsSet(1.0, 6.0, TEST_PRECISION);
- List<Arc> aList = a.asList();
- Assert.assertEquals(1, aList.size());
- Assert.assertEquals(1.0, aList.get(0).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, aList.get(0).getSup(), TEST_EPS);
-
- ArcsSet b = new ArcsSet(3.0, 5.0, TEST_PRECISION);
- List<Arc> bList = b.asList();
- Assert.assertEquals(1, bList.size());
- Assert.assertEquals(3.0, bList.get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, bList.get(0).getSup(), TEST_EPS);
-
- ArcsSet aMb = (ArcsSet) new RegionFactory<S1Point>().difference(a, b);
- for (int k = -2; k < 3; ++k) {
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(0.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(0.9 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.BOUNDARY, aMb.checkPoint(S1Point.of(1.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.INSIDE, aMb.checkPoint(S1Point.of(1.1 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.INSIDE, aMb.checkPoint(S1Point.of(2.9 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.BOUNDARY, aMb.checkPoint(S1Point.of(3.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(3.1 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(4.9 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.BOUNDARY, aMb.checkPoint(S1Point.of(5.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.INSIDE, aMb.checkPoint(S1Point.of(5.1 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.INSIDE, aMb.checkPoint(S1Point.of(5.9 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.BOUNDARY, aMb.checkPoint(S1Point.of(6.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(6.1 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(6.2 + k * Geometry.TWO_PI)));
- }
-
- List<Arc> aMbList = aMb.asList();
- Assert.assertEquals(2, aMbList.size());
- Assert.assertEquals(1.0, aMbList.get(0).getInf(), TEST_EPS);
- Assert.assertEquals(3.0, aMbList.get(0).getSup(), TEST_EPS);
- Assert.assertEquals(5.0, aMbList.get(1).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, aMbList.get(1).getSup(), TEST_EPS);
-
-
- }
-
- @Test
- public void testIntersection() {
-
- ArcsSet a = (ArcsSet) new RegionFactory<S1Point>().union(new ArcsSet(1.0, 3.0, TEST_PRECISION),
- new ArcsSet(5.0, 6.0, TEST_PRECISION));
- List<Arc> aList = a.asList();
- Assert.assertEquals(2, aList.size());
- Assert.assertEquals(1.0, aList.get(0).getInf(), TEST_EPS);
- Assert.assertEquals(3.0, aList.get(0).getSup(), TEST_EPS);
- Assert.assertEquals(5.0, aList.get(1).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, aList.get(1).getSup(), TEST_EPS);
-
- ArcsSet b = new ArcsSet(0.0, 5.5, TEST_PRECISION);
- List<Arc> bList = b.asList();
- Assert.assertEquals(1, bList.size());
- Assert.assertEquals(0.0, bList.get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.5, bList.get(0).getSup(), TEST_EPS);
-
- ArcsSet aMb = (ArcsSet) new RegionFactory<S1Point>().intersection(a, b);
- for (int k = -2; k < 3; ++k) {
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(0.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.BOUNDARY, aMb.checkPoint(S1Point.of(1.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.INSIDE, aMb.checkPoint(S1Point.of(1.1 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.INSIDE, aMb.checkPoint(S1Point.of(2.9 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.BOUNDARY, aMb.checkPoint(S1Point.of(3.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(3.1 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(4.9 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.BOUNDARY, aMb.checkPoint(S1Point.of(5.0 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.INSIDE, aMb.checkPoint(S1Point.of(5.1 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.INSIDE, aMb.checkPoint(S1Point.of(5.4 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.BOUNDARY, aMb.checkPoint(S1Point.of(5.5 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(5.6 + k * Geometry.TWO_PI)));
- Assert.assertEquals(Location.OUTSIDE, aMb.checkPoint(S1Point.of(6.2 + k * Geometry.TWO_PI)));
- }
-
- List<Arc> aMbList = aMb.asList();
- Assert.assertEquals(2, aMbList.size());
- Assert.assertEquals(1.0, aMbList.get(0).getInf(), TEST_EPS);
- Assert.assertEquals(3.0, aMbList.get(0).getSup(), TEST_EPS);
- Assert.assertEquals(5.0, aMbList.get(1).getInf(), TEST_EPS);
- Assert.assertEquals(5.5, aMbList.get(1).getSup(), TEST_EPS);
-
-
- }
-
- @Test
- public void testMultiple() {
- RegionFactory<S1Point> factory = new RegionFactory<>();
- ArcsSet set = (ArcsSet)
- factory.intersection(factory.union(factory.difference(new ArcsSet(1.0, 6.0, TEST_PRECISION),
- new ArcsSet(3.0, 5.0, TEST_PRECISION)),
- new ArcsSet(0.5, 2.0, TEST_PRECISION)),
- new ArcsSet(0.0, 5.5, TEST_PRECISION));
- Assert.assertEquals(3.0, set.getSize(), TEST_EPS);
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(S1Point.of(0.0)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(S1Point.of(4.0)));
- Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(S1Point.of(6.0)));
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(1.2)));
- Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(S1Point.of(5.25)));
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(S1Point.of(0.5)));
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(S1Point.of(3.0)));
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(S1Point.of(5.0)));
- Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(S1Point.of(5.5)));
-
- List<Arc> list = set.asList();
- Assert.assertEquals(2, list.size());
- Assert.assertEquals( 0.5, list.get(0).getInf(), TEST_EPS);
- Assert.assertEquals( 3.0, list.get(0).getSup(), TEST_EPS);
- Assert.assertEquals( 5.0, list.get(1).getInf(), TEST_EPS);
- Assert.assertEquals( 5.5, list.get(1).getSup(), TEST_EPS);
-
- }
-
- @Test
- public void testSinglePoint() {
- ArcsSet set = new ArcsSet(1.0, Math.nextAfter(1.0, Double.POSITIVE_INFINITY), TEST_PRECISION);
- Assert.assertEquals(2 * Precision.EPSILON, set.getSize(), Precision.SAFE_MIN);
- }
-
- @Test
- public void testIteration() {
- ArcsSet set = (ArcsSet) new RegionFactory<S1Point>().difference(new ArcsSet(1.0, 6.0, TEST_PRECISION),
- new ArcsSet(3.0, 5.0, TEST_PRECISION));
- Iterator<double[]> iterator = set.iterator();
- try {
- iterator.remove();
- Assert.fail("an exception should have been thrown");
- } catch (UnsupportedOperationException uoe) {
- // expected
- }
-
- Assert.assertTrue(iterator.hasNext());
- double[] a0 = iterator.next();
- Assert.assertEquals(2, a0.length);
- Assert.assertEquals(1.0, a0[0], TEST_EPS);
- Assert.assertEquals(3.0, a0[1], TEST_EPS);
-
- Assert.assertTrue(iterator.hasNext());
- double[] a1 = iterator.next();
- Assert.assertEquals(2, a1.length);
- Assert.assertEquals(5.0, a1[0], TEST_EPS);
- Assert.assertEquals(6.0, a1[1], TEST_EPS);
-
- Assert.assertFalse(iterator.hasNext());
- try {
- iterator.next();
- Assert.fail("an exception should have been thrown");
- } catch (NoSuchElementException nsee) {
- // expected
- }
-
- }
-
- @Test
- public void testEmptyTree() {
- Assert.assertEquals(Geometry.TWO_PI, new ArcsSet(new BSPTree<S1Point>(Boolean.TRUE), TEST_PRECISION).getSize(), TEST_EPS);
- }
-
- @Test
- public void testShiftedAngles() {
- for (int k = -2; k < 3; ++k) {
- SubLimitAngle l1 = new LimitAngle(S1Point.of(1.0 + k * Geometry.TWO_PI), false, TEST_PRECISION).wholeHyperplane();
- SubLimitAngle l2 = new LimitAngle(S1Point.of(1.5 + k * Geometry.TWO_PI), true, TEST_PRECISION).wholeHyperplane();
- ArcsSet set = new ArcsSet(new BSPTree<>(l1,
- new BSPTree<S1Point>(Boolean.FALSE),
- new BSPTree<>(l2,
- new BSPTree<S1Point>(Boolean.FALSE),
- new BSPTree<S1Point>(Boolean.TRUE),
- null),
- null),
- TEST_PRECISION);
- for (double alpha = 1.0e-6; alpha < Geometry.TWO_PI; alpha += 0.001) {
- if (alpha < 1 || alpha > 1.5) {
- Assert.assertEquals(Location.OUTSIDE, set.checkPoint(S1Point.of(alpha)));
- } else {
- Assert.assertEquals(Location.INSIDE, set.checkPoint(S1Point.of(alpha)));
- }
- }
- }
-
- }
-
- @Test(expected=IllegalArgumentException.class)
- public void testInconsistentState() {
- SubLimitAngle l1 = new LimitAngle(S1Point.of(1.0), false, TEST_PRECISION).wholeHyperplane();
- SubLimitAngle l2 = new LimitAngle(S1Point.of(2.0), true, TEST_PRECISION).wholeHyperplane();
- SubLimitAngle l3 = new LimitAngle(S1Point.of(3.0), false, TEST_PRECISION).wholeHyperplane();
- new ArcsSet(new BSPTree<>(l1,
- new BSPTree<S1Point>(Boolean.FALSE),
- new BSPTree<>(l2,
- new BSPTree<>(l3,
- new BSPTree<S1Point>(Boolean.FALSE),
- new BSPTree<S1Point>(Boolean.TRUE),
- null),
- new BSPTree<S1Point>(Boolean.TRUE),
- null),
- null),
- TEST_PRECISION);
- }
-
- @Test
- public void testSide() {
- ArcsSet set = (ArcsSet) new RegionFactory<S1Point>().difference(new ArcsSet(1.0, 6.0, TEST_PRECISION),
- new ArcsSet(3.0, 5.0, TEST_PRECISION));
- for (int k = -2; k < 3; ++k) {
- Assert.assertEquals(Side.MINUS, set.split(new Arc(0.5 + k * Geometry.TWO_PI,
- 6.1 + k * Geometry.TWO_PI,
- set.getPrecision())).getSide());
- Assert.assertEquals(Side.PLUS, set.split(new Arc(0.5 + k * Geometry.TWO_PI,
- 0.8 + k * Geometry.TWO_PI,
- set.getPrecision())).getSide());
- Assert.assertEquals(Side.PLUS, set.split(new Arc(6.2 + k * Geometry.TWO_PI,
- 6.3 + k * Geometry.TWO_PI,
- set.getPrecision())).getSide());
- Assert.assertEquals(Side.PLUS, set.split(new Arc(3.5 + k * Geometry.TWO_PI,
- 4.5 + k * Geometry.TWO_PI,
- set.getPrecision())).getSide());
- Assert.assertEquals(Side.BOTH, set.split(new Arc(2.9 + k * Geometry.TWO_PI,
- 4.5 + k * Geometry.TWO_PI,
- set.getPrecision())).getSide());
- Assert.assertEquals(Side.BOTH, set.split(new Arc(0.5 + k * Geometry.TWO_PI,
- 1.2 + k * Geometry.TWO_PI,
- set.getPrecision())).getSide());
- Assert.assertEquals(Side.BOTH, set.split(new Arc(0.5 + k * Geometry.TWO_PI,
- 5.9 + k * Geometry.TWO_PI,
- set.getPrecision())).getSide());
- }
- }
-
- @Test
- public void testSideEmbedded() {
-
- ArcsSet s35 = new ArcsSet(3.0, 5.0, TEST_PRECISION);
- ArcsSet s16 = new ArcsSet(1.0, 6.0, TEST_PRECISION);
-
- Assert.assertEquals(Side.BOTH, s16.split(new Arc(3.0, 5.0, TEST_PRECISION)).getSide());
- Assert.assertEquals(Side.BOTH, s16.split(new Arc(5.0, 3.0 + Geometry.TWO_PI, TEST_PRECISION)).getSide());
- Assert.assertEquals(Side.MINUS, s35.split(new Arc(1.0, 6.0, TEST_PRECISION)).getSide());
- Assert.assertEquals(Side.PLUS, s35.split(new Arc(6.0, 1.0 + Geometry.TWO_PI, TEST_PRECISION)).getSide());
-
- }
-
- @Test
- public void testSideOverlapping() {
- ArcsSet s35 = new ArcsSet(3.0, 5.0, TEST_PRECISION);
- ArcsSet s46 = new ArcsSet(4.0, 6.0, TEST_PRECISION);
-
- Assert.assertEquals(Side.BOTH, s46.split(new Arc(3.0, 5.0, TEST_PRECISION)).getSide());
- Assert.assertEquals(Side.BOTH, s46.split(new Arc(5.0, 3.0 + Geometry.TWO_PI, TEST_PRECISION)).getSide());
- Assert.assertEquals(Side.BOTH, s35.split(new Arc(4.0, 6.0, TEST_PRECISION)).getSide());
- Assert.assertEquals(Side.BOTH, s35.split(new Arc(6.0, 4.0 + Geometry.TWO_PI, TEST_PRECISION)).getSide());
- }
-
- @Test
- public void testSideHyper() {
- ArcsSet sub = (ArcsSet) new RegionFactory<S1Point>().getComplement(new ArcsSet(TEST_PRECISION));
- Assert.assertTrue(sub.isEmpty());
- Assert.assertEquals(Side.HYPER, sub.split(new Arc(2.0, 3.0, TEST_PRECISION)).getSide());
- }
-
- @Test
- public void testSplitEmbedded() {
-
- ArcsSet s35 = new ArcsSet(3.0, 5.0, TEST_PRECISION);
- ArcsSet s16 = new ArcsSet(1.0, 6.0, TEST_PRECISION);
-
- ArcsSet.Split split1 = s16.split(new Arc(3.0, 5.0, TEST_PRECISION));
- ArcsSet split1Plus = split1.getPlus();
- ArcsSet split1Minus = split1.getMinus();
- Assert.assertEquals(3.0, split1Plus.getSize(), TEST_EPS);
- Assert.assertEquals(2, split1Plus.asList().size());
- Assert.assertEquals(1.0, split1Plus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(3.0, split1Plus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(5.0, split1Plus.asList().get(1).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, split1Plus.asList().get(1).getSup(), TEST_EPS);
- Assert.assertEquals(2.0, split1Minus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split1Minus.asList().size());
- Assert.assertEquals(3.0, split1Minus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, split1Minus.asList().get(0).getSup(), TEST_EPS);
-
- ArcsSet.Split split2 = s16.split(new Arc(5.0, 3.0 + Geometry.TWO_PI, TEST_PRECISION));
- ArcsSet split2Plus = split2.getPlus();
- ArcsSet split2Minus = split2.getMinus();
- Assert.assertEquals(2.0, split2Plus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split2Plus.asList().size());
- Assert.assertEquals(3.0, split2Plus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, split2Plus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(3.0, split2Minus.getSize(), TEST_EPS);
- Assert.assertEquals(2, split2Minus.asList().size());
- Assert.assertEquals(1.0, split2Minus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(3.0, split2Minus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(5.0, split2Minus.asList().get(1).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, split2Minus.asList().get(1).getSup(), TEST_EPS);
-
- ArcsSet.Split split3 = s35.split(new Arc(1.0, 6.0, TEST_PRECISION));
- ArcsSet split3Plus = split3.getPlus();
- ArcsSet split3Minus = split3.getMinus();
- Assert.assertNull(split3Plus);
- Assert.assertEquals(2.0, split3Minus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split3Minus.asList().size());
- Assert.assertEquals(3.0, split3Minus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, split3Minus.asList().get(0).getSup(), TEST_EPS);
-
- ArcsSet.Split split4 = s35.split(new Arc(6.0, 1.0 + Geometry.TWO_PI, TEST_PRECISION));
- ArcsSet split4Plus = split4.getPlus();
- ArcsSet split4Minus = split4.getMinus();
- Assert.assertEquals(2.0, split4Plus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split4Plus.asList().size());
- Assert.assertEquals(3.0, split4Plus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, split4Plus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertNull(split4Minus);
-
- }
-
- @Test
- public void testSplitOverlapping() {
-
- ArcsSet s35 = new ArcsSet(3.0, 5.0, TEST_PRECISION);
- ArcsSet s46 = new ArcsSet(4.0, 6.0, TEST_PRECISION);
-
- ArcsSet.Split split1 = s46.split(new Arc(3.0, 5.0, TEST_PRECISION));
- ArcsSet split1Plus = split1.getPlus();
- ArcsSet split1Minus = split1.getMinus();
- Assert.assertEquals(1.0, split1Plus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split1Plus.asList().size());
- Assert.assertEquals(5.0, split1Plus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, split1Plus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(1.0, split1Minus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split1Minus.asList().size());
- Assert.assertEquals(4.0, split1Minus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, split1Minus.asList().get(0).getSup(), TEST_EPS);
-
- ArcsSet.Split split2 = s46.split(new Arc(5.0, 3.0 + Geometry.TWO_PI, TEST_PRECISION));
- ArcsSet split2Plus = split2.getPlus();
- ArcsSet split2Minus = split2.getMinus();
- Assert.assertEquals(1.0, split2Plus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split2Plus.asList().size());
- Assert.assertEquals(4.0, split2Plus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, split2Plus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(1.0, split2Minus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split2Minus.asList().size());
- Assert.assertEquals(5.0, split2Minus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, split2Minus.asList().get(0).getSup(), TEST_EPS);
-
- ArcsSet.Split split3 = s35.split(new Arc(4.0, 6.0, TEST_PRECISION));
- ArcsSet split3Plus = split3.getPlus();
- ArcsSet split3Minus = split3.getMinus();
- Assert.assertEquals(1.0, split3Plus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split3Plus.asList().size());
- Assert.assertEquals(3.0, split3Plus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(4.0, split3Plus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(1.0, split3Minus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split3Minus.asList().size());
- Assert.assertEquals(4.0, split3Minus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, split3Minus.asList().get(0).getSup(), TEST_EPS);
-
- ArcsSet.Split split4 = s35.split(new Arc(6.0, 4.0 + Geometry.TWO_PI, TEST_PRECISION));
- ArcsSet split4Plus = split4.getPlus();
- ArcsSet split4Minus = split4.getMinus();
- Assert.assertEquals(1.0, split4Plus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split4Plus.asList().size());
- Assert.assertEquals(4.0, split4Plus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(5.0, split4Plus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(1.0, split4Minus.getSize(), TEST_EPS);
- Assert.assertEquals(1, split4Minus.asList().size());
- Assert.assertEquals(3.0, split4Minus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(4.0, split4Minus.asList().get(0).getSup(), TEST_EPS);
-
- }
-
- @Test
- public void testFarSplit() {
- ArcsSet set = new ArcsSet(Math.PI, 2.5 * Math.PI, TEST_PRECISION);
- ArcsSet.Split split = set.split(new Arc(0.5 * Math.PI, 1.5 * Math.PI, TEST_PRECISION));
- ArcsSet splitPlus = split.getPlus();
- ArcsSet splitMinus = split.getMinus();
- Assert.assertEquals(1, splitMinus.asList().size());
- Assert.assertEquals(1.0 * Math.PI, splitMinus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(1.5 * Math.PI, splitMinus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, splitMinus.getSize(), TEST_EPS);
- Assert.assertEquals(1, splitPlus.asList().size());
- Assert.assertEquals(1.5 * Math.PI, splitPlus.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(2.5 * Math.PI, splitPlus.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(1.0 * Math.PI, splitPlus.getSize(), TEST_EPS);
-
- }
-
- @Test
- public void testSplitWithinEpsilon() {
- double epsilon = TEST_EPS;
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(epsilon);
- double a = 6.25;
- double b = a - 0.5 * epsilon;
- ArcsSet set = new ArcsSet(a - 1, a, precision);
- Arc arc = new Arc(b, b + Math.PI, precision);
- ArcsSet.Split split = set.split(arc);
- Assert.assertEquals(set.getSize(), split.getPlus().getSize(), epsilon);
- Assert.assertNull(split.getMinus());
- }
-
- @Test
- public void testSideSplitConsistency() {
- double epsilon = 1.0e-6;
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(epsilon);
- double a = 4.725;
- ArcsSet set = new ArcsSet(a, a + 0.5, precision);
- Arc arc = new Arc(a + 0.5 * epsilon, a + 1, precision);
- ArcsSet.Split split = set.split(arc);
- Assert.assertNotNull(split.getMinus());
- Assert.assertNull(split.getPlus());
- Assert.assertEquals(Side.MINUS, set.split(arc).getSide());
- }
-
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
new file mode 100644
index 0000000..2a241b6
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
@@ -0,0 +1,596 @@
+/*
+ * 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.oned;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane.Builder;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.spherical.oned.CutAngle.SubCutAngle;
+import org.apache.commons.geometry.spherical.oned.CutAngle.SubCutAngleBuilder;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CutAngleTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testFromAzimuthAndDirection() {
+ // act/assert
+ checkCutAngle(CutAngle.fromAzimuthAndDirection(Geometry.ZERO_PI, true, TEST_PRECISION),
+ Geometry.ZERO_PI, true);
+ checkCutAngle(CutAngle.fromAzimuthAndDirection(Geometry.PI, true, TEST_PRECISION),
+ Geometry.PI, true);
+ checkCutAngle(CutAngle.fromAzimuthAndDirection(Geometry.MINUS_HALF_PI, true, TEST_PRECISION),
+ Geometry.MINUS_HALF_PI, true);
+
+ checkCutAngle(CutAngle.fromAzimuthAndDirection(Geometry.ZERO_PI, false, TEST_PRECISION),
+ Geometry.ZERO_PI, false);
+ checkCutAngle(CutAngle.fromAzimuthAndDirection(Geometry.PI, false, TEST_PRECISION),
+ Geometry.PI, false);
+ checkCutAngle(CutAngle.fromAzimuthAndDirection(Geometry.MINUS_HALF_PI, false, TEST_PRECISION),
+ Geometry.MINUS_HALF_PI, false);
+ }
+
+ @Test
+ public void testFromPointAndDirection() {
+ // arrange
+ Point1S pt = Point1S.of(Geometry.MINUS_HALF_PI);
+
+ // act/assert
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.ZERO, true, TEST_PRECISION),
+ Geometry.ZERO_PI, true);
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.PI, true, TEST_PRECISION),
+ Geometry.PI, true);
+ checkCutAngle(CutAngle.fromPointAndDirection(pt, true, TEST_PRECISION),
+ Geometry.MINUS_HALF_PI, true);
+
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.ZERO, false, TEST_PRECISION),
+ Geometry.ZERO_PI, false);
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.PI, false, TEST_PRECISION),
+ Geometry.PI, false);
+ checkCutAngle(CutAngle.fromPointAndDirection(pt, false, TEST_PRECISION),
+ Geometry.MINUS_HALF_PI, false);
+ }
+
+ @Test
+ public void testCreatePositiveFacing() {
+ // act/assert
+ checkCutAngle(CutAngle.createPositiveFacing(Point1S.ZERO, TEST_PRECISION),
+ Geometry.ZERO_PI, true);
+ checkCutAngle(CutAngle.createPositiveFacing(Point1S.PI, TEST_PRECISION),
+ Geometry.PI, true);
+ checkCutAngle(CutAngle.createPositiveFacing(Geometry.MINUS_HALF_PI, TEST_PRECISION),
+ Geometry.MINUS_HALF_PI, true);
+ }
+
+ @Test
+ public void testCreateNegativeFacing() {
+ // act/assert
+ checkCutAngle(CutAngle.createNegativeFacing(Point1S.ZERO, TEST_PRECISION),
+ Geometry.ZERO_PI, false);
+ checkCutAngle(CutAngle.createNegativeFacing(Point1S.PI, TEST_PRECISION),
+ Geometry.PI, false);
+ checkCutAngle(CutAngle.createNegativeFacing(Geometry.MINUS_HALF_PI, TEST_PRECISION),
+ Geometry.MINUS_HALF_PI, false);
+ }
+
+ @Test
+ public void testOffset() {
+ // arrange
+ CutAngle zeroPos = CutAngle.createPositiveFacing(Geometry.ZERO_PI, TEST_PRECISION);
+ CutAngle zeroNeg = CutAngle.createNegativeFacing(Geometry.ZERO_PI, TEST_PRECISION);
+ CutAngle negPiPos = CutAngle.createPositiveFacing(-Geometry.PI, TEST_PRECISION);
+
+ CutAngle piNeg = CutAngle.createNegativeFacing(Geometry.PI, TEST_PRECISION);
+ CutAngle twoAndAHalfPiPos = CutAngle.createPositiveFacing(2.5 * Geometry.PI, TEST_PRECISION);
+
+ // act/assert
+ checkOffset(zeroPos, 0, 0);
+ checkOffset(zeroPos, Geometry.TWO_PI, 0);
+ checkOffset(zeroPos, 2.5 * Geometry.PI, Geometry.HALF_PI);
+ checkOffset(zeroPos, Geometry.PI, Geometry.PI);
+ checkOffset(zeroPos, 3.5 * Geometry.PI, 1.5 * Geometry.PI);
+
+ checkOffset(zeroNeg, 0, 0);
+ checkOffset(zeroNeg, Geometry.TWO_PI, 0);
+ checkOffset(zeroNeg, 2.5 * Geometry.PI, Geometry.MINUS_HALF_PI);
+ checkOffset(zeroNeg, Geometry.PI, -Geometry.PI);
+ checkOffset(zeroNeg, 3.5 * Geometry.PI, -1.5 * Geometry.PI);
+
+ checkOffset(negPiPos, 0, -Geometry.PI);
+ checkOffset(negPiPos, Geometry.TWO_PI, -Geometry.PI);
+ checkOffset(negPiPos, 2.5 * Geometry.PI, Geometry.MINUS_HALF_PI);
+ checkOffset(negPiPos, Geometry.PI, 0);
+ checkOffset(negPiPos, 3.5 * Geometry.PI, Geometry.HALF_PI);
+
+ checkOffset(piNeg, 0, Geometry.PI);
+ checkOffset(piNeg, Geometry.TWO_PI, Geometry.PI);
+ checkOffset(piNeg, 2.5 * Geometry.PI, Geometry.HALF_PI);
+ checkOffset(piNeg, Geometry.PI, 0);
+ checkOffset(piNeg, 3.5 * Geometry.PI, Geometry.MINUS_HALF_PI);
+
+ checkOffset(twoAndAHalfPiPos, 0, Geometry.MINUS_HALF_PI);
+ checkOffset(twoAndAHalfPiPos, Geometry.TWO_PI, Geometry.MINUS_HALF_PI);
+ checkOffset(twoAndAHalfPiPos, 2.5 * Geometry.PI, 0);
+ checkOffset(twoAndAHalfPiPos, Geometry.PI, Geometry.HALF_PI);
+ checkOffset(twoAndAHalfPiPos, 3.5 * Geometry.PI, Geometry.PI);
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ CutAngle zeroPos = CutAngle.createPositiveFacing(Geometry.ZERO_PI, TEST_PRECISION);
+ CutAngle zeroNeg = CutAngle.createNegativeFacing(Geometry.ZERO_PI, TEST_PRECISION);
+ CutAngle negPiPos = CutAngle.createPositiveFacing(-Geometry.PI, TEST_PRECISION);
+
+ // act/assert
+ checkClassify(zeroPos, HyperplaneLocation.ON,
+ 0, 1e-16, -1e-16,
+ Geometry.TWO_PI - 1e-11, Geometry.TWO_PI + 1e-11);
+ checkClassify(zeroPos, HyperplaneLocation.PLUS,
+ 0.5, 2.5 * Geometry.PI,
+ -0.5, Geometry.MINUS_HALF_PI);
+
+ checkClassify(zeroNeg, HyperplaneLocation.ON,
+ 0, 1e-16, -1e-16,
+ Geometry.TWO_PI - 1e-11, Geometry.TWO_PI + 1e-11);
+ checkClassify(zeroNeg, HyperplaneLocation.MINUS,
+ 0.5, 2.5 * Geometry.PI,
+ -0.5, Geometry.MINUS_HALF_PI);
+
+ checkClassify(negPiPos, HyperplaneLocation.ON, Geometry.PI, Geometry.PI + 1e-11);
+ checkClassify(negPiPos, HyperplaneLocation.MINUS, 0.5, 2.5 * Geometry.PI,
+ 0, 1e-11, Geometry.TWO_PI, Geometry.TWO_PI - 1e-11);
+ checkClassify(negPiPos, HyperplaneLocation.PLUS, -0.5, Geometry.MINUS_HALF_PI);
+ }
+
+ @Test
+ public void testContains() {
+ // arrange
+ CutAngle pt = CutAngle.createNegativeFacing(Geometry.HALF_PI, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertFalse(pt.contains(Point1S.ZERO));
+ Assert.assertFalse(pt.contains(Point1S.of(Geometry.TWO_PI)));
+
+ Assert.assertFalse(pt.contains(Point1S.of(Geometry.PI)));
+ Assert.assertFalse(pt.contains(Point1S.of(0.25 * Geometry.PI)));
+ Assert.assertFalse(pt.contains(Point1S.of(-0.25 * Geometry.PI)));
+
+ Assert.assertTrue(pt.contains(Point1S.of(Geometry.HALF_PI)));
+ Assert.assertTrue(pt.contains(Point1S.of(Geometry.HALF_PI + 1e-11)));
+ Assert.assertTrue(pt.contains(Point1S.of(2.5 * Geometry.PI)));
+ Assert.assertTrue(pt.contains(Point1S.of(-3.5 * Geometry.PI)));
+ }
+
+ @Test
+ public void testReverse() {
+ // arrange
+ CutAngle pt = CutAngle.createNegativeFacing(Geometry.HALF_PI, TEST_PRECISION);
+
+ // act
+ CutAngle result = pt.reverse();
+
+ // assert
+ checkCutAngle(result, Geometry.HALF_PI, true);
+ Assert.assertSame(TEST_PRECISION, result.getPrecision());
+
+ checkCutAngle(result.reverse(), Geometry.HALF_PI, false);
+ }
+
+ @Test
+ public void testProject() {
+ // arrange
+ CutAngle pt = CutAngle.createNegativeFacing(Geometry.HALF_PI, TEST_PRECISION);
+
+ // act/assert
+ for (double az = -Geometry.TWO_PI; az <= Geometry.TWO_PI; az += 0.2) {
+ Assert.assertEquals(Geometry.HALF_PI, pt.project(Point1S.of(az)).getAzimuth(), TEST_EPS);
+ }
+ }
+
+ @Test
+ public void testSimilarOrientation() {
+ // arrange
+ CutAngle a = CutAngle.createPositiveFacing(Geometry.ZERO_PI, TEST_PRECISION);
+ CutAngle b = CutAngle.createNegativeFacing(Geometry.ZERO_PI, TEST_PRECISION);
+ CutAngle c = CutAngle.createPositiveFacing(Geometry.MINUS_HALF_PI, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(a.similarOrientation(a));
+ Assert.assertFalse(a.similarOrientation(b));
+ Assert.assertTrue(a.similarOrientation(c));
+ }
+
+ @Test
+ public void testTransform_rotate() {
+ // arrange
+ Transform1S transform = Transform1S.createRotation(Geometry.HALF_PI);
+
+ // act
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.ZERO, true, TEST_PRECISION).transform(transform),
+ Geometry.HALF_PI, true);
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.ZERO, false, TEST_PRECISION).transform(transform),
+ Geometry.HALF_PI, false);
+
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.of(1.5 * Geometry.PI), true, TEST_PRECISION).transform(transform),
+ Geometry.TWO_PI, true);
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.of(Geometry.MINUS_HALF_PI), false, TEST_PRECISION).transform(transform),
+ Geometry.ZERO_PI, false);
+ }
+
+ @Test
+ public void testTransform_negate() {
+ // arrange
+ Transform1S transform = Transform1S.createNegation();
+
+ // act
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.ZERO, true, TEST_PRECISION).transform(transform),
+ Geometry.ZERO_PI, false);
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.ZERO, false, TEST_PRECISION).transform(transform),
+ Geometry.ZERO_PI, true);
+
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.of(1.5 * Geometry.PI), true, TEST_PRECISION).transform(transform),
+ -1.5 * Geometry.PI, false);
+ checkCutAngle(CutAngle.fromPointAndDirection(Point1S.of(Geometry.MINUS_HALF_PI), false, TEST_PRECISION).transform(transform),
+ Geometry.HALF_PI, true);
+ }
+
+ @Test
+ public void testSpan() {
+ // arrange
+ CutAngle pt = CutAngle.fromPointAndDirection(Point1S.of(1.0), false, TEST_PRECISION);
+
+ // act
+ SubCutAngle result = pt.span();
+
+ // assert
+ Assert.assertSame(pt, result.getHyperplane());
+ }
+
+ @Test
+ public void testEq() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ CutAngle a = CutAngle.fromPointAndDirection(Point1S.ZERO, true, precision);
+
+ CutAngle b = CutAngle.fromPointAndDirection(Point1S.PI, true, precision);
+ CutAngle c = CutAngle.fromPointAndDirection(Point1S.ZERO, false, precision);
+ CutAngle d = CutAngle.fromPointAndDirection(Point1S.ZERO, true, TEST_PRECISION);
+
+ CutAngle e = CutAngle.fromPointAndDirection(Point1S.ZERO, true, precision);
+ CutAngle f = CutAngle.fromPointAndDirection(Point1S.of(Geometry.TWO_PI), true, precision);
+ CutAngle g = CutAngle.fromPointAndDirection(Point1S.of(1e-4), true, precision);
+ CutAngle h = CutAngle.fromPointAndDirection(Point1S.of(-1e-4), true, precision);
+
+ // act/assert
+ Assert.assertTrue(a.eq(a));
+
+ Assert.assertFalse(a.eq(b));
+ Assert.assertFalse(a.eq(c));
+ Assert.assertFalse(a.eq(d));
+
+ Assert.assertTrue(a.eq(e));
+ Assert.assertTrue(a.eq(f));
+ Assert.assertTrue(a.eq(g));
+ Assert.assertTrue(a.eq(h));
+ }
+
+ @Test
+ public void testHashCode() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ CutAngle a = CutAngle.fromPointAndDirection(Point1S.ZERO, true, TEST_PRECISION);
+ CutAngle b = CutAngle.fromPointAndDirection(Point1S.PI, true, TEST_PRECISION);
+ CutAngle c = CutAngle.fromPointAndDirection(Point1S.ZERO, false, TEST_PRECISION);
+ CutAngle d = CutAngle.fromPointAndDirection(Point1S.ZERO, true, precision);
+ CutAngle e = CutAngle.fromPointAndDirection(Point1S.ZERO, true, TEST_PRECISION);
+
+ int hash = a.hashCode();
+
+ // act/assert
+ Assert.assertEquals(hash, a.hashCode());
+
+ Assert.assertNotEquals(hash, b.hashCode());
+ Assert.assertNotEquals(hash, c.hashCode());
+ Assert.assertNotEquals(hash, d.hashCode());
+
+ Assert.assertEquals(hash, e.hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ CutAngle a = CutAngle.fromPointAndDirection(Point1S.ZERO, true, TEST_PRECISION);
+ CutAngle b = CutAngle.fromPointAndDirection(Point1S.PI, true, TEST_PRECISION);
+ CutAngle c = CutAngle.fromPointAndDirection(Point1S.ZERO, false, TEST_PRECISION);
+ CutAngle d = CutAngle.fromPointAndDirection(Point1S.ZERO, true, precision);
+ CutAngle e = CutAngle.fromPointAndDirection(Point1S.ZERO, true, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertFalse(a.equals(null));
+ Assert.assertFalse(a.equals(new Object()));
+
+ Assert.assertTrue(a.equals(a));
+
+ Assert.assertFalse(a.equals(b));
+ Assert.assertFalse(a.equals(c));
+ Assert.assertFalse(a.equals(d));
+
+ Assert.assertTrue(a.equals(e));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ CutAngle pt = CutAngle.createPositiveFacing(Geometry.ZERO_PI, TEST_PRECISION);
+
+ // act
+ String str = pt.toString();
+
+ // assert
+ Assert.assertTrue(str.startsWith("CutAngle["));
+ Assert.assertTrue(str.contains("point= ") && str.contains("positiveFacing= "));
+ }
+
+ @Test
+ public void testSubHyperplane_split() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ CutAngle pt = CutAngle.createPositiveFacing(-1.5, precision);
+ SubCutAngle sub = pt.span();
+
+ // act/assert
+ checkSplit(sub, CutAngle.createPositiveFacing(1.0, precision), false, true);
+ checkSplit(sub, CutAngle.createPositiveFacing(-1.5 + 1e-2, precision), true, false);
+
+ checkSplit(sub, CutAngle.createNegativeFacing(1.0, precision), true, false);
+ checkSplit(sub, CutAngle.createNegativeFacing(-1.5 + 1e-2, precision), false, true);
+
+ checkSplit(sub, CutAngle.createNegativeFacing(-1.5, precision), false, false);
+ checkSplit(sub, CutAngle.createNegativeFacing(-1.5 + 1e-4, precision), false, false);
+ checkSplit(sub, CutAngle.createNegativeFacing(-1.5 - 1e-4, precision), false, false);
+ }
+
+ private void checkSplit(SubCutAngle sub, CutAngle splitter, boolean minus, boolean plus) {
+ Split<SubCutAngle> split = sub.split(splitter);
+
+ Assert.assertSame(minus ? sub : null, split.getMinus());
+ Assert.assertSame(plus ? sub : null, split.getPlus());
+ }
+
+ @Test
+ public void testSubHyperplane_simpleMethods() {
+ // arrange
+ CutAngle pt = CutAngle.createPositiveFacing(0, TEST_PRECISION);
+ SubCutAngle sub = pt.span();
+
+ // act/assert
+ Assert.assertSame(pt, sub.getHyperplane());
+ Assert.assertFalse(sub.isFull());
+ Assert.assertFalse(sub.isEmpty());
+ Assert.assertFalse(sub.isInfinite());
+ Assert.assertTrue(sub.isFinite());
+ Assert.assertEquals(0.0, sub.getSize(), TEST_EPS);
+
+ List<SubCutAngle> list = sub.toConvex();
+ Assert.assertEquals(1, list.size());
+ Assert.assertSame(sub, list.get(0));
+ }
+
+ @Test
+ public void testSubHyperplane_classify() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ CutAngle pt = CutAngle.createPositiveFacing(1, precision);
+ SubCutAngle sub = pt.span();
+
+ // act/assert
+ Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(Point1S.of(0.95)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(Point1S.of(1)));
+ Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(Point1S.of(1.05)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Point1S.of(1.11)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Point1S.of(0.89)));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Point1S.of(-3)));
+ Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(Point1S.of(10)));
+ }
+
+ @Test
+ public void testSubHyperplane_contains() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ CutAngle pt = CutAngle.createPositiveFacing(1, precision);
+ SubCutAngle sub = pt.span();
+
+ // act/assert
+ Assert.assertTrue(sub.contains(Point1S.of(0.95)));
+ Assert.assertTrue(sub.contains(Point1S.of(1)));
+ Assert.assertTrue(sub.contains(Point1S.of(1.05)));
+
+ Assert.assertFalse(sub.contains(Point1S.of(1.11)));
+ Assert.assertFalse(sub.contains(Point1S.of(0.89)));
+
+ Assert.assertFalse(sub.contains(Point1S.of(-3)));
+ Assert.assertFalse(sub.contains(Point1S.of(10)));
+ }
+
+ @Test
+ public void testSubHyperplane_closestContained() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+ CutAngle pt = CutAngle.createPositiveFacing(1, precision);
+ SubCutAngle sub = pt.span();
+
+ Point1S expected = Point1S.of(1);
+
+ // act/assert
+ Assert.assertEquals(expected, sub.closest(Point1S.ZERO));
+ Assert.assertEquals(expected, sub.closest(Point1S.of(Geometry.HALF_PI)));
+ Assert.assertEquals(expected, sub.closest(Point1S.PI));
+ Assert.assertEquals(expected, sub.closest(Point1S.of(Geometry.MINUS_HALF_PI)));
+ Assert.assertEquals(expected, sub.closest(Point1S.of(Geometry.TWO_PI)));
+ }
+
+ @Test
+ public void testSubHyperplane_transform() {
+ // arrange
+ CutAngle pt = CutAngle.fromPointAndDirection(Point1S.of(Geometry.HALF_PI), true, TEST_PRECISION);
+
+ Transform1S transform = Transform1S.createNegation().rotate(Geometry.PI);
+
+ // act
+ SubCutAngle result = pt.span().transform(transform);
+
+ // assert
+ checkCutAngle(result.getHyperplane(), Geometry.HALF_PI, false);
+ }
+
+ @Test
+ public void testSubHyperplane_reverse() {
+ // arrange
+ CutAngle pt = CutAngle.createPositiveFacing(2.0, TEST_PRECISION);
+ SubCutAngle sub = pt.span();
+
+ // act
+ SubCutAngle result = sub.reverse();
+
+ // assert
+ Assert.assertEquals(2.0, result.getHyperplane().getAzimuth(), TEST_EPS);
+ Assert.assertFalse(result.getHyperplane().isPositiveFacing());
+
+ Assert.assertEquals(sub.getHyperplane(), result.reverse().getHyperplane());
+ }
+
+ @Test
+ public void testSubHyperplane_toString() {
+ // arrange
+ CutAngle pt = CutAngle.createPositiveFacing(2, TEST_PRECISION);
+ SubCutAngle sub = pt.span();
+
+ // act
+ String str = sub.toString();
+
+ //assert
+ Assert.assertTrue(str.contains("SubCutAngle["));
+ Assert.assertTrue(str.contains("point= "));
+ Assert.assertTrue(str.contains("positiveFacing= "));
+ }
+
+ @Test
+ public void testBuilder() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ CutAngle pt = CutAngle.createPositiveFacing(0, precision);
+ SubCutAngle sub = pt.span();
+
+ // act
+ Builder<Point1S> builder = sub.builder();
+
+ builder.add(sub);
+ builder.add(CutAngle.createPositiveFacing(1e-4, precision).span());
+ builder.add((SubHyperplane<Point1S>) sub);
+
+ SubHyperplane<Point1S> result = builder.build();
+
+ // assert
+ Assert.assertSame(sub, result);
+ }
+
+ @Test
+ public void testBuilder_invalidArgs() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ CutAngle pt = CutAngle.createPositiveFacing(0, precision);
+ SubCutAngle sub = pt.span();
+
+ Builder<Point1S> builder = sub.builder();
+
+ // act/assert
+ GeometryTestUtils.assertThrows(
+ () -> builder.add(CutAngle.createPositiveFacing(2e-3, precision).span()),
+ GeometryException.class);
+ GeometryTestUtils.assertThrows(
+ () -> builder.add(CutAngle.createNegativeFacing(2e-3, precision).span()),
+ GeometryException.class);
+
+ GeometryTestUtils.assertThrows(
+ () -> builder.add((SubHyperplane<Point1S>) CutAngle.createPositiveFacing(2e-3, precision).span()),
+ GeometryException.class);
+ }
+
+ @Test
+ public void testBuilder_toString() {
+ // arrange
+ CutAngle pt = CutAngle.createPositiveFacing(2, TEST_PRECISION);
+ SubCutAngleBuilder builder = pt.span().builder();
+
+ // act
+ String str = builder.toString();
+
+ //assert
+ Assert.assertTrue(str.contains("SubCutAngleBuilder["));
+ Assert.assertTrue(str.contains("base= SubCutAngle["));
+ Assert.assertTrue(str.contains("point= "));
+ Assert.assertTrue(str.contains("positiveFacing= "));
+ }
+
+ private static void checkCutAngle(CutAngle angle, double az, boolean positiveFacing) {
+ checkCutAngle(angle, az, positiveFacing, TEST_PRECISION);
+ }
+
+ private static void checkCutAngle(CutAngle angle, double az, boolean positiveFacing, DoublePrecisionContext precision) {
+ Assert.assertEquals(az, angle.getAzimuth(), TEST_EPS);
+ Assert.assertEquals(PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(az), angle.getNormalizedAzimuth(), TEST_EPS);
+ Assert.assertEquals(az, angle.getPoint().getAzimuth(), TEST_EPS);
+ Assert.assertEquals(positiveFacing, angle.isPositiveFacing());
+
+ Assert.assertSame(precision, angle.getPrecision());
+ }
+
+ private static void checkOffset(CutAngle pt, double az, double offset) {
+ Assert.assertEquals(offset, pt.offset(Point1S.of(az)), TEST_EPS);
+ }
+
+ private static void checkClassify(CutAngle pt, HyperplaneLocation loc, double ... azimuths) {
+ for (double az : azimuths) {
+ Assert.assertEquals("Unexpected location for azimuth " + az, loc, pt.classify(Point1S.of(az)));
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/LimitAngleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/LimitAngleTest.java
deleted file mode 100644
index eb29564..0000000
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/LimitAngleTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.oned;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class LimitAngleTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testReversedLimit() {
- for (int k = -2; k < 3; ++k) {
- LimitAngle l = new LimitAngle(S1Point.of(1.0 + k * Geometry.TWO_PI), false, TEST_PRECISION);
- Assert.assertEquals(l.getLocation().getAzimuth(), l.getReverse().getLocation().getAzimuth(), TEST_EPS);
- Assert.assertSame(l.getPrecision(), l.getReverse().getPrecision());
- Assert.assertTrue(l.sameOrientationAs(l));
- Assert.assertFalse(l.sameOrientationAs(l.getReverse()));
- Assert.assertEquals(Geometry.TWO_PI, l.wholeSpace().getSize(), TEST_EPS);
- Assert.assertEquals(Geometry.TWO_PI, l.getReverse().wholeSpace().getSize(), TEST_EPS);
- }
- }
-
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Point1STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Point1STest.java
new file mode 100644
index 0000000..f4a08e6
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Point1STest.java
@@ -0,0 +1,481 @@
+/*
+ * 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.oned;
+
+import java.util.Comparator;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.exception.GeometryValueException;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.numbers.angle.PlaneAngle;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Point1STest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ @Test
+ public void testConstants() {
+ // act/assert
+ Assert.assertEquals(0.0, Point1S.ZERO.getAzimuth(), TEST_EPS);
+ Assert.assertEquals(Math.PI, Point1S.PI.getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testNormalizedAzimuthComparator() {
+ // arrange
+ Comparator<Point1S> comp = Point1S.NORMALIZED_AZIMUTH_ASCENDING_ORDER;
+
+ // act/assert
+ Assert.assertEquals(0, comp.compare(Point1S.of(1), Point1S.of(1)));
+ Assert.assertEquals(-1, comp.compare(Point1S.of(0), Point1S.of(1)));
+ Assert.assertEquals(1, comp.compare(Point1S.of(1), Point1S.of(0)));
+ Assert.assertEquals(1, comp.compare(Point1S.of(1), Point1S.of(0.1 + Geometry.TWO_PI)));
+
+ Assert.assertEquals(1, comp.compare(null, Point1S.of(0)));
+ Assert.assertEquals(-1, comp.compare(Point1S.of(0), null));
+ Assert.assertEquals(0, comp.compare(null, null));
+ }
+
+ @Test
+ public void testOf() {
+ // act/assert
+ checkPoint(Point1S.of(0), 0, 0);
+ checkPoint(Point1S.of(1), 1, 1);
+ checkPoint(Point1S.of(-1), -1, Geometry.TWO_PI - 1);
+
+ checkPoint(Point1S.of(PlaneAngle.ofDegrees(90)), Geometry.HALF_PI, Geometry.HALF_PI);
+ checkPoint(Point1S.of(PlaneAngle.ofTurns(0.5)), Geometry.PI, Geometry.PI);
+ checkPoint(Point1S.of(Geometry.MINUS_HALF_PI), Geometry.MINUS_HALF_PI, 1.5 * Geometry.PI);
+
+ double base = Geometry.HALF_PI;
+ for (int k = -3; k <= 3; ++k) {
+ double az = base + (k * Geometry.TWO_PI);
+ checkPoint(Point1S.of(az), az, base);
+ }
+ }
+
+ @Test
+ public void testFrom_vector() {
+ // act/assert
+ checkPoint(Point1S.from(Vector2D.of(2, 0)), Geometry.ZERO_PI);
+ checkPoint(Point1S.from(Vector2D.of(0, 0.1)), Geometry.HALF_PI);
+ checkPoint(Point1S.from(Vector2D.of(-0.5, 0)), Geometry.PI);
+ checkPoint(Point1S.from(Vector2D.of(0, -100)), 1.5 * Geometry.PI);
+ }
+
+ @Test
+ public void testFrom_polar() {
+ // act/assert
+ checkPoint(Point1S.from(PolarCoordinates.of(100, 0)), Geometry.ZERO_PI);
+ checkPoint(Point1S.from(PolarCoordinates.of(1, Geometry.HALF_PI)), Geometry.HALF_PI);
+ checkPoint(Point1S.from(PolarCoordinates.of(0.5, Geometry.PI)), Geometry.PI);
+ checkPoint(Point1S.from(PolarCoordinates.of(1e-4, Geometry.MINUS_HALF_PI)), 1.5 * Geometry.PI);
+ }
+
+ @Test
+ public void testFrom_polar_invalidAzimuths() {
+ // act/assert
+ checkPoint(Point1S.from(PolarCoordinates.of(100, Double.POSITIVE_INFINITY)), Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
+ checkPoint(Point1S.from(PolarCoordinates.of(100, Double.NEGATIVE_INFINITY)), Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
+ checkPoint(Point1S.from(PolarCoordinates.of(100, Double.NaN)), Double.NaN, Double.NaN);
+ }
+
+ @Test
+ public void testNaN() {
+ // act
+ Point1S pt = Point1S.of(Double.NaN);
+
+ // assert
+ Assert.assertTrue(pt.isNaN());
+ Assert.assertTrue(Point1S.NaN.isNaN());
+
+ Assert.assertTrue(Double.isNaN(pt.getAzimuth()));
+ Assert.assertTrue(Double.isNaN(pt.getNormalizedAzimuth()));
+ Assert.assertNull(pt.getVector());
+
+ Assert.assertTrue(Point1S.NaN.equals(pt));
+ Assert.assertFalse(Point1S.of(1.0).equals(Point1S.NaN));
+ }
+
+ @Test
+ public void testGetDimension() {
+ // arrange
+ Point1S p = Point1S.of(0.0);
+
+ // act/assert
+ Assert.assertEquals(1, p.getDimension());
+ }
+
+ @Test
+ public void testInfinite() {
+ // act/assert
+ Assert.assertTrue(Point1S.of(Double.POSITIVE_INFINITY).isInfinite());
+ Assert.assertTrue(Point1S.of(Double.NEGATIVE_INFINITY).isInfinite());
+
+ Assert.assertFalse(Point1S.NaN.isInfinite());
+ Assert.assertFalse(Point1S.of(1).isInfinite());
+ }
+
+ @Test
+ public void testFinite() {
+ // act/assert
+ Assert.assertTrue(Point1S.of(0).isFinite());
+ Assert.assertTrue(Point1S.of(1).isFinite());
+
+ Assert.assertFalse(Point1S.of(Double.POSITIVE_INFINITY).isFinite());
+ Assert.assertFalse(Point1S.of(Double.NEGATIVE_INFINITY).isFinite());
+ Assert.assertFalse(Point1S.NaN.isFinite());
+ }
+
+ @Test
+ public void testAntipodal() {
+ for (double az = -6 * Geometry.PI; az <= 6 * Geometry.PI; az += 0.1) {
+ // arrange
+ Point1S pt = Point1S.of(az);
+
+ // act
+ Point1S result = pt.antipodal();
+
+ // assert
+ Assert.assertTrue(result.getAzimuth() >= 0 && result.getAzimuth() < Geometry.TWO_PI);
+ Assert.assertEquals(Geometry.PI, pt.distance(result), TEST_EPS);
+ }
+ }
+
+ @Test
+ public void testHashCode() {
+ // act
+ Point1S a = Point1S.of(1.0);
+ Point1S b = Point1S.of(2.0);
+ Point1S c = Point1S.of(1.0);
+ Point1S d = Point1S.of(1.0 + Geometry.PI);
+
+ int hash = a.hashCode();
+
+ // assert
+ Assert.assertEquals(hash, a.hashCode());
+ Assert.assertNotEquals(hash, b.hashCode());
+ Assert.assertEquals(hash, c.hashCode());
+ Assert.assertNotEquals(hash, d.hashCode());
+
+ Assert.assertEquals(Point1S.NaN.hashCode(), Point1S.of(Double.NaN).hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // act
+ Point1S a = Point1S.of(1.0);
+ Point1S b = Point1S.of(2.0);
+ Point1S c = Point1S.of(1.0 + Geometry.PI);
+ Point1S d = Point1S.of(1.0);
+ Point1S e = Point1S.of(Double.NaN);
+
+ // assert
+ Assert.assertFalse(a.equals(null));
+ Assert.assertFalse(a.equals(new Object()));
+
+ Assert.assertTrue(a.equals(a));
+
+ Assert.assertFalse(a.equals(b));
+ Assert.assertFalse(b.equals(a));
+
+ Assert.assertFalse(a.equals(c));
+ Assert.assertFalse(c.equals(a));
+
+ Assert.assertTrue(a.equals(d));
+ Assert.assertTrue(d.equals(a));
+
+ Assert.assertFalse(a.equals(e));
+ Assert.assertTrue(e.equals(Point1S.NaN));
+ }
+
+ @Test
+ public void testEq() {
+ // arrange
+ DoublePrecisionContext highPrecision = new EpsilonDoublePrecisionContext(1e-10);
+ DoublePrecisionContext lowPrecision = new EpsilonDoublePrecisionContext(1e-2);
+
+ Point1S a = Point1S.of(1);
+ Point1S b = Point1S.of(0.9999);
+ Point1S c = Point1S.of(1.00001);
+ Point1S d = Point1S.of(1 + (3 * Geometry.TWO_PI));
+
+ // act/assert
+ Assert.assertTrue(a.eq(a, highPrecision));
+ Assert.assertTrue(a.eq(a, lowPrecision));
+
+ Assert.assertFalse(a.eq(b, highPrecision));
+ Assert.assertTrue(a.eq(b, lowPrecision));
+
+ Assert.assertFalse(a.eq(c, highPrecision));
+ Assert.assertTrue(a.eq(c, lowPrecision));
+
+ Assert.assertTrue(a.eq(d, highPrecision));
+ Assert.assertTrue(a.eq(d, lowPrecision));
+ }
+
+ @Test
+ public void testEq_wrapAround() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ Point1S a = Point1S.ZERO;
+ Point1S b = Point1S.of(1e-3);
+ Point1S c = Point1S.of(-1e-3);
+
+ // act/assert
+ Assert.assertTrue(a.eq(a, precision));
+
+ Assert.assertTrue(a.eq(b, precision));
+ Assert.assertTrue(b.eq(a, precision));
+
+ Assert.assertTrue(a.eq(c, precision));
+ Assert.assertTrue(c.eq(a, precision));
+ }
+
+ @Test
+ public void testDistance() {
+ // arrange
+ Point1S a = Point1S.of(0.0);
+ Point1S b = Point1S.of(Geometry.PI - 0.5);
+ Point1S c = Point1S.of(Geometry.PI);
+ Point1S d = Point1S.of(Geometry.PI + 0.5);
+ Point1S e = Point1S.of(4.0);
+
+ // act/assert
+ Assert.assertEquals(0.0, a.distance(a), TEST_EPS);
+ Assert.assertEquals(Geometry.PI - 0.5, a.distance(b), TEST_EPS);
+ Assert.assertEquals(Geometry.PI - 0.5, b.distance(a), TEST_EPS);
+
+ Assert.assertEquals(Geometry.PI, a.distance(c), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, c.distance(a), TEST_EPS);
+
+ Assert.assertEquals(Geometry.PI - 0.5, a.distance(d), TEST_EPS);
+ Assert.assertEquals(Geometry.PI - 0.5, d.distance(a), TEST_EPS);
+
+ Assert.assertEquals(Geometry.TWO_PI - 4, a.distance(e), TEST_EPS);
+ Assert.assertEquals(Geometry.TWO_PI - 4, e.distance(a), TEST_EPS);
+ }
+
+ @Test
+ public void testSignedDistance() {
+ // arrange
+ Point1S a = Point1S.of(0.0);
+ Point1S b = Point1S.of(Geometry.PI - 0.5);
+ Point1S c = Point1S.of(Geometry.PI);
+ Point1S d = Point1S.of(Geometry.PI + 0.5);
+ Point1S e = Point1S.of(4.0);
+
+ // act/assert
+ Assert.assertEquals(0.0, a.signedDistance(a), TEST_EPS);
+ Assert.assertEquals(Geometry.PI - 0.5, a.signedDistance(b), TEST_EPS);
+ Assert.assertEquals(-Geometry.PI + 0.5, b.signedDistance(a), TEST_EPS);
+
+ Assert.assertEquals(-Geometry.PI, a.signedDistance(c), TEST_EPS);
+ Assert.assertEquals(-Geometry.PI, c.signedDistance(a), TEST_EPS);
+
+ Assert.assertEquals(-Geometry.PI + 0.5, a.signedDistance(d), TEST_EPS);
+ Assert.assertEquals(Geometry.PI - 0.5, d.signedDistance(a), TEST_EPS);
+
+ Assert.assertEquals(-Geometry.TWO_PI + 4, a.signedDistance(e), TEST_EPS);
+ Assert.assertEquals(Geometry.TWO_PI - 4, e.signedDistance(a), TEST_EPS);
+ }
+
+ @Test
+ public void testDistance_inRangeZeroToPi() {
+ for (double a = -4 * Geometry.PI; a < 4 * Geometry.PI; a += 0.1) {
+ for (double b = -4 * Geometry.PI; b < 4 * Geometry.PI; b += 0.1) {
+ // arrange
+ Point1S p1 = Point1S.of(a);
+ Point1S p2 = Point1S.of(b);
+
+ // act/assert
+ double d1 = p1.distance(p2);
+ Assert.assertTrue(d1 >= 0 && d1 <= Geometry.PI);
+
+ double d2 = p2.distance(p1);
+ Assert.assertTrue(d2 >= 0 && d2 <= Geometry.PI);
+ }
+ }
+ }
+
+ @Test
+ public void testNormalize() {
+ for (double az = -Geometry.TWO_PI; az < 2 * Geometry.TWO_PI; az += 0.2) {
+ // arrange
+ Point1S pt = Point1S.of(az);
+
+ double expectedPiNorm = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(az);
+ double expectedZeroNorm = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(az);
+
+ // act
+ Point1S piNorm = pt.normalize(Point1S.PI);
+ Point1S zeroNorm = pt.normalize(Geometry.ZERO_PI);
+
+ // assert
+ Assert.assertEquals(expectedPiNorm, piNorm.getAzimuth(), TEST_EPS);
+ Assert.assertEquals(pt.getNormalizedAzimuth(), piNorm.getNormalizedAzimuth(), TEST_EPS);
+
+ Assert.assertEquals(expectedZeroNorm, zeroNorm.getAzimuth(), TEST_EPS);
+ Assert.assertEquals(pt.getNormalizedAzimuth(), zeroNorm.getNormalizedAzimuth(), TEST_EPS);
+ }
+ }
+
+ @Test
+ public void testNormalize_nonFinite() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.POSITIVE_INFINITY).normalize(Geometry.ZERO_PI);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.NEGATIVE_INFINITY).normalize(Geometry.ZERO_PI);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.NaN).normalize(Point1S.ZERO);
+ }, GeometryValueException.class);
+ }
+
+ @Test
+ public void testAbove() {
+ // arrange
+ Point1S p1 = Point1S.ZERO;
+ Point1S p2 = Point1S.of(PlaneAngle.ofDegrees(90));
+ Point1S p3 = Point1S.PI;
+ Point1S p4 = Point1S.of(PlaneAngle.ofDegrees(-90));
+ Point1S p5 = Point1S.of(Geometry.TWO_PI);
+
+ // act/assert
+ checkPoint(p1.above(p1), 0);
+ checkPoint(p2.above(p1), Geometry.HALF_PI);
+ checkPoint(p3.above(p1), Geometry.PI);
+ checkPoint(p4.above(p1), 1.5 * Geometry.PI);
+ checkPoint(p5.above(p1), 0);
+
+ checkPoint(p1.above(p3), Geometry.TWO_PI);
+ checkPoint(p2.above(p3), 2.5 * Geometry.PI);
+ checkPoint(p3.above(p3), Geometry.PI);
+ checkPoint(p4.above(p3), 1.5 * Geometry.PI);
+ checkPoint(p5.above(p3), Geometry.TWO_PI);
+ }
+
+ @Test
+ public void testAbove_nonFinite() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.POSITIVE_INFINITY).above(Point1S.ZERO);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.NEGATIVE_INFINITY).above(Point1S.ZERO);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.NaN).above(Point1S.ZERO);
+ }, GeometryValueException.class);
+ }
+
+ @Test
+ public void testBelow() {
+ // arrange
+ Point1S p1 = Point1S.ZERO;
+ Point1S p2 = Point1S.of(PlaneAngle.ofDegrees(90));
+ Point1S p3 = Point1S.PI;
+ Point1S p4 = Point1S.of(PlaneAngle.ofDegrees(-90));
+ Point1S p5 = Point1S.of(Geometry.TWO_PI);
+
+ // act/assert
+ checkPoint(p1.below(p1), -Geometry.TWO_PI);
+ checkPoint(p2.below(p1), -1.5 * Geometry.PI);
+ checkPoint(p3.below(p1), -Geometry.PI);
+ checkPoint(p4.below(p1), Geometry.MINUS_HALF_PI);
+ checkPoint(p5.below(p1), -Geometry.TWO_PI);
+
+ checkPoint(p1.below(p3), Geometry.ZERO_PI);
+ checkPoint(p2.below(p3), Geometry.HALF_PI);
+ checkPoint(p3.below(p3), -Geometry.PI);
+ checkPoint(p4.below(p3), Geometry.MINUS_HALF_PI);
+ checkPoint(p5.below(p3), Geometry.ZERO_PI);
+ }
+
+ @Test
+ public void testBelow_nonFinite() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.POSITIVE_INFINITY).below(Point1S.ZERO);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.NEGATIVE_INFINITY).below(Point1S.ZERO);
+ }, GeometryValueException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Point1S.of(Double.NaN).below(Point1S.ZERO);
+ }, GeometryValueException.class);
+ }
+
+ @Test
+ public void testToString() {
+ // act/assert
+ Assert.assertEquals("(0.0)", Point1S.of(0.0).toString());
+ Assert.assertEquals("(1.0)", Point1S.of(1.0).toString());
+ }
+
+ @Test
+ public void testParse() {
+ // act/assert
+ checkPoint(Point1S.parse("(0)"), 0.0);
+ checkPoint(Point1S.parse("(1)"), 1.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testParse_failure() {
+ // act/assert
+ Point1S.parse("abc");
+ }
+
+ private static void checkPoint(Point1S pt, double az) {
+ checkPoint(pt, az, PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(az));
+ }
+
+ private static void checkPoint(Point1S pt, double az, double normAz) {
+ Assert.assertEquals(az, pt.getAzimuth(), TEST_EPS);
+ Assert.assertEquals(normAz, pt.getNormalizedAzimuth(), TEST_EPS);
+
+ Assert.assertEquals(1, pt.getDimension());
+
+ Assert.assertEquals(Double.isFinite(az), pt.isFinite());
+ Assert.assertEquals(Double.isInfinite(az), pt.isInfinite());
+
+ Vector2D vec = pt.getVector();
+ if (pt.isFinite()) {
+ Assert.assertEquals(1, vec.norm(), TEST_EPS);
+ Assert.assertEquals(normAz, PolarCoordinates.fromCartesian(vec).getAzimuth(), TEST_EPS);
+ }
+ else {
+ Assert.assertNull(vec);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1STest.java
new file mode 100644
index 0000000..6ae23a7
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1STest.java
@@ -0,0 +1,923 @@
+/*
+ * 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.oned;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RegionBSPTree1STest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final Transform1S HALF_PI_PLUS_AZ = Transform1S.createRotation(Geometry.HALF_PI);
+
+ private static final Transform1S PI_MINUS_AZ = Transform1S.createNegation().rotate(Geometry.PI);
+
+ @Test
+ public void testConstructor_default() {
+ // act
+ RegionBSPTree1S tree = new RegionBSPTree1S();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+
+ Assert.assertEquals(0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+ }
+
+ @Test
+ public void testConstructor_true() {
+ // act
+ RegionBSPTree1S tree = new RegionBSPTree1S(true);
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(Geometry.TWO_PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+ }
+
+ @Test
+ public void testConstructor_false() {
+ // act
+ RegionBSPTree1S tree = new RegionBSPTree1S(false);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+
+ Assert.assertEquals(0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+ }
+
+ @Test
+ public void testFull() {
+ // act
+ RegionBSPTree1S tree = RegionBSPTree1S.full();
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(Geometry.TWO_PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+ }
+
+ @Test
+ public void testEmpty() {
+ // act
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+
+ Assert.assertEquals(0, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+ }
+
+ @Test
+ public void testCopy() {
+ // arrange
+ RegionBSPTree1S orig = RegionBSPTree1S.fromInterval(AngularInterval.of(0, Geometry.PI, TEST_PRECISION));
+
+ // act
+ RegionBSPTree1S copy = orig.copy();
+
+ // assert
+ Assert.assertNotSame(orig, copy);
+
+ orig.setEmpty();
+
+ checkSingleInterval(copy, 0, Geometry.PI);
+ }
+
+ @Test
+ public void testFromInterval_full() {
+ // act
+ RegionBSPTree1S tree = RegionBSPTree1S.fromInterval(AngularInterval.full());
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ }
+
+ @Test
+ public void testFromInterval_nonFull() {
+ for (double theta = Geometry.ZERO_PI; theta <= Geometry.TWO_PI; theta += 0.2) {
+ // arrange
+ double min = theta;
+ double max = theta + Geometry.HALF_PI;
+
+ // act
+ RegionBSPTree1S tree = RegionBSPTree1S.fromInterval(AngularInterval.of(min, max, TEST_PRECISION));
+
+ checkSingleInterval(tree, min, max);
+
+ Assert.assertEquals(Geometry.HALF_PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(theta + (0.25 * Geometry.PI)),
+ tree.getBarycenter().getNormalizedAzimuth(), TEST_EPS);
+ }
+ }
+
+ @Test
+ public void testClassify_full() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.full();
+
+ // act/assert
+ for (double az = -Geometry.TWO_PI; az <= 2 * Geometry.TWO_PI; az += 0.2) {
+ checkClassify(tree, RegionLocation.INSIDE, az);
+ }
+ }
+
+ @Test
+ public void testClassify_empty() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+
+ // act/assert
+ for (double az = -Geometry.TWO_PI; az <= 2 * Geometry.TWO_PI; az += 0.2) {
+ checkClassify(tree, RegionLocation.OUTSIDE, az);
+ }
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.fromInterval(
+ AngularInterval.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI, TEST_PRECISION));
+
+ // act/assert
+ checkClassify(tree, RegionLocation.BOUNDARY,
+ Geometry.MINUS_HALF_PI, Geometry.HALF_PI,
+ Geometry.MINUS_HALF_PI - Geometry.TWO_PI, Geometry.HALF_PI + Geometry.TWO_PI);
+ checkClassify(tree, RegionLocation.INSIDE,
+ Geometry.ZERO_PI, 0.5, -0.5,
+ Geometry.TWO_PI, 0.5 + Geometry.TWO_PI, -0.5 - Geometry.TWO_PI);
+ checkClassify(tree, RegionLocation.OUTSIDE,
+ Geometry.PI, Geometry.PI + 0.5, Geometry.PI - 0.5,
+ Geometry.PI + Geometry.TWO_PI, Geometry.PI + 0.5 + Geometry.TWO_PI,
+ Geometry.PI - 0.5 + Geometry.TWO_PI);
+ }
+
+ @Test
+ public void testToIntervals_full() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.full();
+
+ // act
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+
+ AngularInterval interval = intervals.get(0);
+ Assert.assertTrue(interval.isFull());
+ }
+
+ @Test
+ public void testToIntervals_empty() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+
+ // act
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(0, intervals.size());
+ }
+
+ @Test
+ public void testToIntervals_singleCut() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+
+ for (double theta = 0; theta <= Geometry.TWO_PI; theta += 0.2) {
+ // act/assert
+ tree.setEmpty();
+ tree.getRoot().cut(CutAngle.createPositiveFacing(theta, TEST_PRECISION));
+
+ checkSingleInterval(tree, 0, theta);
+
+ tree.setEmpty();
+ tree.getRoot().cut(CutAngle.createNegativeFacing(theta, TEST_PRECISION));
+
+ checkSingleInterval(tree, theta, Geometry.TWO_PI);
+ }
+ }
+
+ @Test
+ public void testToIntervals_wrapAround_joinedIntervalsOnPositiveSide() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(0.25 * Geometry.PI, Geometry.HALF_PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(1.5 * Geometry.PI, 0.25 * Geometry.PI, TEST_PRECISION));
+
+ // act
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+
+ checkInterval(intervals.get(0), 1.5 * Geometry.PI, Geometry.HALF_PI);
+ }
+
+ @Test
+ public void testToIntervals_wrapAround_joinedIntervalsOnNegativeSide() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(1.75 * Geometry.PI, Geometry.HALF_PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(1.5 * Geometry.PI, 1.75 * Geometry.PI, TEST_PRECISION));
+
+ // act
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(1, intervals.size());
+
+ checkInterval(intervals.get(0), 1.5 * Geometry.PI, Geometry.HALF_PI);
+ }
+
+ @Test
+ public void testToIntervals_multipleIntervals() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(Geometry.PI - 0.5, Geometry.PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(Geometry.PI, Geometry.PI + 0.5, TEST_PRECISION));
+
+ // act
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(2, intervals.size());
+
+ checkInterval(intervals.get(0), Geometry.PI - 0.5, Geometry.PI + 0.5);
+ checkInterval(intervals.get(1), Geometry.MINUS_HALF_PI, Geometry.HALF_PI);
+ }
+
+ @Test
+ public void testToIntervals_multipleIntervals_complement() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(Geometry.PI - 0.5, Geometry.PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(Geometry.PI, Geometry.PI + 0.5, TEST_PRECISION));
+
+ tree.complement();
+
+ // act
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ // assert
+ Assert.assertEquals(2, intervals.size());
+
+ checkInterval(intervals.get(0), Geometry.HALF_PI, Geometry.PI - 0.5);
+ checkInterval(intervals.get(1), Geometry.PI + 0.5, Geometry.MINUS_HALF_PI);
+ }
+
+ @Test
+ public void testSplit_empty() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+
+ // act/assert
+ Assert.assertEquals(SplitLocation.NEITHER,
+ tree.split(CutAngle.createPositiveFacing(0, TEST_PRECISION)).getLocation());
+ Assert.assertEquals(SplitLocation.NEITHER,
+ tree.split(CutAngle.createNegativeFacing(Geometry.HALF_PI, TEST_PRECISION)).getLocation());
+ Assert.assertEquals(SplitLocation.NEITHER,
+ tree.split(CutAngle.createPositiveFacing(Geometry.PI, TEST_PRECISION)).getLocation());
+ Assert.assertEquals(SplitLocation.NEITHER,
+ tree.split(CutAngle.createNegativeFacing(Geometry.MINUS_HALF_PI, TEST_PRECISION)).getLocation());
+ Assert.assertEquals(SplitLocation.NEITHER,
+ tree.split(CutAngle.createPositiveFacing(Geometry.TWO_PI, TEST_PRECISION)).getLocation());
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.full();
+
+ // act/assert
+ checkSimpleSplit(
+ tree.split(CutAngle.createPositiveFacing(1e-6, TEST_PRECISION)),
+ AngularInterval.of(0, 1e-6, TEST_PRECISION),
+ AngularInterval.of(1e-6, Geometry.TWO_PI, TEST_PRECISION)
+ );
+ checkSimpleSplit(
+ tree.split(CutAngle.createNegativeFacing(Geometry.HALF_PI, TEST_PRECISION)),
+ AngularInterval.of(Geometry.HALF_PI, Geometry.TWO_PI, TEST_PRECISION),
+ AngularInterval.of(0, Geometry.HALF_PI, TEST_PRECISION)
+ );
+ checkSimpleSplit(
+ tree.split(CutAngle.createPositiveFacing(Geometry.PI, TEST_PRECISION)),
+ AngularInterval.of(0, Geometry.PI, TEST_PRECISION),
+ AngularInterval.of(Geometry.PI, Geometry.TWO_PI, TEST_PRECISION)
+ );
+ checkSimpleSplit(
+ tree.split(CutAngle.createNegativeFacing(Geometry.MINUS_HALF_PI, TEST_PRECISION)),
+ AngularInterval.of(Geometry.MINUS_HALF_PI, Geometry.TWO_PI, TEST_PRECISION),
+ AngularInterval.of(0, Geometry.MINUS_HALF_PI, TEST_PRECISION)
+ );
+ checkSimpleSplit(
+ tree.split(CutAngle.createPositiveFacing(Geometry.TWO_PI - 1e-6, TEST_PRECISION)),
+ AngularInterval.of(0, Geometry.TWO_PI - 1e-6, TEST_PRECISION),
+ AngularInterval.of(Geometry.TWO_PI - 1e-6, Geometry.TWO_PI, TEST_PRECISION)
+ );
+ }
+
+ @Test
+ public void testSplit_full_cutEquivalentToZero() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.full();
+
+ AngularInterval twoPi = AngularInterval.of(0, Geometry.TWO_PI, TEST_PRECISION);
+
+ // act/assert
+ checkSimpleSplit(
+ tree.split(CutAngle.createPositiveFacing(0, TEST_PRECISION)),
+ null,
+ twoPi
+ );
+ checkSimpleSplit(
+ tree.split(CutAngle.createNegativeFacing(0, TEST_PRECISION)),
+ twoPi,
+ null
+ );
+
+ checkSimpleSplit(
+ tree.split(CutAngle.createPositiveFacing(Geometry.TWO_PI - 1e-18, TEST_PRECISION)),
+ null,
+ twoPi
+ );
+ checkSimpleSplit(
+ tree.split(CutAngle.createNegativeFacing(Geometry.TWO_PI - 1e-18, TEST_PRECISION)),
+ twoPi,
+ null
+ );
+ }
+
+ @Test
+ public void testSplit_singleInterval() {
+ // arrange
+ AngularInterval interval = AngularInterval.of(Geometry.HALF_PI, Geometry.MINUS_HALF_PI, TEST_PRECISION);
+ RegionBSPTree1S tree = interval.toTree();
+
+ // act
+ checkSimpleSplit(
+ tree.split(CutAngle.createNegativeFacing(0, TEST_PRECISION)),
+ interval,
+ null
+ );
+ checkSimpleSplit(
+ tree.split(CutAngle.createNegativeFacing(-Geometry.TWO_PI, TEST_PRECISION)),
+ interval,
+ null
+ );
+
+ checkSimpleSplit(
+ tree.split(CutAngle.createPositiveFacing(Geometry.TWO_PI + Geometry.HALF_PI, TEST_PRECISION)),
+ null,
+ interval
+ );
+ checkSimpleSplit(
+ tree.split(CutAngle.createPositiveFacing(1.5 * Geometry.PI, TEST_PRECISION)),
+ interval,
+ null
+ );
+
+ checkSimpleSplit(
+ tree.split(CutAngle.createNegativeFacing(Geometry.PI, TEST_PRECISION)),
+ AngularInterval.of(Geometry.PI, Geometry.MINUS_HALF_PI, TEST_PRECISION),
+ AngularInterval.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION)
+ );
+ }
+
+ @Test
+ public void testSplit_singleIntervalSplitIntoTwoIntervalsOnSameSide() {
+ // arrange
+ RegionBSPTree1S tree = AngularInterval.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI, TEST_PRECISION).toTree();
+
+ CutAngle cut = CutAngle.createPositiveFacing(0, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S> split = tree.split(cut);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ Assert.assertNull(minus);
+
+ RegionBSPTree1S plus = split.getPlus();
+ List<AngularInterval> plusIntervals = plus.toIntervals();
+ Assert.assertEquals(1, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), Geometry.MINUS_HALF_PI, Geometry.HALF_PI);
+ }
+
+ @Test
+ public void testSplit_multipleRegions() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(Geometry.TWO_PI - 1, Geometry.HALF_PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(Geometry.PI, Geometry.MINUS_HALF_PI, TEST_PRECISION));
+
+ CutAngle cut = CutAngle.createNegativeFacing(1, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S> split = tree.split(cut);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ List<AngularInterval> minusIntervals = minus.toIntervals();
+ Assert.assertEquals(3, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 1, Geometry.HALF_PI);
+ checkInterval(minusIntervals.get(1), Geometry.PI, Geometry.MINUS_HALF_PI);
+ checkInterval(minusIntervals.get(2), Geometry.TWO_PI - 1, 0);
+
+ RegionBSPTree1S plus = split.getPlus();
+ List<AngularInterval> plusIntervals = plus.toIntervals();
+ Assert.assertEquals(1, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 0, 1);
+ }
+
+ @Test
+ public void testSplitDiameter_full() {
+ // arrange
+ RegionBSPTree1S full = RegionBSPTree1S.full();
+ CutAngle splitter = CutAngle.createPositiveFacing(Geometry.HALF_PI, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S> split = full.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ List<AngularInterval> minusIntervals = minus.toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 1.5 * Geometry.PI, 2.5 * Geometry.PI);
+
+ RegionBSPTree1S plus = split.getPlus();
+ List<AngularInterval> plusIntervals = plus.toIntervals();
+ Assert.assertEquals(1, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), Geometry.HALF_PI, 1.5 * Geometry.PI);
+ }
+
+ @Test
+ public void testSplitDiameter_empty() {
+ // arrange
+ RegionBSPTree1S empty = RegionBSPTree1S.empty();
+ CutAngle splitter = CutAngle.createPositiveFacing(Geometry.HALF_PI, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S> split = empty.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ Assert.assertNull(minus);
+
+ RegionBSPTree1S plus = split.getPlus();
+ Assert.assertNull(plus);
+ }
+
+ @Test
+ public void testSplitDiameter_minus_zeroOnMinusSide() {
+ // arrange
+ RegionBSPTree1S tree = AngularInterval.of(0, 1, TEST_PRECISION).toTree();
+ CutAngle splitter = CutAngle.createPositiveFacing(1, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S>split = tree.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ List<AngularInterval> minusIntervals = minus.toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 0, 1);
+
+ RegionBSPTree1S plus = split.getPlus();
+ Assert.assertNull(plus);
+ }
+
+ @Test
+ public void testSplitDiameter_minus_zeroOnPlusSide() {
+ // arrange
+ RegionBSPTree1S tree = AngularInterval.of(1, 2, TEST_PRECISION).toTree();
+ CutAngle splitter = CutAngle.createNegativeFacing(0, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S>split = tree.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ List<AngularInterval> minusIntervals = minus.toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 1, 2);
+
+ RegionBSPTree1S plus = split.getPlus();
+ Assert.assertNull(plus);
+ }
+
+ @Test
+ public void testSplitDiameter_plus_zeroOnMinusSide() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(1, 1.1, TEST_PRECISION));
+ tree.add(AngularInterval.of(2, 2.1, TEST_PRECISION));
+
+ CutAngle splitter = CutAngle.createPositiveFacing(1, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S>split = tree.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ Assert.assertNull(minus);
+
+ RegionBSPTree1S plus = split.getPlus();
+ List<AngularInterval> plusIntervals = plus.toIntervals();
+ Assert.assertEquals(2, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 1, 1.1);
+ checkInterval(plusIntervals.get(1), 2, 2.1);
+ }
+
+ @Test
+ public void testSplitDiameter_plus_zeroOnPlusSide() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(1, 1.1, TEST_PRECISION));
+ tree.add(AngularInterval.of(2, 2.1, TEST_PRECISION));
+
+ CutAngle splitter = CutAngle.createNegativeFacing(Geometry.PI - 1, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S>split = tree.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ Assert.assertNull(minus);
+
+ RegionBSPTree1S plus = split.getPlus();
+ List<AngularInterval> plusIntervals = plus.toIntervals();
+ Assert.assertEquals(2, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 1, 1.1);
+ checkInterval(plusIntervals.get(1), 2, 2.1);
+ }
+
+ @Test
+ public void testSplitDiameter_both_zeroOnMinusSide() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(1, 1.1, TEST_PRECISION));
+ tree.add(AngularInterval.of(2, 3, TEST_PRECISION));
+
+ CutAngle splitter = CutAngle.createPositiveFacing(2.5, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S>split = tree.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ List<AngularInterval> plusIntervals = minus.toIntervals();
+ Assert.assertEquals(2, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 1, 1.1);
+ checkInterval(plusIntervals.get(1), 2, 2.5);
+
+ RegionBSPTree1S plus = split.getPlus();
+ List<AngularInterval> minusIntervals = plus.toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 2.5, 3);
+ }
+
+ @Test
+ public void testSplitDiameter_both_zeroOnPlusSide() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(1, 1.1, TEST_PRECISION));
+ tree.add(AngularInterval.of(2, 3, TEST_PRECISION));
+
+ CutAngle splitter = CutAngle.createNegativeFacing(2.5, TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree1S>split = tree.splitDiameter(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ RegionBSPTree1S minus = split.getMinus();
+ List<AngularInterval> minusIntervals = minus.toIntervals();
+ Assert.assertEquals(1, minusIntervals.size());
+ checkInterval(minusIntervals.get(0), 2.5, 3);
+
+ RegionBSPTree1S plus = split.getPlus();
+ List<AngularInterval> plusIntervals = plus.toIntervals();
+ Assert.assertEquals(2, plusIntervals.size());
+ checkInterval(plusIntervals.get(0), 1, 1.1);
+ checkInterval(plusIntervals.get(1), 2, 2.5);
+ }
+
+ @Test
+ public void testRegionProperties_singleInterval_wrapsZero() {
+ // arrange
+ RegionBSPTree1S tree = AngularInterval.of(Geometry.MINUS_HALF_PI, Geometry.PI,
+ TEST_PRECISION).toTree();
+
+ // act/assert
+ Assert.assertEquals(1.5 * Geometry.PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.25 * Geometry.PI, tree.getBarycenter().getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testRegionProperties_singleInterval_doesNotWrap() {
+ // arrange
+ RegionBSPTree1S tree = AngularInterval.of(Geometry.HALF_PI, Geometry.TWO_PI,
+ TEST_PRECISION).toTree();
+
+ // act/assert
+ Assert.assertEquals(1.5 * Geometry.PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(1.25 * Geometry.PI, tree.getBarycenter().getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testRegionProperties_multipleIntervals_sameSize() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(0, 0.1, TEST_PRECISION));
+ tree.add(AngularInterval.of(0.2, 0.3, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertEquals(0.2, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.15, tree.getBarycenter().getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testRegionProperties_multipleIntervals_differentSizes() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(0, 0.2, TEST_PRECISION));
+ tree.add(AngularInterval.of(0.3, 0.7, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertEquals(0.6, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(2.2 / 6, tree.getBarycenter().getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform_fullAndEmpty() {
+ // arrange
+ RegionBSPTree1S full = RegionBSPTree1S.full();
+ RegionBSPTree1S empty = RegionBSPTree1S.empty();
+
+ // act
+ full.transform(PI_MINUS_AZ);
+ empty.transform(HALF_PI_PLUS_AZ);
+
+ // assert
+ Assert.assertTrue(full.isFull());
+ Assert.assertFalse(full.isEmpty());
+
+ Assert.assertFalse(empty.isFull());
+ Assert.assertTrue(empty.isEmpty());
+ }
+
+ @Test
+ public void testTransform_halfPiPlusAz() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(-1, 1, TEST_PRECISION));
+ tree.add(AngularInterval.of(2, 3, TEST_PRECISION));
+
+ // act
+ tree.transform(HALF_PI_PLUS_AZ);
+
+ // assert
+ Assert.assertEquals(3, tree.getSize(), TEST_EPS);
+
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ Assert.assertEquals(2, intervals.size());
+ checkInterval(intervals.get(0), Geometry.HALF_PI - 1, Geometry.HALF_PI + 1);
+ checkInterval(intervals.get(1), Geometry.HALF_PI + 2, Geometry.HALF_PI + 3);
+ }
+
+ @Test
+ public void testTransform_piMinusAz() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(-1, 1, TEST_PRECISION));
+ tree.add(AngularInterval.of(2, 3, TEST_PRECISION));
+
+ // act
+ tree.transform(PI_MINUS_AZ);
+
+ // assert
+ Assert.assertEquals(3, tree.getSize(), TEST_EPS);
+
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ Assert.assertEquals(2, intervals.size());
+ checkInterval(intervals.get(0), Geometry.PI - 3, Geometry.PI - 2);
+ checkInterval(intervals.get(1), Geometry.PI - 1, Geometry.PI + 1);
+ }
+
+ @Test
+ public void testProject_fullAndEmpty() {
+ // arrange
+ RegionBSPTree1S full = RegionBSPTree1S.full();
+ RegionBSPTree1S empty = RegionBSPTree1S.empty();
+
+ // act/assert
+ Assert.assertNull(full.project(Point1S.ZERO));
+ Assert.assertNull(full.project(Point1S.PI));
+
+ Assert.assertNull(empty.project(Point1S.ZERO));
+ Assert.assertNull(empty.project(Point1S.PI));
+ }
+
+ @Test
+ public void testProject_withIntervals() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(Geometry.PI - 1, Geometry.PI + 1, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertEquals(Geometry.MINUS_HALF_PI,
+ tree.project(Point1S.of(Geometry.MINUS_HALF_PI - 0.1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(Geometry.MINUS_HALF_PI,
+ tree.project(Point1S.of(Geometry.MINUS_HALF_PI)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(Geometry.MINUS_HALF_PI,
+ tree.project(Point1S.of(Geometry.MINUS_HALF_PI + 0.1)).getAzimuth(), TEST_EPS);
+
+ Assert.assertEquals(Geometry.MINUS_HALF_PI, tree.project(Point1S.of(-0.1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, tree.project(Point1S.ZERO).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, tree.project(Point1S.of(0.1)).getAzimuth(), TEST_EPS);
+
+ Assert.assertEquals(Geometry.PI - 1,
+ tree.project(Point1S.of(Geometry.PI - 0.5)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(Geometry.PI + 1,
+ tree.project(Point1S.of(Geometry.PI + 0.5)).getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_equidistant() {
+ // arrange
+ RegionBSPTree1S tree = AngularInterval.of(1, 2, TEST_PRECISION).toTree();
+ RegionBSPTree1S treeComplement = tree.copy();
+ treeComplement.complement();
+
+ // act/assert
+ Assert.assertEquals(1, tree.project(Point1S.of(1.5)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(1, treeComplement.project(Point1S.of(1.5)).getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_intervalAroundZero_closerOnMinSide() {
+ // arrange
+ double start = -1;
+ double end = 0.5;
+ RegionBSPTree1S tree = AngularInterval.of(start, end, TEST_PRECISION).toTree();
+
+ // act/assert
+ Assert.assertEquals(end, tree.project(Point1S.of(-1.5 * Geometry.PI)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-Geometry.PI)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-0.5 * Geometry.PI)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-0.5)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(-0.25)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(-0.1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.ZERO).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(0.1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(0.25)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(0.5)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(0.75)).getAzimuth(), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_intervalAroundZero_closerOnMaxSide() {
+ // arrange
+ double start = -0.5;
+ double end = 1;
+ RegionBSPTree1S tree = AngularInterval.of(start, end, TEST_PRECISION).toTree();
+
+ // act/assert
+ Assert.assertEquals(end, tree.project(Point1S.of(-1.5 * Geometry.PI)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(-Geometry.PI)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-0.5 * Geometry.PI)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-0.5)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-0.25)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(-0.1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.ZERO).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(start, tree.project(Point1S.of(0.1)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(0.25)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(0.5)).getAzimuth(), TEST_EPS);
+ Assert.assertEquals(end, tree.project(Point1S.of(0.75)).getAzimuth(), TEST_EPS);
+ }
+
+ private static void checkSimpleSplit(Split<RegionBSPTree1S> split, AngularInterval minusInterval,
+ AngularInterval plusInterval) {
+
+ RegionBSPTree1S minus = split.getMinus();
+ if (minusInterval != null) {
+ Assert.assertNotNull("Expected minus region to not be null", minus);
+ checkSingleInterval(minus, minusInterval.getMin(), minusInterval.getMax());
+ }
+ else {
+ Assert.assertNull("Expected minus region to be null", minus);
+ }
+
+ RegionBSPTree1S plus = split.getPlus();
+ if (plusInterval != null) {
+ Assert.assertNotNull("Expected plus region to not be null", plus);
+ checkSingleInterval(plus, plusInterval.getMin(), plusInterval.getMax());
+ }
+ else {
+ Assert.assertNull("Expected plus region to be null", plus);
+ }
+ }
+
+ private static void checkSingleInterval(RegionBSPTree1S tree, double min, double max) {
+ List<AngularInterval> intervals = tree.toIntervals();
+
+ Assert.assertEquals("Expected a single interval in the tree", 1, intervals.size());
+
+ checkInterval(intervals.get(0), min, max);
+ }
+
+ private static void checkInterval(AngularInterval interval, double min, double max) {
+ double normalizedMin = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(min);
+ double normalizedMax = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(max);
+
+ if (TEST_PRECISION.eq(normalizedMin, normalizedMax)) {
+ Assert.assertTrue(interval.isFull());
+ }
+ else {
+ Assert.assertEquals(normalizedMin,
+ interval.getMinBoundary().getPoint().getNormalizedAzimuth(), TEST_EPS);
+ Assert.assertEquals(normalizedMax,
+ interval.getMaxBoundary().getPoint().getNormalizedAzimuth(), TEST_EPS);
+ }
+ }
+
+ private static void checkClassify(Region<Point1S> region, RegionLocation loc, double ... pts) {
+ for (double pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, loc, region.classify(Point1S.of(pt)));
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/S1PointTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/S1PointTest.java
deleted file mode 100644
index f67fc50..0000000
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/S1PointTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.oned;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class S1PointTest {
-
- private static final double EPS = 1e-10;
-
- @Test
- public void testS1Point() {
- for (int k = -2; k < 3; ++k) {
- S1Point p = S1Point.of(1.0 + k * Geometry.TWO_PI);
- Assert.assertEquals(Math.cos(1.0), p.getVector().getX(), EPS);
- Assert.assertEquals(Math.sin(1.0), p.getVector().getY(), EPS);
- Assert.assertFalse(p.isNaN());
- }
- }
-
- @Test
- public void testNaN() {
- Assert.assertTrue(S1Point.NaN.isNaN());
- Assert.assertTrue(S1Point.NaN.equals(S1Point.of(Double.NaN)));
- Assert.assertFalse(S1Point.of(1.0).equals(S1Point.NaN));
- }
-
- @Test
- public void testEquals() {
- S1Point a = S1Point.of(1.0);
- S1Point b = S1Point.of(1.0);
- Assert.assertEquals(a.hashCode(), b.hashCode());
- Assert.assertFalse(a == b);
- Assert.assertTrue(a.equals(b));
- Assert.assertTrue(a.equals(a));
- Assert.assertFalse(a.equals('a'));
- }
-
- @Test
- public void testDistance() {
- S1Point a = S1Point.of(1.0);
- S1Point b = S1Point.of(a.getAzimuth() + 0.5 * Math.PI);
- Assert.assertEquals(0.5 * Math.PI, a.distance(b), 1.0e-10);
- }
-
- @Test
- public void testToString() {
- // act/assert
- Assert.assertEquals("(0.0)", S1Point.of(0.0).toString());
- Assert.assertEquals("(1.0)", S1Point.of(1.0).toString());
- }
-
- @Test
- public void testParse() {
- // act/assert
- checkPoint(S1Point.parse("(0)"), 0.0);
- checkPoint(S1Point.parse("(1)"), 1.0);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testParse_failure() {
- // act/assert
- S1Point.parse("abc");
- }
-
- private void checkPoint(S1Point p, double alpha) {
- Assert.assertEquals(alpha, p.getAzimuth(), EPS);
- }
-
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Transform1STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Transform1STest.java
new file mode 100644
index 0000000..7248ce7
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Transform1STest.java
@@ -0,0 +1,254 @@
+/*
+ * 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.oned;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Transform1STest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final Point1S ZERO = Point1S.ZERO;
+
+ private static final Point1S HALF_PI = Point1S.of(Geometry.HALF_PI);
+
+ private static final Point1S PI = Point1S.of(Geometry.PI);
+
+ private static final Point1S MINUS_HALF_PI = Point1S.of(Geometry.MINUS_HALF_PI);
+
+ @Test
+ public void testIdentity() {
+ // act
+ Transform1S t = Transform1S.identity();
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+ Assert.assertFalse(t.isNegation());
+ Assert.assertEquals(0, t.getRotation(), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(ZERO, t.apply(ZERO), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(HALF_PI, t.apply(HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(PI, t.apply(PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(MINUS_HALF_PI, t.apply(MINUS_HALF_PI), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testRotate_positive() {
+ // arrange
+ Transform1S t = Transform1S.createRotation(Geometry.HALF_PI);
+
+ // act/assert
+ Assert.assertTrue(t.preservesOrientation());
+ Assert.assertFalse(t.isNegation());
+ Assert.assertEquals(Geometry.HALF_PI, t.getRotation(), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(HALF_PI, t.apply(ZERO), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(PI, t.apply(HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.of(1.5 * Geometry.PI), t.apply(PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.ZERO, t.apply(MINUS_HALF_PI), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testRotate_negative() {
+ // arrange
+ Transform1S t = Transform1S.createRotation(-Geometry.HALF_PI);
+
+ // act/assert
+ Assert.assertTrue(t.preservesOrientation());
+ Assert.assertFalse(t.isNegation());
+ Assert.assertEquals(-Geometry.HALF_PI, t.getRotation(), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(MINUS_HALF_PI, t.apply(ZERO), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(ZERO, t.apply(HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(HALF_PI, t.apply(PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.of(-Geometry.PI), t.apply(MINUS_HALF_PI), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testNegate() {
+ // arrange
+ Transform1S t = Transform1S.createNegation();
+
+ // act/assert
+ Assert.assertFalse(t.preservesOrientation());
+ Assert.assertTrue(t.isNegation());
+ Assert.assertEquals(0, t.getRotation(), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(ZERO, t.apply(ZERO), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(MINUS_HALF_PI, t.apply(HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.of(-Geometry.PI), t.apply(PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(HALF_PI, t.apply(MINUS_HALF_PI), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testNegateThenRotate() {
+ // arrange
+ Transform1S t = Transform1S.createNegation().rotate(Geometry.HALF_PI);
+
+ // act/assert
+ Assert.assertFalse(t.preservesOrientation());
+ Assert.assertTrue(t.isNegation());
+ Assert.assertEquals(Geometry.HALF_PI, t.getRotation(), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(HALF_PI, t.apply(ZERO), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(ZERO, t.apply(HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(MINUS_HALF_PI, t.apply(PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(PI, t.apply(MINUS_HALF_PI), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testRotateThenNegate() {
+ // arrange
+ Transform1S t = Transform1S.createRotation(Geometry.HALF_PI).negate();
+
+ // act/assert
+ Assert.assertFalse(t.preservesOrientation());
+ Assert.assertTrue(t.isNegation());
+ Assert.assertEquals(-Geometry.HALF_PI, t.getRotation(), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(MINUS_HALF_PI, t.apply(ZERO), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.of(-Geometry.PI), t.apply(HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.of(-1.5 * Geometry.PI), t.apply(PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(ZERO, t.apply(MINUS_HALF_PI), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testMultiply() {
+ // arrange
+ Transform1S neg = Transform1S.identity().negate();
+ Transform1S rot = Transform1S.identity().rotate(Geometry.HALF_PI);
+
+ // act
+ Transform1S t = rot.multiply(neg);
+
+ // assert
+ Assert.assertFalse(t.preservesOrientation());
+ Assert.assertTrue(t.isNegation());
+ Assert.assertEquals(Geometry.HALF_PI, t.getRotation(), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(HALF_PI, t.apply(ZERO), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(ZERO, t.apply(HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(MINUS_HALF_PI, t.apply(PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(PI, t.apply(MINUS_HALF_PI), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testPreultiply() {
+ // arrange
+ Transform1S neg = Transform1S.identity().negate();
+ Transform1S rot = Transform1S.identity().rotate(Geometry.HALF_PI);
+
+ // act
+ Transform1S t = neg.premultiply(rot);
+
+ // assert
+ Assert.assertFalse(t.preservesOrientation());
+ Assert.assertTrue(t.isNegation());
+ Assert.assertEquals(Geometry.HALF_PI, t.getRotation(), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(HALF_PI, t.apply(ZERO), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(ZERO, t.apply(HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(MINUS_HALF_PI, t.apply(PI), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(PI, t.apply(MINUS_HALF_PI), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testHashCode() {
+ // arrange
+ Transform1S a = Transform1S.identity().negate().rotate(Geometry.HALF_PI);
+ Transform1S b = Transform1S.identity().rotate(Geometry.HALF_PI);
+ Transform1S c = Transform1S.identity().negate().rotate(-Geometry.HALF_PI);
+ Transform1S d = Transform1S.identity().negate().rotate(Geometry.HALF_PI);
+
+ // act/assert
+ Assert.assertTrue(a.equals(a));
+
+ Assert.assertFalse(a.equals(null));
+ Assert.assertFalse(a.equals(new Object()));
+
+ Assert.assertFalse(a.equals(b));
+ Assert.assertFalse(a.equals(c));
+
+ Assert.assertTrue(a.equals(d));
+ Assert.assertTrue(d.equals(a));
+ }
+
+ @Test
+ public void testEquals() {
+ // arrange
+ Transform1S a = Transform1S.identity().negate().rotate(Geometry.HALF_PI);
+ Transform1S b = Transform1S.identity().rotate(Geometry.HALF_PI);
+ Transform1S c = Transform1S.identity().negate().rotate(-Geometry.HALF_PI);
+ Transform1S d = Transform1S.identity().negate().rotate(Geometry.HALF_PI);
+
+ // act
+ int hash = a.hashCode();
+
+ // assert
+ Assert.assertEquals(hash, a.hashCode());
+
+ Assert.assertNotEquals(hash, b.hashCode());
+ Assert.assertNotEquals(hash, c.hashCode());
+
+ Assert.assertEquals(hash, d.hashCode());
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Transform1S t = Transform1S.identity().negate().rotate(1);
+
+ // act
+ String str = t.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("Transform1S", str);
+ GeometryTestUtils.assertContains("negate= true", str);
+ GeometryTestUtils.assertContains("rotate= 1", str);
+ }
+
+ private static void checkInverse(Transform1S t) {
+ Transform1S inv = t.inverse();
+
+ for (double x = -Geometry.TWO_PI; x <= 2 * Geometry.TWO_PI; x += 0.2) {
+ Point1S pt = Point1S.of(x);
+
+ SphericalTestUtils.assertPointsEqual(pt, inv.apply(t.apply(pt)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(pt, t.apply(inv.apply(pt)), TEST_EPS);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/AbstractGreatArcPathConnectorTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/AbstractGreatArcPathConnectorTest.java
new file mode 100644
index 0000000..76ea922
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/AbstractGreatArcPathConnectorTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AbstractGreatArcPathConnectorTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final GreatCircle XY_PLANE = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ private static final GreatCircle XZ_PLANE = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ private TestConnector connector = new TestConnector();
+
+ @Test
+ public void testConnectAll_emptyCollection() {
+ // act
+ List<GreatArcPath> paths = connector.connectAll(Collections.emptyList());
+
+ // assert
+ Assert.assertEquals(0, paths.size());
+ }
+
+ @Test
+ public void testConnectAll_singleFullArc() {
+ // act
+ connector.add(Arrays.asList(XY_PLANE.span()));
+ List<GreatArcPath> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ GreatArcPath a = paths.get(0);
+ Assert.assertEquals(1, a.getArcs().size());
+ Assert.assertSame(XY_PLANE, a.getStartArc().getCircle());
+ }
+
+ @Test
+ public void testConnectAll_twoFullArcs() {
+ // act
+ connector.add(XZ_PLANE.span());
+ List<GreatArcPath> paths = connector.connectAll(Arrays.asList(XY_PLANE.span()));
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ GreatArcPath a = paths.get(0);
+ Assert.assertEquals(1, a.getArcs().size());
+ Assert.assertSame(XY_PLANE, a.getStartArc().getCircle());
+
+ GreatArcPath b = paths.get(1);
+ Assert.assertEquals(1, b.getArcs().size());
+ Assert.assertSame(XZ_PLANE, b.getStartArc().getCircle());
+ }
+
+ @Test
+ public void testConnectAll_singleLune() {
+ // arrange
+ GreatCircle upperBound = GreatCircle.fromPoleAndU(
+ Vector3D.of(0, 1, -1), Vector3D.Unit.PLUS_X , TEST_PRECISION);
+
+ connector.add(XY_PLANE.arc(0, Geometry.PI));
+ connector.add(upperBound.arc(Geometry.PI, 0));
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ GreatArcPath a = paths.get(0);
+ Assert.assertEquals(2, a.getArcs().size());
+ Assert.assertSame(XY_PLANE, a.getStartArc().getCircle());
+ Assert.assertSame(upperBound, a.getEndArc().getCircle());
+ }
+
+ @Test
+ public void testConnectAll_singleLune_pathsNotOrientedCorrectly() {
+ // arrange
+ GreatCircle upperBound = GreatCircle.fromPoleAndU(
+ Vector3D.of(0, 1, -1), Vector3D.Unit.PLUS_X , TEST_PRECISION);
+
+ connector.add(XY_PLANE.arc(0, Geometry.PI));
+ connector.add(upperBound.arc(0, Geometry.PI));
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ GreatArcPath a = paths.get(0);
+ Assert.assertEquals(1, a.getArcs().size());
+ Assert.assertSame(XY_PLANE, a.getStartArc().getCircle());
+
+ GreatArcPath b = paths.get(1);
+ Assert.assertEquals(1, b.getArcs().size());
+ Assert.assertSame(upperBound, b.getStartArc().getCircle());
+ }
+
+ @Test
+ public void testConnectAll_largeTriangle() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_I;
+ Point2S p2 = Point2S.PLUS_J;
+ Point2S p3 = Point2S.PLUS_K;
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll(Arrays.asList(
+ GreatArc.fromPoints(p1, p2, TEST_PRECISION),
+ GreatArc.fromPoints(p2, p3, TEST_PRECISION),
+ GreatArc.fromPoints(p3, p1, TEST_PRECISION)
+ ));
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ GreatArcPath a = paths.get(0);
+ Assert.assertEquals(3, a.getArcs().size());
+
+ assertPathPoints(a, p3, p1, p2, p3);
+ }
+
+ @Test
+ public void testConnectAll_smallTriangleWithDisconnectedLuneAndArc() {
+ // arrange
+ Point2S p1 = Point2S.of(0, 0);
+ Point2S p2 = Point2S.of(0, 0.1 * Geometry.PI);
+ Point2S p3 = Point2S.of(0.1, 0.1 * Geometry.PI);
+
+ GreatArc luneEdge1 = GreatCircle.fromPoints(
+ Point2S.PLUS_J,
+ Point2S.MINUS_I,
+ TEST_PRECISION)
+ .arc(0, Geometry.PI);
+ GreatArc luneEdge2 = GreatCircle.fromPoints(
+ Point2S.MINUS_J,
+ Point2S.of(Geometry.HALF_PI, 0.4 * Geometry.PI),
+ TEST_PRECISION)
+ .arc(0, Geometry.PI);
+
+ GreatArc separateArc = GreatArc.fromPoints(
+ Point2S.of(Geometry.MINUS_HALF_PI, 0.7 * Geometry.PI),
+ Point2S.of(Geometry.MINUS_HALF_PI, 0.8 * Geometry.PI),
+ TEST_PRECISION);
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll(Arrays.asList(
+ luneEdge1,
+ GreatArc.fromPoints(p2, p3, TEST_PRECISION),
+ separateArc,
+ GreatArc.fromPoints(p1, p2, TEST_PRECISION),
+ GreatArc.fromPoints(p3, p1, TEST_PRECISION),
+ luneEdge2
+ ));
+
+ // assert
+ Assert.assertEquals(3, paths.size());
+
+ GreatArcPath triangle = paths.get(0);
+ Assert.assertEquals(3, triangle.getArcs().size());
+ assertPathPoints(triangle, p1, p2, p3, p1);
+
+ GreatArcPath lune = paths.get(1);
+ Assert.assertEquals(2, lune.getArcs().size());
+ Assert.assertSame(luneEdge1, lune.getStartArc());
+ Assert.assertSame(luneEdge2, lune.getEndArc());
+
+ GreatArcPath separate = paths.get(2);
+ Assert.assertEquals(1, separate.getArcs().size());
+ Assert.assertSame(separateArc, separate.getStartArc());
+ }
+
+ @Test
+ public void testConnectAll_choosesBestPointLikeConnection() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
+
+ Point2S p1 = Point2S.PLUS_I;
+ Point2S p2 = Point2S.of(1, Geometry.HALF_PI);
+ Point2S p3 = Point2S.of(1.001, 0.491 * Geometry.PI);
+ Point2S p4 = Point2S.of(1.001, 0.502 * Geometry.PI);
+
+ connector.add(GreatArc.fromPoints(p2, p3, TEST_PRECISION));
+ connector.add(GreatArc.fromPoints(p2, p4, TEST_PRECISION));
+ connector.add(GreatArc.fromPoints(p1, p2, precision));
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ GreatArcPath a = paths.get(0);
+ Assert.assertEquals(2, a.getArcs().size());
+ assertPathPoints(a, p1, p2, p4);
+
+ GreatArcPath b = paths.get(1);
+ Assert.assertEquals(1, b.getArcs().size());
+ assertPathPoints(b, p2, p3);
+ }
+
+ @Test
+ public void testConnect() {
+ // arrange
+ GreatArc arcA = GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatArc arcB = GreatArc.fromPoints(Point2S.PLUS_J, Point2S.MINUS_I, TEST_PRECISION);
+ GreatArc arcC = GreatArc.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ // act
+ connector.connect(Arrays.asList(
+ arcB,
+ arcA
+ ));
+
+ connector.connect(Arrays.asList(arcC));
+
+ List<GreatArcPath> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ GreatArcPath a = paths.get(0);
+ Assert.assertEquals(2, a.getArcs().size());
+ assertPathPoints(a, Point2S.PLUS_I, Point2S.PLUS_J, Point2S.MINUS_I);
+
+ GreatArcPath b = paths.get(1);
+ Assert.assertEquals(1, b.getArcs().size());
+ assertPathPoints(b, Point2S.PLUS_J, Point2S.PLUS_K);
+ }
+
+ @Test
+ public void testConnectorCanBeReused() {
+ // arrange
+ GreatArc a = GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatArc b = GreatArc.fromPoints(Point2S.MINUS_I, Point2S.MINUS_J, TEST_PRECISION);
+
+ // act
+ List<GreatArcPath> path1 = connector.connectAll(Arrays.asList(a));
+ List<GreatArcPath> path2 = connector.connectAll(Arrays.asList(b));
+
+ // assert
+ Assert.assertEquals(1, path1.size());
+ assertPathPoints(path1.get(0), Point2S.PLUS_I, Point2S.PLUS_J);
+
+ Assert.assertEquals(1, path2.size());
+ assertPathPoints(path2.get(0), Point2S.MINUS_I, Point2S.MINUS_J);
+ }
+
+ private static void assertPathPoints(GreatArcPath path, Point2S ... points) {
+ List<Point2S> expectedPoints = Arrays.asList(points);
+ List<Point2S> actualPoints = path.getVertices();
+
+ String msg = "Expected path points to equal " + expectedPoints + " but was " + actualPoints;
+ Assert.assertEquals(msg, expectedPoints.size(), actualPoints.size());
+
+ for (int i=0; i<expectedPoints.size(); ++i) {
+ SphericalTestUtils.assertPointsEq(expectedPoints.get(i), actualPoints.get(i), TEST_EPS);
+ }
+ }
+
+ private static class TestConnector extends AbstractGreatArcConnector {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected ConnectableGreatArc selectConnection(ConnectableGreatArc incoming,
+ List<ConnectableGreatArc> outgoing) {
+
+ // just choose the first element
+ return outgoing.get(0);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/CircleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/CircleTest.java
deleted file mode 100644
index b81a44b..0000000
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/CircleTest.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * 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 org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.partitioning.Transform;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.spherical.oned.Arc;
-import org.apache.commons.geometry.spherical.oned.LimitAngle;
-import org.apache.commons.geometry.spherical.oned.S1Point;
-import org.apache.commons.geometry.spherical.oned.SubLimitAngle;
-import org.apache.commons.rng.UniformRandomProvider;
-import org.apache.commons.rng.sampling.UnitSphereSampler;
-import org.apache.commons.rng.simple.RandomSource;
-import org.junit.Assert;
-import org.junit.Test;
-
-
-public class CircleTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testEquator() {
- Circle circle = new Circle(Vector3D.of(0, 0, 1000), TEST_PRECISION).copySelf();
- Assert.assertEquals(Vector3D.Unit.PLUS_Z, circle.getPole());
- Assert.assertEquals(TEST_PRECISION, circle.getPrecision());
- circle.revertSelf();
- Assert.assertEquals(Vector3D.Unit.MINUS_Z, circle.getPole());
- Assert.assertEquals(Vector3D.Unit.PLUS_Z, circle.getReverse().getPole());
- Assert.assertEquals(Vector3D.Unit.MINUS_Z, circle.getPole());
- }
-
- @Test
- public void testXY() {
- Circle circle = new Circle(S2Point.of(1.2, 2.5), S2Point.of(-4.3, 0), TEST_PRECISION);
- Assert.assertEquals(0.0, circle.getPointAt(0).distance(circle.getXAxis()), TEST_EPS);
- Assert.assertEquals(0.0, circle.getPointAt(0.5 * Math.PI).distance(circle.getYAxis()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, circle.getXAxis().angle(circle.getYAxis()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, circle.getXAxis().angle(circle.getPole()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, circle.getPole().angle(circle.getYAxis()), TEST_EPS);
- Assert.assertEquals(0.0,
- circle.getPole().distance(circle.getXAxis().cross(circle.getYAxis())),
- TEST_EPS);
- }
-
- @Test
- public void testReverse() {
- Circle circle = new Circle(S2Point.of(1.2, 2.5), S2Point.of(-4.3, 0), TEST_PRECISION);
- Circle reversed = circle.getReverse();
- Assert.assertEquals(0.0, reversed.getPointAt(0).distance(reversed.getXAxis()), TEST_EPS);
- Assert.assertEquals(0.0, reversed.getPointAt(0.5 * Math.PI).distance(reversed.getYAxis()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, reversed.getXAxis().angle(reversed.getYAxis()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, reversed.getXAxis().angle(reversed.getPole()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, reversed.getPole().angle(reversed.getYAxis()), TEST_EPS);
- Assert.assertEquals(0.0,
- reversed.getPole().distance(reversed.getXAxis().cross(reversed.getYAxis())),
- TEST_EPS);
-
- Assert.assertEquals(0, circle.getXAxis().angle(reversed.getXAxis()), TEST_EPS);
- Assert.assertEquals(Math.PI, circle.getYAxis().angle(reversed.getYAxis()), TEST_EPS);
- Assert.assertEquals(Math.PI, circle.getPole().angle(reversed.getPole()), TEST_EPS);
-
- Assert.assertTrue(circle.sameOrientationAs(circle));
- Assert.assertFalse(circle.sameOrientationAs(reversed));
- }
-
- @Test
- public void testPhase() {
- Circle circle = new Circle(S2Point.of(1.2, 2.5), S2Point.of(-4.3, 0), TEST_PRECISION);
- Vector3D p = Vector3D.of(1, 2, -4);
- Vector3D samePhase = circle.getPointAt(circle.getPhase(p));
- Assert.assertEquals(0.0,
- circle.getPole().cross(p).angle(
- circle.getPole().cross(samePhase)),
- TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, circle.getPole().angle(samePhase), TEST_EPS);
- Assert.assertEquals(circle.getPhase(p), circle.getPhase(samePhase), TEST_EPS);
- Assert.assertEquals(0.0, circle.getPhase(circle.getXAxis()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, circle.getPhase(circle.getYAxis()), TEST_EPS);
- }
-
- @Test
- public void testSubSpace() {
- Circle circle = new Circle(S2Point.of(1.2, 2.5), S2Point.of(-4.3, 0), TEST_PRECISION);
- Assert.assertEquals(0.0, circle.toSubSpace(S2Point.ofVector(circle.getXAxis())).getAzimuth(), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, circle.toSubSpace(S2Point.ofVector(circle.getYAxis())).getAzimuth(), TEST_EPS);
- Vector3D p = Vector3D.of(1, 2, -4);
- Assert.assertEquals(circle.getPhase(p), circle.toSubSpace(S2Point.ofVector(p)).getAzimuth(), TEST_EPS);
- }
-
- @Test
- public void testSpace() {
- Circle circle = new Circle(S2Point.of(1.2, 2.5), S2Point.of(-4.3, 0), TEST_PRECISION);
- for (double alpha = 0; alpha < Geometry.TWO_PI; alpha += 0.1) {
- Vector3D p = Vector3D.linearCombination(Math.cos(alpha), circle.getXAxis(),
- Math.sin(alpha), circle.getYAxis());
- Vector3D q = circle.toSpace(S1Point.of(alpha)).getVector();
- Assert.assertEquals(0.0, p.distance(q), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, circle.getPole().angle(q), TEST_EPS);
- }
- }
-
- @Test
- public void testOffset() {
- Circle circle = new Circle(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
- Assert.assertEquals(0.0, circle.getOffset(S2Point.ofVector(Vector3D.Unit.PLUS_X)), TEST_EPS);
- Assert.assertEquals(0.0, circle.getOffset(S2Point.ofVector(Vector3D.Unit.MINUS_X)), TEST_EPS);
- Assert.assertEquals(0.0, circle.getOffset(S2Point.ofVector(Vector3D.Unit.PLUS_Y)), TEST_EPS);
- Assert.assertEquals(0.0, circle.getOffset(S2Point.ofVector(Vector3D.Unit.MINUS_Y)), TEST_EPS);
- Assert.assertEquals(-0.5 * Math.PI, circle.getOffset(S2Point.ofVector(Vector3D.Unit.PLUS_Z)), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, circle.getOffset(S2Point.ofVector(Vector3D.Unit.MINUS_Z)), TEST_EPS);
-
- }
-
- @Test
- public void testInsideArc() {
- UnitSphereSampler sphRandom = new UnitSphereSampler(3, RandomSource.create(RandomSource.WELL_1024_A,
- 0xbfd34e92231bbcfel));
- for (int i = 0; i < 100; ++i) {
- Circle c1 = new Circle(Vector3D.of(sphRandom.nextVector()), TEST_PRECISION);
- Circle c2 = new Circle(Vector3D.of(sphRandom.nextVector()), TEST_PRECISION);
- checkArcIsInside(c1, c2);
- checkArcIsInside(c2, c1);
- }
- }
-
- private void checkArcIsInside(final Circle arcCircle, final Circle otherCircle) {
- Arc arc = arcCircle.getInsideArc(otherCircle);
- Assert.assertEquals(Math.PI, arc.getSize(), TEST_EPS);
- for (double alpha = arc.getInf(); alpha < arc.getSup(); alpha += 0.1) {
- Assert.assertTrue(otherCircle.getOffset(arcCircle.getPointAt(alpha)) <= 2.0e-15);
- }
- for (double alpha = arc.getSup(); alpha < arc.getInf() + Geometry.TWO_PI; alpha += 0.1) {
- Assert.assertTrue(otherCircle.getOffset(arcCircle.getPointAt(alpha)) >= -2.0e-15);
- }
- }
-
- @Test
- public void testTransform() {
- UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A,
- 0x16992fc4294bf2f1l);
- UnitSphereSampler sphRandom = new UnitSphereSampler(3, random);
- for (int i = 0; i < 100; ++i) {
-
- QuaternionRotation r = QuaternionRotation.fromAxisAngle(Vector3D.of(sphRandom.nextVector()),
- Math.PI * random.nextDouble());
- Transform<S2Point, S1Point> t = Circle.getTransform(r);
-
- S2Point p = S2Point.ofVector(Vector3D.of(sphRandom.nextVector()));
- S2Point tp = t.apply(p);
- Assert.assertEquals(0.0, r.apply(p.getVector()).distance(tp.getVector()), TEST_EPS);
-
- Circle c = new Circle(Vector3D.of(sphRandom.nextVector()), TEST_PRECISION);
- Circle tc = (Circle) t.apply(c);
- Assert.assertEquals(0.0, r.apply(c.getPole()).distance(tc.getPole()), TEST_EPS);
- Assert.assertEquals(0.0, r.apply(c.getXAxis()).distance(tc.getXAxis()), TEST_EPS);
- Assert.assertEquals(0.0, r.apply(c.getYAxis()).distance(tc.getYAxis()), TEST_EPS);
- Assert.assertSame(c.getPrecision(), ((Circle) t.apply(c)).getPrecision());
-
- SubLimitAngle sub = new LimitAngle(S1Point.of(Geometry.TWO_PI * random.nextDouble()),
- random.nextBoolean(), TEST_PRECISION).wholeHyperplane();
- Vector3D psub = c.getPointAt(((LimitAngle) sub.getHyperplane()).getLocation().getAzimuth());
- SubLimitAngle tsub = (SubLimitAngle) t.apply(sub, c, tc);
- Vector3D ptsub = tc.getPointAt(((LimitAngle) tsub.getHyperplane()).getLocation().getAzimuth());
- Assert.assertEquals(0.0, r.apply(psub).distance(ptsub), TEST_EPS);
-
- }
- }
-
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java
new file mode 100644
index 0000000..d626f96
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java
@@ -0,0 +1,795 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ConvexArea2STest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ // epsilon value for use when comparing computed barycenter locations;
+ // this must currently be set much higher than the other epsilon
+ private static final double BARYCENTER_EPS = 1e-2;
+
+ @Test
+ public void testFull() {
+ // act
+ ConvexArea2S area = ConvexArea2S.full();
+
+ // assert
+ Assert.assertTrue(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(0, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(4 * Geometry.PI, area.getSize(), TEST_EPS);
+ Assert.assertNull(area.getBarycenter());
+
+ Assert.assertEquals(0, area.getBoundaries().size());
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.PLUS_I, Point2S.MINUS_I,
+ Point2S.PLUS_J, Point2S.MINUS_J,
+ Point2S.PLUS_K, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromBounds_empty() {
+ // act
+ ConvexArea2S area = ConvexArea2S.fromBounds();
+
+ // assert
+ Assert.assertTrue(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(0, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(4 * Geometry.PI, area.getSize(), TEST_EPS);
+ Assert.assertNull(area.getBarycenter());
+
+ Assert.assertEquals(0, area.getBoundaries().size());
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.PLUS_I, Point2S.MINUS_I,
+ Point2S.PLUS_J, Point2S.MINUS_J,
+ Point2S.PLUS_K, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromBounds_singleBound() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_K, Point2S.PLUS_I, TEST_PRECISION);
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromBounds(circle);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(2 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(2 * Geometry.PI, area.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_J, area.getBarycenter(), TEST_EPS);
+ checkBarycenterConsistency(area);
+
+ Assert.assertEquals(1, area.getBoundaries().size());
+ GreatArc arc = area.getBoundaries().get(0);
+ Assert.assertTrue(arc.isFull());
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_J, arc.getCircle().getPolePoint(), TEST_EPS);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE, Point2S.PLUS_J);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.MINUS_I,
+ Point2S.PLUS_K, Point2S.MINUS_K);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE, Point2S.MINUS_J);
+ }
+
+ @Test
+ public void testFromBounds_lune_intersectionAtPoles() {
+ // arrange
+ GreatCircle a = GreatCircle.fromPoints(Point2S.PLUS_K, Point2S.PLUS_I, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPoints(
+ Point2S.of(0.25 * Geometry.PI, Geometry.HALF_PI), Point2S.PLUS_K, TEST_PRECISION);
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromBounds(a, b);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(2 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, area.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0.125 * Geometry.PI, Geometry.HALF_PI), area.getBarycenter(), TEST_EPS);
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(2, arcs.size());
+ checkArc(arcs.get(0), Point2S.PLUS_K, Point2S.MINUS_K);
+ checkArc(arcs.get(1), Point2S.MINUS_K, Point2S.PLUS_K);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.of(0.125 * Geometry.PI, 0.1),
+ Point2S.of(0.125 * Geometry.PI, Geometry.HALF_PI),
+ Point2S.of(0.125 * Geometry.PI, Geometry.PI - 0.1));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.of(0.25 * Geometry.PI, Geometry.HALF_PI),
+ Point2S.PLUS_K, Point2S.MINUS_K);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.PLUS_J, Point2S.MINUS_J);
+ }
+
+ @Test
+ public void testFromBounds_lune_intersectionAtEquator() {
+ // arrange
+ GreatCircle a = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromBounds(a, b);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(2 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, area.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0, 0.25 * Geometry.PI), area.getBarycenter(), TEST_EPS);
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(2, arcs.size());
+ checkArc(arcs.get(0), Point2S.PLUS_J, Point2S.MINUS_J);
+ checkArc(arcs.get(1), Point2S.MINUS_J, Point2S.PLUS_J);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.of(0, 0.25 * Geometry.PI),
+ Point2S.of(0.25, 0.4 * Geometry.PI),
+ Point2S.of(-0.25, 0.4 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.PLUS_K,
+ Point2S.PLUS_J, Point2S.MINUS_J);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.MINUS_I, Point2S.MINUS_K,
+ Point2S.of(Geometry.PI, 0.25 * Geometry.PI),
+ Point2S.of(Geometry.PI, 0.75 * Geometry.PI));
+ }
+
+ @Test
+ public void testFromBounds_triangle_large() {
+ // arrange
+ GreatCircle a = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPole(Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ GreatCircle c = GreatCircle.fromPole(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromBounds(Arrays.asList(a, b, c));
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(1.5 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, area.getSize(), TEST_EPS);
+
+ Point2S expectedBarycenter = triangleBarycenter(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+ SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(3, arcs.size());
+ checkArc(arcs.get(0), Point2S.PLUS_K, Point2S.PLUS_I);
+ checkArc(arcs.get(1), Point2S.PLUS_I, Point2S.PLUS_J);
+ checkArc(arcs.get(2), Point2S.PLUS_J, Point2S.PLUS_K);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.of(0.25 * Geometry.PI, 0.25 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K,
+ Point2S.of(0, 0.25 * Geometry.PI), Point2S.of(Geometry.HALF_PI, 0.304 * Geometry.PI),
+ Point2S.of(0.25 * Geometry.PI, Geometry.HALF_PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.MINUS_I, Point2S.MINUS_J, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromBounds_triangle_small() {
+ // arrange
+ double azMin = 1.125 * Geometry.PI;
+ double azMax = 1.375 * Geometry.PI;
+ double azMid = 0.5 * (azMin + azMax);
+ double polarTop = 0.1;
+ double polarBottom = 0.25 * Geometry.PI;
+
+ Point2S p1 = Point2S.of(azMin, polarBottom);
+ Point2S p2 = Point2S.of(azMax, polarBottom);
+ Point2S p3 = Point2S.of(azMid, polarTop);
+
+ GreatCircle a = GreatCircle.fromPoints(p1, p2, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPoints(p2, p3, TEST_PRECISION);
+ GreatCircle c = GreatCircle.fromPoints(p3, p1, TEST_PRECISION);
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromBounds(Arrays.asList(a, b, c));
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(p1.distance(p2) + p2.distance(p3) + p3.distance(p1),
+ area.getBoundarySize(), TEST_EPS);
+ double size = Geometry.TWO_PI - a.angle(b) - b.angle(c) - c.angle(a);
+ Assert.assertEquals(size, area.getSize(), TEST_EPS);
+
+ Point2S expectedBarycenter = triangleBarycenter(p1, p2, p3);
+ SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), BARYCENTER_EPS);
+
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(3, arcs.size());
+
+ checkArc(arcs.get(0), p3, p1);
+ checkArc(arcs.get(1), p1, p2);
+ checkArc(arcs.get(2), p2, p3);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE, Point2S.of(azMid, 0.11));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ p1, p2, p3, p1.slerp(p2, 0.2));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K,
+ Point2S.MINUS_I, Point2S.MINUS_J, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromBounds_quad() {
+ // arrange
+ Point2S p1 = Point2S.of(0.2, 0.1);
+ Point2S p2 = Point2S.of(0.1, 0.2);
+ Point2S p3 = Point2S.of(0.2, 0.5);
+ Point2S p4 = Point2S.of(0.3, 0.2);
+
+ GreatCircle c1 = GreatCircle.fromPoints(p1, p2, TEST_PRECISION);
+ GreatCircle c2 = GreatCircle.fromPoints(p2, p3, TEST_PRECISION);
+ GreatCircle c3 = GreatCircle.fromPoints(p3, p4, TEST_PRECISION);
+ GreatCircle c4 = GreatCircle.fromPoints(p4, p1, TEST_PRECISION);
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromBounds(c1, c2, c3, c4);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(p1.distance(p2) + p2.distance(p3) + p3.distance(p4) + p4.distance(p1),
+ area.getBoundarySize(), TEST_EPS);
+
+ double size = 2 * Geometry.PI - c1.angle(c2) - c2.angle(c3) - c3.angle(c4) - c4.angle(c1);
+ Assert.assertEquals(size, area.getSize(), TEST_EPS);
+
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(4, arcs.size());
+
+ checkArc(arcs.get(0), p1, p2);
+ checkArc(arcs.get(1), p2, p3);
+ checkArc(arcs.get(2), p4, p1);
+ checkArc(arcs.get(3), p3, p4);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE, Point2S.of(0.2, 0.11));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ p1, p2, p3, p4, p1.slerp(p2, 0.2));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K,
+ Point2S.MINUS_I, Point2S.MINUS_J, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromPath_empty() {
+ // act
+ ConvexArea2S area = ConvexArea2S.fromPath(GreatArcPath.empty());
+
+ // assert
+ Assert.assertSame(ConvexArea2S.full(), area);
+ }
+
+ @Test
+ public void testFromPath() {
+ // arrange
+ GreatArcPath path = GreatArcPath.builder(TEST_PRECISION)
+ .append(Point2S.MINUS_I)
+ .append(Point2S.MINUS_K)
+ .append(Point2S.MINUS_J)
+ .close();
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromPath(path);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(1.5 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, area.getSize(), TEST_EPS);
+
+ Point2S expectedBarycenter = triangleBarycenter(Point2S.MINUS_I, Point2S.MINUS_K, Point2S.MINUS_J);
+ SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(3, arcs.size());
+ checkArc(arcs.get(0), Point2S.MINUS_I, Point2S.MINUS_K);
+ checkArc(arcs.get(1), Point2S.MINUS_J, Point2S.MINUS_I);
+ checkArc(arcs.get(2), Point2S.MINUS_K, Point2S.MINUS_J);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.of(1.25 * Geometry.PI, 0.75 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.MINUS_I, Point2S.MINUS_J, Point2S.MINUS_K);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+ }
+
+ @Test
+ public void testFromVertices_empty() {
+ // act
+ ConvexArea2S area = ConvexArea2S.fromVertices(Collections.emptyList(), TEST_PRECISION);
+
+ // assert
+ Assert.assertSame(ConvexArea2S.full(), area);
+ }
+
+ @Test
+ public void testFromVertices() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_I;
+ Point2S p2 = Point2S.PLUS_J;
+ Point2S p3 = Point2S.PLUS_K;
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromVertices(Arrays.asList(p1, p2, p3), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(2 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, area.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0, 0.25 * Geometry.PI), area.getBarycenter(), TEST_EPS);
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(2, arcs.size());
+ checkArc(arcs.get(0), Point2S.PLUS_J, Point2S.MINUS_J);
+ checkArc(arcs.get(1), Point2S.MINUS_J, Point2S.PLUS_J);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.of(-0.25 * Geometry.PI, 0.25 * Geometry.PI),
+ Point2S.of(0, 0.25 * Geometry.PI),
+ Point2S.of(0.25 * Geometry.PI, 0.25 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.PLUS_J,
+ Point2S.PLUS_K, Point2S.MINUS_J);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.MINUS_I, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromVertices_lastVertexRepeated() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_I;
+ Point2S p2 = Point2S.PLUS_J;
+ Point2S p3 = Point2S.PLUS_K;
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromVertices(Arrays.asList(p1, p2, p3, p1), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(1.5 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, area.getSize(), TEST_EPS);
+
+ Point2S expectedBarycenter = triangleBarycenter(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+ SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(3, arcs.size());
+ checkArc(arcs.get(0), Point2S.PLUS_K, Point2S.PLUS_I);
+ checkArc(arcs.get(1), Point2S.PLUS_I, Point2S.PLUS_J);
+ checkArc(arcs.get(2), Point2S.PLUS_J, Point2S.PLUS_K);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.of(0.25 * Geometry.PI, 0.25 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K,
+ Point2S.of(0, 0.25 * Geometry.PI), Point2S.of(Geometry.HALF_PI, 0.304 * Geometry.PI),
+ Point2S.of(0.25 * Geometry.PI, Geometry.HALF_PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.MINUS_I, Point2S.MINUS_J, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromVertices_verticesRepeated() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_I;
+ Point2S p2 = Point2S.PLUS_J;
+ Point2S p3 = Point2S.PLUS_K;
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromVertices(Arrays.asList(
+ p1, Point2S.of(1e-17, Geometry.HALF_PI), p2, p3, p3, p1), true, TEST_PRECISION);
+
+ // assert
+ Assert.assertEquals(Geometry.HALF_PI, area.getSize(), TEST_EPS);
+
+ Point2S expectedBarycenter = triangleBarycenter(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+ SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+
+ List<Point2S> vertices = area.getBoundaryPath().getVertices();
+ Assert.assertEquals(4, vertices.size());
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, vertices.get(0), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_I, vertices.get(1), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_J, vertices.get(2), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, vertices.get(3), TEST_EPS);
+ }
+
+ @Test
+ public void testFromVertices_invalidArguments() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea2S.fromVertices(Arrays.asList(Point2S.PLUS_I), TEST_PRECISION);
+ }, IllegalStateException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea2S.fromVertices(Arrays.asList(Point2S.PLUS_I, Point2S.of(1e-16, Geometry.HALF_PI)), TEST_PRECISION);
+ }, IllegalStateException.class);
+ }
+
+ @Test
+ public void testFromVertexLoop() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_I;
+ Point2S p2 = Point2S.PLUS_J;
+ Point2S p3 = Point2S.PLUS_K;
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromVertexLoop(Arrays.asList(p1, p2, p3), TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(1.5 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, area.getSize(), TEST_EPS);
+
+ Point2S expectedBarycenter = triangleBarycenter(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+ SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(3, arcs.size());
+ checkArc(arcs.get(0), Point2S.PLUS_K, Point2S.PLUS_I);
+ checkArc(arcs.get(1), Point2S.PLUS_I, Point2S.PLUS_J);
+ checkArc(arcs.get(2), Point2S.PLUS_J, Point2S.PLUS_K);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.of(0.25 * Geometry.PI, 0.25 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K,
+ Point2S.of(0, 0.25 * Geometry.PI), Point2S.of(Geometry.HALF_PI, 0.304 * Geometry.PI),
+ Point2S.of(0.25 * Geometry.PI, Geometry.HALF_PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.MINUS_I, Point2S.MINUS_J, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromVertexLoop_empty() {
+ // act
+ ConvexArea2S area = ConvexArea2S.fromVertexLoop(Collections.emptyList(), TEST_PRECISION);
+
+ // assert
+ Assert.assertSame(ConvexArea2S.full(), area);
+ }
+
+ @Test
+ public void testGetInteriorAngles_noAngles() {
+ // act/assert
+ Assert.assertEquals(0, ConvexArea2S.full().getInteriorAngles().length);
+ Assert.assertEquals(0, ConvexArea2S.fromBounds(GreatCircle.fromPole(Vector3D.Unit.PLUS_X, TEST_PRECISION))
+ .getInteriorAngles().length);
+ }
+
+ @Test
+ public void testGetInteriorAngles() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_K;
+ Point2S p2 = Point2S.PLUS_I;
+ Point2S p4 = Point2S.PLUS_J;
+
+ GreatCircle base = GreatCircle.fromPoints(p2, p4, TEST_PRECISION);
+ GreatCircle c1 = base.transform(Transform2S.createRotation(p2, -0.2));
+ GreatCircle c2 = base.transform(Transform2S.createRotation(p4, 0.1));
+
+ Point2S p3 = c1.intersection(c2);
+
+ // act
+ ConvexArea2S area = ConvexArea2S.fromVertexLoop(Arrays.asList(p1, p2, p3, p4), TEST_PRECISION);
+
+ // assert
+ double[] angles = area.getInteriorAngles();
+ Assert.assertEquals(4, angles.length);
+ Assert.assertEquals(Geometry.HALF_PI + 0.2, angles[0], TEST_EPS);
+ Assert.assertEquals(Geometry.PI - c1.angle(c2), angles[1], TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI + 0.1, angles[2], TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, angles[3], TEST_EPS);
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ Transform2S t = Transform2S.createReflection(Point2S.PLUS_J);
+ ConvexArea2S input = ConvexArea2S.fromVertexLoop(
+ Arrays.asList(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K), TEST_PRECISION);
+
+ // act
+ ConvexArea2S area = input.transform(t);
+
+ // assert
+ Assert.assertFalse(area.isFull());
+ Assert.assertFalse(area.isEmpty());
+ Assert.assertEquals(1.5 * Geometry.PI, area.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, area.getSize(), TEST_EPS);
+
+ Point2S expectedBarycenter = triangleBarycenter(Point2S.MINUS_J, Point2S.PLUS_I, Point2S.PLUS_K);
+ SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+
+ checkBarycenterConsistency(area);
+
+ List<GreatArc> arcs = sortArcs(area.getBoundaries());
+ Assert.assertEquals(3, arcs.size());
+ checkArc(arcs.get(0), Point2S.PLUS_K, Point2S.MINUS_J);
+ checkArc(arcs.get(1), Point2S.PLUS_I, Point2S.PLUS_K);
+ checkArc(arcs.get(2), Point2S.MINUS_J, Point2S.PLUS_I);
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE,
+ Point2S.of(-0.25 * Geometry.PI, 0.25 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.MINUS_J, Point2S.PLUS_K,
+ Point2S.of(0, 0.25 * Geometry.PI), Point2S.of(Geometry.MINUS_HALF_PI, 0.304 * Geometry.PI),
+ Point2S.of(-0.25 * Geometry.PI, Geometry.HALF_PI));
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.OUTSIDE,
+ Point2S.PLUS_J, Point2S.MINUS_I, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testTrim() {
+ // arrange
+ GreatCircle c1 = GreatCircle.fromPole(Vector3D.Unit.MINUS_X, TEST_PRECISION);
+ GreatCircle c2 = GreatCircle.fromPole(Vector3D.of(1, 1, 0), TEST_PRECISION);
+
+ GreatCircle slanted = GreatCircle.fromPole(Vector3D.of(-1, 0, 1), TEST_PRECISION);
+
+ ConvexArea2S area = ConvexArea2S.fromBounds(c1, c2);
+
+ // act/assert
+ checkArc(area.trim(GreatArc.fromPoints(Point2S.of(0.1, Geometry.HALF_PI), Point2S.MINUS_I, TEST_PRECISION)),
+ Point2S.PLUS_J, Point2S.of(0.75 * Geometry.PI, Geometry.HALF_PI));
+
+ checkArc(area.trim(GreatArc.fromPoints(Point2S.MINUS_I, Point2S.of(0.2, Geometry.HALF_PI), TEST_PRECISION)),
+ Point2S.of(0.75 * Geometry.PI, Geometry.HALF_PI), Point2S.PLUS_J);
+
+ checkArc(area.trim(GreatArc.fromPoints(Point2S.of(0.6 * Geometry.PI, 0.1), Point2S.of(0.7 * Geometry.PI, 0.8), TEST_PRECISION)),
+ Point2S.of(0.6 * Geometry.PI, 0.1), Point2S.of(0.7 * Geometry.PI, 0.8));
+
+ Assert.assertNull(area.trim(GreatArc.fromPoints(Point2S.MINUS_I, Point2S.MINUS_J, TEST_PRECISION)));
+
+ checkArc(area.trim(slanted.span()), c1.intersection(slanted), slanted.intersection(c2));
+ }
+
+ @Test
+ public void testSplit_both() {
+ // arrange
+ GreatCircle c1 = GreatCircle.fromPole(Vector3D.Unit.MINUS_X, TEST_PRECISION);
+ GreatCircle c2 = GreatCircle.fromPole(Vector3D.of(1, 1, 0), TEST_PRECISION);
+
+ ConvexArea2S area = ConvexArea2S.fromBounds(c1, c2);
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(-1, 0, 1), TEST_PRECISION);
+
+ // act
+ Split<ConvexArea2S> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ Point2S p1 = c1.intersection(splitter);
+ Point2S p2 = splitter.intersection(c2);
+
+ ConvexArea2S minus = split.getMinus();
+ assertPath(minus.getBoundaryPath(), Point2S.PLUS_K, p1, p2, Point2S.PLUS_K);
+
+ ConvexArea2S plus = split.getPlus();
+ assertPath(plus.getBoundaryPath(), p1, Point2S.MINUS_K, p2, p1);
+
+ Assert.assertEquals(area.getSize(), minus.getSize() + plus.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testSplit_minus() {
+ // arrange
+ ConvexArea2S area = ConvexArea2S.fromVertexLoop(Arrays.asList(
+ Point2S.PLUS_I, Point2S.PLUS_K, Point2S.MINUS_J
+ ), TEST_PRECISION);
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(0, -1, 1), TEST_PRECISION);
+
+ // act
+ Split<ConvexArea2S> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(area, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_plus() {
+ // arrange
+ ConvexArea2S area = ConvexArea2S.fromVertexLoop(Arrays.asList(
+ Point2S.PLUS_I, Point2S.PLUS_K, Point2S.MINUS_J
+ ), TEST_PRECISION);
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(0, 1, -1), TEST_PRECISION);
+
+ // act
+ Split<ConvexArea2S> split = area.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(area, split.getPlus());
+ }
+
+ @Test
+ public void testToTree() {
+ // arrange
+ ConvexArea2S area = ConvexArea2S.fromVertexLoop(Arrays.asList(
+ Point2S.of(0.1, 0.1), Point2S.of(-0.4, 1),
+ Point2S.of(0.15, 1.5), Point2S.of(0.3, 1.2),
+ Point2S.of(0.1, 0.1)
+ ), TEST_PRECISION);
+
+ // act
+ RegionBSPTree2S tree = area.toTree();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(area.getSize(), tree.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(area.getBarycenter(), tree.getBarycenter(), TEST_EPS);
+ }
+
+ private static List<GreatArc> sortArcs(List<GreatArc> arcs) {
+ List<GreatArc> result = new ArrayList<>(arcs);
+
+ Collections.sort(result, (a, b) ->
+ Point2S.POLAR_AZIMUTH_ASCENDING_ORDER.compare(a.getStartPoint(), b.getStartPoint()));
+
+ return result;
+ }
+
+ private static Point2S triangleBarycenter(Point2S p1, Point2S p2, Point2S p3) {
+ // compute the barycenter using intersection mid point arcs
+ GreatCircle c1 = GreatCircle.fromPoints(p1, p2.slerp(p3, 0.5), TEST_PRECISION);
+ GreatCircle c2 = GreatCircle.fromPoints(p2, p1.slerp(p3, 0.5), TEST_PRECISION);
+
+ return c1.intersection(c2);
+ }
+
+ private static void checkArc(GreatArc arc, Point2S start, Point2S end) {
+ SphericalTestUtils.assertPointsEq(start, arc.getStartPoint(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(end, arc.getEndPoint(), TEST_EPS);
+ }
+
+ private static void assertPath(GreatArcPath path, Point2S ... expectedVertices) {
+ List<Point2S> vertices = path.getVertices();
+
+ Assert.assertEquals(expectedVertices.length, vertices.size());
+ for (int i = 0; i < expectedVertices.length; ++i) {
+
+ if (!expectedVertices[i].eq(vertices.get(i), TEST_PRECISION)) {
+ String msg = "Unexpected point in path at index " + i + ". Expected " +
+ Arrays.toString(expectedVertices) + " but received " + vertices;
+ Assert.fail(msg);
+ }
+ }
+ }
+
+ private static void checkBarycenterConsistency(ConvexArea2S area) {
+ Point2S barycenter = area.getBarycenter();
+ double size = area.getSize();
+
+ SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE, barycenter);
+
+ GreatCircle circle = GreatCircle.fromPole(barycenter.getVector(), TEST_PRECISION);
+ for (double az = 0; az <= Geometry.TWO_PI; az += 0.2) {
+ Point2S pt = circle.toSpace(Point1S.of(az));
+ GreatCircle splitter = GreatCircle.fromPoints(barycenter, pt, TEST_PRECISION);
+
+ Split<ConvexArea2S> split = area.split(splitter);
+
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ ConvexArea2S minus = split.getMinus();
+ double minusSize = minus.getSize();
+ Point2S minusBc = minus.getBarycenter();
+
+ Vector3D weightedMinus = minusBc.getVector()
+ .multiply(minus.getSize());
+
+ ConvexArea2S plus = split.getPlus();
+ double plusSize = plus.getSize();
+ Point2S plusBc = plus.getBarycenter();
+
+ Vector3D weightedPlus = plusBc.getVector()
+ .multiply(plus.getSize());
+ Point2S computedBarycenter = Point2S.from(weightedMinus.add(weightedPlus));
+
+ Assert.assertEquals(size, minusSize + plusSize, TEST_EPS);
+ SphericalTestUtils.assertPointsEq(barycenter, computedBarycenter, BARYCENTER_EPS);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java
new file mode 100644
index 0000000..7911f9d
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java
@@ -0,0 +1,641 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GreatArcPathTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testEmpty() {
+ // act
+ GreatArcPath path = GreatArcPath.empty();
+
+ // assert
+ Assert.assertTrue(path.isEmpty());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertNull(path.getStartVertex());
+ Assert.assertNull(path.getEndVertex());
+
+ Assert.assertNull(path.getStartArc());
+ Assert.assertNull(path.getEndArc());
+
+ Assert.assertEquals(0, path.getArcs().size());
+ Assert.assertEquals(0, path.getVertices().size());
+ }
+
+ @Test
+ public void testFromVertices_boolean_empty() {
+ // act
+ GreatArcPath path = GreatArcPath.fromVertices(Collections.emptyList(), true, TEST_PRECISION);
+
+ // assert
+ Assert.assertTrue(path.isEmpty());
+
+ Assert.assertNull(path.getStartVertex());
+ Assert.assertNull(path.getEndVertex());
+
+ Assert.assertNull(path.getStartArc());
+ Assert.assertNull(path.getEndArc());
+
+ Assert.assertEquals(0, path.getArcs().size());
+ Assert.assertEquals(0, path.getVertices().size());
+ }
+
+ @Test
+ public void testFromVertices_boolean_notClosed() {
+ // arrange
+ List<Point2S> points = Arrays.asList(
+ Point2S.PLUS_I,
+ Point2S.PLUS_K,
+ Point2S.PLUS_J);
+
+ // act
+ GreatArcPath path = GreatArcPath.fromVertices(points, false, TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_I, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_J, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(2, arcs.size());
+ assertArc(arcs.get(0), Point2S.PLUS_I, Point2S.PLUS_K);
+ assertArc(arcs.get(1), Point2S.PLUS_K, Point2S.PLUS_J);
+
+ assertPoints(points, path.getVertices());
+ }
+
+ @Test
+ public void testFromVertices_boolean_closed() {
+ // arrange
+ List<Point2S> points = Arrays.asList(
+ Point2S.PLUS_I,
+ Point2S.PLUS_K,
+ Point2S.PLUS_J);
+
+ // act
+ GreatArcPath path = GreatArcPath.fromVertices(points, true, TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_I, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_I, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(3, arcs.size());
+ assertArc(arcs.get(0), Point2S.PLUS_I, Point2S.PLUS_K);
+ assertArc(arcs.get(1), Point2S.PLUS_K, Point2S.PLUS_J);
+ assertArc(arcs.get(2), Point2S.PLUS_J, Point2S.PLUS_I);
+
+ assertPoints(Arrays.asList(
+ Point2S.PLUS_I,
+ Point2S.PLUS_K,
+ Point2S.PLUS_J,
+ Point2S.PLUS_I), path.getVertices());
+ }
+
+ @Test
+ public void testFromVertices_boolean_closed_pointsConsideredEqual() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+ Point2S almostPlusI = Point2S.of(1e-4, Geometry.HALF_PI);
+
+ List<Point2S> points = Arrays.asList(
+ Point2S.PLUS_I,
+ Point2S.PLUS_K,
+ Point2S.PLUS_J,
+ almostPlusI);
+
+ // act
+ GreatArcPath path = GreatArcPath.fromVertices(points, true, precision);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_I, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(almostPlusI, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(3, arcs.size());
+ assertArc(arcs.get(0), Point2S.PLUS_I, Point2S.PLUS_K);
+ assertArc(arcs.get(1), Point2S.PLUS_K, Point2S.PLUS_J);
+ assertArc(arcs.get(2), Point2S.PLUS_J, almostPlusI);
+
+ assertPoints(Arrays.asList(
+ Point2S.PLUS_I,
+ Point2S.PLUS_K,
+ Point2S.PLUS_J,
+ almostPlusI), path.getVertices());
+ }
+
+ @Test
+ public void testFromVertices() {
+ // arrange
+ List<Point2S> points = Arrays.asList(
+ Point2S.MINUS_I,
+ Point2S.MINUS_J,
+ Point2S.PLUS_I);
+
+ // act
+ GreatArcPath path = GreatArcPath.fromVertices(points, TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_I, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_I, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(2, arcs.size());
+ assertArc(arcs.get(0), Point2S.MINUS_I, Point2S.MINUS_J);
+ assertArc(arcs.get(1), Point2S.MINUS_J, Point2S.PLUS_I);
+
+ assertPoints(points, path.getVertices());
+ }
+
+ @Test
+ public void testFromVertexLoop() {
+ // arrange
+ List<Point2S> points = Arrays.asList(
+ Point2S.MINUS_I,
+ Point2S.MINUS_J,
+ Point2S.MINUS_K);
+
+ // act
+ GreatArcPath path = GreatArcPath.fromVertexLoop(points, TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_I, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_I, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(3, arcs.size());
+ assertArc(arcs.get(0), Point2S.MINUS_I, Point2S.MINUS_J);
+ assertArc(arcs.get(1), Point2S.MINUS_J, Point2S.MINUS_K);
+ assertArc(arcs.get(2), Point2S.MINUS_K, Point2S.MINUS_I);
+
+ assertPoints(Arrays.asList(
+ Point2S.MINUS_I,
+ Point2S.MINUS_J,
+ Point2S.MINUS_K,
+ Point2S.MINUS_I), path.getVertices());
+ }
+
+ @Test
+ public void testFromArcs() {
+ // arrange
+ Point2S ptA = Point2S.PLUS_I;
+ Point2S ptB = Point2S.of(1, Geometry.HALF_PI);
+ Point2S ptC = Point2S.of(1, Geometry.HALF_PI - 1);
+ Point2S ptD = Point2S.of(2, Geometry.HALF_PI - 1);
+
+ GreatArc a = GreatArc.fromPoints(ptA, ptB, TEST_PRECISION);
+ GreatArc b = GreatArc.fromPoints(ptB, ptC, TEST_PRECISION);
+ GreatArc c = GreatArc.fromPoints(ptC, ptD, TEST_PRECISION);
+
+ // act
+ GreatArcPath path = GreatArcPath.fromArcs(a, b, c);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(ptA, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(ptD, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(3, arcs.size());
+ assertArc(arcs.get(0), ptA, ptB);
+ assertArc(arcs.get(1), ptB, ptC);
+ assertArc(arcs.get(2), ptC, ptD);
+
+ assertPoints(Arrays.asList(ptA, ptB, ptC, ptD), path.getVertices());
+ }
+
+ @Test
+ public void testFromArcs_full() {
+ // arrange
+ GreatArc fullArc = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, TEST_PRECISION).span();
+
+ // act
+ GreatArcPath path = GreatArcPath.fromArcs(fullArc);
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isClosed());
+
+ Assert.assertSame(fullArc, path.getStartArc());
+ Assert.assertSame(fullArc, path.getEndArc());
+
+ Assert.assertNull(path.getStartVertex());
+ Assert.assertNull(path.getEndVertex());
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(1, arcs.size());
+
+ Assert.assertSame(fullArc, arcs.get(0));
+ }
+
+ @Test
+ public void testIterator() {
+ // arrange
+ GreatArc a = GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatArc b = GreatArc.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ GreatArcPath path = GreatArcPath.fromArcs(Arrays.asList(a, b));
+
+ List<GreatArc> arcs = new ArrayList<>();
+
+ // act
+ for (GreatArc arc : path) {
+ arcs.add(arc);
+ }
+
+ // assert
+ Assert.assertEquals(arcs, Arrays.asList(a, b));
+ }
+
+ @Test
+ public void testToTree_empty() {
+ // act
+ RegionBSPTree2S tree = GreatArcPath.empty().toTree();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ }
+
+ @Test
+ public void testToTree_halfSpace() {
+ // arrange
+ GreatArcPath path = GreatArcPath.builder(TEST_PRECISION)
+ .append(Point2S.PLUS_I)
+ .append(Point2S.PLUS_J)
+ .build();
+
+ // act
+ RegionBSPTree2S tree = path.toTree();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(Geometry.TWO_PI, tree.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.getBarycenter(), TEST_EPS);
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE, Point2S.PLUS_K);
+ SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testToTree_triangle() {
+ // arrange
+ GreatArcPath path = GreatArcPath.builder(TEST_PRECISION)
+ .append(Point2S.PLUS_I)
+ .append(Point2S.PLUS_J)
+ .append(Point2S.PLUS_K)
+ .close();
+
+ // act
+ RegionBSPTree2S tree = path.toTree();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(Geometry.HALF_PI, tree.getSize(), TEST_EPS);
+
+ Point2S bc = Point2S.from(Point2S.PLUS_I.getVector()
+ .add(Point2S.PLUS_J.getVector())
+ .add(Point2S.PLUS_K.getVector()));
+
+ SphericalTestUtils.assertPointsEq(bc, tree.getBarycenter(), TEST_EPS);
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE, Point2S.of(0.5, 0.5));
+ SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
+ Point2S.MINUS_K, Point2S.MINUS_I, Point2S.MINUS_J);
+ }
+
+ @Test
+ public void testBuilder_append() {
+ // arrange
+ Point2S a = Point2S.PLUS_I;
+ Point2S b = Point2S.PLUS_J;
+ Point2S c = Point2S.PLUS_K;
+ Point2S d = Point2S.of(-1, Geometry.HALF_PI);
+ Point2S e = Point2S.of(0, 0.6 * Geometry.PI);
+
+ GreatArcPath.Builder builder = GreatArcPath.builder(TEST_PRECISION);
+
+ // act
+ GreatArcPath path = builder.append(GreatArc.fromPoints(a, b, TEST_PRECISION))
+ .appendVertices(c, d)
+ .append(e)
+ .append(GreatArc.fromPoints(e, a, TEST_PRECISION))
+ .build();
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(a, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(a, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(5, arcs.size());
+ assertArc(arcs.get(0), a, b);
+ assertArc(arcs.get(1), b, c);
+ assertArc(arcs.get(2), c, d);
+ assertArc(arcs.get(3), d, e);
+ assertArc(arcs.get(4), e, a);
+
+ assertPoints(Arrays.asList(a, b, c, d, e, a), path.getVertices());
+ }
+
+ @Test
+ public void testBuilder_prepend() {
+ // arrange
+ Point2S a = Point2S.PLUS_I;
+ Point2S b = Point2S.PLUS_J;
+ Point2S c = Point2S.PLUS_K;
+ Point2S d = Point2S.of(-1, Geometry.HALF_PI);
+ Point2S e = Point2S.of(0, 0.6 * Geometry.PI);
+
+ GreatArcPath.Builder builder = GreatArcPath.builder(TEST_PRECISION);
+
+ // act
+ GreatArcPath path = builder.prepend(GreatArc.fromPoints(e, a, TEST_PRECISION))
+ .prependPoints(Arrays.asList(c, d))
+ .prepend(b)
+ .prepend(GreatArc.fromPoints(a, b, TEST_PRECISION))
+ .build();
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(a, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(a, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(5, arcs.size());
+ assertArc(arcs.get(0), a, b);
+ assertArc(arcs.get(1), b, c);
+ assertArc(arcs.get(2), c, d);
+ assertArc(arcs.get(3), d, e);
+ assertArc(arcs.get(4), e, a);
+
+ assertPoints(Arrays.asList(a, b, c, d, e, a), path.getVertices());
+ }
+
+ @Test
+ public void testBuilder_appendAndPrepend_points() {
+ // arrange
+ Point2S a = Point2S.PLUS_I;
+ Point2S b = Point2S.PLUS_J;
+ Point2S c = Point2S.PLUS_K;
+ Point2S d = Point2S.of(-1, Geometry.HALF_PI);
+ Point2S e = Point2S.of(0, 0.6 * Geometry.PI);
+
+ GreatArcPath.Builder builder = GreatArcPath.builder(TEST_PRECISION);
+
+ // act
+ GreatArcPath path = builder.prepend(a)
+ .append(b)
+ .prepend(e)
+ .append(c)
+ .prepend(d)
+ .build();
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertFalse(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(d, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(c, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(4, arcs.size());
+ assertArc(arcs.get(0), d, e);
+ assertArc(arcs.get(1), e, a);
+ assertArc(arcs.get(2), a, b);
+ assertArc(arcs.get(3), b, c);
+
+ assertPoints(Arrays.asList(d, e, a, b, c), path.getVertices());
+ }
+
+ @Test
+ public void testBuilder_appendAndPrepend_mixedArguments() {
+ // arrange
+ Point2S a = Point2S.PLUS_I;
+ Point2S b = Point2S.PLUS_J;
+ Point2S c = Point2S.PLUS_K;
+ Point2S d = Point2S.of(-1, Geometry.HALF_PI);
+ Point2S e = Point2S.of(0, 0.6 * Geometry.PI);
+
+ GreatArcPath.Builder builder = GreatArcPath.builder(TEST_PRECISION);
+
+ // act
+ GreatArcPath path = builder.append(GreatArc.fromPoints(a, b, TEST_PRECISION))
+ .prepend(GreatArc.fromPoints(e, a, TEST_PRECISION))
+ .append(c)
+ .prepend(d)
+ .append(GreatArc.fromPoints(c, d, TEST_PRECISION))
+ .build();
+
+ // assert
+ Assert.assertFalse(path.isEmpty());
+ Assert.assertTrue(path.isClosed());
+
+ SphericalTestUtils.assertPointsEq(d, path.getStartVertex(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(d, path.getEndVertex(), TEST_EPS);
+
+ List<GreatArc> arcs = path.getArcs();
+ Assert.assertEquals(5, arcs.size());
+ assertArc(arcs.get(0), d, e);
+ assertArc(arcs.get(1), e, a);
+ assertArc(arcs.get(2), a, b);
+ assertArc(arcs.get(3), b, c);
+ assertArc(arcs.get(4), c, d);
+
+ assertPoints(Arrays.asList(d, e, a, b, c, d), path.getVertices());
+ }
+
+ @Test
+ public void testBuilder_points_noPrecisionGiven() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(null)
+ .append(Point2S.PLUS_I)
+ .append(Point2S.PLUS_J);
+ }, IllegalStateException.class, "Unable to create arc: no point precision specified");
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(null)
+ .prepend(Point2S.PLUS_I)
+ .prepend(Point2S.PLUS_J);
+ }, IllegalStateException.class, "Unable to create arc: no point precision specified");
+ }
+
+ @Test
+ public void testBuilder_arcsNotConnected() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(TEST_PRECISION)
+ .append(Point2S.PLUS_I)
+ .append(Point2S.PLUS_J)
+ .append(GreatArc.fromPoints(Point2S.PLUS_K, Point2S.MINUS_J, TEST_PRECISION));
+ }, IllegalStateException.class, Pattern.compile("^Path arcs are not connected.*"));
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(TEST_PRECISION)
+ .prepend(Point2S.PLUS_I)
+ .prepend(Point2S.PLUS_J)
+ .prepend(GreatArc.fromPoints(Point2S.PLUS_K, Point2S.MINUS_J, TEST_PRECISION));
+ }, IllegalStateException.class, Pattern.compile("^Path arcs are not connected.*"));
+ }
+
+ @Test
+ public void testBuilder_addToFullArc() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(TEST_PRECISION)
+ .append(GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION).span())
+ .append(Point2S.PLUS_J);
+ }, IllegalStateException.class, Pattern.compile("^Cannot add point .* after full arc.*"));
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(TEST_PRECISION)
+ .prepend(GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION).span())
+ .prepend(Point2S.PLUS_J);
+ }, IllegalStateException.class, Pattern.compile("^Cannot add point .* before full arc.*"));
+ }
+
+ @Test
+ public void testBuilder_onlySinglePointGiven() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(TEST_PRECISION)
+ .append(Point2S.PLUS_J)
+ .build();
+ }, IllegalStateException.class, Pattern.compile("^Unable to create path; only a single point provided.*"));
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(TEST_PRECISION)
+ .prepend(Point2S.PLUS_J)
+ .build();
+ }, IllegalStateException.class, Pattern.compile("^Unable to create path; only a single point provided.*"));
+ }
+
+ @Test
+ public void testBuilder_cannotClose() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArcPath.builder(TEST_PRECISION)
+ .append(GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION).span())
+ .close();
+ }, IllegalStateException.class, "Unable to close path: path is full");
+ }
+
+ @Test
+ public void testToString_empty() {
+ // arrange
+ GreatArcPath path = GreatArcPath.empty();
+
+ // act
+ String str = path.toString();
+
+ // assert
+ Assert.assertEquals("GreatArcPath[empty= true]", str);
+ }
+
+ @Test
+ public void testToString_singleFullArc() {
+ // arrange
+ GreatArcPath path = GreatArcPath.fromArcs(GreatCircle.fromPole(Vector3D.Unit.PLUS_Z, TEST_PRECISION).span());
+
+ // act
+ String str = path.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("GreatArcPath[full= true, circle= GreatCircle[", str);
+ }
+
+ @Test
+ public void testToString_nonFullArcs() {
+ // arrange
+ GreatArcPath path = GreatArcPath.builder(TEST_PRECISION)
+ .append(Point2S.PLUS_I)
+ .append(Point2S.PLUS_J)
+ .build();
+
+ // act
+ String str = path.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("ArcPath[vertices= [", str);
+ }
+
+ private static void assertArc(GreatArc arc, Point2S start, Point2S end) {
+ SphericalTestUtils.assertPointsEq(start, arc.getStartPoint(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(end, arc.getEndPoint(), TEST_EPS);
+ }
+
+ private static void assertPoints(Collection<Point2S> expected, Collection<Point2S> actual) {
+ Assert.assertEquals(expected.size(), actual.size());
+
+ Iterator<Point2S> expIt = expected.iterator();
+ Iterator<Point2S> actIt = actual.iterator();
+
+ while (expIt.hasNext() && actIt.hasNext()) {
+ SphericalTestUtils.assertPointsEq(expIt.next(), actIt.next(), TEST_EPS);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcTest.java
new file mode 100644
index 0000000..0263555
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcTest.java
@@ -0,0 +1,383 @@
+/*
+ * 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.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.apache.commons.geometry.spherical.oned.AngularInterval;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GreatArcTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testFromInterval_full() {
+ // act
+ GreatArc arc = GreatArc.fromInterval(
+ GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION),
+ AngularInterval.full());
+
+ // assert
+ Assert.assertTrue(arc.isFull());
+ Assert.assertFalse(arc.isEmpty());
+ Assert.assertTrue(arc.isFinite());
+ Assert.assertFalse(arc.isInfinite());
+
+ Assert.assertNull(arc.getStartPoint());
+ Assert.assertNull(arc.getEndPoint());
+
+ Assert.assertEquals(Geometry.TWO_PI, arc.getSize(), TEST_EPS);
+
+ for (double az = 0; az < Geometry.TWO_PI; az += 0.1) {
+ checkClassify(arc, RegionLocation.INSIDE, Point2S.of(az, Geometry.HALF_PI));
+ }
+
+ checkClassify(arc, RegionLocation.OUTSIDE,
+ Point2S.PLUS_K, Point2S.of(0, Geometry.HALF_PI + 0.1),
+ Point2S.MINUS_K, Point2S.of(0, Geometry.HALF_PI - 0.1));
+ }
+
+ @Test
+ public void testFromInterval_partial() {
+ // arrange
+ GreatArc arc = GreatArc.fromInterval(
+ GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION),
+ AngularInterval.Convex.of(Geometry.HALF_PI, 1.5 * Geometry.PI, TEST_PRECISION));
+
+ // assert
+ Assert.assertFalse(arc.isFull());
+ Assert.assertFalse(arc.isEmpty());
+ Assert.assertTrue(arc.isFinite());
+ Assert.assertFalse(arc.isInfinite());
+
+ checkArc(arc, Point2S.PLUS_K, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromPoints() {
+ // arrange
+ Point2S start = Point2S.PLUS_I;
+ Point2S end = Point2S.MINUS_K;
+
+ // act
+ GreatArc arc = GreatArc.fromPoints(start, end, TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(arc.isFull());
+ Assert.assertFalse(arc.isEmpty());
+ Assert.assertTrue(arc.isFinite());
+ Assert.assertFalse(arc.isInfinite());
+
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_Y, arc.getCircle().getPole(), TEST_EPS);
+
+ checkArc(arc, start, end);
+
+ checkClassify(arc, RegionLocation.INSIDE, Point2S.of(0, 0.75 * Geometry.PI));
+ checkClassify(arc, RegionLocation.BOUNDARY, start, end);
+ checkClassify(arc, RegionLocation.OUTSIDE,
+ Point2S.of(0, 0.25 * Geometry.PI), Point2S.of(Geometry.PI, 0.75 * Geometry.PI),
+ Point2S.of(Geometry.PI, 0.25 * Geometry.PI));
+ }
+
+ @Test
+ public void testFromPoints_almostPi() {
+ // arrange
+ Point2S start = Point2S.PLUS_J;
+ Point2S end = Point2S.of(1.5 * Geometry.PI, Geometry.HALF_PI - 1e-5);
+
+ // act
+ GreatArc arc = GreatArc.fromPoints(start, end, TEST_PRECISION);
+
+ // assert
+ Assert.assertFalse(arc.isFull());
+ Assert.assertFalse(arc.isEmpty());
+ Assert.assertTrue(arc.isFinite());
+ Assert.assertFalse(arc.isInfinite());
+
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_X, arc.getCircle().getPole(), TEST_EPS);
+
+ checkArc(arc, start, end);
+
+ checkClassify(arc, RegionLocation.INSIDE, Point2S.PLUS_K);
+ checkClassify(arc, RegionLocation.BOUNDARY, start, end);
+ checkClassify(arc, RegionLocation.OUTSIDE, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testFromPoints_usesShortestPath() {
+ // act/assert
+ SphericalTestUtils.assertVectorsEqual(
+ Vector3D.Unit.MINUS_Y,
+ GreatArc.fromPoints(
+ Point2S.PLUS_I,
+ Point2S.of(Geometry.PI, Geometry.HALF_PI - 1e-5),
+ TEST_PRECISION).getCircle().getPole(), TEST_EPS);
+
+ SphericalTestUtils.assertVectorsEqual(
+ Vector3D.Unit.PLUS_Y,
+ GreatArc.fromPoints(
+ Point2S.PLUS_I,
+ Point2S.of(Geometry.PI, Geometry.HALF_PI + 1e-5),
+ TEST_PRECISION).getCircle().getPole(), TEST_EPS);
+ }
+
+ @Test
+ public void testFromPoints_invalidPoints() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArc.fromPoints(Point2S.PLUS_I, Point2S.of(1e-12, Geometry.HALF_PI), TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatArc.fromPoints(Point2S.PLUS_I, Point2S.MINUS_I, TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testToConvex() {
+ // arrange
+ GreatArc arc = GreatArc.fromInterval(
+ GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.MINUS_I, TEST_PRECISION),
+ AngularInterval.Convex.of(Geometry.ZERO_PI, Geometry.PI, TEST_PRECISION));
+
+ // act
+ List<GreatArc> result = arc.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+ Assert.assertSame(arc, result.get(0));
+ }
+
+ @Test
+ public void testReverse_full() {
+ // arrange
+ GreatArc arc = GreatArc.fromInterval(
+ GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.MINUS_I, TEST_PRECISION),
+ AngularInterval.full());
+
+ // act
+ GreatArc result = arc.reverse();
+
+ // assert
+ checkGreatCircle(result.getCircle(), Vector3D.Unit.MINUS_Z, Vector3D.Unit.PLUS_Y);
+
+ Assert.assertTrue(result.isFull());
+ }
+
+ @Test
+ public void testReverse() {
+ // arrange
+ GreatArc arc = GreatArc.fromInterval(
+ GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.MINUS_I, TEST_PRECISION),
+ AngularInterval.Convex.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION));
+
+ // act
+ GreatArc result = arc.reverse();
+
+ // assert
+ checkGreatCircle(result.getCircle(), Vector3D.Unit.MINUS_Z, Vector3D.Unit.PLUS_Y);
+
+ checkArc(result, Point2S.MINUS_J, Point2S.MINUS_I);
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ GreatArc arc = GreatCircle.fromPoints(Point2S.PLUS_K, Point2S.MINUS_I, TEST_PRECISION)
+ .arc(Geometry.PI, Geometry.MINUS_HALF_PI);
+
+ Transform2S t = Transform2S.createRotation(Point2S.PLUS_I, Geometry.HALF_PI)
+ .reflect(Point2S.of(-0.25 * Geometry.PI, Geometry.HALF_PI));
+
+ // act
+ GreatArc result = arc.transform(t);
+
+ // assert
+ checkArc(result, Point2S.PLUS_I, Point2S.PLUS_J);
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ GreatArc arc = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION).span();
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(-1, 0, 1), TEST_PRECISION);
+
+ // act
+ Split<GreatArc> split = arc.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ GreatArc minus = split.getMinus();
+ Assert.assertSame(arc.getCircle(), minus.getCircle());
+ checkArc(minus, Point2S.PLUS_J, Point2S.MINUS_J);
+ checkClassify(minus, RegionLocation.OUTSIDE, Point2S.PLUS_I);
+ checkClassify(minus, RegionLocation.INSIDE, Point2S.MINUS_I);
+
+ GreatArc plus = split.getPlus();
+ Assert.assertSame(arc.getCircle(), plus.getCircle());
+ checkArc(plus, Point2S.MINUS_J, Point2S.PLUS_J);
+ checkClassify(plus, RegionLocation.INSIDE, Point2S.PLUS_I);
+ checkClassify(plus, RegionLocation.OUTSIDE, Point2S.MINUS_I);
+ }
+
+ @Test
+ public void testSplit_both() {
+ // arrange
+ GreatArc arc = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION)
+ .arc(Geometry.HALF_PI, Geometry.PI);
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(0, 1, 1), TEST_PRECISION);
+
+ // act
+ Split<GreatArc> split = arc.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ GreatArc minus = split.getMinus();
+ Assert.assertSame(arc.getCircle(), minus.getCircle());
+ checkArc(minus, Point2S.of(0, 0), Point2S.of(1.5 * Geometry.PI, 0.25 * Geometry.PI));
+
+ GreatArc plus = split.getPlus();
+ Assert.assertSame(arc.getCircle(), plus.getCircle());
+ checkArc(plus, Point2S.of(1.5 * Geometry.PI, 0.25 * Geometry.PI), Point2S.MINUS_J);
+ }
+
+ @Test
+ public void testSplit_minus() {
+ // arrange
+ GreatArc arc = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION)
+ .arc(Geometry.HALF_PI, Geometry.PI);
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+
+ // act
+ Split<GreatArc> split = arc.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ GreatArc minus = split.getMinus();
+ Assert.assertSame(arc, minus);
+
+ GreatArc plus = split.getPlus();
+ Assert.assertNull(plus);
+ }
+
+ @Test
+ public void testSplit_plus() {
+ // arrange
+ GreatArc arc = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION)
+ .arc(Geometry.HALF_PI, Geometry.PI);
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.Unit.from(-1, 0, -1), TEST_PRECISION);
+
+ // act
+ Split<GreatArc> split = arc.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ GreatArc minus = split.getMinus();
+ Assert.assertNull(minus);
+
+ GreatArc plus = split.getPlus();
+ Assert.assertSame(arc, plus);
+ }
+
+ @Test
+ public void testSplit_parallelAndAntiparallel() {
+ // arrange
+ GreatArc arc = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION).span();
+
+ // act/assert
+ Assert.assertEquals(SplitLocation.NEITHER,
+ arc.split(GreatCircle.fromPole(Vector3D.Unit.PLUS_Z, TEST_PRECISION)).getLocation());
+ Assert.assertEquals(SplitLocation.NEITHER,
+ arc.split(GreatCircle.fromPole(Vector3D.Unit.MINUS_Z, TEST_PRECISION)).getLocation());
+ }
+
+ @Test
+ public void testToString_full() {
+ // arrange
+ GreatArc arc = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION).span();
+
+ // act
+ String str = arc.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("GreatArc[", str);
+ GeometryTestUtils.assertContains("full= true", str);
+ GeometryTestUtils.assertContains("circle= GreatCircle[", str);
+ }
+
+ @Test
+ public void testToString_notFull() {
+ // arrange
+ GreatArc arc = GreatArc.fromInterval(
+ GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION),
+ AngularInterval.Convex.of(1, 2, TEST_PRECISION));
+
+ // act
+ String str = arc.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("GreatArc[", str);
+ GeometryTestUtils.assertContains("start= (", str);
+ GeometryTestUtils.assertContains("end= (", str);
+ }
+
+ private static void checkClassify(GreatArc arc, RegionLocation loc, Point2S ... pts) {
+ for (Point2S pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, loc, arc.classify(pt));
+ }
+ }
+
+ private static void checkArc(GreatArc arc, Point2S start, Point2S end) {
+ SphericalTestUtils.assertPointsEq(start, arc.getStartPoint(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(end, arc.getEndPoint(), TEST_EPS);
+
+ checkClassify(arc, RegionLocation.BOUNDARY, start, end);
+
+ Point2S mid = arc.getCircle().toSpace(arc.getInterval().getMidPoint());
+
+ checkClassify(arc, RegionLocation.INSIDE, mid);
+ checkClassify(arc, RegionLocation.OUTSIDE, mid.antipodal());
+
+ Assert.assertEquals(start.distance(end), arc.getSize(), TEST_EPS);
+ }
+
+ private static void checkGreatCircle(GreatCircle circle, Vector3D pole, Vector3D x) {
+ SphericalTestUtils.assertVectorsEqual(pole, circle.getPole(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(x, circle.getU(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(pole.cross(x), circle.getV(), TEST_EPS);
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleTest.java
new file mode 100644
index 0000000..6835862
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleTest.java
@@ -0,0 +1,753 @@
+/*
+ * 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.util.regex.Pattern;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.apache.commons.geometry.spherical.oned.AngularInterval;
+import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GreatCircleTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final Vector3D.Unit X = Vector3D.Unit.PLUS_X;
+ private static final Vector3D.Unit Y = Vector3D.Unit.PLUS_Y;
+ private static final Vector3D.Unit Z = Vector3D.Unit.PLUS_Z;
+
+ @Test
+ public void testFromPole() {
+ // act/assert
+ checkGreatCircle(GreatCircle.fromPole(X, TEST_PRECISION), X, Z);
+ checkGreatCircle(GreatCircle.fromPole(Y, TEST_PRECISION), Y, Z.negate());
+ checkGreatCircle(GreatCircle.fromPole(Z, TEST_PRECISION), Z, Y);
+ }
+
+ @Test
+ public void testFromPoleAndXAxis() {
+ // act/assert
+ checkGreatCircle(GreatCircle.fromPoleAndU(X, Y, TEST_PRECISION), X, Y);
+ checkGreatCircle(GreatCircle.fromPoleAndU(X, Z, TEST_PRECISION), X, Z);
+ checkGreatCircle(GreatCircle.fromPoleAndU(Y, Z, TEST_PRECISION), Y, Z);
+ }
+
+ @Test
+ public void testFromPoints() {
+ // act/assert
+ checkGreatCircle(GreatCircle.fromPoints(
+ Point2S.of(0, Geometry.HALF_PI),
+ Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI),
+ TEST_PRECISION),
+ Z, X);
+
+ checkGreatCircle(GreatCircle.fromPoints(
+ Point2S.of(0, Geometry.HALF_PI),
+ Point2S.of(-0.1 * Geometry.PI, Geometry.HALF_PI),
+ TEST_PRECISION),
+ Z.negate(), X);
+
+ checkGreatCircle(GreatCircle.fromPoints(
+ Point2S.of(0, Geometry.HALF_PI),
+ Point2S.of(1.5 * Geometry.PI, Geometry.HALF_PI),
+ TEST_PRECISION),
+ Z.negate(), X);
+
+ checkGreatCircle(GreatCircle.fromPoints(
+ Point2S.of(0, 0),
+ Point2S.of(0, Geometry.HALF_PI),
+ TEST_PRECISION),
+ Y, Z);
+ }
+
+ @Test
+ public void testFromPoints_invalidPoints() {
+ // arrange
+ Point2S p1 = Point2S.of(0, Geometry.HALF_PI);
+ Point2S p2 = Point2S.of(Geometry.PI, Geometry.HALF_PI);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ GreatCircle.fromPoints(p1, p1, TEST_PRECISION);
+ }, GeometryException.class, Pattern.compile("^.*points are equal$"));
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatCircle.fromPoints(p1, Point2S.of(1e-12, Geometry.HALF_PI), TEST_PRECISION);
+ }, GeometryException.class, Pattern.compile("^.*points are equal$"));
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatCircle.fromPoints(
+ Point2S.from(Vector3D.Unit.PLUS_X),
+ Point2S.from(Vector3D.Unit.MINUS_X),
+ TEST_PRECISION);
+ }, GeometryException.class, Pattern.compile("^.*points are antipodal$"));
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatCircle.fromPoints(p1, Point2S.NaN, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatCircle.fromPoints(Point2S.NaN, p2, TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatCircle.fromPoints(p1, Point2S.of(Double.POSITIVE_INFINITY, Geometry.HALF_PI), TEST_PRECISION);
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ GreatCircle.fromPoints(Point2S.of(Double.POSITIVE_INFINITY, Geometry.HALF_PI), p2, TEST_PRECISION);
+ }, GeometryException.class);
+ }
+
+ @Test
+ public void testOffset_point() {
+ // --- arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // --- act/assert
+
+ // on circle
+ for (double polar = Geometry.MINUS_HALF_PI; polar <= Geometry.HALF_PI; polar += 0.1) {
+ Assert.assertEquals(0, circle.offset(Point2S.of(Geometry.HALF_PI, polar)), TEST_EPS);
+ Assert.assertEquals(0, circle.offset(Point2S.of(Geometry.MINUS_HALF_PI, polar)), TEST_EPS);
+ }
+
+ // +1/-1
+ Assert.assertEquals(-1, circle.offset(Point2S.of(Geometry.HALF_PI + 1, Geometry.HALF_PI)), TEST_EPS);
+ Assert.assertEquals(1, circle.offset(Point2S.of(Geometry.MINUS_HALF_PI + 1, Geometry.HALF_PI)), TEST_EPS);
+
+ // poles
+ Assert.assertEquals(Geometry.MINUS_HALF_PI, circle.offset(Point2S.of(Geometry.PI, Geometry.HALF_PI)), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, circle.offset(Point2S.of(Geometry.ZERO_PI, Geometry.HALF_PI)), TEST_EPS);
+ }
+
+ @Test
+ public void testOffset_vector() {
+ // --- arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // --- act/assert
+
+ // on circle
+ Assert.assertEquals(0, circle.offset(Vector3D.of(0, 1, 0)), TEST_EPS);
+ Assert.assertEquals(0, circle.offset(Vector3D.of(0, 0, 1)), TEST_EPS);
+ Assert.assertEquals(0, circle.offset(Vector3D.of(0, -1, 0)), TEST_EPS);
+ Assert.assertEquals(0, circle.offset(Vector3D.of(0, 0, -1)), TEST_EPS);
+
+ // +1/-1
+ Assert.assertEquals(-0.25 * Geometry.PI, circle.offset(Vector3D.of(-1, 1, 0)), TEST_EPS);
+ Assert.assertEquals(-0.25 * Geometry.PI, circle.offset(Vector3D.of(-1, 0, 1)), TEST_EPS);
+ Assert.assertEquals(-0.25 * Geometry.PI, circle.offset(Vector3D.of(-1, -1, 0)), TEST_EPS);
+ Assert.assertEquals(-0.25 * Geometry.PI, circle.offset(Vector3D.of(-1, 0, -1)), TEST_EPS);
+
+ Assert.assertEquals(0.25 * Geometry.PI, circle.offset(Vector3D.of(1, 1, 0)), TEST_EPS);
+ Assert.assertEquals(0.25 * Geometry.PI, circle.offset(Vector3D.of(1, 0, 1)), TEST_EPS);
+ Assert.assertEquals(0.25 * Geometry.PI, circle.offset(Vector3D.of(1, -1, 0)), TEST_EPS);
+ Assert.assertEquals(0.25 * Geometry.PI, circle.offset(Vector3D.of(1, 0, -1)), TEST_EPS);
+
+ // poles
+ Assert.assertEquals(Geometry.MINUS_HALF_PI, circle.offset(Vector3D.Unit.MINUS_X), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, circle.offset(Vector3D.Unit.PLUS_X), TEST_EPS);
+ }
+
+ @Test
+ public void testAzimuth_point() {
+ // --- arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // --- act/assert
+
+ // on circle
+ Assert.assertEquals(Geometry.HALF_PI, circle.azimuth(Point2S.from(Vector3D.of(0, 1, 0))), TEST_EPS);
+ Assert.assertEquals(Geometry.ZERO_PI, circle.azimuth(Point2S.from(Vector3D.of(0, 0, 1))), TEST_EPS);
+ Assert.assertEquals(1.5 * Geometry.PI, circle.azimuth(Point2S.from(Vector3D.of(0, -1, 0))), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, circle.azimuth(Point2S.from(Vector3D.of(0, 0, -1))), TEST_EPS);
+
+ // +1/-1
+ Assert.assertEquals(Geometry.HALF_PI, circle.azimuth(Point2S.from(Vector3D.of(-1, 1, 0))), TEST_EPS);
+ Assert.assertEquals(Geometry.ZERO_PI, circle.azimuth(Point2S.from(Vector3D.of(-1, 0, 1))), TEST_EPS);
+ Assert.assertEquals(1.5 * Geometry.PI, circle.azimuth(Point2S.from(Vector3D.of(-1, -1, 0))), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, circle.azimuth(Point2S.from(Vector3D.of(-1, 0, -1))), TEST_EPS);
+
+ Assert.assertEquals(Geometry.HALF_PI, circle.azimuth(Point2S.from(Vector3D.of(1, 1, 0))), TEST_EPS);
+ Assert.assertEquals(Geometry.ZERO_PI, circle.azimuth(Point2S.from(Vector3D.of(1, 0, 1))), TEST_EPS);
+ Assert.assertEquals(1.5 * Geometry.PI, circle.azimuth(Point2S.from(Vector3D.of(1, -1, 0))), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, circle.azimuth(Point2S.from(Vector3D.of(1, 0, -1))), TEST_EPS);
+
+ // poles
+ Assert.assertEquals(0, circle.azimuth(Point2S.from(Vector3D.Unit.MINUS_X)), TEST_EPS);
+ Assert.assertEquals(0, circle.azimuth(Point2S.from(Vector3D.Unit.PLUS_X)), TEST_EPS);
+ }
+
+ @Test
+ public void testAzimuth_vector() {
+ // --- arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // --- act/assert
+
+ // on circle
+ Assert.assertEquals(Geometry.HALF_PI, circle.azimuth(Vector3D.of(0, 1, 0)), TEST_EPS);
+ Assert.assertEquals(Geometry.ZERO_PI, circle.azimuth(Vector3D.of(0, 0, 1)), TEST_EPS);
+ Assert.assertEquals(1.5 * Geometry.PI, circle.azimuth(Vector3D.of(0, -1, 0)), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, circle.azimuth(Vector3D.of(0, 0, -1)), TEST_EPS);
+
+ // +1/-1
+ Assert.assertEquals(Geometry.HALF_PI, circle.azimuth(Vector3D.of(-1, 1, 0)), TEST_EPS);
+ Assert.assertEquals(Geometry.ZERO_PI, circle.azimuth(Vector3D.of(-1, 0, 1)), TEST_EPS);
+ Assert.assertEquals(1.5 * Geometry.PI, circle.azimuth(Vector3D.of(-1, -1, 0)), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, circle.azimuth(Vector3D.of(-1, 0, -1)), TEST_EPS);
+
+ Assert.assertEquals(Geometry.HALF_PI, circle.azimuth(Vector3D.of(1, 1, 0)), TEST_EPS);
+ Assert.assertEquals(Geometry.ZERO_PI, circle.azimuth(Vector3D.of(1, 0, 1)), TEST_EPS);
+ Assert.assertEquals(1.5 * Geometry.PI, circle.azimuth(Vector3D.of(1, -1, 0)), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, circle.azimuth(Vector3D.of(1, 0, -1)), TEST_EPS);
+
+ // poles
+ Assert.assertEquals(0, circle.azimuth(Vector3D.Unit.MINUS_X), TEST_EPS);
+ Assert.assertEquals(0, circle.azimuth(Vector3D.Unit.PLUS_X), TEST_EPS);
+ }
+
+ @Test
+ public void testVectorAt() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act/assert
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_Z, circle.vectorAt(Geometry.ZERO_PI), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_Y, circle.vectorAt(Geometry.HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.MINUS_Z, circle.vectorAt(Geometry.PI), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.MINUS_Y, circle.vectorAt(Geometry.MINUS_HALF_PI), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_Z, circle.vectorAt(Geometry.TWO_PI), TEST_EPS);
+ }
+
+ @Test
+ public void testProject() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act/assert
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI),
+ circle.project(Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI),
+ circle.project(Point2S.of(Geometry.HALF_PI + 1, Geometry.HALF_PI)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI),
+ circle.project(Point2S.of(Geometry.HALF_PI - 1, Geometry.HALF_PI)), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI),
+ circle.project(Point2S.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI),
+ circle.project(Point2S.of(Geometry.MINUS_HALF_PI + 1, Geometry.HALF_PI)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.MINUS_HALF_PI, Geometry.HALF_PI),
+ circle.project(Point2S.of(Geometry.MINUS_HALF_PI - 1, Geometry.HALF_PI)), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_poles() {
+ // arrange
+ GreatCircle minusXCircle = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ GreatCircle plusZCircle = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_Y, TEST_PRECISION);
+
+ // act
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.ZERO_PI, Geometry.ZERO_PI),
+ minusXCircle.project(Point2S.from(Vector3D.Unit.MINUS_X)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.ZERO_PI, Geometry.ZERO_PI),
+ minusXCircle.project(Point2S.from(Vector3D.Unit.PLUS_X)), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.of(1.5 * Geometry.PI, Geometry.HALF_PI),
+ plusZCircle.project(Point2S.from(Vector3D.Unit.PLUS_Z)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(1.5 * Geometry.PI, Geometry.HALF_PI),
+ plusZCircle.project(Point2S.from(Vector3D.Unit.MINUS_Z)), TEST_EPS);
+ }
+
+ @Test
+ public void testReverse() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ GreatCircle reverse = circle.reverse();
+
+ // assert
+ checkGreatCircle(reverse, Vector3D.Unit.MINUS_Z, Vector3D.Unit.PLUS_X);
+ }
+
+ @Test
+ public void testTransform_rotateAroundPole() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(
+ Point2S.of(0, Geometry.HALF_PI),
+ Point2S.of(1, Geometry.HALF_PI),
+ TEST_PRECISION);
+
+ Transform2S t = Transform2S.createRotation(circle.getPolePoint(), 0.25 * Geometry.PI);
+
+ // act
+ GreatCircle result = circle.transform(t);
+
+ // assert
+ Assert.assertNotSame(circle, result);
+ checkGreatCircle(result, Vector3D.Unit.PLUS_Z, Vector3D.Unit.from(1, 1, 0));
+ }
+
+ @Test
+ public void testTransform_rotateAroundNonPole() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(
+ Point2S.of(0, Geometry.HALF_PI),
+ Point2S.of(1, Geometry.HALF_PI),
+ TEST_PRECISION);
+
+ Transform2S t = Transform2S.createRotation(Point2S.of(0, Geometry.HALF_PI), Geometry.HALF_PI);
+
+ // act
+ GreatCircle result = circle.transform(t);
+
+ // assert
+ Assert.assertNotSame(circle, result);
+ checkGreatCircle(result, Vector3D.Unit.MINUS_Y, Vector3D.Unit.PLUS_X);
+ }
+
+ @Test
+ public void testTransform_piMinusAzimuth() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(
+ Point2S.of(0, Geometry.HALF_PI),
+ Point2S.of(1, Geometry.HALF_PI),
+ TEST_PRECISION);
+
+ Transform2S t = Transform2S.createReflection(Point2S.PLUS_J)
+ .rotate(Point2S.PLUS_K, Geometry.PI);
+
+ // act
+ GreatCircle result = circle.transform(t);
+
+ // assert
+ Assert.assertNotSame(circle, result);
+ checkGreatCircle(result, Vector3D.Unit.MINUS_Z, Vector3D.Unit.MINUS_X);
+ }
+
+ @Test
+ public void testSimilarOrientation() {
+ // arrange
+ GreatCircle a = GreatCircle.fromPole(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+ GreatCircle c = GreatCircle.fromPole(Vector3D.Unit.MINUS_Z, TEST_PRECISION);
+ GreatCircle d = GreatCircle.fromPole(Vector3D.Unit.from(1, 1, -1), TEST_PRECISION);
+ GreatCircle e = GreatCircle.fromPole(Vector3D.Unit.from(1, 1, 1), TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(a.similarOrientation(a));
+
+ Assert.assertFalse(a.similarOrientation(b));
+ Assert.assertFalse(a.similarOrientation(c));
+ Assert.assertFalse(a.similarOrientation(d));
+
+ Assert.assertTrue(a.similarOrientation(e));
+ }
+
+ @Test
+ public void testSpan() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ GreatArc span = circle.span();
+
+ // assert
+ Assert.assertSame(circle, span.getCircle());
+ Assert.assertTrue(span.getInterval().isFull());
+
+ Assert.assertNull(span.getStartPoint());
+ Assert.assertNull(span.getEndPoint());
+ }
+
+ @Test
+ public void testArc_points_2s() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act/assert
+ checkArc(circle.arc(Point2S.of(1, Geometry.HALF_PI), Point2S.of(0, 1)),
+ Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI), Point2S.of(0, 0));
+
+ Assert.assertTrue(circle.arc(Point2S.PLUS_I, Point2S.PLUS_I).isFull());
+ }
+
+ @Test
+ public void testArc_points_1s() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act/assert
+ checkArc(circle.arc(Point1S.of(Geometry.PI), Point1S.of(1.5 * Geometry.PI)),
+ Point2S.of(0, Geometry.PI), Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI));
+
+ Assert.assertTrue(circle.arc(Point1S.of(1), Point1S.of(1)).isFull());
+ }
+
+ @Test
+ public void testArc_azimuths() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act/assert
+ checkArc(circle.arc(Geometry.PI, 1.5 * Geometry.PI),
+ Point2S.of(0, Geometry.PI), Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI));
+
+ Assert.assertTrue(circle.arc(1, 1).isFull());
+ }
+
+ @Test
+ public void testArc_interval() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+ AngularInterval.Convex interval = AngularInterval.Convex.of(1, 2, TEST_PRECISION);
+
+ // act
+ GreatArc arc = circle.arc(interval);
+
+ // assert
+ Assert.assertSame(circle, arc.getCircle());
+ Assert.assertSame(interval, arc.getInterval());
+ }
+
+ @Test
+ public void testIntersection_parallel() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ GreatCircle a = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, precision);
+ GreatCircle b = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, precision);
+ GreatCircle c = GreatCircle.fromPole(Vector3D.Unit.of(1, 1e-4, 1e-4), precision);
+ GreatCircle d = GreatCircle.fromPole(Vector3D.Unit.MINUS_X, precision);
+ GreatCircle e = GreatCircle.fromPole(Vector3D.Unit.of(-1, 1e-4, 1e-4), precision);
+
+ // act/assert
+ Assert.assertNull(a.intersection(b));
+ Assert.assertNull(a.intersection(c));
+ Assert.assertNull(a.intersection(d));
+ Assert.assertNull(a.intersection(e));
+ }
+
+ @Test
+ public void testIntersection() {
+ // arrange
+ GreatCircle a = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPole(Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ GreatCircle c = GreatCircle.fromPole(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act/assert
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_Z,
+ a.intersection(b).getVector(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.MINUS_Z,
+ b.intersection(a).getVector(), TEST_EPS);
+
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_X,
+ b.intersection(c).getVector(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.MINUS_X,
+ c.intersection(b).getVector(), TEST_EPS);
+ }
+
+ @Test
+ public void testAngle_withoutReferencePoint() {
+ // arrange
+ GreatCircle a = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_I, TEST_PRECISION);
+ GreatCircle c = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_K, TEST_PRECISION);
+ GreatCircle d = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+ GreatCircle e = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.of(1, 0, 1),
+ Vector3D.Unit.PLUS_Y,
+ TEST_PRECISION);
+
+ GreatCircle f = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.of(1, 0, -1),
+ Vector3D.Unit.PLUS_Y,
+ TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(0, a.angle(a), TEST_EPS);
+ Assert.assertEquals(Geometry.PI, a.angle(b), TEST_EPS);
+
+ Assert.assertEquals(Geometry.HALF_PI, a.angle(c), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, c.angle(a), TEST_EPS);
+
+ Assert.assertEquals(Geometry.HALF_PI, a.angle(d), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, d.angle(a), TEST_EPS);
+
+ Assert.assertEquals(0.25 * Geometry.PI, a.angle(e), TEST_EPS);
+ Assert.assertEquals(0.25 * Geometry.PI, e.angle(a), TEST_EPS);
+
+ Assert.assertEquals(0.75 * Geometry.PI, a.angle(f), TEST_EPS);
+ Assert.assertEquals(0.75 * Geometry.PI, f.angle(a), TEST_EPS);
+ }
+
+ @Test
+ public void testAngle_withReferencePoint() {
+ // arrange
+ GreatCircle a = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_I, TEST_PRECISION);
+ GreatCircle c = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_K, TEST_PRECISION);
+ GreatCircle d = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+ GreatCircle e = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.of(1, 0, 1),
+ Vector3D.Unit.PLUS_Y,
+ TEST_PRECISION);
+
+ GreatCircle f = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.of(1, 0, -1),
+ Vector3D.Unit.PLUS_Y,
+ TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(0, a.angle(a, Point2S.PLUS_J), TEST_EPS);
+ Assert.assertEquals(0, a.angle(a, Point2S.MINUS_J), TEST_EPS);
+
+ Assert.assertEquals(-Geometry.PI, a.angle(b, Point2S.PLUS_J), TEST_EPS);
+ Assert.assertEquals(-Geometry.PI, a.angle(b, Point2S.MINUS_J), TEST_EPS);
+
+ Assert.assertEquals(Geometry.HALF_PI, a.angle(c, Point2S.PLUS_I), TEST_EPS);
+ Assert.assertEquals(-Geometry.HALF_PI, a.angle(c, Point2S.MINUS_I), TEST_EPS);
+
+ Assert.assertEquals(-Geometry.HALF_PI, c.angle(a, Point2S.PLUS_I), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, c.angle(a, Point2S.MINUS_I), TEST_EPS);
+
+ Assert.assertEquals(Geometry.HALF_PI, a.angle(d, Point2S.PLUS_J), TEST_EPS);
+ Assert.assertEquals(-Geometry.HALF_PI, a.angle(d, Point2S.MINUS_J), TEST_EPS);
+
+ Assert.assertEquals(-Geometry.HALF_PI, d.angle(a, Point2S.PLUS_J), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, d.angle(a, Point2S.MINUS_J), TEST_EPS);
+
+ Assert.assertEquals(0.25 * Geometry.PI, a.angle(e, Point2S.PLUS_J), TEST_EPS);
+ Assert.assertEquals(-0.25 * Geometry.PI, a.angle(e, Point2S.MINUS_J), TEST_EPS);
+
+ Assert.assertEquals(-0.25 * Geometry.PI, e.angle(a, Point2S.PLUS_J), TEST_EPS);
+ Assert.assertEquals(0.25 * Geometry.PI, e.angle(a, Point2S.MINUS_J), TEST_EPS);
+
+ Assert.assertEquals(0.75 * Geometry.PI, a.angle(f, Point2S.PLUS_J), TEST_EPS);
+ Assert.assertEquals(-0.75 * Geometry.PI, a.angle(f, Point2S.MINUS_J), TEST_EPS);
+
+ Assert.assertEquals(-0.75 * Geometry.PI, f.angle(a, Point2S.PLUS_J), TEST_EPS);
+ Assert.assertEquals(0.75 * Geometry.PI, f.angle(a, Point2S.MINUS_J), TEST_EPS);
+ }
+
+ @Test
+ public void testAngle_withReferencePoint_pointEquidistanceFromIntersections() {
+ // arrange
+ GreatCircle a = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatCircle b = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.of(1, 0, 1),
+ Vector3D.Unit.PLUS_Y,
+ TEST_PRECISION);
+
+ // act/assert
+ Assert.assertEquals(-0.25 * Geometry.PI, a.angle(b, Point2S.PLUS_I), TEST_EPS);
+ Assert.assertEquals(-0.25 * Geometry.PI, a.angle(b, Point2S.MINUS_I), TEST_EPS);
+ }
+
+ @Test
+ public void testToSubspace() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z, TEST_PRECISION);
+
+ // act/assert
+ SphericalTestUtils.assertPointsEqual(Point1S.ZERO,
+ circle.toSubspace(Point2S.from(Vector3D.Unit.MINUS_Z)), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point1S.of(0.25 * Geometry.PI),
+ circle.toSubspace(Point2S.from(Vector3D.of(-1, -1, -1))), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.of(0.75 * Geometry.PI),
+ circle.toSubspace(Point2S.from(Vector3D.of(-1, 1, 1))), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.of(1.25 * Geometry.PI),
+ circle.toSubspace(Point2S.from(Vector3D.of(1, -1, 1))), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.of(1.75 * Geometry.PI),
+ circle.toSubspace(Point2S.from(Vector3D.of(1, 1, -1))), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point1S.ZERO,
+ circle.toSubspace(Point2S.from(Vector3D.Unit.PLUS_Y)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point1S.ZERO,
+ circle.toSubspace(Point2S.from(Vector3D.Unit.MINUS_Y)), TEST_EPS);
+ }
+
+ @Test
+ public void testToSpace() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z, TEST_PRECISION);
+
+ // act/assert
+ SphericalTestUtils.assertPointsEqual(Point2S.from(Vector3D.Unit.MINUS_Z),
+ circle.toSpace(Point1S.ZERO), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.from(Vector3D.of(-1, 0, -1)),
+ circle.toSpace(Point1S.of(0.25 * Geometry.PI)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.from(Vector3D.of(-1, 0, 1)),
+ circle.toSpace(Point1S.of(0.75 * Geometry.PI)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.from(Vector3D.of(1, 0, 1)),
+ circle.toSpace(Point1S.of(1.25 * Geometry.PI)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.from(Vector3D.of(1, 0, -1)),
+ circle.toSpace(Point1S.of(1.75 * Geometry.PI)), TEST_EPS);
+ }
+
+ @Test
+ public void testEq() {
+ // arrange
+ double eps = 1e-3;
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(eps);
+
+ GreatCircle a = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, precision);
+
+ GreatCircle b = GreatCircle.fromPoleAndU(Vector3D.Unit.MINUS_Z, Vector3D.Unit.PLUS_X, precision);
+ GreatCircle c = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_X, precision);
+ GreatCircle d = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ GreatCircle e = GreatCircle.fromPoleAndU(Vector3D.of(1e-6, 0, 1), Vector3D.Unit.PLUS_X, precision);
+ GreatCircle f = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.of(1, 1e-6, 0), precision);
+ GreatCircle g = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X,
+ new EpsilonDoublePrecisionContext(eps));
+
+ // act/assert
+ Assert.assertTrue(a.eq(a));;
+
+ Assert.assertFalse(a.eq(b));
+ Assert.assertFalse(a.eq(c));
+ Assert.assertFalse(a.eq(d));
+
+ Assert.assertTrue(a.eq(e));
+ Assert.assertTrue(e.eq(a));
+
+ Assert.assertTrue(a.eq(f));
+ Assert.assertTrue(f.eq(a));
+
+ Assert.assertTrue(g.eq(e));
+ Assert.assertTrue(e.eq(g));
+ }
+
+ @Test
+ public void testHashCode() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ GreatCircle a = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ GreatCircle b = GreatCircle.fromPoleAndU(Vector3D.of(0, 1, 1), Vector3D.Unit.PLUS_X, TEST_PRECISION);
+ GreatCircle c = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_X, TEST_PRECISION);
+ GreatCircle d = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, precision);
+
+ GreatCircle e = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ int hash = a.hashCode();
+
+ // act/assert
+ Assert.assertEquals(hash, a.hashCode());
+
+ Assert.assertNotEquals(hash, b.hashCode());
+ Assert.assertNotEquals(hash, c.hashCode());
+ Assert.assertNotEquals(hash, d.hashCode());
+
+ Assert.assertEquals(hash, e.hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // arrange
+ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
+
+ GreatCircle a = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ GreatCircle b = GreatCircle.fromPoleAndU(Vector3D.Unit.MINUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+ GreatCircle c = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_X, TEST_PRECISION);
+ GreatCircle d = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, precision);
+
+ GreatCircle e = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act/assert
+ Assert.assertTrue(a.equals(a));
+
+ Assert.assertFalse(a.equals(null));
+ Assert.assertFalse(a.equals(new Object()));
+
+ Assert.assertFalse(a.equals(b));
+ Assert.assertFalse(a.equals(c));
+ Assert.assertFalse(a.equals(d));
+
+ Assert.assertTrue(a.equals(e));
+ Assert.assertTrue(e.equals(a));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoleAndU(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ String str = circle.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("GreatCircle[", str);
+ GeometryTestUtils.assertContains("pole= (0.0, 0.0, 1.0)", str);
+ GeometryTestUtils.assertContains("u= (1.0, 0.0, 0.0)", str);
+ GeometryTestUtils.assertContains("v= (0.0, 1.0, 0.0)", str);
+ }
+
+ private static void checkGreatCircle(GreatCircle circle, Vector3D pole, Vector3D u) {
+ SphericalTestUtils.assertVectorsEqual(pole, circle.getPole(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(pole, circle.getW(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(u, circle.getU(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(pole.cross(u), circle.getV(), TEST_EPS);
+
+ Point2S plusPolePt = Point2S.from(circle.getPole());
+ Point2S minusPolePt = Point2S.from(circle.getPole().negate());
+ Point2S origin = Point2S.from(circle.getU());
+
+ SphericalTestUtils.assertPointsEqual(plusPolePt, circle.getPolePoint(), TEST_EPS);
+
+ Assert.assertFalse(circle.contains(plusPolePt));
+ Assert.assertFalse(circle.contains(minusPolePt));
+ Assert.assertTrue(circle.contains(origin));
+
+ Assert.assertEquals(HyperplaneLocation.MINUS, circle.classify(plusPolePt));
+ Assert.assertEquals(HyperplaneLocation.PLUS, circle.classify(minusPolePt));
+ Assert.assertEquals(HyperplaneLocation.ON, circle.classify(origin));
+ }
+
+ private static void checkArc(GreatArc arc, Point2S start, Point2S end) {
+ SphericalTestUtils.assertPointsEq(start, arc.getStartPoint(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(end, arc.getEndPoint(), TEST_EPS);
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/InteriorAngleGreatArcConnectorTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/InteriorAngleGreatArcConnectorTest.java
new file mode 100644
index 0000000..867ef64
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/InteriorAngleGreatArcConnectorTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.apache.commons.geometry.spherical.twod.InteriorAngleGreatArcConnector.Maximize;
+import org.apache.commons.geometry.spherical.twod.InteriorAngleGreatArcConnector.Minimize;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class InteriorAngleGreatArcConnectorTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testConnectAll_empty() {
+ runWithMaxAndMin(connector -> {
+ // arrange
+ List<GreatArc> arcs = new ArrayList<>();
+ connector.add(arcs);
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(0, paths.size());
+ });
+ }
+
+ @Test
+ public void testConnectAll_singlePath() {
+ runWithMaxAndMin(connector -> {
+ // arrange
+ List<GreatArc> arcs = Arrays.asList(
+ GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION)
+ );
+ connector.add(arcs);
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll();
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ GreatArcPath a = paths.get(0);
+ Assert.assertEquals(1, a.getArcs().size());
+ assertPathPoints(a, Point2S.PLUS_I, Point2S.PLUS_J);
+ });
+ }
+
+ @Test
+ public void testConnectAll_maximize_instance() {
+ // arrange
+ GreatArc a1 = GreatArc.fromPoints(Point2S.PLUS_K, Point2S.PLUS_I, TEST_PRECISION);
+ GreatArc a2 = GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatArc a3 = GreatArc.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ GreatArc b1 = GreatArc.fromPoints(Point2S.PLUS_K, Point2S.MINUS_I, TEST_PRECISION);
+ GreatArc b2 = GreatArc.fromPoints(Point2S.MINUS_I, Point2S.MINUS_J, TEST_PRECISION);
+ GreatArc b3 = GreatArc.fromPoints(Point2S.MINUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ InteriorAngleGreatArcConnector connector = new InteriorAngleGreatArcConnector.Maximize();
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll(Arrays.asList(b3, b1, a1, a3, b2, a2));
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertPathPoints(paths.get(0),
+ Point2S.PLUS_K,
+ Point2S.MINUS_I,
+ Point2S.MINUS_J,
+ Point2S.PLUS_K,
+ Point2S.PLUS_I,
+ Point2S.PLUS_J,
+ Point2S.PLUS_K
+ );
+ }
+
+ @Test
+ public void testConnectAll_maximize_method() {
+ // arrange
+ GreatArc a1 = GreatArc.fromPoints(Point2S.PLUS_K, Point2S.PLUS_I, TEST_PRECISION);
+ GreatArc a2 = GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatArc a3 = GreatArc.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ GreatArc b1 = GreatArc.fromPoints(Point2S.PLUS_K, Point2S.MINUS_I, TEST_PRECISION);
+ GreatArc b2 = GreatArc.fromPoints(Point2S.MINUS_I, Point2S.MINUS_J, TEST_PRECISION);
+ GreatArc b3 = GreatArc.fromPoints(Point2S.MINUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ // act
+ List<GreatArcPath> paths = InteriorAngleGreatArcConnector.connectMaximized(
+ Arrays.asList(b3, b1, a1, a3, b2, a2));
+
+ // assert
+ Assert.assertEquals(1, paths.size());
+
+ assertPathPoints(paths.get(0),
+ Point2S.PLUS_K,
+ Point2S.MINUS_I,
+ Point2S.MINUS_J,
+ Point2S.PLUS_K,
+ Point2S.PLUS_I,
+ Point2S.PLUS_J,
+ Point2S.PLUS_K
+ );
+ }
+
+ @Test
+ public void testConnectAll_minimize_instance() {
+ // arrange
+ GreatArc a1 = GreatArc.fromPoints(Point2S.PLUS_K, Point2S.PLUS_I, TEST_PRECISION);
+ GreatArc a2 = GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatArc a3 = GreatArc.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ GreatArc b1 = GreatArc.fromPoints(Point2S.PLUS_K, Point2S.MINUS_I, TEST_PRECISION);
+ GreatArc b2 = GreatArc.fromPoints(Point2S.MINUS_I, Point2S.MINUS_J, TEST_PRECISION);
+ GreatArc b3 = GreatArc.fromPoints(Point2S.MINUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ InteriorAngleGreatArcConnector connector = new InteriorAngleGreatArcConnector.Minimize();
+
+ // act
+ List<GreatArcPath> paths = connector.connectAll(Arrays.asList(b3, b1, a1, a3, b2, a2));
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertPathPoints(paths.get(0),
+ Point2S.PLUS_K,
+ Point2S.MINUS_I,
+ Point2S.MINUS_J,
+ Point2S.PLUS_K
+ );
+
+ assertPathPoints(paths.get(1),
+ Point2S.PLUS_K,
+ Point2S.PLUS_I,
+ Point2S.PLUS_J,
+ Point2S.PLUS_K
+ );
+ }
+
+ @Test
+ public void testConnectAll_minimize_method() {
+ // arrange
+ GreatArc a1 = GreatArc.fromPoints(Point2S.PLUS_K, Point2S.PLUS_I, TEST_PRECISION);
+ GreatArc a2 = GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ GreatArc a3 = GreatArc.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ GreatArc b1 = GreatArc.fromPoints(Point2S.PLUS_K, Point2S.MINUS_I, TEST_PRECISION);
+ GreatArc b2 = GreatArc.fromPoints(Point2S.MINUS_I, Point2S.MINUS_J, TEST_PRECISION);
+ GreatArc b3 = GreatArc.fromPoints(Point2S.MINUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ // act
+ List<GreatArcPath> paths = InteriorAngleGreatArcConnector.connectMinimized(
+ Arrays.asList(b3, b1, a1, a3, b2, a2));
+
+ // assert
+ Assert.assertEquals(2, paths.size());
+
+ assertPathPoints(paths.get(0),
+ Point2S.PLUS_K,
+ Point2S.MINUS_I,
+ Point2S.MINUS_J,
+ Point2S.PLUS_K
+ );
+
+ assertPathPoints(paths.get(1),
+ Point2S.PLUS_K,
+ Point2S.PLUS_I,
+ Point2S.PLUS_J,
+ Point2S.PLUS_K
+ );
+ }
+
+ /**
+ * Run the given consumer function twice, once with a Maximize instance and once with
+ * a Minimize instance.
+ */
+ private static void runWithMaxAndMin(Consumer<InteriorAngleGreatArcConnector> body) {
+ body.accept(new Maximize());
+ body.accept(new Minimize());
+ }
+
+ private static void assertPathPoints(GreatArcPath path, Point2S ... points) {
+ List<Point2S> expectedPoints = Arrays.asList(points);
+ List<Point2S> actualPoints = path.getVertices();
+
+ String msg = "Expected path points to equal " + expectedPoints + " but was " + actualPoints;
+ Assert.assertEquals(msg, expectedPoints.size(), actualPoints.size());
+
+ for (int i=0; i<expectedPoints.size(); ++i) {
+ SphericalTestUtils.assertPointsEq(expectedPoints.get(i), actualPoints.get(i), TEST_EPS);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/Point2STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/Point2STest.java
new file mode 100644
index 0000000..fbcdab6
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/Point2STest.java
@@ -0,0 +1,346 @@
+/*
+ * 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.util.Comparator;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Point2STest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ @Test
+ public void testProperties() {
+ for (int k = -2; k < 3; ++k) {
+ // arrange
+ Point2S p = Point2S.of(1.0 + k * Geometry.TWO_PI, 1.4);
+
+ // act/assert
+ Assert.assertEquals(1.0, p.getAzimuth(), TEST_EPS);
+ Assert.assertEquals(1.4, p.getPolar(), TEST_EPS);
+
+ Assert.assertEquals(Math.cos(1.0) * Math.sin(1.4), p.getVector().getX(), TEST_EPS);
+ Assert.assertEquals(Math.sin(1.0) * Math.sin(1.4), p.getVector().getY(), TEST_EPS);
+ Assert.assertEquals(Math.cos(1.4), p.getVector().getZ(), TEST_EPS);
+
+ Assert.assertFalse(p.isNaN());
+ }
+ }
+
+ @Test
+ public void testAzimuthPolarComparator() {
+ // arrange
+ Comparator<Point2S> comp = Point2S.POLAR_AZIMUTH_ASCENDING_ORDER;
+
+ // act/assert
+ Assert.assertEquals(0, comp.compare(Point2S.of(1, 2), Point2S.of(1, 2)));
+ Assert.assertEquals(1, comp.compare(Point2S.of(1, 2), Point2S.of(2, 1)));
+ Assert.assertEquals(-1, comp.compare(Point2S.of(2, 1), Point2S.of(1, 2)));
+
+ Assert.assertEquals(-1, comp.compare(Point2S.of(1, 2), Point2S.of(1, 3)));
+ Assert.assertEquals(1, comp.compare(Point2S.of(1, 3), Point2S.of(1, 2)));
+
+ Assert.assertEquals(1, comp.compare(null, Point2S.of(1, 2)));
+ Assert.assertEquals(-1, comp.compare(Point2S.of(1, 2), null));
+ Assert.assertEquals(0, comp.compare(null, null));
+ }
+
+ @Test
+ public void testFrom_vector() {
+ // arrange
+ double quarterPi = 0.25 * Geometry.PI;
+
+ // act/assert
+ checkPoint(Point2S.from(Vector3D.of(1, 1, 0)), quarterPi, Geometry.HALF_PI);
+ checkPoint(Point2S.from(Vector3D.of(1, 0, 1)), 0, quarterPi);
+ checkPoint(Point2S.from(Vector3D.of(0, 1, 1)), Geometry.HALF_PI, quarterPi);
+
+ checkPoint(Point2S.from(Vector3D.of(1, -1, 0)), Geometry.TWO_PI - quarterPi, Geometry.HALF_PI);
+ checkPoint(Point2S.from(Vector3D.of(-1, 0, -1)), Geometry.PI, Geometry.PI - quarterPi);
+ checkPoint(Point2S.from(Vector3D.of(0, -1, -1)), Geometry.TWO_PI - Geometry.HALF_PI, Geometry.PI - quarterPi);
+ }
+
+ @Test
+ public void testNaN() {
+ // act/assert
+ Assert.assertTrue(Point2S.NaN.isNaN());
+ Assert.assertTrue(Point2S.NaN.equals(Point2S.of(Double.NaN, 1.0)));
+ Assert.assertFalse(Point2S.of(1.0, 1.3).equals(Point2S.NaN));
+ Assert.assertNull(Point2S.NaN.getVector());
+
+ Assert.assertEquals(Point2S.NaN.hashCode(), Point2S.of(Double.NaN, Double.NaN).hashCode());
+ }
+
+ @Test
+ public void testInfinite() {
+ // act/assert
+ Assert.assertTrue(Point2S.of(0, Double.POSITIVE_INFINITY).isInfinite());
+ Assert.assertTrue(Point2S.of(Double.POSITIVE_INFINITY, 0).isInfinite());
+
+ Assert.assertTrue(Point2S.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY).isInfinite());
+
+ Assert.assertFalse(Point2S.of(0, 0).isInfinite());
+ Assert.assertFalse(Point2S.of(1, 1).isInfinite());
+ Assert.assertFalse(Point2S.NaN.isInfinite());
+ }
+
+ @Test
+ public void testFinite() {
+ // act/assert
+ Assert.assertTrue(Point2S.of(0, 0).isFinite());
+ Assert.assertTrue(Point2S.of(1, 1).isFinite());
+
+ Assert.assertFalse(Point2S.of(0, Double.POSITIVE_INFINITY).isFinite());
+ Assert.assertFalse(Point2S.of(Double.POSITIVE_INFINITY, 0).isFinite());
+ Assert.assertFalse(Point2S.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY).isFinite());
+
+ Assert.assertFalse(Point2S.NaN.isFinite());
+ }
+
+ @Test
+ public void testDistance() {
+ // arrange
+ Point2S a = Point2S.of(1.0, 0.5 * Geometry.PI);
+ Point2S b = Point2S.of(a.getAzimuth() + 0.5 * Geometry.PI, a.getPolar());
+
+ // act/assert
+ Assert.assertEquals(0.5 * Geometry.PI, a.distance(b), 1.0e-10);
+ Assert.assertEquals(Geometry.PI, a.distance(a.antipodal()), 1.0e-10);
+ Assert.assertEquals(0.5 * Geometry.PI, Point2S.MINUS_I.distance(Point2S.MINUS_K), 1.0e-10);
+ Assert.assertEquals(0.0, Point2S.of(1.0, 0).distance(Point2S.of(2.0, 0)), 1.0e-10);
+ }
+
+ @Test
+ public void testSlerp_alongEquator() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_I;
+ Point2S p2 = Point2S.PLUS_J;
+
+ // act/assert
+ SphericalTestUtils.assertPointsEq(p1, p1.slerp(p2, 0), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0.25 * Geometry.HALF_PI, Geometry.HALF_PI), p1.slerp(p2, 0.25), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0.5 * Geometry.HALF_PI, Geometry.HALF_PI), p1.slerp(p2, 0.5), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0.75 * Geometry.HALF_PI, Geometry.HALF_PI), p1.slerp(p2, 0.75), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(p2, p1.slerp(p2, 1), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEq(p2, p2.slerp(p1, 0), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0.75 * Geometry.HALF_PI, Geometry.HALF_PI), p2.slerp(p1, 0.25), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0.5 * Geometry.HALF_PI, Geometry.HALF_PI), p2.slerp(p1, 0.5), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0.25 * Geometry.HALF_PI, Geometry.HALF_PI), p2.slerp(p1, 0.75), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(p1, p2.slerp(p1, 1), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_I, p1.slerp(p2, 2), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_J, p1.slerp(p2, -1), TEST_EPS);
+ }
+
+ @Test
+ public void testSlerp_alongMeridian() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_J;
+ Point2S p2 = Point2S.PLUS_K;
+
+ // act/assert
+ SphericalTestUtils.assertPointsEq(p1, p1.slerp(p2, 0), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(Geometry.HALF_PI, 0.75 * Geometry.HALF_PI), p1.slerp(p2, 0.25), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(Geometry.HALF_PI, 0.5 * Geometry.HALF_PI), p1.slerp(p2, 0.5), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(Geometry.HALF_PI, 0.25 * Geometry.HALF_PI), p1.slerp(p2, 0.75), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(p2, p1.slerp(p2, 1), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEq(p2, p2.slerp(p1, 0), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(Geometry.HALF_PI, 0.25 * Geometry.HALF_PI), p2.slerp(p1, 0.25), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(Geometry.HALF_PI, 0.5 * Geometry.HALF_PI), p2.slerp(p1, 0.5), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(Geometry.HALF_PI, 0.75 * Geometry.HALF_PI), p2.slerp(p1, 0.75), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(p1, p2.slerp(p1, 1), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_J, p1.slerp(p2, 2), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_K, p1.slerp(p2, -1), TEST_EPS);
+ }
+
+ @Test
+ public void testSlerp_samePoint() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_I;
+
+ // act/assert
+ SphericalTestUtils.assertPointsEq(p1, p1.slerp(p1, 0), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(p1, p1.slerp(p1, 0.5), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(p1, p1.slerp(p1, 1), TEST_EPS);
+ }
+
+ @Test
+ public void testSlerp_antipodal() {
+ // arrange
+ Point2S p1 = Point2S.PLUS_I;
+ Point2S p2 = Point2S.MINUS_I;
+
+ // act/assert
+ SphericalTestUtils.assertPointsEq(p1, p1.slerp(p1, 0), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(p1, p1.slerp(p1, 1), TEST_EPS);
+
+ Point2S pt = p1.slerp(p2, 0.5);
+ Assert.assertEquals(p1.distance(pt), p2.distance(pt), TEST_EPS);
+ }
+
+ @Test
+ public void testAntipodal() {
+ for (double az = -6 * Geometry.PI; az <= 6 * Geometry.PI; az += 0.1) {
+ for (double p = 0; p <= Geometry.PI; p += 0.1) {
+ // arrange
+ Point2S pt = Point2S.of(az, p);
+
+ // act
+ Point2S result = pt.antipodal();
+
+ // assert
+ Assert.assertEquals(Geometry.PI, pt.distance(result), TEST_EPS);
+ Assert.assertEquals(-1, pt.getVector().dot(result.getVector()), TEST_EPS);
+ }
+ }
+ }
+
+ @Test
+ public void testDimension() {
+ // arrange
+ Point2S pt = Point2S.of(1, 2);
+
+ // act/assert
+ Assert.assertEquals(2, pt.getDimension());
+ }
+
+ @Test
+ public void testEq() {
+ // arrange
+ DoublePrecisionContext smallEps = new EpsilonDoublePrecisionContext(1e-5);
+ DoublePrecisionContext largeEps = new EpsilonDoublePrecisionContext(5e-1);
+
+ Point2S a = Point2S.of(1.0, 2.0);
+ Point2S b = Point2S.of(1.0, 2.01);
+ Point2S c = Point2S.of(1.01, 2.0);
+ Point2S d = Point2S.of(1.0, 2.0);
+ Point2S e = Point2S.of(3.0, 2.0);
+
+ // act/assert
+ Assert.assertTrue(a.eq(a, smallEps));
+ Assert.assertFalse(a.eq(b, smallEps));
+ Assert.assertFalse(a.eq(c, smallEps));
+ Assert.assertTrue(a.eq(d, smallEps));
+ Assert.assertFalse(a.eq(e, smallEps));
+
+ Assert.assertTrue(a.eq(a, largeEps));
+ Assert.assertTrue(a.eq(b, largeEps));
+ Assert.assertTrue(a.eq(c, largeEps));
+ Assert.assertTrue(a.eq(d, largeEps));
+ Assert.assertFalse(a.eq(e, largeEps));
+ }
+
+ @Test
+ public void testHashCode() {
+ // arrange
+ Point2S a = Point2S.of(1.0, 2.0);
+ Point2S b = Point2S.of(1.0, 3.0);
+ Point2S c = Point2S.of(4.0, 2.0);
+ Point2S d = Point2S.of(1.0, 2.0);
+
+ // act
+ int hash = a.hashCode();
+
+ // assert
+ Assert.assertEquals(hash, a.hashCode());
+
+ Assert.assertNotEquals(hash, b.hashCode());
+ Assert.assertNotEquals(hash, c.hashCode());
+
+ Assert.assertEquals(hash, d.hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // arrange
+ Point2S a = Point2S.of(1.0, 2.0);
+ Point2S b = Point2S.of(1.0, 3.0);
+ Point2S c = Point2S.of(4.0, 2.0);
+ Point2S d = Point2S.of(1.0, 2.0);
+
+ // act/assert
+ Assert.assertFalse(a.equals(null));
+ Assert.assertFalse(a.equals(new Object()));
+
+ Assert.assertTrue(a.equals(a));
+
+ Assert.assertFalse(a.equals(b));
+ Assert.assertFalse(a.equals(c));
+
+ Assert.assertTrue(a.equals(d));
+ Assert.assertTrue(d.equals(a));
+ }
+
+ @Test
+ public void testEquals_poles() {
+ // arrange
+ Point2S a = Point2S.of(1.0, 0.0);
+ Point2S b = Point2S.of(0.0, 0.0);
+ Point2S c = Point2S.of(1.0, 0.0);
+
+ Point2S d = Point2S.of(-1.0, Geometry.PI);
+ Point2S e = Point2S.of(0.0, Geometry.PI);
+ Point2S f = Point2S.of(-1.0, Geometry.PI);
+
+ // act/assert
+ Assert.assertTrue(a.equals(a));
+ Assert.assertFalse(a.equals(b));
+ Assert.assertTrue(a.equals(c));
+
+ Assert.assertTrue(d.equals(d));
+ Assert.assertFalse(d.equals(e));
+ Assert.assertTrue(d.equals(f));
+ }
+
+ @Test
+ public void testToString() {
+ // act/assert
+ Assert.assertEquals("(0.0, 0.0)", Point2S.of(0.0, 0.0).toString());
+ Assert.assertEquals("(1.0, 2.0)", Point2S.of(1.0, 2.0).toString());
+ }
+
+ @Test
+ public void testParse() {
+ // act/assert
+ checkPoint(Point2S.parse("(0,0)"), 0.0, 0.0);
+ checkPoint(Point2S.parse("(1,2)"), 1.0, 2.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testParse_failure() {
+ // act/assert
+ Point2S.parse("abc");
+ }
+
+ private static void checkPoint(Point2S p, double az, double polar) {
+ String msg = "Expected (" + az + "," + polar + ") but was " + p;
+
+ Assert.assertEquals(msg, az, p.getAzimuth(), TEST_EPS);
+ Assert.assertEquals(msg, polar, p.getPolar(), TEST_EPS);
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java
new file mode 100644
index 0000000..29b51b5
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java
@@ -0,0 +1,714 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.apache.commons.geometry.spherical.twod.RegionBSPTree2S.RegionNode2S;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RegionBSPTree2STest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ // epsilon value for use when comparing computed barycenter locations;
+ // this must currently be set much higher than the other epsilon
+ private static final double BARYCENTER_EPS = 1e-2;
+
+ private static final GreatCircle EQUATOR = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ private static final GreatCircle X_MERIDIAN = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ private static final GreatCircle Y_MERIDIAN = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ @Test
+ public void testCtor_booleanArg_true() {
+ // act
+ RegionBSPTree2S tree = new RegionBSPTree2S(true);
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testCtor_booleanArg_false() {
+ // act
+ RegionBSPTree2S tree = new RegionBSPTree2S(false);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testCtor_default() {
+ // act
+ RegionBSPTree2S tree = new RegionBSPTree2S();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testFull_factoryMethod() {
+ // act
+ RegionBSPTree2S tree = RegionBSPTree2S.full();
+
+ // assert
+ Assert.assertTrue(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testEmpty_factoryMethod() {
+ // act
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertTrue(tree.isEmpty());
+ Assert.assertEquals(1, tree.count());
+ }
+
+ @Test
+ public void testFromConvexArea() {
+ // arrange
+ ConvexArea2S area = ConvexArea2S.fromVertexLoop(Arrays.asList(
+ Point2S.of(0.1, 0.1), Point2S.of(0, 0.5),
+ Point2S.of(0.15, 0.75), Point2S.of(0.3, 0.5),
+ Point2S.of(0.1, 0.1)
+ ), TEST_PRECISION);
+
+ // act
+ RegionBSPTree2S tree = RegionBSPTree2S.from(area);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+
+ Assert.assertEquals(area.getSize(), tree.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(area.getBarycenter(), tree.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testCopy() {
+ // arrange
+ RegionBSPTree2S tree = new RegionBSPTree2S(true);
+ tree.getRoot().cut(EQUATOR);
+
+ // act
+ RegionBSPTree2S copy = tree.copy();
+
+ // assert
+ Assert.assertNotSame(tree, copy);
+ Assert.assertEquals(3, copy.count());
+ }
+
+ @Test
+ public void testBoundaries() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+ insertPositiveQuadrant(tree);
+
+ // act
+ List<GreatArc> arcs = new ArrayList<>();
+ tree.boundaries().forEach(arcs::add);
+
+ // assert
+ Assert.assertEquals(3, arcs.size());
+ }
+
+ @Test
+ public void testGetBoundaries() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+ insertPositiveQuadrant(tree);
+
+ // act
+ List<GreatArc> arcs = tree.getBoundaries();
+
+ // assert
+ Assert.assertEquals(3, arcs.size());
+ }
+
+ @Test
+ public void testGetBoundaryPaths_cachesResult() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+ insertPositiveQuadrant(tree);
+
+ // act
+ List<GreatArcPath> a = tree.getBoundaryPaths();
+ List<GreatArcPath> b = tree.getBoundaryPaths();
+
+ // assert
+ Assert.assertSame(a, b);
+ }
+
+ @Test
+ public void testGetBoundaryPaths_recomputesResultOnChange() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+ tree.insert(EQUATOR.span());
+
+ // act
+ List<GreatArcPath> a = tree.getBoundaryPaths();
+ tree.insert(X_MERIDIAN.span());
+ List<GreatArcPath> b = tree.getBoundaryPaths();
+
+ // assert
+ Assert.assertNotSame(a, b);
+ }
+
+ @Test
+ public void testGetBoundaryPaths_isUnmodifiable() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+ tree.insert(EQUATOR.span());
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ tree.getBoundaryPaths().add(GreatArcPath.empty());
+ }, UnsupportedOperationException.class);
+ }
+
+ @Test
+ public void testToConvex_full() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.full();
+
+ // act
+ List<ConvexArea2S> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(1, result.size());
+ Assert.assertTrue(result.get(0).isFull());
+ }
+
+ @Test
+ public void testToConvex_empty() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+
+ // act
+ List<ConvexArea2S> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(0, result.size());
+ }
+
+ @Test
+ public void testToConvex_doubleLune() {
+ // arrange
+ RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
+ .append(EQUATOR.arc(0, Geometry.PI))
+ .append(X_MERIDIAN.arc(Geometry.PI, 0))
+ .append(EQUATOR.reverse().arc(0, Geometry.PI))
+ .append(X_MERIDIAN.reverse().arc(Geometry.PI, 0))
+ .build()
+ .toTree();
+
+ // act
+ List<ConvexArea2S> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(2, result.size());
+
+ double size = result.stream().collect(Collectors.summingDouble(a -> a.getSize()));
+ Assert.assertEquals(Geometry.TWO_PI, size, TEST_EPS);
+ }
+
+ @Test
+ public void testToConvex_doubleLune_comlement() {
+ // arrange
+ RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
+ .append(EQUATOR.arc(0, Geometry.PI))
+ .append(X_MERIDIAN.arc(Geometry.PI, 0))
+ .append(EQUATOR.reverse().arc(0, Geometry.PI))
+ .append(X_MERIDIAN.reverse().arc(Geometry.PI, 0))
+ .build()
+ .toTree();
+
+ // act
+ List<ConvexArea2S> result = tree.toConvex();
+
+ // assert
+ Assert.assertEquals(2, result.size());
+
+ double size = result.stream().collect(Collectors.summingDouble(a -> a.getSize()));
+ Assert.assertEquals(Geometry.TWO_PI, size, TEST_EPS);
+ }
+
+ @Test
+ public void testProject() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+ tree.insert(EQUATOR.arc(0, Geometry.PI));
+ tree.insert(X_MERIDIAN.arc(Geometry.PI, 0));
+
+ // act/assert
+ SphericalTestUtils.assertPointsEq(Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI),
+ tree.project(Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI + 0.2)), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K,
+ tree.project(Point2S.of(Geometry.MINUS_HALF_PI, 0.2)), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_I,
+ tree.project(Point2S.of(-0.5, Geometry.HALF_PI)), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_I,
+ tree.project(Point2S.of(Geometry.PI + 0.5, Geometry.HALF_PI)), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.project(tree.getBarycenter()), TEST_EPS);
+ }
+
+ @Test
+ public void testProject_noBoundaries() {
+ // act/assert
+ Assert.assertNull(RegionBSPTree2S.empty().project(Point2S.PLUS_I));
+ Assert.assertNull(RegionBSPTree2S.full().project(Point2S.PLUS_I));
+ }
+
+ @Test
+ public void testGeometricProperties_full() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.full();
+
+ // act/assert
+ Assert.assertEquals(4 * Geometry.PI, tree.getSize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+
+ Assert.assertEquals(0, tree.getBoundaries().size());
+ Assert.assertEquals(0, tree.getBoundaryPaths().size());
+ }
+
+ @Test
+ public void testGeometricProperties_empty() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+
+ // act/assert
+ Assert.assertEquals(0, tree.getSize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+
+ Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
+
+ Assert.assertEquals(0, tree.getBoundaries().size());
+ Assert.assertEquals(0, tree.getBoundaryPaths().size());
+ }
+
+ @Test
+ public void testGeometricProperties_halfSpace() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.full();
+ tree.getRoot().cut(EQUATOR);
+
+ // act/assert
+ Assert.assertEquals(Geometry.TWO_PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(Geometry.TWO_PI, tree.getBoundarySize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.getBarycenter(), TEST_EPS);
+
+ checkBarycenterConsistency(tree);
+
+ List<GreatArc> arcs = tree.getBoundaries();
+ Assert.assertEquals(1, arcs.size());
+
+ GreatArc arc = arcs.get(0);
+ Assert.assertSame(EQUATOR, arc.getCircle());
+ Assert.assertNull(arc.getStartPoint());
+ Assert.assertNull(arc.getEndPoint());
+
+ List<GreatArcPath> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ GreatArcPath path = paths.get(0);
+ Assert.assertEquals(1, path.getArcs().size());
+ Assert.assertTrue(path.getArcs().get(0).isFull());
+ }
+
+ @Test
+ public void testGeometricProperties_doubleLune() {
+ // act
+ RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
+ .append(EQUATOR.arc(0, Geometry.PI))
+ .append(X_MERIDIAN.arc(Geometry.PI, 0))
+ .append(EQUATOR.reverse().arc(0, Geometry.PI))
+ .append(X_MERIDIAN.reverse().arc(Geometry.PI, 0))
+ .build()
+ .toTree();
+
+ // assert
+ Assert.assertEquals(2 * Geometry.PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(4 * Geometry.PI, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertNull(tree.getBarycenter());
+
+ List<GreatArcPath> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(2, paths.size());
+
+ assertPath(paths.get(0), Point2S.PLUS_I, Point2S.MINUS_I, Point2S.PLUS_I);
+ assertPath(paths.get(1), Point2S.PLUS_I, Point2S.MINUS_I, Point2S.PLUS_I);
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
+ Point2S.of(0.5 * Geometry.PI, 0.25 * Geometry.PI),
+ Point2S.of(1.5 * Geometry.PI, 0.75 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
+ Point2S.of(0.5 * Geometry.PI, 0.75 * Geometry.PI),
+ Point2S.of(1.5 * Geometry.PI, 0.25 * Geometry.PI));
+ }
+
+ @Test
+ public void testGeometricProperties_quadrant() {
+ // act
+ RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
+ .appendVertices(Point2S.MINUS_K, Point2S.PLUS_I, Point2S.MINUS_J)
+ .close()
+ .toTree();
+
+ // assert
+ Assert.assertEquals(0.5 * Geometry.PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(1.5 * Geometry.PI, tree.getBoundarySize(), TEST_EPS);
+
+ Point2S center = Point2S.from(Point2S.MINUS_K.getVector()
+ .add(Point2S.PLUS_I.getVector())
+ .add(Point2S.MINUS_J.getVector()));
+ SphericalTestUtils.assertPointsEq(center, tree.getBarycenter(), TEST_EPS);
+
+ checkBarycenterConsistency(tree);
+
+ List<GreatArcPath> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ assertPath(paths.get(0), Point2S.MINUS_J, Point2S.MINUS_K, Point2S.PLUS_I, Point2S.MINUS_J);
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
+ Point2S.of(1.75 * Geometry.PI, 0.75 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
+ Point2S.PLUS_J, Point2S.PLUS_K, Point2S.MINUS_I);
+ }
+
+ @Test
+ public void testGeometricProperties_quadrant_complement() {
+ // arrange
+ RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
+ .appendVertices(Point2S.MINUS_K, Point2S.PLUS_I, Point2S.MINUS_J)
+ .close()
+ .toTree();
+
+ // act
+ tree.complement();
+
+ // assert
+ Assert.assertEquals(3.5 * Geometry.PI, tree.getSize(), TEST_EPS);
+ Assert.assertEquals(1.5 * Geometry.PI, tree.getBoundarySize(), TEST_EPS);
+
+// Point2S center = Point2S.from(Point2S.MINUS_K.getVector()
+// .add(Point2S.PLUS_I.getVector())
+// .add(Point2S.MINUS_J.getVector()));
+// SphericalTestUtils.assertPointsEq(center.antipodal(), tree.getBarycenter(), TEST_EPS);
+//
+// checkBarycenterConsistency(tree);
+
+ List<GreatArcPath> paths = tree.getBoundaryPaths();
+ Assert.assertEquals(1, paths.size());
+
+ assertPath(paths.get(0), Point2S.MINUS_J, Point2S.PLUS_I, Point2S.MINUS_K, Point2S.MINUS_J);
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
+ Point2S.of(1.75 * Geometry.PI, 0.75 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
+ Point2S.PLUS_J, Point2S.PLUS_K, Point2S.MINUS_I);
+ }
+
+ @Test
+ public void testSplit_both() {
+ // arrange
+ GreatCircle c1 = GreatCircle.fromPole(Vector3D.Unit.MINUS_X, TEST_PRECISION);
+ GreatCircle c2 = GreatCircle.fromPole(Vector3D.of(1, 1, 0), TEST_PRECISION);
+
+ RegionBSPTree2S tree = ConvexArea2S.fromBounds(c1, c2).toTree();
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(-1, 0, 1), TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree2S> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ Point2S p1 = c1.intersection(splitter);
+ Point2S p2 = splitter.intersection(c2);
+
+ RegionBSPTree2S minus = split.getMinus();
+ List<GreatArcPath> minusPaths = minus.getBoundaryPaths();
+ Assert.assertEquals(1, minusPaths.size());
+ assertPath(minusPaths.get(0), Point2S.PLUS_K, p1, p2, Point2S.PLUS_K);
+
+ RegionBSPTree2S plus = split.getPlus();
+ List<GreatArcPath> plusPaths = plus.getBoundaryPaths();
+ Assert.assertEquals(1, plusPaths.size());
+ assertPath(plusPaths.get(0), p1, Point2S.MINUS_K, p2, p1);
+
+ Assert.assertEquals(tree.getSize(), minus.getSize() + plus.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testSplit_minus() {
+ // arrange
+ RegionBSPTree2S tree = ConvexArea2S.fromVertexLoop(Arrays.asList(
+ Point2S.PLUS_I, Point2S.PLUS_K, Point2S.MINUS_J
+ ), TEST_PRECISION).toTree();
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(0, -1, 1), TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree2S> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ RegionBSPTree2S minus = split.getMinus();
+ Assert.assertNotSame(tree, minus);
+ Assert.assertEquals(tree.getSize(), minus.getSize(), TEST_EPS);
+
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_plus() {
+ // arrange
+ RegionBSPTree2S tree = ConvexArea2S.fromVertexLoop(Arrays.asList(
+ Point2S.PLUS_I, Point2S.PLUS_K, Point2S.MINUS_J
+ ), TEST_PRECISION).toTree();
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(0, 1, -1), TEST_PRECISION);
+
+ // act
+ Split<RegionBSPTree2S> split = tree.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+
+ RegionBSPTree2S plus = split.getPlus();
+ Assert.assertNotSame(tree, plus);
+ Assert.assertEquals(tree.getSize(), plus.getSize(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ Transform2S t = Transform2S.createReflection(Point2S.PLUS_J);
+ RegionBSPTree2S tree = ConvexArea2S.fromVertexLoop(
+ Arrays.asList(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K), TEST_PRECISION).toTree();
+
+ // act
+ tree.transform(t);
+
+ // assert
+ Assert.assertFalse(tree.isFull());
+ Assert.assertFalse(tree.isEmpty());
+ Assert.assertEquals(1.5 * Geometry.PI, tree.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(Geometry.HALF_PI, tree.getSize(), TEST_EPS);
+
+ Point2S expectedBarycenter = triangleBarycenter(Point2S.MINUS_J, Point2S.PLUS_I, Point2S.PLUS_K);
+ SphericalTestUtils.assertPointsEq(expectedBarycenter, tree.getBarycenter(), TEST_EPS);
+
+ checkBarycenterConsistency(tree);
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
+ Point2S.of(-0.25 * Geometry.PI, 0.25 * Geometry.PI));
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.BOUNDARY,
+ Point2S.PLUS_I, Point2S.MINUS_J, Point2S.PLUS_K,
+ Point2S.of(0, 0.25 * Geometry.PI), Point2S.of(Geometry.MINUS_HALF_PI, 0.304 * Geometry.PI),
+ Point2S.of(-0.25 * Geometry.PI, Geometry.HALF_PI));
+
+ SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
+ Point2S.PLUS_J, Point2S.MINUS_I, Point2S.MINUS_K);
+ }
+
+ @Test
+ public void testRegionNode_getNodeRegion() {
+ // arrange
+ RegionBSPTree2S tree = RegionBSPTree2S.empty();
+
+ RegionNode2S root = tree.getRoot();
+ RegionNode2S minus = root.cut(EQUATOR).getMinus();
+ RegionNode2S minusPlus = minus.cut(X_MERIDIAN).getPlus();
+
+ // act/assert
+ ConvexArea2S rootRegion = root.getNodeRegion();
+ Assert.assertEquals(4 * Geometry.PI, rootRegion.getSize(), TEST_EPS);
+ Assert.assertNull(rootRegion.getBarycenter());
+
+ ConvexArea2S minusRegion = minus.getNodeRegion();
+ Assert.assertEquals(2 * Geometry.PI, minusRegion.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, minusRegion.getBarycenter(), TEST_EPS);
+
+ ConvexArea2S minusPlusRegion = minusPlus.getNodeRegion();
+ Assert.assertEquals(Geometry.PI, minusPlusRegion.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(1.5 * Geometry.PI, 0.25 * Geometry.PI),
+ minusPlusRegion.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testGeographicMap() {
+ // arrange
+ RegionBSPTree2S continental = latLongToTree(new double[][] {
+ { 51.14850, 2.51357 }, { 50.94660, 1.63900 }, { 50.12717, 1.33876 }, { 49.34737, -0.98946 },
+ { 49.77634, -1.93349 }, { 48.64442, -1.61651 }, { 48.90169, -3.29581 }, { 48.68416, -4.59234 },
+ { 47.95495, -4.49155 }, { 47.57032, -2.96327 }, { 46.01491, -1.19379 }, { 44.02261, -1.38422 },
+ { 43.42280, -1.90135 }, { 43.03401, -1.50277 }, { 42.34338, 1.82679 }, { 42.47301, 2.98599 },
+ { 43.07520, 3.10041 }, { 43.39965, 4.55696 }, { 43.12889, 6.52924 }, { 43.69384, 7.43518 },
+ { 44.12790, 7.54959 }, { 45.02851, 6.74995 }, { 45.33309, 7.09665 }, { 46.42967, 6.50009 },
+ { 46.27298, 6.02260 }, { 46.72577, 6.03738 }, { 47.62058, 7.46675 }, { 49.01778, 8.09927 },
+ { 49.20195, 6.65822 }, { 49.44266, 5.89775 }, { 49.98537, 4.79922 }
+ });
+ RegionBSPTree2S corsica = latLongToTree(new double[][] {
+ { 42.15249, 9.56001 }, { 43.00998, 9.39000 }, { 42.62812, 8.74600 }, { 42.25651, 8.54421 },
+ { 41.58361, 8.77572 }, { 41.38000, 9.22975 }
+ });
+
+ // act
+ RegionBSPTree2S france = RegionBSPTree2S.empty();
+ france.union(continental, corsica);
+
+ // assert
+ Assert.assertEquals(0.6316801448267251, france.getBoundarySize(), TEST_EPS);
+ Assert.assertEquals(0.013964220234478741, france.getSize(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.of(0.04368552749392928, 0.7590839905197961),
+ france.getBarycenter(), BARYCENTER_EPS);
+
+ checkBarycenterConsistency(france);
+ }
+
+ /**
+ * Insert convex subhyperplanes defining the positive quadrant area.
+ * @param tree
+ */
+ private static void insertPositiveQuadrant(RegionBSPTree2S tree) {
+ tree.insert(Arrays.asList(
+ EQUATOR.arc(Point2S.PLUS_I, Point2S.PLUS_J),
+ X_MERIDIAN.arc(Point2S.PLUS_K, Point2S.PLUS_I),
+ Y_MERIDIAN.arc(Point2S.PLUS_J, Point2S.PLUS_K)
+ ));
+ }
+
+ private static Point2S triangleBarycenter(Point2S p1, Point2S p2, Point2S p3) {
+ // compute the barycenter using intersection mid point arcs
+ GreatCircle c1 = GreatCircle.fromPoints(p1, p2.slerp(p3, 0.5), TEST_PRECISION);
+ GreatCircle c2 = GreatCircle.fromPoints(p2, p1.slerp(p3, 0.5), TEST_PRECISION);
+
+ return c1.intersection(c2);
+ }
+
+ private static void assertPath(GreatArcPath path, Point2S ... vertices) {
+ List<Point2S> expected = Arrays.asList(vertices);
+ List<Point2S> actual = path.getVertices();
+
+ if (expected.size() != actual.size()) {
+ Assert.fail("Unexpected path size. Expected path " + expected +
+ " but was " + actual);
+ }
+
+ for (int i = 0; i < expected.size(); ++i) {
+ if (!expected.get(i).eq(actual.get(i), TEST_PRECISION)) {
+ Assert.fail("Unexpected path vertex at index " + i + ". Expected path " + expected +
+ " but was " + actual);
+ }
+ }
+ }
+
+ private static RegionBSPTree2S latLongToTree(double[][] points) {
+ GreatArcPath.Builder pathBuilder = GreatArcPath.builder(TEST_PRECISION);
+
+ for (int i = 0; i < points.length; ++i) {
+ pathBuilder.append(latLongToPoint(points[i][0], points[i][1]));
+ }
+
+ return pathBuilder.close().toTree();
+ }
+
+ private static Point2S latLongToPoint(double latitude, double longitude) {
+ return Point2S.of(Math.toRadians(longitude), Math.toRadians(90.0 - latitude));
+ }
+
+ private static void checkBarycenterConsistency(RegionBSPTree2S region) {
+ Point2S barycenter = region.getBarycenter();
+ double size = region.getSize();
+
+ SphericalTestUtils.checkClassify(region, RegionLocation.INSIDE, barycenter);
+
+ GreatCircle circle = GreatCircle.fromPole(barycenter.getVector(), TEST_PRECISION);
+ for (double az = 0; az <= Geometry.TWO_PI; az += 0.2) {
+ Point2S pt = circle.toSpace(Point1S.of(az));
+ GreatCircle splitter = GreatCircle.fromPoints(barycenter, pt, TEST_PRECISION);
+
+ Split<RegionBSPTree2S> split = region.split(splitter);
+
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ RegionBSPTree2S minus = split.getMinus();
+ double minusSize = minus.getSize();
+ Point2S minusBc = minus.getBarycenter();
+
+ Vector3D weightedMinus = minusBc.getVector()
+ .multiply(minus.getSize());
+
+ RegionBSPTree2S plus = split.getPlus();
+ double plusSize = plus.getSize();
+ Point2S plusBc = plus.getBarycenter();
+
+ Vector3D weightedPlus = plusBc.getVector()
+ .multiply(plus.getSize());
+ Point2S computedBarycenter = Point2S.from(weightedMinus.add(weightedPlus));
+
+ Assert.assertEquals(size, minusSize + plusSize, TEST_EPS);
+ SphericalTestUtils.assertPointsEq(barycenter, computedBarycenter, BARYCENTER_EPS);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/S2PointTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/S2PointTest.java
deleted file mode 100644
index 827f107..0000000
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/S2PointTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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 org.apache.commons.geometry.core.Geometry;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class S2PointTest {
-
- private static final double EPS = 1e-10;
-
- @Test
- public void testS2Point() {
- for (int k = -2; k < 3; ++k) {
- S2Point p = S2Point.of(1.0 + k * Geometry.TWO_PI, 1.4);
- Assert.assertEquals(1.0, p.getAzimuth(), EPS);
- Assert.assertEquals(1.4, p.getPolar(), EPS);
- Assert.assertEquals(Math.cos(1.0) * Math.sin(1.4), p.getVector().getX(), EPS);
- Assert.assertEquals(Math.sin(1.0) * Math.sin(1.4), p.getVector().getY(), EPS);
- Assert.assertEquals(Math.cos(1.4), p.getVector().getZ(), EPS);
- Assert.assertFalse(p.isNaN());
- }
- }
-
- @Test
- public void testNaN() {
- Assert.assertTrue(S2Point.NaN.isNaN());
- Assert.assertTrue(S2Point.NaN.equals(S2Point.of(Double.NaN, 1.0)));
- Assert.assertFalse(S2Point.of(1.0, 1.3).equals(S2Point.NaN));
- }
-
- @Test
- public void testEquals() {
- S2Point a = S2Point.of(1.0, 1.0);
- S2Point b = S2Point.of(1.0, 1.0);
- Assert.assertEquals(a.hashCode(), b.hashCode());
- Assert.assertFalse(a == b);
- Assert.assertTrue(a.equals(b));
- Assert.assertTrue(a.equals(a));
- Assert.assertFalse(a.equals('a'));
- }
-
- @Test
- public void testDistance() {
- S2Point a = S2Point.of(1.0, 0.5 * Math.PI);
- S2Point b = S2Point.of(a.getAzimuth() + 0.5 * Math.PI, a.getPolar());
- Assert.assertEquals(0.5 * Math.PI, a.distance(b), 1.0e-10);
- Assert.assertEquals(Math.PI, a.distance(a.negate()), 1.0e-10);
- Assert.assertEquals(0.5 * Math.PI, S2Point.MINUS_I.distance(S2Point.MINUS_K), 1.0e-10);
- Assert.assertEquals(0.0, S2Point.of(1.0, 0).distance(S2Point.of(2.0, 0)), 1.0e-10);
- }
-
- @Test
- public void testToString() {
- // act/assert
- Assert.assertEquals("(0.0, 0.0)", S2Point.of(0.0, 0.0).toString());
- Assert.assertEquals("(1.0, 2.0)", S2Point.of(1.0, 2.0).toString());
- }
-
- @Test
- public void testParse() {
- // act/assert
- checkPoint(S2Point.parse("(0,0)"), 0.0, 0.0);
- checkPoint(S2Point.parse("(1,2)"), 1.0, 2.0);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testParse_failure() {
- // act/assert
- S2Point.parse("abc");
- }
-
- private void checkPoint(S2Point p, double theta, double phi) {
- Assert.assertEquals(theta, p.getAzimuth(), EPS);
- Assert.assertEquals(phi, p.getPolar(), EPS);
- }
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSetTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSetTest.java
deleted file mode 100644
index c711051..0000000
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSetTest.java
+++ /dev/null
@@ -1,570 +0,0 @@
-/*
- * 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.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.enclosing.EnclosingBall;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.spherical.oned.ArcsSet;
-import org.apache.commons.geometry.spherical.oned.S1Point;
-import org.apache.commons.rng.sampling.UnitSphereSampler;
-import org.apache.commons.rng.simple.RandomSource;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class SphericalPolygonsSetTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testFullSphere() {
- SphericalPolygonsSet full = new SphericalPolygonsSet(TEST_PRECISION);
- UnitSphereSampler random =
- new UnitSphereSampler(3, RandomSource.create(RandomSource.WELL_1024_A,
- 0x852fd2a0ed8d2f6dl));
- for (int i = 0; i < 1000; ++i) {
- Vector3D v = Vector3D.of(random.nextVector());
- Assert.assertEquals(Location.INSIDE, full.checkPoint(S2Point.ofVector(v)));
- }
- Assert.assertEquals(4 * Math.PI, new SphericalPolygonsSet(createPrecision(0.01), new S2Point[0]).getSize(), TEST_EPS);
- Assert.assertEquals(0, new SphericalPolygonsSet(createPrecision(0.01), new S2Point[0]).getBoundarySize(), TEST_EPS);
- Assert.assertEquals(0, full.getBoundaryLoops().size());
- Assert.assertTrue(full.getEnclosingCap().getRadius() > 0);
- Assert.assertTrue(Double.isInfinite(full.getEnclosingCap().getRadius()));
- }
-
- @Test
- public void testEmpty() {
- SphericalPolygonsSet empty =
- (SphericalPolygonsSet) new RegionFactory<S2Point>().getComplement(new SphericalPolygonsSet(TEST_PRECISION));
- UnitSphereSampler random =
- new UnitSphereSampler(3, RandomSource.create(RandomSource.WELL_1024_A,
- 0x76d9205d6167b6ddl));
- for (int i = 0; i < 1000; ++i) {
- Vector3D v = Vector3D.of(random.nextVector());
- Assert.assertEquals(Location.OUTSIDE, empty.checkPoint(S2Point.ofVector(v)));
- }
- Assert.assertEquals(0, empty.getSize(), TEST_EPS);
- Assert.assertEquals(0, empty.getBoundarySize(), TEST_EPS);
- Assert.assertEquals(0, empty.getBoundaryLoops().size());
- Assert.assertTrue(empty.getEnclosingCap().getRadius() < 0);
- Assert.assertTrue(Double.isInfinite(empty.getEnclosingCap().getRadius()));
- }
-
- @Test
- public void testSouthHemisphere() {
- double tol = 0.01;
- double sinTol = Math.sin(tol);
- SphericalPolygonsSet south = new SphericalPolygonsSet(Vector3D.Unit.MINUS_Z, createPrecision(tol));
- UnitSphereSampler random =
- new UnitSphereSampler(3, RandomSource.create(RandomSource.WELL_1024_A,
- 0x6b9d4a6ad90d7b0bl));
- for (int i = 0; i < 1000; ++i) {
- Vector3D v = Vector3D.of(random.nextVector());
- if (v.getZ() < -sinTol) {
- Assert.assertEquals(Location.INSIDE, south.checkPoint(S2Point.ofVector(v)));
- } else if (v.getZ() > sinTol) {
- Assert.assertEquals(Location.OUTSIDE, south.checkPoint(S2Point.ofVector(v)));
- } else {
- Assert.assertEquals(Location.BOUNDARY, south.checkPoint(S2Point.ofVector(v)));
- }
- }
- Assert.assertEquals(1, south.getBoundaryLoops().size());
-
- EnclosingBall<S2Point> southCap = south.getEnclosingCap();
- Assert.assertEquals(0.0, S2Point.MINUS_K.distance(southCap.getCenter()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, southCap.getRadius(), TEST_EPS);
-
- EnclosingBall<S2Point> northCap =
- ((SphericalPolygonsSet) new RegionFactory<S2Point>().getComplement(south)).getEnclosingCap();
- Assert.assertEquals(0.0, S2Point.PLUS_K.distance(northCap.getCenter()), TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, northCap.getRadius(), TEST_EPS);
-
- }
-
- @Test
- public void testPositiveOctantByIntersection() {
- double tol = 0.01;
- double sinTol = Math.sin(tol);
- DoublePrecisionContext precision = createPrecision(tol);
- RegionFactory<S2Point> factory = new RegionFactory<>();
- SphericalPolygonsSet plusX = new SphericalPolygonsSet(Vector3D.Unit.PLUS_X, precision);
- SphericalPolygonsSet plusY = new SphericalPolygonsSet(Vector3D.Unit.PLUS_Y, precision);
- SphericalPolygonsSet plusZ = new SphericalPolygonsSet(Vector3D.Unit.PLUS_Z, precision);
- SphericalPolygonsSet octant =
- (SphericalPolygonsSet) factory.intersection(factory.intersection(plusX, plusY), plusZ);
- UnitSphereSampler random =
- new UnitSphereSampler(3, RandomSource.create(RandomSource.WELL_1024_A,
- 0x9c9802fde3cbcf25l));
- for (int i = 0; i < 1000; ++i) {
- Vector3D v = Vector3D.of(random.nextVector());
- if ((v.getX() > sinTol) && (v.getY() > sinTol) && (v.getZ() > sinTol)) {
- Assert.assertEquals(Location.INSIDE, octant.checkPoint(S2Point.ofVector(v)));
- } else if ((v.getX() < -sinTol) || (v.getY() < -sinTol) || (v.getZ() < -sinTol)) {
- Assert.assertEquals(Location.OUTSIDE, octant.checkPoint(S2Point.ofVector(v)));
- } else {
- Assert.assertEquals(Location.BOUNDARY, octant.checkPoint(S2Point.ofVector(v)));
- }
- }
-
- List<Vertex> loops = octant.getBoundaryLoops();
- Assert.assertEquals(1, loops.size());
- boolean xPFound = false;
- boolean yPFound = false;
- boolean zPFound = false;
- boolean xVFound = false;
- boolean yVFound = false;
- boolean zVFound = false;
- Vertex first = loops.get(0);
- int count = 0;
- for (Vertex v = first; count == 0 || v != first; v = v.getOutgoing().getEnd()) {
- ++count;
- Edge e = v.getIncoming();
- Assert.assertTrue(v == e.getStart().getOutgoing().getEnd());
- xPFound = xPFound || e.getCircle().getPole().distance(Vector3D.Unit.PLUS_X) < TEST_EPS;
- yPFound = yPFound || e.getCircle().getPole().distance(Vector3D.Unit.PLUS_Y) < TEST_EPS;
- zPFound = zPFound || e.getCircle().getPole().distance(Vector3D.Unit.PLUS_Z) < TEST_EPS;
- Assert.assertEquals(0.5 * Math.PI, e.getLength(), TEST_EPS);
- xVFound = xVFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_X) < TEST_EPS;
- yVFound = yVFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_Y) < TEST_EPS;
- zVFound = zVFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_Z) < TEST_EPS;
- }
- Assert.assertTrue(xPFound);
- Assert.assertTrue(yPFound);
- Assert.assertTrue(zPFound);
- Assert.assertTrue(xVFound);
- Assert.assertTrue(yVFound);
- Assert.assertTrue(zVFound);
- Assert.assertEquals(3, count);
-
- Assert.assertEquals(0.0,
- octant.getBarycenter().distance(S2Point.ofVector(Vector3D.of(1, 1, 1))),
- TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, octant.getSize(), TEST_EPS);
-
- EnclosingBall<S2Point> cap = octant.getEnclosingCap();
- Assert.assertEquals(0.0, octant.getBarycenter().distance(cap.getCenter()), TEST_EPS);
- Assert.assertEquals(Math.acos(1.0 / Math.sqrt(3)), cap.getRadius(), TEST_EPS);
-
- EnclosingBall<S2Point> reversedCap =
- ((SphericalPolygonsSet) factory.getComplement(octant)).getEnclosingCap();
- Assert.assertEquals(0, reversedCap.getCenter().distance(S2Point.ofVector(Vector3D.of(-1, -1, -1))), TEST_EPS);
- Assert.assertEquals(Math.PI - Math.asin(1.0 / Math.sqrt(3)), reversedCap.getRadius(), TEST_EPS);
-
- }
-
- @Test
- public void testPositiveOctantByVertices() {
- double tol = 0.01;
- double sinTol = Math.sin(tol);
- SphericalPolygonsSet octant = new SphericalPolygonsSet(createPrecision(tol), S2Point.PLUS_I, S2Point.PLUS_J, S2Point.PLUS_K);
- UnitSphereSampler random =
- new UnitSphereSampler(3, RandomSource.create(RandomSource.WELL_1024_A,
- 0xb8fc5acc91044308l));
- for (int i = 0; i < 1000; ++i) {
- Vector3D v = Vector3D.of(random.nextVector());
- if ((v.getX() > sinTol) && (v.getY() > sinTol) && (v.getZ() > sinTol)) {
- Assert.assertEquals(Location.INSIDE, octant.checkPoint(S2Point.ofVector(v)));
- } else if ((v.getX() < -sinTol) || (v.getY() < -sinTol) || (v.getZ() < -sinTol)) {
- Assert.assertEquals(Location.OUTSIDE, octant.checkPoint(S2Point.ofVector(v)));
- } else {
- Assert.assertEquals(Location.BOUNDARY, octant.checkPoint(S2Point.ofVector(v)));
- }
- }
- }
-
- @Test
- public void testNonConvex() {
- double tol = 0.01;
- double sinTol = Math.sin(tol);
- DoublePrecisionContext precision = createPrecision(tol);
- RegionFactory<S2Point> factory = new RegionFactory<>();
- SphericalPolygonsSet plusX = new SphericalPolygonsSet(Vector3D.Unit.PLUS_X, precision);
- SphericalPolygonsSet plusY = new SphericalPolygonsSet(Vector3D.Unit.PLUS_Y, precision);
- SphericalPolygonsSet plusZ = new SphericalPolygonsSet(Vector3D.Unit.PLUS_Z, precision);
- SphericalPolygonsSet threeOctants =
- (SphericalPolygonsSet) factory.difference(plusZ, factory.intersection(plusX, plusY));
-
- UnitSphereSampler random =
- new UnitSphereSampler(3, RandomSource.create(RandomSource.WELL_1024_A,
- 0x9c9802fde3cbcf25l));
- for (int i = 0; i < 1000; ++i) {
- Vector3D v = Vector3D.of(random.nextVector());
- if (((v.getX() < -sinTol) || (v.getY() < -sinTol)) && (v.getZ() > sinTol)) {
- Assert.assertEquals(Location.INSIDE, threeOctants.checkPoint(S2Point.ofVector(v)));
- } else if (((v.getX() > sinTol) && (v.getY() > sinTol)) || (v.getZ() < -sinTol)) {
- Assert.assertEquals(Location.OUTSIDE, threeOctants.checkPoint(S2Point.ofVector(v)));
- } else {
- Assert.assertEquals(Location.BOUNDARY, threeOctants.checkPoint(S2Point.ofVector(v)));
- }
- }
-
- List<Vertex> loops = threeOctants.getBoundaryLoops();
- Assert.assertEquals(1, loops.size());
- boolean xPFound = false;
- boolean yPFound = false;
- boolean zPFound = false;
- boolean xVFound = false;
- boolean yVFound = false;
- boolean zVFound = false;
- Vertex first = loops.get(0);
- int count = 0;
- double sumPoleX = 0;
- double sumPoleY = 0;
- double sumPoleZ = 0;
- for (Vertex v = first; count == 0 || v != first; v = v.getOutgoing().getEnd()) {
- ++count;
- Edge e = v.getIncoming();
- Assert.assertTrue(v == e.getStart().getOutgoing().getEnd());
- if (e.getCircle().getPole().distance(Vector3D.Unit.MINUS_X) < TEST_EPS) {
- xPFound = true;
- sumPoleX += e.getLength();
- } else if (e.getCircle().getPole().distance(Vector3D.Unit.MINUS_Y) < TEST_EPS) {
- yPFound = true;
- sumPoleY += e.getLength();
- } else {
- Assert.assertEquals(0.0, e.getCircle().getPole().distance(Vector3D.Unit.PLUS_Z), TEST_EPS);
- zPFound = true;
- sumPoleZ += e.getLength();
- }
- xVFound = xVFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_X) < TEST_EPS;
- yVFound = yVFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_Y) < TEST_EPS;
- zVFound = zVFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_Z) < TEST_EPS;
- }
- Assert.assertTrue(xPFound);
- Assert.assertTrue(yPFound);
- Assert.assertTrue(zPFound);
- Assert.assertTrue(xVFound);
- Assert.assertTrue(yVFound);
- Assert.assertTrue(zVFound);
- Assert.assertEquals(0.5 * Math.PI, sumPoleX, TEST_EPS);
- Assert.assertEquals(0.5 * Math.PI, sumPoleY, TEST_EPS);
- Assert.assertEquals(1.5 * Math.PI, sumPoleZ, TEST_EPS);
-
- Assert.assertEquals(1.5 * Math.PI, threeOctants.getSize(), TEST_EPS);
-
- }
-
- @Test
- public void testModeratlyComplexShape() {
- double tol = 0.01;
- DoublePrecisionContext precision = createPrecision(tol);
- List<SubHyperplane<S2Point>> boundary = new ArrayList<>();
- boundary.add(create(Vector3D.Unit.MINUS_Y, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_Y, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.MINUS_Y, Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_Z, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_Z, Vector3D.Unit.MINUS_Y, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_Y, Vector3D.Unit.PLUS_X, precision, 0.0, 0.5 * Math.PI));
- SphericalPolygonsSet polygon = new SphericalPolygonsSet(boundary, precision);
-
- Assert.assertEquals(Location.OUTSIDE, polygon.checkPoint(S2Point.ofVector(Vector3D.of( 1, 1, 1).normalize())));
- Assert.assertEquals(Location.INSIDE, polygon.checkPoint(S2Point.ofVector(Vector3D.of(-1, 1, 1).normalize())));
- Assert.assertEquals(Location.INSIDE, polygon.checkPoint(S2Point.ofVector(Vector3D.of(-1, -1, 1).normalize())));
- Assert.assertEquals(Location.INSIDE, polygon.checkPoint(S2Point.ofVector(Vector3D.of( 1, -1, 1).normalize())));
- Assert.assertEquals(Location.OUTSIDE, polygon.checkPoint(S2Point.ofVector(Vector3D.of( 1, 1, -1).normalize())));
- Assert.assertEquals(Location.OUTSIDE, polygon.checkPoint(S2Point.ofVector(Vector3D.of(-1, 1, -1).normalize())));
- Assert.assertEquals(Location.INSIDE, polygon.checkPoint(S2Point.ofVector(Vector3D.of(-1, -1, -1).normalize())));
- Assert.assertEquals(Location.OUTSIDE, polygon.checkPoint(S2Point.ofVector(Vector3D.of( 1, -1, -1).normalize())));
-
- Assert.assertEquals(Geometry.TWO_PI, polygon.getSize(), TEST_EPS);
- Assert.assertEquals(3 * Math.PI, polygon.getBoundarySize(), TEST_EPS);
-
- List<Vertex> loops = polygon.getBoundaryLoops();
- Assert.assertEquals(1, loops.size());
- boolean pXFound = false;
- boolean mXFound = false;
- boolean pYFound = false;
- boolean mYFound = false;
- boolean pZFound = false;
- boolean mZFound = false;
- Vertex first = loops.get(0);
- int count = 0;
- for (Vertex v = first; count == 0 || v != first; v = v.getOutgoing().getEnd()) {
- ++count;
- Edge e = v.getIncoming();
- Assert.assertTrue(v == e.getStart().getOutgoing().getEnd());
- pXFound = pXFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_X) < TEST_EPS;
- mXFound = mXFound || v.getLocation().getVector().distance(Vector3D.Unit.MINUS_X) < TEST_EPS;
- pYFound = pYFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_Y) < TEST_EPS;
- mYFound = mYFound || v.getLocation().getVector().distance(Vector3D.Unit.MINUS_Y) < TEST_EPS;
- pZFound = pZFound || v.getLocation().getVector().distance(Vector3D.Unit.PLUS_Z) < TEST_EPS;
- mZFound = mZFound || v.getLocation().getVector().distance(Vector3D.Unit.MINUS_Z) < TEST_EPS;
- Assert.assertEquals(0.5 * Math.PI, e.getLength(), TEST_EPS);
- }
- Assert.assertTrue(pXFound);
- Assert.assertTrue(mXFound);
- Assert.assertTrue(pYFound);
- Assert.assertTrue(mYFound);
- Assert.assertTrue(pZFound);
- Assert.assertTrue(mZFound);
- Assert.assertEquals(6, count);
-
- }
-
- @Test
- public void testSeveralParts() {
- double tol = 0.01;
- double sinTol = Math.sin(tol);
- DoublePrecisionContext precision = createPrecision(tol);
- List<SubHyperplane<S2Point>> boundary = new ArrayList<>();
-
- // first part: +X, +Y, +Z octant
- boundary.add(create(Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z, precision, 0.0, 0.5 * Math.PI));
-
- // first part: -X, -Y, -Z octant
- boundary.add(create(Vector3D.Unit.MINUS_Y, Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_Z, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_Z, Vector3D.Unit.MINUS_Y, precision, 0.0, 0.5 * Math.PI));
- boundary.add(create(Vector3D.Unit.MINUS_Z, Vector3D.Unit.MINUS_Y, Vector3D.Unit.MINUS_X, precision, 0.0, 0.5 * Math.PI));
-
- SphericalPolygonsSet polygon = new SphericalPolygonsSet(boundary, precision);
-
- UnitSphereSampler random =
- new UnitSphereSampler(3, RandomSource.create(RandomSource.WELL_1024_A,
- 0xcc5ce49949e0d3ecl));
- for (int i = 0; i < 1000; ++i) {
- Vector3D v = Vector3D.of(random.nextVector());
- if ((v.getX() < -sinTol) && (v.getY() < -sinTol) && (v.getZ() < -sinTol)) {
- Assert.assertEquals(Location.INSIDE, polygon.checkPoint(S2Point.ofVector(v)));
- } else if ((v.getX() < sinTol) && (v.getY() < sinTol) && (v.getZ() < sinTol)) {
- Assert.assertEquals(Location.BOUNDARY, polygon.checkPoint(S2Point.ofVector(v)));
- } else if ((v.getX() > sinTol) && (v.getY() > sinTol) && (v.getZ() > sinTol)) {
- Assert.assertEquals(Location.INSIDE, polygon.checkPoint(S2Point.ofVector(v)));
- } else if ((v.getX() > -sinTol) && (v.getY() > -sinTol) && (v.getZ() > -sinTol)) {
- Assert.assertEquals(Location.BOUNDARY, polygon.checkPoint(S2Point.ofVector(v)));
- } else {
- Assert.assertEquals(Location.OUTSIDE, polygon.checkPoint(S2Point.ofVector(v)));
- }
- }
-
- Assert.assertEquals(Math.PI, polygon.getSize(), TEST_EPS);
- Assert.assertEquals(3 * Math.PI, polygon.getBoundarySize(), TEST_EPS);
-
- // there should be two separate boundary loops
- Assert.assertEquals(2, polygon.getBoundaryLoops().size());
-
- }
-
- @Test
- public void testPartWithHole() {
- double tol = 0.01;
- double alpha = 0.7;
- DoublePrecisionContext precision = createPrecision(tol);
- S2Point center = S2Point.ofVector(Vector3D.of(1, 1, 1));
- SphericalPolygonsSet hexa = new SphericalPolygonsSet(center.getVector(), Vector3D.Unit.PLUS_Z, alpha, 6, precision);
- SphericalPolygonsSet hole = new SphericalPolygonsSet(precision,
- S2Point.of(Math.PI / 6, Math.PI / 3),
- S2Point.of(Math.PI / 3, Math.PI / 3),
- S2Point.of(Math.PI / 4, Math.PI / 6));
- SphericalPolygonsSet hexaWithHole =
- (SphericalPolygonsSet) new RegionFactory<S2Point>().difference(hexa, hole);
-
- for (double phi = center.getPolar() - alpha + 0.1; phi < center.getPolar() + alpha - 0.1; phi += 0.07) {
- Location l = hexaWithHole.checkPoint(S2Point.of(Math.PI / 4, phi));
- if (phi < Math.PI / 6 || phi > Math.PI / 3) {
- Assert.assertEquals(Location.INSIDE, l);
- } else {
- Assert.assertEquals(Location.OUTSIDE, l);
- }
- }
-
- // there should be two separate boundary loops
- Assert.assertEquals(2, hexaWithHole.getBoundaryLoops().size());
-
- Assert.assertEquals(hexa.getBoundarySize() + hole.getBoundarySize(), hexaWithHole.getBoundarySize(), TEST_EPS);
- Assert.assertEquals(hexa.getSize() - hole.getSize(), hexaWithHole.getSize(), TEST_EPS);
-
- }
-
- @Test
- public void testConcentricSubParts() {
- double tol = 0.001;
- DoublePrecisionContext precision = createPrecision(tol);
- Vector3D center = Vector3D.of(1, 1, 1);
- SphericalPolygonsSet hexaOut = new SphericalPolygonsSet(center, Vector3D.Unit.PLUS_Z, 0.9, 6, precision);
- SphericalPolygonsSet hexaIn = new SphericalPolygonsSet(center, Vector3D.Unit.PLUS_Z, 0.8, 6, precision);
- SphericalPolygonsSet pentaOut = new SphericalPolygonsSet(center, Vector3D.Unit.PLUS_Z, 0.7, 5, precision);
- SphericalPolygonsSet pentaIn = new SphericalPolygonsSet(center, Vector3D.Unit.PLUS_Z, 0.6, 5, precision);
- SphericalPolygonsSet quadriOut = new SphericalPolygonsSet(center, Vector3D.Unit.PLUS_Z, 0.5, 4, precision);
- SphericalPolygonsSet quadriIn = new SphericalPolygonsSet(center, Vector3D.Unit.PLUS_Z, 0.4, 4, precision);
- SphericalPolygonsSet triOut = new SphericalPolygonsSet(center, Vector3D.Unit.PLUS_Z, 0.25, 3, precision);
- SphericalPolygonsSet triIn = new SphericalPolygonsSet(center, Vector3D.Unit.PLUS_Z, 0.15, 3, precision);
-
- RegionFactory<S2Point> factory = new RegionFactory<>();
- SphericalPolygonsSet hexa = (SphericalPolygonsSet) factory.difference(hexaOut, hexaIn);
- SphericalPolygonsSet penta = (SphericalPolygonsSet) factory.difference(pentaOut, pentaIn);
- SphericalPolygonsSet quadri = (SphericalPolygonsSet) factory.difference(quadriOut, quadriIn);
- SphericalPolygonsSet tri = (SphericalPolygonsSet) factory.difference(triOut, triIn);
- SphericalPolygonsSet concentric =
- (SphericalPolygonsSet) factory.union(factory.union(hexa, penta), factory.union(quadri, tri));
-
- // there should be two separate boundary loops
- Assert.assertEquals(8, concentric.getBoundaryLoops().size());
-
- Assert.assertEquals(hexaOut.getBoundarySize() + hexaIn.getBoundarySize() +
- pentaOut.getBoundarySize() + pentaIn.getBoundarySize() +
- quadriOut.getBoundarySize() + quadriIn.getBoundarySize() +
- triOut.getBoundarySize() + triIn.getBoundarySize(),
- concentric.getBoundarySize(), TEST_EPS);
- Assert.assertEquals(hexaOut.getSize() - hexaIn.getSize() +
- pentaOut.getSize() - pentaIn.getSize() +
- quadriOut.getSize() - quadriIn.getSize() +
- triOut.getSize() - triIn.getSize(),
- concentric.getSize(), TEST_EPS);
-
- // we expect lots of sign changes as we traverse all concentric rings
- double phi = S2Point.ofVector(center).getPolar();
- Assert.assertEquals(+0.207, concentric.projectToBoundary(S2Point.of(-0.60, phi)).getOffset(), 0.01);
- Assert.assertEquals(-0.048, concentric.projectToBoundary(S2Point.of(-0.21, phi)).getOffset(), 0.01);
- Assert.assertEquals(+0.027, concentric.projectToBoundary(S2Point.of(-0.10, phi)).getOffset(), 0.01);
- Assert.assertEquals(-0.041, concentric.projectToBoundary(S2Point.of( 0.01, phi)).getOffset(), 0.01);
- Assert.assertEquals(+0.049, concentric.projectToBoundary(S2Point.of( 0.16, phi)).getOffset(), 0.01);
- Assert.assertEquals(-0.038, concentric.projectToBoundary(S2Point.of( 0.29, phi)).getOffset(), 0.01);
- Assert.assertEquals(+0.097, concentric.projectToBoundary(S2Point.of( 0.48, phi)).getOffset(), 0.01);
- Assert.assertEquals(-0.022, concentric.projectToBoundary(S2Point.of( 0.64, phi)).getOffset(), 0.01);
- Assert.assertEquals(+0.072, concentric.projectToBoundary(S2Point.of( 0.79, phi)).getOffset(), 0.01);
- Assert.assertEquals(-0.022, concentric.projectToBoundary(S2Point.of( 0.93, phi)).getOffset(), 0.01);
- Assert.assertEquals(+0.091, concentric.projectToBoundary(S2Point.of( 1.08, phi)).getOffset(), 0.01);
- Assert.assertEquals(-0.037, concentric.projectToBoundary(S2Point.of( 1.28, phi)).getOffset(), 0.01);
- Assert.assertEquals(+0.051, concentric.projectToBoundary(S2Point.of( 1.40, phi)).getOffset(), 0.01);
- Assert.assertEquals(-0.041, concentric.projectToBoundary(S2Point.of( 1.55, phi)).getOffset(), 0.01);
- Assert.assertEquals(+0.027, concentric.projectToBoundary(S2Point.of( 1.67, phi)).getOffset(), 0.01);
- Assert.assertEquals(-0.044, concentric.projectToBoundary(S2Point.of( 1.79, phi)).getOffset(), 0.01);
- Assert.assertEquals(+0.201, concentric.projectToBoundary(S2Point.of( 2.16, phi)).getOffset(), 0.01);
-
- }
-
- @Test
- public void testGeographicalMap() {
-
- SphericalPolygonsSet continental = buildSimpleZone(new double[][] {
- { 51.14850, 2.51357 }, { 50.94660, 1.63900 }, { 50.12717, 1.33876 }, { 49.34737, -0.98946 },
- { 49.77634, -1.93349 }, { 48.64442, -1.61651 }, { 48.90169, -3.29581 }, { 48.68416, -4.59234 },
- { 47.95495, -4.49155 }, { 47.57032, -2.96327 }, { 46.01491, -1.19379 }, { 44.02261, -1.38422 },
- { 43.42280, -1.90135 }, { 43.03401, -1.50277 }, { 42.34338, 1.82679 }, { 42.47301, 2.98599 },
- { 43.07520, 3.10041 }, { 43.39965, 4.55696 }, { 43.12889, 6.52924 }, { 43.69384, 7.43518 },
- { 44.12790, 7.54959 }, { 45.02851, 6.74995 }, { 45.33309, 7.09665 }, { 46.42967, 6.50009 },
- { 46.27298, 6.02260 }, { 46.72577, 6.03738 }, { 47.62058, 7.46675 }, { 49.01778, 8.09927 },
- { 49.20195, 6.65822 }, { 49.44266, 5.89775 }, { 49.98537, 4.79922 }
- });
- SphericalPolygonsSet corsica = buildSimpleZone(new double[][] {
- { 42.15249, 9.56001 }, { 43.00998, 9.39000 }, { 42.62812, 8.74600 }, { 42.25651, 8.54421 },
- { 41.58361, 8.77572 }, { 41.38000, 9.22975 }
- });
- RegionFactory<S2Point> factory = new RegionFactory<>();
- SphericalPolygonsSet zone = (SphericalPolygonsSet) factory.union(continental, corsica);
- EnclosingBall<S2Point> enclosing = zone.getEnclosingCap();
- Vector3D enclosingCenter = enclosing.getCenter().getVector();
-
- double step = Math.toRadians(0.1);
- for (Vertex loopStart : zone.getBoundaryLoops()) {
- int count = 0;
- for (Vertex v = loopStart; count == 0 || v != loopStart; v = v.getOutgoing().getEnd()) {
- ++count;
- for (int i = 0; i < Math.ceil(v.getOutgoing().getLength() / step); ++i) {
- Vector3D p = v.getOutgoing().getPointAt(i * step);
- Assert.assertTrue(p.angle(enclosingCenter) <= enclosing.getRadius());
- }
- }
- }
-
- S2Point supportPointA = s2Point(48.68416, -4.59234);
- S2Point supportPointB = s2Point(41.38000, 9.22975);
- Assert.assertEquals(enclosing.getRadius(), supportPointA.distance(enclosing.getCenter()), TEST_EPS);
- Assert.assertEquals(enclosing.getRadius(), supportPointB.distance(enclosing.getCenter()), TEST_EPS);
- Assert.assertEquals(0.5 * supportPointA.distance(supportPointB), enclosing.getRadius(), TEST_EPS);
- Assert.assertEquals(2, enclosing.getSupportSize());
-
- EnclosingBall<S2Point> continentalInscribed =
- ((SphericalPolygonsSet) factory.getComplement(continental)).getEnclosingCap();
- Vector3D continentalCenter = continentalInscribed.getCenter().getVector();
- Assert.assertEquals(2.2, Math.toDegrees(Math.PI - continentalInscribed.getRadius()), 0.1);
- for (Vertex loopStart : continental.getBoundaryLoops()) {
- int count = 0;
- for (Vertex v = loopStart; count == 0 || v != loopStart; v = v.getOutgoing().getEnd()) {
- ++count;
- for (int i = 0; i < Math.ceil(v.getOutgoing().getLength() / step); ++i) {
- Vector3D p = v.getOutgoing().getPointAt(i * step);
- Assert.assertTrue(p.angle(continentalCenter) <= continentalInscribed.getRadius());
- }
- }
- }
-
- EnclosingBall<S2Point> corsicaInscribed =
- ((SphericalPolygonsSet) factory.getComplement(corsica)).getEnclosingCap();
- Vector3D corsicaCenter = corsicaInscribed.getCenter().getVector();
- Assert.assertEquals(0.34, Math.toDegrees(Math.PI - corsicaInscribed.getRadius()), 0.01);
- for (Vertex loopStart : corsica.getBoundaryLoops()) {
- int count = 0;
- for (Vertex v = loopStart; count == 0 || v != loopStart; v = v.getOutgoing().getEnd()) {
- ++count;
- for (int i = 0; i < Math.ceil(v.getOutgoing().getLength() / step); ++i) {
- Vector3D p = v.getOutgoing().getPointAt(i * step);
- Assert.assertTrue(p.angle(corsicaCenter) <= corsicaInscribed.getRadius());
- }
- }
- }
-
- }
-
- private SubCircle create(Vector3D pole, Vector3D x, Vector3D y,
- DoublePrecisionContext precision, double ... limits) {
- RegionFactory<S1Point> factory = new RegionFactory<>();
- Circle circle = new Circle(pole, precision);
- Circle phased =
- (Circle) Circle.getTransform(QuaternionRotation.createBasisRotation(circle.getXAxis(), circle.getYAxis(), x, y)).apply(circle);
- ArcsSet set = (ArcsSet) factory.getComplement(new ArcsSet(precision));
- for (int i = 0; i < limits.length; i += 2) {
- set = (ArcsSet) factory.union(set, new ArcsSet(limits[i], limits[i + 1], precision));
- }
- return new SubCircle(phased, set);
- }
-
- private SphericalPolygonsSet buildSimpleZone(double[][] points) {
- final S2Point[] vertices = new S2Point[points.length];
- for (int i = 0; i < points.length; ++i) {
- vertices[i] = s2Point(points[i][0], points[i][1]);
- }
- return new SphericalPolygonsSet(TEST_PRECISION, vertices);
- }
-
- private S2Point s2Point(double latitude, double longitude) {
- return S2Point.of(Math.toRadians(longitude), Math.toRadians(90.0 - latitude));
- }
-
- /** Create a {@link DoublePrecisionContext} with the given epsilon value.
- * @param eps epsilon value
- * @return new precision context
- */
- private static DoublePrecisionContext createPrecision(final double eps) {
- return new EpsilonDoublePrecisionContext(eps);
- }
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SubCircleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SubCircleTest.java
deleted file mode 100644
index b4664a0..0000000
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SubCircleTest.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * 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 org.apache.commons.geometry.core.Geometry;
-import org.apache.commons.geometry.core.partitioning.RegionFactory;
-import org.apache.commons.geometry.core.partitioning.Side;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane.SplitSubHyperplane;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.threed.Vector3D;
-import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.spherical.oned.ArcsSet;
-import org.apache.commons.geometry.spherical.oned.S1Point;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class SubCircleTest {
-
- private static final double TEST_EPS = 1e-10;
-
- private static final DoublePrecisionContext TEST_PRECISION =
- new EpsilonDoublePrecisionContext(TEST_EPS);
-
- @Test
- public void testFullCircle() {
- Circle circle = new Circle(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
- SubCircle set = circle.wholeHyperplane();
- Assert.assertEquals(Geometry.TWO_PI, set.getSize(), TEST_EPS);
- Assert.assertTrue(circle == set.getHyperplane());
- Assert.assertTrue(circle != set.copySelf().getHyperplane());
- }
-
- @Test
- public void testSide() {
-
- Circle xzPlane = new Circle(Vector3D.Unit.PLUS_Y, TEST_PRECISION);
-
- SubCircle sc1 = create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION, 1.0, 3.0, 5.0, 6.0);
- Assert.assertEquals(Side.BOTH, sc1.split(xzPlane).getSide());
-
- SubCircle sc2 = create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION, 1.0, 3.0);
- Assert.assertEquals(Side.MINUS, sc2.split(xzPlane).getSide());
-
- SubCircle sc3 = create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION, 5.0, 6.0);
- Assert.assertEquals(Side.PLUS, sc3.split(xzPlane).getSide());
-
- SubCircle sc4 = create(Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION, 5.0, 6.0);
- Assert.assertEquals(Side.HYPER, sc4.split(xzPlane).getSide());
-
- SubCircle sc5 = create(Vector3D.Unit.MINUS_Y, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION, 5.0, 6.0);
- Assert.assertEquals(Side.HYPER, sc5.split(xzPlane).getSide());
-
- }
-
- @Test
- public void testSPlit() {
-
- Circle xzPlane = new Circle(Vector3D.Unit.PLUS_Y, TEST_PRECISION);
-
- SubCircle sc1 = create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION, 1.0, 3.0, 5.0, 6.0);
- SplitSubHyperplane<S2Point> split1 = sc1.split(xzPlane);
- ArcsSet plus1 = (ArcsSet) ((SubCircle) split1.getPlus()).getRemainingRegion();
- ArcsSet minus1 = (ArcsSet) ((SubCircle) split1.getMinus()).getRemainingRegion();
- Assert.assertEquals(1, plus1.asList().size());
- Assert.assertEquals(5.0, plus1.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, plus1.asList().get(0).getSup(), TEST_EPS);
- Assert.assertEquals(1, minus1.asList().size());
- Assert.assertEquals(1.0, minus1.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(3.0, minus1.asList().get(0).getSup(), TEST_EPS);
-
- SubCircle sc2 = create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION, 1.0, 3.0);
- SplitSubHyperplane<S2Point> split2 = sc2.split(xzPlane);
- Assert.assertNull(split2.getPlus());
- ArcsSet minus2 = (ArcsSet) ((SubCircle) split2.getMinus()).getRemainingRegion();
- Assert.assertEquals(1, minus2.asList().size());
- Assert.assertEquals(1.0, minus2.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(3.0, minus2.asList().get(0).getSup(), TEST_EPS);
-
- SubCircle sc3 = create(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION, 5.0, 6.0);
- SplitSubHyperplane<S2Point> split3 = sc3.split(xzPlane);
- ArcsSet plus3 = (ArcsSet) ((SubCircle) split3.getPlus()).getRemainingRegion();
- Assert.assertEquals(1, plus3.asList().size());
- Assert.assertEquals(5.0, plus3.asList().get(0).getInf(), TEST_EPS);
- Assert.assertEquals(6.0, plus3.asList().get(0).getSup(), TEST_EPS);
- Assert.assertNull(split3.getMinus());
-
- SubCircle sc4 = create(Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION, 5.0, 6.0);
- SplitSubHyperplane<S2Point> split4 = sc4.split(xzPlane);
- Assert.assertEquals(Side.HYPER, sc4.split(xzPlane).getSide());
- Assert.assertNull(split4.getPlus());
- Assert.assertNull(split4.getMinus());
-
- SubCircle sc5 = create(Vector3D.Unit.MINUS_Y, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Z, TEST_PRECISION, 5.0, 6.0);
- SplitSubHyperplane<S2Point> split5 = sc5.split(xzPlane);
- Assert.assertEquals(Side.HYPER, sc5.split(xzPlane).getSide());
- Assert.assertNull(split5.getPlus());
- Assert.assertNull(split5.getMinus());
-
- }
-
- @Test
- public void testSideSplitConsistency() {
-
- double tolerance = 1.0e-6;
- DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(tolerance);
- Circle hyperplane = new Circle(Vector3D.of(9.738804529764676E-5, -0.6772824575010357, -0.7357230887208355),
- precision);
- SubCircle sub = new SubCircle(new Circle(Vector3D.of(2.1793884139073498E-4, 0.9790647032675541, -0.20354915700704285),
- precision),
- new ArcsSet(4.7121441684170700, 4.7125386635004760, precision));
- SplitSubHyperplane<S2Point> split = sub.split(hyperplane);
- Assert.assertNotNull(split.getMinus());
- Assert.assertNull(split.getPlus());
- Assert.assertEquals(Side.MINUS, sub.split(hyperplane).getSide());
-
- }
-
- private SubCircle create(Vector3D pole, Vector3D x, Vector3D y,
- DoublePrecisionContext precision, double ... limits) {
- RegionFactory<S1Point> factory = new RegionFactory<>();
- Circle circle = new Circle(pole, precision);
- Circle phased =
- (Circle) Circle.getTransform(QuaternionRotation.createBasisRotation(circle.getXAxis(), circle.getYAxis(), x, y)).apply(circle);
- ArcsSet set = (ArcsSet) factory.getComplement(new ArcsSet(precision));
- for (int i = 0; i < limits.length; i += 2) {
- set = (ArcsSet) factory.union(set, new ArcsSet(limits[i], limits[i + 1], precision));
- }
- return new SubCircle(phased, set);
- }
-
-}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SubGreatCircleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SubGreatCircleTest.java
new file mode 100644
index 0000000..18ed3bd
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SubGreatCircleTest.java
@@ -0,0 +1,529 @@
+/*
+ * 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.util.List;
+
+import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.apache.commons.geometry.spherical.oned.AngularInterval;
+import org.apache.commons.geometry.spherical.oned.RegionBSPTree1S;
+import org.apache.commons.geometry.spherical.twod.SubGreatCircle.SubGreatCircleBuilder;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SubGreatCircleTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final GreatCircle XY_CIRCLE = GreatCircle.fromPoleAndU(
+ Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ @Test
+ public void testCtor_default() {
+ // act
+ SubGreatCircle sub = new SubGreatCircle(XY_CIRCLE);
+
+ // assert
+ Assert.assertFalse(sub.isFull());
+ Assert.assertTrue(sub.isEmpty());
+ Assert.assertTrue(sub.isFinite());
+ Assert.assertFalse(sub.isInfinite());
+
+ Assert.assertEquals(0, sub.getSize(), TEST_EPS);
+
+ for (double az = 0; az <= Geometry.TWO_PI; az += 0.5) {
+ for (double p = 0; p <= Geometry.PI; p += 0.5) {
+ checkClassify(sub, RegionLocation.OUTSIDE, Point2S.of(az, p));
+ }
+ }
+ }
+
+ @Test
+ public void testCtor_boolean_true() {
+ // act
+ SubGreatCircle sub = new SubGreatCircle(XY_CIRCLE, true);
+
+ // assert
+ Assert.assertTrue(sub.isFull());
+ Assert.assertFalse(sub.isEmpty());
+ Assert.assertTrue(sub.isFinite());
+ Assert.assertFalse(sub.isInfinite());
+
+ Assert.assertEquals(Geometry.TWO_PI, sub.getSize(), TEST_EPS);
+
+ for (double az = 0; az < Geometry.TWO_PI; az += 0.1) {
+ checkClassify(sub, RegionLocation.INSIDE, Point2S.of(az, Geometry.HALF_PI));
+ }
+
+ checkClassify(sub, RegionLocation.OUTSIDE,
+ Point2S.PLUS_K, Point2S.of(0, Geometry.HALF_PI + 0.1),
+ Point2S.MINUS_K, Point2S.of(0, Geometry.HALF_PI - 0.1));
+ }
+
+ @Test
+ public void testCtor_boolean_false() {
+ // act
+ SubGreatCircle sub = new SubGreatCircle(XY_CIRCLE, false);
+
+ // assert
+ Assert.assertFalse(sub.isFull());
+ Assert.assertTrue(sub.isEmpty());
+ Assert.assertTrue(sub.isFinite());
+ Assert.assertFalse(sub.isInfinite());
+
+ Assert.assertEquals(0, sub.getSize(), TEST_EPS);
+
+ for (double az = 0; az <= Geometry.TWO_PI; az += 0.5) {
+ for (double p = 0; p <= Geometry.PI; p += 0.5) {
+ checkClassify(sub, RegionLocation.OUTSIDE, Point2S.of(az, p));
+ }
+ }
+ }
+
+ @Test
+ public void testCtor_tree() {
+ // arrange
+ RegionBSPTree1S tree = RegionBSPTree1S.fromInterval(AngularInterval.of(1, 2, TEST_PRECISION));
+
+ // act
+ SubGreatCircle sub = new SubGreatCircle(XY_CIRCLE, tree);
+
+ // assert
+ Assert.assertFalse(sub.isFull());
+ Assert.assertFalse(sub.isEmpty());
+ Assert.assertTrue(sub.isFinite());
+ Assert.assertFalse(sub.isInfinite());
+
+ Assert.assertEquals(1, sub.getSize(), TEST_EPS);
+
+ checkClassify(sub, RegionLocation.INSIDE, Point2S.of(1.5, Geometry.HALF_PI));
+
+ checkClassify(sub, RegionLocation.BOUNDARY,
+ Point2S.of(1, Geometry.HALF_PI), Point2S.of(2, Geometry.HALF_PI));
+
+ checkClassify(sub, RegionLocation.OUTSIDE,
+ Point2S.of(0.5, Geometry.HALF_PI), Point2S.of(2.5, Geometry.HALF_PI),
+ Point2S.of(1.5, 1), Point2S.of(1.5, Geometry.PI - 1));
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_K, Point2S.MINUS_I, TEST_PRECISION);
+ RegionBSPTree1S region = RegionBSPTree1S.empty();
+ region.add(AngularInterval.of(Geometry.PI, Geometry.MINUS_HALF_PI, TEST_PRECISION));
+ region.add(AngularInterval.of(0, Geometry.HALF_PI, TEST_PRECISION));
+
+ Transform2S t = Transform2S.createRotation(Point2S.PLUS_I, Geometry.HALF_PI)
+ .reflect(Point2S.of(-0.25 * Geometry.PI, Geometry.HALF_PI));
+
+ SubGreatCircle sub = new SubGreatCircle(circle, region);
+
+ // act
+ SubGreatCircle result = sub.transform(t);
+
+ // assert
+ List<GreatArc> arcs = result.toConvex();
+ Assert.assertEquals(2, arcs.size());
+
+ checkArc(arcs.get(0), Point2S.MINUS_I, Point2S.MINUS_J);
+ checkArc(arcs.get(1), Point2S.PLUS_I, Point2S.PLUS_J);
+ }
+
+ @Test
+ public void testSplit_full() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ SubGreatCircle sub = new SubGreatCircle(circle, true);
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(-1, 0, 1), TEST_PRECISION);
+
+ // act
+ Split<SubGreatCircle> split = sub.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ SubGreatCircle minus = split.getMinus();
+ Assert.assertSame(sub.getCircle(), minus.getCircle());
+
+ List<GreatArc> minusArcs = minus.toConvex();
+ Assert.assertEquals(1, minusArcs.size());
+ checkArc(minusArcs.get(0), Point2S.MINUS_J, Point2S.PLUS_J);
+
+ checkClassify(minus, RegionLocation.OUTSIDE, Point2S.MINUS_I);
+ checkClassify(minus, RegionLocation.INSIDE, Point2S.PLUS_I);
+
+ SubGreatCircle plus = split.getPlus();
+ Assert.assertSame(sub.getCircle(), plus.getCircle());
+
+ List<GreatArc> plusArcs = plus.toConvex();
+ Assert.assertEquals(1, plusArcs.size());
+ checkArc(plusArcs.get(0), Point2S.PLUS_J, Point2S.MINUS_J);
+
+ checkClassify(plus, RegionLocation.INSIDE, Point2S.MINUS_I);
+ checkClassify(plus, RegionLocation.OUTSIDE, Point2S.PLUS_I);
+ }
+
+ @Test
+ public void testSplit_empty() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ SubGreatCircle sub = new SubGreatCircle(circle, false);
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(-1, 0, 1), TEST_PRECISION);
+
+ // act
+ Split<SubGreatCircle> split = sub.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ SubGreatCircle minus = split.getMinus();
+ Assert.assertNull(minus);
+
+ SubGreatCircle plus = split.getPlus();
+ Assert.assertNull(plus);
+ }
+
+ @Test
+ public void testSplit_both() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+
+ RegionBSPTree1S tree = RegionBSPTree1S.empty();
+ tree.add(AngularInterval.of(0, 1, TEST_PRECISION));
+ tree.add(AngularInterval.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION));
+ tree.add(AngularInterval.of(Geometry.PI + 1, Geometry.PI + 2, TEST_PRECISION));
+
+ SubGreatCircle sub = new SubGreatCircle(circle, tree);
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.of(0, 1, 1), TEST_PRECISION);
+
+ // act
+ Split<SubGreatCircle> split = sub.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ SubGreatCircle minus = split.getMinus();
+ Assert.assertSame(sub.getCircle(), minus.getCircle());
+ List<GreatArc> minusArcs = minus.toConvex();
+ Assert.assertEquals(2, minusArcs.size());
+ checkArc(minusArcs.get(0), Point2S.of(1.5 * Geometry.PI, 0.25 * Geometry.PI), Point2S.MINUS_J);
+ checkArc(minusArcs.get(1), Point2S.of(1.5 * Geometry.PI, Geometry.HALF_PI + 1),
+ Point2S.of(0.5 * Geometry.PI, (1.5 * Geometry.PI) - 2));
+
+ SubGreatCircle plus = split.getPlus();
+ Assert.assertSame(sub.getCircle(), plus.getCircle());
+ List<GreatArc> plusArcs = plus.toConvex();
+ Assert.assertEquals(2, plusArcs.size());
+ checkArc(plusArcs.get(0), Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI), Point2S.of(Geometry.HALF_PI, Geometry.HALF_PI - 1));
+ checkArc(plusArcs.get(1), Point2S.of(0, 0), Point2S.of(1.5 * Geometry.PI, 0.25 * Geometry.PI));
+ }
+
+ @Test
+ public void testSplit_minus() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+ RegionBSPTree1S tree = AngularInterval.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION).toTree();
+
+ SubGreatCircle sub = new SubGreatCircle(circle, tree);
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.Unit.from(-1, 0, -1), TEST_PRECISION);
+
+ // act
+ Split<SubGreatCircle> split = sub.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ SubGreatCircle minus = split.getMinus();
+ Assert.assertSame(sub, minus);
+
+ SubGreatCircle plus = split.getPlus();
+ Assert.assertNull(plus);
+ }
+
+ @Test
+ public void testSplit_plus() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+ RegionBSPTree1S tree = AngularInterval.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION).toTree();
+
+ SubGreatCircle sub = new SubGreatCircle(circle, tree);
+
+ GreatCircle splitter = GreatCircle.fromPole(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ // act
+ Split<SubGreatCircle> split = sub.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ SubGreatCircle minus = split.getMinus();
+ Assert.assertNull(minus);
+
+ SubGreatCircle plus = split.getPlus();
+ Assert.assertSame(sub, plus);
+ }
+
+ @Test
+ public void testSplit_parallelAndAntiparallel() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ RegionBSPTree1S tree = AngularInterval.of(Geometry.HALF_PI, Geometry.PI, TEST_PRECISION).toTree();
+
+ SubGreatCircle sub = new SubGreatCircle(circle, tree);
+
+ // act/assert
+ Assert.assertEquals(SplitLocation.NEITHER,
+ sub.split(GreatCircle.fromPole(Vector3D.Unit.PLUS_Z, TEST_PRECISION)).getLocation());
+ Assert.assertEquals(SplitLocation.NEITHER,
+ sub.split(GreatCircle.fromPole(Vector3D.Unit.MINUS_Z, TEST_PRECISION)).getLocation());
+ }
+
+ @Test
+ public void testAdd_arc() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.MINUS_K, Point2S.MINUS_J, TEST_PRECISION);
+ GreatCircle closeCircle = GreatCircle.fromPoints(Point2S.MINUS_K,
+ Point2S.of((1.5 * Geometry.PI) - 1e-11, Geometry.HALF_PI), TEST_PRECISION);
+
+ SubGreatCircle sub = new SubGreatCircle(circle);
+
+ // act
+ sub.add(circle.arc(Point2S.of(1.5 * Geometry.PI, 0.75 * Geometry.PI), Point2S.MINUS_J));
+ sub.add(closeCircle.arc(Point2S.PLUS_J, Point2S.of(1.5 * Geometry.PI, 0.75 * Geometry.PI)));
+
+ // assert
+ List<GreatArc> arcs = sub.toConvex();
+
+ Assert.assertEquals(1, arcs.size());
+ checkArc(arcs.get(0), Point2S.PLUS_J, Point2S.MINUS_J);
+ }
+
+ @Test(expected = GeometryException.class)
+ public void testAdd_arc_differentCircle() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.MINUS_K, Point2S.MINUS_J, TEST_PRECISION);
+ GreatCircle otherCircle = GreatCircle.fromPoints(Point2S.MINUS_K,
+ Point2S.of((1.5 * Geometry.PI) - 1e-2, Geometry.HALF_PI), TEST_PRECISION);
+
+ SubGreatCircle sub = new SubGreatCircle(circle);
+
+ // act/assert
+ sub.add(otherCircle.arc(Point2S.PLUS_J, Point2S.of(1.5 * Geometry.PI, 0.75 * Geometry.PI)));
+ }
+
+ @Test
+ public void testAdd_subGreatCircle() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.MINUS_K, Point2S.MINUS_J, TEST_PRECISION);
+ GreatCircle closeCircle = GreatCircle.fromPoints(Point2S.MINUS_K,
+ Point2S.of((1.5 * Geometry.PI) - 1e-11, Geometry.HALF_PI), TEST_PRECISION);
+
+ SubGreatCircle sub = new SubGreatCircle(circle);
+
+ RegionBSPTree1S regionA = RegionBSPTree1S.empty();
+ regionA.add(AngularInterval.of(Geometry.PI, 1.25 * Geometry.PI, TEST_PRECISION));
+ regionA.add(AngularInterval.of(0.25 * Geometry.PI, Geometry.HALF_PI, TEST_PRECISION));
+
+ RegionBSPTree1S regionB = RegionBSPTree1S.empty();
+ regionB.add(AngularInterval.of(1.5 * Geometry.PI, 0.25 * Geometry.PI, TEST_PRECISION));
+
+ // act
+ sub.add(new SubGreatCircle(circle, regionA));
+ sub.add(new SubGreatCircle(closeCircle, regionB));
+
+ // assert
+ List<GreatArc> arcs = sub.toConvex();
+
+ Assert.assertEquals(2, arcs.size());
+ checkArc(arcs.get(0), Point2S.of(Geometry.HALF_PI, 0), Point2S.of(Geometry.HALF_PI, 0.25 * Geometry.PI));
+ checkArc(arcs.get(1), Point2S.PLUS_J, Point2S.MINUS_J);
+ }
+
+ @Test(expected = GeometryException.class)
+ public void testAdd_subGreatCircle_otherCircle() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.MINUS_K, Point2S.MINUS_J, TEST_PRECISION);
+ GreatCircle otherCircle = GreatCircle.fromPoints(Point2S.MINUS_K,
+ Point2S.of((1.5 * Geometry.PI) - 1e-5, Geometry.HALF_PI), TEST_PRECISION);
+
+ SubGreatCircle sub = new SubGreatCircle(circle);
+
+ // act/assert
+ sub.add(new SubGreatCircle(otherCircle, RegionBSPTree1S.full()));
+ }
+
+ @Test
+ public void testBuilder() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.MINUS_K, Point2S.MINUS_J, TEST_PRECISION);
+
+ SubGreatCircle sub = new SubGreatCircle(circle);
+
+ RegionBSPTree1S region = RegionBSPTree1S.empty();
+ region.add(AngularInterval.of(Geometry.PI, 1.25 * Geometry.PI, TEST_PRECISION));
+ region.add(AngularInterval.of(0.25 * Geometry.PI, Geometry.HALF_PI, TEST_PRECISION));
+
+ // act
+ SubGreatCircleBuilder builder = sub.builder();
+
+ builder.add(new SubGreatCircle(circle, region));
+ builder.add(circle.arc(1.5 * Geometry.PI, 0.25 * Geometry.PI));
+
+ SubGreatCircle result = builder.build();
+
+ // assert
+ List<GreatArc> arcs = result.toConvex();
+
+ Assert.assertEquals(2, arcs.size());
+ checkArc(arcs.get(0), Point2S.of(Geometry.HALF_PI, 0), Point2S.of(Geometry.HALF_PI, 0.25 * Geometry.PI));
+ checkArc(arcs.get(1), Point2S.PLUS_J, Point2S.MINUS_J);
+ }
+
+ @Test
+ public void testBuilder_invalidArgs() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.MINUS_K, Point2S.MINUS_J, TEST_PRECISION);
+ GreatCircle otherCircle = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+
+ SubGreatCircle sub = new SubGreatCircle(circle);
+
+ SubGreatCircleBuilder builder = sub.builder();
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ builder.add(otherCircle.span());
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.add(new SubGreatCircle(otherCircle));
+ }, GeometryException.class);
+
+ GeometryTestUtils.assertThrows(() -> {
+ builder.add(new UnknownSubHyperplane());
+ }, IllegalArgumentException.class);
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ GreatCircle circle = GreatCircle.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
+ SubGreatCircle sub = new SubGreatCircle(circle);
+
+ // act
+ String str = sub.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("SubGreatCircle[", str);
+ GeometryTestUtils.assertContains("circle= GreatCircle[", str);
+ GeometryTestUtils.assertContains("region= RegionBSPTree1S[", str);
+ }
+
+ private static void checkClassify(SubHyperplane<Point2S> sub, RegionLocation loc, Point2S ... pts) {
+ for (Point2S pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, loc, sub.classify(pt));
+ }
+ }
+
+ private static void checkArc(GreatArc arc, Point2S start, Point2S end) {
+ SphericalTestUtils.assertPointsEq(start, arc.getStartPoint(), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(end, arc.getEndPoint(), TEST_EPS);
+ }
+
+ private static class UnknownSubHyperplane implements SubHyperplane<Point2S> {
+
+ @Override
+ public Split<? extends SubHyperplane<Point2S>> split(Hyperplane<Point2S> splitter) {
+ return null;
+ }
+
+ @Override
+ public Hyperplane<Point2S> getHyperplane() {
+ return null;
+ }
+
+ @Override
+ public boolean isFull() {
+ return false;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ @Override
+ public boolean isInfinite() {
+ return false;
+ }
+
+ @Override
+ public boolean isFinite() {
+ return false;
+ }
+
+ @Override
+ public double getSize() {
+ return 0;
+ }
+
+ @Override
+ public RegionLocation classify(Point2S point) {
+ return null;
+ }
+
+ @Override
+ public Point2S closest(Point2S point) {
+ return null;
+ }
+
+ @Override
+ public Builder<Point2S> builder() {
+ return null;
+ }
+
+ @Override
+ public SubHyperplane<Point2S> transform(Transform<Point2S> transform) {
+ return null;
+ }
+
+ @Override
+ public List<? extends ConvexSubHyperplane<Point2S>> toConvex() {
+ return null;
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/Transform2STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/Transform2STest.java
new file mode 100644
index 0000000..2c5a396
--- /dev/null
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/Transform2STest.java
@@ -0,0 +1,287 @@
+/*
+ * 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 org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Transform2STest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ @Test
+ public void testIdentity() {
+ // act
+ Transform2S t = Transform2S.identity();
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+ Assert.assertArrayEquals(new double[] {
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0
+ }, t.getEuclideanTransform().toArray(), 0);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_I, t.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_J, t.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_K, t.apply(Point2S.PLUS_K), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testRotation() {
+ // arrange
+ Transform2S aroundPole = Transform2S.createRotation(Point2S.PLUS_K, Geometry.HALF_PI);
+ Transform2S aroundX = Transform2S.createRotation(Vector3D.Unit.PLUS_X, -Geometry.HALF_PI);
+ Transform2S aroundY = Transform2S.createRotation(
+ QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // act/assert
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_J, aroundPole.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.MINUS_I, aroundPole.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, aroundPole.apply(Point2S.PLUS_K), TEST_EPS);
+ checkInverse(aroundPole);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_I, aroundX.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_K, aroundX.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_J, aroundX.apply(Point2S.PLUS_K), TEST_EPS);
+ checkInverse(aroundX);
+
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_K, aroundY.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_J, aroundY.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_I, aroundY.apply(Point2S.PLUS_K), TEST_EPS);
+ checkInverse(aroundY);
+ }
+
+ @Test
+ public void testMultipleRotations() {
+ // act
+ Transform2S t = Transform2S.identity()
+ .rotate(Point2S.PLUS_K, Geometry.HALF_PI)
+ .rotate(Vector3D.Unit.PLUS_X, -Geometry.HALF_PI)
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Geometry.HALF_PI));
+
+ // assert
+ SphericalTestUtils.assertPointsEqual(Point2S.MINUS_I, t.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, t.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_J, t.apply(Point2S.PLUS_K), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testMultiply() {
+ // act
+ Transform2S t = Transform2S.identity()
+ .multiply(Transform2S.createRotation(Point2S.PLUS_K, Geometry.HALF_PI))
+ .multiply(Transform2S.createRotation(Point2S.PLUS_J, Geometry.HALF_PI));
+
+ // assert
+ SphericalTestUtils.assertPointsEq(Point2S.MINUS_K, t.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.MINUS_I, t.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_J, t.apply(Point2S.PLUS_K), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testPremultiply() {
+ // act
+ Transform2S t = Transform2S.identity()
+ .premultiply(Transform2S.createRotation(Point2S.PLUS_K, Geometry.HALF_PI))
+ .premultiply(Transform2S.createRotation(Point2S.PLUS_J, Geometry.HALF_PI));
+
+ // assert
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_J, t.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, t.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_I, t.apply(Point2S.PLUS_K), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testReflection_point() {
+ // arrange
+ Point2S a = Point2S.of(1, 1);
+ Point2S b = Point2S.of(-1, 1);
+
+ Point2S c = Point2S.of(1, Geometry.PI - 1);
+ Point2S d = Point2S.of(-1, Geometry.PI - 1);
+
+ // act
+ Transform2S t = Transform2S.createReflection(Point2S.PLUS_I);
+
+ // assert
+ Assert.assertFalse(t.preservesOrientation());
+
+ SphericalTestUtils.assertPointsEqual(Point2S.MINUS_I, t.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_J, t.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, t.apply(Point2S.PLUS_K), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.PI - 1, 1), t.apply(a), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.PI + 1, 1), t.apply(b), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.PI - 1, Geometry.PI - 1), t.apply(c), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.PI + 1, Geometry.PI - 1), t.apply(d), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testReflection_vector() {
+ // arrange
+ Point2S a = Point2S.of(1, 1);
+ Point2S b = Point2S.of(-1, 1);
+
+ Point2S c = Point2S.of(1, Geometry.PI - 1);
+ Point2S d = Point2S.of(-1, Geometry.PI - 1);
+
+ // act
+ Transform2S t = Transform2S.createReflection(Vector3D.Unit.PLUS_Y);
+
+ // assert
+ Assert.assertFalse(t.preservesOrientation());
+
+ SphericalTestUtils.assertPointsEqual(Point2S.PLUS_I, t.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.MINUS_J, t.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, t.apply(Point2S.PLUS_K), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(b, t.apply(a), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(a, t.apply(b), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(d, t.apply(c), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(c, t.apply(d), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testDoubleReflection() {
+ // arrange
+ Point2S a = Point2S.of(1, 1);
+ Point2S b = Point2S.of(-1, 1);
+
+ Point2S c = Point2S.of(1, Geometry.PI - 1);
+ Point2S d = Point2S.of(-1, Geometry.PI - 1);
+
+ // act
+ Transform2S t = Transform2S.identity()
+ .reflect(Point2S.PLUS_I)
+ .reflect(Vector3D.Unit.PLUS_Y);
+
+ // assert
+ Assert.assertTrue(t.preservesOrientation());
+
+ SphericalTestUtils.assertPointsEqual(Point2S.MINUS_I, t.apply(Point2S.PLUS_I), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.MINUS_J, t.apply(Point2S.PLUS_J), TEST_EPS);
+ SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, t.apply(Point2S.PLUS_K), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.PI + 1, 1), t.apply(a), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.PI - 1, 1), t.apply(b), TEST_EPS);
+
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.PI + 1, Geometry.PI - 1), t.apply(c), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(Point2S.of(Geometry.PI - 1, Geometry.PI - 1), t.apply(d), TEST_EPS);
+
+ checkInverse(t);
+ }
+
+ @Test
+ public void testHashcode() {
+ // arrange
+ Transform2S a = Transform2S.createRotation(Point2S.PLUS_I, Geometry.HALF_PI);
+ Transform2S b = Transform2S.createRotation(Point2S.PLUS_J, Geometry.HALF_PI);
+ Transform2S c = Transform2S.createRotation(Point2S.PLUS_I, Geometry.PI);
+ Transform2S d = Transform2S.createRotation(Point2S.PLUS_I, Geometry.HALF_PI);
+
+ // act
+ int hash = a.hashCode();
+
+ // assert
+ Assert.assertEquals(hash, a.hashCode());
+
+ Assert.assertNotEquals(hash, b.hashCode());
+ Assert.assertNotEquals(hash, c.hashCode());
+
+ Assert.assertEquals(hash, d.hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // arrange
+ Transform2S a = Transform2S.createRotation(Point2S.PLUS_I, Geometry.HALF_PI);
+ Transform2S b = Transform2S.createRotation(Point2S.PLUS_J, Geometry.HALF_PI);
+ Transform2S c = Transform2S.createRotation(Point2S.PLUS_I, Geometry.PI);
+ Transform2S d = Transform2S.createRotation(Point2S.PLUS_I, Geometry.HALF_PI);
+
+ // act/assert
+ Assert.assertTrue(a.equals(a));
+
+ Assert.assertFalse(a.equals(null));
+ Assert.assertFalse(a.equals(new Object()));
+
+ Assert.assertFalse(a.equals(b));
+ Assert.assertFalse(a.equals(c));
+
+ Assert.assertTrue(a.equals(d));
+ Assert.assertTrue(d.equals(a));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Transform2S t = Transform2S.identity();
+
+ // act
+ String str = t.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("Transform2S", str);
+ GeometryTestUtils.assertContains("euclideanTransform= [", str);
+ }
+
+ private static void checkInverse(Transform2S t) {
+ Transform2S inv = t.inverse();
+
+ // test non-pole points
+ for (double az = -Geometry.TWO_PI; az <= 2 * Geometry.TWO_PI; az += 0.2) {
+ for (double p = 0.1; p < Geometry.PI; p += 0.2) {
+
+ Point2S pt = Point2S.of(az, p);
+
+ SphericalTestUtils.assertPointsEqual(pt, inv.apply(t.apply(pt)), TEST_EPS);
+ SphericalTestUtils.assertPointsEqual(pt, t.apply(inv.apply(pt)), TEST_EPS);
+ }
+ }
+
+ // test poles
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_Z,
+ inv.apply(t.apply(Point2S.of(1, 0))).getVector(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.PLUS_Z,
+ t.apply(inv.apply(Point2S.of(-1, 0))).getVector(), TEST_EPS);
+
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.MINUS_Z,
+ inv.apply(t.apply(Point2S.of(1, Geometry.PI))).getVector(), TEST_EPS);
+ SphericalTestUtils.assertVectorsEqual(Vector3D.Unit.MINUS_Z,
+ t.apply(inv.apply(Point2S.of(-1, Geometry.PI))).getVector(), TEST_EPS);
+ }
+}
diff --git a/src/main/resources/checkstyle/checkstyle-suppressions.xml b/src/main/resources/checkstyle/checkstyle-suppressions.xml
index 96c8490..f352d2a 100644
--- a/src/main/resources/checkstyle/checkstyle-suppressions.xml
+++ b/src/main/resources/checkstyle/checkstyle-suppressions.xml
@@ -19,6 +19,13 @@
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
+ <suppress checks="ParameterNumber" files=".*/internal/Matrices" />
+ <suppress checks="ParameterNumber" files=".*/oned/Vector1D" />
+ <suppress checks="ParameterNumber" files=".*/threed/AffineTransformMatrix3D" />
+ <suppress checks="ParameterNumber" files=".*/threed/Vector3D" />
+ <suppress checks="ParameterNumber" files=".*/threed/rotation/QuaternionRotation" />
+ <suppress checks="ParameterNumber" files=".*/twod/Vector2D" />
+
<!-- Be more lenient on tests. -->
<suppress checks="Javadoc" files=".*[/\\]test[/\\].*" />
<suppress checks="MultipleStringLiterals" files=".*[/\\]test[/\\].*" />