You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2018/04/20 14:36:12 UTC
[commons-geometry] 01/06: GEOMETRY-1: moving over existing geometry
code from commons-math
This is an automated email from the ASF dual-hosted git repository.
erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git
commit fdbc5cd789284dd41e4f2d7efdaaecfec70f814e
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Wed Apr 18 23:20:56 2018 -0400
GEOMETRY-1: moving over existing geometry code from commons-math
---
.../pom.xml | 29 +-
.../org/apache/commons/geometry/core/Geometry.java | 32 +
.../org/apache/commons/geometry/core/Point.java | 45 +
.../org/apache/commons/geometry/core/Space.java | 39 +
.../org/apache/commons/geometry/core/Vector.java | 160 ++
.../apache/commons/geometry/core/package-info.java | 25 +
.../geometry/core/partitioning/AbstractRegion.java | 545 ++++++
.../core/partitioning/AbstractSubHyperplane.java | 189 ++
.../geometry/core/partitioning/BSPTree.java | 775 ++++++++
.../geometry/core/partitioning/BSPTreeVisitor.java | 112 ++
.../core/partitioning/BoundaryAttribute.java | 97 ++
.../core/partitioning/BoundaryBuilder.java | 97 ++
.../core/partitioning/BoundaryProjection.java | 82 +
.../core/partitioning/BoundaryProjector.java | 201 +++
.../core/partitioning/BoundarySizeVisitor.java | 67 +
.../core/partitioning/Characterization.java | 196 +++
.../geometry/core/partitioning/Embedding.java | 67 +
.../geometry/core/partitioning/Hyperplane.java | 94 +
.../geometry/core/partitioning/InsideFinder.java | 149 ++
.../geometry/core/partitioning/NodesSet.java | 72 +
.../commons/geometry/core/partitioning/Region.java | 205 +++
.../geometry/core/partitioning/RegionFactory.java | 384 ++++
.../commons/geometry/core/partitioning/Side.java | 36 +
.../geometry/core/partitioning/SubHyperplane.java | 142 ++
.../geometry/core/partitioning/Transform.java | 78 +
.../geometry/core/partitioning/package-info.java | 114 ++
.../commons/geometry/core/GeometryTestUtils.java | 69 +
.../geometry/core/partitioning/TreeBuilder.java | 164 ++
.../geometry/core/partitioning/TreeDumper.java | 106 ++
.../geometry/core/partitioning/TreePrinter.java | 137 ++
.../pom.xml | 46 +-
.../commons/geometry/enclosing/Encloser.java | 35 +
.../commons/geometry/enclosing/EnclosingBall.java | 103 ++
.../geometry/enclosing/SupportBallGenerator.java | 41 +
.../commons/geometry/enclosing/WelzlEncloser.java | 180 ++
.../commons/geometry/enclosing/package-info.java | 24 +
.../threed/enclosing/SphereGenerator.java | 154 ++
.../euclidean/twod/enclosing/DiskGenerator.java | 109 ++
.../geometry/enclosing/WelzlEncloser2DTest.java | 179 ++
.../geometry/enclosing/WelzlEncloser3DTest.java | 187 ++
.../threed/enclosing/SphereGeneratorTest.java | 186 ++
.../twod/enclosing/DiskGeneratorTest.java | 121 ++
commons-geometry-euclidean-twod/pom.xml | 46 -
commons-geometry-euclidean/pom.xml | 92 +
.../geometry/euclidean/oned/Cartesian1D.java | 382 ++++
.../geometry/euclidean/oned/Euclidean1D.java | 80 +
.../commons/geometry/euclidean/oned/Interval.java | 89 +
.../geometry/euclidean/oned/IntervalsSet.java | 619 +++++++
.../geometry/euclidean/oned/OrientedPoint.java | 140 ++
.../geometry/euclidean/oned/SubOrientedPoint.java | 76 +
.../commons/geometry/euclidean/oned/Vector1D.java | 31 +
.../geometry/euclidean/oned/package-info.java | 24 +
.../geometry/euclidean/threed/Cartesian3D.java | 621 +++++++
.../geometry/euclidean/threed/Euclidean3D.java | 75 +
.../commons/geometry/euclidean/threed/Line.java | 274 +++
.../euclidean/threed/OutlineExtractor.java | 264 +++
.../commons/geometry/euclidean/threed/Plane.java | 498 ++++++
.../geometry/euclidean/threed/PolyhedronsSet.java | 705 ++++++++
.../geometry/euclidean/threed/Rotation.java | 1419 +++++++++++++++
.../euclidean/threed/RotationConvention.java | 78 +
.../geometry/euclidean/threed/RotationOrder.java | 172 ++
.../commons/geometry/euclidean/threed/Segment.java | 65 +
.../commons/geometry/euclidean/threed/SubLine.java | 147 ++
.../geometry/euclidean/threed/SubPlane.java | 105 ++
.../geometry/euclidean/threed/Vector3D.java | 45 +
.../geometry/euclidean/threed/package-info.java | 24 +
.../geometry/euclidean/twod/Cartesian2D.java | 491 ++++++
.../geometry/euclidean/twod/Euclidean2D.java | 75 +
.../commons/geometry/euclidean/twod/Line.java | 559 ++++++
.../geometry/euclidean/twod/NestedLoops.java | 195 +++
.../geometry/euclidean/twod/PolygonsSet.java | 1105 ++++++++++++
.../commons/geometry/euclidean/twod/Segment.java | 109 ++
.../commons/geometry/euclidean/twod/SubLine.java | 198 +++
.../commons/geometry/euclidean/twod/Vector2D.java | 37 +
.../geometry/euclidean/twod/package-info.java | 24 +
.../core/partitioning/CharacterizationTest.java | 427 +++++
.../geometry/euclidean/EuclideanTestUtils.java | 376 ++++
.../geometry/euclidean/oned/Cartesian1DTest.java | 385 ++++
.../geometry/euclidean/oned/Euclidean1DTest.java | 43 +
.../geometry/euclidean/oned/IntervalTest.java | 182 ++
.../geometry/euclidean/oned/IntervalsSetTest.java | 586 +++++++
.../geometry/euclidean/oned/OrientedPointTest.java | 189 ++
.../euclidean/oned/SubOrientedPointTest.java | 160 ++
.../geometry/euclidean/threed/Euclidean3DTest.java | 44 +
.../geometry/euclidean/threed/LineTest.java | 146 ++
.../geometry/euclidean/threed/OBJWriter.java | 336 ++++
.../geometry/euclidean/threed/PLYParser.java | 289 +++
.../geometry/euclidean/threed/PlaneTest.java | 169 ++
.../euclidean/threed/PolyhedronsSetTest.java | 1494 ++++++++++++++++
.../euclidean/threed/RotationOrderTest.java | 59 +
.../geometry/euclidean/threed/RotationTest.java | 812 +++++++++
.../geometry/euclidean/threed/SubLineTest.java | 167 ++
.../geometry/euclidean/threed/Vector3DTest.java | 407 +++++
.../geometry/euclidean/twod/Cartesian2DTest.java | 232 +++
.../geometry/euclidean/twod/Euclidean2DTest.java | 44 +
.../commons/geometry/euclidean/twod/LineTest.java | 131 ++
.../geometry/euclidean/twod/NestedLoopsTest.java | 66 +
.../geometry/euclidean/twod/PolygonsSetTest.java | 1842 ++++++++++++++++++++
.../geometry/euclidean/twod/SegmentTest.java | 42 +
.../geometry/euclidean/twod/SubLineTest.java | 159 ++
.../geometry/euclidean/threed/issue-1211.bsp | 15 +
.../threed/pentomino-N-bad-orientation.ply | 40 +
.../geometry/euclidean/threed/pentomino-N-hole.ply | 39 +
.../euclidean/threed/pentomino-N-out-of-plane.ply | 40 +
.../euclidean/threed/pentomino-N-too-close.ply | 86 +
.../geometry/euclidean/threed/pentomino-N.ply | 39 +
commons-geometry-hull/pom.xml | 84 +
.../twod/hull/AbstractConvexHullGenerator2D.java | 108 ++
.../euclidean/twod/hull/AklToussaintHeuristic.java | 152 ++
.../geometry/euclidean/twod/hull/ConvexHull2D.java | 169 ++
.../euclidean/twod/hull/ConvexHullGenerator2D.java | 34 +
.../euclidean/twod/hull/MonotoneChain.java | 180 ++
.../geometry/euclidean/twod/hull/package-info.java | 25 +
.../apache/commons/geometry/hull/ConvexHull.java | 46 +
.../commons/geometry/hull/ConvexHullGenerator.java | 44 +
.../apache/commons/geometry/hull/package-info.java | 24 +
.../twod/hull/AklToussaintHeuristicTest.java | 41 +
.../hull/ConvexHullGenerator2DAbstractTest.java | 437 +++++
.../euclidean/twod/hull/MonotoneChainTest.java | 54 +
.../pom.xml | 39 +-
.../geometry/spherical/SphericalCoordinates.java | 394 +++++
.../commons/geometry/spherical/oned/Arc.java | 128 ++
.../commons/geometry/spherical/oned/ArcsSet.java | 950 ++++++++++
.../geometry/spherical/oned/LimitAngle.java | 133 ++
.../commons/geometry/spherical/oned/S1Point.java | 158 ++
.../commons/geometry/spherical/oned/Sphere1D.java | 86 +
.../geometry/spherical/oned/SubLimitAngle.java | 65 +
.../geometry/spherical/oned/package-info.java | 30 +
.../commons/geometry/spherical/twod/Circle.java | 335 ++++
.../commons/geometry/spherical/twod/Edge.java | 221 +++
.../geometry/spherical/twod/EdgesBuilder.java | 169 ++
.../spherical/twod/PropertiesComputer.java | 173 ++
.../commons/geometry/spherical/twod/S2Point.java | 235 +++
.../commons/geometry/spherical/twod/Sphere2D.java | 81 +
.../spherical/twod/SphericalPolygonsSet.java | 562 ++++++
.../commons/geometry/spherical/twod/SubCircle.java | 70 +
.../commons/geometry/spherical/twod/Vertex.java | 123 ++
.../geometry/spherical/twod/package-info.java | 30 +
.../spherical/SphericalCoordinatesTest.java | 83 +
.../geometry/spherical/SphericalTestUtils.java | 121 ++
pom.xml | 81 +-
141 files changed, 29369 insertions(+), 79 deletions(-)
diff --git a/commons-geometry-bsp/pom.xml b/commons-geometry-core/pom.xml
similarity index 63%
rename from commons-geometry-bsp/pom.xml
rename to commons-geometry-core/pom.xml
index a02beae..0d10b34 100644
--- a/commons-geometry-bsp/pom.xml
+++ b/commons-geometry-core/pom.xml
@@ -27,20 +27,37 @@
</parent>
<groupId>org.apache.commons</groupId>
- <artifactId>commons-geometry-bsp</artifactId>
+ <artifactId>commons-geometry-core</artifactId>
<version>1.0-SNAPSHOT</version>
- <name>Apache Commons Geometry Binary Space Partition</name>
+ <name>Apache Commons Geometry Core</name>
- <description></description>
+ <description>Core interfaces and classes for Apache Commons Geometry.</description>
<properties>
<!-- OSGi -->
- <commons.osgi.symbolicName>org.apache.commons.geometry.bsp</commons.osgi.symbolicName>
- <commons.osgi.export>org.apache.commons.geometry.bsp</commons.osgi.export>
+ <commons.osgi.symbolicName>org.apache.commons.geometry.core</commons.osgi.symbolicName>
+ <commons.osgi.export>org.apache.commons.geometry.core</commons.osgi.export>
<!-- Java 9+ -->
- <commons.automatic.module.name>org.apache.commons.geometry.bsp</commons.automatic.module.name>
+ <commons.automatic.module.name>org.apache.commons.geometry.core</commons.automatic.module.name>
<!-- Workaround to avoid duplicating config files. -->
<geometry.parent.dir>${basedir}/..</geometry.parent.dir>
</properties>
+
+ <build>
+ <plugins>
+ <!-- Make the core test utilities accessible to other projects. -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
</project>
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
new file mode 100644
index 0000000..adef1ab
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/** Class containing geometric constants.
+ */
+public class Geometry {
+
+ /** Alias for {@link Math#PI}, placed here for completeness. */
+ public static final double PI = Math.PI;
+
+ /** Constant representing {@code 2*pi}.
+ */
+ public static final double TWO_PI = 2.0 * Math.PI;
+
+ /** Private constructor */
+ private Geometry() {}
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Point.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Point.java
new file mode 100644
index 0000000..9a9b2f4
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Point.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;
+
+import java.io.Serializable;
+
+/** This interface represents a generic geometrical point.
+ * @param <S> Type of the space.
+ * @see Space
+ * @see Vector
+ */
+public interface Point<S extends Space> extends Serializable {
+
+ /** Get the space to which the point belongs.
+ * @return containing space
+ */
+ Space getSpace();
+
+ /**
+ * Returns true if any coordinate of this point is NaN; false otherwise
+ * @return true if any coordinate of this point is NaN; false otherwise
+ */
+ boolean isNaN();
+
+ /** Compute the distance between the instance and another point.
+ * @param p second point
+ * @return the distance between the instance and p
+ */
+ double distance(Point<S> p);
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Space.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Space.java
new file mode 100644
index 0000000..a932550
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Space.java
@@ -0,0 +1,39 @@
+/*
+ * 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.io.Serializable;
+
+/** This interface represents a generic space, with affine and vectorial counterparts.
+ * @see Vector
+ */
+public interface Space extends Serializable {
+
+ /** Get the dimension of the space.
+ * @return dimension of the space
+ */
+ int getDimension();
+
+ /** Get the n-1 dimension subspace of this space.
+ * @return n-1 dimension sub-space of this space
+ * @see #getDimension()
+ * @exception UnsupportedOperationException for dimension-1 spaces
+ * which do not have sub-spaces
+ */
+ Space getSubSpace() throws UnsupportedOperationException;
+
+}
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
new file mode 100644
index 0000000..e13799c
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
@@ -0,0 +1,160 @@
+/*
+ * 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.text.NumberFormat;
+
+/** This interface represents a generic vector in a vectorial space or a point in an affine space.
+ * @param <S> Type of the space.
+ * @see Space
+ * @see Point
+ */
+public interface Vector<S extends Space> {
+
+ /** Get the space to which the point belongs.
+ * @return containing space
+ */
+ Space getSpace();
+
+ /** Get the null vector of the vectorial space or origin point of the affine space.
+ * @return null vector of the vectorial space or origin point of the affine space
+ */
+ Vector<S> getZero();
+
+ /** Get the L<sub>1</sub> norm for the vector.
+ * @return L<sub>1</sub> norm for the vector
+ */
+ double getNorm1();
+
+ /** Get the L<sub>2</sub> norm for the vector.
+ * @return Euclidean norm for the vector
+ */
+ double getNorm();
+
+ /** Get the square of the norm for the vector.
+ * @return square of the Euclidean norm for the vector
+ */
+ double getNormSq();
+
+ /** Get the L<sub>∞</sub> norm for the vector.
+ * @return L<sub>∞</sub> norm for the vector
+ */
+ double getNormInf();
+
+ /** Add a vector to the instance.
+ * @param v vector to add
+ * @return a new vector
+ */
+ Vector<S> add(Vector<S> v);
+
+ /** Add a scaled vector to the instance.
+ * @param factor scale factor to apply to v before adding it
+ * @param v vector to add
+ * @return a new vector
+ */
+ Vector<S> add(double factor, Vector<S> v);
+
+ /** Subtract a vector from the instance.
+ * @param v vector to subtract
+ * @return a new vector
+ */
+ Vector<S> subtract(Vector<S> v);
+
+ /** Subtract a scaled vector from the instance.
+ * @param factor scale factor to apply to v before subtracting it
+ * @param v vector to subtract
+ * @return a new vector
+ */
+ Vector<S> subtract(double factor, Vector<S> v);
+
+ /** Get the opposite of the instance.
+ * @return a new vector which is opposite to the instance
+ */
+ Vector<S> negate();
+
+ /** Get a normalized vector aligned with the instance.
+ * @return a new normalized vector
+ * @exception IllegalStateException if the norm is zero
+ */
+ Vector<S> normalize() throws IllegalStateException;
+
+ /** Multiply the instance by a scalar.
+ * @param a scalar
+ * @return a new vector
+ */
+ Vector<S> scalarMultiply(double a);
+
+ /**
+ * Returns true if any coordinate of this point is NaN; false otherwise
+ * @return true if any coordinate of this point is NaN; false otherwise
+ */
+ boolean isNaN();
+
+ /**
+ * Returns true if any coordinate of this vector is infinite and none are NaN;
+ * false otherwise
+ * @return true if any coordinate of this vector is infinite and none are NaN;
+ * false otherwise
+ */
+ boolean isInfinite();
+
+ /** Compute the distance between the instance and another vector according to the L<sub>1</sub> norm.
+ * <p>Calling this method is equivalent to calling:
+ * <code>q.subtract(p).getNorm1()</code> except that no intermediate
+ * vector is built</p>
+ * @param v second vector
+ * @return the distance between the instance and p according to the L<sub>1</sub> norm
+ */
+ double distance1(Vector<S> v);
+
+ /** Compute the distance between the instance and another vector.
+ * @param v second vector
+ * @return the distance between the instance and v
+ */
+ double distance(Vector<S> v);
+
+ /** Compute the distance between the instance and another vector according to the L<sub>∞</sub> norm.
+ * <p>Calling this method is equivalent to calling:
+ * <code>q.subtract(p).getNormInf()</code> except that no intermediate
+ * vector is built</p>
+ * @param v second vector
+ * @return the distance between the instance and p according to the L<sub>∞</sub> norm
+ */
+ double distanceInf(Vector<S> v);
+
+ /** Compute the square of the distance between the instance and another vector.
+ * <p>Calling this method is equivalent to calling:
+ * <code>q.subtract(p).getNormSq()</code> except that no intermediate
+ * vector is built</p>
+ * @param v second vector
+ * @return the square of the distance between the instance and p
+ */
+ double distanceSq(Vector<S> v);
+
+ /** Compute the dot-product of the instance and another vector.
+ * @param v second vector
+ * @return the dot product this.v
+ */
+ double dotProduct(Vector<S> v);
+
+ /** Get a string representation of this vector.
+ * @param format the custom format for components
+ * @return a string representation of this vector
+ */
+ String toString(final NumberFormat format);
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/package-info.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/package-info.java
new file mode 100644
index 0000000..a45caa8
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * <p>
+ * This package is the top level package for geometry. It provides only a few interfaces
+ * related to vectorial/affine spaces that are implemented in sub-packages.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.core;
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
new file mode 100644
index 0000000..bc23114
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java
@@ -0,0 +1,545 @@
+/*
+ * 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.Space;
+import org.apache.commons.geometry.core.Vector;
+
+/** Abstract class for all regions, independently of geometry type or dimension.
+
+ * @param <S> Type of the space.
+ * @param <T> Type of the sub-space.
+ */
+public abstract class AbstractRegion<S extends Space, T extends Space> implements Region<S> {
+
+ /** Inside/Outside BSP tree. */
+ private BSPTree<S> tree;
+
+ /** Tolerance below which points are considered to belong to hyperplanes. */
+ private final double tolerance;
+
+ /** Size of the instance. */
+ private double size;
+
+ /** Barycenter. */
+ private Point<S> barycenter;
+
+ /** Build a region representing the whole space.
+ * @param tolerance tolerance below which points are considered identical.
+ */
+ protected AbstractRegion(final double tolerance) {
+ this.tree = new BSPTree<>(Boolean.TRUE);
+ this.tolerance = tolerance;
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical.
+ */
+ protected AbstractRegion(final BSPTree<S> tree, final double tolerance) {
+ this.tree = tree;
+ this.tolerance = tolerance;
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical.
+ */
+ protected AbstractRegion(final Collection<SubHyperplane<S>> boundary, final double tolerance) {
+
+ this.tolerance = tolerance;
+
+ 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<S>> ordered = new TreeSet<>(new Comparator<SubHyperplane<S>>() {
+ /** {@inheritDoc} */
+ @Override
+ public int compare(final SubHyperplane<S> o1, final SubHyperplane<S> 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<S>() {
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(final BSPTree<S> node) {
+ return Order.PLUS_SUB_MINUS;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitInternalNode(final BSPTree<S> node) {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitLeafNode(final BSPTree<S> 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 tolerance tolerance below which points are considered identical.
+ */
+ public AbstractRegion(final Hyperplane<S>[] hyperplanes, final double tolerance) {
+ this.tolerance = tolerance;
+ 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<S> node = tree;
+ node.setAttribute(Boolean.TRUE);
+ for (final Hyperplane<S> 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<S, T> buildNew(BSPTree<S> newTree);
+
+ /** Get the tolerance below which points are considered to belong to hyperplanes.
+ * @return tolerance below which points are considered to belong to hyperplanes
+ */
+ public double getTolerance() {
+ return tolerance;
+ }
+
+ /** 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<S> node, final Collection<SubHyperplane<S>> boundary) {
+
+ final Iterator<SubHyperplane<S>> iterator = boundary.iterator();
+
+ // build the current level
+ Hyperplane<S> 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<S>> plusList = new ArrayList<>();
+ final ArrayList<SubHyperplane<S>> minusList = new ArrayList<>();
+ while (iterator.hasNext()) {
+ final SubHyperplane<S> other = iterator.next();
+ final SubHyperplane.SplitSubHyperplane<S> 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<S, T> copySelf() {
+ return buildNew(tree.copySelf());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ return isEmpty(tree);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty(final BSPTree<S> 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<S> 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<S> region) {
+ return new RegionFactory<S>().difference(region, this).isEmpty();
+ }
+
+ /** {@inheritDoc}
+ */
+ @Override
+ public BoundaryProjection<S> projectToBoundary(final Point<S> point) {
+ final BoundaryProjector<S, T> projector = new BoundaryProjector<>(point);
+ getTree(true).visit(projector);
+ return projector.getProjection();
+ }
+
+ /** Check a point with respect to the region.
+ * @param point point to check
+ * @return a code representing the point status: either {@link
+ * Region.Location#INSIDE}, {@link Region.Location#OUTSIDE} or
+ * {@link Region.Location#BOUNDARY}
+ */
+// public Location checkPoint(final Vector<S> point) {
+// return checkPoint((Point<S>) point);
+// }
+
+ /** {@inheritDoc} */
+ @Override
+ public Location checkPoint(final Point<S> 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<S> node, final Vector<S> point) {
+ return checkPoint(node, (Point<S>) 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<S> node, final Point<S> point) {
+ final BSPTree<S> cell = node.getCell(point, tolerance);
+ 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<S> getTree(final boolean includeBoundaryAttributes) {
+ if (includeBoundaryAttributes && (tree.getCut() != null) && (tree.getAttribute() == null)) {
+ // compute the boundary attributes
+ tree.visit(new BoundaryBuilder<S>());
+ }
+ return tree;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getBoundarySize() {
+ final BoundarySizeVisitor<S> 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 Point<S> getBarycenter() {
+ if (barycenter == null) {
+ computeGeometricalProperties();
+ }
+ return barycenter;
+ }
+
+ /** Set the barycenter of the instance.
+ * @param barycenter barycenter of the instance
+ */
+ protected void setBarycenter(final Vector<S> barycenter) {
+ setBarycenter((Point<S>) barycenter);
+ }
+
+ /** Set the barycenter of the instance.
+ * @param barycenter barycenter of the instance
+ */
+ protected void setBarycenter(final Point<S> 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<S> intersection(final SubHyperplane<S> 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<S> recurseIntersection(final BSPTree<S> node, final SubHyperplane<S> sub) {
+
+ if (node.getCut() == null) {
+ return (Boolean) node.getAttribute() ? sub.copySelf() : null;
+ }
+
+ final Hyperplane<S> hyperplane = node.getCut().getHyperplane();
+ final SubHyperplane.SplitSubHyperplane<S> split = sub.split(hyperplane);
+ if (split.getPlus() != null) {
+ if (split.getMinus() != null) {
+ // both sides
+ final SubHyperplane<S> plus = recurseIntersection(node.getPlus(), split.getPlus());
+ final SubHyperplane<S> 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<S, T> applyTransform(final Transform<S, T> transform) {
+
+ // transform the tree, except for boundary attribute splitters
+ final Map<BSPTree<S>, BSPTree<S>> map = new HashMap<>();
+ final BSPTree<S> transformedTree = recurseTransform(getTree(false), 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(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<S> recurseTransform(final BSPTree<S> node, final Transform<S, T> transform,
+ final Map<BSPTree<S>, BSPTree<S>> map) {
+
+ final BSPTree<S> transformedNode;
+ if (node.getCut() == null) {
+ transformedNode = new BSPTree<>(node.getAttribute());
+ } else {
+
+ final SubHyperplane<S> sub = node.getCut();
+ final SubHyperplane<S> tSub = ((AbstractSubHyperplane<S, T>) sub).applyTransform(transform);
+ BoundaryAttribute<S> attribute = (BoundaryAttribute<S>) node.getAttribute();
+ if (attribute != null) {
+ final SubHyperplane<S> tPO = (attribute.getPlusOutside() == null) ?
+ null : ((AbstractSubHyperplane<S, T>) attribute.getPlusOutside()).applyTransform(transform);
+ final SubHyperplane<S> tPI = (attribute.getPlusInside() == null) ?
+ null : ((AbstractSubHyperplane<S, T>) 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<S>());
+ }
+
+ 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
new file mode 100644
index 0000000..08d885e
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java
@@ -0,0 +1,189 @@
+/*
+ * 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.Space;
+
+/** 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 <S> Type of the embedding space.
+ * @param <T> Type of the embedded sub-space.
+ */
+public abstract class AbstractSubHyperplane<S extends Space, T extends Space>
+ implements SubHyperplane<S> {
+
+ /** Underlying hyperplane. */
+ private final Hyperplane<S> hyperplane;
+
+ /** Remaining region of the hyperplane. */
+ private final Region<T> 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<S> hyperplane,
+ final Region<T> 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<S, T> buildNew(final Hyperplane<S> hyper,
+ final Region<T> remaining);
+
+ /** {@inheritDoc} */
+ @Override
+ public AbstractSubHyperplane<S, T> copySelf() {
+ return buildNew(hyperplane.copySelf(), remainingRegion);
+ }
+
+ /** Get the underlying hyperplane.
+ * @return underlying hyperplane
+ */
+ @Override
+ public Hyperplane<S> 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<T> getRemainingRegion() {
+ return remainingRegion;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return remainingRegion.getSize();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AbstractSubHyperplane<S, T> reunite(final SubHyperplane<S> other) {
+ @SuppressWarnings("unchecked")
+ AbstractSubHyperplane<S, T> o = (AbstractSubHyperplane<S, T>) other;
+ return buildNew(hyperplane,
+ new RegionFactory<T>().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<S, T> applyTransform(final Transform<S, T> transform) {
+ final Hyperplane<S> tHyperplane = transform.apply(hyperplane);
+
+ // transform the tree, except for boundary attribute splitters
+ final Map<BSPTree<T>, BSPTree<T>> map = new HashMap<>();
+ final BSPTree<T> tTree =
+ recurseTransform(remainingRegion.getTree(false), tHyperplane, transform, map);
+
+ // set up the boundary attributes splitters
+ for (final Map.Entry<BSPTree<T>, BSPTree<T>> entry : map.entrySet()) {
+ if (entry.getKey().getCut() != null) {
+ @SuppressWarnings("unchecked")
+ BoundaryAttribute<T> original = (BoundaryAttribute<T>) entry.getKey().getAttribute();
+ if (original != null) {
+ @SuppressWarnings("unchecked")
+ BoundaryAttribute<T> transformed = (BoundaryAttribute<T>) entry.getValue().getAttribute();
+ for (final BSPTree<T> 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<T> recurseTransform(final BSPTree<T> node,
+ final Hyperplane<S> transformed,
+ final Transform<S, T> transform,
+ final Map<BSPTree<T>, BSPTree<T>> map) {
+
+ final BSPTree<T> transformedNode;
+ if (node.getCut() == null) {
+ transformedNode = new BSPTree<>(node.getAttribute());
+ } else {
+
+ @SuppressWarnings("unchecked")
+ BoundaryAttribute<T> attribute = (BoundaryAttribute<T>) node.getAttribute();
+ if (attribute != null) {
+ final SubHyperplane<T> tPO = (attribute.getPlusOutside() == null) ?
+ null : transform.apply(attribute.getPlusOutside(), hyperplane, transformed);
+ final SubHyperplane<T> 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<T>());
+ }
+
+ 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<S> split(Hyperplane<S> 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
new file mode 100644
index 0000000..cd57774
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java
@@ -0,0 +1,775 @@
+/*
+ * 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.Space;
+
+/** 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 <S> Type of the space.
+ */
+public class BSPTree<S extends Space> {
+
+ /** Cut sub-hyperplane. */
+ private SubHyperplane<S> cut;
+
+ /** Tree at the plus side of the cut hyperplane. */
+ private BSPTree<S> plus;
+
+ /** Tree at the minus side of the cut hyperplane. */
+ private BSPTree<S> minus;
+
+ /** Parent tree. */
+ private BSPTree<S> 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<S> cut, final BSPTree<S> plus, final BSPTree<S> 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<S> hyperplane) {
+
+ if (cut != null) {
+ plus.parent = null;
+ minus.parent = null;
+ }
+
+ final SubHyperplane<S> 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<S> 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<S> 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<S> 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<S> getMinus() {
+ return minus;
+ }
+
+ /** Get the parent node.
+ * @return parent node, null if the node has no parents
+ */
+ public BSPTree<S> 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<S> visitor) {
+ if (cut == null) {
+ visitor.visitLeafNode(this);
+ } else {
+ switch (visitor.visitOrder(this)) {
+ 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;
+ }
+
+ }
+ }
+
+ /** 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<S> fitToCell(final SubHyperplane<S> sub) {
+ SubHyperplane<S> s = sub;
+ for (BSPTree<S> 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 tolerance tolerance below 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<S> getCell(final Point<S> point, final double tolerance) {
+
+ if (cut == null) {
+ return this;
+ }
+
+ // position of the point with respect to the cut hyperplane
+ final double offset = cut.getHyperplane().getOffset(point);
+
+ if (Math.abs(offset) < tolerance) {
+ return this;
+ } else if (offset <= 0) {
+ // point is on the minus side of the cut hyperplane
+ return minus.getCell(point, tolerance);
+ } else {
+ // point is on the plus side of the cut hyperplane
+ return plus.getCell(point, tolerance);
+ }
+
+ }
+
+ /** 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<S>> getCloseCuts(final Point<S> point, final double maxOffset) {
+ final List<BSPTree<S>> 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 Point<S> point, final double maxOffset,
+ final List<BSPTree<S>> 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<S> merge(final BSPTree<S> tree, final LeafMerger<S> 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<S> merge(final BSPTree<S> tree, final LeafMerger<S> leafMerger,
+ final BSPTree<S> 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<S> 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 Space> {
+
+ /** 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 Space> {
+
+ /** 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<S> split(final SubHyperplane<S> sub) {
+
+ if (cut == null) {
+ return new BSPTree<>(sub, copySelf(), new BSPTree<S>(attribute), null);
+ }
+
+ final Hyperplane<S> cHyperplane = cut.getHyperplane();
+ final Hyperplane<S> sHyperplane = sub.getHyperplane();
+ final SubHyperplane.SplitSubHyperplane<S> subParts = sub.split(cHyperplane);
+ switch (subParts.getSide()) {
+ case PLUS :
+ { // the partitioning sub-hyperplane is entirely in the plus sub-tree
+ final BSPTree<S> 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<S> 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<S> cutParts = cut.split(sHyperplane);
+ final BSPTree<S> 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<S> 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<S> parentTree, final boolean isPlusChild,
+ final VanishingCutHandler<S> 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<S> tree = this; tree.parent != null; tree = tree.parent) {
+
+ // this is an hyperplane of some parent node
+ final Hyperplane<S> 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<S> 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<S> pruneAroundConvexCell(final Object cellAttribute,
+ final Object otherLeafsAttributes,
+ final Object internalAttributes) {
+
+ // build the current cell leaf
+ BSPTree<S> tree = new BSPTree<>(cellAttribute);
+
+ // build the pruned tree bottom-up
+ for (BSPTree<S> current = this; current.parent != null; current = current.parent) {
+ final SubHyperplane<S> parentCut = current.parent.cut.copySelf();
+ final BSPTree<S> 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<S> hyperplane, final VanishingCutHandler<S> 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<S> 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<S> hyperplane, final VanishingCutHandler<S> 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<S> 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
new file mode 100644
index 0000000..f7bbdbb
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java
@@ -0,0 +1,112 @@
+/*
+ * 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.Space;
+
+/** 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 <S> Type of the space.
+
+ * @see BSPTree
+ * @see SubHyperplane
+ */
+public interface BSPTreeVisitor<S extends Space> {
+
+ /** 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<S> 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<S> 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<S> 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
new file mode 100644
index 0000000..ad6a365
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.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;
+
+import org.apache.commons.geometry.core.Space;
+
+/** 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 <S> Type of the space.
+ * @see Region#getTree
+ */
+public class BoundaryAttribute<S extends Space> {
+
+ /** 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<S> 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<S> plusInside;
+
+ /** Sub-hyperplanes that were used to split the boundary part. */
+ private final NodesSet<S> 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<S> plusOutside,
+ final SubHyperplane<S> plusInside,
+ final NodesSet<S> 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<S> 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<S> 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<S> 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
new file mode 100644
index 0000000..816d3c2
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.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;
+
+import org.apache.commons.geometry.core.Space;
+
+/** Visitor building boundary shell tree.
+ * <p>
+ * The boundary shell is represented as {@link BoundaryAttribute boundary attributes}
+ * at each internal node.
+ * </p>
+ * @param <S> Type of the space.
+ */
+class BoundaryBuilder<S extends Space> implements BSPTreeVisitor<S> {
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(BSPTree<S> node) {
+ return Order.PLUS_MINUS_SUB;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitInternalNode(BSPTree<S> node) {
+
+ SubHyperplane<S> plusOutside = null;
+ SubHyperplane<S> plusInside = null;
+ NodesSet<S> splitters = null;
+
+ // characterize the cut sub-hyperplane,
+ // first with respect to the plus sub-tree
+ final Characterization<S> 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<S> 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<S> 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<S> 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<S> 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
new file mode 100644
index 0000000..1d5254d
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.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.partitioning;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** 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 <S> Type of the space.
+ * @see AbstractRegion#projectToBoundary(Point)
+ */
+public class BoundaryProjection<S extends Space> {
+
+ /** Original point. */
+ private final Point<S> original;
+
+ /** Projected point. */
+ private final Point<S> 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 Point<S> original, final Point<S> projected, final double offset) {
+ this.original = original;
+ this.projected = projected;
+ this.offset = offset;
+ }
+
+ /** Get the original point.
+ * @return original point
+ */
+ public Point<S> getOriginal() {
+ return original;
+ }
+
+ /** Projected point.
+ * @return projected point, or null if there are no boundary
+ */
+ public Point<S> 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
new file mode 100644
index 0000000..390695c
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java
@@ -0,0 +1,201 @@
+/*
+ * 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.Space;
+import org.apache.commons.geometry.core.partitioning.Region.Location;
+
+/** Local tree visitor to compute projection on boundary.
+ * @param <S> Type of the space.
+ * @param <T> Type of the sub-space.
+ */
+class BoundaryProjector<S extends Space, T extends Space> implements BSPTreeVisitor<S> {
+
+ /** Original point. */
+ private final Point<S> original;
+
+ /** Current best projected point. */
+ private Point<S> projected;
+
+ /** Leaf node closest to the test point. */
+ private BSPTree<S> leaf;
+
+ /** Current offset. */
+ private double offset;
+
+ /** Simple constructor.
+ * @param original original point
+ */
+ BoundaryProjector(final Point<S> original) {
+ this.original = original;
+ this.projected = null;
+ this.leaf = null;
+ this.offset = Double.POSITIVE_INFINITY;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(final BSPTree<S> 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<S> node) {
+
+ // project the point on the cut sub-hyperplane
+ final Hyperplane<S> hyperplane = node.getCut().getHyperplane();
+ final double signedOffset = hyperplane.getOffset(original);
+ if (Math.abs(signedOffset) < offset) {
+
+ // project point
+ final Point<S> regular = hyperplane.project(original);
+
+ // get boundary parts
+ final List<Region<T>> boundaryParts = boundaryRegions(node);
+
+ // check if regular projection really belongs to the boundary
+ boolean regularFound = false;
+ for (final Region<T> 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<T> part : boundaryParts) {
+ final Point<S> 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<S> 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<S> 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<T>> boundaryRegions(final BSPTree<S> node) {
+
+ final List<Region<T>> regions = new ArrayList<>(2);
+
+ @SuppressWarnings("unchecked")
+ final BoundaryAttribute<S> ba = (BoundaryAttribute<S>) 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<S> sub, final List<Region<T>> list) {
+ if (sub != null) {
+ @SuppressWarnings("unchecked")
+ final Region<T> region = ((AbstractSubHyperplane<S, T>) 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 Point<S> point, final Hyperplane<S> hyperplane,
+ final Region<T> part) {
+
+ // there is a non-null sub-space, we can dive into smaller dimensions
+ @SuppressWarnings("unchecked")
+ final Embedding<S, T> embedding = (Embedding<S, T>) 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 Point<S> singularProjection(final Point<S> point, final Hyperplane<S> hyperplane,
+ final Region<T> part) {
+
+ // there is a non-null sub-space, we can dive into smaller dimensions
+ @SuppressWarnings("unchecked")
+ final Embedding<S, T> embedding = (Embedding<S, T>) hyperplane;
+ final BoundaryProjection<T> 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
new file mode 100644
index 0000000..b305a36
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java
@@ -0,0 +1,67 @@
+/*
+ * 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.Space;
+
+/** Visitor computing the boundary size.
+ * @param <S> Type of the space.
+ */
+class BoundarySizeVisitor<S extends Space> implements BSPTreeVisitor<S> {
+
+ /** Size of the boundary. */
+ private double boundarySize;
+
+ /** Simple constructor.
+ */
+ BoundarySizeVisitor() {
+ boundarySize = 0;
+ }
+
+ /** {@inheritDoc}*/
+ @Override
+ public Order visitOrder(final BSPTree<S> node) {
+ return Order.MINUS_SUB_PLUS;
+ }
+
+ /** {@inheritDoc}*/
+ @Override
+ public void visitInternalNode(final BSPTree<S> node) {
+ @SuppressWarnings("unchecked")
+ final BoundaryAttribute<S> attribute =
+ (BoundaryAttribute<S>) 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<S> 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
new file mode 100644
index 0000000..7184c96
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java
@@ -0,0 +1,196 @@
+/*
+ * 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.Space;
+
+/** Cut sub-hyperplanes characterization with respect to inside/outside cells.
+ * @see BoundaryBuilder
+ * @param <S> Type of the space.
+ */
+class Characterization<S extends Space> {
+
+ /** Part of the cut sub-hyperplane that touch outside cells. */
+ private SubHyperplane<S> outsideTouching;
+
+ /** Part of the cut sub-hyperplane that touch inside cells. */
+ private SubHyperplane<S> insideTouching;
+
+ /** Nodes that were used to split the outside touching part. */
+ private final NodesSet<S> outsideSplitters;
+
+ /** Nodes that were used to split the outside touching part. */
+ private final NodesSet<S> 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<S> node, final SubHyperplane<S> sub) {
+ outsideTouching = null;
+ insideTouching = null;
+ outsideSplitters = new NodesSet<>();
+ insideSplitters = new NodesSet<>();
+ characterize(node, sub, new ArrayList<BSPTree<S>>());
+ }
+
+ /** 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<S> node, final SubHyperplane<S> sub,
+ final List<BSPTree<S>> 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<S> hyperplane = node.getCut().getHyperplane();
+ final SubHyperplane.SplitSubHyperplane<S> 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<S> sub,
+ final List<BSPTree<S>> 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<S> sub,
+ final List<BSPTree<S>> 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<S> 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<S> 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<S> 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<S> getInsideSplitters() {
+ return insideSplitters;
+ }
+
+}
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
new file mode 100644
index 0000000..7ed9ef5
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java
@@ -0,0 +1,67 @@
+/*
+ * 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.Space;
+
+/** 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. As an example, {@link
+ * org.apache.commons.geometry.euclidean.threed.Line Line} in 3D
+ * implements Embedding<{@link
+ * org.apache.commons.geometry.euclidean.threed.Cartesian3D Cartesian3D}, {@link
+ * org.apache.commons.geometry.euclidean.oned.Cartesian1D Cartesian1D}>, i.e. it
+ * maps directly dimensions 3 and 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 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 <S> Type of the embedding space.
+ * @param <T> Type of the embedded sub-space.
+
+ * @see Hyperplane
+ */
+public interface Embedding<S extends Space, T extends Space> {
+
+ /** 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
+ */
+ Point<T> toSubSpace(Point<S> 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
+ */
+ Point<S> toSpace(Point<T> point);
+
+}
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
new file mode 100644
index 0000000..8041658
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
@@ -0,0 +1,94 @@
+/*
+ * 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.Space;
+
+/** 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 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 <S> Type of the space.
+ */
+public interface Hyperplane<S extends Space> {
+
+ /** 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
+ */
+ Hyperplane<S> copySelf();
+
+ /** 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
+ */
+ double getOffset(Point<S> point);
+
+ /** Project a point to the hyperplane.
+ * @param point point to project
+ * @return projected point
+ */
+ Point<S> project(Point<S> point);
+
+ /** Get the tolerance below which points are considered to belong to the hyperplane.
+ * @return tolerance below which points are considered to belong to the hyperplane
+ */
+ double getTolerance();
+
+ /** 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
+ */
+ boolean sameOrientationAs(Hyperplane<S> other);
+
+ /** Build a sub-hyperplane covering the whole hyperplane.
+ * @return a sub-hyperplane covering the whole hyperplane
+ */
+ SubHyperplane<S> wholeHyperplane();
+
+ /** Build a region covering the whole space.
+ * @return a region containing the instance
+ */
+ Region<S> wholeSpace();
+
+}
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
new file mode 100644
index 0000000..aec8a41
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java
@@ -0,0 +1,149 @@
+/*
+ * 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.Space;
+
+/** Utility class checking if inside nodes can be found
+ * on the plus and minus sides of an hyperplane.
+ * @param <S> Type of the space.
+ */
+class InsideFinder<S extends Space> {
+
+ /** Region on which to operate. */
+ private final Region<S> 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<S> 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<S> node, final SubHyperplane<S> 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<S> hyperplane = node.getCut().getHyperplane();
+ final SubHyperplane.SplitSubHyperplane<S> 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
new file mode 100644
index 0000000..20ef6b7
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java
@@ -0,0 +1,72 @@
+/*
+ * 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.Space;
+
+/** Set of {@link BSPTree BSP tree} nodes.
+ * @see BoundaryAttribute
+ * @param <S> Type of the space.
+ */
+public class NodesSet<S extends Space> implements Iterable<BSPTree<S>> {
+
+ /** List of sub-hyperplanes. */
+ private final List<BSPTree<S>> 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<S> node) {
+
+ for (final BSPTree<S> 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<S>> iterator) {
+ for (final BSPTree<S> node : iterator) {
+ add(node);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterator<BSPTree<S>> 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
new file mode 100644
index 0000000..63155a5
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java
@@ -0,0 +1,205 @@
+/*
+ * 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.Space;
+
+/** 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 <S> Type of the space.
+ */
+public interface Region<S extends Space> {
+
+ /** 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<S> buildNew(BSPTree<S> 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<S> 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<S> 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<S> 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<S> 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 Point<S> point);
+
+ /** Project a point on the boundary of the region.
+ * @param point point to check
+ * @return projection of the point on the boundary
+ */
+ BoundaryProjection<S> projectToBoundary(final Point<S> 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<S> 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
+ */
+ Point<S> 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<S> intersection(final SubHyperplane<S> 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
new file mode 100644
index 0000000..61a888a
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java
@@ -0,0 +1,384 @@
+/*
+ * 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.Space;
+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 <S> Type of the space.
+ */
+public class RegionFactory<S extends Space> {
+
+ /** 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<S> buildConvex(final Hyperplane<S> ... hyperplanes) {
+ if ((hyperplanes == null) || (hyperplanes.length == 0)) {
+ return null;
+ }
+
+ // use the first hyperplane to build the right class
+ final Region<S> region = hyperplanes[0].wholeSpace();
+
+ // chop off parts of the space
+ BSPTree<S> node = region.getTree(false);
+ node.setAttribute(Boolean.TRUE);
+ for (final Hyperplane<S> 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<S> s = hyperplane.wholeHyperplane();
+ for (BSPTree<S> tree = node; tree.getParent() != null && s != null; tree = tree.getParent()) {
+ final Hyperplane<S> other = tree.getParent().getCut().getHyperplane();
+ final SplitSubHyperplane<S> 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<S> union(final Region<S> region1, final Region<S> region2) {
+ final BSPTree<S> 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<S> intersection(final Region<S> region1, final Region<S> region2) {
+ final BSPTree<S> 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<S> xor(final Region<S> region1, final Region<S> region2) {
+ final BSPTree<S> 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<S> difference(final Region<S> region1, final Region<S> region2) {
+ final BSPTree<S> 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<S> getComplement(final Region<S> 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<S> recurseComplement(final BSPTree<S> node) {
+
+ // transform the tree, except for boundary attribute splitters
+ final Map<BSPTree<S>, BSPTree<S>> map = new HashMap<>();
+ final BSPTree<S> transformedTree = recurseComplement(node, 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 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<S> recurseComplement(final BSPTree<S> node,
+ final Map<BSPTree<S>, BSPTree<S>> map) {
+
+ final BSPTree<S> transformedNode;
+ if (node.getCut() == null) {
+ transformedNode = new BSPTree<>(((Boolean) node.getAttribute()) ? Boolean.FALSE : Boolean.TRUE);
+ } else {
+
+ @SuppressWarnings("unchecked")
+ BoundaryAttribute<S> attribute = (BoundaryAttribute<S>) node.getAttribute();
+ if (attribute != null) {
+ final SubHyperplane<S> plusOutside =
+ (attribute.getPlusInside() == null) ? null : attribute.getPlusInside().copySelf();
+ final SubHyperplane<S> 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<S>());
+ }
+
+ 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<S> {
+ /** {@inheritDoc} */
+ @Override
+ public BSPTree<S> merge(final BSPTree<S> leaf, final BSPTree<S> tree,
+ final BSPTree<S> 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<S> {
+ /** {@inheritDoc} */
+ @Override
+ public BSPTree<S> merge(final BSPTree<S> leaf, final BSPTree<S> tree,
+ final BSPTree<S> 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<S> {
+ /** {@inheritDoc} */
+ @Override
+ public BSPTree<S> merge(final BSPTree<S> leaf, final BSPTree<S> tree,
+ final BSPTree<S> parentTree, final boolean isPlusChild,
+ final boolean leafFromInstance) {
+ BSPTree<S> 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<S>, VanishingCutHandler<S> {
+
+ /** Region to subtract from. */
+ private final Region<S> region1;
+
+ /** Region to subtract. */
+ private final Region<S> region2;
+
+ /** Simple constructor.
+ * @param region1 region to subtract from
+ * @param region2 region to subtract
+ */
+ DifferenceMerger(final Region<S> region1, final Region<S> region2) {
+ this.region1 = region1.copySelf();
+ this.region2 = region2.copySelf();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public BSPTree<S> merge(final BSPTree<S> leaf, final BSPTree<S> tree,
+ final BSPTree<S> parentTree, final boolean isPlusChild,
+ final boolean leafFromInstance) {
+ if ((Boolean) leaf.getAttribute()) {
+ // the leaf node represents an inside cell
+ final BSPTree<S> argTree =
+ recurseComplement(leafFromInstance ? tree : leaf);
+ argTree.insertInTree(parentTree, isPlusChild, this);
+ return argTree;
+ }
+ // the leaf node represents an outside cell
+ final BSPTree<S> instanceTree =
+ leafFromInstance ? leaf : tree;
+ instanceTree.insertInTree(parentTree, isPlusChild, this);
+ return instanceTree;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public BSPTree<S> fixNode(final BSPTree<S> node) {
+ // get a representative point in the degenerate cell
+ final BSPTree<S> cell = node.pruneAroundConvexCell(Boolean.TRUE, Boolean.FALSE, null);
+ final Region<S> r = region1.buildNew(cell);
+ final Point<S> 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<S> {
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(final BSPTree<S> node) {
+ return Order.PLUS_SUB_MINUS;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitInternalNode(final BSPTree<S> node) {
+ node.setAttribute(null);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitLeafNode(final BSPTree<S> node) {
+ }
+
+ }
+
+ /** Handler replacing nodes with vanishing cuts with leaf nodes. */
+ private class VanishingToLeaf implements VanishingCutHandler<S> {
+
+ /** 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<S> fixNode(final BSPTree<S> 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/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
new file mode 100644
index 0000000..046defe
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/** Enumerate representing the location of an element with respect to an
+ * {@link Hyperplane hyperplane} of a space.
+ */
+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;
+
+}
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
new file mode 100644
index 0000000..da8b24d
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.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;
+
+import org.apache.commons.geometry.core.Space;
+
+/** This interface represents the remaining parts of an hyperplane after
+ * other parts have been chopped off.
+
+ * <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 <S> Type of the embedding space.
+ */
+public interface SubHyperplane<S extends Space> {
+
+ /** 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<S> copySelf();
+
+ /** Get the underlying hyperplane.
+ * @return underlying hyperplane
+ */
+ Hyperplane<S> getHyperplane();
+
+ /** Check if the instance is empty.
+ * @return true if the instance is empty
+ */
+ 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 ...)
+ */
+ 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
+ */
+ SplitSubHyperplane<S> split(Hyperplane<S> hyperplane);
+
+ /** 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
+ */
+ SubHyperplane<S> reunite(SubHyperplane<S> other);
+
+ /** Class holding the results of the {@link #split split} method.
+ * @param <U> Type of the embedding space.
+ */
+ class SplitSubHyperplane<U extends Space> {
+
+ /** 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;
+ }
+ }
+
+ }
+
+}
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
new file mode 100644
index 0000000..a034d6c
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java
@@ -0,0 +1,78 @@
+/*
+ * 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.Space;
+
+
+/** 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 <S> Type of the embedding space.
+ * @param <T> Type of the embedded sub-space.
+ */
+public interface Transform<S extends Space, T extends Space> {
+
+ /** Transform a point of a space.
+ * @param point point to transform
+ * @return a new object representing the transformed point
+ */
+ Point<S> apply(Point<S> point);
+
+ /** Transform an hyperplane of a space.
+ * @param hyperplane hyperplane to transform
+ * @return a new object representing the transformed hyperplane
+ */
+ Hyperplane<S> apply(Hyperplane<S> 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<T> apply(SubHyperplane<T> sub, Hyperplane<S> original, Hyperplane<S> transformed);
+
+}
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
new file mode 100644
index 0000000..c1a1208
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * This package provides classes to implement Binary Space Partition trees.
+ *
+ * <p>
+ * {@link org.apache.commons.geometry.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).
+ * </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.partitioning.Region Region} class is
+ * devoted to this representation, it is build on top of the {@link
+ * org.apache.commons.geometry.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/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
new file mode 100644
index 0000000..3dbf58a
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.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.core;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.junit.Assert;
+
+/** Class containing various geometry-related test utilities.
+ */
+public class GeometryTestUtils {
+
+ /** Asserts that the given value is positive infinity.
+ * @param value
+ */
+ public static void assertPositiveInfinity(double value) {
+ String msg = "Expected value to be positive infinity but was " + value;
+ Assert.assertTrue(msg, Double.isInfinite(value));
+ Assert.assertTrue(msg, value > 0);
+ }
+
+ /** Asserts that the given value is negative infinity..
+ * @param value
+ */
+ public static void assertNegativeInfinity(double value) {
+ String msg = "Expected value to be negative infinity but was " + value;
+ Assert.assertTrue(msg, Double.isInfinite(value));
+ Assert.assertTrue(msg, value < 0);
+ }
+
+ /**
+ * Serializes and then recovers an object from a byte array. Returns the deserialized object.
+ *
+ * @param obj object to serialize and recover
+ * @return the recovered, deserialized object
+ */
+ public static Object serializeAndRecover(Object obj) {
+ try {
+ // serialize the Object
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream so = new ObjectOutputStream(bos);
+ so.writeObject(obj);
+
+ // deserialize the Object
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ ObjectInputStream si = new ObjectInputStream(bis);
+ return si.readObject();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
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
new file mode 100644
index 0000000..364d702
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeBuilder.java
@@ -0,0 +1,164 @@
+/*
+ * 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.IOException;
+import java.text.ParseException;
+import java.util.StringTokenizer;
+
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.partitioning.AbstractRegion;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+
+/** Local class for building an {@link AbstractRegion} tree.
+ * @param <S> Type of the space.
+ */
+public abstract class TreeBuilder<S extends Space> {
+
+ /** Keyword for tolerance. */
+ private static final String TOLERANCE = "tolerance";
+
+ /** 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<S> root;
+
+ /** Tolerance. */
+ private final double tolerance;
+
+ /** Tokenizer parsing string representation. */
+ private final StringTokenizer tokenizer;
+
+ /** Simple constructor.
+ * @param type type of the expected representation
+ * @param reader reader for the string representation
+ * @exception IOException if the string cannot be read
+ * @exception ParseException if the string cannot be parsed
+ */
+ public TreeBuilder(final String type, final String s)
+ throws IOException, ParseException {
+ root = null;
+ tokenizer = new StringTokenizer(s);
+ getWord(type);
+ getWord(TOLERANCE);
+ tolerance = getNumber();
+ 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 IOException if the string cannot be read
+ * @exception ParseException if the string cannot be parsed
+ */
+ private void parseTree(final BSPTree<S> node)
+ throws IOException, 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 IOException if the string cannot be read
+ * @exception ParseException if the string cannot be parsed
+ */
+ protected String getWord(final String ... allowed)
+ throws IOException, 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 IOException if the string cannot be read
+ * @exception NumberFormatException if the string cannot be parsed
+ */
+ protected double getNumber()
+ throws IOException, NumberFormatException {
+ return Double.parseDouble(tokenizer.nextToken());
+ }
+
+ /** Get next boolean.
+ * @return parsed boolean
+ * @exception IOException if the string cannot be read
+ * @exception ParseException if the string cannot be parsed
+ */
+ protected boolean getBoolean()
+ throws IOException, ParseException {
+ return getWord(TRUE, FALSE).equals(TRUE);
+ }
+
+ /** Get the built tree.
+ * @return built tree
+ */
+ public BSPTree<S> getTree() {
+ return root;
+ }
+
+ /** Get the tolerance.
+ * @return tolerance
+ */
+ public double getTolerance() {
+ return tolerance;
+ }
+
+ /** Parse an hyperplane.
+ * @return next hyperplane from the stream
+ * @exception IOException if the string cannot be read
+ * @exception ParseException if the string cannot be parsed
+ */
+ protected abstract Hyperplane<S> parseHyperplane()
+ throws IOException, 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
new file mode 100644
index 0000000..532b9f9
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeDumper.java
@@ -0,0 +1,106 @@
+/*
+ * 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.Space;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+
+/** Dumping visitor.
+ * @param <S> Type of the space.
+ */
+public abstract class TreeDumper<S extends Space> implements BSPTreeVisitor<S> {
+ /** 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
+ * @param tolerance tolerance of the region
+ */
+ public TreeDumper(final String type, final double tolerance) {
+ this.dump = new StringBuilder();
+ this.formatter = new Formatter(dump, Locale.US);
+ this.prefix = "";
+ formatter.format("%s%n", type);
+ formatter.format("tolerance %22.15e%n", tolerance);
+ }
+
+ /** 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<S> hyperplane);
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(final BSPTree<S> node) {
+ return Order.SUB_MINUS_PLUS;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitInternalNode(final BSPTree<S> 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<S> node) {
+ formatter.format("%s %s leaf %s%n",
+ prefix, type(node), node.getAttribute());
+ for (BSPTree<S> 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<S> 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
new file mode 100644
index 0000000..6f79537
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreePrinter.java
@@ -0,0 +1,137 @@
+/*
+ * 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.Space;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+
+/** Base for classes that create string representations of {@link BSPTree}s.
+ * @param <S>
+ */
+public abstract class TreePrinter<S extends Space> implements BSPTreeVisitor<S> {
+
+ /** 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<S> tree) {
+ output.delete(0, output.length());
+
+ tree.visit(this);
+
+ return output.toString();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(BSPTree<S> node) {
+ return Order.SUB_MINUS_PLUS;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitInternalNode(BSPTree<S> node) {
+ writeLinePrefix(node);
+ writeInternalNode(node);
+
+ write("\n");
+
+ ++depth;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitLeafNode(BSPTree<S> node) {
+ writeLinePrefix(node);
+ writeLeafNode(node);
+
+ write("\n");
+
+ BSPTree<S> 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<S> 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<S> 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<S> node);
+
+ /** Writes a leaf node. The default implementation here simply writes
+ * the node attribute as a string.
+ * @param node
+ */
+ protected void writeLeafNode(BSPTree<S> node) {
+ write(String.valueOf(node.getAttribute()));
+ }
+}
\ No newline at end of file
diff --git a/commons-geometry-euclidean-threed/pom.xml b/commons-geometry-enclosing/pom.xml
similarity index 52%
rename from commons-geometry-euclidean-threed/pom.xml
rename to commons-geometry-enclosing/pom.xml
index 613cac8..e1ccc81 100644
--- a/commons-geometry-euclidean-threed/pom.xml
+++ b/commons-geometry-enclosing/pom.xml
@@ -27,20 +27,54 @@
</parent>
<groupId>org.apache.commons</groupId>
- <artifactId>commons-geometry-euclidean-threed</artifactId>
+ <artifactId>commons-geometry-enclosing</artifactId>
<version>1.0-SNAPSHOT</version>
- <name>Apache Commons Geometry Three-Dimensional Euclidean Space</name>
+ <name>Apache Commons Geometry Enclosing</name>
- <description></description>
+ <description>Algorithms for computing enclosing balls.</description>
<properties>
<!-- OSGi -->
- <commons.osgi.symbolicName>org.apache.commons.geometry.euclidean.threed</commons.osgi.symbolicName>
- <commons.osgi.export>org.apache.commons.geometry.euclidean.threed</commons.osgi.export>
+ <commons.osgi.symbolicName>org.apache.commons.geometry.enclosing</commons.osgi.symbolicName>
+ <commons.osgi.export>org.apache.commons.geometry.enclosing</commons.osgi.export>
<!-- Java 9+ -->
- <commons.automatic.module.name>org.apache.commons.geometry.euclidean.threed</commons.automatic.module.name>
+ <commons.automatic.module.name>org.apache.commons.geometry.enclosing</commons.automatic.module.name>
<!-- Workaround to avoid duplicating config files. -->
<geometry.parent.dir>${basedir}/..</geometry.parent.dir>
</properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-geometry-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-geometry-euclidean</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-numbers-fraction</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-rng-client-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-rng-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-rng-sampling</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
</project>
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/Encloser.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/Encloser.java
new file mode 100644
index 0000000..a99023a
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/Encloser.java
@@ -0,0 +1,35 @@
+/*
+ * 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.enclosing;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** Interface for algorithms computing enclosing balls.
+ * @param <S> Space type.
+ * @param <P> Point type.
+ * @see EnclosingBall
+ */
+public interface Encloser<S extends Space, P extends Point<S>> {
+
+ /** Find a ball enclosing a list of points.
+ * @param points points to enclose
+ * @return enclosing ball
+ */
+ EnclosingBall<S, P> enclose(Iterable<P> points);
+
+}
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
new file mode 100644
index 0000000..b269747
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/EnclosingBall.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.enclosing;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** This class represents a ball enclosing some points.
+ * @param <S> Space type.
+ * @param <P> Point type.
+ * @see Space
+ * @see Point
+ * @see Encloser
+ */
+public class EnclosingBall<S extends Space, P extends Point<S>> implements Serializable {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20140126L;
+
+ /** Center of the ball. */
+ private final P center;
+
+ /** Radius of the ball. */
+ private final double radius;
+
+ /** Support points used to define the ball. */
+ private final P[] support;
+
+ /** Simple constructor.
+ * @param center center of the ball
+ * @param radius radius of the ball
+ * @param support support points used to define the ball
+ */
+ @SafeVarargs
+ public EnclosingBall(final P center, final double radius, final P ... support) {
+ this.center = center;
+ this.radius = radius;
+ this.support = support.clone();
+ }
+
+ /** Get the center of the ball.
+ * @return center of the ball
+ */
+ public P getCenter() {
+ return center;
+ }
+
+ /** Get the radius of the ball.
+ * @return radius of the ball (can be negative if the ball is empty)
+ */
+ public double getRadius() {
+ return radius;
+ }
+
+ /** Get the support points used to define the ball.
+ * @return support points used to define the ball
+ */
+ public P[] getSupport() {
+ return support.clone();
+ }
+
+ /** Get the number of support points used to define the ball.
+ * @return number of support points used to define the ball
+ */
+ public int getSupportSize() {
+ return support.length;
+ }
+
+ /** Check if a point is within the ball or at boundary.
+ * @param point point to test
+ * @return true if the point is within the ball or at boundary
+ */
+ public boolean contains(final P point) {
+ return point.distance(center) <= radius;
+ }
+
+ /** Check if a point is within an enlarged ball or at boundary.
+ * @param point point to test
+ * @param margin margin to consider
+ * @return true if the point is within the ball enlarged
+ * by the margin or at boundary
+ */
+ public boolean contains(final P point, final double margin) {
+ return point.distance(center) <= radius + margin;
+ }
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/SupportBallGenerator.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/SupportBallGenerator.java
new file mode 100644
index 0000000..53a9229
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/SupportBallGenerator.java
@@ -0,0 +1,41 @@
+/*
+ * 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.enclosing;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** Interface for generating balls based on support points.
+ * <p>
+ * This generator is used in the {@link WelzlEncloser Emo Welzl} algorithm
+ * and its derivatives.
+ * </p>
+ * @param <S> Space type.
+ * @param <P> Point type.
+ * @see EnclosingBall
+ */
+public interface SupportBallGenerator<S extends Space, P extends Point<S>> {
+
+ /** Create a ball whose boundary lies on prescribed support points.
+ * @param support support points (may be empty)
+ * @return ball whose boundary lies on the prescribed support points
+ */
+ EnclosingBall<S, P> ballOnSupport(List<P> support);
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/WelzlEncloser.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/WelzlEncloser.java
new file mode 100644
index 0000000..c20c706
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/WelzlEncloser.java
@@ -0,0 +1,180 @@
+/*
+ * 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.enclosing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** Class implementing Emo Welzl algorithm to find the smallest enclosing ball in linear time.
+ * <p>
+ * The class implements the algorithm described in paper <a
+ * href="http://www.inf.ethz.ch/personal/emo/PublFiles/SmallEnclDisk_LNCS555_91.pdf">Smallest
+ * Enclosing Disks (Balls and Ellipsoids)</a> by Emo Welzl, Lecture Notes in Computer Science
+ * 555 (1991) 359-370. The pivoting improvement published in the paper <a
+ * href="http://www.inf.ethz.ch/personal/gaertner/texts/own_work/esa99_final.pdf">Fast and
+ * Robust Smallest Enclosing Balls</a>, by Bernd Gärtner and further modified in
+ * paper <a
+ * href="http://www.idt.mdh.se/kurser/ct3340/ht12/MINICONFERENCE/FinalPapers/ircse12_submission_30.pdf">
+ * Efficient Computation of Smallest Enclosing Balls in Three Dimensions</a> by Linus Källberg
+ * to avoid performing local copies of data have been included.
+ * </p>
+ * @param <S> Space type.
+ * @param <P> Point type.
+ */
+public class WelzlEncloser<S extends Space, P extends Point<S>> implements Encloser<S, P> {
+
+ /** Tolerance below which points are consider to be identical. */
+ private final double tolerance;
+
+ /** Generator for balls on support. */
+ private final SupportBallGenerator<S, P> generator;
+
+ /** Simple constructor.
+ * @param tolerance below which points are consider to be identical
+ * @param generator generator for balls on support
+ */
+ public WelzlEncloser(final double tolerance, final SupportBallGenerator<S, P> generator) {
+ this.tolerance = tolerance;
+ this.generator = generator;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public EnclosingBall<S, P> enclose(final Iterable<P> points) {
+
+ if (points == null || !points.iterator().hasNext()) {
+ // return an empty ball
+ return generator.ballOnSupport(new ArrayList<P>());
+ }
+
+ // Emo Welzl algorithm with Bernd Gärtner and Linus Källberg improvements
+ return pivotingBall(points);
+
+ }
+
+ /** Compute enclosing ball using Gärtner's pivoting heuristic.
+ * @param points points to be enclosed
+ * @return enclosing ball
+ */
+ private EnclosingBall<S, P> pivotingBall(final Iterable<P> points) {
+
+ final P first = points.iterator().next();
+ final List<P> extreme = new ArrayList<>(first.getSpace().getDimension() + 1);
+ final List<P> support = new ArrayList<>(first.getSpace().getDimension() + 1);
+
+ // start with only first point selected as a candidate support
+ extreme.add(first);
+ EnclosingBall<S, P> ball = moveToFrontBall(extreme, extreme.size(), support);
+
+ while (true) {
+
+ // select the point farthest to current ball
+ final P farthest = selectFarthest(points, ball);
+
+ if (ball.contains(farthest, tolerance)) {
+ // we have found a ball containing all points
+ return ball;
+ }
+
+ // recurse search, restricted to the small subset containing support and farthest point
+ support.clear();
+ support.add(farthest);
+ EnclosingBall<S, P> savedBall = ball;
+ ball = moveToFrontBall(extreme, extreme.size(), support);
+ if (ball.getRadius() < savedBall.getRadius()) {
+ // this should never happen
+ throw new IllegalStateException("Please file a bug report");
+ }
+
+ // it was an interesting point, move it to the front
+ // according to Gärtner's heuristic
+ extreme.add(0, farthest);
+
+ // prune the least interesting points
+ extreme.subList(ball.getSupportSize(), extreme.size()).clear();
+
+
+ }
+ }
+
+ /** Compute enclosing ball using Welzl's move to front heuristic.
+ * @param extreme subset of extreme points
+ * @param nbExtreme number of extreme points to consider
+ * @param support points that must belong to the ball support
+ * @return enclosing ball, for the extreme subset only
+ */
+ private EnclosingBall<S, P> moveToFrontBall(final List<P> extreme, final int nbExtreme,
+ final List<P> support) {
+
+ // create a new ball on the prescribed support
+ EnclosingBall<S, P> ball = generator.ballOnSupport(support);
+
+ if (ball.getSupportSize() <= ball.getCenter().getSpace().getDimension()) {
+
+ for (int i = 0; i < nbExtreme; ++i) {
+ final P pi = extreme.get(i);
+ if (!ball.contains(pi, tolerance)) {
+
+ // we have found an outside point,
+ // enlarge the ball by adding it to the support
+ support.add(pi);
+ ball = moveToFrontBall(extreme, i, support);
+ support.remove(support.size() - 1);
+
+ // it was an interesting point, move it to the front
+ // according to Welzl's heuristic
+ for (int j = i; j > 0; --j) {
+ extreme.set(j, extreme.get(j - 1));
+ }
+ extreme.set(0, pi);
+
+ }
+ }
+
+ }
+
+ return ball;
+
+ }
+
+ /** Select the point farthest to the current ball.
+ * @param points points to be enclosed
+ * @param ball current ball
+ * @return farthest point
+ */
+ public P selectFarthest(final Iterable<P> points, final EnclosingBall<S, P> ball) {
+
+ final P center = ball.getCenter();
+ P farthest = null;
+ double dMax = -1.0;
+
+ for (final P point : points) {
+ final double d = point.distance(center);
+ if (d > dMax) {
+ farthest = point;
+ dMax = d;
+ }
+ }
+
+ return farthest;
+
+ }
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/package-info.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/package-info.java
new file mode 100644
index 0000000..7338211
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * <p>
+ * This package provides interfaces and classes related to the smallest enclosing ball problem.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.enclosing;
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
new file mode 100644
index 0000000..5bdc69c
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGenerator.java
@@ -0,0 +1,154 @@
+/*
+ * 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.enclosing;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.enclosing.SupportBallGenerator;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+import org.apache.commons.geometry.euclidean.threed.Euclidean3D;
+import org.apache.commons.geometry.euclidean.threed.Plane;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.enclosing.DiskGenerator;
+import org.apache.commons.numbers.fraction.BigFraction;
+
+/** Class generating an enclosing ball from its support points.
+ */
+public class SphereGenerator implements SupportBallGenerator<Euclidean3D, Cartesian3D> {
+
+ /** {@inheritDoc} */
+ @Override
+ public EnclosingBall<Euclidean3D, Cartesian3D> ballOnSupport(final List<Cartesian3D> support) {
+
+ if (support.size() < 1) {
+ return new EnclosingBall<>(Cartesian3D.ZERO, Double.NEGATIVE_INFINITY);
+ } else {
+ final Cartesian3D vA = support.get(0);
+ if (support.size() < 2) {
+ return new EnclosingBall<>(vA, 0, vA);
+ } else {
+ final Cartesian3D vB = support.get(1);
+ if (support.size() < 3) {
+ return new EnclosingBall<>(new Cartesian3D(0.5, vA, 0.5, vB),
+ 0.5 * vA.distance(vB),
+ vA, vB);
+ } else {
+ final Cartesian3D vC = support.get(2);
+ if (support.size() < 4) {
+
+ // delegate to 2D disk generator
+ final Plane p = new Plane(vA, vB, vC,
+ 1.0e-10 * (vA.getNorm1() + vB.getNorm1() + vC.getNorm1()));
+ final EnclosingBall<Euclidean2D, Cartesian2D> disk =
+ 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()),
+ disk.getRadius(), vA, vB, vC);
+
+ } else {
+ final Cartesian3D vD = support.get(3);
+ // a sphere is 3D can be defined as:
+ // (1) (x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = r^2
+ // which can be written:
+ // (2) (x^2 + y^2 + z^2) - 2 x_0 x - 2 y_0 y - 2 z_0 z + (x_0^2 + y_0^2 + z_0^2 - r^2) = 0
+ // or simply:
+ // (3) (x^2 + y^2 + z^2) + a x + b y + c z + d = 0
+ // with sphere center coordinates -a/2, -b/2, -c/2
+ // If the sphere exists, a b, c and d are a non zero solution to
+ // [ (x^2 + y^2 + z^2) x y z 1 ] [ 1 ] [ 0 ]
+ // [ (xA^2 + yA^2 + zA^2) xA yA zA 1 ] [ a ] [ 0 ]
+ // [ (xB^2 + yB^2 + zB^2) xB yB zB 1 ] * [ b ] = [ 0 ]
+ // [ (xC^2 + yC^2 + zC^2) xC yC zC 1 ] [ c ] [ 0 ]
+ // [ (xD^2 + yD^2 + zD^2) xD yD zD 1 ] [ d ] [ 0 ]
+ // So the determinant of the matrix is zero. Computing this determinant
+ // by expanding it using the minors m_ij of first row leads to
+ // (4) m_11 (x^2 + y^2 + z^2) - m_12 x + m_13 y - m_14 z + m_15 = 0
+ // So by identifying equations (2) and (4) we get the coordinates
+ // of center as:
+ // x_0 = +m_12 / (2 m_11)
+ // y_0 = -m_13 / (2 m_11)
+ // z_0 = +m_14 / (2 m_11)
+ // Note that the minors m_11, m_12, m_13 and m_14 all have the last column
+ // filled with 1.0, hence simplifying the computation
+ final BigFraction[] c2 = new BigFraction[] {
+ new BigFraction(vA.getX()), new BigFraction(vB.getX()),
+ new BigFraction(vC.getX()), new BigFraction(vD.getX())
+ };
+ final BigFraction[] c3 = new BigFraction[] {
+ new BigFraction(vA.getY()), new BigFraction(vB.getY()),
+ new BigFraction(vC.getY()), new BigFraction(vD.getY())
+ };
+ final BigFraction[] c4 = new BigFraction[] {
+ new BigFraction(vA.getZ()), new BigFraction(vB.getZ()),
+ new BigFraction(vC.getZ()), new BigFraction(vD.getZ())
+ };
+ final BigFraction[] c1 = new BigFraction[] {
+ c2[0].multiply(c2[0]).add(c3[0].multiply(c3[0])).add(c4[0].multiply(c4[0])),
+ c2[1].multiply(c2[1]).add(c3[1].multiply(c3[1])).add(c4[1].multiply(c4[1])),
+ c2[2].multiply(c2[2]).add(c3[2].multiply(c3[2])).add(c4[2].multiply(c4[2])),
+ c2[3].multiply(c2[3]).add(c3[3].multiply(c3[3])).add(c4[3].multiply(c4[3]))
+ };
+ final BigFraction twoM11 = minor(c2, c3, c4).multiply(2);
+ final BigFraction m12 = minor(c1, c3, c4);
+ final BigFraction m13 = minor(c1, c2, c4);
+ final BigFraction m14 = minor(c1, c2, c3);
+ final BigFraction centerX = m12.divide(twoM11);
+ final BigFraction centerY = m13.divide(twoM11).negate();
+ final BigFraction centerZ = m14.divide(twoM11);
+ final BigFraction dx = c2[0].subtract(centerX);
+ final BigFraction dy = c3[0].subtract(centerY);
+ final BigFraction dz = c4[0].subtract(centerZ);
+ final BigFraction r2 = dx.multiply(dx).add(dy.multiply(dy)).add(dz.multiply(dz));
+ return new EnclosingBall<>(new Cartesian3D(centerX.doubleValue(),
+ centerY.doubleValue(),
+ centerZ.doubleValue()),
+ Math.sqrt(r2.doubleValue()),
+ vA, vB, vC, vD);
+ }
+ }
+ }
+ }
+ }
+
+ /** Compute a dimension 4 minor, when 4<sup>th</sup> column is known to be filled with 1.0.
+ * @param c1 first column
+ * @param c2 second column
+ * @param c3 third column
+ * @return value of the minor computed has an exact fraction
+ */
+ private BigFraction minor(final BigFraction[] c1, final BigFraction[] c2, final BigFraction[] c3) {
+ return c2[0].multiply(c3[1]).multiply(c1[2].subtract(c1[3])).
+ add(c2[0].multiply(c3[2]).multiply(c1[3].subtract(c1[1]))).
+ add(c2[0].multiply(c3[3]).multiply(c1[1].subtract(c1[2]))).
+ add(c2[1].multiply(c3[0]).multiply(c1[3].subtract(c1[2]))).
+ add(c2[1].multiply(c3[2]).multiply(c1[0].subtract(c1[3]))).
+ add(c2[1].multiply(c3[3]).multiply(c1[2].subtract(c1[0]))).
+ add(c2[2].multiply(c3[0]).multiply(c1[1].subtract(c1[3]))).
+ add(c2[2].multiply(c3[1]).multiply(c1[3].subtract(c1[0]))).
+ add(c2[2].multiply(c3[3]).multiply(c1[0].subtract(c1[1]))).
+ add(c2[3].multiply(c3[0]).multiply(c1[2].subtract(c1[1]))).
+ add(c2[3].multiply(c3[1]).multiply(c1[0].subtract(c1[2]))).
+ add(c2[3].multiply(c3[2]).multiply(c1[1].subtract(c1[0])));
+ }
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGenerator.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGenerator.java
new file mode 100644
index 0000000..52b5626
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGenerator.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.euclidean.twod.enclosing;
+
+import java.util.List;
+
+import org.apache.commons.numbers.fraction.BigFraction;
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.enclosing.SupportBallGenerator;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+
+/** Class generating an enclosing ball from its support points.
+ */
+public class DiskGenerator implements SupportBallGenerator<Euclidean2D, Cartesian2D> {
+
+ /** {@inheritDoc} */
+ @Override
+ public EnclosingBall<Euclidean2D, Cartesian2D> ballOnSupport(final List<Cartesian2D> support) {
+
+ if (support.size() < 1) {
+ return new EnclosingBall<>(Cartesian2D.ZERO, Double.NEGATIVE_INFINITY);
+ } else {
+ final Cartesian2D vA = support.get(0);
+ if (support.size() < 2) {
+ return new EnclosingBall<>(vA, 0, vA);
+ } else {
+ final Cartesian2D vB = support.get(1);
+ if (support.size() < 3) {
+ return new EnclosingBall<>(new Cartesian2D(0.5, vA, 0.5, vB),
+ 0.5 * vA.distance(vB),
+ vA, vB);
+ } else {
+ final Cartesian2D vC = support.get(2);
+ // a disk is 2D can be defined as:
+ // (1) (x - x_0)^2 + (y - y_0)^2 = r^2
+ // which can be written:
+ // (2) (x^2 + y^2) - 2 x_0 x - 2 y_0 y + (x_0^2 + y_0^2 - r^2) = 0
+ // or simply:
+ // (3) (x^2 + y^2) + a x + b y + c = 0
+ // with disk center coordinates -a/2, -b/2
+ // If the disk exists, a, b and c are a non-zero solution to
+ // [ (x^2 + y^2 ) x y 1 ] [ 1 ] [ 0 ]
+ // [ (xA^2 + yA^2) xA yA 1 ] [ a ] [ 0 ]
+ // [ (xB^2 + yB^2) xB yB 1 ] * [ b ] = [ 0 ]
+ // [ (xC^2 + yC^2) xC yC 1 ] [ c ] [ 0 ]
+ // So the determinant of the matrix is zero. Computing this determinant
+ // by expanding it using the minors m_ij of first row leads to
+ // (4) m_11 (x^2 + y^2) - m_12 x + m_13 y - m_14 = 0
+ // So by identifying equations (2) and (4) we get the coordinates
+ // of center as:
+ // x_0 = +m_12 / (2 m_11)
+ // y_0 = -m_13 / (2 m_11)
+ // Note that the minors m_11, m_12 and m_13 all have the last column
+ // filled with 1.0, hence simplifying the computation
+ final BigFraction[] c2 = new BigFraction[] {
+ new BigFraction(vA.getX()), new BigFraction(vB.getX()), new BigFraction(vC.getX())
+ };
+ final BigFraction[] c3 = new BigFraction[] {
+ new BigFraction(vA.getY()), new BigFraction(vB.getY()), new BigFraction(vC.getY())
+ };
+ final BigFraction[] c1 = new BigFraction[] {
+ c2[0].multiply(c2[0]).add(c3[0].multiply(c3[0])),
+ c2[1].multiply(c2[1]).add(c3[1].multiply(c3[1])),
+ c2[2].multiply(c2[2]).add(c3[2].multiply(c3[2]))
+ };
+ final BigFraction twoM11 = minor(c2, c3).multiply(2);
+ final BigFraction m12 = minor(c1, c3);
+ final BigFraction m13 = minor(c1, c2);
+ final BigFraction centerX = m12.divide(twoM11);
+ final BigFraction centerY = m13.divide(twoM11).negate();
+ final BigFraction dx = c2[0].subtract(centerX);
+ final BigFraction dy = c3[0].subtract(centerY);
+ final BigFraction r2 = dx.multiply(dx).add(dy.multiply(dy));
+ return new EnclosingBall<>(new Cartesian2D(centerX.doubleValue(),
+ centerY.doubleValue()),
+ Math.sqrt(r2.doubleValue()),
+ vA, vB, vC);
+ }
+ }
+ }
+ }
+
+ /** Compute a dimension 3 minor, when 3<sup>d</sup> column is known to be filled with 1.0.
+ * @param c1 first column
+ * @param c2 second column
+ * @return value of the minor computed has an exact fraction
+ */
+ private BigFraction minor(final BigFraction[] c1, final BigFraction[] c2) {
+ return c2[0].multiply(c1[2].subtract(c1[1])).
+ add(c2[1].multiply(c1[0].subtract(c1[2]))).
+ add(c2[2].multiply(c1[1].subtract(c1[0])));
+ }
+
+}
diff --git a/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser2DTest.java b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser2DTest.java
new file mode 100644
index 0000000..21d9cd6
--- /dev/null
+++ b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser2DTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.enclosing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.enclosing.WelzlEncloser;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.enclosing.DiskGenerator;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.Assert;
+import org.junit.Test;
+
+
+public class WelzlEncloser2DTest {
+
+ @Test
+ public void testNullList() {
+ DiskGenerator generator = new DiskGenerator();
+ WelzlEncloser<Euclidean2D, Cartesian2D> encloser =
+ new WelzlEncloser<>(1.0e-10, generator);
+ EnclosingBall<Euclidean2D, Cartesian2D> ball = encloser.enclose(null);
+ Assert.assertTrue(ball.getRadius() < 0);
+ }
+
+ @Test
+ public void testNoPoints() {
+ DiskGenerator generator = new DiskGenerator();
+ WelzlEncloser<Euclidean2D, Cartesian2D> encloser =
+ new WelzlEncloser<>(1.0e-10, generator);
+ EnclosingBall<Euclidean2D, Cartesian2D> ball = encloser.enclose(new ArrayList<Cartesian2D>());
+ Assert.assertTrue(ball.getRadius() < 0);
+ }
+
+ @Test
+ public void testRegularPoints() {
+ List<Cartesian2D> list = buildList(22, 26, 30, 38, 64, 28, 8, 54, 11, 15);
+ checkDisk(list, Arrays.asList(list.get(2), list.get(3), list.get(4)));
+ }
+
+ @Test
+ public void testSolutionOnDiameter() {
+ List<Cartesian2D> list = buildList(22, 26, 30, 38, 64, 28, 8, 54);
+ checkDisk(list, Arrays.asList(list.get(2), list.get(3)));
+ }
+
+ @Test
+ public void testReducingBall1() {
+ List<Cartesian2D> list = buildList(0.05380958511396061, 0.57332359658700000,
+ 0.99348810731127870, 0.02056421361521466,
+ 0.01203950647796437, 0.99779675042261860,
+ 0.00810189987706078, 0.00589246003827815,
+ 0.00465180821202149, 0.99219972923046940);
+ checkDisk(list, Arrays.asList(list.get(1), list.get(3), list.get(4)));
+ }
+
+ @Test
+ public void testReducingBall2() {
+ List<Cartesian2D> list = buildList(0.016930586154703, 0.333955448537779,
+ 0.987189104892331, 0.969778855274507,
+ 0.983696889599935, 0.012904580013266,
+ 0.013114499572905, 0.034740156356895);
+ checkDisk(list, Arrays.asList(list.get(1), list.get(2), list.get(3)));
+ }
+
+ @Test
+ public void testLargeSamples() {
+ UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A, 0xa2a63cad12c01fb2l);
+ for (int k = 0; k < 100; ++k) {
+ int nbPoints = random.nextInt(10000);
+ List<Cartesian2D> points = new ArrayList<>();
+ for (int i = 0; i < nbPoints; ++i) {
+ double x = random.nextDouble();
+ double y = random.nextDouble();
+ points.add(new Cartesian2D(x, y));
+ }
+ checkDisk(points);
+ }
+ }
+
+ private List<Cartesian2D> buildList(final double ... coordinates) {
+ List<Cartesian2D> list = new ArrayList<>(coordinates.length / 2);
+ for (int i = 0; i < coordinates.length; i += 2) {
+ list.add(new Cartesian2D(coordinates[i], coordinates[i + 1]));
+ }
+ return list;
+ }
+
+ private void checkDisk(List<Cartesian2D> points, List<Cartesian2D> refSupport) {
+
+ EnclosingBall<Euclidean2D, Cartesian2D> disk = checkDisk(points);
+
+ // compare computed disk with expected disk
+ DiskGenerator generator = new DiskGenerator();
+ EnclosingBall<Euclidean2D, Cartesian2D> expected = generator.ballOnSupport(refSupport);
+ Assert.assertEquals(refSupport.size(), disk.getSupportSize());
+ Assert.assertEquals(expected.getRadius(), disk.getRadius(), 1.0e-10);
+ Assert.assertEquals(expected.getCenter().getX(), disk.getCenter().getX(), 1.0e-10);
+ Assert.assertEquals(expected.getCenter().getY(), disk.getCenter().getY(), 1.0e-10);
+
+ for (Cartesian2D s : disk.getSupport()) {
+ boolean found = false;
+ for (Cartesian2D rs : refSupport) {
+ if (s == rs) {
+ found = true;
+ }
+ }
+ Assert.assertTrue(found);
+ }
+
+ // check removing any point of the support disk fails to enclose the point
+ for (int i = 0; i < disk.getSupportSize(); ++i) {
+ List<Cartesian2D> reducedSupport = new ArrayList<>();
+ int count = 0;
+ for (Cartesian2D s : disk.getSupport()) {
+ if (count++ != i) {
+ reducedSupport.add(s);
+ }
+ }
+ EnclosingBall<Euclidean2D, Cartesian2D> reducedDisk = generator.ballOnSupport(reducedSupport);
+ boolean foundOutside = false;
+ for (int j = 0; j < points.size() && !foundOutside; ++j) {
+ if (!reducedDisk.contains(points.get(j), 1.0e-10)) {
+ foundOutside = true;
+ }
+ }
+ Assert.assertTrue(foundOutside);
+ }
+
+ }
+
+ private EnclosingBall<Euclidean2D, Cartesian2D> checkDisk(List<Cartesian2D> points) {
+
+ WelzlEncloser<Euclidean2D, Cartesian2D> encloser =
+ new WelzlEncloser<>(1.0e-10, new DiskGenerator());
+ EnclosingBall<Euclidean2D, Cartesian2D> disk = encloser.enclose(points);
+
+ // all points are enclosed
+ for (Cartesian2D v : points) {
+ Assert.assertTrue(disk.contains(v, 1.0e-10));
+ }
+
+ for (Cartesian2D v : points) {
+ boolean inSupport = false;
+ for (Cartesian2D s : disk.getSupport()) {
+ if (v == s) {
+ inSupport = true;
+ }
+ }
+ if (inSupport) {
+ // points on the support should be outside of reduced ball
+ Assert.assertFalse(disk.contains(v, -0.001));
+ }
+ }
+
+ return disk;
+
+ }
+
+}
diff --git a/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser3DTest.java b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser3DTest.java
new file mode 100644
index 0000000..0466b48
--- /dev/null
+++ b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser3DTest.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.enclosing;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.apache.commons.rng.sampling.UnitSphereSampler;
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.enclosing.WelzlEncloser;
+import org.apache.commons.geometry.euclidean.threed.Euclidean3D;
+import org.apache.commons.geometry.euclidean.threed.enclosing.SphereGenerator;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+
+
+public class WelzlEncloser3DTest {
+
+ @Test
+ public void testNullList() {
+ SphereGenerator generator = new SphereGenerator();
+ WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+ new WelzlEncloser<>(1.0e-10, generator);
+ EnclosingBall<Euclidean3D, Cartesian3D> ball = encloser.enclose(null);
+ Assert.assertTrue(ball.getRadius() < 0);
+ }
+
+ @Test
+ public void testNoPoints() {
+ SphereGenerator generator = new SphereGenerator();
+ WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+ new WelzlEncloser<>(1.0e-10, generator);
+ EnclosingBall<Euclidean3D, Cartesian3D> ball = encloser.enclose(new ArrayList<Cartesian3D>());
+ Assert.assertTrue(ball.getRadius() < 0);
+ }
+
+ @Test
+ public void testReducingBall() {
+ List<Cartesian3D> list =
+ Arrays.asList(new Cartesian3D(-7.140397329568118, -16.571661242582177, 11.714458961735405),
+ new Cartesian3D(-7.137986707455888, -16.570767323375720, 11.708602108715928),
+ new Cartesian3D(-7.139185068549035, -16.570891204702250, 11.715554057357394),
+ new Cartesian3D(-7.142682716997507, -16.571609818234290, 11.710787934580328),
+ new Cartesian3D(-7.139018392423351, -16.574405614157020, 11.710518716711425),
+ new Cartesian3D(-7.140870659936730, -16.567993074240455, 11.710914678204503),
+ new Cartesian3D(-7.136350173659562, -16.570498228820930, 11.713965225900928),
+ new Cartesian3D(-7.141675762759172, -16.572852471407028, 11.714033471449508),
+ new Cartesian3D(-7.140453077221105, -16.570212820780647, 11.708624578004980),
+ new Cartesian3D(-7.140322188726825, -16.574152894557717, 11.710305611121410),
+ new Cartesian3D(-7.141116131477088, -16.574061164624560, 11.712938509321699));
+ WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+ new WelzlEncloser<>(1.0e-10, new SphereGenerator());
+ EnclosingBall<Euclidean3D, Cartesian3D> ball = encloser.enclose(list);
+ Assert.assertTrue(ball.getRadius() > 0);
+ }
+
+ @Test
+ public void testInfiniteLoop() {
+ // this test used to generate an infinite loop
+ List<Cartesian3D> list =
+ Arrays.asList(new Cartesian3D( -0.89227075512164380, -2.89317694645713900, 14.84572323743355500),
+ new Cartesian3D( -0.92099498940693580, -2.31086108263908940, 12.92071026467688300),
+ new Cartesian3D( -0.85227999411005200, -3.06314731441320730, 15.40163831651287000),
+ new Cartesian3D( -1.77399413020785970, -3.65630391378114260, 14.13190097751873400),
+ new Cartesian3D( 0.33157833272465354, -2.22813591757792160, 14.21225234159008200),
+ new Cartesian3D( -1.53065579165484400, -1.65692084770139570, 14.61483055714788500),
+ new Cartesian3D( -1.08457093941217140, -1.96100325935602980, 13.09265170575555000),
+ new Cartesian3D( 0.30029469589708850, -3.05470831395667370, 14.56352400426342600),
+ new Cartesian3D( -0.95007443938638460, -1.86810946486118360, 15.14491234340057000),
+ new Cartesian3D( -1.89661503804130830, -2.17004080885185860, 14.81235128513927000),
+ new Cartesian3D( -0.72193328761607530, -1.44513142833618270, 14.52355724218561800),
+ new Cartesian3D( -0.26895980939606550, -3.69512371522084140, 14.72272846327652000),
+ new Cartesian3D( -1.53501693431786170, -3.25055166611021900, 15.15509062584274800),
+ new Cartesian3D( -0.71727553535519410, -3.62284279460799100, 13.26256700929380700),
+ new Cartesian3D( -0.30220950676137365, -3.25410412500779070, 13.13682612771606000),
+ new Cartesian3D( -0.04543996608267075, -1.93081853923797750, 14.79497997883171400),
+ new Cartesian3D( -1.53348892951571640, -3.66688919703524900, 14.73095600812074200),
+ new Cartesian3D( -0.98034899533935820, -3.34004481162763960, 13.03245014017556800));
+
+ WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+ new WelzlEncloser<>(1.0e-10, new SphereGenerator());
+ EnclosingBall<Euclidean3D, Cartesian3D> ball = encloser.enclose(list);
+ Assert.assertTrue(ball.getRadius() > 0);
+ }
+
+ @Test
+ public void testLargeSamples() throws IOException {
+ final UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A,
+ 0x35ddecfc78131e1dl);
+ final UnitSphereSampler sr = new UnitSphereSampler(3, random);
+ for (int k = 0; k < 50; ++k) {
+
+ // define the reference sphere we want to compute
+ double d = 25 * random.nextDouble();
+ double refRadius = 10 * random.nextDouble();
+ Cartesian3D refCenter = new Cartesian3D(d, new Cartesian3D(sr.nextVector()));
+ // set up a large sample inside the reference sphere
+ int nbPoints = random.nextInt(1000);
+ List<Cartesian3D> points = new ArrayList<>();
+ for (int i = 0; i < nbPoints; ++i) {
+ double r = refRadius * random.nextDouble();
+ points.add(new Cartesian3D(1.0, refCenter, r, new Cartesian3D(sr.nextVector())));
+ }
+
+ // test we find a sphere at most as large as the one used for random drawings
+ checkSphere(points, refRadius);
+
+ }
+ }
+
+ private void checkSphere(List<Cartesian3D> points, double refRadius) {
+
+ EnclosingBall<Euclidean3D, Cartesian3D> sphere = checkSphere(points);
+
+ // compare computed sphere with bounding sphere
+ Assert.assertTrue(sphere.getRadius() <= refRadius);
+
+ // check removing any point of the support Sphere fails to enclose the point
+ for (int i = 0; i < sphere.getSupportSize(); ++i) {
+ List<Cartesian3D> reducedSupport = new ArrayList<>();
+ int count = 0;
+ for (Cartesian3D s : sphere.getSupport()) {
+ if (count++ != i) {
+ reducedSupport.add(s);
+ }
+ }
+ EnclosingBall<Euclidean3D, Cartesian3D> reducedSphere =
+ new SphereGenerator().ballOnSupport(reducedSupport);
+ boolean foundOutside = false;
+ for (int j = 0; j < points.size() && !foundOutside; ++j) {
+ if (!reducedSphere.contains(points.get(j), 1.0e-10)) {
+ foundOutside = true;
+ }
+ }
+ Assert.assertTrue(foundOutside);
+ }
+
+ }
+
+ private EnclosingBall<Euclidean3D, Cartesian3D> checkSphere(List<Cartesian3D> points) {
+
+ WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+ new WelzlEncloser<>(1.0e-10, new SphereGenerator());
+ EnclosingBall<Euclidean3D, Cartesian3D> Sphere = encloser.enclose(points);
+
+ // all points are enclosed
+ for (Cartesian3D v : points) {
+ Assert.assertTrue(Sphere.contains(v, 1.0e-10));
+ }
+
+ for (Cartesian3D v : points) {
+ boolean inSupport = false;
+ for (Cartesian3D s : Sphere.getSupport()) {
+ if (v == s) {
+ inSupport = true;
+ }
+ }
+ if (inSupport) {
+ // points on the support should be outside of reduced ball
+ Assert.assertFalse(Sphere.contains(v, -0.001));
+ }
+ }
+
+ return Sphere;
+
+ }
+
+}
diff --git a/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGeneratorTest.java b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGeneratorTest.java
new file mode 100644
index 0000000..1adbaa5
--- /dev/null
+++ b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGeneratorTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.enclosing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+import org.apache.commons.geometry.euclidean.threed.Euclidean3D;
+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 SphereGeneratorTest {
+
+ @Test
+ public void testSupport0Point() {
+ List<Cartesian3D> support = Arrays.asList(new Cartesian3D[0]);
+ EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+ Assert.assertTrue(sphere.getRadius() < 0);
+ Assert.assertEquals(0, sphere.getSupportSize());
+ Assert.assertEquals(0, sphere.getSupport().length);
+ }
+
+ @Test
+ public void testSupport1Point() {
+ List<Cartesian3D> support = Arrays.asList(new Cartesian3D(1, 2, 3));
+ EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+ Assert.assertEquals(0.0, sphere.getRadius(), 1.0e-10);
+ Assert.assertTrue(sphere.contains(support.get(0)));
+ Assert.assertTrue(sphere.contains(support.get(0), 0.5));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(support.get(0).getX() + 0.1,
+ support.get(0).getY() + 0.1,
+ support.get(0).getZ() + 0.1),
+ 0.001));
+ Assert.assertTrue(sphere.contains(new Cartesian3D(support.get(0).getX() + 0.1,
+ support.get(0).getY() + 0.1,
+ support.get(0).getZ() + 0.1),
+ 0.5));
+ Assert.assertEquals(0, support.get(0).distance(sphere.getCenter()), 1.0e-10);
+ Assert.assertEquals(1, sphere.getSupportSize());
+ Assert.assertTrue(support.get(0) == sphere.getSupport()[0]);
+ }
+
+ @Test
+ public void testSupport2Points() {
+ List<Cartesian3D> support = Arrays.asList(new Cartesian3D(1, 0, 0),
+ new Cartesian3D(3, 0, 0));
+ EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+ Assert.assertEquals(1.0, sphere.getRadius(), 1.0e-10);
+ int i = 0;
+ for (Cartesian3D v : support) {
+ Assert.assertTrue(sphere.contains(v));
+ Assert.assertEquals(1.0, v.distance(sphere.getCenter()), 1.0e-10);
+ Assert.assertTrue(v == sphere.getSupport()[i++]);
+ }
+ Assert.assertTrue(sphere.contains(new Cartesian3D(2, 0.9, 0)));
+ Assert.assertFalse(sphere.contains(Cartesian3D.ZERO));
+ Assert.assertEquals(0.0, new Cartesian3D(2, 0, 0).distance(sphere.getCenter()), 1.0e-10);
+ Assert.assertEquals(2, sphere.getSupportSize());
+ }
+
+ @Test
+ public void testSupport3Points() {
+ List<Cartesian3D> support = Arrays.asList(new Cartesian3D(1, 0, 0),
+ new Cartesian3D(3, 0, 0),
+ new Cartesian3D(2, 2, 0));
+ EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+ Assert.assertEquals(5.0 / 4.0, sphere.getRadius(), 1.0e-10);
+ int i = 0;
+ for (Cartesian3D v : support) {
+ Assert.assertTrue(sphere.contains(v));
+ Assert.assertEquals(5.0 / 4.0, v.distance(sphere.getCenter()), 1.0e-10);
+ Assert.assertTrue(v == sphere.getSupport()[i++]);
+ }
+ Assert.assertTrue(sphere.contains(new Cartesian3D(2, 0.9, 0)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(0.9, 0, 0)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(3.1, 0, 0)));
+ Assert.assertTrue(sphere.contains(new Cartesian3D(2.0, -0.499, 0)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(2.0, -0.501, 0)));
+ Assert.assertTrue(sphere.contains(new Cartesian3D(2.0, 3.0 / 4.0, -1.249)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(2.0, 3.0 / 4.0, -1.251)));
+ Assert.assertEquals(0.0, new Cartesian3D(2.0, 3.0 / 4.0, 0).distance(sphere.getCenter()), 1.0e-10);
+ Assert.assertEquals(3, sphere.getSupportSize());
+ }
+
+ @Test
+ public void testSupport4Points() {
+ List<Cartesian3D> support = Arrays.asList(new Cartesian3D(17, 14, 18),
+ new Cartesian3D(11, 14, 22),
+ new Cartesian3D( 2, 22, 17),
+ new Cartesian3D(22, 11, -10));
+ EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+ Assert.assertEquals(25.0, sphere.getRadius(), 1.0e-10);
+ int i = 0;
+ for (Cartesian3D v : support) {
+ Assert.assertTrue(sphere.contains(v));
+ Assert.assertEquals(25.0, v.distance(sphere.getCenter()), 1.0e-10);
+ Assert.assertTrue(v == sphere.getSupport()[i++]);
+ }
+ Assert.assertTrue(sphere.contains (new Cartesian3D(-22.999, 2, 2)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(-23.001, 2, 2)));
+ Assert.assertTrue(sphere.contains (new Cartesian3D( 26.999, 2, 2)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D( 27.001, 2, 2)));
+ Assert.assertTrue(sphere.contains (new Cartesian3D(2, -22.999, 2)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(2, -23.001, 2)));
+ Assert.assertTrue(sphere.contains (new Cartesian3D(2, 26.999, 2)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(2, 27.001, 2)));
+ Assert.assertTrue(sphere.contains (new Cartesian3D(2, 2, -22.999)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(2, 2, -23.001)));
+ Assert.assertTrue(sphere.contains (new Cartesian3D(2, 2, 26.999)));
+ Assert.assertFalse(sphere.contains(new Cartesian3D(2, 2, 27.001)));
+ Assert.assertEquals(0.0, new Cartesian3D(2.0, 2.0, 2.0).distance(sphere.getCenter()), 1.0e-10);
+ Assert.assertEquals(4, sphere.getSupportSize());
+ }
+
+ @Test
+ public void testRandom() {
+ final UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A,
+ 0xd015982e9f31ee04l);
+ final UnitSphereSampler sr = new UnitSphereSampler(3, random);
+ for (int i = 0; i < 100; ++i) {
+ double d = 25 * random.nextDouble();
+ double refRadius = 10 * random.nextDouble();
+ Cartesian3D refCenter = new Cartesian3D(d, new Cartesian3D(sr.nextVector()));
+ List<Cartesian3D> support = new ArrayList<>();
+ for (int j = 0; j < 5; ++j) {
+ support.add(new Cartesian3D(1.0, refCenter, refRadius, new Cartesian3D(sr.nextVector())));
+ }
+ EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+ Assert.assertEquals(0.0, refCenter.distance(sphere.getCenter()), 4e-7 * refRadius);
+ Assert.assertEquals(refRadius, sphere.getRadius(), 1e-7 * refRadius);
+ }
+ }
+
+ @Test
+ public void testDegeneratedCase() {
+ final List<Cartesian3D> support =
+ Arrays.asList(new Cartesian3D(Math.scalb(-8039905610797991.0, -50), // -7.140870659936730
+ Math.scalb(-4663475464714142.0, -48), // -16.567993074240455
+ Math.scalb( 6592658872616184.0, -49)), // 11.710914678204503
+ new Cartesian3D(Math.scalb(-8036658568968473.0, -50), // -7.137986707455888
+ Math.scalb(-4664256346424880.0, -48), // -16.570767323375720
+ Math.scalb( 6591357011730307.0, -49)), // 11.708602108715928)
+ new Cartesian3D(Math.scalb(-8037820142977230.0, -50), // -7.139018392423351
+ Math.scalb(-4665280434237813.0, -48), // -16.574405614157020
+ Math.scalb( 6592435966112099.0, -49)), // 11.710518716711425
+ new Cartesian3D(Math.scalb(-8038007803611611.0, -50), // -7.139185068549035
+ Math.scalb(-4664291215918380.0, -48), // -16.570891204702250
+ Math.scalb( 6595270610894208.0, -49))); // 11.715554057357394
+ EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+
+ // the following values have been computed using Emacs calc with exact arithmetic from the
+ // rational representation corresponding to the scalb calls (i.e. -8039905610797991/2^50, ...)
+ // The results were converted to decimal representation rounded to 1.0e-30 when writing the reference
+ // values in this test
+ Assert.assertEquals( 0.003616820213530053297575846168, sphere.getRadius(), 1.0e-20);
+ Assert.assertEquals( -7.139325643360503322823511839511, sphere.getCenter().getX(), 1.0e-20);
+ Assert.assertEquals(-16.571096474251747245361467833760, sphere.getCenter().getY(), 1.0e-20);
+ Assert.assertEquals( 11.711945804096960876521111630800, sphere.getCenter().getZ(), 1.0e-20);
+
+ for (Cartesian3D v : support) {
+ Assert.assertTrue(sphere.contains(v, 1.0e-14));
+ }
+
+ }
+
+}
diff --git a/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGeneratorTest.java b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGeneratorTest.java
new file mode 100644
index 0000000..4fbe657
--- /dev/null
+++ b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGeneratorTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.enclosing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.apache.commons.rng.sampling.UnitSphereSampler;
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.euclidean.twod.enclosing.DiskGenerator;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+
+
+public class DiskGeneratorTest {
+
+ @Test
+ public void testSupport0Point() {
+ List<Cartesian2D> support = Arrays.asList(new Cartesian2D[0]);
+ EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+ Assert.assertTrue(disk.getRadius() < 0);
+ Assert.assertEquals(0, disk.getSupportSize());
+ Assert.assertEquals(0, disk.getSupport().length);
+ }
+
+ @Test
+ public void testSupport1Point() {
+ List<Cartesian2D> support = Arrays.asList(new Cartesian2D(1, 2));
+ EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+ Assert.assertEquals(0.0, disk.getRadius(), 1.0e-10);
+ Assert.assertTrue(disk.contains(support.get(0)));
+ Assert.assertTrue(disk.contains(support.get(0), 0.5));
+ Assert.assertFalse(disk.contains(new Cartesian2D(support.get(0).getX() + 0.1,
+ support.get(0).getY() - 0.1),
+ 0.001));
+ Assert.assertTrue(disk.contains(new Cartesian2D(support.get(0).getX() + 0.1,
+ support.get(0).getY() - 0.1),
+ 0.5));
+ Assert.assertEquals(0, support.get(0).distance(disk.getCenter()), 1.0e-10);
+ Assert.assertEquals(1, disk.getSupportSize());
+ Assert.assertTrue(support.get(0) == disk.getSupport()[0]);
+ }
+
+ @Test
+ public void testSupport2Points() {
+ List<Cartesian2D> support = Arrays.asList(new Cartesian2D(1, 0),
+ new Cartesian2D(3, 0));
+ EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+ Assert.assertEquals(1.0, disk.getRadius(), 1.0e-10);
+ int i = 0;
+ for (Cartesian2D v : support) {
+ Assert.assertTrue(disk.contains(v));
+ Assert.assertEquals(1.0, v.distance(disk.getCenter()), 1.0e-10);
+ Assert.assertTrue(v == disk.getSupport()[i++]);
+ }
+ Assert.assertTrue(disk.contains(new Cartesian2D(2, 0.9)));
+ Assert.assertFalse(disk.contains(Cartesian2D.ZERO));
+ Assert.assertEquals(0.0, new Cartesian2D(2, 0).distance(disk.getCenter()), 1.0e-10);
+ Assert.assertEquals(2, disk.getSupportSize());
+ }
+
+ @Test
+ public void testSupport3Points() {
+ List<Cartesian2D> support = Arrays.asList(new Cartesian2D(1, 0),
+ new Cartesian2D(3, 0),
+ new Cartesian2D(2, 2));
+ EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+ Assert.assertEquals(5.0 / 4.0, disk.getRadius(), 1.0e-10);
+ int i = 0;
+ for (Cartesian2D v : support) {
+ Assert.assertTrue(disk.contains(v));
+ Assert.assertEquals(5.0 / 4.0, v.distance(disk.getCenter()), 1.0e-10);
+ Assert.assertTrue(v == disk.getSupport()[i++]);
+ }
+ Assert.assertTrue(disk.contains(new Cartesian2D(2, 0.9)));
+ Assert.assertFalse(disk.contains(new Cartesian2D(0.9, 0)));
+ Assert.assertFalse(disk.contains(new Cartesian2D(3.1, 0)));
+ Assert.assertTrue(disk.contains(new Cartesian2D(2.0, -0.499)));
+ Assert.assertFalse(disk.contains(new Cartesian2D(2.0, -0.501)));
+ Assert.assertEquals(0.0, new Cartesian2D(2.0, 3.0 / 4.0).distance(disk.getCenter()), 1.0e-10);
+ Assert.assertEquals(3, disk.getSupportSize());
+ }
+
+ @Test
+ public void testRandom() {
+ final UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A,
+ 0x12faa818373ffe90l);
+ final UnitSphereSampler sr = new UnitSphereSampler(2, random);
+ for (int i = 0; i < 500; ++i) {
+ double d = 25 * random.nextDouble();
+ double refRadius = 10 * random.nextDouble();
+ Cartesian2D refCenter = new Cartesian2D(d, new Cartesian2D(sr.nextVector()));
+ List<Cartesian2D> support = new ArrayList<>();
+ for (int j = 0; j < 3; ++j) {
+ support.add(new Cartesian2D(1.0, refCenter, refRadius, new Cartesian2D(sr.nextVector())));
+ }
+ EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+ Assert.assertEquals(0.0, refCenter.distance(disk.getCenter()), 3e-9 * refRadius);
+ Assert.assertEquals(refRadius, disk.getRadius(), 7e-10 * refRadius);
+ }
+
+ }
+}
diff --git a/commons-geometry-euclidean-twod/pom.xml b/commons-geometry-euclidean-twod/pom.xml
deleted file mode 100644
index c077816..0000000
--- a/commons-geometry-euclidean-twod/pom.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.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.
--->
-<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
- xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-geometry-parent</artifactId>
- <version>1.0-SNAPSHOT</version>
- </parent>
-
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-geometry-euclidean-twod</artifactId>
- <version>1.0-SNAPSHOT</version>
- <name>Apache Commons Geometry Two-Dimensional Euclidean Space</name>
-
- <description></description>
-
- <properties>
- <!-- OSGi -->
- <commons.osgi.symbolicName>org.apache.commons.geometry.euclidean.twod</commons.osgi.symbolicName>
- <commons.osgi.export>org.apache.commons.geometry.euclidean.twod</commons.osgi.export>
- <!-- Java 9+ -->
- <commons.automatic.module.name>org.apache.commons.geometry.euclidean.twod</commons.automatic.module.name>
- <!-- Workaround to avoid duplicating config files. -->
- <geometry.parent.dir>${basedir}/..</geometry.parent.dir>
- </properties>
-
-</project>
diff --git a/commons-geometry-euclidean/pom.xml b/commons-geometry-euclidean/pom.xml
new file mode 100644
index 0000000..6dfece6
--- /dev/null
+++ b/commons-geometry-euclidean/pom.xml
@@ -0,0 +1,92 @@
+<?xml version="1.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.
+-->
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-geometry-parent</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ </parent>
+
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-geometry-euclidean</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <name>Apache Commons Geometry Euclidean</name>
+
+ <description>Geometric primitives for euclidean space.</description>
+
+ <properties>
+ <!-- OSGi -->
+ <commons.osgi.symbolicName>org.apache.commons.geometry.euclidean</commons.osgi.symbolicName>
+ <commons.osgi.export>org.apache.commons.geometry.euclidean</commons.osgi.export>
+ <!-- Java 9+ -->
+ <commons.automatic.module.name>org.apache.commons.geometry.euclidean</commons.automatic.module.name>
+ <!-- Workaround to avoid duplicating config files. -->
+ <geometry.parent.dir>${basedir}/..</geometry.parent.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-geometry-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-numbers-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-numbers-arrays</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-numbers-angle</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-geometry-core</artifactId>
+ <version>${project.version}</version>
+ <classifier>tests</classifier>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-rng-client-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-rng-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-rng-sampling</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Cartesian1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Cartesian1D.java
new file mode 100644
index 0000000..4517616
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Cartesian1D.java
@@ -0,0 +1,382 @@
+/*
+ * 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.text.NumberFormat;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.Vector;
+
+/** This class represents a 1D point or a 1D vector.
+ * <p>An instance of Cartesian1D represents the point with the corresponding
+ * Cartesian coordinates.</p>
+ * <p>An instance of Cartesian1D also represents the vector which begins at
+ * the origin and ends at the point corresponding to the coordinates.</p>
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public class Cartesian1D extends Vector1D implements Point<Euclidean1D> {
+
+ /** Origin (coordinates: 0). */
+ public static final Cartesian1D ZERO = new Cartesian1D(0.0);
+
+ /** Unit (coordinates: 1). */
+ public static final Cartesian1D ONE = new Cartesian1D(1.0);
+
+ // CHECKSTYLE: stop ConstantName
+ /** A vector with all coordinates set to NaN. */
+ public static final Cartesian1D NaN = new Cartesian1D(Double.NaN);
+ // CHECKSTYLE: resume ConstantName
+
+ /** A vector with all coordinates set to positive infinity. */
+ public static final Cartesian1D POSITIVE_INFINITY =
+ new Cartesian1D(Double.POSITIVE_INFINITY);
+
+ /** A vector with all coordinates set to negative infinity. */
+ public static final Cartesian1D NEGATIVE_INFINITY =
+ new Cartesian1D(Double.NEGATIVE_INFINITY);
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 7556674948671647925L;
+
+ /** Abscissa. */
+ private final double x;
+
+ /** Simple constructor.
+ * Build a vector from its coordinates
+ * @param x abscissa
+ * @see #getX()
+ */
+ public Cartesian1D(double x) {
+ this.x = x;
+ }
+
+ /** Multiplicative constructor
+ * Build a vector from another one and a scale factor.
+ * The vector built will be a * u
+ * @param a scale factor
+ * @param u base (unscaled) vector
+ */
+ public Cartesian1D(double a, Cartesian1D u) {
+ this.x = a * u.x;
+ }
+
+ /** Linear constructor
+ * Build a vector from two other ones and corresponding scale factors.
+ * The vector built will be a1 * u1 + a2 * u2
+ * @param a1 first scale factor
+ * @param u1 first base (unscaled) vector
+ * @param a2 second scale factor
+ * @param u2 second base (unscaled) vector
+ */
+ public Cartesian1D(double a1, Cartesian1D u1, double a2, Cartesian1D u2) {
+ this.x = a1 * u1.x + a2 * u2.x;
+ }
+
+ /** Linear constructor
+ * Build a vector from three other ones and corresponding scale factors.
+ * The vector built will be a1 * u1 + a2 * u2 + a3 * u3
+ * @param a1 first scale factor
+ * @param u1 first base (unscaled) vector
+ * @param a2 second scale factor
+ * @param u2 second base (unscaled) vector
+ * @param a3 third scale factor
+ * @param u3 third base (unscaled) vector
+ */
+ public Cartesian1D(double a1, Cartesian1D u1, double a2, Cartesian1D u2,
+ double a3, Cartesian1D u3) {
+ this.x = a1 * u1.x + a2 * u2.x + a3 * u3.x;
+ }
+
+ /** Linear constructor
+ * Build a vector from four other ones and corresponding scale factors.
+ * The vector built will be a1 * u1 + a2 * u2 + a3 * u3 + a4 * u4
+ * @param a1 first scale factor
+ * @param u1 first base (unscaled) vector
+ * @param a2 second scale factor
+ * @param u2 second base (unscaled) vector
+ * @param a3 third scale factor
+ * @param u3 third base (unscaled) vector
+ * @param a4 fourth scale factor
+ * @param u4 fourth base (unscaled) vector
+ */
+ public Cartesian1D(double a1, Cartesian1D u1, double a2, Cartesian1D u2,
+ double a3, Cartesian1D u3, double a4, Cartesian1D u4) {
+ this.x = a1 * u1.x + a2 * u2.x + a3 * u3.x + a4 * u4.x;
+ }
+
+ /** Get the abscissa of the vector.
+ * @return abscissa of the vector
+ * @see #Cartesian1D(double)
+ */
+ @Override
+ public double getX() {
+ return x;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Space getSpace() {
+ return Euclidean1D.getInstance();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian1D getZero() {
+ return ZERO;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getNorm1() {
+ return Math.abs(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getNorm() {
+ return Math.abs(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getNormSq() {
+ return x * x;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getNormInf() {
+ return Math.abs(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian1D add(Vector<Euclidean1D> v) {
+ Cartesian1D v1 = (Cartesian1D) v;
+ return new Cartesian1D(x + v1.getX());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian1D add(double factor, Vector<Euclidean1D> v) {
+ Cartesian1D v1 = (Cartesian1D) v;
+ return new Cartesian1D(x + factor * v1.getX());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian1D subtract(Vector<Euclidean1D> p) {
+ Cartesian1D p3 = (Cartesian1D) p;
+ return new Cartesian1D(x - p3.x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian1D subtract(double factor, Vector<Euclidean1D> v) {
+ Cartesian1D v1 = (Cartesian1D) v;
+ return new Cartesian1D(x - factor * v1.getX());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian1D normalize() throws IllegalStateException {
+ double s = getNorm();
+ if (s == 0) {
+ throw new IllegalStateException("Norm is zero");
+ }
+ return scalarMultiply(1 / s);
+ }
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian1D negate() {
+ return new Cartesian1D(-x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian1D scalarMultiply(double a) {
+ return new Cartesian1D(a * x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isNaN() {
+ return Double.isNaN(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return !isNaN() && Double.isInfinite(x);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance1(Vector<Euclidean1D> p) {
+ Cartesian1D p1 = (Cartesian1D) p;
+ final double dx = Math.abs(p1.x - x);
+ return dx;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance(Point<Euclidean1D> p) {
+ return distance((Cartesian1D) p);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance(Vector<Euclidean1D> v) {
+ return distance((Cartesian1D) v);
+ }
+
+ /** Compute the distance between the instance and other coordinates.
+ * @param c other coordinates
+ * @return the distance between the instance and c
+ */
+ public double distance(Cartesian1D c) {
+ final double dx = c.x - x;
+ return Math.abs(dx);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distanceInf(Vector<Euclidean1D> p) {
+ Cartesian1D p1 = (Cartesian1D) p;
+ final double dx = Math.abs(p1.x - x);
+ return dx;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distanceSq(Vector<Euclidean1D> p) {
+ Cartesian1D p1 = (Cartesian1D) p;
+ final double dx = p1.x - x;
+ return dx * dx;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double dotProduct(final Vector<Euclidean1D> v) {
+ final Cartesian1D v1 = (Cartesian1D) v;
+ return x * v1.x;
+ }
+
+ /** Compute the distance between two points according to the L<sub>2</sub> norm.
+ * <p>Calling this method is equivalent to calling:
+ * <code>p1.subtract(p2).getNorm()</code> except that no intermediate
+ * vector is built</p>
+ * @param p1 first vector
+ * @param p2 second vector
+ * @return the distance between p1 and p2 according to the L<sub>2</sub> norm
+ */
+ public static double distance(Cartesian1D p1, Cartesian1D p2) {
+ return p1.distance(p2);
+ }
+
+ /** Compute the distance between two points according to the L<sub>∞</sub> norm.
+ * <p>Calling this method is equivalent to calling:
+ * <code>p1.subtract(p2).getNormInf()</code> except that no intermediate
+ * vector is built</p>
+ * @param p1 first vector
+ * @param p2 second vector
+ * @return the distance between p1 and p2 according to the L<sub>∞</sub> norm
+ */
+ public static double distanceInf(Cartesian1D p1, Cartesian1D p2) {
+ return p1.distanceInf(p2);
+ }
+
+ /** Compute the square of the distance between two points.
+ * <p>Calling this method is equivalent to calling:
+ * <code>p1.subtract(p2).getNormSq()</code> except that no intermediate
+ * vector is built</p>
+ * @param p1 first vector
+ * @param p2 second vector
+ * @return the square of the distance between p1 and p2
+ */
+ public static double distanceSq(Cartesian1D p1, Cartesian1D p2) {
+ return p1.distanceSq(p2);
+ }
+
+ /**
+ * Test for the equality of two 1D vectors.
+ * <p>
+ * If all coordinates of two 1D vectors are exactly the same, and none are
+ * <code>Double.NaN</code>, the two 1D vectors 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
+ * 1D vector are equal to <code>Double.NaN</code>, the 1D vector is equal to
+ * {@link #NaN}.
+ * </p>
+ *
+ * @param other Object to test for equality to this
+ * @return true if two 1D vector objects are equal, false if
+ * object is null, not an instance of Cartesian1D, or
+ * not equal to this Cartesian1D instance
+ *
+ */
+ @Override
+ public boolean equals(Object other) {
+
+ if (this == other) {
+ return true;
+ }
+
+ if (other instanceof Cartesian1D) {
+ final Cartesian1D rhs = (Cartesian1D)other;
+ if (rhs.isNaN()) {
+ return this.isNaN();
+ }
+
+ return x == rhs.x;
+ }
+ return false;
+ }
+
+ /**
+ * Get a hashCode for the 1D 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 7785;
+ }
+ return 997 * Double.hashCode(x);
+ }
+
+ /** Get a string representation of this vector.
+ * @return a string representation of this vector
+ */
+ @Override
+ public String toString() {
+ return toString(NumberFormat.getInstance());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString(final NumberFormat format) {
+ return "{" + format.format(x) + "}";
+ }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Euclidean1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Euclidean1D.java
new file mode 100644
index 0000000..394e317
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Euclidean1D.java
@@ -0,0 +1,80 @@
+/*
+ * 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.io.Serializable;
+
+import org.apache.commons.geometry.core.Space;
+
+/**
+ * This class implements a one-dimensional space.
+ */
+public class Euclidean1D implements Serializable, Space {
+
+ /** Serializable version identifier. */
+ private static final long serialVersionUID = -1178039568877797126L;
+
+ /** Private constructor for the singleton.
+ */
+ private Euclidean1D() {
+ }
+
+ /** Get the unique instance.
+ * @return the unique instance
+ */
+ public static Euclidean1D getInstance() {
+ return LazyHolder.INSTANCE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getDimension() {
+ return 1;
+ }
+
+ /** {@inheritDoc}
+ * <p>
+ * As the 1-dimension Euclidean space does not have proper sub-spaces,
+ * this method always throws a {@link UnsupportedOperationException}
+ * </p>
+ * @return nothing
+ * @throws UnsupportedOperationException in all cases
+ */
+ @Override
+ public Space getSubSpace() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("One-dimensional space does not have a subspace");
+ }
+
+ // CHECKSTYLE: stop HideUtilityClassConstructor
+ /** Holder for the instance.
+ * <p>We use here the Initialization On Demand Holder Idiom.</p>
+ */
+ private static class LazyHolder {
+ /** Cached field instance. */
+ private static final Euclidean1D INSTANCE = new Euclidean1D();
+ }
+ // CHECKSTYLE: resume HideUtilityClassConstructor
+
+ /** Handle deserialization of the singleton.
+ * @return the singleton instance
+ */
+ private Object readResolve() {
+ // return the singleton instance
+ return LazyHolder.INSTANCE;
+ }
+}
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
new file mode 100644
index 0000000..c4c39af
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
@@ -0,0 +1,89 @@
+/*
+ * 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.Region.Location;
+
+/** This class represents a 1D interval.
+ * @see IntervalsSet
+ */
+public class Interval {
+
+ /** The lower bound of the interval. */
+ private final double lower;
+
+ /** The upper bound of the interval. */
+ private final double upper;
+
+ /** Simple constructor.
+ * @param lower lower bound of the interval
+ * @param upper upper bound of the interval
+ */
+ public Interval(final double lower, final double upper) {
+ if (upper < lower) {
+ throw new IllegalArgumentException("Endpoints do not specify an interval: [{" + upper + "}, {" + lower + "}]");
+ }
+ this.lower = lower;
+ this.upper = upper;
+ }
+
+ /** Get the lower bound of the interval.
+ * @return lower bound of the interval
+ */
+ 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;
+ } else {
+ return Location.BOUNDARY;
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..3bdcd17
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/IntervalsSet.java
@@ -0,0 +1,619 @@
+/*
+ * 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.Point;
+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;
+
+/** This class represents a 1D region: a set of intervals.
+ */
+public class IntervalsSet extends AbstractRegion<Euclidean1D, Euclidean1D> implements Iterable<double[]> {
+
+ /** Build an intervals set representing the whole real line.
+ * @param tolerance tolerance below which points are considered identical.
+ */
+ public IntervalsSet(final double tolerance) {
+ super(tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical.
+ */
+ public IntervalsSet(final double lower, final double upper, final double tolerance) {
+ super(buildTree(lower, upper, tolerance), tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical.
+ */
+ public IntervalsSet(final BSPTree<Euclidean1D> tree, final double tolerance) {
+ super(tree, tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical.
+ */
+ public IntervalsSet(final Collection<SubHyperplane<Euclidean1D>> boundary,
+ final double tolerance) {
+ super(boundary, tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical.
+ * @return the built tree
+ */
+ private static BSPTree<Euclidean1D> buildTree(final double lower, final double upper,
+ final double tolerance) {
+ 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<Euclidean1D> upperCut =
+ new OrientedPoint(new Cartesian1D(upper), true, tolerance).wholeHyperplane();
+ return new BSPTree<>(upperCut,
+ new BSPTree<Euclidean1D>(Boolean.FALSE),
+ new BSPTree<Euclidean1D>(Boolean.TRUE),
+ null);
+ }
+ final SubHyperplane<Euclidean1D> lowerCut =
+ new OrientedPoint(new Cartesian1D(lower), false, tolerance).wholeHyperplane();
+ if (Double.isInfinite(upper) && (upper > 0)) {
+ // the tree must be open on the positive infinity side
+ return new BSPTree<>(lowerCut,
+ new BSPTree<Euclidean1D>(Boolean.FALSE),
+ new BSPTree<Euclidean1D>(Boolean.TRUE),
+ null);
+ }
+
+ // the tree must be bounded on the two sides
+ final SubHyperplane<Euclidean1D> upperCut =
+ new OrientedPoint(new Cartesian1D(upper), true, tolerance).wholeHyperplane();
+ return new BSPTree<>(lowerCut,
+ new BSPTree<Euclidean1D>(Boolean.FALSE),
+ new BSPTree<>(upperCut,
+ new BSPTree<Euclidean1D>(Boolean.FALSE),
+ new BSPTree<Euclidean1D>(Boolean.TRUE),
+ null),
+ null);
+
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public IntervalsSet buildNew(final BSPTree<Euclidean1D> tree) {
+ return new IntervalsSet(tree, getTolerance());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void computeGeometricalProperties() {
+ if (getTree(false).getCut() == null) {
+ setBarycenter((Point<Euclidean1D>) Cartesian1D.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((Point<Euclidean1D>) Cartesian1D.NaN);
+ } else if (size > 0) {
+ setBarycenter((Point<Euclidean1D>) new Cartesian1D(sum / size));
+ } else {
+ setBarycenter((Point<Euclidean1D>) ((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<Euclidean1D> 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.isDirect() ? 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<Euclidean1D> 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.isDirect() ? node.getPlus() : node.getMinus();
+ }
+ return ((Boolean) node.getAttribute()) ? Double.POSITIVE_INFINITY : sup;
+ }
+
+ /** {@inheritDoc}
+ */
+ @Override
+ public BoundaryProjection<Euclidean1D> projectToBoundary(final Point<Euclidean1D> point) {
+
+ // get position of test point
+ final double x = ((Cartesian1D) 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 Cartesian1D finiteOrNullPoint(final double x) {
+ return Double.isInfinite(x) ? null : new Cartesian1D(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<Euclidean1D> getFirstLeaf(final BSPTree<Euclidean1D> root) {
+
+ if (root.getCut() == null) {
+ return root;
+ }
+
+ // find the smallest internal node
+ BSPTree<Euclidean1D> smallest = null;
+ for (BSPTree<Euclidean1D> 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<Euclidean1D> getFirstIntervalBoundary() {
+
+ // start search at the tree root
+ BSPTree<Euclidean1D> 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<Euclidean1D> 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<Euclidean1D> 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<Euclidean1D> nextInternalNode(BSPTree<Euclidean1D> 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<Euclidean1D> previousInternalNode(BSPTree<Euclidean1D> 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<Euclidean1D> leafBefore(BSPTree<Euclidean1D> 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<Euclidean1D> leafAfter(BSPTree<Euclidean1D> 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<Euclidean1D> node) {
+ final BSPTree<Euclidean1D> 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<Euclidean1D> node) {
+ final BSPTree<Euclidean1D> 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<Euclidean1D> childBefore(BSPTree<Euclidean1D> 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<Euclidean1D> childAfter(BSPTree<Euclidean1D> 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<Euclidean1D> node) {
+ return ((OrientedPoint) node.getCut().getHyperplane()).isDirect();
+ }
+
+ /** Get the abscissa of an internal node.
+ * @param node internal node to check
+ * @return abscissa
+ */
+ private double getAngle(final BSPTree<Euclidean1D> 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<Euclidean1D> 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<Euclidean1D> 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<Euclidean1D> 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
new file mode 100644
index 0000000..b1d0d89
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
@@ -0,0 +1,140 @@
+/*
+ * 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.Point;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+
+/** This class represents a 1D oriented hyperplane.
+ * <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 OrientedPoint implements Hyperplane<Euclidean1D> {
+
+ /** Vector location. */
+ private final Cartesian1D location;
+
+ /** Orientation. */
+ private boolean direct;
+
+ /** Tolerance below which points are considered to belong to the hyperplane. */
+ private final double tolerance;
+
+ /** Simple constructor.
+ * @param location location of the hyperplane
+ * @param direct if true, the plus side of the hyperplane is towards
+ * abscissas greater than {@code location}
+ * @param tolerance tolerance below which points are considered to belong to the hyperplane
+ */
+ public OrientedPoint(final Cartesian1D location, final boolean direct, final double tolerance) {
+ this.location = location;
+ this.direct = direct;
+ this.tolerance = tolerance;
+ }
+
+ /** 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;
+ }
+
+ /** Get the offset (oriented distance) of a vector.
+ * @param vector vector to check
+ * @return offset of the vector
+ */
+ public double getOffset(Vector<Euclidean1D> vector) {
+ return getOffset((Point<Euclidean1D>) vector);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getOffset(final Point<Euclidean1D> point) {
+ final double delta = ((Cartesian1D) point).getX() - location.getX();
+ return direct ? 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
+ */
+ @Override
+ public SubOrientedPoint wholeHyperplane() {
+ return new SubOrientedPoint(this, null);
+ }
+
+ /** Build a region covering the whole space.
+ * @return a region containing the instance (really an {@link
+ * IntervalsSet IntervalsSet} instance)
+ */
+ @Override
+ public IntervalsSet wholeSpace() {
+ return new IntervalsSet(tolerance);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean sameOrientationAs(final Hyperplane<Euclidean1D> other) {
+ return !(direct ^ ((OrientedPoint) other).direct);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point<Euclidean1D> project(Point<Euclidean1D> point) {
+ return location;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getTolerance() {
+ return tolerance;
+ }
+
+ /** Get the hyperplane location on the real line.
+ * @return the hyperplane location
+ */
+ public Cartesian1D getLocation() {
+ return location;
+ }
+
+ /** Check if the hyperplane orientation is direct.
+ * @return true if the plus side of the hyperplane is towards
+ * abscissae greater than hyperplane location
+ */
+ public boolean isDirect() {
+ return direct;
+ }
+
+ /** Revert the instance.
+ */
+ public void revertSelf() {
+ direct = !direct;
+ }
+
+}
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
new file mode 100644
index 0000000..8e05e5a
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPoint.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+/** 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<Euclidean1D, Euclidean1D> {
+
+ /** Simple constructor.
+ * @param hyperplane underlying hyperplane
+ * @param remainingRegion remaining region of the hyperplane
+ */
+ public SubOrientedPoint(final Hyperplane<Euclidean1D> hyperplane,
+ final Region<Euclidean1D> remainingRegion) {
+ super(hyperplane, remainingRegion);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected AbstractSubHyperplane<Euclidean1D, Euclidean1D> buildNew(final Hyperplane<Euclidean1D> hyperplane,
+ final Region<Euclidean1D> remainingRegion) {
+ return new SubOrientedPoint(hyperplane, remainingRegion);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SplitSubHyperplane<Euclidean1D> split(final Hyperplane<Euclidean1D> hyperplane) {
+ final OrientedPoint thisHyperplane = (OrientedPoint) getHyperplane();
+ final double global = hyperplane.getOffset(thisHyperplane.getLocation());
+
+ // use the tolerance value from our parent hyperplane to determine equality
+ final double tolerance = thisHyperplane.getTolerance();
+
+ if (global < -tolerance) {
+ return new SplitSubHyperplane<Euclidean1D>(null, this);
+ } else if (global > tolerance) {
+ return new SplitSubHyperplane<Euclidean1D>(this, null);
+ } else {
+ return new SplitSubHyperplane<Euclidean1D>(null, null);
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..e9fdaf1
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
@@ -0,0 +1,31 @@
+/*
+ * 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.Vector;
+
+/** This class represents a 1D vector.
+ */
+public abstract class Vector1D implements Vector<Euclidean1D> {
+
+ /** Get the abscissa of the vector.
+ * @return abscissa of the vector
+ * @see Cartesian1D#Cartesian1D(double)
+ */
+ public abstract double getX();
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/package-info.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/package-info.java
new file mode 100644
index 0000000..f987815
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * <p>
+ * This package provides basic 1D geometry components.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.euclidean.oned;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java
new file mode 100644
index 0000000..79b975f
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java
@@ -0,0 +1,621 @@
+/*
+ * 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.text.NumberFormat;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.numbers.arrays.LinearCombination;
+
+/**
+ * This class represents points or vectors in a three-dimensional space.
+ * <p>An instance of Cartesian3D represents the point with the corresponding
+ * coordinates.</p>
+ * <p>An instance of Cartesian3D also represents the vector which begins at
+ * the origin and ends at the point corresponding to the coordinates.</p>
+ * <p>Instance of this class are guaranteed to be immutable.</p>
+ */
+public class Cartesian3D extends Vector3D implements Serializable, Point<Euclidean3D> {
+
+ /** Null vector (coordinates: 0, 0, 0). */
+ public static final Cartesian3D ZERO = new Cartesian3D(0, 0, 0);
+
+ /** First canonical vector (coordinates: 1, 0, 0). */
+ public static final Cartesian3D PLUS_I = new Cartesian3D(1, 0, 0);
+
+ /** Opposite of the first canonical vector (coordinates: -1, 0, 0). */
+ public static final Cartesian3D MINUS_I = new Cartesian3D(-1, 0, 0);
+
+ /** Second canonical vector (coordinates: 0, 1, 0). */
+ public static final Cartesian3D PLUS_J = new Cartesian3D(0, 1, 0);
+
+ /** Opposite of the second canonical vector (coordinates: 0, -1, 0). */
+ public static final Cartesian3D MINUS_J = new Cartesian3D(0, -1, 0);
+
+ /** Third canonical vector (coordinates: 0, 0, 1). */
+ public static final Cartesian3D PLUS_K = new Cartesian3D(0, 0, 1);
+
+ /** Opposite of the third canonical vector (coordinates: 0, 0, -1). */
+ public static final Cartesian3D MINUS_K = new Cartesian3D(0, 0, -1);
+
+ // CHECKSTYLE: stop ConstantName
+ /** A vector with all coordinates set to NaN. */
+ public static final Cartesian3D NaN = new Cartesian3D(Double.NaN, Double.NaN, Double.NaN);
+ // CHECKSTYLE: resume ConstantName
+
+ /** A vector with all coordinates set to positive infinity. */
+ public static final Cartesian3D POSITIVE_INFINITY =
+ new Cartesian3D(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+ /** A vector with all coordinates set to negative infinity. */
+ public static final Cartesian3D NEGATIVE_INFINITY =
+ new Cartesian3D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
+
+ /** Serializable version identifier. */
+ private static final long serialVersionUID = 1313493323784566947L;
+
+ /** Error message when norms are zero. */
+ private static final String ZERO_NORM_MSG = "Norm is zero";
+
+ /** Abscissa. */
+ private final double x;
+
+ /** Ordinate. */
+ private final double y;
+
+ /** Height. */
+ private final double z;
+
+ /** Simple constructor.
+ * Build a vector from its coordinates
+ * @param x abscissa
+ * @param y ordinate
+ * @param z height
+ * @see #getX()
+ * @see #getY()
+ * @see #getZ()
+ */
+ public Cartesian3D(double x, double y, double z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ /** Simple constructor.
+ * Build a vector from its coordinates
+ * @param v coordinates array
+ * @exception DimensionMismatchException if array does not have 3 elements
+ * @see #toArray()
+ */
+ public Cartesian3D(double[] v) throws IllegalArgumentException {
+ if (v.length != 3) {
+ throw new IllegalArgumentException("Dimension mismatch: " + v.length + " != 3");
+ }
+ this.x = v[0];
+ this.y = v[1];
+ this.z = v[2];
+ }
+
+ /** Simple constructor.
+ * Build a vector from its azimuthal coordinates
+ * @param alpha azimuth (α) around Z
+ * (0 is +X, π/2 is +Y, π is -X and 3π/2 is -Y)
+ * @param delta elevation (δ) above (XY) plane, from -π/2 to +π/2
+ * @see #getAlpha()
+ * @see #getDelta()
+ */
+ public Cartesian3D(double alpha, double delta) {
+ double cosDelta = Math.cos(delta);
+ this.x = Math.cos(alpha) * cosDelta;
+ this.y = Math.sin(alpha) * cosDelta;
+ this.z = Math.sin(delta);
+ }
+
+ /** Multiplicative constructor
+ * Build a vector from another one and a scale factor.
+ * The vector built will be a * u
+ * @param a scale factor
+ * @param u base (unscaled) vector
+ */
+ public Cartesian3D(double a, Cartesian3D u) {
+ this.x = a * u.x;
+ this.y = a * u.y;
+ this.z = a * u.z;
+ }
+
+ /** Linear constructor
+ * Build a vector from two other ones and corresponding scale factors.
+ * The vector built will be a1 * u1 + a2 * u2
+ * @param a1 first scale factor
+ * @param u1 first base (unscaled) vector
+ * @param a2 second scale factor
+ * @param u2 second base (unscaled) vector
+ */
+ public Cartesian3D(double a1, Cartesian3D u1, double a2, Cartesian3D u2) {
+ this.x = LinearCombination.value(a1, u1.x, a2, u2.x);
+ this.y = LinearCombination.value(a1, u1.y, a2, u2.y);
+ this.z = LinearCombination.value(a1, u1.z, a2, u2.z);
+ }
+
+ /** Linear constructor
+ * Build a vector from three other ones and corresponding scale factors.
+ * The vector built will be a1 * u1 + a2 * u2 + a3 * u3
+ * @param a1 first scale factor
+ * @param u1 first base (unscaled) vector
+ * @param a2 second scale factor
+ * @param u2 second base (unscaled) vector
+ * @param a3 third scale factor
+ * @param u3 third base (unscaled) vector
+ */
+ public Cartesian3D(double a1, Cartesian3D u1, double a2, Cartesian3D u2,
+ double a3, Cartesian3D u3) {
+ this.x = LinearCombination.value(a1, u1.x, a2, u2.x, a3, u3.x);
+ this.y = LinearCombination.value(a1, u1.y, a2, u2.y, a3, u3.y);
+ this.z = LinearCombination.value(a1, u1.z, a2, u2.z, a3, u3.z);
+ }
+
+ /** Linear constructor
+ * Build a vector from four other ones and corresponding scale factors.
+ * The vector built will be a1 * u1 + a2 * u2 + a3 * u3 + a4 * u4
+ * @param a1 first scale factor
+ * @param u1 first base (unscaled) vector
+ * @param a2 second scale factor
+ * @param u2 second base (unscaled) vector
+ * @param a3 third scale factor
+ * @param u3 third base (unscaled) vector
+ * @param a4 fourth scale factor
+ * @param u4 fourth base (unscaled) vector
+ */
+ public Cartesian3D(double a1, Cartesian3D u1, double a2, Cartesian3D u2,
+ double a3, Cartesian3D u3, double a4, Cartesian3D u4) {
+ this.x = LinearCombination.value(a1, u1.x, a2, u2.x, a3, u3.x, a4, u4.x);
+ this.y = LinearCombination.value(a1, u1.y, a2, u2.y, a3, u3.y, a4, u4.y);
+ this.z = LinearCombination.value(a1, u1.z, a2, u2.z, a3, u3.z, a4, u4.z);
+ }
+
+ /** Get the abscissa of the vector.
+ * @return abscissa of the vector
+ * @see #Cartesian3D(double, double, double)
+ */
+ @Override
+ public double getX() {
+ return x;
+ }
+
+ /** Get the ordinate of the vector.
+ * @return ordinate of the vector
+ * @see #Cartesian3D(double, double, double)
+ */
+ @Override
+ public double getY() {
+ return y;
+ }
+
+ /** Get the height of the vector.
+ * @return height of the vector
+ * @see #Cartesian3D(double, double, double)
+ */
+ @Override
+ public double getZ() {
+ return z;
+ }
+
+ /** Get the vector coordinates as a dimension 3 array.
+ * @return vector coordinates
+ * @see #Cartesian3D(double[])
+ */
+ public double[] toArray() {
+ return new double[] { x, y, z };
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Space getSpace() {
+ return Euclidean3D.getInstance();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian3D getZero() {
+ return ZERO;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getNorm1() {
+ return Math.abs(x) + Math.abs(y) + Math.abs(z);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getNorm() {
+ // there are no cancellation problems here, so we use the straightforward formula
+ return Math.sqrt (x * x + y * y + z * z);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getNormSq() {
+ // there are no cancellation problems here, so we use the straightforward formula
+ return x * x + y * y + z * z;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getNormInf() {
+ return Math.max(Math.max(Math.abs(x), Math.abs(y)), Math.abs(z));
+ }
+
+ /** Get the azimuth of the vector.
+ * @return azimuth (α) of the vector, between -π and +π
+ * @see #Cartesian3D(double, double)
+ */
+ public double getAlpha() {
+ return Math.atan2(y, x);
+ }
+
+ /** Get the elevation of the vector.
+ * @return elevation (δ) of the vector, between -π/2 and +π/2
+ * @see #Cartesian3D(double, double)
+ */
+ public double getDelta() {
+ return Math.asin(z / getNorm());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian3D add(final Vector<Euclidean3D> v) {
+ final Cartesian3D v3 = (Cartesian3D) v;
+ return new Cartesian3D(x + v3.x, y + v3.y, z + v3.z);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian3D add(double factor, final Vector<Euclidean3D> v) {
+ return new Cartesian3D(1, this, factor, (Cartesian3D) v);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian3D subtract(final Vector<Euclidean3D> v) {
+ final Cartesian3D v3 = (Cartesian3D) v;
+ return new Cartesian3D(x - v3.x, y - v3.y, z - v3.z);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian3D subtract(final double factor, final Vector<Euclidean3D> v) {
+ return new Cartesian3D(1, this, -factor, (Cartesian3D) v);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian3D normalize() throws IllegalStateException {
+ double s = getNorm();
+ if (s == 0) {
+ throw new IllegalStateException(ZERO_NORM_MSG);
+ }
+ return scalarMultiply(1 / s);
+ }
+
+ /** Get a vector orthogonal to the instance.
+ * <p>There are an infinite number of normalized vectors orthogonal
+ * to the instance. This method picks up one of them almost
+ * arbitrarily. It is useful when one needs to compute a reference
+ * frame with one of the axes in a predefined direction. The
+ * following example shows how to build a frame having the k axis
+ * aligned with the known vector u :
+ * <pre><code>
+ * Cartesian3D k = u.normalize();
+ * Cartesian3D i = k.orthogonal();
+ * Cartesian3D j = Cartesian3D.crossProduct(k, i);
+ * </code></pre>
+ * @return a new normalized vector orthogonal to the instance
+ * @exception IllegalStateException if the norm of the instance is zero
+ */
+ public Cartesian3D orthogonal() throws IllegalStateException {
+
+ double threshold = 0.6 * getNorm();
+ if (threshold == 0) {
+ throw new IllegalStateException(ZERO_NORM_MSG);
+ }
+
+ if (Math.abs(x) <= threshold) {
+ double inverse = 1 / Math.sqrt(y * y + z * z);
+ return new Cartesian3D(0, inverse * z, -inverse * y);
+ } else if (Math.abs(y) <= threshold) {
+ double inverse = 1 / Math.sqrt(x * x + z * z);
+ return new Cartesian3D(-inverse * z, 0, inverse * x);
+ }
+ double inverse = 1 / Math.sqrt(x * x + y * y);
+ return new Cartesian3D(inverse * y, -inverse * x, 0);
+
+ }
+
+ /** Compute the angular separation between two vectors.
+ * <p>This method computes the angular separation between two
+ * vectors using the dot product for well separated vectors and the
+ * cross product for almost aligned vectors. This allows to have a
+ * good accuracy in all cases, even for vectors very close to each
+ * other.</p>
+ * @param v1 first vector
+ * @param v2 second vector
+ * @return angular separation between v1 and v2
+ * @exception IllegalArgumentException if either vector has a zero norm
+ */
+ public static double angle(Cartesian3D v1, Cartesian3D v2) throws IllegalArgumentException {
+
+ double normProduct = v1.getNorm() * v2.getNorm();
+ if (normProduct == 0) {
+ throw new IllegalArgumentException(ZERO_NORM_MSG);
+ }
+
+ double dot = v1.dotProduct(v2);
+ double threshold = normProduct * 0.9999;
+ if ((dot < -threshold) || (dot > threshold)) {
+ // the vectors are almost aligned, compute using the sine
+ Cartesian3D v3 = crossProduct(v1, v2);
+ if (dot >= 0) {
+ return Math.asin(v3.getNorm() / normProduct);
+ }
+ return Math.PI - Math.asin(v3.getNorm() / normProduct);
+ }
+
+ // the vectors are sufficiently separated to use the cosine
+ return Math.acos(dot / normProduct);
+
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian3D negate() {
+ return new Cartesian3D(-x, -y, -z);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cartesian3D scalarMultiply(double a) {
+ return new Cartesian3D(a * x, a * y, a * z);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isNaN() {
+ return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isInfinite() {
+ return !isNaN() && (Double.isInfinite(x) || Double.isInfinite(y) || Double.isInfinite(z));
+ }
+
+ /**
+ * Test for the equality of two 3D vectors.
+ * <p>
+ * If all coordinates of two 3D vectors are exactly the same, and none are
+ * <code>Double.NaN</code>, the two 3D vectors 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
+ * 3D vector are equal to <code>Double.NaN</code>, the 3D vector is equal to
+ * {@link #NaN}.
+ * </p>
+ *
+ * @param other Object to test for equality to this
+ * @return true if two 3D vector objects are equal, false if
+ * object is null, not an instance of Cartesian3D, or
+ * not equal to this Cartesian3D instance
+ *
+ */
+ @Override
+ public boolean equals(Object other) {
+
+ if (this == other) {
+ return true;
+ }
+
+ if (other instanceof Cartesian3D) {
+ final Cartesian3D rhs = (Cartesian3D)other;
+ if (rhs.isNaN()) {
+ return this.isNaN();
+ }
+
+ return (x == rhs.x) && (y == rhs.y) && (z == rhs.z);
+ }
+ return false;
+ }
+
+ /**
+ * Get a hashCode for the 3D 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 642;
+ }
+ return 643 * (164 * Double.hashCode(x) + 3 * Double.hashCode(y) + Double.hashCode(z));
+ }
+
+ /** {@inheritDoc}
+ * <p>
+ * The implementation uses specific multiplication and addition
+ * algorithms to preserve accuracy and reduce cancellation effects.
+ * It should be very accurate even for nearly orthogonal vectors.
+ * </p>
+ * @see LinearCombination#value(double, double, double, double, double, double)
+ */
+ @Override
+ public double dotProduct(final Vector<Euclidean3D> v) {
+ final Cartesian3D v3 = (Cartesian3D) v;
+ return LinearCombination.value(x, v3.x, y, v3.y, z, v3.z);
+ }
+
+ /** Compute the cross-product of the instance with another vector.
+ * @param v other vector
+ * @return the cross product this ^ v as a new Cartesian3D
+ */
+ public Cartesian3D crossProduct(final Vector<Euclidean3D> v) {
+ final Cartesian3D v3 = (Cartesian3D) v;
+ return new Cartesian3D(LinearCombination.value(y, v3.z, -z, v3.y),
+ LinearCombination.value(z, v3.x, -x, v3.z),
+ LinearCombination.value(x, v3.y, -y, v3.x));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance1(Vector<Euclidean3D> v) {
+ final Cartesian3D v3 = (Cartesian3D) v;
+ final double dx = Math.abs(v3.x - x);
+ final double dy = Math.abs(v3.y - y);
+ final double dz = Math.abs(v3.z - z);
+ return dx + dy + dz;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance(Point<Euclidean3D> p) {
+ return distance((Cartesian3D) p);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distance(Vector<Euclidean3D> v) {
+ return distance((Cartesian3D) v);
+ }
+
+ /** Compute the distance between the instance and other coordinates.
+ * @param c other coordinates
+ * @return the distance between the instance and c
+ */
+ public double distance(Cartesian3D c) {
+ final double dx = c.x - x;
+ final double dy = c.y - y;
+ final double dz = c.z - z;
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distanceInf(Vector<Euclidean3D> v) {
+ final Cartesian3D v3 = (Cartesian3D) v;
+ final double dx = Math.abs(v3.x - x);
+ final double dy = Math.abs(v3.y - y);
+ final double dz = Math.abs(v3.z - z);
+ return Math.max(Math.max(dx, dy), dz);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double distanceSq(Vector<Euclidean3D> v) {
+ final Cartesian3D v3 = (Cartesian3D) v;
+ final double dx = v3.x - x;
+ final double dy = v3.y - y;
+ final double dz = v3.z - z;
+ return dx * dx + dy * dy + dz * dz;
+ }
+
+ /** Compute the dot-product of two vectors.
+ * @param v1 first vector
+ * @param v2 second vector
+ * @return the dot product v1.v2
+ */
+ public static double dotProduct(Cartesian3D v1, Cartesian3D v2) {
+ return v1.dotProduct(v2);
+ }
+
+ /** Compute the cross-product of two vectors.
+ * @param v1 first vector
+ * @param v2 second vector
+ * @return the cross product v1 ^ v2 as a new Vector
+ */
+ public static Cartesian3D crossProduct(final Cartesian3D v1, final Cartesian3D v2) {
+ return v1.crossProduct(v2);
+ }
+
+ /** Compute the distance between two vectors according to the L<sub>1</sub> norm.
+ * <p>Calling this method is equivalent to calling:
+ * <code>v1.subtract(v2).getNorm1()</code> except that no intermediate
+ * vector is built</p>
+ * @param v1 first vector
+ * @param v2 second vector
+ * @return the distance between v1 and v2 according to the L<sub>1</sub> norm
+ */
+ public static double distance1(Cartesian3D v1, Cartesian3D v2) {
+ return v1.distance1(v2);
+ }
+
+ /** Compute the distance between two vectors according to the L<sub>2</sub> norm.
+ * <p>Calling this method is equivalent to calling:
+ * <code>v1.subtract(v2).getNorm()</code> except that no intermediate
+ * vector is built</p>
+ * @param v1 first vector
+ * @param v2 second vector
+ * @return the distance between v1 and v2 according to the L<sub>2</sub> norm
+ */
+ public static double distance(Cartesian3D v1, Cartesian3D v2) {
+ return v1.distance(v2);
+ }
+
+ /** Compute the distance between two vectors according to the L<sub>∞</sub> norm.
+ * <p>Calling this method is equivalent to calling:
+ * <code>v1.subtract(v2).getNormInf()</code> except that no intermediate
+ * vector is built</p>
+ * @param v1 first vector
+ * @param v2 second vector
+ * @return the distance between v1 and v2 according to the L<sub>∞</sub> norm
+ */
+ public static double distanceInf(Cartesian3D v1, Cartesian3D v2) {
+ return v1.distanceInf(v2);
+ }
+
+ /** Compute the square of the distance between two vectors.
+ * <p>Calling this method is equivalent to calling:
+ * <code>v1.subtract(v2).getNormSq()</code> except that no intermediate
+ * vector is built</p>
+ * @param v1 first vector
+ * @param v2 second vector
+ * @return the square of the distance between v1 and v2
+ */
+ public static double distanceSq(Cartesian3D v1, Cartesian3D v2) {
+ return v1.distanceSq(v2);
+ }
+
+ /** Get a string representation of this vector.
+ * @return a string representation of this vector
+ */
+ @Override
+ public String toString() {
+ return toString(NumberFormat.getInstance());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString(final NumberFormat format) {
+ return "{" + format.format(x) + "; " + format.format(y) + "; " + format.format(z) + "}";
+ }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Euclidean3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Euclidean3D.java
new file mode 100644
index 0000000..4988476
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Euclidean3D.java
@@ -0,0 +1,75 @@
+/*
+ * 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.Space;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+
+/**
+ * This class implements a three-dimensional space.
+ */
+public class Euclidean3D implements Serializable, Space {
+
+ /** Serializable version identifier. */
+ private static final long serialVersionUID = 6249091865814886817L;
+
+ /** Private constructor for the singleton.
+ */
+ private Euclidean3D() {
+ }
+
+ /** Get the unique instance.
+ * @return the unique instance
+ */
+ public static Euclidean3D getInstance() {
+ return LazyHolder.INSTANCE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getDimension() {
+ return 3;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Euclidean2D getSubSpace() {
+ return Euclidean2D.getInstance();
+ }
+
+ // CHECKSTYLE: stop HideUtilityClassConstructor
+ /** Holder for the instance.
+ * <p>We use here the Initialization On Demand Holder Idiom.</p>
+ */
+ private static class LazyHolder {
+ /** Cached field instance. */
+ private static final Euclidean3D INSTANCE = new Euclidean3D();
+ }
+ // CHECKSTYLE: resume HideUtilityClassConstructor
+
+ /** Handle deserialization of the singleton.
+ * @return the singleton instance
+ */
+ private Object readResolve() {
+ // return the singleton instance
+ return LazyHolder.INSTANCE;
+ }
+
+}
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
new file mode 100644
index 0000000..2024032
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line.java
@@ -0,0 +1,274 @@
+/*
+ * 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.Point;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.geometry.core.partitioning.Embedding;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.geometry.euclidean.oned.Euclidean1D;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+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<Euclidean3D, Euclidean1D> {
+
+ /** Line direction. */
+ private Cartesian3D direction;
+
+ /** Line point closest to the origin. */
+ private Cartesian3D zero;
+
+ /** Tolerance below which points are considered identical. */
+ private final double tolerance;
+
+ /** 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 tolerance tolerance below which points are considered identical
+ * @exception IllegalArgumentException if the points are equal
+ */
+ public Line(final Cartesian3D p1, final Cartesian3D p2, final double tolerance)
+ throws IllegalArgumentException {
+ reset(p1, p2);
+ this.tolerance = tolerance;
+ }
+
+ /** 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.tolerance = line.tolerance;
+ }
+
+ /** 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 Cartesian3D p1, final Cartesian3D p2) throws IllegalStateException {
+ final Cartesian3D delta = p2.subtract(p1);
+ final double norm2 = delta.getNormSq();
+ if (norm2 == 0.0) {
+ throw new IllegalArgumentException("Points are equal");
+ }
+ this.direction = new Cartesian3D(1.0 / Math.sqrt(norm2), delta);
+ zero = new Cartesian3D(1.0, p1, -p1.dotProduct(delta) / norm2, delta);
+ }
+
+ /** Get the tolerance below which points are considered identical.
+ * @return tolerance below which points are considered identical
+ */
+ public double getTolerance() {
+ return tolerance;
+ }
+
+ /** 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 Cartesian3D getDirection() {
+ return direction;
+ }
+
+ /** Get the line point closest to the origin.
+ * @return line point closest to the origin
+ */
+ public Cartesian3D 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 Cartesian3D point) {
+ return point.subtract(zero).dotProduct(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 Cartesian3D pointAt(final double abscissa) {
+ return new Cartesian3D(1.0, zero, abscissa, direction);
+ }
+
+ /** Transform a space point into a sub-space point.
+ * @param vector n-dimension point of the space
+ * @return (n-1)-dimension point of the sub-space corresponding to
+ * the specified space point
+ */
+ public Cartesian1D toSubSpace(Vector<Euclidean3D> vector) {
+ return toSubSpace((Point<Euclidean3D>) vector);
+ }
+
+ /** Transform a sub-space point into a space point.
+ * @param vector (n-1)-dimension point of the sub-space
+ * @return n-dimension point of the space corresponding to the
+ * specified sub-space point
+ */
+ public Cartesian3D toSpace(Vector<Euclidean1D> vector) {
+ return toSpace((Point<Euclidean1D>) vector);
+ }
+
+ /** {@inheritDoc}
+ * @see #getAbscissa(Cartesian3D)
+ */
+ @Override
+ public Cartesian1D toSubSpace(final Point<Euclidean3D> point) {
+ return toSubSpace((Cartesian3D) point);
+ }
+
+ /** {@inheritDoc}
+ * @see #pointAt(double)
+ */
+ @Override
+ public Cartesian3D toSpace(final Point<Euclidean1D> point) {
+ return toSpace((Cartesian1D) point);
+ }
+
+ /** 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
+ */
+ public Cartesian1D toSubSpace(final Cartesian3D point) {
+ return new Cartesian1D(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
+ */
+ public Cartesian3D toSpace(final Cartesian1D 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 = Cartesian3D.angle(direction, line.direction);
+ return ((angle < tolerance) || (angle > (Math.PI - tolerance))) && 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 Cartesian3D p) {
+ return distance(p) < tolerance;
+ }
+
+ /** 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 Cartesian3D p) {
+ final Cartesian3D d = p.subtract(zero);
+ final Cartesian3D n = new Cartesian3D(1.0, d, -d.dotProduct(direction), direction);
+ return n.getNorm();
+ }
+
+ /** 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 Cartesian3D normal = Cartesian3D.crossProduct(direction, line.direction);
+ final double n = normal.getNorm();
+ 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).dotProduct(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 Cartesian3D closestPoint(final Line line) {
+
+ final double cos = direction.dotProduct(line.direction);
+ final double n = 1 - cos * cos;
+ if (n < Precision.EPSILON) {
+ // the lines are parallel
+ return zero;
+ }
+
+ final Cartesian3D delta0 = line.zero.subtract(zero);
+ final double a = delta0.dotProduct(direction);
+ final double b = delta0.dotProduct(line.direction);
+
+ return new Cartesian3D(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 Cartesian3D intersection(final Line line) {
+ final Cartesian3D 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(tolerance));
+ }
+
+}
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
new file mode 100644
index 0000000..3f25191
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.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.euclidean.threed;
+
+import java.util.ArrayList;
+
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.core.Point;
+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.euclidean.twod.Cartesian2D;
+
+/** 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 Cartesian3D u;
+
+ /** Ordinate axis of the projection plane. */
+ private final Cartesian3D v;
+
+ /** Normal of the projection plane (viewing direction). */
+ private final Cartesian3D 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 Cartesian3D u, final Cartesian3D v) {
+ this.u = u;
+ this.v = v;
+ w = Cartesian3D.crossProduct(u, 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 Cartesian2D[][] getOutline(final PolyhedronsSet polyhedronsSet) {
+
+ // project all boundary facets into one polygons set
+ final BoundaryProjector projector = new BoundaryProjector(polyhedronsSet.getTolerance());
+ polyhedronsSet.getTree(true).visit(projector);
+ final PolygonsSet projected = projector.getProjected();
+
+ // Remove the spurious intermediate vertices from the outline
+ final Cartesian2D[][] outline = projected.getVertices();
+ for (int i = 0; i < outline.length; ++i) {
+ final Cartesian2D[] 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 Cartesian2D[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 Cartesian2D[] loop, final int n, final int i) {
+ final Cartesian2D previous = loop[(i + n - 1) % n];
+ final Cartesian2D current = loop[i];
+ final Cartesian2D 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<Euclidean3D> {
+
+ /** Projection of the polyhedrons set on the plane. */
+ private PolygonsSet projected;
+
+ /** Tolerance below which points are considered identical. */
+ private final double tolerance;
+
+ /** Simple constructor.
+ * @param tolerance tolerance below which points are considered identical
+ */
+ BoundaryProjector(final double tolerance) {
+ this.projected = new PolygonsSet(new BSPTree<Euclidean2D>(Boolean.FALSE), tolerance);
+ this.tolerance = tolerance;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Order visitOrder(final BSPTree<Euclidean3D> node) {
+ return Order.MINUS_SUB_PLUS;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void visitInternalNode(final BSPTree<Euclidean3D> node) {
+ @SuppressWarnings("unchecked")
+ final BoundaryAttribute<Euclidean3D> attribute =
+ (BoundaryAttribute<Euclidean3D>) 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<Euclidean3D> 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<Euclidean3D> facet, final boolean reversed) {
+
+ // extract the vertices of the facet
+ @SuppressWarnings("unchecked")
+ final AbstractSubHyperplane<Euclidean3D, Euclidean2D> absFacet =
+ (AbstractSubHyperplane<Euclidean3D, Euclidean2D>) facet;
+ final Plane plane = (Plane) facet.getHyperplane();
+
+ final double scal = plane.getNormal().dotProduct(w);
+ if (Math.abs(scal) > 1.0e-3) {
+ Cartesian2D[][] vertices =
+ ((PolygonsSet) absFacet.getRemainingRegion()).getVertices();
+
+ if ((scal < 0) ^ reversed) {
+ // the facet is seen from the inside,
+ // we need to invert its boundary orientation
+ final Cartesian2D[][] newVertices = new Cartesian2D[vertices.length][];
+ for (int i = 0; i < vertices.length; ++i) {
+ final Cartesian2D[] loop = vertices[i];
+ final Cartesian2D[] newLoop = new Cartesian2D[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<Euclidean2D>> edges = new ArrayList<>();
+ for (Cartesian2D[] loop : vertices) {
+ final boolean closed = loop[0] != null;
+ int previous = closed ? (loop.length - 1) : 1;
+ Cartesian3D previous3D = plane.toSpace(loop[previous]);
+ int current = (previous + 1) % loop.length;
+ Cartesian2D pPoint = new Cartesian2D(previous3D.dotProduct(u),
+ previous3D.dotProduct(v));
+ while (current < loop.length) {
+
+ final Cartesian3D current3D = plane.toSpace((Point<Euclidean2D>) loop[current]);
+ final Cartesian2D cPoint = new Cartesian2D(current3D.dotProduct(u),
+ current3D.dotProduct(v));
+ final org.apache.commons.geometry.euclidean.twod.Line line =
+ new org.apache.commons.geometry.euclidean.twod.Line(pPoint, cPoint, tolerance);
+ SubHyperplane<Euclidean2D> 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 =
+ new org.apache.commons.geometry.euclidean.twod.Line(pPoint, angle, tolerance);
+ 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 =
+ new org.apache.commons.geometry.euclidean.twod.Line(cPoint, angle, tolerance);
+ edge = edge.split(l).getMinus();
+ }
+
+ edges.add(edge);
+
+ previous = current++;
+ previous3D = current3D;
+ pPoint = cPoint;
+
+ }
+ }
+ final PolygonsSet projectedFacet = new PolygonsSet(edges, tolerance);
+
+ // add the contribution of the facet to the global outline
+ projected = (PolygonsSet) new RegionFactory<Euclidean2D>().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
new file mode 100644
index 0000000..8a54620
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
@@ -0,0 +1,498 @@
+/*
+ * 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.Point;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.geometry.core.partitioning.Embedding;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+
+/** The class represent planes in a three dimensional space.
+ */
+public class Plane implements Hyperplane<Euclidean3D>, Embedding<Euclidean3D, Euclidean2D> {
+
+ /** Offset of the origin with respect to the plane. */
+ private double originOffset;
+
+ /** Origin of the plane frame. */
+ private Cartesian3D origin;
+
+ /** First vector of the plane frame (in plane). */
+ private Cartesian3D u;
+
+ /** Second vector of the plane frame (in plane). */
+ private Cartesian3D v;
+
+ /** Third vector of the plane frame (plane normal). */
+ private Cartesian3D w;
+
+ /** Tolerance below which points are considered identical. */
+ private final double tolerance;
+
+ /** Build a plane normal to a given direction and containing the origin.
+ * @param normal normal direction to the plane
+ * @param tolerance tolerance below which points are considered identical
+ * @exception IllegalArgumentException if the normal norm is too small
+ */
+ public Plane(final Cartesian3D normal, final double tolerance)
+ throws IllegalArgumentException {
+ setNormal(normal);
+ this.tolerance = tolerance;
+ originOffset = 0;
+ setFrame();
+ }
+
+ /** Build a plane from a point and a normal.
+ * @param p point belonging to the plane
+ * @param normal normal direction to the plane
+ * @param tolerance tolerance below which points are considered identical
+ * @exception IllegalArgumentException if the normal norm is too small
+ */
+ public Plane(final Cartesian3D p, final Cartesian3D normal, final double tolerance)
+ throws IllegalArgumentException {
+ setNormal(normal);
+ this.tolerance = tolerance;
+ originOffset = -p.dotProduct(w);
+ setFrame();
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical
+ * @exception IllegalArgumentException if the points do not constitute a plane
+ */
+ public Plane(final Cartesian3D p1, final Cartesian3D p2, final Cartesian3D p3, final double tolerance)
+ throws IllegalArgumentException {
+ this(p1, p2.subtract(p1).crossProduct(p3.subtract(p1)), tolerance);
+ }
+
+ /** Copy constructor.
+ * <p>The instance created is completely independent of the original
+ * one. A deep copy is used, none of the underlying object are
+ * shared.</p>
+ * @param plane plane to copy
+ */
+ public Plane(final Plane plane) {
+ originOffset = plane.originOffset;
+ origin = plane.origin;
+ u = plane.u;
+ v = plane.v;
+ w = plane.w;
+ tolerance = plane.tolerance;
+ }
+
+ /** 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
+ */
+ @Override
+ public Plane copySelf() {
+ return new Plane(this);
+ }
+
+ /** Reset the instance as if built from a point and a normal.
+ * @param p point belonging to the plane
+ * @param normal normal direction to the plane
+ * @exception IllegalArgumentException if the normal norm is too small
+ */
+ public void reset(final Cartesian3D p, final Cartesian3D normal) throws IllegalArgumentException {
+ setNormal(normal);
+ originOffset = -p.dotProduct(w);
+ setFrame();
+ }
+
+ /** Reset the instance from another one.
+ * <p>The updated instance is completely independant of the original
+ * one. A deep reset is used none of the underlying object is
+ * shared.</p>
+ * @param original plane to reset from
+ */
+ public void reset(final Plane original) {
+ originOffset = original.originOffset;
+ origin = original.origin;
+ u = original.u;
+ v = original.v;
+ w = original.w;
+ }
+
+ /** Set the normal vactor.
+ * @param normal normal direction to the plane (will be copied)
+ * @exception IllegalArgumentException if the normal norm is too close to zero
+ */
+ private void setNormal(final Cartesian3D normal) throws IllegalArgumentException {
+ final double norm = normal.getNorm();
+ if (norm < 1.0e-10) {
+ throw new IllegalArgumentException("Norm is zero");
+ }
+ w = new Cartesian3D(1.0 / norm, normal);
+ }
+
+ /** Reset the plane frame.
+ */
+ private void setFrame() {
+ origin = new Cartesian3D(-originOffset, w);
+ u = w.orthogonal();
+ v = Cartesian3D.crossProduct(w, u);
+ }
+
+ /** Get the origin point of the plane frame.
+ * <p>The point returned is the orthogonal projection of the
+ * 3D-space origin in the plane.</p>
+ * @return the origin point of the plane frame (point closest to the
+ * 3D-space origin)
+ */
+ public Cartesian3D getOrigin() {
+ return origin;
+ }
+
+ /** Get the normalized normal vector.
+ * <p>The frame defined by ({@link #getU getU}, {@link #getV getV},
+ * {@link #getNormal getNormal}) is a rigth-handed orthonormalized
+ * frame).</p>
+ * @return normalized normal vector
+ * @see #getU
+ * @see #getV
+ */
+ public Cartesian3D getNormal() {
+ return w;
+ }
+
+ /** Get the plane first canonical vector.
+ * <p>The frame defined by ({@link #getU getU}, {@link #getV getV},
+ * {@link #getNormal getNormal}) is a rigth-handed orthonormalized
+ * frame).</p>
+ * @return normalized first canonical vector
+ * @see #getV
+ * @see #getNormal
+ */
+ public Cartesian3D getU() {
+ return u;
+ }
+
+ /** Get the plane second canonical vector.
+ * <p>The frame defined by ({@link #getU getU}, {@link #getV getV},
+ * {@link #getNormal getNormal}) is a rigth-handed orthonormalized
+ * frame).</p>
+ * @return normalized second canonical vector
+ * @see #getU
+ * @see #getNormal
+ */
+ public Cartesian3D getV() {
+ return v;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Point<Euclidean3D> project(Point<Euclidean3D> point) {
+ return toSpace(toSubSpace(point));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getTolerance() {
+ return tolerance;
+ }
+
+ /** Revert the plane.
+ * <p>Replace the instance by a similar plane with opposite orientation.</p>
+ * <p>The new plane frame is chosen in such a way that a 3D point that had
+ * {@code (x, y)} in-plane coordinates and {@code z} offset with
+ * respect to the plane and is unaffected by the change will have
+ * {@code (y, x)} in-plane coordinates and {@code -z} offset with
+ * respect to the new plane. This means that the {@code u} and {@code v}
+ * vectors returned by the {@link #getU} and {@link #getV} methods are exchanged,
+ * and the {@code w} vector returned by the {@link #getNormal} method is
+ * reversed.</p>
+ */
+ public void revertSelf() {
+ final Cartesian3D tmp = u;
+ u = v;
+ v = tmp;
+ w = w.negate();
+ originOffset = -originOffset;
+ }
+
+ /** Transform a space vector into a sub-space vector.
+ * @param vector n-dimension vector of the space
+ * @return (n-1)-dimension vector of the sub-space corresponding to
+ * the specified space vector
+ */
+ public Cartesian2D toSubSpace(Vector<Euclidean3D> vector) {
+ return toSubSpace((Cartesian3D) vector);
+ }
+
+ /** Transform a sub-space point into a space point.
+ * @param vector (n-1)-dimension point of the sub-space
+ * @return n-dimension point of the space corresponding to the
+ * specified sub-space point
+ */
+ public Cartesian3D toSpace(Vector<Euclidean2D> vector) {
+ return toSpace((Cartesian2D) vector);
+ }
+
+ /** Transform a 3D space point into an in-plane point.
+ * @param point point of the space (must be a {@link Cartesian3D} instance)
+ * @return in-plane point
+ * @see #toSpace
+ */
+ @Override
+ public Cartesian2D toSubSpace(final Point<Euclidean3D> point) {
+ return toSubSpace((Cartesian3D) point);
+ }
+
+ /** Transform an in-plane point into a 3D space point.
+ * @param point in-plane point (must be a {@link Cartesian2D} instance)
+ * @return 3D space point
+ * @see #toSubSpace
+ */
+ @Override
+ public Cartesian3D toSpace(final Point<Euclidean2D> point) {
+ return toSpace((Cartesian2D) point);
+ }
+
+ /** Transform a 3D space point into an in-plane point.
+ * @param point point of the space
+ * @return in-plane point
+ * @see #toSpace
+ */
+ public Cartesian2D toSubSpace(final Cartesian3D point) {
+ return new Cartesian2D(point.dotProduct(u), point.dotProduct(v));
+ }
+
+ /** Transform an in-plane point into a 3D space point.
+ * @param point in-plane point
+ * @return 3D space point
+ * @see #toSubSpace
+ */
+ public Cartesian3D toSpace(final Cartesian2D point) {
+ return new Cartesian3D(point.getX(), u, point.getY(), v, -originOffset, w);
+ }
+
+ /** Get one point from the 3D-space.
+ * @param inPlane desired in-plane coordinates for the point in the
+ * plane
+ * @param offset desired offset for the point
+ * @return one point in the 3D-space, with given coordinates and offset
+ * relative to the plane
+ */
+ public Cartesian3D getPointAt(final Cartesian2D inPlane, final double offset) {
+ return new Cartesian3D(inPlane.getX(), u, inPlane.getY(), v, offset - originOffset, w);
+ }
+
+ /** Check if the instance is similar to another plane.
+ * <p>Planes are considered similar if they contain the same
+ * points. This does not mean they are equal since they can have
+ * opposite normals.</p>
+ * @param plane plane to which the instance is compared
+ * @return true if the planes are similar
+ */
+ public boolean isSimilarTo(final Plane plane) {
+ final double angle = Cartesian3D.angle(w, plane.w);
+ return ((angle < 1.0e-10) && (Math.abs(originOffset - plane.originOffset) < tolerance)) ||
+ ((angle > (Math.PI - 1.0e-10)) && (Math.abs(originOffset + plane.originOffset) < tolerance));
+ }
+
+ /** 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 vectorial rotation operator
+ * @return a new plane
+ */
+ public Plane rotate(final Cartesian3D center, final Rotation rotation) {
+
+ final Cartesian3D delta = origin.subtract(center);
+ final Plane plane = new Plane(center.add(rotation.applyTo(delta)),
+ rotation.applyTo(w), tolerance);
+
+ // make sure the frame is transformed as desired
+ plane.u = rotation.applyTo(u);
+ plane.v = rotation.applyTo(v);
+
+ return plane;
+
+ }
+
+ /** 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 Cartesian3D translation) {
+
+ final Plane plane = new Plane(origin.add(translation), w, tolerance);
+
+ // make sure the frame is transformed as desired
+ plane.u = u;
+ plane.v = v;
+
+ return plane;
+
+ }
+
+ /** Get the intersection of a line with the instance.
+ * @param line line intersecting the instance
+ * @return intersection point between between the line and the
+ * instance (null if the line is parallel to the instance)
+ */
+ public Cartesian3D intersection(final Line line) {
+ final Cartesian3D direction = line.getDirection();
+ final double dot = w.dotProduct(direction);
+ if (Math.abs(dot) < 1.0e-10) {
+ return null;
+ }
+ final Cartesian3D point = line.toSpace(Cartesian1D.ZERO);
+ final double k = -(originOffset + w.dotProduct(point)) / dot;
+ return new Cartesian3D(1.0, point, k, direction);
+ }
+
+ /** Build the line shared by the instance and another plane.
+ * @param other other plane
+ * @return line at the intersection of the instance and the
+ * other plane (really a {@link Line Line} instance)
+ */
+ public Line intersection(final Plane other) {
+ final Cartesian3D direction = Cartesian3D.crossProduct(w, other.w);
+ if (direction.getNorm() < tolerance) {
+ return null;
+ }
+ final Cartesian3D point = intersection(this, other, new Plane(direction, tolerance));
+ return new Line(point, point.add(direction), tolerance);
+ }
+
+ /** Get the intersection point of three planes.
+ * @param plane1 first plane1
+ * @param plane2 second plane2
+ * @param plane3 third plane2
+ * @return intersection point of three planes, null if some planes are parallel
+ */
+ public static Cartesian3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {
+
+ // coefficients of the three planes linear equations
+ final double a1 = plane1.w.getX();
+ final double b1 = plane1.w.getY();
+ final double c1 = plane1.w.getZ();
+ final double d1 = plane1.originOffset;
+
+ final double a2 = plane2.w.getX();
+ final double b2 = plane2.w.getY();
+ final double c2 = plane2.w.getZ();
+ final double d2 = plane2.originOffset;
+
+ final double a3 = plane3.w.getX();
+ final double b3 = plane3.w.getY();
+ final double c3 = plane3.w.getZ();
+ final double d3 = plane3.originOffset;
+
+ // 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) {
+ return null;
+ }
+
+ final double r = 1.0 / determinant;
+ return new Cartesian3D(
+ (-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
+ */
+ @Override
+ public SubPlane wholeHyperplane() {
+ return new SubPlane(this, new PolygonsSet(tolerance));
+ }
+
+ /** 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(tolerance);
+ }
+
+ /** Check if the instance contains a point.
+ * @param p point to check
+ * @return true if p belongs to the plane
+ */
+ public boolean contains(final Cartesian3D p) {
+ return Math.abs(getOffset(p)) < tolerance;
+ }
+
+ /** 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);
+ }
+
+ /** Get the offset (oriented distance) of a vector.
+ * @param vector vector to check
+ * @return offset of the vector
+ */
+// public double getOffset(Vector<Euclidean3D> vector) {
+// return getOffset((Point<Euclidean3D>) vector);
+// }
+
+ /** 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
+ */
+ @Override
+ public double getOffset(final Point<Euclidean3D> point) {
+ return ((Cartesian3D) point).dotProduct(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
+ */
+ @Override
+ public boolean sameOrientationAs(final Hyperplane<Euclidean3D> other) {
+ return (((Plane) other).w).dotProduct(w) > 0.0;
+ }
+
+}
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
new file mode 100644
index 0000000..6cb2771
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java
@@ -0,0 +1,705 @@
+/*
+ * 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.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.euclidean.oned.Euclidean1D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.euclidean.twod.SubLine;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+
+/** This class represents a 3D region: a set of polyhedrons.
+ */
+public class PolyhedronsSet extends AbstractRegion<Euclidean3D, Euclidean2D> {
+
+ /** Build a polyhedrons set representing the whole real line.
+ * @param tolerance tolerance below which points are considered identical
+ */
+ public PolyhedronsSet(final double tolerance) {
+ super(tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical
+ */
+ public PolyhedronsSet(final BSPTree<Euclidean3D> tree, final double tolerance) {
+ super(tree, tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical
+ */
+ public PolyhedronsSet(final Collection<SubHyperplane<Euclidean3D>> boundary,
+ final double tolerance) {
+ super(boundary, tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical
+ * @exception IllegalArgumentException if some basic sanity checks fail
+ */
+ public PolyhedronsSet(final List<Cartesian3D> vertices, final List<int[]> facets,
+ final double tolerance) {
+ super(buildBoundary(vertices, facets, tolerance), tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical
+ */
+ public PolyhedronsSet(final double xMin, final double xMax,
+ final double yMin, final double yMax,
+ final double zMin, final double zMax,
+ final double tolerance) {
+ super(buildBoundary(xMin, xMax, yMin, yMax, zMin, zMax, tolerance), tolerance);
+ }
+
+ /** 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 tolerance tolerance below which points are considered identical
+ * @return boundary tree
+ */
+ private static BSPTree<Euclidean3D> buildBoundary(final double xMin, final double xMax,
+ final double yMin, final double yMax,
+ final double zMin, final double zMax,
+ final double tolerance) {
+ if ((xMin >= xMax - tolerance) || (yMin >= yMax - tolerance) || (zMin >= zMax - tolerance)) {
+ // too thin box, build an empty polygons set
+ return new BSPTree<>(Boolean.FALSE);
+ }
+ final Plane pxMin = new Plane(new Cartesian3D(xMin, 0, 0), Cartesian3D.MINUS_I, tolerance);
+ final Plane pxMax = new Plane(new Cartesian3D(xMax, 0, 0), Cartesian3D.PLUS_I, tolerance);
+ final Plane pyMin = new Plane(new Cartesian3D(0, yMin, 0), Cartesian3D.MINUS_J, tolerance);
+ final Plane pyMax = new Plane(new Cartesian3D(0, yMax, 0), Cartesian3D.PLUS_J, tolerance);
+ final Plane pzMin = new Plane(new Cartesian3D(0, 0, zMin), Cartesian3D.MINUS_K, tolerance);
+ final Plane pzMax = new Plane(new Cartesian3D(0, 0, zMax), Cartesian3D.PLUS_K, tolerance);
+ final Region<Euclidean3D> boundary =
+ new RegionFactory<Euclidean3D>().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 tolerance tolerance below which points are considered identical
+ * @return boundary as a list of sub-hyperplanes
+ * @exception IllegalArgumentException if some basic sanity checks fail
+ */
+ private static List<SubHyperplane<Euclidean3D>> buildBoundary(final List<Cartesian3D> vertices,
+ final List<int[]> facets,
+ final double tolerance) {
+
+ // check vertices distances
+ for (int i = 0; i < vertices.size() - 1; ++i) {
+ final Cartesian3D vi = vertices.get(i);
+ for (int j = i + 1; j < vertices.size(); ++j) {
+ if (Cartesian3D.distance(vi, vertices.get(j)) <= tolerance) {
+ 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 Cartesian3D start = vertices.get(vA);
+ final Cartesian3D end = vertices.get(vB);
+ throw new IllegalArgumentException("Edge joining points " + start + " and " + end + " is connected to one facet only");
+ }
+ }
+ }
+ }
+
+ final List<SubHyperplane<Euclidean3D>> boundary = new ArrayList<>();
+
+ for (final int[] facet : facets) {
+
+ // define facet plane from the first 3 points
+ Plane plane = new Plane(vertices.get(facet[0]), vertices.get(facet[1]), vertices.get(facet[2]),
+ tolerance);
+
+ // check all points are in the plane
+ final Cartesian2D[] two2Points = new Cartesian2D[facet.length];
+ for (int i = 0 ; i < facet.length; ++i) {
+ final Cartesian3D v = vertices.get(facet[i]);
+ if (!plane.contains(v)) {
+ throw new IllegalArgumentException("Point " + v + " is out of plane");
+ }
+ two2Points[i] = plane.toSubSpace(v);
+ }
+
+ // create the polygonal facet
+ boundary.add(new SubPlane(plane, new PolygonsSet(tolerance, two2Points)));
+
+ }
+
+ 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<Cartesian3D> 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
... 20526 lines suppressed ...
--
To stop receiving notification emails like this one, please contact
erans@apache.org.