From 03fa7c14316371f43c62dd486036be9145ab98d0 Mon Sep 17 00:00:00 2001 From: Luc Maisonobe Date: Sun, 15 May 2011 16:02:05 +0000 Subject: [PATCH] A complete generic implementation of Binary Space Partitioning Trees (BSP trees) has been added. A few specializations of this implementation are also provided for 1D, 2D and 3D Euclidean geometry. This allows support for arbitrary intervals sets (1D), polygons sets (2D) and polyhedrons sets (3D) with all sets operations (union, intersection, symmetric difference, difference, complement), with predicates (point inside/outside/on boundary, emptiness, other region contained), with geometrical computation (barycenter, size, boundary size) and with conversions from and to boundary representation. JIRA: MATH-576 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/math/trunk@1103438 13f79535-47bb-0310-9956-ffa450edef68 --- findbugs-exclude-filter.xml | 2 +- .../math/exception/util/LocalizedFormats.java | 4 + .../geometry/euclidean/oneD/Interval.java | 69 ++ .../geometry/euclidean/oneD/IntervalsSet.java | 239 ++++ .../euclidean/oneD/OrientedPoint.java | 222 ++++ .../math/geometry/euclidean/oneD/Point1D.java | 53 + .../math/geometry/euclidean/oneD/package.html | 24 + .../CardanEulerSingularityException.java | 2 +- .../math/geometry/euclidean/threeD/Line.java | 153 +++ .../threeD}/NotARotationMatrixException.java | 2 +- .../euclidean/threeD/OutlineExtractor.java | 250 ++++ .../math/geometry/euclidean/threeD/Plane.java | 526 ++++++++ .../geometry/euclidean/threeD/Point3D.java | 112 ++ .../euclidean/threeD/PolyhedronsSet.java | 413 +++++++ .../{ => euclidean/threeD}/Rotation.java | 2 +- .../{ => euclidean/threeD}/RotationOrder.java | 2 +- .../{ => euclidean/threeD}/Vector3D.java | 2 +- .../threeD}/Vector3DFormat.java | 2 +- .../{ => euclidean/threeD}/package.html | 0 .../math/geometry/euclidean/twoD/Line.java | 512 ++++++++ .../geometry/euclidean/twoD/NestedLoops.java | 190 +++ .../math/geometry/euclidean/twoD/Point2D.java | 72 ++ .../geometry/euclidean/twoD/PolygonsSet.java | 343 ++++++ .../math/geometry/euclidean/twoD/Segment.java | 112 ++ .../euclidean/twoD/SegmentBuilder.java | 90 ++ .../math/geometry/euclidean/twoD/package.html | 24 + .../math/geometry/partitioning/BSPTree.java | 631 ++++++++++ .../geometry/partitioning/BSPTreeVisitor.java | 110 ++ .../partitioning/Characterization.java | 90 ++ .../geometry/partitioning/Hyperplane.java | 159 +++ .../math/geometry/partitioning/Point.java | 29 + .../math/geometry/partitioning/Region.java | 1069 +++++++++++++++++ .../geometry/partitioning/SubHyperplane.java | 138 +++ .../math/geometry/partitioning/SubSpace.java | 50 + .../math/geometry/partitioning/Transform.java | 74 ++ .../math/geometry/partitioning/package.html | 107 ++ .../partitioning/utilities/AVLTree.java | 631 ++++++++++ .../partitioning/utilities/OrderedTuple.java | 417 +++++++ .../utilities/doc-files/OrderedTuple.png | Bin 0 -> 28882 bytes .../partitioning/utilities/package.html | 24 + src/site/xdoc/changes.xml | 10 + src/site/xdoc/userguide/geometry.xml | 76 +- src/site/xdoc/userguide/overview.xml | 2 +- .../euclidean/oneD/IntervalsSetTest.java | 96 ++ .../threeD}/FrenchVector3DFormatTest.java | 2 +- .../geometry/euclidean/threeD/LineTest.java | 75 ++ .../geometry/euclidean/threeD/PlaneTest.java | 168 +++ .../euclidean/threeD/PolyhedronsSetTest.java | 243 ++++ .../threeD}/RotationOrderTest.java | 4 +- .../{ => euclidean/threeD}/RotationTest.java | 12 +- .../threeD}/Vector3DFormatAbstractTest.java | 4 +- .../threeD}/Vector3DFormatTest.java | 2 +- .../{ => euclidean/threeD}/Vector3DTest.java | 4 +- .../geometry/euclidean/twoD/LineTest.java | 129 ++ .../euclidean/twoD/PolygonsSetTest.java | 883 ++++++++++++++ .../partitioning/utilities/AVLTreeTest.java | 175 +++ 56 files changed, 8808 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/oneD/Interval.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/oneD/IntervalsSet.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/oneD/OrientedPoint.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/oneD/Point1D.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/oneD/package.html rename src/main/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/CardanEulerSingularityException.java (96%) create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Line.java rename src/main/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/NotARotationMatrixException.java (96%) create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/threeD/OutlineExtractor.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Plane.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Point3D.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/threeD/PolyhedronsSet.java rename src/main/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/Rotation.java (99%) rename src/main/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/RotationOrder.java (98%) rename src/main/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/Vector3D.java (99%) rename src/main/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/Vector3DFormat.java (99%) rename src/main/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/package.html (100%) create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Line.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/twoD/NestedLoops.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Point2D.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/twoD/PolygonsSet.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Segment.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/twoD/SegmentBuilder.java create mode 100644 src/main/java/org/apache/commons/math/geometry/euclidean/twoD/package.html create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/BSPTree.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/BSPTreeVisitor.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/Characterization.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/Hyperplane.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/Point.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/Region.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/SubHyperplane.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/SubSpace.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/Transform.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/package.html create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/utilities/AVLTree.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/utilities/OrderedTuple.java create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/utilities/doc-files/OrderedTuple.png create mode 100644 src/main/java/org/apache/commons/math/geometry/partitioning/utilities/package.html create mode 100644 src/test/java/org/apache/commons/math/geometry/euclidean/oneD/IntervalsSetTest.java rename src/test/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/FrenchVector3DFormatTest.java (94%) create mode 100644 src/test/java/org/apache/commons/math/geometry/euclidean/threeD/LineTest.java create mode 100644 src/test/java/org/apache/commons/math/geometry/euclidean/threeD/PlaneTest.java create mode 100644 src/test/java/org/apache/commons/math/geometry/euclidean/threeD/PolyhedronsSetTest.java rename src/test/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/RotationOrderTest.java (93%) rename src/test/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/RotationTest.java (97%) rename src/test/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/Vector3DFormatAbstractTest.java (98%) rename src/test/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/Vector3DFormatTest.java (94%) rename src/test/java/org/apache/commons/math/geometry/{ => euclidean/threeD}/Vector3DTest.java (98%) create mode 100644 src/test/java/org/apache/commons/math/geometry/euclidean/twoD/LineTest.java create mode 100644 src/test/java/org/apache/commons/math/geometry/euclidean/twoD/PolygonsSetTest.java create mode 100644 src/test/java/org/apache/commons/math/geometry/partitioning/utilities/AVLTreeTest.java diff --git a/findbugs-exclude-filter.xml b/findbugs-exclude-filter.xml index dfe145e64..63a83ec05 100644 --- a/findbugs-exclude-filter.xml +++ b/findbugs-exclude-filter.xml @@ -65,7 +65,7 @@ - + diff --git a/src/main/java/org/apache/commons/math/exception/util/LocalizedFormats.java b/src/main/java/org/apache/commons/math/exception/util/LocalizedFormats.java index 6bc404bf2..e0a50e094 100644 --- a/src/main/java/org/apache/commons/math/exception/util/LocalizedFormats.java +++ b/src/main/java/org/apache/commons/math/exception/util/LocalizedFormats.java @@ -77,6 +77,7 @@ public enum LocalizedFormats implements Localizable { CONTRACTION_CRITERIA_SMALLER_THAN_EXPANSION_FACTOR("contraction criteria ({0}) smaller than the expansion factor ({1}). This would lead to a never ending loop of expansion and contraction as a newly expanded internal storage array would immediately satisfy the criteria for contraction."), CONTRACTION_CRITERIA_SMALLER_THAN_ONE("contraction criteria smaller than one ({0}). This would lead to a never ending loop of expansion and contraction as an internal storage array length equal to the number of elements would satisfy the contraction criteria."), CONVERGENCE_FAILED("convergence failed"), /* keep */ + CROSSING_BOUNDARY_LOOPS("some outline boundary loops cross each other"), CUMULATIVE_PROBABILITY_RETURNED_NAN("Cumulative probability function returned NaN for argument {0} p = {1}"), DIFFERENT_ROWS_LENGTHS("some rows have length {0} while others have length {1}"), DIGEST_NOT_INITIALIZED("digest not initialized"), @@ -163,6 +164,7 @@ public enum LocalizedFormats implements Localizable { ROBUSTNESS_ITERATIONS("number of robustness iterations ({0})"), START_POSITION("start position ({0})"), /* keep */ NON_CONVERGENT_CONTINUED_FRACTION("Continued fraction convergents failed to converge (in less than {0} iterations) for value {1}"), + NON_INVERTIBLE_TRANSFORM("non-invertible affine transform collapses some lines into single points"), NON_POSITIVE_MICROSPHERE_ELEMENTS("number of microsphere elements must be positive, but got {0}"), NON_POSITIVE_POLYNOMIAL_DEGREE("polynomial degree must be positive: degree={0}"), NON_REAL_FINITE_ABSCISSA("all abscissae must be finite real numbers, but {0}-th is {1}"), @@ -219,6 +221,7 @@ public enum LocalizedFormats implements Localizable { NOT_STRICTLY_INCREASING_NUMBER_OF_POINTS("points {0} and {1} are not strictly increasing ({2} >= {3})"), NOT_STRICTLY_INCREASING_SEQUENCE("points {3} and {2} are not strictly increasing ({1} >= {0})"), /* keep */ NOT_SUBTRACTION_COMPATIBLE_MATRICES("{0}x{1} and {2}x{3} matrices are not subtraction compatible"), + NOT_SUPPORTED_IN_DIMENSION_N("method not supported in dimension {0}"), NOT_SYMMETRIC_MATRIX("not symmetric matrix"), NON_SYMMETRIC_MATRIX("non symmetric matrix: the difference between entries at ({0},{1}) and ({1},{0}) is larger than {2}"), /* keep */ NO_BIN_SELECTED("no bin selected"), @@ -259,6 +262,7 @@ public enum LocalizedFormats implements Localizable { OUT_OF_RANGE_ROOT_OF_UNITY_INDEX("out of range root of unity index {0} (must be in [{1};{2}])"), OUT_OF_RANGE("out of range"), /* keep */ OUT_OF_RANGE_SIMPLE("{0} out of [{1}, {2}] range"), /* keep */ + OUTLINE_BOUNDARY_LOOP_OPEN("an outline boundary loop is open"), OVERFLOW_IN_FRACTION("overflow in fraction {0}/{1}, cannot negate"), OVERFLOW_IN_ADDITION("overflow in addition: {0} + {1}"), OVERFLOW_IN_SUBTRACTION("overflow in subtraction: {0} - {1}"), diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/Interval.java b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/Interval.java new file mode 100644 index 000000000..b8d8b924c --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/Interval.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.math.geometry.euclidean.oneD; + + +/** This class represents a 1D interval. + * @see IntervalsSet + * @version $Revision$ $Date$ + */ +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) { + this.lower = lower; + this.upper = upper; + } + + /** Get the lower bound of the interval. + * @return lower bound of the interval + */ + public double getLower() { + return lower; + } + + /** Get the upper bound of the interval. + * @return upper bound of the interval + */ + public double getUpper() { + return upper; + } + + /** Get the length of the interval. + * @return length of the interval + */ + public double getLength() { + return upper - lower; + } + + /** Get the midpoint of the interval. + * @return midpoint of the interval + */ + public double getMidPoint() { + return 0.5 * (lower + upper); + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/IntervalsSet.java b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/IntervalsSet.java new file mode 100644 index 000000000..bf77274cd --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/IntervalsSet.java @@ -0,0 +1,239 @@ +/* + * 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.math.geometry.euclidean.oneD; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; + +/** This class represents a 1D region: a set of intervals. + * @version $Revision$ $Date$ + */ +public class IntervalsSet extends Region { + + /** Build an intervals set representing the whole real line. + */ + public IntervalsSet() { + super(); + } + + /** 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}) + */ + public IntervalsSet(final double lower, final double upper) { + super(buildTree(lower, upper)); + } + + /** Build an intervals set from an inside/outside BSP tree. + *

The leaf nodes of the BSP tree must 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}

+ * @param tree inside/outside BSP tree representing the intervals set + */ + public IntervalsSet(final BSPTree tree) { + super(tree); + } + + /** Build an intervals set from a Boundary REPresentation (B-rep). + *

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.

+ *

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 + * Region#checkPoint(org.apache.commons.math.geometry.partitioning.Point) + * checkPoint} method will not be meaningful anymore.

+ *

If the boundary is empty, the region will represent the whole + * space.

+ * @param boundary collection of boundary elements + */ + public IntervalsSet(final Collection boundary) { + super(boundary); + } + + /** 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}) + * @return the built tree + */ + private static BSPTree buildTree(final double lower, final double upper) { + 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 upperCut = + new SubHyperplane(new OrientedPoint(new Point1D(upper), true)); + return new BSPTree(upperCut, + new BSPTree(Boolean.FALSE), + new BSPTree(Boolean.TRUE), + null); + } + final SubHyperplane lowerCut = + new SubHyperplane(new OrientedPoint(new Point1D(lower), false)); + if (Double.isInfinite(upper) && (upper > 0)) { + // the tree must be open on the positive infinity side + return new BSPTree(lowerCut, + new BSPTree(Boolean.FALSE), + new BSPTree(Boolean.TRUE), + null); + } + + // the tree must be bounded on the two sides + final SubHyperplane upperCut = + new SubHyperplane(new OrientedPoint(new Point1D(upper), true)); + return new BSPTree(lowerCut, + new BSPTree(Boolean.FALSE), + new BSPTree(upperCut, + new BSPTree(Boolean.FALSE), + new BSPTree(Boolean.TRUE), + null), + null); + + } + + /** {@inheritDoc} */ + public Region buildNew(final BSPTree tree) { + return new IntervalsSet(tree); + } + + /** {@inheritDoc} */ + protected void computeGeometricalProperties() { + if (getTree(false).getCut() == null) { + setBarycenter(Point1D.UNDEFINED); + 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.getLength(); + sum += interval.getLength() * interval.getMidPoint(); + } + setSize(size); + setBarycenter(Double.isInfinite(size) ? Point1D.UNDEFINED : new Point1D(sum / size)); + } + } + + /** 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 node = getTree(false); + double inf = Double.POSITIVE_INFINITY; + while (node.getCut() != null) { + final OrientedPoint op = (OrientedPoint) node.getCut().getHyperplane(); + inf = op.getLocation().getAbscissa(); + 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 node = getTree(false); + double sup = Double.NEGATIVE_INFINITY; + while (node.getCut() != null) { + final OrientedPoint op = (OrientedPoint) node.getCut().getHyperplane(); + sup = op.getLocation().getAbscissa(); + node = op.isDirect() ? node.getPlus() : node.getMinus(); + } + return ((Boolean) node.getAttribute()) ? Double.POSITIVE_INFINITY : sup; + } + + /** Build an ordered list of intervals representing the instance. + *

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 beeing + * infinite.

+ * @return a new ordered list containing {@link Interval Interval} + * elements + */ + public List asList() { + final List list = new ArrayList(); + recurseList(getTree(false), list, + Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + return list; + } + + /** Update an intervals list. + * @param node current node + * @param list list to update + * @param lower lower bound of the current convex cell + * @param upper upper bound of the current convex cell + */ + private void recurseList(final BSPTree node, final List list, + final double lower, final double upper) { + + if (node.getCut() == null) { + if ((Boolean) node.getAttribute()) { + // this leaf cell is an inside cell: an interval + list.add(new Interval(lower, upper)); + } + } else { + final OrientedPoint op = (OrientedPoint) node.getCut().getHyperplane(); + final Point1D loc = op.getLocation(); + double x = loc.getAbscissa(); + + // make sure we explore the tree in increasing order + final BSPTree low = op.isDirect() ? node.getMinus() : node.getPlus(); + final BSPTree high = op.isDirect() ? node.getPlus() : node.getMinus(); + + recurseList(low, list, lower, x); + if ((checkPoint(low, loc) == Location.INSIDE) && + (checkPoint(high, loc) == Location.INSIDE)) { + // merge the last interval added and the first one of the high sub-tree + x = ((Interval) list.remove(list.size() - 1)).getLower(); + } + recurseList(high, list, x, upper); + + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/OrientedPoint.java b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/OrientedPoint.java new file mode 100644 index 000000000..0535c7e55 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/OrientedPoint.java @@ -0,0 +1,222 @@ +/* + * 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.math.geometry.euclidean.oneD; + +import org.apache.commons.math.exception.MathUnsupportedOperationException; +import org.apache.commons.math.exception.util.LocalizedFormats; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.Hyperplane; +import org.apache.commons.math.geometry.partitioning.Point; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.geometry.partitioning.SubSpace; + +/** This class represents a 1D oriented hyperplane. + *

An hyperplane in 1D is a simple point, its orientation being a + * boolean.

+ *

Instances of this class are guaranteed to be immutable.

+ * @version $Revision$ $Date$ + */ +public class OrientedPoint implements Hyperplane { + + /** Dummy region returned by the {@link #wholeHyperplane} method. */ + private static final Region DUMMY_REGION = new DummyRegion(); + + /** Point location. */ + private Point1D location; + + /** Orientation. */ + private boolean direct; + + /** Simple constructor. + * @param location location of the hyperplane + * @param direct if true, the plus side of the hyperplane is towards + * abscissae greater than {@code location} + */ + public OrientedPoint(final Point1D location, final boolean direct) { + this.location = location; + this.direct = direct; + } + + /** Copy the instance. + *

Since instances are immutable, this method directly returns + * the instance.

+ * @return the instance itself + */ + public Hyperplane copySelf() { + return this; + } + + /** Get the offset (oriented distance) of a point to the hyperplane. + * @param point point to check + * @return offset of the point + */ + public double getOffset(final Point point) { + final double delta = ((Point1D) point).getAbscissa() - location.getAbscissa(); + return direct ? delta : -delta; + } + + /** Transform a space point into a sub-space point. + *

Since this class represent zero dimension spaces which does + * not have lower dimension sub-spaces, this method cannot be + * supported here. It always throws a {@code RuntimeException} + * when called.

+ * @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 + */ + public Point toSubSpace(final Point point) { + throw new MathUnsupportedOperationException(LocalizedFormats.NOT_SUPPORTED_IN_DIMENSION_N, 1); + } + + /** Transform a sub-space point into a space point. + *

Since this class represent zero dimension spaces which does + * not have lower dimension sub-spaces, this method cannot be + * supported here. It always throws a {@code RuntimeException} + * when called.

+ * @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 + */ + public Point toSpace(final Point point) { + throw new MathUnsupportedOperationException(LocalizedFormats.NOT_SUPPORTED_IN_DIMENSION_N, 1); + } + + /** Build the sub-space shared by the instance and another hyperplane. + *

Since this class represent zero dimension spaces which does + * not have lower dimension sub-spaces, this method cannot be + * supported here. It always throws a {@code RuntimeException} + * when called.

+ * @param other other sub-space (must have the same dimension as the + * instance) + * @return a sub-space at the intersection of the instance and the + * other sub-space (it has a dimension one unit less than the + * instance) + */ + public SubSpace intersection(final Hyperplane other) { + throw new MathUnsupportedOperationException(LocalizedFormats.NOT_SUPPORTED_IN_DIMENSION_N, 1); + } + + /** Build a region covering the whole hyperplane. + *

Since this class represent zero dimension spaces which does + * not have lower dimension sub-spaces, this method returns a dummy + * implementation of a {@link Region Region} (always the same + * instance). This implementation is only used to allow the {@link + * SubHyperplane SubHyperplane} class implementation to work + * properly, it should not be used otherwise.

+ * @return a dummy region + */ + public Region wholeHyperplane() { + return DUMMY_REGION; + } + + /** Build a region covering the whole space. + * @return a region containing the instance (really an {@link + * IntervalsSet IntervalsSet} instance) + */ + public Region wholeSpace() { + return new IntervalsSet(); + } + + /** Check if the instance has the same orientation as another hyperplane. + *

This method is expected to be called on parallel hyperplanes + * (i.e. when the {@link #side side} method would return {@link + * org.apache.commons.math.geometry.partitioning.Hyperplane.Side#HYPER} + * for some sub-hyperplane having the specified hyperplane + * as its underlying hyperplane). The method should not + * re-check for parallelism, only for orientation, typically by + * testing something like the sign of the dot-products of + * normals.

+ * @param other other hyperplane to check against the instance + * @return true if the instance and the other hyperplane have + * the same orientation + */ + public boolean sameOrientationAs(final Hyperplane other) { + return !(direct ^ ((OrientedPoint) other).direct); + } + + /** Compute the relative position of a sub-hyperplane with respect + * to the instance. + * @param sub sub-hyperplane to check + * @return one of {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#PLUS PLUS}, + * {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#MINUS MINUS} + * or {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#HYPER HYPER} + * (in dimension 1, this method never returns {@link + * org.apache.commons.math.geometry.partitioning.Hyperplane.Side#BOTH BOTH}) + * + */ + public Side side(final SubHyperplane sub) { + final double global = getOffset(((OrientedPoint) sub.getHyperplane()).location); + return (global < -1.0e-10) ? Side.MINUS : ((global > 1.0e-10) ? Side.PLUS : Side.HYPER); + } + + /** Split a sub-hyperplane in two parts by the instance. + * @param sub sub-hyperplane to split + * @return an object containing both the part of the sub-hyperplane + * on the plus side of the instance and the part of the + * sub-hyperplane on the minus side of the instance + */ + public SplitSubHyperplane split(final SubHyperplane sub) { + final double global = getOffset(((OrientedPoint) sub.getHyperplane()).location); + return (global < -1.0e-10) ? new SplitSubHyperplane(null, sub) : new SplitSubHyperplane(sub, null); + } + + /** Get the hyperplane location on the real line. + * @return the hyperplane location + */ + public Point1D 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; + } + + /** Dummy region representing the whole set of reals. */ + private static class DummyRegion extends Region { + + /** Simple constructor. + */ + public DummyRegion() { + super(); + } + + /** {@inheritDoc} */ + public Region buildNew(final BSPTree tree) { + return this; + } + + /** {@inheritDoc} */ + protected void computeGeometricalProperties() { + setSize(0); + setBarycenter(Point1D.ZERO); + } + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/Point1D.java b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/Point1D.java new file mode 100644 index 000000000..cb607ac6f --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/Point1D.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math.geometry.euclidean.oneD; + +import org.apache.commons.math.geometry.partitioning.Point; + +/** This class represents a 1D point. + *

Instances of this class are guaranteed to be immutable.

+ * @version $Revision$ $Date$ + */ +public class Point1D implements Point { + + /** Point at 0.0 abscissa. */ + public static final Point1D ZERO = new Point1D(0.0); + + /** Point at 1.0 abscissa. */ + public static final Point1D ONE = new Point1D(1.0); + + /** Point at undefined (NaN) abscissa. */ + public static final Point1D UNDEFINED = new Point1D(Double.NaN); + + /** Abscissa of the point. */ + private double x; + + /** Simple constructor. + * @param x abscissa of the point + */ + public Point1D(final double x) { + this.x = x; + } + + /** Get the abscissa of the point. + * @return abscissa of the point + */ + public double getAbscissa() { + return x; + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/package.html b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/package.html new file mode 100644 index 000000000..2ac354fe7 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/oneD/package.html @@ -0,0 +1,24 @@ + + + + +

+This package provides basic 1D geometry components. +

+ + diff --git a/src/main/java/org/apache/commons/math/geometry/CardanEulerSingularityException.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/CardanEulerSingularityException.java similarity index 96% rename from src/main/java/org/apache/commons/math/geometry/CardanEulerSingularityException.java rename to src/main/java/org/apache/commons/math/geometry/euclidean/threeD/CardanEulerSingularityException.java index 1125687ac..c6498fb65 100644 --- a/src/main/java/org/apache/commons/math/geometry/CardanEulerSingularityException.java +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/CardanEulerSingularityException.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import org.apache.commons.math.MathException; import org.apache.commons.math.exception.util.LocalizedFormats; diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Line.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Line.java new file mode 100644 index 000000000..0604e5286 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Line.java @@ -0,0 +1,153 @@ +/* + * 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.math.geometry.euclidean.threeD; + +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.partitioning.Point; +import org.apache.commons.math.geometry.partitioning.SubSpace; +import org.apache.commons.math.util.FastMath; + +/** The class represent lines in a three dimensional space. + + *

Each oriented line is intrinsically associated with an abscissa + * wich 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.

+ + * @version $Revision$ $Date$ + */ +public class Line implements SubSpace { + + /** Line direction. */ + private Vector3D direction; + + /** Line point closest to the origin. */ + private Point3D zero; + + /** Build a line from a point and a direction. + * @param p point belonging to the line (this can be any point) + * @param direction direction of the line + * @exception IllegalArgumentException if the direction norm is too small + */ + public Line(final Vector3D p, final Vector3D direction) { + reset(p, direction); + } + + /** Reset the instance as if built from a point and a normal. + * @param p point belonging to the line (this can be any point) + * @param dir direction of the line + * @exception IllegalArgumentException if the direction norm is too small + */ + public void reset(final Vector3D p, final Vector3D dir) { + final double norm = dir.getNorm(); + if (norm == 0.0) { + throw new IllegalArgumentException("null norm"); + } + this.direction = new Vector3D(1.0 / norm, dir); + zero = new Point3D(1.0, p, -Vector3D.dotProduct(p, this.direction), this.direction); + } + + /** Revert the line direction. + */ + public void revertSelf() { + direction = direction.negate(); + } + + /** Get the normalized direction vector. + * @return normalized direction vector + */ + public Vector3D getDirection() { + return direction; + } + + /** Get the abscissa of a point with respect to the line. + *

The abscissa is 0 if the projection of the point and the + * projection of the frame origin on the line are the same + * point.

+ * @param point point to check (must be a {@link Vector3D Vector3D} + * instance) + * @return abscissa of the point (really a + * {org.apache.commons.math.geometry.euclidean.oneD.Point1D Point1D} instance) + */ + public Point toSubSpace(final Point point) { + final double x = Vector3D.dotProduct(((Vector3D) point).subtract(zero), direction); + return new Point1D(x); + } + + /** Get one point from the line. + * @param point desired abscissa for the point (must be a + * {org.apache.commons.math.geometry.euclidean.oneD.Point1D Point1D} instance) + * @return one point belonging to the line, at specified abscissa + * (really a {@link Vector3D Vector3D} instance) + */ + public Point toSpace(final Point point) { + return new Point3D(1.0, zero, ((Point1D) point).getAbscissa(), direction); + } + + /** Check if the instance is similar to another line. + *

Lines are considered similar if they contain the same + * points. This does not mean they are equal since they can have + * opposite directions.

+ * @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 = Vector3D.angle(direction, line.direction); + return ((angle < 1.0e-10) || (angle > (FastMath.PI - 1.0e-10))) && contains(line.zero); + } + + /** Check if the instance contains a point. + * @param p point to check + * @return true if p belongs to the line + */ + public boolean contains(final Vector3D p) { + return distance(p) < 1.0e-10; + } + + /** Compute the distance between the instance and a point. + * @param p to check + * @return distance between the instance and the point + */ + public double distance(final Vector3D p) { + final Vector3D d = p.subtract(zero); + final Vector3D n = new Vector3D(1.0, d, -Vector3D.dotProduct(d, direction), direction); + return n.getNorm(); + } + + /** Compute the shortest distance between the instance and another line. + * @param line line to check agains the instance + * @return shortest distance between the instance and the line + */ + public double distance(final Line line) { + + final Vector3D normal = Vector3D.crossProduct(direction, line.direction); + if (normal.getNorm() < 1.0e-10) { + // lines are parallel + return distance(line.zero); + } + + // separating middle plane + final Plane middle = new Plane(new Vector3D(0.5, zero, 0.5, line.zero), normal); + + // the lines are at the same distance on either side of the plane + return 2 * FastMath.abs(middle.getOffset(zero)); + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/NotARotationMatrixException.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/NotARotationMatrixException.java similarity index 96% rename from src/main/java/org/apache/commons/math/geometry/NotARotationMatrixException.java rename to src/main/java/org/apache/commons/math/geometry/euclidean/threeD/NotARotationMatrixException.java index 910e40157..2b5737b9f 100644 --- a/src/main/java/org/apache/commons/math/geometry/NotARotationMatrixException.java +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/NotARotationMatrixException.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import org.apache.commons.math.MathException; import org.apache.commons.math.exception.util.Localizable; diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/OutlineExtractor.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/OutlineExtractor.java new file mode 100644 index 000000000..842945297 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/OutlineExtractor.java @@ -0,0 +1,250 @@ +/* + * 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.math.geometry.euclidean.threeD; + +import org.apache.commons.math.geometry.euclidean.twoD.Point2D; +import org.apache.commons.math.geometry.euclidean.twoD.PolygonsSet; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.BSPTreeVisitor; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.util.FastMath; + +import java.util.ArrayList; + +/** Extractor for {@link PolygonsSet polyhedrons sets} outlines. + *

This class extracts the 2D outlines from {{@link PolygonsSet + * polyhedrons sets} in a specified projection plane.

+ * @version $Revision$ $Date$ + */ +public class OutlineExtractor { + + /** Abscissa axis of the projection plane. */ + private Vector3D u; + + /** Ordinate axis of the projection plane. */ + private Vector3D v; + + /** Normal of the projection plane (viewing direction). */ + private Vector3D w; + + /** Build an extractor for a specific projection plane. + * @param u abscissa axis of the projection point + * @param v ordinate axis of the projection point + */ + public OutlineExtractor(final Vector3D u, final Vector3D v) { + this.u = u; + this.v = v; + w = Vector3D.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 Point2D[][] getOutline(final PolyhedronsSet polyhedronsSet) { + + // project all boundary facets into one polygons set + final BoundaryProjector projector = new BoundaryProjector(); + polyhedronsSet.getTree(true).visit(projector); + final PolygonsSet projected = projector.getProjected(); + + // Remove the spurious intermediate vertices from the outline + final Point2D[][] outline = projected.getVertices(); + for (int i = 0; i < outline.length; ++i) { + final Point2D[] 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 Point2D[end]; + System.arraycopy(rawLoop, 0, outline[i], 0, end); + } + } + + return outline; + + } + + /** Check if a point is geometrically between its neighbour in an array. + *

The neighbours are computed considering the array is a loop + * (i.e. point at index (n-1) is before point at index 0)

+ * @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 neighbours + */ + private boolean pointIsBetween(final Point2D[] loop, final int n, final int i) { + final Point2D previous = loop[(i + n - 1) % n]; + final Point2D current = loop[i]; + final Point2D next = loop[(i + 1) % n]; + final double dx1 = current.x - previous.x; + final double dy1 = current.y - previous.y; + final double dx2 = next.x - current.x; + final double dy2 = next.y - current.y; + final double cross = dx1 * dy2 - dx2 * dy1; + final double dot = dx1 * dx2 + dy1 * dy2; + final double d1d2 = FastMath.sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2)); + return (FastMath.abs(cross) <= (1.0e-6 * d1d2)) && (dot >= 0.0); + } + + /** Visitor projecting the boundary facets on a plane. */ + private class BoundaryProjector implements BSPTreeVisitor { + + /** Projection of the polyhedrons set on the plane. */ + private PolygonsSet projected; + + /** Simple constructor. + */ + public BoundaryProjector() { + projected = new PolygonsSet(new BSPTree(Boolean.FALSE)); + } + + /** {@inheritDoc} */ + public Order visitOrder(final BSPTree node) { + return Order.MINUS_SUB_PLUS; + } + + /** {@inheritDoc} */ + public void visitInternalNode(final BSPTree node) { + final Region.BoundaryAttribute attribute = + (Region.BoundaryAttribute) node.getAttribute(); + if (attribute.getPlusOutside() != null) { + addContribution(attribute.getPlusOutside(), false); + } + if (attribute.getPlusInside() != null) { + addContribution(attribute.getPlusInside(), true); + } + } + + /** {@inheritDoc} */ + public void visitLeafNode(final BSPTree 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 facet, final boolean reversed) { + + // extract the vertices of the facet + final Plane plane = (Plane) facet.getHyperplane(); + Point2D[][] vertices = + ((PolygonsSet) facet.getRemainingRegion()).getVertices(); + + final double scal = Vector3D.dotProduct(plane.getNormal(), w); + if (FastMath.abs(scal) > 1.0e-3) { + + if ((scal < 0) ^ reversed) { + // the facet is seen from the inside, + // we need to invert its boundary orientation + final Point2D[][] newVertices = new Point2D[vertices.length][]; + for (int i = 0; i < vertices.length; ++i) { + final Point2D[] loop = vertices[i]; + final Point2D[] newLoop = new Point2D[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 edges = new ArrayList(); + for (Point2D[] loop : vertices) { + final boolean closed = loop[0] != null; + int previous = closed ? (loop.length - 1) : 1; + Vector3D previous3D = (Vector3D) plane.toSpace(loop[previous]); + int current = (previous + 1) % loop.length; + Point2D pPoint = new Point2D(Vector3D.dotProduct(previous3D, u), + Vector3D.dotProduct(previous3D, v)); + while (current < loop.length) { + + final Vector3D current3D = (Vector3D) plane.toSpace(loop[current]); + final Point2D cPoint = new Point2D(Vector3D.dotProduct(current3D, u), + Vector3D.dotProduct(current3D, v)); + final org.apache.commons.math.geometry.euclidean.twoD.Line line = + new org.apache.commons.math.geometry.euclidean.twoD.Line(pPoint, cPoint); + SubHyperplane edge = new SubHyperplane(line); + + 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 * FastMath.PI; + final org.apache.commons.math.geometry.euclidean.twoD.Line l = + new org.apache.commons.math.geometry.euclidean.twoD.Line(pPoint, angle); + edge = l.split(edge).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 * FastMath.PI; + final org.apache.commons.math.geometry.euclidean.twoD.Line l = + new org.apache.commons.math.geometry.euclidean.twoD.Line(cPoint, angle); + edge = l.split(edge).getMinus(); + } + + edges.add(edge); + + previous = current++; + previous3D = current3D; + pPoint = cPoint; + + } + } + final PolygonsSet projectedFacet = new PolygonsSet(edges); + + // add the contribution of the facet to the global outline + projected = (PolygonsSet) Region.union(projected, projectedFacet); + + } + } + + /** Get the projecion of the polyhedrons set on the plane. + * @return projecion of the polyhedrons set on the plane + */ + public PolygonsSet getProjected() { + return projected; + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Plane.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Plane.java new file mode 100644 index 000000000..eb6c77964 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Plane.java @@ -0,0 +1,526 @@ +/* + * 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.math.geometry.euclidean.threeD; + +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.euclidean.twoD.Point2D; +import org.apache.commons.math.geometry.euclidean.twoD.PolygonsSet; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.Hyperplane; +import org.apache.commons.math.geometry.partitioning.Point; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.geometry.partitioning.SubSpace; +import org.apache.commons.math.util.FastMath; + +/** The class represent planes in a three dimensional space. + * @version $Revision$ $Date$ + */ +public class Plane implements Hyperplane { + + /** Offset of the origin with respect to the plane. */ + private double originOffset; + + /** Origin of the plane frame. */ + private Point3D origin; + + /** First vector of the plane frame (in plane). */ + private Vector3D u; + + /** Second vector of the plane frame (in plane). */ + private Vector3D v; + + /** Third vector of the plane frame (plane normal). */ + private Vector3D w; + + /** Build a plane normal to a given direction and containing the origin. + * @param normal normal direction to the plane + * @exception IllegalArgumentException if the normal norm is too small + */ + public Plane(final Vector3D normal) { + setNormal(normal); + 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 + * @exception IllegalArgumentException if the normal norm is too small + */ + public Plane(final Vector3D p, final Vector3D normal) { + setNormal(normal); + originOffset = -Vector3D.dotProduct(p, w); + setFrame(); + } + + /** Build a plane from three points. + *

The plane is oriented in the direction of + * {@code (p2-p1) ^ (p3-p1)}

+ * @param p1 first point belonging to the plane + * @param p2 second point belonging to the plane + * @param p3 third point belonging to the plane + * @exception IllegalArgumentException if the points do not constitute a plane + */ + public Plane(final Vector3D p1, final Vector3D p2, final Vector3D p3) { + this(p1, Vector3D.crossProduct(p2.subtract(p1), p3.subtract(p1))); + } + + /** Copy constructor. + *

The instance created is completely independant of the original + * one. A deep copy is used, none of the underlying object are + * shared.

+ * @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; + } + + /** Copy the instance. + *

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).

+ * @return a new hyperplane, copy of the instance + */ + public Hyperplane 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 + */ + public void reset(final Vector3D p, final Vector3D normal) { + setNormal(normal); + originOffset = -Vector3D.dotProduct(p, w); + setFrame(); + } + + /** Reset the instance from another one. + *

The updated instance is completely independant of the original + * one. A deep reset is used none of the underlying object is + * shared.

+ * @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 small + */ + private void setNormal(final Vector3D normal) { + final double norm = normal.getNorm(); + if (norm < 1.0e-10) { + throw new IllegalArgumentException("null norm"); + } + w = new Vector3D(1.0 / norm, normal); + } + + /** Reset the plane frame. + */ + private void setFrame() { + origin = new Point3D(-originOffset, w); + u = w.orthogonal(); + v = Vector3D.crossProduct(w, u); + } + + /** Get the origin point of the plane frame. + *

The point returned is the orthogonal projection of the + * 3D-space origin in the plane.

+ * @return the origin point of the plane frame (point closest to the + * 3D-space origin) + */ + public Point3D getOrigin() { + return origin; + } + + /** Get the normalized normal vector. + *

The frame defined by ({@link #getU getU}, {@link #getV getV}, + * {@link #getNormal getNormal}) is a rigth-handed orthonormalized + * frame).

+ * @return normalized normal vector + * @see #getU + * @see #getV + */ + public Vector3D getNormal() { + return w; + } + + /** Get the plane first canonical vector. + *

The frame defined by ({@link #getU getU}, {@link #getV getV}, + * {@link #getNormal getNormal}) is a rigth-handed orthonormalized + * frame).

+ * @return normalized first canonical vector + * @see #getV + * @see #getNormal + */ + public Vector3D getU() { + return u; + } + + /** Get the plane second canonical vector. + *

The frame defined by ({@link #getU getU}, {@link #getV getV}, + * {@link #getNormal getNormal}) is a rigth-handed orthonormalized + * frame).

+ * @return normalized second canonical vector + * @see #getU + * @see #getNormal + */ + public Vector3D getV() { + return v; + } + + /** Revert the plane. + *

Replace the instance by a similar plane with opposite orientation.

+ *

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.

+ */ + public void revertSelf() { + final Vector3D tmp = u; + u = v; + v = tmp; + w = w.negate(); + originOffset = -originOffset; + } + + /** Transform a 3D space point into an in-plane point. + * @param point point of the space (must be a {@link Vector3D + * Vector3D} instance) + * @return in-plane point (really a {@link + * org.apache.commons.math.geometry.euclidean.twoD.Point2D Point2D} instance) + * @see #toSpace + */ + public Point toSubSpace(final Point point) { + final Vector3D p3D = (Vector3D) point; + return new Point2D(Vector3D.dotProduct(p3D, u), + Vector3D.dotProduct(p3D, v)); + } + + /** Transform an in-plane point into a 3D space point. + * @param point in-plane point (must be a {@link + * org.apache.commons.math.geometry.euclidean.twoD.Point2D Point2D} instance) + * @return 3D space point (really a {@link Vector3D Vector3D} instance) + * @see #toSubSpace + */ + public Point toSpace(final Point point) { + final Point2D p2D = (Point2D) point; + return new Point3D(p2D.x, u, p2D.y, 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 Vector3D getPointAt(final Point2D inPlane, final double offset) { + return new Vector3D(inPlane.x, u, inPlane.y, v, offset - originOffset, w); + } + + /** Check if the instance is similar to another plane. + *

Planes are considered similar if they contain the same + * points. This does not mean they are equal since they can have + * opposite normals.

+ * @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 = Vector3D.angle(w, plane.w); + return ((angle < 1.0e-10) && (FastMath.abs(originOffset - plane.originOffset) < 1.0e-10)) || + ((angle > (FastMath.PI - 1.0e-10)) && (FastMath.abs(originOffset + plane.originOffset) < 1.0e-10)); + } + + /** Rotate the plane around the specified point. + *

The instance is not modified, a new instance is created.

+ * @param center rotation center + * @param rotation vectorial rotation operator + * @return a new plane + */ + public Plane rotate(final Vector3D center, final Rotation rotation) { + + final Vector3D delta = origin.subtract(center); + final Plane plane = new Plane(center.add(rotation.applyTo(delta)), + rotation.applyTo(w)); + + // 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. + *

The instance is not modified, a new instance is created.

+ * @param translation translation to apply + * @return a new plane + */ + public Plane translate(final Vector3D translation) { + + final Plane plane = new Plane(origin.add(translation), w); + + // 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 Point3D intersection(final Line line) { + final Vector3D direction = line.getDirection(); + final double dot = Vector3D.dotProduct(w, direction); + if (FastMath.abs(dot) < 1.0e-10) { + return null; + } + final Vector3D point = (Vector3D) line.toSpace(Point1D.ZERO); + final double k = -(originOffset + Vector3D.dotProduct(w, point)) / dot; + return new Point3D(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 SubSpace intersection(final Hyperplane other) { + final Plane otherP = (Plane) other; + final Vector3D direction = Vector3D.crossProduct(w, otherP.w); + if (direction.getNorm() < 1.0e-10) { + return null; + } + return new Line(intersection(this, otherP, new Plane(direction)), + direction); + } + + /** 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 Vector3D 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 (FastMath.abs(determinant) < 1.0e-10) { + return null; + } + + final double r = 1.0 / determinant; + return new Vector3D( + (-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 + */ + public Region wholeHyperplane() { + return new PolygonsSet(); + } + + /** Build a region covering the whole space. + * @return a region containing the instance (really a {@link + * PolyhedronsSet PolyhedronsSet} instance) + */ + public Region wholeSpace() { + return new PolyhedronsSet(); + } + + /** Check if the instance contains a point. + * @param p point to check + * @return true if p belongs to the plane + */ + public boolean contains(final Point3D p) { + return FastMath.abs(getOffset(p)) < 1.0e-10; + } + + /** Get the offset (oriented distance) of a parallel plane. + *

This method should be called only for parallel planes otherwise + * the result is not meaningful.

+ *

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.

+ * @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 point. + *

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.

+ * @param point point to check + * @return offset of the point + */ + public double getOffset(final Point point) { + return Vector3D.dotProduct((Vector3D) point, 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 + */ + public boolean sameOrientationAs(final Hyperplane other) { + return Vector3D.dotProduct(((Plane) other).w, w) > 0.0; + } + + /** Compute the relative position of a sub-hyperplane with respect + * to the instance. + * @param sub sub-hyperplane to check + * @return one of {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#PLUS PLUS}, + * {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#MINUS MINUS}, + * {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#BOTH BOTH}, + * {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#HYPER HYPER} + */ + public Side side(final SubHyperplane sub) { + + final Plane otherPlane = (Plane) sub.getHyperplane(); + final Line inter = (Line) intersection(otherPlane); + + if (inter == null) { + // the hyperplanes are parallel, + // any point can be used to check their relative position + final double global = getOffset(otherPlane); + return (global < -1.0e-10) ? Side.MINUS : ((global > 1.0e-10) ? Side.PLUS : Side.HYPER); + } + + // create a 2D line in the otherPlane canonical 2D frame such that: + // - the line is the crossing line of the two planes in 3D + // - the line splits the otherPlane in two half planes with an + // orientation consistent with the orientation of the instance + // (i.e. the 3D half space on the plus side (resp. minus side) + // of the instance contains the 2D half plane on the plus side + // (resp. minus side) of the 2D line + Point2D p = (Point2D) otherPlane.toSubSpace(inter.toSpace(Point1D.ZERO)); + Point2D q = (Point2D) otherPlane.toSubSpace(inter.toSpace(Point1D.ONE)); + if (Vector3D.dotProduct(Vector3D.crossProduct(inter.getDirection(), + otherPlane.getNormal()), + w) < 0) { + final Point2D tmp = p; + p = q; + q = tmp; + } + final Hyperplane line2D = new org.apache.commons.math.geometry.euclidean.twoD.Line(p, q); + + // check the side on the 2D plane + return sub.getRemainingRegion().side(line2D); + + } + + /** Split a sub-hyperplane in two parts by the instance. + * @param sub sub-hyperplane to split + * @return an object containing both the part of the sub-hyperplane + * on the plus side of the instance and the part of the + * sub-hyperplane on the minus side of the instance + */ + public SplitSubHyperplane split(final SubHyperplane sub) { + + final Plane otherPlane = (Plane) sub.getHyperplane(); + final Line inter = (Line) intersection(otherPlane); + + if (inter == null) { + // the hyperplanes are parallel + final double global = getOffset(otherPlane); + return (global < -1.0e-10) ? new SplitSubHyperplane(null, sub) : new SplitSubHyperplane(sub, null); + } + + // the hyperplanes do intersect + Point2D p = (Point2D) otherPlane.toSubSpace(inter.toSpace(Point1D.ZERO)); + Point2D q = (Point2D) otherPlane.toSubSpace(inter.toSpace(Point1D.ONE)); + if (Vector3D.dotProduct(Vector3D.crossProduct(inter.getDirection(), + otherPlane.getNormal()), + w) < 0) { + final Point2D tmp = p; + p = q; + q = tmp; + } + final SubHyperplane l2DMinus = + new SubHyperplane(new org.apache.commons.math.geometry.euclidean.twoD.Line(p, q)); + final SubHyperplane l2DPlus = + new SubHyperplane(new org.apache.commons.math.geometry.euclidean.twoD.Line(q, p)); + + final BSPTree splitTree = + sub.getRemainingRegion().getTree(false).split(l2DMinus); + final BSPTree plusTree = Region.isEmpty(splitTree.getPlus()) ? + new BSPTree(Boolean.FALSE) : + new BSPTree(l2DPlus, new BSPTree(Boolean.FALSE), + splitTree.getPlus(), null); + + final BSPTree minusTree = Region.isEmpty(splitTree.getMinus()) ? + new BSPTree(Boolean.FALSE) : + new BSPTree(l2DMinus, new BSPTree(Boolean.FALSE), + splitTree.getMinus(), null); + + return new SplitSubHyperplane(new SubHyperplane(otherPlane.copySelf(), + new PolygonsSet(plusTree)), + new SubHyperplane(otherPlane.copySelf(), + new PolygonsSet(minusTree))); + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Point3D.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Point3D.java new file mode 100644 index 000000000..127ae8d9d --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Point3D.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.math.geometry.euclidean.threeD; + +import org.apache.commons.math.geometry.partitioning.Point; + +/** This class represents a 3D point. + *

Instances of this class are guaranteed to be immutable.

+ * @version $Revision$ $Date$ + */ +public class Point3D extends Vector3D implements Point { + + /** Point at undefined (NaN) coordinates. */ + public static final Point3D UNDEFINED = new Point3D(Double.NaN, Double.NaN, Double.NaN); + + /** Serializable UID. */ + private static final long serialVersionUID = 9128130934224884451L; + + /** Simple constructor. + * Build a vector from its coordinates + * @param x abscissa + * @param y ordinate + * @param z height + * @see #getX() + * @see #getY() + * @see #getZ() + */ + public Point3D(final double x, final double y, final double z) { + super(x, y, z); + } + + /** 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 Point3D(final double alpha, final double delta) { + super(alpha, 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 Point3D(final double a, final Vector3D u) { + super(a, u); + } + + /** 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 Point3D(final double a1, final Vector3D u1, final double a2, final Vector3D u2) { + super(a1, u1, a2, u2); + } + + /** 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 Point3D(final double a1, final Vector3D u1, final double a2, final Vector3D u2, + final double a3, final Vector3D u3) { + super(a1, u1, a2, u2, a3, u3); + } + + /** 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 Point3D(final double a1, final Vector3D u1, final double a2, final Vector3D u2, + final double a3, final Vector3D u3, final double a4, final Vector3D u4) { + super(a1, u1, a2, u2, a3, u3, a4, u4); + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/PolyhedronsSet.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/PolyhedronsSet.java new file mode 100644 index 000000000..a3629397a --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/PolyhedronsSet.java @@ -0,0 +1,413 @@ +/* + * 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.math.geometry.euclidean.threeD; + +import java.awt.geom.AffineTransform; +import java.util.Arrays; +import java.util.Collection; + +import org.apache.commons.math.geometry.euclidean.twoD.Point2D; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.BSPTreeVisitor; +import org.apache.commons.math.geometry.partitioning.Hyperplane; +import org.apache.commons.math.geometry.partitioning.Point; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.geometry.partitioning.Transform; +import org.apache.commons.math.util.FastMath; + +/** This class represents a 3D region: a set of polyhedrons. + * @version $Revision$ $Date$ + */ +public class PolyhedronsSet extends Region { + + /** Build a polyhedrons set representing the whole real line. + */ + public PolyhedronsSet() { + super(); + } + + /** Build a polyhedrons set from a BSP tree. + *

The leaf nodes of the BSP tree must 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}

+ * @param tree inside/outside BSP tree representing the region + */ + public PolyhedronsSet(final BSPTree tree) { + super(tree); + } + + /** Build a polyhedrons set from a Boundary REPresentation (B-rep). + *

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.

+ *

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 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 Region#checkPoint(Point) checkPoint} method will + * not be meaningful anymore.

+ *

If the boundary is empty, the region will represent the whole + * space.

+ * @param boundary collection of boundary elements, as a + * collection of {@link SubHyperplane SubHyperplane} objects + */ + public PolyhedronsSet(final Collection boundary) { + super(boundary); + } + + /** 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 + */ + public PolyhedronsSet(final double xMin, final double xMax, + final double yMin, final double yMax, + final double zMin, final double zMax) { + this(buildConvex(Arrays.asList(new Hyperplane[] { + new Plane(new Vector3D(xMin, 0, 0), Vector3D.MINUS_I), + new Plane(new Vector3D(xMax, 0, 0), Vector3D.PLUS_I), + new Plane(new Vector3D(0, yMin, 0), Vector3D.MINUS_J), + new Plane(new Vector3D(0, yMax, 0), Vector3D.PLUS_J), + new Plane(new Vector3D(0, 0, zMin), Vector3D.MINUS_K), + new Plane(new Vector3D(0, 0, zMax), Vector3D.PLUS_K) + })).getTree(false)); + } + + /** {@inheritDoc} */ + public Region buildNew(final BSPTree tree) { + return new PolyhedronsSet(tree); + } + + /** {@inheritDoc} */ + protected void computeGeometricalProperties() { + + // compute the contribution of all boundary facets + getTree(true).visit(new FacetsContributionVisitor()); + + if (getSize() < 0) { + // the polyhedrons set as a finite outside + // surrounded by an infinite inside + setSize(Double.POSITIVE_INFINITY); + setBarycenter(Point3D.UNDEFINED); + } else { + // the polyhedrons set is finite, apply the remaining scaling factors + setSize(getSize() / 3.0); + setBarycenter(new Point3D(1.0 / (4 * getSize()), (Vector3D) getBarycenter())); + } + + } + + /** Visitor computing geometrical properties. */ + private class FacetsContributionVisitor implements BSPTreeVisitor { + + /** Simple constructor. */ + public FacetsContributionVisitor() { + setSize(0); + setBarycenter(new Point3D(0, 0, 0)); + } + + /** {@inheritDoc} */ + public Order visitOrder(final BSPTree node) { + return Order.MINUS_SUB_PLUS; + } + + /** {@inheritDoc} */ + public void visitInternalNode(final BSPTree node) { + final BoundaryAttribute attribute = (BoundaryAttribute) node.getAttribute(); + if (attribute.getPlusOutside() != null) { + addContribution(attribute.getPlusOutside(), false); + } + if (attribute.getPlusInside() != null) { + addContribution(attribute.getPlusInside(), true); + } + } + + /** {@inheritDoc} */ + public void visitLeafNode(final BSPTree 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 facet, final boolean reversed) { + + final Region polygon = facet.getRemainingRegion(); + final double area = polygon.getSize(); + + if (Double.isInfinite(area)) { + setSize(Double.POSITIVE_INFINITY); + setBarycenter(Point3D.UNDEFINED); + } else { + + final Plane plane = (Plane) facet.getHyperplane(); + final Vector3D facetB = (Point3D) plane.toSpace(polygon.getBarycenter()); + double scaled = area * Vector3D.dotProduct(facetB, plane.getNormal()); + if (reversed) { + scaled = -scaled; + } + + setSize(getSize() + scaled); + setBarycenter(new Point3D(1.0, (Point3D) getBarycenter(), scaled, facetB)); + + } + + } + + } + + /** Get the first sub-hyperplane crossed by a semi-infinite line. + * @param point start point of the part of the line considered + * @param line line to consider (contains point) + * @return the first sub-hyperplaned crossed by the line after the + * given point, or null if the line does not intersect any + * sub-hyperplaned + */ + public SubHyperplane firstIntersection(final Vector3D point, final Line line) { + return recurseFirstIntersection(getTree(true), point, line); + } + + /** Get the first sub-hyperplane crossed by a semi-infinite line. + * @param node current node + * @param point start point of the part of the line considered + * @param line line to consider (contains point) + * @return the first sub-hyperplaned crossed by the line after the + * given point, or null if the line does not intersect any + * sub-hyperplaned + */ + private SubHyperplane recurseFirstIntersection(final BSPTree node, + final Vector3D point, + final Line line) { + + final SubHyperplane cut = node.getCut(); + if (cut == null) { + return null; + } + final BSPTree minus = node.getMinus(); + final BSPTree plus = node.getPlus(); + final Plane plane = (Plane) cut.getHyperplane(); + + // establish search order + final double offset = plane.getOffset((Point) point); + final boolean in = FastMath.abs(offset) < 1.0e-10; + final BSPTree near; + final BSPTree far; + if (offset < 0) { + near = minus; + far = plus; + } else { + near = plus; + far = minus; + } + + if (in) { + // search in the cut hyperplane + final SubHyperplane facet = boundaryFacet(point, node); + if (facet != null) { + return facet; + } + } + + // search in the near branch + final SubHyperplane crossed = recurseFirstIntersection(near, point, line); + if (crossed != null) { + return crossed; + } + + if (!in) { + // search in the cut hyperplane + final Vector3D hit3D = plane.intersection(line); + if (hit3D != null) { + final SubHyperplane facet = boundaryFacet(hit3D, node); + if (facet != null) { + return facet; + } + } + } + + // search in the far branch + return recurseFirstIntersection(far, point, line); + + } + + /** Check if a point belongs to the boundary part of a node. + * @param point point to check + * @param node node containing the boundary facet to check + * @return the boundary facet this points belongs to (or null if it + * does not belong to any boundary facet) + */ + private SubHyperplane boundaryFacet(final Vector3D point, final BSPTree node) { + final Point point2D = node.getCut().getHyperplane().toSubSpace((Point) point); + final BoundaryAttribute attribute = (BoundaryAttribute) node.getAttribute(); + if ((attribute.getPlusOutside() != null) && + (attribute.getPlusOutside().getRemainingRegion().checkPoint(point2D) == Location.INSIDE)) { + return attribute.getPlusOutside(); + } + if ((attribute.getPlusInside() != null) && + (attribute.getPlusInside().getRemainingRegion().checkPoint(point2D) == Location.INSIDE)) { + return attribute.getPlusInside(); + } + return null; + } + + /** Rotate the region around the specified point. + *

The instance is not modified, a new instance is created.

+ * @param center rotation center + * @param rotation vectorial rotation operator + * @return a new instance representing the rotated region + */ + public PolyhedronsSet rotate(final Vector3D center, final Rotation rotation) { + return (PolyhedronsSet) applyTransform(new RotationTransform(center, rotation)); + } + + /** 3D rotation as a Transform. */ + private static class RotationTransform implements Transform { + + /** Center point of the rotation. */ + private Vector3D center; + + /** Vectorial rotation. */ + private Rotation rotation; + + /** Cached original hyperplane. */ + private Hyperplane cachedOriginal; + + /** Cached 2D transform valid inside the cached original hyperplane. */ + private Transform cachedTransform; + + /** Build a rotation transform. + * @param center center point of the rotation + * @param rotation vectorial rotation + */ + public RotationTransform(final Vector3D center, final Rotation rotation) { + this.center = center; + this.rotation = rotation; + } + + /** {@inheritDoc} */ + public Point apply(final Point point) { + final Vector3D delta = ((Vector3D) point).subtract(center); + return new Point3D(1.0, center, 1.0, rotation.applyTo(delta)); + } + + /** {@inheritDoc} */ + public Hyperplane apply(final Hyperplane hyperplane) { + return ((Plane) hyperplane).rotate(center, rotation); + } + + /** {@inheritDoc} */ + public SubHyperplane apply(final SubHyperplane sub, + final Hyperplane original, final Hyperplane transformed) { + if (original != cachedOriginal) { + // we have changed hyperplane, reset the in-hyperplane transform + + final Plane oPlane = (Plane) original; + final Plane tPlane = (Plane) transformed; + final Vector3D p00 = oPlane.getOrigin(); + final Vector3D p10 = (Vector3D) oPlane.toSpace(new Point2D(1.0, 0.0)); + final Vector3D p01 = (Vector3D) oPlane.toSpace(new Point2D(0.0, 1.0)); + final Point2D tP00 = (Point2D) tPlane.toSubSpace(apply((Point) p00)); + final Point2D tP10 = (Point2D) tPlane.toSubSpace(apply((Point) p10)); + final Point2D tP01 = (Point2D) tPlane.toSubSpace(apply((Point) p01)); + final AffineTransform at = + new AffineTransform(tP10.getX() - tP00.getX(), tP10.getY() - tP00.getY(), + tP01.getX() - tP00.getX(), tP01.getY() - tP00.getY(), + tP00.getX(), tP00.getY()); + + cachedOriginal = original; + cachedTransform = org.apache.commons.math.geometry.euclidean.twoD.Line.getTransform(at); + + } + return sub.applyTransform(cachedTransform); + } + + } + + /** Translate the region by the specified amount. + *

The instance is not modified, a new instance is created.

+ * @param translation translation to apply + * @return a new instance representing the translated region + */ + public PolyhedronsSet translate(final Vector3D translation) { + return (PolyhedronsSet) applyTransform(new TranslationTransform(translation)); + } + + /** 3D translation as a transform. */ + private static class TranslationTransform implements Transform { + + /** Translation vector. */ + private Vector3D translation; + + /** Cached original hyperplane. */ + private Hyperplane cachedOriginal; + + /** Cached 2D transform valid inside the cached original hyperplane. */ + private Transform cachedTransform; + + /** Build a translation transform. + * @param translation translation vector + */ + public TranslationTransform(final Vector3D translation) { + this.translation = translation; + } + + /** {@inheritDoc} */ + public Point apply(final Point point) { + return new Point3D(1.0, (Vector3D) point, 1.0, translation); + } + + /** {@inheritDoc} */ + public Hyperplane apply(final Hyperplane hyperplane) { + return ((Plane) hyperplane).translate(translation); + } + + /** {@inheritDoc} */ + public SubHyperplane apply(final SubHyperplane sub, + final Hyperplane original, final Hyperplane transformed) { + if (original != cachedOriginal) { + // we have changed hyperplane, reset the in-hyperplane transform + + final Plane oPlane = (Plane) original; + final Plane tPlane = (Plane) transformed; + final Point2D shift = (Point2D) tPlane.toSubSpace(apply((Point) oPlane.getOrigin())); + final AffineTransform at = + AffineTransform.getTranslateInstance(shift.getX(), shift.getY()); + + cachedOriginal = original; + cachedTransform = + org.apache.commons.math.geometry.euclidean.twoD.Line.getTransform(at); + + } + + return sub.applyTransform(cachedTransform); + + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/Rotation.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Rotation.java similarity index 99% rename from src/main/java/org/apache/commons/math/geometry/Rotation.java rename to src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Rotation.java index babc4a2a6..7f4f443c9 100644 --- a/src/main/java/org/apache/commons/math/geometry/Rotation.java +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Rotation.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import java.io.Serializable; diff --git a/src/main/java/org/apache/commons/math/geometry/RotationOrder.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/RotationOrder.java similarity index 98% rename from src/main/java/org/apache/commons/math/geometry/RotationOrder.java rename to src/main/java/org/apache/commons/math/geometry/euclidean/threeD/RotationOrder.java index f6aae1972..844ddefd5 100644 --- a/src/main/java/org/apache/commons/math/geometry/RotationOrder.java +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/RotationOrder.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; /** * This class is a utility representing a rotation order specification diff --git a/src/main/java/org/apache/commons/math/geometry/Vector3D.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3D.java similarity index 99% rename from src/main/java/org/apache/commons/math/geometry/Vector3D.java rename to src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3D.java index 2d915e570..5b50c2265 100644 --- a/src/main/java/org/apache/commons/math/geometry/Vector3D.java +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3D.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import java.io.Serializable; diff --git a/src/main/java/org/apache/commons/math/geometry/Vector3DFormat.java b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormat.java similarity index 99% rename from src/main/java/org/apache/commons/math/geometry/Vector3DFormat.java rename to src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormat.java index ff63a0e94..2aff0925e 100644 --- a/src/main/java/org/apache/commons/math/geometry/Vector3DFormat.java +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormat.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import java.text.FieldPosition; import java.text.NumberFormat; diff --git a/src/main/java/org/apache/commons/math/geometry/package.html b/src/main/java/org/apache/commons/math/geometry/euclidean/threeD/package.html similarity index 100% rename from src/main/java/org/apache/commons/math/geometry/package.html rename to src/main/java/org/apache/commons/math/geometry/euclidean/threeD/package.html diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Line.java b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Line.java new file mode 100644 index 000000000..5e8754e10 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Line.java @@ -0,0 +1,512 @@ +/* + * 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.math.geometry.euclidean.twoD; + +import java.awt.geom.AffineTransform; + +import org.apache.commons.math.exception.MathIllegalArgumentException; +import org.apache.commons.math.exception.util.LocalizedFormats; +import org.apache.commons.math.geometry.euclidean.oneD.IntervalsSet; +import org.apache.commons.math.geometry.euclidean.oneD.OrientedPoint; +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.Hyperplane; +import org.apache.commons.math.geometry.partitioning.Point; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.geometry.partitioning.SubSpace; +import org.apache.commons.math.geometry.partitioning.Transform; +import org.apache.commons.math.util.FastMath; +import org.apache.commons.math.util.MathUtils; + +/** This class represents an oriented line in the 2D plane. + + *

An oriented line can be defined either by prolongating a line + * segment between two points past these points, or by one point and + * an angular direction (in trigonometric orientation).

+ + *

Since it is oriented the two half planes at its two sides are + * unambiguously identified as a left half plane and a right half + * plane. This can be used to identify the interior and the exterior + * in a simple way by local properties only when part of a line is + * used to define part of a polygon boundary.

+ + *

A line can also be used to completely define a reference frame + * in the plane. It is sufficient to select one specific point in the + * line (the orthogonal projection of the original reference frame on + * the line) and to use the unit vector in the line direction and the + * orthogonal vector oriented from left half plane to right half + * plane. We define two coordinates by the process, the + * abscissa along the line, and the offset across + * the line. All points of the plane are uniquely identified by these + * two coordinates. The line is the set of points at zero offset, the + * left half plane is the set of points with negative offsets and the + * right half plane is the set of points with positive offsets.

+ + * @version $Revision$ $Date$ + */ +public class Line implements Hyperplane { + + /** Angle with respect to the abscissa axis. */ + private double angle; + + /** Cosine of the line angle. */ + private double cos; + + /** Sine of the line angle. */ + private double sin; + + /** Offset of the frame origin. */ + private double originOffset; + + /** Build a line from two points. + *

The line is oriented from p1 to p2

+ * @param p1 first point + * @param p2 second point + */ + public Line(final Point2D p1, final Point2D p2) { + reset(p1, p2); + } + + /** Build a line from a point and an angle. + * @param p point belonging to the line + * @param angle angle of the line with respect to abscissa axis + */ + public Line(final Point2D p, final double angle) { + reset(p, angle); + } + + /** Build a line from its internal characteristics. + * @param angle angle of the line with respect to abscissa axis + * @param cos cosine of the angle + * @param sin sine of the angle + * @param originOffset offset of the origin + */ + private Line(final double angle, final double cos, final double sin, final double originOffset) { + this.angle = angle; + this.cos = cos; + this.sin = sin; + this.originOffset = originOffset; + } + + /** Copy constructor. + *

The created instance is completely independant from the + * original instance, it is a deep copy.

+ * @param line line to copy + */ + public Line(final Line line) { + angle = MathUtils.normalizeAngle(line.angle, FastMath.PI); + cos = FastMath.cos(angle); + sin = FastMath.sin(angle); + originOffset = line.originOffset; + } + + /** {@inheritDoc} */ + public Hyperplane copySelf() { + return new Line(this); + } + + /** Reset the instance as if built from two points. + *

The line is oriented from p1 to p2

+ * @param p1 first point + * @param p2 second point + */ + public void reset(final Point2D p1, final Point2D p2) { + final double dx = p2.x - p1.x; + final double dy = p2.y - p1.y; + final double d = FastMath.hypot(dx, dy); + if (d == 0.0) { + angle = 0.0; + cos = 1.0; + sin = 0.0; + originOffset = p1.y; + } else { + angle = FastMath.PI + FastMath.atan2(-dy, -dx); + cos = FastMath.cos(angle); + sin = FastMath.sin(angle); + originOffset = (p2.x * p1.y - p1.x * p2.y) / d; + } + } + + /** Reset the instance as if built from a line and an angle. + * @param p point belonging to the line + * @param alpha angle of the line with respect to abscissa axis + */ + public void reset(final Point2D p, final double alpha) { + this.angle = MathUtils.normalizeAngle(alpha, FastMath.PI); + cos = FastMath.cos(this.angle); + sin = FastMath.sin(this.angle); + originOffset = cos * p.y - sin * p.x; + } + + /** Revert the instance. + */ + public void revertSelf() { + if (angle < FastMath.PI) { + angle += FastMath.PI; + } else { + angle -= FastMath.PI; + } + cos = -cos; + sin = -sin; + originOffset = -originOffset; + } + + /** Get the reverse of the instance. + *

Get a line with reversed orientation with respect to the + * instance. A new object is built, the instance is untouched.

+ * @return a new line, with orientation opposite to the instance orientation + */ + public Line getReverse() { + return new Line((angle < FastMath.PI) ? (angle + FastMath.PI) : (angle - FastMath.PI), + -cos, -sin, -originOffset); + } + + /** Transform a 2D space point into a line point. + * @param point 2D point (must be a {@link Point2D Point2D} + * instance) + * @return line point corresponding to the 2D point (really a {@link + * org.apache.commons.math.geometry.euclidean.oneD.Point1D Point1D} instance) + * @see #toSpace + */ + public Point toSubSpace(final Point point) { + final Point2D p2D = (Point2D) point; + return new Point1D(cos * p2D.x + sin * p2D.y); + } + + /** Get one point from the line. + * @param point desired abscissa for the point (must be a {@link + * org.apache.commons.math.geometry.euclidean.oneD.Point1D Point1D} instance) + * @return line point at specified abscissa (really a {@link Point2D + * Point2D} instance) + */ + public Point toSpace(final Point point) { + final double abscissa = ((Point1D) point).getAbscissa(); + return new Point2D(abscissa * cos - originOffset * sin, + abscissa * sin + originOffset * cos); + } + + /** Get the intersection point of the instance and another line. + * @param other other line + * @return intersection point of the instance and the other line + * (really a {@link Point2D Point2D} instance) + */ + public SubSpace intersection(final Hyperplane other) { + final Line otherL = (Line) other; + final double d = sin * otherL.cos - otherL.sin * cos; + if (FastMath.abs(d) < 1.0e-10) { + return null; + } + return new Point2D((cos * otherL.originOffset - otherL.cos * originOffset) / d, + (sin * otherL.originOffset - otherL.sin * originOffset) / d); + } + + /** Build a region covering the whole hyperplane. + * @return a region covering the whole hyperplane + */ + public Region wholeHyperplane() { + return new IntervalsSet(); + } + + /** Build a region covering the whole space. + * @return a region containing the instance (really a {@link + * PolygonsSet PolygonsSet} instance) + */ + public Region wholeSpace() { + return new PolygonsSet(); + } + + /** Get the offset (oriented distance) of a parallel line. + *

This method should be called only for parallel lines otherwise + * the result is not meaningful.

+ *

The offset is 0 if both lines are the same, it is + * positive if the line is on the right side of the instance and + * negative if it is on the left side, according to its natural + * orientation.

+ * @param line line to check + * @return offset of the line + */ + public double getOffset(final Line line) { + return originOffset + + ((cos * line.cos + sin * line.sin > 0) ? -line.originOffset : line.originOffset); + } + + /** Get the offset (oriented distance) of a point to the line. + *

The offset is 0 if the point belongs to the line, it is + * positive if the point is on the right side of the line and + * negative if it is on the left side, according to its natural + * orientation.

+ * @param point point to check (must be a {@link Point2D Point2D} instance) + * @return offset of the point + */ + public double getOffset(final Point point) { + final Point2D p2D = (Point2D) point; + return sin * p2D.x - cos * p2D.y + originOffset; + } + + /** Check if the instance has the same orientation as another hyperplane. + *

This method is expected to be called on parallel hyperplanes + * (i.e. when the {@link #side side} method would return {@link + * org.apache.commons.math.geometry.partitioning.Hyperplane.Side#HYPER HYPER} + * for some sub-hyperplane having the specified hyperplane + * as its underlying hyperplane). The method should not + * re-check for parallelism, only for orientation, typically by + * testing something like the sign of the dot-products of + * normals.

+ * @param other other hyperplane to check against the instance + * @return true if the instance and the other hyperplane have + * the same orientation + */ + public boolean sameOrientationAs(final Hyperplane other) { + final Line otherL = (Line) other; + return (sin * otherL.sin + cos * otherL.cos) >= 0.0; + } + + /** Get one point from the plane. + * @param abscissa desired abscissa for the point + * @param offset desired offset for the point + * @return one point in the plane, with given abscissa and offset + * relative to the line + */ + public Point2D getPointAt(final Point1D abscissa, final double offset) { + final double x = abscissa.getAbscissa(); + final double dOffset = offset - originOffset; + return new Point2D(x * cos + dOffset * sin, x * sin - dOffset * cos); + } + + /** Check if the line contains a point. + * @param p point to check + * @return true if p belongs to the line + */ + public boolean contains(final Point2D p) { + return FastMath.abs(getOffset(p)) < 1.0e-10; + } + + /** Check the instance is parallel to another line. + * @param line other line to check + * @return true if the instance is parallel to the other line + * (they can have either the same or opposite orientations) + */ + public boolean isParallelTo(final Line line) { + return FastMath.abs(sin * line.cos - cos * line.sin) < 1.0e-10; + } + + /** Translate the line to force it passing by a point. + * @param p point by which the line should pass + */ + public void translateToPoint(final Point2D p) { + originOffset = cos * p.y - sin * p.x; + } + + /** Get the angle of the line. + * @return the angle of the line with respect to the abscissa axis + */ + public double getAngle() { + return MathUtils.normalizeAngle(angle, FastMath.PI); + } + + /** Set the angle of the line. + * @param angle new angle of the line with respect to the abscissa axis + */ + public void setAngle(final double angle) { + this.angle = MathUtils.normalizeAngle(angle, FastMath.PI); + cos = FastMath.cos(this.angle); + sin = FastMath.sin(this.angle); + } + + /** Get the offset of the origin. + * @return the offset of the origin + */ + public double getOriginOffset() { + return originOffset; + } + + /** Set the offset of the origin. + * @param offset offset of the origin + */ + public void setOriginOffset(final double offset) { + originOffset = offset; + } + + /** Compute the relative position of a sub-hyperplane with respect + * to the instance. + * @param sub sub-hyperplane to check + * @return one of {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#PLUS PLUS}, + * {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#MINUS MINUS}, + * {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#BOTH BOTH}, + * {@link org.apache.commons.math.geometry.partitioning.Hyperplane.Side#HYPER HYPER} + */ + public Side side(final SubHyperplane sub) { + + final Hyperplane otherHyp = sub.getHyperplane(); + final Point2D crossing = (Point2D) intersection(otherHyp); + + if (crossing == null) { + // the lines are parallel, + final double global = getOffset((Line) otherHyp); + return (global < -1.0e-10) ? Side.MINUS : ((global > 1.0e-10) ? Side.PLUS : Side.HYPER); + } + + // the lines do intersect + final boolean direct = FastMath.sin(((Line) otherHyp).angle - angle) < 0; + final Point1D x = (Point1D) otherHyp.toSubSpace(crossing); + return sub.getRemainingRegion().side(new OrientedPoint(x, direct)); + + } + + /** Split a sub-hyperplane in two parts by the instance. + * @param sub sub-hyperplane to split + * @return an object containing both the part of the sub-hyperplane + * on the plus side of the instance and the part of the + * sub-hyperplane on the minus side of the instance + */ + public SplitSubHyperplane split(final SubHyperplane sub) { + + final Line otherLine = (Line) sub.getHyperplane(); + final Point2D crossing = (Point2D) intersection(otherLine); + + if (crossing == null) { + // the lines are parallel + final double global = getOffset(otherLine); + return (global < -1.0e-10) ? + new SplitSubHyperplane(null, sub) : + new SplitSubHyperplane(sub, null); + } + + // the lines do intersect + final boolean direct = FastMath.sin(otherLine.angle - angle) < 0; + final Point1D x = (Point1D) otherLine.toSubSpace(crossing); + final SubHyperplane subPlus = new SubHyperplane(new OrientedPoint(x, !direct)); + final SubHyperplane subMinus = new SubHyperplane(new OrientedPoint(x, direct)); + + final BSPTree splitTree = + sub.getRemainingRegion().getTree(false).split(subMinus); + final BSPTree plusTree = Region.isEmpty(splitTree.getPlus()) ? + new BSPTree(Boolean.FALSE) : + new BSPTree(subPlus, new BSPTree(Boolean.FALSE), + splitTree.getPlus(), null); + final BSPTree minusTree = Region.isEmpty(splitTree.getMinus()) ? + new BSPTree(Boolean.FALSE) : + new BSPTree(subMinus, new BSPTree(Boolean.FALSE), + splitTree.getMinus(), null); + + return new SplitSubHyperplane(new SubHyperplane(otherLine.copySelf(), + new IntervalsSet(plusTree)), + new SubHyperplane(otherLine.copySelf(), + new IntervalsSet(minusTree))); + + } + + /** Get a {@link org.apache.commons.math.geometry.partitioning.Transform + * Transform} embedding an affine transform. + * @param transform affine transform to embed (must be inversible + * otherwise the {@link + * org.apache.commons.math.geometry.partitioning.Transform#apply(Hyperplane) + * apply(Hyperplane)} method would work only for some lines, and + * fail for other ones) + * @return a new transform that can be applied to either {@link + * Point2D Point2D}, {@link Line Line} or {@link + * org.apache.commons.math.geometry.partitioning.SubHyperplane + * SubHyperplane} instances + * @exception MathIllegalArgumentException if the transform is non invertible + */ + public static Transform getTransform(final AffineTransform transform) throws MathIllegalArgumentException { + return new LineTransform(transform); + } + + /** Class embedding an affine transform. + *

This class is used in order to apply an affine transform to a + * line. Using a specific object allow to perform some computations + * on the transform only once even if the same transform is to be + * applied to a large number of lines (for example to a large + * polygon)./

+ */ + private static class LineTransform implements Transform { + + // CHECKSTYLE: stop JavadocVariable check + private double cXX; + private double cXY; + private double cX1; + private double cYX; + private double cYY; + private double cY1; + + private double c1Y; + private double c1X; + private double c11; + // CHECKSTYLE: resume JavadocVariable check + + /** Build an affine line transform from a n {@code AffineTransform}. + * @param transform transform to use (must be invertible otherwise + * the {@link LineTransform#apply(Hyperplane)} method would work + * only for some lines, and fail for other ones) + * @exception MathIllegalArgumentException if the transform is non invertible + */ + public LineTransform(final AffineTransform transform) throws MathIllegalArgumentException { + + final double[] m = new double[6]; + transform.getMatrix(m); + cXX = m[0]; + cXY = m[2]; + cX1 = m[4]; + cYX = m[1]; + cYY = m[3]; + cY1 = m[5]; + + c1Y = cXY * cY1 - cYY * cX1; + c1X = cXX * cY1 - cYX * cX1; + c11 = cXX * cYY - cYX * cXY; + + if (FastMath.abs(c11) < 1.0e-20) { + throw new MathIllegalArgumentException(LocalizedFormats.NON_INVERTIBLE_TRANSFORM); + } + + } + + /** {@inheritDoc} */ + public Point apply(final Point point) { + final Point2D p2D = (Point2D) point; + final double x = p2D.getX(); + final double y = p2D.getY(); + return new Point2D(cXX * x + cXY * y + cX1, + cYX * x + cYY * y + cY1); + } + + /** {@inheritDoc} */ + public Hyperplane apply(final Hyperplane hyperplane) { + final Line line = (Line) hyperplane; + final double rOffset = c1X * line.cos + c1Y * line.sin + c11 * line.originOffset; + final double rCos = cXX * line.cos + cXY * line.sin; + final double rSin = cYX * line.cos + cYY * line.sin; + final double inv = 1.0 / FastMath.sqrt(rSin * rSin + rCos * rCos); + return new Line(FastMath.PI + FastMath.atan2(-rSin, -rCos), + inv * rCos, inv * rSin, + inv * rOffset); + } + + /** {@inheritDoc} */ + public SubHyperplane apply(final SubHyperplane sub, + final Hyperplane original, final Hyperplane transformed) { + final OrientedPoint op = (OrientedPoint) sub.getHyperplane(); + final Point1D newLoc = + (Point1D) transformed.toSubSpace(apply(original.toSpace(op.getLocation()))); + return new SubHyperplane(new OrientedPoint(newLoc, op.isDirect())); + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/NestedLoops.java b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/NestedLoops.java new file mode 100644 index 000000000..e9be044f2 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/NestedLoops.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math.geometry.euclidean.twoD; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; + +import org.apache.commons.math.exception.MathIllegalArgumentException; +import org.apache.commons.math.exception.util.LocalizedFormats; +import org.apache.commons.math.geometry.euclidean.oneD.OrientedPoint; +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.partitioning.Hyperplane; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; + +/** This class represent a tree of nested 2D boundary loops. + + *

This class is used during Piece instances construction. + * Beams are built using the outline edges as + * representative of facets, the orientation of these facets are + * meaningful. However, we want to allow the user to specify its + * outline loops without having to take care of this orientation. This + * class is devoted to correct mis-oriented loops.

+ + *

Orientation is computed assuming the piece is finite, i.e. the + * outermost loops have their exterior side facing points at infinity, + * and hence are oriented counter-clockwise. The orientation of + * internal loops is computed as the reverse of the orientation of + * their immediate surrounding loop.

+ + * @version $Revision$ $Date$ + */ +class NestedLoops { + + /** Boundary loop. */ + private Point2D[] loop; + + /** Surrounded loops. */ + private ArrayList surrounded; + + /** Polygon enclosing a finite region. */ + private Region polygon; + + /** Indicator for original loop orientation. */ + private boolean originalIsClockwise; + + /** Simple Constructor. + *

Build an empty tree of nested loops. This instance will become + * the root node of a complete tree, it is not associated with any + * loop by itself, the outermost loops are in the root tree child + * nodes.

+ */ + public NestedLoops() { + surrounded = new ArrayList(); + } + + /** Constructor. + *

Build a tree node with neither parent nor children

+ * @param loop boundary loop (will be reversed in place if needed) + * @exception MathIllegalArgumentException if an outline has an open boundary loop + */ + private NestedLoops(final Point2D[] loop) throws MathIllegalArgumentException { + + if (loop[0] == null) { + throw new MathIllegalArgumentException(LocalizedFormats.OUTLINE_BOUNDARY_LOOP_OPEN); + } + + this.loop = loop; + surrounded = new ArrayList(); + + // build the polygon defined by the loop + final ArrayList edges = new ArrayList(); + Point2D current = loop[loop.length - 1]; + for (int i = 0; i < loop.length; ++i) { + final Point2D previous = current; + current = loop[i]; + final Line line = new Line(previous, current); + final Region region = Region.buildConvex(Arrays.asList(new Hyperplane[] { + new OrientedPoint((Point1D) line.toSubSpace(previous), false), + new OrientedPoint((Point1D) line.toSubSpace(current), true) + })); + edges.add(new SubHyperplane(line, region)); + } + polygon = new PolygonsSet(edges); + + // ensure the polygon encloses a finite region of the plane + if (Double.isInfinite(polygon.getSize())) { + polygon = polygon.getComplement(); + originalIsClockwise = false; + } else { + originalIsClockwise = true; + } + + } + + /** Add a loop in a tree. + * @param bLoop boundary loop (will be reversed in place if needed) + * @exception MathIllegalArgumentException if an outline has crossing + * boundary loops or open boundary loops + */ + public void add(final Point2D[] bLoop) throws MathIllegalArgumentException { + add(new NestedLoops(bLoop)); + } + + /** Add a loop in a tree. + * @param node boundary loop (will be reversed in place if needed) + * @exception MathIllegalArgumentException if an outline has boundary + * loops that cross each other + */ + private void add(final NestedLoops node) throws MathIllegalArgumentException { + + // check if we can go deeper in the tree + for (final NestedLoops child : surrounded) { + if (child.polygon.contains(node.polygon)) { + child.add(node); + return; + } + } + + // check if we can absorb some of the instance children + for (final Iterator iterator = surrounded.iterator(); iterator.hasNext();) { + final NestedLoops child = iterator.next(); + if (node.polygon.contains(child.polygon)) { + node.surrounded.add(child); + iterator.remove(); + } + } + + // we should be separate from the remaining children + for (final NestedLoops child : surrounded) { + if (!Region.intersection(node.polygon, child.polygon).isEmpty()) { + throw new MathIllegalArgumentException(LocalizedFormats.CROSSING_BOUNDARY_LOOPS); + } + } + + surrounded.add(node); + + } + + /** Correct the orientation of the loops contained in the tree. + *

This is this method that really inverts the loops that where + * provided through the {@link #add(Point2D[]) add} method if + * they are mis-oriented

+ */ + public void correctOrientation() { + for (NestedLoops child : surrounded) { + child.setClockWise(true); + } + } + + /** Set the loop orientation. + * @param clockwise if true, the loop should be set to clockwise + * orientation + */ + private void setClockWise(final boolean clockwise) { + + if (originalIsClockwise ^ clockwise) { + // we need to inverse the original loop + int min = -1; + int max = loop.length; + while (++min < --max) { + final Point2D tmp = loop[min]; + loop[min] = loop[max]; + loop[max] = tmp; + } + } + + // go deeper in the tree + for (final NestedLoops child : surrounded) { + child.setClockWise(!clockwise); + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Point2D.java b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Point2D.java new file mode 100644 index 000000000..eb8fa2da9 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Point2D.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.math.geometry.euclidean.twoD; + +import org.apache.commons.math.geometry.partitioning.Point; +import org.apache.commons.math.geometry.partitioning.SubSpace; + +/** This class represents a 2D point. + *

Instances of this class are guaranteed to be immutable.

+ * @version $Revision$ $Date$ + */ +public class Point2D extends java.awt.geom.Point2D.Double implements Point, SubSpace { + + /** Point at undefined (NaN) coordinates. */ + public static final Point2D UNDEFINED = new Point2D(java.lang.Double.NaN, java.lang.Double.NaN); + + /** Serializable UID. */ + private static final long serialVersionUID = 8883702098988517151L; + + /** Build a point with default coordinates. + */ + public Point2D() { + } + + /** Build a point from its coordinates. + * @param x abscissa + * @param y ordinate + */ + public Point2D(final double x, final double y) { + super(x, y); + } + + /** Build a point from a java awt point. + * @param point java awt point + */ + public Point2D(final java.awt.geom.Point2D.Double point) { + super(point.x, point.y); + } + + /** Transform a 2D space point into a sub-space point. + * @param point 2D point of the space + * @return always return null + * @see #toSpace + */ + public Point toSubSpace(final Point point) { + return null; + } + + /** Transform a sub-space point into a space point. + * @param point ignored parameter + * @return always return the instance + * @see #toSubSpace + */ + public Point toSpace(final Point point) { + return this; + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/PolygonsSet.java b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/PolygonsSet.java new file mode 100644 index 000000000..728a357bb --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/PolygonsSet.java @@ -0,0 +1,343 @@ +/* + * 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.math.geometry.euclidean.twoD; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.Hyperplane; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.geometry.partitioning.utilities.AVLTree; +import org.apache.commons.math.util.FastMath; + +/** This class represents a 2D region: a set of polygons. + * @version $Revision$ $Date$ + */ +public class PolygonsSet extends Region { + + /** Vertices organized as boundary loops. */ + private Point2D[][] vertices; + + /** Build a polygons set representing the whole real line. + */ + public PolygonsSet() { + super(); + } + + /** Build a polygons set from a BSP tree. + *

The leaf nodes of the BSP tree must 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}

+ * @param tree inside/outside BSP tree representing the region + */ + public PolygonsSet(final BSPTree tree) { + super(tree); + } + + /** Build a polygons set from a Boundary REPresentation (B-rep). + *

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.

+ *

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 + * Region#checkPoint(org.apache.commons.math.geometry.partitioning.Point) + * checkPoint} method will not be meaningful anymore.

+ *

If the boundary is empty, the region will represent the whole + * space.

+ * @param boundary collection of boundary elements, as a + * collection of {@link SubHyperplane SubHyperplane} objects + */ + public PolygonsSet(final Collection boundary) { + super(boundary); + } + + /** 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 + */ + public PolygonsSet(final double xMin, final double xMax, + final double yMin, final double yMax) { + this(buildConvex(boxBoundary(xMin, xMax, yMin, yMax)).getTree(false)); + } + + /** Create a list of hyperplanes representing the boundary of a box. + * @param xMin low bound along the x direction + * @param xMax high bound along the x direction + * @param yMin low bound along the y direction + * @param yMax high bound along the y direction + * @return boundary of the box + */ + private static List boxBoundary(final double xMin, final double xMax, + final double yMin, final double yMax) { + final Point2D minMin = new Point2D(xMin, yMin); + final Point2D minMax = new Point2D(xMin, yMax); + final Point2D maxMin = new Point2D(xMax, yMin); + final Point2D maxMax = new Point2D(xMax, yMax); + return Arrays.asList(new Hyperplane[] { + new Line(minMin, maxMin), + new Line(maxMin, maxMax), + new Line(maxMax, minMax), + new Line(minMax, minMin) + }); + } + + /** {@inheritDoc} */ + public Region buildNew(final BSPTree tree) { + return new PolygonsSet(tree); + } + + /** {@inheritDoc} */ + protected void computeGeometricalProperties() { + + final Point2D[][] v = getVertices(); + + if (v.length == 0) { + if ((Boolean) getTree(false).getAttribute()) { + setSize(Double.POSITIVE_INFINITY); + setBarycenter(Point2D.UNDEFINED); + } else { + setSize(0); + setBarycenter(new Point2D(0, 0)); + } + } else if (v[0][0] == null) { + // there is at least one open-loop: the polygon is infinite + setSize(Double.POSITIVE_INFINITY); + setBarycenter(Point2D.UNDEFINED); + } else { + // all loops are closed, we compute some integrals around the shape + + double sum = 0; + double sumX = 0; + double sumY = 0; + + for (Point2D[] loop : v) { + double x1 = loop[loop.length - 1].x; + double y1 = loop[loop.length - 1].y; + for (final Point2D point : loop) { + final double x0 = x1; + final double y0 = y1; + x1 = point.x; + y1 = point.y; + final double factor = x0 * y1 - y0 * x1; + sum += factor; + sumX += factor * (x0 + x1); + sumY += factor * (y0 + y1); + } + } + + if (sum < 0) { + // the polygon as a finite outside surrounded by an infinite inside + setSize(Double.POSITIVE_INFINITY); + setBarycenter(Point2D.UNDEFINED); + } else { + setSize(sum / 2); + setBarycenter(new Point2D(sumX / (3 * sum), sumY / (3 * sum))); + } + + } + + } + + /** Get the vertices of the polygon. + *

The polygon boundary can be represented as an array of loops, + * each loop being itself an array of vertices.

+ *

In order to identify open loops which start and end by + * infinite edges, the open loops arrays start with a null point. In + * this case, the first non null point and the last point of the + * array do not represent real vertices, they are dummy points + * intended only to get the direction of the first and last edge. An + * open loop consisting of a single infinite line will therefore be + * represented by a three elements array with one null point + * followed by two dummy points. The open loops are always the first + * ones in the loops array.

+ *

If the polygon has no boundary at all, a zero length loop + * array will be returned.

+ *

All line segments in the various loops have the inside of the + * region on their left side and the outside on their right side + * when moving in the underlying line direction. This means that + * closed loops surrounding finite areas obey the direct + * trigonometric orientation.

+ * @return vertices of the polygon, organized as oriented boundary + * loops with the open loops first (the returned value is guaranteed + * to be non-null) + */ + public Point2D[][] getVertices() { + if (vertices == null) { + if (getTree(false).getCut() == null) { + vertices = new Point2D[0][]; + } else { + + // sort the segmfinal ents according to their start point + final SegmentsBuilder visitor = new SegmentsBuilder(); + getTree(true).visit(visitor); + final AVLTree sorted = visitor.getSorted(); + + // identify the loops, starting from the open ones + // (their start segments final are naturally at the sorted set beginning) + final ArrayList> loops = new ArrayList>(); + while (!sorted.isEmpty()) { + final AVLTree.Node node = sorted.getSmallest(); + final List loop = followLoop(node, sorted); + if (loop != null) { + loops.add(loop); + } + } + + // tranform the loops in an array of arrays of points + vertices = new Point2D[loops.size()][]; + int i = 0; + + for (final List loop : loops) { + if (loop.size() < 2) { + // sifinal ngle infinite line + final Line line = ((Segment) loop.get(0)).getLine(); + vertices[i++] = new Point2D[] { + null, + (Point2D) line.toSpace(new Point1D(-Float.MAX_VALUE)), + (Point2D) line.toSpace(new Point1D(+Float.MAX_VALUE)) + }; + } else if (((Segment) loop.get(0)).getStart() == null) { + // open lofinal op with at least one real point + final Point2D[] array = new Point2D[loop.size() + 2]; + int j = 0; + for (Segment segment : loop) { + + if (j == 0) { + // null point and first dummy point + double x = + ((Point1D) segment.getLine().toSubSpace(segment.getEnd())).getAbscissa(); + x -= FastMath.max(1.0, FastMath.abs(x / 2)); + array[j++] = null; + array[j++] = (Point2D) segment.getLine().toSpace(new Point1D(x)); + } + + if (j < (array.length - 1)) { + // current point + array[j++] = segment.getEnd(); + } + + if (j == (array.length - 1)) { + // last dummy point + double x = + ((Point1D) segment.getLine().toSubSpace(segment.getStart())).getAbscissa(); + x += FastMath.max(1.0, FastMath.abs(x / 2)); + array[j++] = (Point2D) segment.getLine().toSpace(new Point1D(x)); + } + + } + vertices[i++] = array; + } else { + final Point2D[] array = new Point2D[loop.size()]; + int j = 0; + for (Segment segment : loop) { + array[j++] = segment.getStart(); + } + vertices[i++] = array; + } + } + + } + } + + return vertices.clone(); + + } + + /** Follow a boundary loop. + * @param node node containing the segment starting the loop + * @param sorted set of segments belonging to the boundary, sorted by + * start points (contains {@code node}) + * @return a list of connected sub-hyperplanes starting at + * {@code node} + */ + private List followLoop(final AVLTree.Node node, + final AVLTree sorted) { + + final ArrayList loop = new ArrayList(); + Segment segment = (Segment) node.getElement(); + loop.add(segment); + final Point2D globalStart = segment.getStart(); + Point2D end = segment.getEnd(); + node.delete(); + + // is this an open or a closed loop ? + final boolean open = segment.getStart() == null; + + while ((end != null) && (open || (globalStart.distance(end) > 1.0e-10))) { + + // search the sub-hyperplane starting where the previous one ended + AVLTree.Node selectedNode = null; + Segment selectedSegment = null; + double selectedDistance = Double.POSITIVE_INFINITY; + final Segment lowerLeft = new Segment(end, -1.0e-10, -1.0e-10); + final Segment upperRight = new Segment(end, +1.0e-10, +1.0e-10); + for (AVLTree.Node n = sorted.getNotSmaller(lowerLeft); + (n != null) && (n.getElement().compareTo(upperRight) <= 0); + n = n.getNext()) { + segment = (Segment) n.getElement(); + final double distance = end.distance(segment.getStart()); + if (distance < selectedDistance) { + selectedNode = n; + selectedSegment = segment; + selectedDistance = distance; + } + } + + if (selectedDistance > 1.0e-10) { + // this is a degenerated loop, it probably comes from a very + // tiny region with some segments smaller than the threshold, we + // simply ignore it + return null; + } + + end = selectedSegment.getEnd(); + loop.add(selectedSegment); + selectedNode.delete(); + + } + + if ((loop.size() == 2) && !open) { + // this is a degenerated infinitely thin loop, we simply ignore it + return null; + } + + if ((end == null) && !open) { + throw new RuntimeException("internal error"); + } + + return loop; + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Segment.java b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Segment.java new file mode 100644 index 000000000..42e6403bf --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/Segment.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.math.geometry.euclidean.twoD; + +import org.apache.commons.math.geometry.partitioning.utilities.OrderedTuple; + +/** This class holds segments information before they are connected. + * @version $Revision$ $Date$ + */ +class Segment implements Comparable { + + /** Start point of the segment. */ + private final Point2D start; + + /** End point of the segments. */ + private final Point2D end; + + /** Line containing the segment. */ + private final Line line; + + /** Sorting key. */ + private OrderedTuple sortingKey; + + /** Build a segment. + * @param start start point of the segment + * @param end end point of the segment + * @param line line containing the segment + */ + public Segment(final Point2D start, final Point2D end, final Line line) { + this.start = start; + this.end = end; + this.line = line; + sortingKey = (start == null) ? + new OrderedTuple(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY) : + new OrderedTuple(start.x, start.y); + } + + /** Build a dummy segment. + *

+ * The object built is not a real segment, only the sorting key is used to + * allow searching in the neighborhood of a point. This is an horrible hack ... + *

+ * @param start start point of the segment + * @param dx abscissa offset from the start point + * @param dy ordinate offset from the start point + */ + public Segment(final Point2D start, final double dx, final double dy) { + this.start = null; + this.end = null; + this.line = null; + sortingKey = new OrderedTuple(start.x + dx, start.y + dy); + } + + /** Get the start point of the segment. + * @return start point of the segment + */ + public Point2D getStart() { + return start; + } + + /** Get the end point of the segment. + * @return end point of the segment + */ + public Point2D getEnd() { + return end; + } + + /** Get the line containing the segment. + * @return line containing the segment + */ + public Line getLine() { + return line; + } + + /** {@inheritDoc} */ + public int compareTo(final Segment o) { + return sortingKey.compareTo(o.sortingKey); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } else if (other instanceof Segment) { + return compareTo((Segment) other) == 0; + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return start.hashCode() ^ end.hashCode() ^ line.hashCode() ^ sortingKey.hashCode(); + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/SegmentBuilder.java b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/SegmentBuilder.java new file mode 100644 index 000000000..bf77c2448 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/SegmentBuilder.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math.geometry.euclidean.twoD; + +import java.util.List; + +import org.apache.commons.math.geometry.euclidean.oneD.Interval; +import org.apache.commons.math.geometry.euclidean.oneD.IntervalsSet; +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.BSPTreeVisitor; +import org.apache.commons.math.geometry.partitioning.Region.BoundaryAttribute; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.geometry.partitioning.utilities.AVLTree; + +/** Visitor building segments. + * @version $Revision$ $Date$ + */ +class SegmentsBuilder implements BSPTreeVisitor { + + /** Sorted segments. */ + private AVLTree sorted; + + /** Simple constructor. */ + public SegmentsBuilder() { + sorted = new AVLTree(); + } + + /** {@inheritDoc} */ + public Order visitOrder(final BSPTree node) { + return Order.MINUS_SUB_PLUS; + } + + /** {@inheritDoc} */ + public void visitInternalNode(final BSPTree node) { + final BoundaryAttribute attribute = (BoundaryAttribute) node.getAttribute(); + if (attribute.getPlusOutside() != null) { + addContribution(attribute.getPlusOutside(), false); + } + if (attribute.getPlusInside() != null) { + addContribution(attribute.getPlusInside(), true); + } + } + + /** {@inheritDoc} */ + public void visitLeafNode(final BSPTree node) { + } + + /** Add he contribution of a boundary facet. + * @param sub boundary facet + * @param reversed if true, the facet has the inside on its plus side + */ + private void addContribution(final SubHyperplane sub, final boolean reversed) { + final Line line = (Line) sub.getHyperplane(); + final List intervals = ((IntervalsSet) sub.getRemainingRegion()).asList(); + for (final Interval i : intervals) { + final Point2D start = Double.isInfinite(i.getLower()) ? + null : (Point2D) line.toSpace(new Point1D(i.getLower())); + final Point2D end = Double.isInfinite(i.getUpper()) ? + null : (Point2D) line.toSpace(new Point1D(i.getUpper())); + if (reversed) { + sorted.insert(new Segment(end, start, line.getReverse())); + } else { + sorted.insert(new Segment(start, end, line)); + } + } + } + + /** Get the sorted segments. + * @return sorted segments + */ + public AVLTree getSorted() { + return sorted; + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/package.html b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/package.html new file mode 100644 index 000000000..4e4ff1bfe --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/euclidean/twoD/package.html @@ -0,0 +1,24 @@ + + + + +

+This package provides basic 2D geometry components. +

+ + diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/BSPTree.java b/src/main/java/org/apache/commons/math/geometry/partitioning/BSPTree.java new file mode 100644 index 000000000..18c105309 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/BSPTree.java @@ -0,0 +1,631 @@ +/* + * 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.math.geometry.partitioning; + +import org.apache.commons.math.geometry.partitioning.Hyperplane.Side; +import org.apache.commons.math.util.FastMath; + +/** This class represent a Binary Space Partition tree. + + *

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.

+ + *

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.

+ + *

Another example would be to represent Voronoi tesselations, the + * attribute of each cell holding the defining point of the cell.

+ + *

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.

+ + *

One of the main sources for the development of this package was + * Bruce Naylor, John Amanatides and William Thibault paper Merging + * BSP Trees Yields Polyhedral Set Operations Proc. Siggraph '90, + * Computer Graphics 24(4), August 1990, pp 115-124, published by the + * Association for Computing Machinery (ACM).

+ + * @version $Revision$ $Date$ + */ +public class BSPTree { + + /** Cut sub-hyperplane. */ + private SubHyperplane cut; + + /** Tree at the plus side of the cut hyperplane. */ + private BSPTree plus; + + /** Tree at the minus side of the cut hyperplane. */ + private BSPTree minus; + + /** Parent tree. */ + private BSPTree 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. + *

This method does not perform any verification on + * consistency of its arguments, it should therefore be used only + * when then caller knows what it is doing.

+ *

This method is mainly useful kto build trees + * bottom-up. Building trees top-down is realized with the help of + * method {@link #insertCut insertCut}.

+ * @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 cut, final BSPTree plus, final BSPTree 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. + *

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 no cut hyperplane will be + * inserted and the cell will be changed to a leaf cell. The + * attribute of the node is never changed.

+ *

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).

+ * @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 hyperplane) { + + if (cut != null) { + plus.parent = null; + minus.parent = null; + } + + final SubHyperplane chopped = fitToCell(new SubHyperplane(hyperplane)); + if (chopped.getRemainingRegion().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. + *

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 nodes attributes and immutable + * objects).

+ * @return a new tree, copy of the instance + */ + public BSPTree 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 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 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 getMinus() { + return minus; + } + + /** Get the parent node. + * @return parent node, null if the node has no parents + */ + public BSPTree 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 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; + default: + throw new RuntimeException("internal error"); + } + + } + } + + /** Fit a sub-hyperplane inside the cell defined by the instance. + *

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.

+ * @param sub sub-hyperplane to fit + * @return a new sub-hyperplane, gueranteed to have no part outside + * of the instance cell + */ + private SubHyperplane fitToCell(final SubHyperplane sub) { + SubHyperplane s = sub; + for (BSPTree tree = this; tree.parent != null; tree = tree.parent) { + if (tree == tree.parent.plus) { + s = tree.parent.cut.getHyperplane().split(s).getPlus(); + } else { + s = tree.parent.cut.getHyperplane().split(s).getMinus(); + } + } + return s; + } + + /** Get the cell to which a point belongs. + *

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.

+ * @param point point to check + * @return the tree cell to which the point belongs (can be + */ + public BSPTree getCell(final Point point) { + + if (cut == null) { + return this; + } + + // position of the point with respect to the cut hyperplane + final double offset = cut.getHyperplane().getOffset(point); + + if (FastMath.abs(offset) < 1.0e-10) { + return this; + } else if (offset <= 0) { + // point is on the minus side of the cut hyperplane + return minus.getCell(point); + } else { + // point is on the plus side of the cut hyperplane + return plus.getCell(point); + } + + } + + /** Perform condensation on a tree. + *

The condensation operation is not recursive, it must be called + * explicitely from leaves to root.

+ */ + 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. + *

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, no such copy is done here!

+ *

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).

+ * @param tree other tree to merge with the instance (will be + * unusable 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 instance <op> + * tree, this value can be ignored if parentTree is not null + * since all connections have already been established + */ + public BSPTree merge(final BSPTree tree, final LeafMerger 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 + * unusable 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 instance <op> + * tree, this value can be ignored if parentTree is not null + * since all connections have already been established + */ + private BSPTree merge(final BSPTree tree, final LeafMerger leafMerger, + final BSPTree 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 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(new SubHyperplane(merged.cut.getHyperplane())); + } + + return merged; + + } + } + + /** This interface gather the merging operations between a BSP tree + * leaf and another BSP tree. + *

As explained in Bruce Naylor, John Amanatides and William + * Thibault paper Merging + * BSP Trees Yields Polyhedral Set Operations, + * 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).

+ * @version $Revision$ $Date$ + */ + public static interface LeafMerger { + + /** Merge a leaf node and a tree node. + *

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.

+ *

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.

+ *

The {@link BSPTree#insertInTree BSPTree.insertInTree} method + * may be useful to implement this method.

+ * @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 merge(BSPTree leaf, BSPTree tree, + BSPTree parentTree, boolean isPlusChild, + boolean leafFromInstance); + + } + + /** Split a BSP tree by an external sub-hyperplane. + *

Split a tree in two halves, on each side of the + * sub-hyperplane. The instance is not modified.

+ *

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 not + * attached to any parent tree yet. This tree is intended to be + * later inserted into an higher level tree.

+ *

The algorithm used here is the one given in Naylor, Amanatides + * and Thibault paper (section III, Binary Partitioning of a BSP + * Tree).

+ * @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 split(final SubHyperplane sub) { + + if (cut == null) { + return new BSPTree(sub, copySelf(), new BSPTree(attribute), null); + } + + final Hyperplane cHyperplane = cut.getHyperplane(); + final Hyperplane sHyperplane = sub.getHyperplane(); + switch (cHyperplane.side(sub)) { + case PLUS : + { // the partitioning sub-hyperplane is entirely in the plus sub-tree + final BSPTree split = plus.split(sub); + if (sHyperplane.side(cut) == 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 split = minus.split(sub); + if (sHyperplane.side(cut) == 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 Hyperplane.SplitSubHyperplane cutParts = sHyperplane.split(cut); + final Hyperplane.SplitSubHyperplane subParts = cHyperplane.split(sub); + final BSPTree 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 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. + *

The instance itself is modified so its former parent should + * not be used anymore.

+ * @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 + * @see LeafMerger + */ + public void insertInTree(final BSPTree parentTree, final boolean isPlusChild) { + + // 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 tree = this; tree.parent != null; tree = tree.parent) { + + // this is an hyperplane of some parent node + final Hyperplane 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 = hyperplane.split(cut).getPlus(); + plus.chopOffMinus(hyperplane); + minus.chopOffMinus(hyperplane); + } else { + cut = hyperplane.split(cut).getMinus(); + plus.chopOffPlus(hyperplane); + minus.chopOffPlus(hyperplane); + } + + } + + // since we may have drop some parts of the inserted tree, + // perform a condensation pass to keep the tree structure simple + condense(); + + } + + } + + /** Chop off parts of the tree. + *

The instance is modified in place, all the parts that are on + * the minus side of the chopping hyperplane are disgarded, only the + * parts on the plus side remain.

+ * @param hyperplane chopping hyperplane + */ + private void chopOffMinus(final Hyperplane hyperplane) { + if (cut != null) { + cut = hyperplane.split(cut).getPlus(); + plus.chopOffMinus(hyperplane); + minus.chopOffMinus(hyperplane); + } + } + + /** Chop off parts of the tree. + *

The instance is modified in place, all the parts that are on + * the plus side of the chopping hyperplane are disgarded, only the + * parts on the minus side remain.

+ * @param hyperplane chopping hyperplane + */ + private void chopOffPlus(final Hyperplane hyperplane) { + if (cut != null) { + cut = hyperplane.split(cut).getMinus(); + plus.chopOffPlus(hyperplane); + minus.chopOffPlus(hyperplane); + } + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/BSPTreeVisitor.java b/src/main/java/org/apache/commons/math/geometry/partitioning/BSPTreeVisitor.java new file mode 100644 index 000000000..6cfd38cbd --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/BSPTreeVisitor.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math.geometry.partitioning; + +/** This interface is used to visit {@link BSPTree BSP tree} nodes. + + *

Navigation through {@link BSPTree BSP trees} can be done using + * two different point of views:

+ *
    + *
  • + * 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 + *
  • + *
  • + * 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. + *
  • + *
+ + * @see BSPTree + * @see SubHyperplane + + * @version $Revision$ $Date$ + */ +public interface BSPTreeVisitor { + + /** 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. + *

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.

+ * @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 node); + + /** Visit a BSP tree node node having a non-null sub-hyperplane. + *

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.

+ * @param node BSP node guaranteed to have a non null cut sub-hyperplane + * @see #visitLeafNode + */ + void visitInternalNode(BSPTree 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 node); + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/Characterization.java b/src/main/java/org/apache/commons/math/geometry/partitioning/Characterization.java new file mode 100644 index 000000000..a96b5ba5b --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/Characterization.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math.geometry.partitioning; + +/** Characterization of a sub-hyperplane. + * @version $Revision$ $Date$ + */ +class Characterization { + + /** Parts of the sub-hyperplane that have inside cells on the tested side. */ + private SubHyperplane in; + + /** Parts of the sub-hyperplane that have outside cells on the tested side. */ + private SubHyperplane out; + + /** Create an empty characterization of a sub-hyperplane. + */ + public Characterization() { + in = null; + out = null; + } + + /** Check if the sub-hyperplane that have inside cells on the tested side. + * @return true if the sub-hyperplane that have inside cells on the tested side + */ + public boolean hasIn() { + return (in != null) && (!in.getRemainingRegion().isEmpty()); + } + + /** Get the parts of the sub-hyperplane that have inside cells on the tested side. + * @return parts of the sub-hyperplane that have inside cells on the tested side + */ + public SubHyperplane getIn() { + return in; + } + + /** Check if the sub-hyperplane that have outside cells on the tested side. + * @return true if the sub-hyperplane that have outside cells on the tested side + */ + public boolean hasOut() { + return (out != null) && (!out.getRemainingRegion().isEmpty()); + } + + /** Get the parts of the sub-hyperplane that have outside cells on the tested side. + * @return parts of the sub-hyperplane that have outside cells on the tested side + */ + public SubHyperplane getOut() { + return out; + } + + /** Add a part of the sub-hyperplane known to have inside or outside cell on the tested side. + * @param sub part of the sub-hyperplane to add + * @param inside if true, the part added as an inside cell on the tested side, otherwise + * it has an outside cell on the tested side + */ + public void add(final SubHyperplane sub, final boolean inside) { + if (inside) { + if (in == null) { + in = sub; + } else { + in = new SubHyperplane(in.getHyperplane(), + Region.union(in.getRemainingRegion(), + sub.getRemainingRegion())); + } + } else { + if (out == null) { + out = sub; + } else { + out = new SubHyperplane(out.getHyperplane(), + Region.union(out.getRemainingRegion(), + sub.getRemainingRegion())); + } + } + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/Hyperplane.java b/src/main/java/org/apache/commons/math/geometry/partitioning/Hyperplane.java new file mode 100644 index 000000000..dd8fa775a --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/Hyperplane.java @@ -0,0 +1,159 @@ +/* + * 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.math.geometry.partitioning; + +/** This interface represents an hyperplane of a space. + + *

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.

+ + * @version $Revision$ $Date$ + */ +public interface Hyperplane extends SubSpace { + + /** Enumerate for specifying sides of the hyperplane. */ + 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; + + } + + /** Copy the instance. + *

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).

+ * @return a new hyperplane, copy of the instance + */ + Hyperplane copySelf(); + + /** Get the offset (oriented distance) of a point. + *

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.

+ * @param point point to check + * @return offset of the point + */ + double getOffset(Point point); + + /** Check if the instance has the same orientation as another hyperplane. + *

This method is expected to be called on parallel hyperplanes + * (i.e. when the {@link #side side} method would return {@link + * Side#HYPER} for some sub-hyperplane having the specified hyperplane + * as its underlying hyperplane). The method should not + * re-check for parallelism, only for orientation, typically by + * testing something like the sign of the dot-products of + * normals.

+ * @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 other); + + /** Build the sub-space shared by the instance and another hyperplane. + * @param other other hyperplane + * @return a sub-space at the intersection of the instance and the + * other sub-space (it has a dimension one unit less than the + * instance) + */ + SubSpace intersection(Hyperplane other); + + /** Build a region covering the whole hyperplane. + *

The region build is restricted to the sub-space defined by the + * hyperplane. This means that the regions points are consistent + * with the argument of the {@link SubSpace#toSpace toSpace} method + * and with the return value of the {@link SubSpace#toSubSpace + * toSubSpace} method.

+ * @return a region covering the whole hyperplane + */ + Region wholeHyperplane(); + + /** Build a region covering the whole space. + * @return a region containing the instance + */ + Region wholeSpace(); + + /** Compute the relative position of a sub-hyperplane with respect + * to the instance. + * @param sub sub-hyperplane to check + * @return one of {@link Side#PLUS}, {@link Side#MINUS}, {@link Side#BOTH}, + * {@link Side#HYPER} + */ + Side side(SubHyperplane sub); + + /** Split a sub-hyperplane in two parts by the instance. + * @param sub sub-hyperplane to split + * @return an object containing both the part of the sub-hyperplane + * on the plus side of the instance and the part of the + * sub-hyperplane on the minus side of the instance + */ + SplitSubHyperplane split(SubHyperplane sub); + + /** Class holding the results of the {@link Hyperplane#split Hyperplane.split} + * method. */ + class SplitSubHyperplane { + + /** Part of the sub-hyperplane on the plus side of the splitting hyperplane. */ + private final SubHyperplane plus; + + /** Part of the sub-hyperplane on the minus side of the splitting hyperplane. */ + private final SubHyperplane 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 plus, final SubHyperplane 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 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 getMinus() { + return minus; + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/Point.java b/src/main/java/org/apache/commons/math/geometry/partitioning/Point.java new file mode 100644 index 000000000..ad9d3daea --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/Point.java @@ -0,0 +1,29 @@ +/* + * 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.math.geometry.partitioning; + +/** This interface represents a generic point to be used in a space partition. + *

Points are completely virtual entities with no specification at + * all, so this class is essentially a marker interface with no + * methods. This allows to perform partition in traditional euclidean + * n-dimensions spaces, but also in more exotic universes like for + * example the surface of the unit sphere.

+ * @version $Revision$ $Date$ + */ +public interface Point { + // nothing here, this is only a marker interface +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/Region.java b/src/main/java/org/apache/commons/math/geometry/partitioning/Region.java new file mode 100644 index 000000000..b28c42361 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/Region.java @@ -0,0 +1,1069 @@ +/* + * 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.math.geometry.partitioning; + +import java.util.Collection; +import java.util.TreeSet; +import java.util.Comparator; +import java.util.Iterator; +import java.util.ArrayList; + +/** This class represent a region of a space as a partition. + + *

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 inside the region from + * points considered to be outside of it. In between, there + * may be points on the boundary of the region.

+ + *

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.

+ + *

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.

+ + * @version $Revision$ $Date$ + */ +public abstract class Region { + + /** Enumerate for the location of a point with respect to the region. */ + public static 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; + } + + /** Inside/Outside BSP tree. */ + private BSPTree tree; + + /** Size of the instance. */ + private double size; + + /** Barycenter. */ + private Point barycenter; + + /** Build a region representing the whole space. + */ + protected Region() { + tree = new BSPTree(Boolean.TRUE); + } + + /** Build a region from an inside/outside BSP tree. + *

The leaf nodes of the BSP tree must 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 must have either null internal nodes or + * internal nodes representing the boundary as specified in the + * {@link #getTree getTree} method).

+ * @param tree inside/outside BSP tree representing the region + */ + protected Region(final BSPTree tree) { + this.tree = tree; + } + + /** Build a Region from a Boundary REPresentation (B-rep). + *

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.

+ *

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.

+ *

If the boundary is empty, the region will represent the whole + * space.

+ * @param boundary collection of boundary elements, as a + * collection of {@link SubHyperplane SubHyperplane} objects + */ + protected Region(final Collection boundary) { + + 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 ordered = new TreeSet(new Comparator() { + public int compare(final SubHyperplane o1, final SubHyperplane o2) { + final double size1 = o1.getRemainingRegion().getSize(); + final double size2 = o2.getRemainingRegion().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() { + + /** {@inheritDoc} */ + public Order visitOrder(final BSPTree node) { + return Order.PLUS_SUB_MINUS; + } + + /** {@inheritDoc} */ + public void visitInternalNode(final BSPTree node) { + } + + /** {@inheritDoc} */ + public void visitLeafNode(final BSPTree node) { + node.setAttribute((node == node.getParent().getPlus()) ? + Boolean.FALSE : Boolean.TRUE); + } + }); + + } + + } + + /** Build a region using the instance as a prototype. + *

This method allow to create new instances without knowing + * exactly the type of the region. It is an application of the + * prototype design pattern.

+ *

The leaf nodes of the BSP tree must 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 must have either null internal nodes or + * internal nodes representing the boundary as specified in the + * {@link #getTree getTree} method).

+ * @param newTree inside/outside BSP tree representing the new region + * @return the built region + */ + public abstract Region buildNew(BSPTree newTree); + + /** 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 node, final Collection boundary) { + + final Iterator iterator = boundary.iterator(); + + // build the current level + Hyperplane 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 plusList = new ArrayList(); + final ArrayList minusList = new ArrayList(); + while (iterator.hasNext()) { + final SubHyperplane other = iterator.next(); + switch (inserted.side(other)) { + case PLUS: + plusList.add(other); + break; + case MINUS: + minusList.add(other); + break; + case BOTH: + final Hyperplane.SplitSubHyperplane split = inserted.split(other); + 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); + + } + + /** 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 + */ + public static Region buildConvex(final Collection hyperplanes) { + if (hyperplanes.isEmpty()) { + return null; + } + + // use the first hyperplane to build the right class + final Region region = hyperplanes.iterator().next().wholeSpace(); + + // chop off parts of the space + BSPTree node = region.tree; + node.setAttribute(Boolean.TRUE); + for (final Hyperplane hyperplane : hyperplanes) { + if (node.insertCut(hyperplane)) { + node.setAttribute(null); + node.getPlus().setAttribute(Boolean.FALSE); + node = node.getMinus(); + node.setAttribute(Boolean.TRUE); + } + } + + return region; + + } + + /** Copy the instance. + *

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).

+ * @return a new region, copy of the instance + */ + public Region copySelf() { + return buildNew(tree.copySelf()); + } + + /** Check if the instance is empty. + * @return true if the instance is empty + */ + public boolean isEmpty() { + return isEmpty(tree); + } + + /** Check if the sub-tree starting at a given node is empty. + * @param node root node of the sub-tree (must 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 + */ + public static boolean isEmpty(final BSPTree 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 !isInside(node); + } + + // check both sides of the sub-tree + return isEmpty(node.getMinus()) && isEmpty(node.getPlus()); + + } + + /** Check a leaf node inside attribute. + * @param node leaf node to check + * @return true if the leaf node is an inside node + */ + private static boolean isInside(final BSPTree node) { + return (Boolean) node.getAttribute(); + } + + /** 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 + */ + public boolean contains(final Region region) { + return difference(region, this).isEmpty(); + } + + /** 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} + */ + public Location checkPoint(final Point 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 + * Location#INSIDE}, {@link Location#OUTSIDE} or {@link Location#BOUNDARY} + */ + protected Location checkPoint(final BSPTree node, final Point point) { + final BSPTree cell = node.getCell(point); + if (cell.getCut() == null) { + // the point is in the interior of a cell, just check the attribute + return isInside(cell) ? 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; + + } + + /** Get the complement of the region (exchanged interior/exterior). + *

The instance is not modified, a new region is built.

+ * @return a new region, complement of the instance + */ + public Region getComplement() { + return buildNew(recurseComplement(tree)); + } + + /** Recursively build the complement of a BSP tree. + * @param node current node of the original tree + * @return new tree, complement of the node + */ + private static BSPTree recurseComplement(final BSPTree node) { + if (node.getCut() == null) { + return new BSPTree(isInside(node) ? Boolean.FALSE : Boolean.TRUE); + } + + BoundaryAttribute attribute = (BoundaryAttribute) node.getAttribute(); + if (attribute != null) { + final SubHyperplane plusOutside = + (attribute.plusInside == null) ? null : attribute.plusInside.copySelf(); + final SubHyperplane plusInside = + (attribute.plusOutside == null) ? null : attribute.plusOutside.copySelf(); + attribute = new BoundaryAttribute(plusOutside, plusInside); + } + + return new BSPTree(node.getCut().copySelf(), + recurseComplement(node.getPlus()), + recurseComplement(node.getMinus()), + attribute); + + } + + /** Get the underlying BSP tree. + + *

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.

+ + *

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#plusInside plusInside} and {@link + * BoundaryAttribute#plusOutside plusOutside} fields both null if + * the corresponding cut sub-hyperplane does not have any parts + * belonging to the boundary.

+ + *

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.

+ + * @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 + */ + public BSPTree getTree(final boolean includeBoundaryAttributes) { + if (includeBoundaryAttributes && (tree.getCut() != null) && (tree.getAttribute() == null)) { + // we need to compute the boundary attributes + recurseBuildBoundary(tree); + } + return tree; + } + + /** Class holding boundary attributes. + *

This class is used for the attributes associated with the + * nodes of region boundary shell trees returned by the {@link + * Region#getTree Region.getTree}. It contains the + * parts of the node cut sub-hyperplane that belong to the + * boundary.

+ *

This class is a simple placeholder, it does not provide any + * processing methods.

+ * @see Region#getTree + */ + public static class BoundaryAttribute { + + /** 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 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 plusInside; + + /** 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) + */ + public BoundaryAttribute(final SubHyperplane plusOutside, + final SubHyperplane plusInside) { + this.plusOutside = plusOutside; + this.plusInside = plusInside; + } + + /** 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 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 getPlusInside() { + return plusInside; + } + + + } + + /** Recursively build the boundary shell tree. + * @param node current node in the inout tree + */ + private void recurseBuildBoundary(final BSPTree node) { + if (node.getCut() != null) { + + SubHyperplane plusOutside = null; + SubHyperplane plusInside = null; + + // characterize the cut sub-hyperplane, + // first with respect to the plus sub-tree + final Characterization plusChar = new Characterization(); + characterize(node.getPlus(), node.getCut().copySelf(), plusChar); + + if (plusChar.hasOut()) { + // plusChar.out 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 minusChar = new Characterization(); + characterize(node.getMinus(), plusChar.getOut(), minusChar); + if (minusChar.hasIn()) { + plusOutside = minusChar.getIn(); + } + } + + if (plusChar.hasIn()) { + // plusChar.in 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 minusChar = new Characterization(); + characterize(node.getMinus(), plusChar.getIn(), minusChar); + if (minusChar.hasOut()) { + plusInside = minusChar.getOut(); + } + } + + node.setAttribute(new BoundaryAttribute(plusOutside, plusInside)); + recurseBuildBoundary(node.getPlus()); + recurseBuildBoundary(node.getMinus()); + + } + } + + /** Filter the parts of an hyperplane belonging to the boundary. + *

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 one 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.

+ * @param node current BSP tree node + * @param sub sub-hyperplane to characterize + * @param characterization placeholder where to put the characterized parts + */ + private static void characterize(final BSPTree node, final SubHyperplane sub, + final Characterization characterization) { + if (node.getCut() == null) { + // we have reached a leaf node + final boolean inside = (Boolean) node.getAttribute(); + characterization.add(sub, inside); + } else { + final Hyperplane hyperplane = node.getCut().getHyperplane(); + switch (hyperplane.side(sub)) { + case PLUS: + characterize(node.getPlus(), sub, characterization); + break; + case MINUS: + characterize(node.getMinus(), sub, characterization); + break; + case BOTH: + final Hyperplane.SplitSubHyperplane split = hyperplane.split(sub); + characterize(node.getPlus(), split.getPlus(), characterization); + characterize(node.getMinus(), split.getMinus(), characterization); + break; + default: + // this should not happen + throw new RuntimeException("internal error"); + } + } + } + + /** 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 ...) + */ + public double getBoundarySize() { + final BoundarySizeVisitor visitor = new BoundarySizeVisitor(); + getTree(true).visit(visitor); + return visitor.getSize(); + } + + /** Visitor computing the boundary size. */ + private static class BoundarySizeVisitor implements BSPTreeVisitor { + + /** Size of the boundary. */ + private double boundarySize; + + /** Simple constructor. + */ + public BoundarySizeVisitor() { + boundarySize = 0; + } + + /** {@inheritDoc}*/ + public Order visitOrder(final BSPTree node) { + return Order.MINUS_SUB_PLUS; + } + + /** {@inheritDoc}*/ + public void visitInternalNode(final BSPTree node) { + final BoundaryAttribute attribute = (BoundaryAttribute) node.getAttribute(); + if (attribute.plusOutside != null) { + boundarySize += attribute.plusOutside.getRemainingRegion().getSize(); + } + if (attribute.plusInside != null) { + boundarySize += attribute.plusInside.getRemainingRegion().getSize(); + } + } + + /** {@inheritDoc}*/ + public void visitLeafNode(final BSPTree node) { + } + + /** Get the size of the boundary. + * @return size of the boundary + */ + public double getSize() { + return boundarySize; + } + + } + + /** 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 ...) + */ + 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; + } + + /** Get the barycenter of the instance. + * @return an object representing the barycenter + */ + public Point getBarycenter() { + if (barycenter == null) { + computeGeometricalProperties(); + } + return barycenter; + } + + /** Set the barycenter of the instance. + * @param barycenter barycenter of the instance + */ + protected void setBarycenter(final Point barycenter) { + this.barycenter = barycenter; + } + + /** Compute some geometrical properties. + *

The properties to compute are the barycenter and the size.

+ */ + protected abstract void computeGeometricalProperties(); + + /** Transform a region. + *

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.

+ * @param transform transform to apply + * @return a new region, resulting from the application of the + * transform to the instance + */ + public Region applyTransform(final Transform transform) { + + // transform the BSP tree + final Region tRegion = buildNew(recurseTransform(tree, transform)); + + // transform the barycenter + if (barycenter != null) { + tRegion.size = size; + tRegion.barycenter = transform.apply(barycenter); + } + + return tRegion; + + } + + /** Recursively transform an inside/outside BSP-tree. + * @param node current BSP tree node + * @param transform transform to apply + * @return a new tree + */ + private BSPTree recurseTransform(final BSPTree node, final Transform transform) { + + if (node.getCut() == null) { + return new BSPTree(node.getAttribute()); + } + + final SubHyperplane sub = node.getCut(); + final SubHyperplane tSub = sub.applyTransform(transform); + BoundaryAttribute attribute = (BoundaryAttribute) node.getAttribute(); + if (attribute != null) { + final SubHyperplane tPO = + (attribute.getPlusOutside() == null) ? null : attribute.getPlusOutside().applyTransform(transform); + final SubHyperplane tPI = + (attribute.getPlusInside() == null) ? null : attribute.getPlusInside().applyTransform(transform); + attribute = new BoundaryAttribute(tPO, tPI); + } + + return new BSPTree(tSub, + recurseTransform(node.getPlus(), transform), + recurseTransform(node.getMinus(), transform), + attribute); + + } + + /** Compute the relative position of the instance with respect to an + * hyperplane. + * @param hyperplane reference hyperplane + * @return one of {@link Hyperplane.Side#PLUS Hyperplane.Side.PLUS}, {@link + * Hyperplane.Side#MINUS Hyperplane.Side.MINUS}, {@link Hyperplane.Side#BOTH + * Hyperplane.Side.BOTH} or {@link Hyperplane.Side#HYPER Hyperplane.Side.HYPER} + * (the latter result can occur only if the tree contains only one + * cut hyperplane) + */ + public Hyperplane.Side side(final Hyperplane hyperplane) { + final Sides sides = new Sides(); + recurseSides(tree, new SubHyperplane(hyperplane), sides); + return sides.plusFound() ? + (sides.minusFound() ? Hyperplane.Side.BOTH : Hyperplane.Side.PLUS) : + (sides.minusFound() ? Hyperplane.Side.MINUS : Hyperplane.Side.HYPER); + } + + /** Search recursively for inside leaf nodes on each side of the given hyperplane. + + *

The algorithm used here is directly derived from the one + * described in section III (Binary Partitioning of a BSP + * Tree) of the Bruce Naylor, John Amanatides and William + * Thibault paper Merging + * BSP Trees Yields Polyhedral Set Operations Proc. Siggraph + * '90, Computer Graphics 24(4), August 1990, pp 115-124, published + * by the Association for Computing Machinery (ACM)..

+ + * @param node current BSP tree node + * @param sub sub-hyperplane + * @param sides object holding the sides found + */ + private void recurseSides(final BSPTree node, final SubHyperplane sub, final Sides sides) { + + if (node.getCut() == null) { + if (isInside(node)) { + // this is an inside cell expanding across the hyperplane + sides.rememberPlusFound(); + sides.rememberMinusFound(); + } + return; + } + + final Hyperplane hyperplane = node.getCut().getHyperplane(); + switch (hyperplane.side(sub)) { + case PLUS : + // the sub-hyperplane is entirely in the plus sub-tree + if (sub.getHyperplane().side(node.getCut()) == Hyperplane.Side.PLUS) { + if (!isEmpty(node.getMinus())) { + sides.rememberPlusFound(); + } + } else { + if (!isEmpty(node.getMinus())) { + sides.rememberMinusFound(); + } + } + if (!(sides.plusFound() && sides.minusFound())) { + recurseSides(node.getPlus(), sub, sides); + } + break; + case MINUS : + // the sub-hyperplane is entirely in the minus sub-tree + if (sub.getHyperplane().side(node.getCut()) == Hyperplane.Side.PLUS) { + if (!isEmpty(node.getPlus())) { + sides.rememberPlusFound(); + } + } else { + if (!isEmpty(node.getPlus())) { + sides.rememberMinusFound(); + } + } + if (!(sides.plusFound() && sides.minusFound())) { + recurseSides(node.getMinus(), sub, sides); + } + break; + case BOTH : + // the sub-hyperplane extends in both sub-trees + final Hyperplane.SplitSubHyperplane split = hyperplane.split(sub); + + // explore first the plus sub-tree + recurseSides(node.getPlus(), split.getPlus(), sides); + + // if needed, explore the minus sub-tree + if (!(sides.plusFound() && sides.minusFound())) { + recurseSides(node.getMinus(), split.getMinus(), sides); + } + 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) || isInside(node.getPlus())) { + sides.rememberPlusFound(); + } + if ((node.getMinus().getCut() != null) || isInside(node.getMinus())) { + sides.rememberMinusFound(); + } + } else { + if ((node.getPlus().getCut() != null) || isInside(node.getPlus())) { + sides.rememberMinusFound(); + } + if ((node.getMinus().getCut() != null) || isInside(node.getMinus())) { + sides.rememberPlusFound(); + } + } + } + + } + + /** Utility class holding the already found sides. */ + private static final class Sides { + + /** 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. + */ + public Sides() { + plusFound = false; + minusFound = false; + } + + /** Remember the fact that inside leaf nodes have been found on the plus side. + */ + public void rememberPlusFound() { + 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; + } + + /** Remember the fact that inside leaf nodes have been found on the minus side. + */ + public void rememberMinusFound() { + minusFound = true; + } + + /** 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; + } + + } + + /** Get the parts of a sub-hyperplane that are contained in the region. + *

The parts of the sub-hyperplane that belong to the boundary are + * not included in the resulting parts.

+ * @param sub sub-hyperplane traversing the region + * @return filtered sub-hyperplane + */ + public SubHyperplane intersection(final SubHyperplane 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 recurseIntersection(final BSPTree node, final SubHyperplane sub) { + + if (node.getCut() == null) { + return isInside(node) ? sub.copySelf() : null; + } + + final Hyperplane hyperplane = node.getCut().getHyperplane(); + switch (hyperplane.side(sub)) { + case PLUS : + return recurseIntersection(node.getPlus(), sub); + case MINUS : + return recurseIntersection(node.getMinus(), sub); + case BOTH : + final Hyperplane.SplitSubHyperplane split = hyperplane.split(sub); + final SubHyperplane plus = recurseIntersection(node.getPlus(), split.getPlus()); + final SubHyperplane minus = recurseIntersection(node.getMinus(), split.getMinus()); + if (plus == null) { + return minus; + } else if (minus == null) { + return plus; + } else { + return new SubHyperplane(plus.getHyperplane(), + Region.union(plus.getRemainingRegion(), + minus.getRemainingRegion())); + } + default : + return recurseIntersection(node.getPlus(), + recurseIntersection(node.getMinus(), sub)); + } + + } + + /** 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 static Region union(final Region region1, final Region region2) { + final BSPTree tree = region1.tree.merge(region2.tree, new UnionMerger()); + tree.visit(new InternalNodesCleaner()); + 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 static Region intersection(final Region region1, final Region region2) { + final BSPTree tree = region1.tree.merge(region2.tree, new IntersectionMerger()); + tree.visit(new InternalNodesCleaner()); + 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 static Region xor(final Region region1, final Region region2) { + final BSPTree tree = region1.tree.merge(region2.tree, new XORMerger()); + tree.visit(new InternalNodesCleaner()); + 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 static Region difference(final Region region1, final Region region2) { + final BSPTree tree = region1.tree.merge(region2.tree, new DifferenceMerger()); + tree.visit(new InternalNodesCleaner()); + return region1.buildNew(tree); + } + + /** Leaf node / tree merger for union operation. */ + private static final class UnionMerger implements BSPTree.LeafMerger { + /** {@inheritDoc} */ + public BSPTree merge(final BSPTree leaf, final BSPTree tree, + final BSPTree parentTree, final boolean isPlusChild, + final boolean leafFromInstance) { + if (isInside(leaf)) { + // the leaf node represents an inside cell + leaf.insertInTree(parentTree, isPlusChild); + return leaf; + } + // the leaf node represents an outside cell + tree.insertInTree(parentTree, isPlusChild); + return tree; + } + }; + + /** Leaf node / tree merger for intersection operation. */ + private static final class IntersectionMerger implements BSPTree.LeafMerger { + /** {@inheritDoc} */ + public BSPTree merge(final BSPTree leaf, final BSPTree tree, + final BSPTree parentTree, final boolean isPlusChild, + final boolean leafFromInstance) { + if (isInside(leaf)) { + // the leaf node represents an inside cell + tree.insertInTree(parentTree, isPlusChild); + return tree; + } + // the leaf node represents an outside cell + leaf.insertInTree(parentTree, isPlusChild); + return leaf; + } + }; + + /** Leaf node / tree merger for xor operation. */ + private static final class XORMerger implements BSPTree.LeafMerger { + /** {@inheritDoc} */ + public BSPTree merge(final BSPTree leaf, final BSPTree tree, + final BSPTree parentTree, final boolean isPlusChild, + final boolean leafFromInstance) { + BSPTree t = tree; + if (isInside(leaf)) { + // the leaf node represents an inside cell + t = recurseComplement(t); + } + t.insertInTree(parentTree, isPlusChild); + return t; + } + }; + + /** Leaf node / tree merger for difference operation. + *

The algorithm used here is directly derived from the one + * described in section III (Binary Partitioning of a BSP + * Tree) of the Naylor, Amanatides and Thibault paper. An error + * was detected and corrected in the figure 5.1 of the article for + * merging leaf nodes with complete trees. Contrary to what is said + * in the figure, the {@code ELSE} part of if is not the same + * as the first part with {@code T1} and {@codeT2} + * swapped. {@code T1} and {@codeT2} must be swapped + * everywhere except in the {@code RETURN} part of the + * {@code DIFFERENCE} operation: if {@codeT2} is an + * in-cell, we must return {@code Complement_Bspt(T2)}, not + * {@code Complement_Bspt(T1)}, and if {@codeT2} is an + * out-cell, we must return {@code T1}, not {@codeT2}

+ */ + private static final class DifferenceMerger implements BSPTree.LeafMerger { + /** {@inheritDoc} */ + public BSPTree merge(final BSPTree leaf, final BSPTree tree, + final BSPTree parentTree, final boolean isPlusChild, + final boolean leafFromInstance) { + if (isInside(leaf)) { + // the leaf node represents an inside cell + final BSPTree argTree = recurseComplement(leafFromInstance ? tree : leaf); + argTree.insertInTree(parentTree, isPlusChild); + return argTree; + } + // the leaf node represents an outside cell + final BSPTree instanceTree = leafFromInstance ? leaf : tree; + instanceTree.insertInTree(parentTree, isPlusChild); + return instanceTree; + } + }; + + /** Visitor removing internal nodes attributes. */ + private static final class InternalNodesCleaner implements BSPTreeVisitor { + + /** {@inheritDoc} */ + public Order visitOrder(final BSPTree node) { + return Order.PLUS_SUB_MINUS; + } + + /** {@inheritDoc} */ + public void visitInternalNode(final BSPTree node) { + node.setAttribute(null); + } + + /** {@inheritDoc} */ + public void visitLeafNode(final BSPTree node) { + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/SubHyperplane.java b/src/main/java/org/apache/commons/math/geometry/partitioning/SubHyperplane.java new file mode 100644 index 000000000..759eeaa94 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/SubHyperplane.java @@ -0,0 +1,138 @@ +/* + * 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.math.geometry.partitioning; + +/** This interface represents the remaining parts of an hyperplane after + * other parts have been chopped off. + + *

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.

+ + * @version $Revision$ $Date$ + */ +public class SubHyperplane { + + /** Underlying hyperplane. */ + private final Hyperplane hyperplane; + + /** Remaining region of the hyperplane. */ + private final Region remainingRegion; + + /** Build a chopped hyperplane that is not chopped at all. + * @param hyperplane underlying hyperplane + */ + public SubHyperplane(final Hyperplane hyperplane) { + this.hyperplane = hyperplane; + remainingRegion = hyperplane.wholeHyperplane(); + } + + /** Build a sub-hyperplane from an hyperplane and a region. + * @param hyperplane underlying hyperplane + * @param remainingRegion remaining region of the hyperplane + */ + public SubHyperplane(final Hyperplane hyperplane, final Region remainingRegion) { + this.hyperplane = hyperplane; + this.remainingRegion = remainingRegion; + } + + /** Copy the instance. + *

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 nodes attributes and immutable + * objects).

+ * @return a new sub-hyperplane, copy of the instance + */ + public SubHyperplane copySelf() { + return new SubHyperplane(hyperplane.copySelf(), remainingRegion.copySelf()); + } + + /** Get the underlying hyperplane. + * @return underlying hyperplane + */ + public Hyperplane getHyperplane() { + return hyperplane; + } + + /** Get the remaining region of the hyperplane. + *

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.

+ * @return remaining region of the hyperplane + */ + public Region getRemainingRegion() { + return remainingRegion; + } + + /** Apply a transform to the instance. + *

The instance must be a (D-1)-dimension sub-hyperplane with + * respect to the transform not 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.

+ * @param transform D-dimension transform to apply + * @return the transformed instance + */ + public SubHyperplane applyTransform(final Transform transform) { + final Hyperplane tHyperplane = transform.apply(hyperplane); + final BSPTree tTree = + recurseTransform(remainingRegion.getTree(false), tHyperplane, transform); + return new SubHyperplane(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 + * @return a new tree + */ + private BSPTree recurseTransform(final BSPTree node, final Hyperplane transformed, + final Transform transform) { + if (node.getCut() == null) { + return new BSPTree(node.getAttribute()); + } + + Region.BoundaryAttribute attribute = + (Region.BoundaryAttribute) node.getAttribute(); + if (attribute != null) { + final SubHyperplane tPO = (attribute.getPlusOutside() == null) ? + null : + transform.apply(attribute.getPlusOutside(), + hyperplane, transformed); + final SubHyperplane tPI = (attribute.getPlusInside() == null) ? + null : + transform.apply(attribute.getPlusInside(), + hyperplane, transformed); + attribute = new Region.BoundaryAttribute(tPO, tPI); + } + + return new BSPTree(transform.apply(node.getCut(), + hyperplane, transformed), + recurseTransform(node.getPlus(), transformed, + transform), + recurseTransform(node.getMinus(), transformed, + transform), + attribute); + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/SubSpace.java b/src/main/java/org/apache/commons/math/geometry/partitioning/SubSpace.java new file mode 100644 index 000000000..148a24329 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/SubSpace.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math.geometry.partitioning; + + +/** This interface represents a sub-space of a space. + + *

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}.

+ + *

In the 3D euclidean space, hyperplanes are 2D planes, and the 1D + * sub-spaces are lines.

+ + * @see Hyperplane + * @version $Revision$ $Date$ + */ +public interface SubSpace { + + /** 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 toSubSpace(Point 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 toSpace(Point point); + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/Transform.java b/src/main/java/org/apache/commons/math/geometry/partitioning/Transform.java new file mode 100644 index 000000000..a55dab567 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/Transform.java @@ -0,0 +1,74 @@ +/* + * 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.math.geometry.partitioning; + + +/** This interface represents an inversible affine transform in a space. + *

Inversible affine transform include for example scalings, + * translations, rotations.

+ + *

Transforms are dimension-specific. The consistency rules between + * the three {@code apply} methods are the following ones for a + * transformed defined for dimension D:

+ *
    + *
  • + * the transform can be applied to a point in the + * D-dimension space using its {@link #apply(Point)} + * method + *
  • + *
  • + * the transform can be applied to a (D-1)-dimension + * hyperplane in the D-dimension space using its + * {@link #apply(Hyperplane)} method + *
  • + *
  • + * 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 + *
  • + *
+ + * @version $Revision$ $Date$ + */ +public interface Transform { + + /** Transform a point of a space. + * @param point point to transform + * @return a new object representing the transformed point + */ + Point apply(Point point); + + /** Transform an hyperplane of a space. + * @param hyperplane hyperplane to transform + * @return a new object representing the transformed hyperplane + */ + Hyperplane apply(Hyperplane 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 + * not been applied to it) + * @param transformed hyperplane in which the sub-hyperplane is + * defined (this is the transformed hyperplane, the transform + * has been applied to it) + * @return a new object representing the transformed sub-hyperplane + */ + SubHyperplane apply(SubHyperplane sub, Hyperplane original, Hyperplane transformed); + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/package.html b/src/main/java/org/apache/commons/math/geometry/partitioning/package.html new file mode 100644 index 000000000..7c7eadfa0 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/package.html @@ -0,0 +1,107 @@ + + + +This package provides classes to implement Binary Space Partition trees. + +

+{@link org.apache.commons.math.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). +

+ +

+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. +

+ +

+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 not 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.math.geometry.partitioning.Region Region} class is +devoted to this representation, it is build on top of the {@link +org.apache.commons.math.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). +

+ +

+Rather than simply associating the internal nodes with an hyperplane, +we consider sub-hyperplanes 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. +

+ +

+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 +O(log(n)) where n is the number of facets) +or if the first tree levels close to the root discriminate large parts +of the total space. +

+ +

+One of the main sources for the development of this package was Bruce +Naylor, John Amanatides and William Thibault paper Merging +BSP Trees Yields Polyhedral Set Operations 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 here. +

+ + + diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/AVLTree.java b/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/AVLTree.java new file mode 100644 index 000000000..e1e5dc4f7 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/AVLTree.java @@ -0,0 +1,631 @@ +/* + * 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.math.geometry.partitioning.utilities; + +/** This class implements AVL trees. + + *

The purpose of this class is to sort elements while allowing + * duplicate elements (i.e. such that {@code a.equals(b)} is + * true). The {@code SortedSet} interface does not allow this, so + * a specific class is needed. Null elements are not allowed.

+ + *

Since the {@code equals} method is not sufficient to + * differentiate elements, the {@link #delete delete} method is + * implemented using the equality operator.

+ + *

In order to clearly mark the methods provided here do not have + * the same semantics as the ones specified in the + * {@code SortedSet} interface, different names are used + * ({@code add} has been replaced by {@link #insert insert} and + * {@code remove} has been replaced by {@link #delete + * delete}).

+ + *

This class is based on the C implementation Georg Kraml has put + * in the public domain. Unfortunately, his page seems not + * to exist any more.

+ + * @param the type of the elements + + * @version $Revision$ $Date$ + */ +public class AVLTree> { + + /** Top level node. */ + private Node top; + + /** Build an empty tree. + */ + public AVLTree() { + top = null; + } + + /** Insert an element in the tree. + * @param element element to insert (silently ignored if null) + */ + public void insert(final T element) { + if (element != null) { + if (top == null) { + top = new Node(element, null); + } else { + top.insert(element); + } + } + } + + /** Delete an element from the tree. + *

The element is deleted only if there is a node {@code n} + * containing exactly the element instance specified, i.e. for which + * {@code n.getElement() == element}. This is purposely + * different from the specification of the + * {@code java.util.Set} {@code remove} method (in fact, + * this is the reason why a specific class has been developed).

+ * @param element element to delete (silently ignored if null) + * @return true if the element was deleted from the tree + */ + public boolean delete(final T element) { + if (element != null) { + for (Node node = getNotSmaller(element); node != null; node = node.getNext()) { + // loop over all elements neither smaller nor larger + // than the specified one + if (node.element == element) { + node.delete(); + return true; + } else if (node.element.compareTo(element) > 0) { + // all the remaining elements are known to be larger, + // the element is not in the tree + return false; + } + } + } + return false; + } + + /** Check if the tree is empty. + * @return true if the tree is empty + */ + public boolean isEmpty() { + return top == null; + } + + + /** Get the number of elements of the tree. + * @return number of elements contained in the tree + */ + public int size() { + return (top == null) ? 0 : top.size(); + } + + /** Get the node whose element is the smallest one in the tree. + * @return the tree node containing the smallest element in the tree + * or null if the tree is empty + * @see #getLargest + * @see #getNotSmaller + * @see #getNotLarger + * @see Node#getPrevious + * @see Node#getNext + */ + public Node getSmallest() { + return (top == null) ? null : top.getSmallest(); + } + + /** Get the node whose element is the largest one in the tree. + * @return the tree node containing the largest element in the tree + * or null if the tree is empty + * @see #getSmallest + * @see #getNotSmaller + * @see #getNotLarger + * @see Node#getPrevious + * @see Node#getNext + */ + public Node getLargest() { + return (top == null) ? null : top.getLargest(); + } + + /** Get the node whose element is not smaller than the reference object. + * @param reference reference object (may not be in the tree) + * @return the tree node containing the smallest element not smaller + * than the reference object or null if either the tree is empty or + * all its elements are smaller than the reference object + * @see #getSmallest + * @see #getLargest + * @see #getNotLarger + * @see Node#getPrevious + * @see Node#getNext + */ + public Node getNotSmaller(final T reference) { + Node candidate = null; + for (Node node = top; node != null;) { + if (node.element.compareTo(reference) < 0) { + if (node.right == null) { + return candidate; + } + node = node.right; + } else { + candidate = node; + if (node.left == null) { + return candidate; + } + node = node.left; + } + } + return null; + } + + /** Get the node whose element is not larger than the reference object. + * @param reference reference object (may not be in the tree) + * @return the tree node containing the largest element not larger + * than the reference object (in which case the node is guaranteed + * not to be empty) or null if either the tree is empty or all its + * elements are larger than the reference object + * @see #getSmallest + * @see #getLargest + * @see #getNotSmaller + * @see Node#getPrevious + * @see Node#getNext + */ + public Node getNotLarger(final T reference) { + Node candidate = null; + for (Node node = top; node != null;) { + if (node.element.compareTo(reference) > 0) { + if (node.left == null) { + return candidate; + } + node = node.left; + } else { + candidate = node; + if (node.right == null) { + return candidate; + } + node = node.right; + } + } + return null; + } + + /** Enum for tree skew factor. */ + private static enum Skew { + /** Code for left high trees. */ + LEFT_HIGH, + + /** Code for right high trees. */ + RIGHT_HIGH, + + /** Code for Skew.BALANCED trees. */ + BALANCED; + } + + /** This class implements AVL trees nodes. + *

AVL tree nodes implement all the logical structure of the + * tree. Nodes are created by the {@link AVLTree AVLTree} class.

+ *

The nodes are not independant from each other but must obey + * specific balancing constraints and the tree structure is + * rearranged as elements are inserted or deleted from the tree. The + * creation, modification and tree-related navigation methods have + * therefore restricted access. Only the order-related navigation, + * reading and delete methods are public.

+ * @see AVLTree + */ + public class Node { + + /** Element contained in the current node. */ + private T element; + + /** Left sub-tree. */ + private Node left; + + /** Right sub-tree. */ + private Node right; + + /** Parent tree. */ + private Node parent; + + /** Skew factor. */ + private Skew skew; + + /** Build a node for a specified element. + * @param element element + * @param parent parent node + */ + Node(final T element, final Node parent) { + this.element = element; + left = null; + right = null; + this.parent = parent; + skew = Skew.BALANCED; + } + + /** Get the contained element. + * @return element contained in the node + */ + public T getElement() { + return element; + } + + /** Get the number of elements of the tree rooted at this node. + * @return number of elements contained in the tree rooted at this node + */ + int size() { + return 1 + ((left == null) ? 0 : left.size()) + ((right == null) ? 0 : right.size()); + } + + /** Get the node whose element is the smallest one in the tree + * rooted at this node. + * @return the tree node containing the smallest element in the + * tree rooted at this node or null if the tree is empty + * @see #getLargest + */ + Node getSmallest() { + Node node = this; + while (node.left != null) { + node = node.left; + } + return node; + } + + /** Get the node whose element is the largest one in the tree + * rooted at this node. + * @return the tree node containing the largest element in the + * tree rooted at this node or null if the tree is empty + * @see #getSmallest + */ + Node getLargest() { + Node node = this; + while (node.right != null) { + node = node.right; + } + return node; + } + + /** Get the node containing the next smaller or equal element. + * @return node containing the next smaller or equal element or + * null if there is no smaller or equal element in the tree + * @see #getNext + */ + public Node getPrevious() { + + if (left != null) { + final Node node = left.getLargest(); + if (node != null) { + return node; + } + } + + for (Node node = this; node.parent != null; node = node.parent) { + if (node != node.parent.left) { + return node.parent; + } + } + + return null; + + } + + /** Get the node containing the next larger or equal element. + * @return node containing the next larger or equal element (in + * which case the node is guaranteed not to be empty) or null if + * there is no larger or equal element in the tree + * @see #getPrevious + */ + public Node getNext() { + + if (right != null) { + final Node node = right.getSmallest(); + if (node != null) { + return node; + } + } + + for (Node node = this; node.parent != null; node = node.parent) { + if (node != node.parent.right) { + return node.parent; + } + } + + return null; + + } + + /** Insert an element in a sub-tree. + * @param newElement element to insert + * @return true if the parent tree should be re-Skew.BALANCED + */ + boolean insert(final T newElement) { + if (newElement.compareTo(this.element) < 0) { + // the inserted element is smaller than the node + if (left == null) { + left = new Node(newElement, this); + return rebalanceLeftGrown(); + } + return left.insert(newElement) ? rebalanceLeftGrown() : false; + } + + // the inserted element is equal to or greater than the node + if (right == null) { + right = new Node(newElement, this); + return rebalanceRightGrown(); + } + return right.insert(newElement) ? rebalanceRightGrown() : false; + + } + + /** Delete the node from the tree. + */ + public void delete() { + if ((parent == null) && (left == null) && (right == null)) { + // this was the last node, the tree is now empty + element = null; + top = null; + } else { + + Node node; + Node child; + boolean leftShrunk; + if ((left == null) && (right == null)) { + node = this; + element = null; + leftShrunk = node == node.parent.left; + child = null; + } else { + node = (left != null) ? left.getLargest() : right.getSmallest(); + element = node.element; + leftShrunk = node == node.parent.left; + child = (node.left != null) ? node.left : node.right; + } + + node = node.parent; + if (leftShrunk) { + node.left = child; + } else { + node.right = child; + } + if (child != null) { + child.parent = node; + } + + while (leftShrunk ? node.rebalanceLeftShrunk() : node.rebalanceRightShrunk()) { + if (node.parent == null) { + return; + } + leftShrunk = node == node.parent.left; + node = node.parent; + } + + } + } + + /** Re-balance the instance as left sub-tree has grown. + * @return true if the parent tree should be reSkew.BALANCED too + */ + private boolean rebalanceLeftGrown() { + switch (skew) { + case LEFT_HIGH: + if (left.skew == Skew.LEFT_HIGH) { + rotateCW(); + skew = Skew.BALANCED; + right.skew = Skew.BALANCED; + } else { + final Skew s = left.right.skew; + left.rotateCCW(); + rotateCW(); + switch(s) { + case LEFT_HIGH: + left.skew = Skew.BALANCED; + right.skew = Skew.RIGHT_HIGH; + break; + case RIGHT_HIGH: + left.skew = Skew.LEFT_HIGH; + right.skew = Skew.BALANCED; + break; + default: + left.skew = Skew.BALANCED; + right.skew = Skew.BALANCED; + } + skew = Skew.BALANCED; + } + return false; + case RIGHT_HIGH: + skew = Skew.BALANCED; + return false; + default: + skew = Skew.LEFT_HIGH; + return true; + } + } + + /** Re-balance the instance as right sub-tree has grown. + * @return true if the parent tree should be reSkew.BALANCED too + */ + private boolean rebalanceRightGrown() { + switch (skew) { + case LEFT_HIGH: + skew = Skew.BALANCED; + return false; + case RIGHT_HIGH: + if (right.skew == Skew.RIGHT_HIGH) { + rotateCCW(); + skew = Skew.BALANCED; + left.skew = Skew.BALANCED; + } else { + final Skew s = right.left.skew; + right.rotateCW(); + rotateCCW(); + switch (s) { + case LEFT_HIGH: + left.skew = Skew.BALANCED; + right.skew = Skew.RIGHT_HIGH; + break; + case RIGHT_HIGH: + left.skew = Skew.LEFT_HIGH; + right.skew = Skew.BALANCED; + break; + default: + left.skew = Skew.BALANCED; + right.skew = Skew.BALANCED; + } + skew = Skew.BALANCED; + } + return false; + default: + skew = Skew.RIGHT_HIGH; + return true; + } + } + + /** Re-balance the instance as left sub-tree has shrunk. + * @return true if the parent tree should be reSkew.BALANCED too + */ + private boolean rebalanceLeftShrunk() { + switch (skew) { + case LEFT_HIGH: + skew = Skew.BALANCED; + return true; + case RIGHT_HIGH: + if (right.skew == Skew.RIGHT_HIGH) { + rotateCCW(); + skew = Skew.BALANCED; + left.skew = Skew.BALANCED; + return true; + } else if (right.skew == Skew.BALANCED) { + rotateCCW(); + skew = Skew.LEFT_HIGH; + left.skew = Skew.RIGHT_HIGH; + return false; + } else { + final Skew s = right.left.skew; + right.rotateCW(); + rotateCCW(); + switch (s) { + case LEFT_HIGH: + left.skew = Skew.BALANCED; + right.skew = Skew.RIGHT_HIGH; + break; + case RIGHT_HIGH: + left.skew = Skew.LEFT_HIGH; + right.skew = Skew.BALANCED; + break; + default: + left.skew = Skew.BALANCED; + right.skew = Skew.BALANCED; + } + skew = Skew.BALANCED; + return true; + } + default: + skew = Skew.RIGHT_HIGH; + return false; + } + } + + /** Re-balance the instance as right sub-tree has shrunk. + * @return true if the parent tree should be reSkew.BALANCED too + */ + private boolean rebalanceRightShrunk() { + switch (skew) { + case RIGHT_HIGH: + skew = Skew.BALANCED; + return true; + case LEFT_HIGH: + if (left.skew == Skew.LEFT_HIGH) { + rotateCW(); + skew = Skew.BALANCED; + right.skew = Skew.BALANCED; + return true; + } else if (left.skew == Skew.BALANCED) { + rotateCW(); + skew = Skew.RIGHT_HIGH; + right.skew = Skew.LEFT_HIGH; + return false; + } else { + final Skew s = left.right.skew; + left.rotateCCW(); + rotateCW(); + switch (s) { + case LEFT_HIGH: + left.skew = Skew.BALANCED; + right.skew = Skew.RIGHT_HIGH; + break; + case RIGHT_HIGH: + left.skew = Skew.LEFT_HIGH; + right.skew = Skew.BALANCED; + break; + default: + left.skew = Skew.BALANCED; + right.skew = Skew.BALANCED; + } + skew = Skew.BALANCED; + return true; + } + default: + skew = Skew.LEFT_HIGH; + return false; + } + } + + /** Perform a clockwise rotation rooted at the instance. + *

The skew factor are not updated by this method, they + * must be updated by the caller

+ */ + private void rotateCW() { + + final T tmpElt = element; + element = left.element; + left.element = tmpElt; + + final Node tmpNode = left; + left = tmpNode.left; + tmpNode.left = tmpNode.right; + tmpNode.right = right; + right = tmpNode; + + if (left != null) { + left.parent = this; + } + if (right.right != null) { + right.right.parent = right; + } + + } + + /** Perform a counter-clockwise rotation rooted at the instance. + *

The skew factor are not updated by this method, they + * must be updated by the caller

+ */ + private void rotateCCW() { + + final T tmpElt = element; + element = right.element; + right.element = tmpElt; + + final Node tmpNode = right; + right = tmpNode.right; + tmpNode.right = tmpNode.left; + tmpNode.left = left; + left = tmpNode; + + if (right != null) { + right.parent = this; + } + if (left.left != null) { + left.left.parent = left; + } + + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/OrderedTuple.java b/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/OrderedTuple.java new file mode 100644 index 000000000..71bfec062 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/OrderedTuple.java @@ -0,0 +1,417 @@ +/* + * 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.math.geometry.partitioning.utilities; + +import java.util.Arrays; + +import org.apache.commons.math.util.FastMath; + +/** This class implements an ordering operation for T-uples. + + *

Ordering is done by encoding all components of the T-uple into a + * single scalar value and using this value as the sorting + * key. Encoding is performed using the method invented by Georg + * Cantor in 1877 when he proved it was possible to establish a + * bijection between a line and a plane. The binary representations of + * the components of the T-uple are mixed together to form a single + * scalar. This means that the 2k bit of component 0 is + * followed by the 2k bit of component 1, then by the + * 2k bit of component 2 up to the 2k bit of + * component {@code t}, which is followed by the 2k-1 + * bit of component 0, followed by the 2k-1 bit of + * component 1 ... The binary representations are extended as needed + * to handle numbers with different scales and a suitable + * 2p offset is added to the components in order to avoid + * negative numbers (this offset is adjusted as needed during the + * comparison operations).

+ + *

The more interesting property of the encoding method for our + * purpose is that it allows to select all the points that are in a + * given range. This is depicted in dimension 2 by the following + * picure:

+ + * + + *

This picture shows a set of 100000 random 2-D pairs having their + * first component between -50 and +150 and their second component + * between -350 and +50. We wanted to extract all pairs having their + * first component between +30 and +70 and their second component + * between -120 and -30. We built the lower left point at coordinates + * (30, -120) and the upper right point at coordinates (70, -30). All + * points smaller than the lower left point are drawn in red and all + * points larger than the upper right point are drawn in blue. The + * green points are between the two limits. This picture shows that + * all the desired points are selected, along with spurious points. In + * this case, we get 15790 points, 4420 of which really belonging to + * the desired rectangle. It is possible to extract very small + * subsets. As an example extracting from the same 100000 points set + * the points having their first component between +30 and +31 and + * their second component between -91 and -90, we get a subset of 11 + * points, 2 of which really belonging to the desired rectangle.

+ + *

the previous selection technique can be applied in all + * dimensions, still using two points to define the interval. The + * first point will have all its components set to their lower bounds + * while the second point will have all its components set to their + * upper bounds.

+ + *

T-uples with negative infinite or positive infinite components + * are sorted logically.

+ + *

Since the specification of the {@code Comparator} interface + * allows only {@code ClassCastException} errors, some arbitrary + * choices have been made to handle specific cases. The rationale for + * these choices is to keep regular and consistent T-uples + * together.

+ *
    + *
  • instances with different dimensions are sorted according to + * their dimension regardless of their components values
  • + *
  • instances with {@code Double.NaN} components are sorted + * after all other ones (even after instances with positive infinite + * components
  • + *
  • instances with both positive and negative infinite components + * are considered as if they had {@code Double.NaN} + * components
  • + *
+ + * @version $Revision$ $Date$ + */ +public class OrderedTuple implements Comparable { + + /** Sign bit mask. */ + private static final long SIGN_MASK = 0x8000000000000000L; + + /** Exponent bits mask. */ + private static final long EXPONENT_MASK = 0x7ff0000000000000L; + + /** Mantissa bits mask. */ + private static final long MANTISSA_MASK = 0x000fffffffffffffL; + + /** Implicit MSB for normalized numbers. */ + private static final long IMPLICIT_ONE = 0x0010000000000000L; + + /** Double components of the T-uple. */ + private double[] components; + + /** Offset scale. */ + private int offset; + + /** Least Significant Bit scale. */ + private int lsb; + + /** Ordering encoding of the double components. */ + private long[] encoding; + + /** Positive infinity marker. */ + private boolean posInf; + + /** Negative infinity marker. */ + private boolean negInf; + + /** Not A Number marker. */ + private boolean nan; + + /** Build an ordered T-uple from its components. + * @param components double components of the T-uple + */ + public OrderedTuple(final double ... components) { + this.components = components.clone(); + int msb = Integer.MIN_VALUE; + lsb = Integer.MAX_VALUE; + posInf = false; + negInf = false; + nan = false; + for (int i = 0; i < components.length; ++i) { + if (Double.isInfinite(components[i])) { + if (components[i] < 0) { + negInf = true; + } else { + posInf = true; + } + } else if (Double.isNaN(components[i])) { + nan = true; + } else { + final long b = Double.doubleToLongBits(components[i]); + final long m = mantissa(b); + if (m != 0) { + final int e = exponent(b); + msb = FastMath.max(msb, e + computeMSB(m)); + lsb = FastMath.min(lsb, e + computeLSB(m)); + } + } + } + + if (posInf && negInf) { + // instance cannot be sorted logically + posInf = false; + negInf = false; + nan = true; + } + + if (lsb <= msb) { + // encode the T-upple with the specified offset + encode(msb + 16); + } else { + encoding = new long[] { + 0x0L + }; + } + + } + + /** Encode the T-uple with a given offset. + * @param minOffset minimal scale of the offset to add to all + * components (must be greater than the MSBs of all components) + */ + private void encode(final int minOffset) { + + // choose an offset with some margins + offset = minOffset + 31; + offset -= offset % 32; + + if ((encoding != null) && (encoding.length == 1) && (encoding[0] == 0x0L)) { + // the components are all zeroes + return; + } + + // allocate an integer array to encode the components (we use only + // 63 bits per element because there is no unsigned long in Java) + final int neededBits = offset + 1 - lsb; + final int neededLongs = (neededBits + 62) / 63; + encoding = new long[components.length * neededLongs]; + + // mix the bits from all components + int eIndex = 0; + int shift = 62; + long word = 0x0L; + for (int k = offset; eIndex < encoding.length; --k) { + for (int vIndex = 0; vIndex < components.length; ++vIndex) { + if (getBit(vIndex, k) != 0) { + word |= 0x1L << shift; + } + if (shift-- == 0) { + encoding[eIndex++] = word; + word = 0x0L; + shift = 62; + } + } + } + + } + + /** Compares this ordered T-uple with the specified object. + + *

The ordering method is detailed in the general description of + * the class. Its main property is to be consistent with distance: + * geometrically close T-uples stay close to each other when stored + * in a sorted collection using this comparison method.

+ + *

T-uples with negative infinite, positive infinite are sorted + * logically.

+ + *

Some arbitrary choices have been made to handle specific + * cases. The rationale for these choices is to keep + * normal and consistent T-uples together.

+ *
    + *
  • instances with different dimensions are sorted according to + * their dimension regardless of their components values
  • + *
  • instances with {@code Double.NaN} components are sorted + * after all other ones (evan after instances with positive infinite + * components
  • + *
  • instances with both positive and negative infinite components + * are considered as if they had {@code Double.NaN} + * components
  • + *
+ + * @param ot T-uple to compare instance with + * @return a negative integer if the instance is less than the + * object, zero if they are equal, or a positive integer if the + * instance is greater than the object + + */ + public int compareTo(final OrderedTuple ot) { + if (components.length == ot.components.length) { + if (nan) { + return +1; + } else if (ot.nan) { + return -1; + } else if (negInf || ot.posInf) { + return -1; + } else if (posInf || ot.negInf) { + return +1; + } else { + + if (offset < ot.offset) { + encode(ot.offset); + } else if (offset > ot.offset) { + ot.encode(offset); + } + + final int limit = FastMath.min(encoding.length, ot.encoding.length); + for (int i = 0; i < limit; ++i) { + if (encoding[i] < ot.encoding[i]) { + return -1; + } else if (encoding[i] > ot.encoding[i]) { + return +1; + } + } + + if (encoding.length < ot.encoding.length) { + return -1; + } else if (encoding.length > ot.encoding.length) { + return +1; + } else { + return 0; + } + + } + } + + return components.length - ot.components.length; + + } + + /** {@inheritDoc} */ + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } else if (other instanceof OrderedTuple) { + return compareTo((OrderedTuple) other) == 0; + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Arrays.hashCode(components) ^ + ((Integer) offset).hashCode() ^ + ((Integer) lsb).hashCode() ^ + ((Boolean) posInf).hashCode() ^ + ((Boolean) negInf).hashCode() ^ + ((Boolean) nan).hashCode(); + } + + /** Get the components array. + * @return array containing the T-uple components + */ + public double[] getComponents() { + return components.clone(); + } + + /** Extract the sign from the bits of a double. + * @param bits binary representation of the double + * @return sign bit (zero if positive, non zero if negative) + */ + private static long sign(final long bits) { + return bits & SIGN_MASK; + } + + /** Extract the exponent from the bits of a double. + * @param bits binary representation of the double + * @return exponent + */ + private static int exponent(final long bits) { + return ((int) ((bits & EXPONENT_MASK) >> 52)) - 1075; + } + + /** Extract the mantissa from the bits of a double. + * @param bits binary representation of the double + * @return mantissa + */ + private static long mantissa(final long bits) { + return ((bits & EXPONENT_MASK) == 0) ? + ((bits & MANTISSA_MASK) << 1) : // subnormal number + (IMPLICIT_ONE | (bits & MANTISSA_MASK)); // normal number + } + + /** Compute the most significant bit of a long. + * @param l long from which the most significant bit is requested + * @return scale of the most significant bit of {@code l}, + * or 0 if {@code l} is zero + * @see #computeLSB + */ + private static int computeMSB(final long l) { + + long ll = l; + long mask = 0xffffffffL; + int scale = 32; + int msb = 0; + + while (scale != 0) { + if ((ll & mask) != ll) { + msb |= scale; + ll = ll >> scale; + } + scale = scale >> 1; + mask = mask >> scale; + } + + return msb; + + } + + /** Compute the least significant bit of a long. + * @param l long from which the least significant bit is requested + * @return scale of the least significant bit of {@code l}, + * or 63 if {@code l} is zero + * @see #computeMSB + */ + private static int computeLSB(final long l) { + + long ll = l; + long mask = 0xffffffff00000000L; + int scale = 32; + int lsb = 0; + + while (scale != 0) { + if ((ll & mask) == ll) { + lsb |= scale; + ll = ll >> scale; + } + scale = scale >> 1; + mask = mask >> scale; + } + + return lsb; + + } + + /** Get a bit from the mantissa of a double. + * @param i index of the component + * @param k scale of the requested bit + * @return the specified bit (either 0 or 1), after the offset has + * been added to the double + */ + private int getBit(final int i, final int k) { + final long bits = Double.doubleToLongBits(components[i]); + final int e = exponent(bits); + if ((k < e) || (k > offset)) { + return 0; + } else if (k == offset) { + return (sign(bits) == 0L) ? 1 : 0; + } else if (k > (e + 52)) { + return (sign(bits) == 0L) ? 0 : 1; + } else { + final long m = (sign(bits) == 0L) ? mantissa(bits) : -mantissa(bits); + return (int) ((m >> (k - e)) & 0x1L); + } + } + +} diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/doc-files/OrderedTuple.png b/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/doc-files/OrderedTuple.png new file mode 100644 index 0000000000000000000000000000000000000000..4eca23302590d126d829cdda6f0a3349a1dc727c GIT binary patch literal 28882 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⋘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 literal 0 HcmV?d00001 diff --git a/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/package.html b/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/package.html new file mode 100644 index 000000000..7934150d4 --- /dev/null +++ b/src/main/java/org/apache/commons/math/geometry/partitioning/utilities/package.html @@ -0,0 +1,24 @@ + + + + +

+This package provides multidimensional ordering features for partitioning. +

+ + diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml index 7cc1ffd7a..bd2043c8b 100644 --- a/src/site/xdoc/changes.xml +++ b/src/site/xdoc/changes.xml @@ -52,6 +52,16 @@ The type attribute can be add,update,fix,remove. If the output is not quite correct, check for invisible trailing spaces! --> + + A complete generic implementation of Binary Space Partitioning Trees (BSP trees) + has been added. A few specializations of this implementation are also provided + for 1D, 2D and 3D Euclidean geometry. This allows support for arbitrary + intervals sets (1D), polygons sets (2D) and polyhedrons sets (3D) with all + sets operations (union, intersection, symmetric difference, difference, complement), + with predicates (point inside/outside/on boundary, emptiness, other region contained), + with geometrical computation (barycenter, size, boundary size) and with conversions + from and to boundary representation. + Avoid some array copying in add and subtract ArrayFieldVector. diff --git a/src/site/xdoc/userguide/geometry.xml b/src/site/xdoc/userguide/geometry.xml index ee8e7abfa..cf0c6038e 100644 --- a/src/site/xdoc/userguide/geometry.xml +++ b/src/site/xdoc/userguide/geometry.xml @@ -30,12 +30,41 @@

The geometry package provides classes useful for many physical simulations - in the real 3D space, namely vectors and rotations. + in Euclidean spaces, like vectors and rotations in 3D, as well as a general + implentation of Binary Space Partitioning Trees (BSP trees).

- + +

+ + Interval and + IntervalsSet represent one dimensional regions. All classical set operations are available + for intervals sets: union, intersection, symmetric difference (exclusive or), difference, complement, + as well as region predicates (point inside/outside/on boundary, emptiness, other region contained). + It is also possible to compute geometrical properties like size, barycenter or boundary size. + Intervals sets can be built by constructive geometry (union, intersection ...) or from a boundary + representation. +

+

+ + PolygonsSet represent two dimensional regions. All classical set operations are available + for polygons sets: union, intersection, symmetric difference (exclusive or), difference, complement, + as well as region predicates (point inside/outside/on boundary, emptiness, other region contained). + It is also possible to compute geometrical properties like size, barycenter or boundary size and + to extract the vertices. Polygons sets can be built by constructive geometry (union, intersection ...) + or from a boundary representation. +

+

+ + PolyhedronsSet represent three dimensional regions. All classical set operations are available + for polyhedrons sets: union, intersection, symmetric difference (exclusive or), difference, complement, + as well as region predicates (point inside/outside/on boundary, emptiness, other region contained). + It is also possible to compute geometrical properties like size, barycenter or boundary size and + to extract the vertices. Polyhedrons sets can be built by constructive geometry (union, intersection ...) + or from a boundary representation. +

- + Vector3D provides a simple vector type. One important feature is that instances of this class are guaranteed to be immutable, this greatly simplifies modelling dynamical systems @@ -57,14 +86,12 @@ is of course also implemented.

- + Vector3DFormat is a specialized format for formatting output or parsing input with text representation of 3D vectors.

- -

- + Rotation represents 3D rotations. Rotation instances are also immutable objects, as Vector3D instances.

@@ -136,6 +163,41 @@ applyInverseTo(Rotation).

+ +

+ + 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. +

+ +

+ 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. +

+ +

+ Another example would be to represent Voronoi tesselations, the + attribute of each cell holding the defining point of the cell. +

+ +

+ 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. +

+
diff --git a/src/site/xdoc/userguide/overview.xml b/src/site/xdoc/userguide/overview.xml index 4d8be4b03..6d57b2b0e 100644 --- a/src/site/xdoc/userguide/overview.xml +++ b/src/site/xdoc/userguide/overview.xml @@ -83,7 +83,7 @@
  • org.apache.commons.math.distribution - probability distributions
  • org.apache.commons.math.fraction - rational numbers
  • org.apache.commons.math.transform - transform methods (Fast Fourier)
  • -
  • org.apache.commons.math.geometry - 3D geometry (vectors and rotations)
  • +
  • org.apache.commons.math.geometry - geometry (Euclidean spaces and Binary Space Partitioning)
  • org.apache.commons.math.optimization - function maximization or minimization
  • org.apache.commons.math.ode - Ordinary Differential Equations integration
  • org.apache.commons.math.genetics - Genetic Algorithms
  • diff --git a/src/test/java/org/apache/commons/math/geometry/euclidean/oneD/IntervalsSetTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/oneD/IntervalsSetTest.java new file mode 100644 index 000000000..a68e5944d --- /dev/null +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/oneD/IntervalsSetTest.java @@ -0,0 +1,96 @@ +/* + * 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.math.geometry.euclidean.oneD; + +import java.util.List; + +import org.apache.commons.math.geometry.euclidean.oneD.Interval; +import org.apache.commons.math.geometry.euclidean.oneD.IntervalsSet; +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.util.FastMath; +import org.junit.Assert; +import org.junit.Test; + +public class IntervalsSetTest { + + @Test + public void testInterval() { + IntervalsSet set = new IntervalsSet(2.3, 5.7); + Assert.assertEquals(3.4, set.getSize(), 1.0e-10); + Assert.assertEquals(4.0, ((Point1D) set.getBarycenter()).getAbscissa(), 1.0e-10); + Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Point1D(2.3))); + Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Point1D(5.7))); + Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Point1D(1.2))); + Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Point1D(8.7))); + Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(new Point1D(3.0))); + Assert.assertEquals(2.3, set.getInf(), 1.0e-10); + Assert.assertEquals(5.7, set.getSup(), 1.0e-10); + } + + @Test + public void testInfinite() { + IntervalsSet set = new IntervalsSet(9.0, Double.POSITIVE_INFINITY); + Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Point1D(9.0))); + Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Point1D(8.4))); + for (double e = 1.0; e <= 6.0; e += 1.0) { + Assert.assertEquals(Region.Location.INSIDE, + set.checkPoint(new Point1D(FastMath.pow(10.0, e)))); + } + Assert.assertTrue(Double.isInfinite(set.getSize())); + Assert.assertEquals(9.0, set.getInf(), 1.0e-10); + Assert.assertTrue(Double.isInfinite(set.getSup())); + + set = (IntervalsSet) set.getComplement(); + Assert.assertEquals(9.0, set.getSup(), 1.0e-10); + Assert.assertTrue(Double.isInfinite(set.getInf())); + + } + + @Test + public void testMultiple() { + IntervalsSet set = (IntervalsSet) + Region.intersection(Region.union(Region.difference(new IntervalsSet(1.0, 6.0), + new IntervalsSet(3.0, 5.0)), + new IntervalsSet(9.0, Double.POSITIVE_INFINITY)), + new IntervalsSet(Double.NEGATIVE_INFINITY, 11.0)); + Assert.assertEquals(5.0, set.getSize(), 1.0e-10); + Assert.assertEquals(5.9, ((Point1D) set.getBarycenter()).getAbscissa(), 1.0e-10); + Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Point1D(0.0))); + Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Point1D(4.0))); + Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Point1D(8.0))); + Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Point1D(12.0))); + Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(new Point1D(1.2))); + Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(new Point1D(5.9))); + Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(new Point1D(9.01))); + Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Point1D(5.0))); + Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Point1D(11.0))); + Assert.assertEquals( 1.0, set.getInf(), 1.0e-10); + Assert.assertEquals(11.0, set.getSup(), 1.0e-10); + + List list = set.asList(); + Assert.assertEquals(3, list.size()); + Assert.assertEquals( 1.0, list.get(0).getLower(), 1.0e-10); + Assert.assertEquals( 3.0, list.get(0).getUpper(), 1.0e-10); + Assert.assertEquals( 5.0, list.get(1).getLower(), 1.0e-10); + Assert.assertEquals( 6.0, list.get(1).getUpper(), 1.0e-10); + Assert.assertEquals( 9.0, list.get(2).getLower(), 1.0e-10); + Assert.assertEquals(11.0, list.get(2).getUpper(), 1.0e-10); + + } + +} diff --git a/src/test/java/org/apache/commons/math/geometry/FrenchVector3DFormatTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/FrenchVector3DFormatTest.java similarity index 94% rename from src/test/java/org/apache/commons/math/geometry/FrenchVector3DFormatTest.java rename to src/test/java/org/apache/commons/math/geometry/euclidean/threeD/FrenchVector3DFormatTest.java index c70b1d9cc..4ee5033f0 100644 --- a/src/test/java/org/apache/commons/math/geometry/FrenchVector3DFormatTest.java +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/FrenchVector3DFormatTest.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import java.util.Locale; diff --git a/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/LineTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/LineTest.java new file mode 100644 index 000000000..2add00d03 --- /dev/null +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/LineTest.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.math.geometry.euclidean.threeD; + +import org.apache.commons.math.geometry.euclidean.threeD.Line; +import org.apache.commons.math.geometry.euclidean.threeD.Vector3D; +import org.apache.commons.math.util.FastMath; +import org.junit.Assert; +import org.junit.Test; + +public class LineTest { + + @Test + public void testContains() { + Vector3D p1 = new Vector3D(0, 0, 1); + Line l = new Line(p1, new Vector3D(0, 0, 1)); + Assert.assertTrue(l.contains(p1)); + Assert.assertTrue(l.contains(new Vector3D(1.0, p1, 0.3, l.getDirection()))); + Vector3D u = l.getDirection().orthogonal(); + Vector3D v = Vector3D.crossProduct(l.getDirection(), u); + for (double alpha = 0; alpha < 2 * FastMath.PI; alpha += 0.3) { + Assert.assertTrue(! l.contains(p1.add(new Vector3D(FastMath.cos(alpha), u, + FastMath.sin(alpha), v)))); + } + } + + @Test + public void testSimilar() { + Vector3D p1 = new Vector3D (1.2, 3.4, -5.8); + Vector3D p2 = new Vector3D (3.4, -5.8, 1.2); + Line lA = new Line(p1, p2.subtract(p1)); + Line lB = new Line(p2, p1.subtract(p2)); + Assert.assertTrue(lA.isSimilarTo(lB)); + Assert.assertTrue(! lA.isSimilarTo(new Line(p1, lA.getDirection().orthogonal()))); + } + + @Test + public void testPointDistance() { + Line l = new Line(new Vector3D(0, 1, 1), new Vector3D(0, 1, 1)); + Assert.assertEquals(FastMath.sqrt(3.0 / 2.0), l.distance(new Vector3D(1, 0, 1)), 1.0e-10); + Assert.assertEquals(0, l.distance(new Vector3D(0, -4, -4)), 1.0e-10); + } + + @Test + public void testLineDistance() { + Line l = new Line(new Vector3D(0, 1, 1), new Vector3D(0, 1, 1)); + Assert.assertEquals(1.0, + l.distance(new Line(new Vector3D(1, 0, 1), Vector3D.PLUS_K)), + 1.0e-10); + Assert.assertEquals(0.5, + l.distance(new Line(new Vector3D(-0.5, 0, 0), new Vector3D(0, -1, -1))), + 1.0e-10); + Assert.assertEquals(0.0, + l.distance(l), + 1.0e-10); + Assert.assertEquals(0.0, + l.distance(new Line(new Vector3D(0, -4, -4), new Vector3D(0, -1, -1))), + 1.0e-10); + } + +} diff --git a/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/PlaneTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/PlaneTest.java new file mode 100644 index 000000000..b1339d5c2 --- /dev/null +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/PlaneTest.java @@ -0,0 +1,168 @@ +/* + * 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.math.geometry.euclidean.threeD; + +import org.apache.commons.math.geometry.euclidean.threeD.Line; +import org.apache.commons.math.geometry.euclidean.threeD.Plane; +import org.apache.commons.math.geometry.euclidean.threeD.Point3D; +import org.apache.commons.math.geometry.euclidean.threeD.Rotation; +import org.apache.commons.math.geometry.euclidean.threeD.Vector3D; +import org.junit.Assert; +import org.junit.Test; + +public class PlaneTest { + + @Test + public void testContains() { + Plane p = new Plane(new Vector3D(0, 0, 1), new Vector3D(0, 0, 1)); + Assert.assertTrue(p.contains(new Point3D(0, 0, 1))); + Assert.assertTrue(p.contains(new Point3D(17, -32, 1))); + Assert.assertTrue(! p.contains(new Point3D(17, -32, 1.001))); + } + + @Test + public void testOffset() { + Vector3D p1 = new Vector3D(1, 1, 1); + Plane p = new Plane(p1, new Vector3D(0.2, 0, 0)); + Assert.assertEquals(-5.0, p.getOffset(new Point3D(-4, 0, 0)), 1.0e-10); + Assert.assertEquals(+5.0, p.getOffset(new Point3D(6, 10, -12)), 1.0e-10); + Assert.assertEquals(0.3, + p.getOffset(new Point3D(1.0, p1, 0.3, p.getNormal())), + 1.0e-10); + Assert.assertEquals(-0.3, + p.getOffset(new Point3D(1.0, p1, -0.3, p.getNormal())), + 1.0e-10); + } + + @Test + public void testPoint() { + Plane p = new Plane(new Vector3D(2, -3, 1), new Vector3D(1, 4, 9)); + Assert.assertTrue(p.contains(p.getOrigin())); + } + + @Test + public void testThreePoints() { + Point3D p1 = new Point3D(1.2, 3.4, -5.8); + Point3D p2 = new Point3D(3.4, -5.8, 1.2); + Point3D p3 = new Point3D(-2.0, 4.3, 0.7); + Plane p = new Plane(p1, p2, p3); + Assert.assertTrue(p.contains(p1)); + Assert.assertTrue(p.contains(p2)); + Assert.assertTrue(p.contains(p3)); + } + + @Test + public void testRotate() { + Point3D p1 = new Point3D(1.2, 3.4, -5.8); + Point3D p2 = new Point3D(3.4, -5.8, 1.2); + Point3D p3 = new Point3D(-2.0, 4.3, 0.7); + Plane p = new Plane(p1, p2, p3); + Vector3D oldNormal = p.getNormal(); + + p = p.rotate(p2, new Rotation(p2.subtract(p1), 1.7)); + Assert.assertTrue(p.contains(p1)); + Assert.assertTrue(p.contains(p2)); + Assert.assertTrue(! p.contains(p3)); + + p = p.rotate(p2, new Rotation(oldNormal, 0.1)); + Assert.assertTrue(! p.contains(p1)); + Assert.assertTrue(p.contains(p2)); + Assert.assertTrue(! p.contains(p3)); + + p = p.rotate(p1, new Rotation(oldNormal, 0.1)); + Assert.assertTrue(! p.contains(p1)); + Assert.assertTrue(! p.contains(p2)); + Assert.assertTrue(! p.contains(p3)); + + } + + @Test + public void testTranslate() { + Point3D p1 = new Point3D(1.2, 3.4, -5.8); + Point3D p2 = new Point3D(3.4, -5.8, 1.2); + Point3D p3 = new Point3D(-2.0, 4.3, 0.7); + Plane p = new Plane(p1, p2, p3); + + p = p.translate(new Vector3D(2.0, p.getU(), -1.5, p.getV())); + Assert.assertTrue(p.contains(p1)); + Assert.assertTrue(p.contains(p2)); + Assert.assertTrue(p.contains(p3)); + + p = p.translate(new Vector3D(-1.2, p.getNormal())); + Assert.assertTrue(! p.contains(p1)); + Assert.assertTrue(! p.contains(p2)); + Assert.assertTrue(! p.contains(p3)); + + p = p.translate(new Vector3D(+1.2, p.getNormal())); + Assert.assertTrue(p.contains(p1)); + Assert.assertTrue(p.contains(p2)); + Assert.assertTrue(p.contains(p3)); + + } + + @Test + public void testIntersection() { + Plane p = new Plane(new Vector3D(1, 2, 3), new Vector3D(-4, 1, -5)); + Line l = new Line(new Vector3D(0.2, -3.5, 0.7), new Vector3D(1, 1, -1)); + Point3D point = p.intersection(l); + Assert.assertTrue(p.contains(point)); + Assert.assertTrue(l.contains(point)); + Assert.assertNull(p.intersection(new Line(new Vector3D(10, 10, 10), + p.getNormal().orthogonal()))); + } + + @Test + public void testIntersection2() { + Vector3D p1 = new Vector3D (1.2, 3.4, -5.8); + Vector3D p2 = new Vector3D (3.4, -5.8, 1.2); + Plane pA = new Plane(p1, p2, new Vector3D (-2.0, 4.3, 0.7)); + Plane pB = new Plane(p1, new Vector3D (11.4, -3.8, 5.1), p2); + Line l = (Line) pA.intersection(pB); + Assert.assertTrue(l.contains(p1)); + Assert.assertTrue(l.contains(p2)); + Assert.assertNull(pA.intersection(pA)); + } + + @Test + public void testIntersection3() { + Vector3D reference = new Vector3D (1.2, 3.4, -5.8); + Plane p1 = new Plane(reference, new Vector3D(1, 3, 3)); + Plane p2 = new Plane(reference, new Vector3D(-2, 4, 0)); + Plane p3 = new Plane(reference, new Vector3D(7, 0, -4)); + Vector3D p = Plane.intersection(p1, p2, p3); + Assert.assertEquals(reference.getX(), p.getX(), 1.0e-10); + Assert.assertEquals(reference.getY(), p.getY(), 1.0e-10); + Assert.assertEquals(reference.getZ(), p.getZ(), 1.0e-10); + } + + @Test + public void testSimilar() { + Vector3D p1 = new Vector3D (1.2, 3.4, -5.8); + Vector3D p2 = new Vector3D (3.4, -5.8, 1.2); + Vector3D p3 = new Vector3D (-2.0, 4.3, 0.7); + Plane pA = new Plane(p1, p2, p3); + Plane pB = new Plane(p1, new Vector3D (11.4, -3.8, 5.1), p2); + Assert.assertTrue(! pA.isSimilarTo(pB)); + Assert.assertTrue(pA.isSimilarTo(pA)); + Assert.assertTrue(pA.isSimilarTo(new Plane(p1, p3, p2))); + Vector3D shift = new Vector3D(0.3, pA.getNormal()); + Assert.assertTrue(! pA.isSimilarTo(new Plane(p1.add(shift), + p3.add(shift), + p2.add(shift)))); + } + +} diff --git a/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/PolyhedronsSetTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/PolyhedronsSetTest.java new file mode 100644 index 000000000..fa284189e --- /dev/null +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/PolyhedronsSetTest.java @@ -0,0 +1,243 @@ +/* + * 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.math.geometry.euclidean.threeD; + +import java.util.Arrays; + +import org.apache.commons.math.geometry.euclidean.threeD.Plane; +import org.apache.commons.math.geometry.euclidean.threeD.Point3D; +import org.apache.commons.math.geometry.euclidean.threeD.PolyhedronsSet; +import org.apache.commons.math.geometry.euclidean.threeD.Rotation; +import org.apache.commons.math.geometry.euclidean.threeD.Vector3D; +import org.apache.commons.math.geometry.euclidean.twoD.Point2D; +import org.apache.commons.math.geometry.euclidean.twoD.PolygonsSet; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.BSPTreeVisitor; +import org.apache.commons.math.geometry.partitioning.Hyperplane; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.util.FastMath; +import org.junit.Assert; +import org.junit.Test; + +public class PolyhedronsSetTest { + + @Test + public void testBox() { + PolyhedronsSet tree = new PolyhedronsSet(0, 1, 0, 1, 0, 1); + Assert.assertEquals(1.0, tree.getSize(), 1.0e-10); + Assert.assertEquals(6.0, tree.getBoundarySize(), 1.0e-10); + Vector3D barycenter = (Vector3D) tree.getBarycenter(); + Assert.assertEquals(0.5, barycenter.getX(), 1.0e-10); + Assert.assertEquals(0.5, barycenter.getY(), 1.0e-10); + Assert.assertEquals(0.5, barycenter.getZ(), 1.0e-10); + for (double x = -0.25; x < 1.25; x += 0.1) { + boolean xOK = (x >= 0.0) && (x <= 1.0); + for (double y = -0.25; y < 1.25; y += 0.1) { + boolean yOK = (y >= 0.0) && (y <= 1.0); + for (double z = -0.25; z < 1.25; z += 0.1) { + boolean zOK = (z >= 0.0) && (z <= 1.0); + Region.Location expected = + (xOK && yOK && zOK) ? Region.Location.INSIDE : Region.Location.OUTSIDE; + Assert.assertEquals(expected, tree.checkPoint(new Point3D(x, y, z))); + } + } + } + checkPoints(Region.Location.BOUNDARY, tree, new Point3D[] { + new Point3D(0.0, 0.5, 0.5), + new Point3D(1.0, 0.5, 0.5), + new Point3D(0.5, 0.0, 0.5), + new Point3D(0.5, 1.0, 0.5), + new Point3D(0.5, 0.5, 0.0), + new Point3D(0.5, 0.5, 1.0) + }); + checkPoints(Region.Location.OUTSIDE, tree, new Point3D[] { + new Point3D(0.0, 1.2, 1.2), + new Point3D(1.0, 1.2, 1.2), + new Point3D(1.2, 0.0, 1.2), + new Point3D(1.2, 1.0, 1.2), + new Point3D(1.2, 1.2, 0.0), + new Point3D(1.2, 1.2, 1.0) + }); + } + + @Test + public void testTetrahedron() { + Point3D vertex1 = new Point3D(1, 2, 3); + Point3D vertex2 = new Point3D(2, 2, 4); + Point3D vertex3 = new Point3D(2, 3, 3); + Point3D vertex4 = new Point3D(1, 3, 4); + PolyhedronsSet tree = + (PolyhedronsSet) Region.buildConvex(Arrays.asList(new Hyperplane[] { + new Plane(vertex3, vertex2, vertex1), + new Plane(vertex2, vertex3, vertex4), + new Plane(vertex4, vertex3, vertex1), + new Plane(vertex1, vertex2, vertex4) + })); + Assert.assertEquals(1.0 / 3.0, tree.getSize(), 1.0e-10); + Assert.assertEquals(2.0 * FastMath.sqrt(3.0), tree.getBoundarySize(), 1.0e-10); + Vector3D barycenter = (Vector3D) tree.getBarycenter(); + Assert.assertEquals(1.5, barycenter.getX(), 1.0e-10); + Assert.assertEquals(2.5, barycenter.getY(), 1.0e-10); + Assert.assertEquals(3.5, barycenter.getZ(), 1.0e-10); + double third = 1.0 / 3.0; + checkPoints(Region.Location.BOUNDARY, tree, new Point3D[] { + vertex1, vertex2, vertex3, vertex4, + new Point3D(third, vertex1, third, vertex2, third, vertex3), + new Point3D(third, vertex2, third, vertex3, third, vertex4), + new Point3D(third, vertex3, third, vertex4, third, vertex1), + new Point3D(third, vertex4, third, vertex1, third, vertex2) + }); + checkPoints(Region.Location.OUTSIDE, tree, new Point3D[] { + new Point3D(1, 2, 4), + new Point3D(2, 2, 3), + new Point3D(2, 3, 4), + new Point3D(1, 3, 3) + }); + } + + @Test + public void testIsometry() { + Vector3D vertex1 = new Vector3D(1.1, 2.2, 3.3); + Vector3D vertex2 = new Vector3D(2.0, 2.4, 4.2); + Vector3D vertex3 = new Vector3D(2.8, 3.3, 3.7); + Vector3D vertex4 = new Vector3D(1.0, 3.6, 4.5); + PolyhedronsSet tree = + (PolyhedronsSet) Region.buildConvex(Arrays.asList(new Hyperplane[] { + new Plane(vertex3, vertex2, vertex1), + new Plane(vertex2, vertex3, vertex4), + new Plane(vertex4, vertex3, vertex1), + new Plane(vertex1, vertex2, vertex4) + })); + Vector3D barycenter = (Vector3D) tree.getBarycenter(); + Vector3D s = new Vector3D(10.2, 4.3, -6.7); + Vector3D c = new Vector3D(-0.2, 2.1, -3.2); + Rotation r = new Rotation(new Vector3D(6.2, -4.4, 2.1), 0.12); + + tree = tree.rotate(c, r).translate(s); + + Vector3D newB = + new Vector3D(1.0, s, + 1.0, c, + 1.0, r.applyTo(barycenter.subtract(c))); + Assert.assertEquals(0.0, + newB.subtract((Vector3D) tree.getBarycenter()).getNorm(), + 1.0e-10); + + final Vector3D[] expectedV = new Vector3D[] { + new Vector3D(1.0, s, + 1.0, c, + 1.0, r.applyTo(vertex1.subtract(c))), + new Vector3D(1.0, s, + 1.0, c, + 1.0, r.applyTo(vertex2.subtract(c))), + new Vector3D(1.0, s, + 1.0, c, + 1.0, r.applyTo(vertex3.subtract(c))), + new Vector3D(1.0, s, + 1.0, c, + 1.0, r.applyTo(vertex4.subtract(c))) + }; + tree.getTree(true).visit(new BSPTreeVisitor() { + + public Order visitOrder(BSPTree node) { + return Order.MINUS_SUB_PLUS; + } + + public void visitInternalNode(BSPTree node) { + Region.BoundaryAttribute attribute = + (Region.BoundaryAttribute) node.getAttribute(); + if (attribute.getPlusOutside() != null) { + checkFacet(attribute.getPlusOutside()); + } + if (attribute.getPlusInside() != null) { + checkFacet(attribute.getPlusInside()); + } + } + + public void visitLeafNode(BSPTree node) { + } + + private void checkFacet(SubHyperplane facet) { + Plane plane = (Plane) facet.getHyperplane(); + Point2D[][] vertices = + ((PolygonsSet) facet.getRemainingRegion()).getVertices(); + Assert.assertEquals(1, vertices.length); + for (int i = 0; i < vertices[0].length; ++i) { + Vector3D v = (Vector3D) plane.toSpace(vertices[0][i]); + double d = Double.POSITIVE_INFINITY; + for (int k = 0; k < expectedV.length; ++k) { + d = FastMath.min(d, v.subtract(expectedV[k]).getNorm()); + } + Assert.assertEquals(0, d, 1.0e-10); + } + } + + }); + + } + + @Test + public void testBuildBox() { + double x = 1.0; + double y = 2.0; + double z = 3.0; + double w = 0.1; + double l = 1.0; + PolyhedronsSet tree = + new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w); + Vector3D barycenter = (Vector3D) tree.getBarycenter(); + Assert.assertEquals(x, barycenter.getX(), 1.0e-10); + Assert.assertEquals(y, barycenter.getY(), 1.0e-10); + Assert.assertEquals(z, barycenter.getZ(), 1.0e-10); + Assert.assertEquals(8 * l * w * w, tree.getSize(), 1.0e-10); + Assert.assertEquals(8 * w * (2 * l + w), tree.getBoundarySize(), 1.0e-10); + } + + @Test + public void testCross() { + + double x = 1.0; + double y = 2.0; + double z = 3.0; + double w = 0.1; + double l = 1.0; + PolyhedronsSet xBeam = + new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w); + PolyhedronsSet yBeam = + new PolyhedronsSet(x - w, x + w, y - l, y + l, z - w, z + w); + PolyhedronsSet zBeam = + new PolyhedronsSet(x - w, x + w, y - w, y + w, z - l, z + l); + PolyhedronsSet tree = + (PolyhedronsSet) Region.union(xBeam, Region.union(yBeam, zBeam)); + Vector3D barycenter = (Vector3D) tree.getBarycenter(); + + Assert.assertEquals(x, barycenter.getX(), 1.0e-10); + Assert.assertEquals(y, barycenter.getY(), 1.0e-10); + Assert.assertEquals(z, barycenter.getZ(), 1.0e-10); + Assert.assertEquals(8 * w * w * (3 * l - 2 * w), tree.getSize(), 1.0e-10); + Assert.assertEquals(24 * w * (2 * l - w), tree.getBoundarySize(), 1.0e-10); + + } + + private void checkPoints(Region.Location expected, PolyhedronsSet tree, Point3D[] points) { + for (int i = 0; i < points.length; ++i) { + Assert.assertEquals(expected, tree.checkPoint(points[i])); + } + } + +} diff --git a/src/test/java/org/apache/commons/math/geometry/RotationOrderTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/RotationOrderTest.java similarity index 93% rename from src/test/java/org/apache/commons/math/geometry/RotationOrderTest.java rename to src/test/java/org/apache/commons/math/geometry/euclidean/threeD/RotationOrderTest.java index c882e04d2..6d50e64a9 100644 --- a/src/test/java/org/apache/commons/math/geometry/RotationOrderTest.java +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/RotationOrderTest.java @@ -15,11 +15,11 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import java.lang.reflect.Field; -import org.apache.commons.math.geometry.RotationOrder; +import org.apache.commons.math.geometry.euclidean.threeD.RotationOrder; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/apache/commons/math/geometry/RotationTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/RotationTest.java similarity index 97% rename from src/test/java/org/apache/commons/math/geometry/RotationTest.java rename to src/test/java/org/apache/commons/math/geometry/euclidean/threeD/RotationTest.java index d36020a7a..9904353fc 100644 --- a/src/test/java/org/apache/commons/math/geometry/RotationTest.java +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/RotationTest.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; -import org.apache.commons.math.geometry.CardanEulerSingularityException; -import org.apache.commons.math.geometry.NotARotationMatrixException; -import org.apache.commons.math.geometry.Rotation; -import org.apache.commons.math.geometry.RotationOrder; -import org.apache.commons.math.geometry.Vector3D; +import org.apache.commons.math.geometry.euclidean.threeD.CardanEulerSingularityException; +import org.apache.commons.math.geometry.euclidean.threeD.NotARotationMatrixException; +import org.apache.commons.math.geometry.euclidean.threeD.Rotation; +import org.apache.commons.math.geometry.euclidean.threeD.RotationOrder; +import org.apache.commons.math.geometry.euclidean.threeD.Vector3D; import org.apache.commons.math.util.FastMath; import org.apache.commons.math.util.MathUtils; import org.junit.Assert; diff --git a/src/test/java/org/apache/commons/math/geometry/Vector3DFormatAbstractTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormatAbstractTest.java similarity index 98% rename from src/test/java/org/apache/commons/math/geometry/Vector3DFormatAbstractTest.java rename to src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormatAbstractTest.java index de46b9567..0d72642af 100644 --- a/src/test/java/org/apache/commons/math/geometry/Vector3DFormatAbstractTest.java +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormatAbstractTest.java @@ -15,12 +15,14 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import java.text.NumberFormat; import java.text.ParsePosition; import java.util.Locale; +import org.apache.commons.math.geometry.euclidean.threeD.Vector3D; +import org.apache.commons.math.geometry.euclidean.threeD.Vector3DFormat; import org.junit.Test; import org.junit.Assert; diff --git a/src/test/java/org/apache/commons/math/geometry/Vector3DFormatTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormatTest.java similarity index 94% rename from src/test/java/org/apache/commons/math/geometry/Vector3DFormatTest.java rename to src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormatTest.java index ca3f28797..de7bf7d9d 100644 --- a/src/test/java/org/apache/commons/math/geometry/Vector3DFormatTest.java +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DFormatTest.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; import java.util.Locale; diff --git a/src/test/java/org/apache/commons/math/geometry/Vector3DTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DTest.java similarity index 98% rename from src/test/java/org/apache/commons/math/geometry/Vector3DTest.java rename to src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DTest.java index 4dc474c62..12b23e4fb 100644 --- a/src/test/java/org/apache/commons/math/geometry/Vector3DTest.java +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/threeD/Vector3DTest.java @@ -15,9 +15,9 @@ * limitations under the License. */ -package org.apache.commons.math.geometry; +package org.apache.commons.math.geometry.euclidean.threeD; -import org.apache.commons.math.geometry.Vector3D; +import org.apache.commons.math.geometry.euclidean.threeD.Vector3D; import org.apache.commons.math.util.FastMath; import org.apache.commons.math.exception.MathArithmeticException; diff --git a/src/test/java/org/apache/commons/math/geometry/euclidean/twoD/LineTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/twoD/LineTest.java new file mode 100644 index 000000000..2030fddac --- /dev/null +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/twoD/LineTest.java @@ -0,0 +1,129 @@ +/* + * 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.math.geometry.euclidean.twoD; + +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.euclidean.twoD.Line; +import org.apache.commons.math.geometry.euclidean.twoD.Point2D; +import org.apache.commons.math.geometry.partitioning.Transform; +import org.apache.commons.math.util.FastMath; +import org.junit.Assert; +import org.junit.Test; + +import java.awt.geom.AffineTransform; + +public class LineTest { + + @Test + public void testContains() { + Line l = new Line(new Point2D(0, 1), new Point2D(1, 2)); + Assert.assertTrue(l.contains(new Point2D(0, 1))); + Assert.assertTrue(l.contains(new Point2D(1, 2))); + Assert.assertTrue(l.contains(new Point2D(7, 8))); + Assert.assertTrue(! l.contains(new Point2D(8, 7))); + } + + @Test + public void testAbscissa() { + Line l = new Line(new Point2D(2, 1), new Point2D(-2, -2)); + Assert.assertEquals(0.0, + ((Point1D) l.toSubSpace(new Point2D(-3, 4))).getAbscissa(), + 1.0e-10); + Assert.assertEquals(0.0, + ((Point1D) l.toSubSpace(new Point2D( 3, -4))).getAbscissa(), + 1.0e-10); + Assert.assertEquals(-5.0, + ((Point1D) l.toSubSpace(new Point2D( 7, -1))).getAbscissa(), + 1.0e-10); + Assert.assertEquals( 5.0, + ((Point1D) l.toSubSpace(new Point2D(-1, -7))).getAbscissa(), + 1.0e-10); + } + + @Test + public void testOffset() { + Line l = new Line(new Point2D(2, 1), new Point2D(-2, -2)); + Assert.assertEquals(-5.0, l.getOffset(new Point2D(5, -3)), 1.0e-10); + Assert.assertEquals(+5.0, l.getOffset(new Point2D(-5, 2)), 1.0e-10); + } + + @Test + public void testPointAt() { + Line l = new Line(new Point2D(2, 1), new Point2D(-2, -2)); + for (double a = -2.0; a < 2.0; a += 0.2) { + Point1D pA = new Point1D(a); + Point2D point = (Point2D) l.toSpace(pA); + Assert.assertEquals(a, ((Point1D) l.toSubSpace(point)).getAbscissa(), 1.0e-10); + Assert.assertEquals(0.0, l.getOffset(point), 1.0e-10); + for (double o = -2.0; o < 2.0; o += 0.2) { + point = l.getPointAt(pA, o); + Assert.assertEquals(a, ((Point1D) l.toSubSpace(point)).getAbscissa(), 1.0e-10); + Assert.assertEquals(o, l.getOffset(point), 1.0e-10); + } + } + } + + @Test + public void testOriginOffset() { + Line l1 = new Line(new Point2D(0, 1), new Point2D(1, 2)); + Assert.assertEquals(FastMath.sqrt(0.5), l1.getOriginOffset(), 1.0e-10); + Line l2 = new Line(new Point2D(1, 2), new Point2D(0, 1)); + Assert.assertEquals(-FastMath.sqrt(0.5), l2.getOriginOffset(), 1.0e-10); + } + + @Test + public void testParallel() { + Line l1 = new Line(new Point2D(0, 1), new Point2D(1, 2)); + Line l2 = new Line(new Point2D(2, 2), new Point2D(3, 3)); + Assert.assertTrue(l1.isParallelTo(l2)); + Line l3 = new Line(new Point2D(1, 0), new Point2D(0.5, -0.5)); + Assert.assertTrue(l1.isParallelTo(l3)); + Line l4 = new Line(new Point2D(1, 0), new Point2D(0.5, -0.51)); + Assert.assertTrue(! l1.isParallelTo(l4)); + } + + @Test + public void testTransform() { + + Line l1 = new Line(new Point2D(1.0 ,1.0), new Point2D(4.0 ,1.0)); + Transform t1 = Line.getTransform(new AffineTransform(0.0, 0.5, + -1.0, 0.0, + 1.0, 1.5)); + Assert.assertEquals(0.5 * FastMath.PI, + ((Line) t1.apply(l1)).getAngle(), + 1.0e-10); + + Line l2 = new Line(new Point2D(0.0, 0.0), new Point2D(1.0, 1.0)); + Transform t2 = Line.getTransform(new AffineTransform(0.0, 0.5, + -1.0, 0.0, + 1.0, 1.5)); + Assert.assertEquals(FastMath.atan2(1.0, -2.0), + ((Line) t2.apply(l2)).getAngle(), + 1.0e-10); + + } + + @Test + public void testIntersection() { + Line l1 = new Line(new Point2D( 0, 1), new Point2D(1, 2)); + Line l2 = new Line(new Point2D(-1, 2), new Point2D(2, 1)); + Point2D p = (Point2D) l1.intersection(l2); + Assert.assertEquals(0.5, p.x, 1.0e-10); + Assert.assertEquals(1.5, p.y, 1.0e-10); + } + +} diff --git a/src/test/java/org/apache/commons/math/geometry/euclidean/twoD/PolygonsSetTest.java b/src/test/java/org/apache/commons/math/geometry/euclidean/twoD/PolygonsSetTest.java new file mode 100644 index 000000000..16706178e --- /dev/null +++ b/src/test/java/org/apache/commons/math/geometry/euclidean/twoD/PolygonsSetTest.java @@ -0,0 +1,883 @@ +/* + * 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.math.geometry.euclidean.twoD; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.math.geometry.euclidean.oneD.Interval; +import org.apache.commons.math.geometry.euclidean.oneD.IntervalsSet; +import org.apache.commons.math.geometry.euclidean.oneD.Point1D; +import org.apache.commons.math.geometry.euclidean.twoD.Line; +import org.apache.commons.math.geometry.euclidean.twoD.Point2D; +import org.apache.commons.math.geometry.euclidean.twoD.PolygonsSet; +import org.apache.commons.math.geometry.partitioning.BSPTree; +import org.apache.commons.math.geometry.partitioning.Region; +import org.apache.commons.math.geometry.partitioning.SubHyperplane; +import org.apache.commons.math.util.FastMath; +import org.junit.Assert; +import org.junit.Test; + +public class PolygonsSetTest { + + @Test + public void testSimplyConnected() { + Point2D[][] vertices = new Point2D[][] { + new Point2D[] { + new Point2D(36.0, 22.0), + new Point2D(39.0, 32.0), + new Point2D(19.0, 32.0), + new Point2D( 6.0, 16.0), + new Point2D(31.0, 10.0), + new Point2D(42.0, 16.0), + new Point2D(34.0, 20.0), + new Point2D(29.0, 19.0), + new Point2D(23.0, 22.0), + new Point2D(33.0, 25.0) + } + }; + PolygonsSet set = buildSet(vertices); + Assert.assertEquals(Region.Location.OUTSIDE, set.checkPoint(new Point2D(50.0, 30.0))); + checkPoints(Region.Location.INSIDE, set, new Point2D[] { + new Point2D(30.0, 15.0), + new Point2D(15.0, 20.0), + new Point2D(24.0, 25.0), + new Point2D(35.0, 30.0), + new Point2D(19.0, 17.0) + }); + checkPoints(Region.Location.OUTSIDE, set, new Point2D[] { + new Point2D(50.0, 30.0), + new Point2D(30.0, 35.0), + new Point2D(10.0, 25.0), + new Point2D(10.0, 10.0), + new Point2D(40.0, 10.0), + new Point2D(50.0, 15.0), + new Point2D(30.0, 22.0) + }); + checkPoints(Region.Location.BOUNDARY, set, new Point2D[] { + new Point2D(30.0, 32.0), + new Point2D(34.0, 20.0) + }); + checkVertices(set.getVertices(), vertices); + } + + @Test + public void testStair() { + Point2D[][] vertices = new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 0.0, 2.0), + new Point2D(-0.1, 2.0), + new Point2D(-0.1, 1.0), + new Point2D(-0.3, 1.0), + new Point2D(-0.3, 1.5), + new Point2D(-1.3, 1.5), + new Point2D(-1.3, 2.0), + new Point2D(-1.8, 2.0), + new Point2D(-1.8 - 1.0 / FastMath.sqrt(2.0), + 2.0 - 1.0 / FastMath.sqrt(2.0)) + } + }; + + PolygonsSet set = buildSet(vertices); + checkVertices(set.getVertices(), vertices); + + Assert.assertEquals(1.1 + 0.95 * FastMath.sqrt(2.0), set.getSize(), 1.0e-10); + + } + + @Test + public void testHole() { + Point2D[][] vertices = new Point2D[][] { + new Point2D[] { + new Point2D(0.0, 0.0), + new Point2D(3.0, 0.0), + new Point2D(3.0, 3.0), + new Point2D(0.0, 3.0) + }, new Point2D[] { + new Point2D(1.0, 2.0), + new Point2D(2.0, 2.0), + new Point2D(2.0, 1.0), + new Point2D(1.0, 1.0) + } + }; + PolygonsSet set = buildSet(vertices); + checkPoints(Region.Location.INSIDE, set, new Point2D[] { + new Point2D(0.5, 0.5), + new Point2D(1.5, 0.5), + new Point2D(2.5, 0.5), + new Point2D(0.5, 1.5), + new Point2D(2.5, 1.5), + new Point2D(0.5, 2.5), + new Point2D(1.5, 2.5), + new Point2D(2.5, 2.5), + new Point2D(0.5, 1.0) + }); + checkPoints(Region.Location.OUTSIDE, set, new Point2D[] { + new Point2D(1.5, 1.5), + new Point2D(3.5, 1.0), + new Point2D(4.0, 1.5), + new Point2D(6.0, 6.0) + }); + checkPoints(Region.Location.BOUNDARY, set, new Point2D[] { + new Point2D(1.0, 1.0), + new Point2D(1.5, 0.0), + new Point2D(1.5, 1.0), + new Point2D(1.5, 2.0), + new Point2D(1.5, 3.0), + new Point2D(3.0, 3.0) + }); + checkVertices(set.getVertices(), vertices); + } + + @Test + public void testDisjointPolygons() { + Point2D[][] vertices = new Point2D[][] { + new Point2D[] { + new Point2D(0.0, 1.0), + new Point2D(2.0, 1.0), + new Point2D(1.0, 2.0) + }, new Point2D[] { + new Point2D(4.0, 0.0), + new Point2D(5.0, 1.0), + new Point2D(3.0, 1.0) + } + }; + PolygonsSet set = buildSet(vertices); + Assert.assertEquals(Region.Location.INSIDE, set.checkPoint(new Point2D(1.0, 1.5))); + checkPoints(Region.Location.INSIDE, set, new Point2D[] { + new Point2D(1.0, 1.5), + new Point2D(4.5, 0.8) + }); + checkPoints(Region.Location.OUTSIDE, set, new Point2D[] { + new Point2D(1.0, 0.0), + new Point2D(3.5, 1.2), + new Point2D(2.5, 1.0), + new Point2D(3.0, 4.0) + }); + checkPoints(Region.Location.BOUNDARY, set, new Point2D[] { + new Point2D(1.0, 1.0), + new Point2D(3.5, 0.5), + new Point2D(0.0, 1.0) + }); + checkVertices(set.getVertices(), vertices); + } + + @Test + public void testOppositeHyperplanes() { + Point2D[][] vertices = new Point2D[][] { + new Point2D[] { + new Point2D(1.0, 0.0), + new Point2D(2.0, 1.0), + new Point2D(3.0, 1.0), + new Point2D(2.0, 2.0), + new Point2D(1.0, 1.0), + new Point2D(0.0, 1.0) + } + }; + PolygonsSet set = buildSet(vertices); + checkVertices(set.getVertices(), vertices); + } + + @Test + public void testSingularPoint() { + Point2D[][] vertices = new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 1.0, 0.0), + new Point2D( 1.0, 1.0), + new Point2D( 0.0, 1.0), + new Point2D( 0.0, 0.0), + new Point2D(-1.0, 0.0), + new Point2D(-1.0, -1.0), + new Point2D( 0.0, -1.0) + } + }; + PolygonsSet set = buildSet(vertices); + checkVertices(set.getVertices(), vertices); + } + + @Test + public void testLineIntersection() { + Point2D[][] vertices = new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 2.0, 0.0), + new Point2D( 2.0, 1.0), + new Point2D( 3.0, 1.0), + new Point2D( 3.0, 3.0), + new Point2D( 1.0, 3.0), + new Point2D( 1.0, 2.0), + new Point2D( 0.0, 2.0) + } + }; + PolygonsSet set = buildSet(vertices); + + Line l1 = new Line(new Point2D(-1.5, 0.0), FastMath.PI / 4); + SubHyperplane s1 = set.intersection(new SubHyperplane(l1)); + List i1 = ((IntervalsSet) s1.getRemainingRegion()).asList(); + Assert.assertEquals(2, i1.size()); + Interval v10 = (Interval) i1.get(0); + Point2D p10Lower = (Point2D) l1.toSpace(new Point1D(v10.getLower())); + Assert.assertEquals(0.0, p10Lower.getX(), 1.0e-10); + Assert.assertEquals(1.5, p10Lower.getY(), 1.0e-10); + Point2D p10Upper = (Point2D) l1.toSpace(new Point1D(v10.getUpper())); + Assert.assertEquals(0.5, p10Upper.getX(), 1.0e-10); + Assert.assertEquals(2.0, p10Upper.getY(), 1.0e-10); + Interval v11 = (Interval) i1.get(1); + Point2D p11Lower = (Point2D) l1.toSpace(new Point1D(v11.getLower())); + Assert.assertEquals(1.0, p11Lower.getX(), 1.0e-10); + Assert.assertEquals(2.5, p11Lower.getY(), 1.0e-10); + Point2D p11Upper = (Point2D) l1.toSpace(new Point1D(v11.getUpper())); + Assert.assertEquals(1.5, p11Upper.getX(), 1.0e-10); + Assert.assertEquals(3.0, p11Upper.getY(), 1.0e-10); + + Line l2 = new Line(new Point2D(-1.0, 2.0), 0); + SubHyperplane s2 = set.intersection(new SubHyperplane(l2)); + List i2 = ((IntervalsSet) s2.getRemainingRegion()).asList(); + Assert.assertEquals(1, i2.size()); + Interval v20 = (Interval) i2.get(0); + Point2D p20Lower = (Point2D) l2.toSpace(new Point1D(v20.getLower())); + Assert.assertEquals(1.0, p20Lower.getX(), 1.0e-10); + Assert.assertEquals(2.0, p20Lower.getY(), 1.0e-10); + Point2D p20Upper = (Point2D) l2.toSpace(new Point1D(v20.getUpper())); + Assert.assertEquals(3.0, p20Upper.getX(), 1.0e-10); + Assert.assertEquals(2.0, p20Upper.getY(), 1.0e-10); + + } + + @Test + public void testUnlimitedSubHyperplane() { + Point2D[][] vertices1 = new Point2D[][] { + new Point2D[] { + new Point2D(0.0, 0.0), + new Point2D(4.0, 0.0), + new Point2D(1.4, 1.5), + new Point2D(0.0, 3.5) + } + }; + PolygonsSet set1 = buildSet(vertices1); + Point2D[][] vertices2 = new Point2D[][] { + new Point2D[] { + new Point2D(1.4, 0.2), + new Point2D(2.8, -1.2), + new Point2D(2.5, 0.6) + } + }; + PolygonsSet set2 = buildSet(vertices2); + + PolygonsSet set = (PolygonsSet) Region.union(set1.copySelf(), + set2.copySelf()); + checkVertices(set1.getVertices(), vertices1); + checkVertices(set2.getVertices(), vertices2); + checkVertices(set.getVertices(), new Point2D[][] { + new Point2D[] { + new Point2D(0.0, 0.0), + new Point2D(1.6, 0.0), + new Point2D(2.8, -1.2), + new Point2D(2.6, 0.0), + new Point2D(4.0, 0.0), + new Point2D(1.4, 1.5), + new Point2D(0.0, 3.5) + } + }); + + } + + @Test + public void testUnion() { + Point2D[][] vertices1 = new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 2.0, 0.0), + new Point2D( 2.0, 2.0), + new Point2D( 0.0, 2.0) + } + }; + PolygonsSet set1 = buildSet(vertices1); + Point2D[][] vertices2 = new Point2D[][] { + new Point2D[] { + new Point2D( 1.0, 1.0), + new Point2D( 3.0, 1.0), + new Point2D( 3.0, 3.0), + new Point2D( 1.0, 3.0) + } + }; + PolygonsSet set2 = buildSet(vertices2); + PolygonsSet set = (PolygonsSet) Region.union(set1.copySelf(), + set2.copySelf()); + checkVertices(set1.getVertices(), vertices1); + checkVertices(set2.getVertices(), vertices2); + checkVertices(set.getVertices(), new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 2.0, 0.0), + new Point2D( 2.0, 1.0), + new Point2D( 3.0, 1.0), + new Point2D( 3.0, 3.0), + new Point2D( 1.0, 3.0), + new Point2D( 1.0, 2.0), + new Point2D( 0.0, 2.0) + } + }); + checkPoints(Region.Location.INSIDE, set, new Point2D[] { + new Point2D(1.0, 1.0), + new Point2D(0.5, 0.5), + new Point2D(2.0, 2.0), + new Point2D(2.5, 2.5), + new Point2D(0.5, 1.5), + new Point2D(1.5, 1.5), + new Point2D(1.5, 0.5), + new Point2D(1.5, 2.5), + new Point2D(2.5, 1.5), + new Point2D(2.5, 2.5) + }); + checkPoints(Region.Location.OUTSIDE, set, new Point2D[] { + new Point2D(-0.5, 0.5), + new Point2D( 0.5, 2.5), + new Point2D( 2.5, 0.5), + new Point2D( 3.5, 2.5) + }); + checkPoints(Region.Location.BOUNDARY, set, new Point2D[] { + new Point2D(0.0, 0.0), + new Point2D(0.5, 2.0), + new Point2D(2.0, 0.5), + new Point2D(2.5, 1.0), + new Point2D(3.0, 2.5) + }); + + } + + @Test + public void testIntersection() { + Point2D[][] vertices1 = new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 2.0, 0.0), + new Point2D( 2.0, 2.0), + new Point2D( 0.0, 2.0) + } + }; + PolygonsSet set1 = buildSet(vertices1); + Point2D[][] vertices2 = new Point2D[][] { + new Point2D[] { + new Point2D( 1.0, 1.0), + new Point2D( 3.0, 1.0), + new Point2D( 3.0, 3.0), + new Point2D( 1.0, 3.0) + } + }; + PolygonsSet set2 = buildSet(vertices2); + PolygonsSet set = (PolygonsSet) Region.intersection(set1.copySelf(), + set2.copySelf()); + checkVertices(set1.getVertices(), vertices1); + checkVertices(set2.getVertices(), vertices2); + checkVertices(set.getVertices(), new Point2D[][] { + new Point2D[] { + new Point2D( 1.0, 1.0), + new Point2D( 2.0, 1.0), + new Point2D( 2.0, 2.0), + new Point2D( 1.0, 2.0) + } + }); + checkPoints(Region.Location.INSIDE, set, new Point2D[] { + new Point2D(1.5, 1.5) + }); + checkPoints(Region.Location.OUTSIDE, set, new Point2D[] { + new Point2D(0.5, 1.5), + new Point2D(2.5, 1.5), + new Point2D(1.5, 0.5), + new Point2D(0.5, 0.5) + }); + checkPoints(Region.Location.BOUNDARY, set, new Point2D[] { + new Point2D(1.0, 1.0), + new Point2D(2.0, 2.0), + new Point2D(1.0, 1.5), + new Point2D(1.5, 2.0) + }); + } + + @Test + public void testXor() { + Point2D[][] vertices1 = new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 2.0, 0.0), + new Point2D( 2.0, 2.0), + new Point2D( 0.0, 2.0) + } + }; + PolygonsSet set1 = buildSet(vertices1); + Point2D[][] vertices2 = new Point2D[][] { + new Point2D[] { + new Point2D( 1.0, 1.0), + new Point2D( 3.0, 1.0), + new Point2D( 3.0, 3.0), + new Point2D( 1.0, 3.0) + } + }; + PolygonsSet set2 = buildSet(vertices2); + PolygonsSet set = (PolygonsSet) Region.xor(set1.copySelf(), + set2.copySelf()); + checkVertices(set1.getVertices(), vertices1); + checkVertices(set2.getVertices(), vertices2); + checkVertices(set.getVertices(), new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 2.0, 0.0), + new Point2D( 2.0, 1.0), + new Point2D( 3.0, 1.0), + new Point2D( 3.0, 3.0), + new Point2D( 1.0, 3.0), + new Point2D( 1.0, 2.0), + new Point2D( 0.0, 2.0) + }, + new Point2D[] { + new Point2D( 1.0, 1.0), + new Point2D( 1.0, 2.0), + new Point2D( 2.0, 2.0), + new Point2D( 2.0, 1.0) + } + }); + checkPoints(Region.Location.INSIDE, set, new Point2D[] { + new Point2D(0.5, 0.5), + new Point2D(2.5, 2.5), + new Point2D(0.5, 1.5), + new Point2D(1.5, 0.5), + new Point2D(1.5, 2.5), + new Point2D(2.5, 1.5), + new Point2D(2.5, 2.5) + }); + checkPoints(Region.Location.OUTSIDE, set, new Point2D[] { + new Point2D(-0.5, 0.5), + new Point2D( 0.5, 2.5), + new Point2D( 2.5, 0.5), + new Point2D( 1.5, 1.5), + new Point2D( 3.5, 2.5) + }); + checkPoints(Region.Location.BOUNDARY, set, new Point2D[] { + new Point2D(1.0, 1.0), + new Point2D(2.0, 2.0), + new Point2D(1.5, 1.0), + new Point2D(2.0, 1.5), + new Point2D(0.0, 0.0), + new Point2D(0.5, 2.0), + new Point2D(2.0, 0.5), + new Point2D(2.5, 1.0), + new Point2D(3.0, 2.5) + }); + } + + @Test + public void testDifference() { + Point2D[][] vertices1 = new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 2.0, 0.0), + new Point2D( 2.0, 2.0), + new Point2D( 0.0, 2.0) + } + }; + PolygonsSet set1 = buildSet(vertices1); + Point2D[][] vertices2 = new Point2D[][] { + new Point2D[] { + new Point2D( 1.0, 1.0), + new Point2D( 3.0, 1.0), + new Point2D( 3.0, 3.0), + new Point2D( 1.0, 3.0) + } + }; + PolygonsSet set2 = buildSet(vertices2); + PolygonsSet set = (PolygonsSet) Region.difference(set1.copySelf(), + set2.copySelf()); + checkVertices(set1.getVertices(), vertices1); + checkVertices(set2.getVertices(), vertices2); + checkVertices(set.getVertices(), new Point2D[][] { + new Point2D[] { + new Point2D( 0.0, 0.0), + new Point2D( 2.0, 0.0), + new Point2D( 2.0, 1.0), + new Point2D( 1.0, 1.0), + new Point2D( 1.0, 2.0), + new Point2D( 0.0, 2.0) + } + }); + checkPoints(Region.Location.INSIDE, set, new Point2D[] { + new Point2D(0.5, 0.5), + new Point2D(0.5, 1.5), + new Point2D(1.5, 0.5) + }); + checkPoints(Region.Location.OUTSIDE, set, new Point2D[] { + new Point2D( 2.5, 2.5), + new Point2D(-0.5, 0.5), + new Point2D( 0.5, 2.5), + new Point2D( 2.5, 0.5), + new Point2D( 1.5, 1.5), + new Point2D( 3.5, 2.5), + new Point2D( 1.5, 2.5), + new Point2D( 2.5, 1.5), + new Point2D( 2.0, 1.5), + new Point2D( 2.0, 2.0), + new Point2D( 2.5, 1.0), + new Point2D( 2.5, 2.5), + new Point2D( 3.0, 2.5) + }); + checkPoints(Region.Location.BOUNDARY, set, new Point2D[] { + new Point2D(1.0, 1.0), + new Point2D(1.5, 1.0), + new Point2D(0.0, 0.0), + new Point2D(0.5, 2.0), + new Point2D(2.0, 0.5) + }); + } + + @Test + public void testEmptyDifference() { + Point2D[][] vertices1 = new Point2D[][] { + new Point2D[] { + new Point2D( 0.5, 3.5), + new Point2D( 0.5, 4.5), + new Point2D(-0.5, 4.5), + new Point2D(-0.5, 3.5) + } + }; + PolygonsSet set1 = buildSet(vertices1); + Point2D[][] vertices2 = new Point2D[][] { + new Point2D[] { + new Point2D( 1.0, 2.0), + new Point2D( 1.0, 8.0), + new Point2D(-1.0, 8.0), + new Point2D(-1.0, 2.0) + } + }; + PolygonsSet set2 = buildSet(vertices2); + Assert.assertTrue(Region.difference(set1.copySelf(), set2.copySelf()).isEmpty()); + } + + @Test + public void testChoppedHexagon() { + double pi6 = FastMath.PI / 6.0; + double sqrt3 = FastMath.sqrt(3.0); + SubHyperplane[] hyp = { + new SubHyperplane(new Line(new Point2D( 0.0, 1.0), 5 * pi6)), + new SubHyperplane(new Line(new Point2D(-sqrt3, 1.0), 7 * pi6)), + new SubHyperplane(new Line(new Point2D(-sqrt3, 1.0), 9 * pi6)), + new SubHyperplane(new Line(new Point2D(-sqrt3, 0.0), 11 * pi6)), + new SubHyperplane(new Line(new Point2D( 0.0, 0.0), 13 * pi6)), + new SubHyperplane(new Line(new Point2D( 0.0, 1.0), 3 * pi6)), + new SubHyperplane(new Line(new Point2D(-5.0 * sqrt3 / 6.0, 0.0), 9 * pi6)) + }; + hyp[1] = hyp[0].getHyperplane().split(hyp[1]).getMinus(); + hyp[2] = hyp[1].getHyperplane().split(hyp[2]).getMinus(); + hyp[3] = hyp[2].getHyperplane().split(hyp[3]).getMinus(); + hyp[4] = hyp[0].getHyperplane().split(hyp[3].getHyperplane().split(hyp[4]).getMinus()).getMinus(); + hyp[5] = hyp[0].getHyperplane().split(hyp[4].getHyperplane().split(hyp[5]).getMinus()).getMinus(); + hyp[6] = hyp[1].getHyperplane().split(hyp[3].getHyperplane().split(hyp[6]).getMinus()).getMinus(); + BSPTree tree = new BSPTree(Boolean.TRUE); + for (int i = hyp.length - 1; i >= 0; --i) { + tree = new BSPTree(hyp[i], new BSPTree(Boolean.FALSE), tree, null); + } + PolygonsSet set = new PolygonsSet(tree); + SubHyperplane splitter = + new SubHyperplane(new Line(new Point2D(-2.0 * sqrt3 / 3.0, 0.0), 9 * pi6)); + PolygonsSet slice = + new PolygonsSet(new BSPTree(splitter, + set.getTree(false).split(splitter).getPlus(), + new BSPTree(Boolean.FALSE), null)); + Assert.assertEquals(Region.Location.OUTSIDE, + slice.checkPoint(new Point2D(0.1, 0.5))); + Assert.assertEquals(11.0 / 3.0, slice.getBoundarySize(), 1.0e-10); + + } + + @Test + public void testConcentric() { + double h = FastMath.sqrt(3.0) / 2.0; + Point2D[][] vertices1 = new Point2D[][] { + new Point2D[] { + new Point2D( 0.00, 0.1 * h), + new Point2D( 0.05, 0.1 * h), + new Point2D( 0.10, 0.2 * h), + new Point2D( 0.05, 0.3 * h), + new Point2D(-0.05, 0.3 * h), + new Point2D(-0.10, 0.2 * h), + new Point2D(-0.05, 0.1 * h) + } + }; + PolygonsSet set1 = buildSet(vertices1); + Point2D[][] vertices2 = new Point2D[][] { + new Point2D[] { + new Point2D( 0.00, 0.0 * h), + new Point2D( 0.10, 0.0 * h), + new Point2D( 0.20, 0.2 * h), + new Point2D( 0.10, 0.4 * h), + new Point2D(-0.10, 0.4 * h), + new Point2D(-0.20, 0.2 * h), + new Point2D(-0.10, 0.0 * h) + } + }; + PolygonsSet set2 = buildSet(vertices2); + Assert.assertTrue(set2.contains(set1)); + } + + @Test + public void testBug20040520() { + BSPTree a0 = new BSPTree(buildSegment(new Point2D(0.85, -0.05), + new Point2D(0.90, -0.10)), + new BSPTree(Boolean.FALSE), + new BSPTree(Boolean.TRUE), + null); + BSPTree a1 = new BSPTree(buildSegment(new Point2D(0.85, -0.10), + new Point2D(0.90, -0.10)), + new BSPTree(Boolean.FALSE), a0, null); + BSPTree a2 = new BSPTree(buildSegment(new Point2D(0.90, -0.05), + new Point2D(0.85, -0.05)), + new BSPTree(Boolean.FALSE), a1, null); + BSPTree a3 = new BSPTree(buildSegment(new Point2D(0.82, -0.05), + new Point2D(0.82, -0.08)), + new BSPTree(Boolean.FALSE), + new BSPTree(Boolean.TRUE), + null); + BSPTree a4 = new BSPTree(buildHalfLine(new Point2D(0.85, -0.05), + new Point2D(0.80, -0.05), + false), + new BSPTree(Boolean.FALSE), a3, null); + BSPTree a5 = new BSPTree(buildSegment(new Point2D(0.82, -0.08), + new Point2D(0.82, -0.18)), + new BSPTree(Boolean.FALSE), + new BSPTree(Boolean.TRUE), + null); + BSPTree a6 = new BSPTree(buildHalfLine(new Point2D(0.82, -0.18), + new Point2D(0.85, -0.15), + true), + new BSPTree(Boolean.FALSE), a5, null); + BSPTree a7 = new BSPTree(buildHalfLine(new Point2D(0.85, -0.05), + new Point2D(0.82, -0.08), + false), + a4, a6, null); + BSPTree a8 = new BSPTree(buildLine(new Point2D(0.85, -0.25), + new Point2D(0.85, 0.05)), + a2, a7, null); + BSPTree a9 = new BSPTree(buildLine(new Point2D(0.90, 0.05), + new Point2D(0.90, -0.50)), + a8, new BSPTree(Boolean.FALSE), null); + + BSPTree b0 = new BSPTree(buildSegment(new Point2D(0.92, -0.12), + new Point2D(0.92, -0.08)), + new BSPTree(Boolean.FALSE), new BSPTree(Boolean.TRUE), + null); + BSPTree b1 = new BSPTree(buildHalfLine(new Point2D(0.92, -0.08), + new Point2D(0.90, -0.10), + true), + new BSPTree(Boolean.FALSE), b0, null); + BSPTree b2 = new BSPTree(buildSegment(new Point2D(0.92, -0.18), + new Point2D(0.92, -0.12)), + new BSPTree(Boolean.FALSE), new BSPTree(Boolean.TRUE), + null); + BSPTree b3 = new BSPTree(buildSegment(new Point2D(0.85, -0.15), + new Point2D(0.90, -0.20)), + new BSPTree(Boolean.FALSE), b2, null); + BSPTree b4 = new BSPTree(buildSegment(new Point2D(0.95, -0.15), + new Point2D(0.85, -0.05)), + b1, b3, null); + BSPTree b5 = new BSPTree(buildHalfLine(new Point2D(0.85, -0.05), + new Point2D(0.85, -0.25), + true), + new BSPTree(Boolean.FALSE), b4, null); + BSPTree b6 = new BSPTree(buildLine(new Point2D(0.0, -1.10), + new Point2D(1.0, -0.10)), + new BSPTree(Boolean.FALSE), b5, null); + + PolygonsSet c = (PolygonsSet) Region.union(new PolygonsSet(a9), + new PolygonsSet(b6)); + + checkPoints(Region.Location.INSIDE, c, new Point2D[] { + new Point2D(0.83, -0.06), + new Point2D(0.83, -0.15), + new Point2D(0.88, -0.15), + new Point2D(0.88, -0.09), + new Point2D(0.88, -0.07), + new Point2D(0.91, -0.18), + new Point2D(0.91, -0.10) + }); + + checkPoints(Region.Location.OUTSIDE, c, new Point2D[] { + new Point2D(0.80, -0.10), + new Point2D(0.83, -0.50), + new Point2D(0.83, -0.20), + new Point2D(0.83, -0.02), + new Point2D(0.87, -0.50), + new Point2D(0.87, -0.20), + new Point2D(0.87, -0.02), + new Point2D(0.91, -0.20), + new Point2D(0.91, -0.08), + new Point2D(0.93, -0.15) + }); + + checkVertices(c.getVertices(), + new Point2D[][] { + new Point2D[] { + new Point2D(0.85, -0.15), + new Point2D(0.90, -0.20), + new Point2D(0.92, -0.18), + new Point2D(0.92, -0.08), + new Point2D(0.90, -0.10), + new Point2D(0.90, -0.05), + new Point2D(0.82, -0.05), + new Point2D(0.82, -0.18), + } + }); + + } + + @Test + public void testBug20041003() { + + Line[] l = { + new Line(new Point2D(0.0, 0.625000007541172), + new Point2D(1.0, 0.625000007541172)), + new Line(new Point2D(-0.19204433621902645, 0.0), + new Point2D(-0.19204433621902645, 1.0)), + new Line(new Point2D(-0.40303524786887, 0.4248364535319128), + new Point2D(-1.12851149797877, -0.2634107480798909)), + new Line(new Point2D(0.0, 2.0), + new Point2D(1.0, 2.0)) + }; + + BSPTree node1 = + new BSPTree(new SubHyperplane(l[0], + new IntervalsSet(intersectionAbscissa(l[0], l[1]), + intersectionAbscissa(l[0], l[2]))), + new BSPTree(Boolean.TRUE), new BSPTree(Boolean.FALSE), + null); + BSPTree node2 = + new BSPTree(new SubHyperplane(l[1], + new IntervalsSet(intersectionAbscissa(l[1], l[2]), + intersectionAbscissa(l[1], l[3]))), + node1, new BSPTree(Boolean.FALSE), null); + BSPTree node3 = + new BSPTree(new SubHyperplane(l[2], + new IntervalsSet(intersectionAbscissa(l[2], l[3]), + Double.POSITIVE_INFINITY)), + node2, new BSPTree(Boolean.FALSE), null); + BSPTree node4 = + new BSPTree(new SubHyperplane(l[3]), + node3, new BSPTree(Boolean.FALSE), null); + + PolygonsSet set = new PolygonsSet(node4); + Assert.assertEquals(0, set.getVertices().length); + + } + + private PolygonsSet buildSet(Point2D[][] vertices) { + ArrayList edges = new ArrayList(); + for (int i = 0; i < vertices.length; ++i) { + int l = vertices[i].length; + for (int j = 0; j < l; ++j) { + edges.add(buildSegment(vertices[i][j], vertices[i][(j + 1) % l])); + } + } + return new PolygonsSet(edges); + } + + private SubHyperplane buildLine(Point2D start, Point2D end) { + return new SubHyperplane(new Line(start, end)); + } + + private double intersectionAbscissa(Line l0, Line l1) { + Point2D p = (Point2D) l0.intersection(l1); + return ((Point1D) l0.toSubSpace(p)).getAbscissa(); + } + + private SubHyperplane buildHalfLine(Point2D start, Point2D end, + boolean startIsVirtual) { + Line line = new Line(start, end); + double lower = startIsVirtual + ? Double.NEGATIVE_INFINITY + : ((Point1D) line.toSubSpace(start)).getAbscissa(); + double upper = startIsVirtual + ? ((Point1D) line.toSubSpace(end)).getAbscissa() + : Double.POSITIVE_INFINITY; + return new SubHyperplane(line, new IntervalsSet(lower, upper)); + } + + private SubHyperplane buildSegment(Point2D start, Point2D end) { + Line line = new Line(start, end); + double lower = ((Point1D) line.toSubSpace(start)).getAbscissa(); + double upper = ((Point1D) line.toSubSpace(end)).getAbscissa(); + return new SubHyperplane(line, new IntervalsSet(lower, upper)); + } + + private void checkPoints(Region.Location expected, PolygonsSet set, + Point2D[] points) { + for (int i = 0; i < points.length; ++i) { + Assert.assertEquals(expected, set.checkPoint(points[i])); + } + } + + private boolean checkInSegment(Point2D p, + Point2D p1, Point2D p2, + double tolerance) { + Line line = new Line(p1, p2); + if (line.getOffset(p) < tolerance) { + double x = ((Point1D) line.toSubSpace(p)).getAbscissa(); + double x1 = ((Point1D) line.toSubSpace(p1)).getAbscissa(); + double x2 = ((Point1D) line.toSubSpace(p2)).getAbscissa(); + return (((x - x1) * (x - x2) <= 0.0) + || (p1.distance(p) < tolerance) + || (p2.distance(p) < tolerance)); + } else { + return false; + } + } + + private void checkVertices(Point2D[][] rebuiltVertices, + Point2D[][] vertices) { + + // each rebuilt vertex should be in a segment joining two original vertices + for (int i = 0; i < rebuiltVertices.length; ++i) { + for (int j = 0; j < rebuiltVertices[i].length; ++j) { + boolean inSegment = false; + Point2D p = rebuiltVertices[i][j]; + for (int k = 0; k < vertices.length; ++k) { + Point2D[] loop = vertices[k]; + int length = loop.length; + for (int l = 0; (! inSegment) && (l < length); ++l) { + inSegment = checkInSegment(p, loop[l], loop[(l + 1) % length], 1.0e-10); + } + } + Assert.assertTrue(inSegment); + } + } + + // each original vertex should have a corresponding rebuilt vertex + for (int k = 0; k < vertices.length; ++k) { + for (int l = 0; l < vertices[k].length; ++l) { + double min = Double.POSITIVE_INFINITY; + for (int i = 0; i < rebuiltVertices.length; ++i) { + for (int j = 0; j < rebuiltVertices[i].length; ++j) { + min = FastMath.min(vertices[k][l].distance(rebuiltVertices[i][j]), + min); + } + } + Assert.assertEquals(0.0, min, 1.0e-10); + } + } + + } + +} diff --git a/src/test/java/org/apache/commons/math/geometry/partitioning/utilities/AVLTreeTest.java b/src/test/java/org/apache/commons/math/geometry/partitioning/utilities/AVLTreeTest.java new file mode 100644 index 000000000..e9cc570d3 --- /dev/null +++ b/src/test/java/org/apache/commons/math/geometry/partitioning/utilities/AVLTreeTest.java @@ -0,0 +1,175 @@ +/* + * 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.math.geometry.partitioning.utilities; + +import org.apache.commons.math.geometry.partitioning.utilities.AVLTree; +import org.junit.Assert; +import org.junit.Test; + +public class AVLTreeTest { + + @Test + public void testInsert() { + // this array in this order allows to pass in all branches + // of the insertion algorithm + int[] array = { 16, 13, 15, 14, 2, 0, 12, 9, 8, 5, + 11, 18, 19, 17, 4, 7, 1, 3, 6, 10 }; + AVLTree tree = buildTree(array); + + Assert.assertEquals(array.length, tree.size()); + + for (int i = 0; i < array.length; ++i) { + Assert.assertEquals(array[i], value(tree.getNotSmaller(new Integer(array[i])))); + } + + checkOrder(tree); + + } + + @Test + public void testDelete1() { + int[][][] arrays = { + { { 16, 13, 15, 14, 2, 0, 12, 9, 8, 5, 11, 18, 19, 17, 4, 7, 1, 3, 6, 10 }, + { 11, 10, 9, 12, 16, 15, 13, 18, 5, 0, 3, 2, 14, 6, 19, 17, 8, 4, 7, 1 } }, + { { 16, 13, 15, 14, 2, 0, 12, 9, 8, 5, 11, 18, 19, 17, 4, 7, 1, 3, 6, 10 }, + { 0, 17, 14, 15, 16, 18, 6 } }, + { { 6, 2, 7, 8, 1, 4, 3, 5 }, { 8 } }, + { { 6, 2, 7, 8, 1, 4, 5 }, { 8 } }, + { { 3, 7, 2, 1, 5, 8, 4 }, { 1 } }, + { { 3, 7, 2, 1, 5, 8, 6 }, { 1 } } + }; + for (int i = 0; i < arrays.length; ++i) { + AVLTree tree = buildTree(arrays[i][0]); + Assert.assertTrue(! tree.delete(new Integer(-2000))); + for (int j = 0; j < arrays[i][1].length; ++j) { + Assert.assertTrue(tree.delete(tree.getNotSmaller(new Integer(arrays[i][1][j])).getElement())); + Assert.assertEquals(arrays[i][0].length - j - 1, tree.size()); + } + } + } + + @Test + public void testNavigation() { + int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + AVLTree tree = buildTree(array); + + AVLTree.Node node = tree.getSmallest(); + Assert.assertEquals(array[0], value(node)); + for (int i = 0; i < array.length; ++i) { + Assert.assertEquals(array[i], value(node)); + node = node.getNext(); + } + Assert.assertNull(node); + + node = tree.getLargest(); + Assert.assertEquals(array[array.length - 1], value(node)); + for (int i = array.length - 1; i >= 0; --i) { + Assert.assertEquals(array[i], value(node)); + node = node.getPrevious(); + } + Assert.assertNull(node); + + checkOrder(tree); + + } + + @Test + public void testSearch() { + int[] array = { 2, 4, 6, 8, 10, 12, 14 }; + AVLTree tree = buildTree(array); + + Assert.assertNull(tree.getNotLarger(new Integer(array[0] - 1))); + Assert.assertNull(tree.getNotSmaller(new Integer(array[array.length - 1] + 1))); + + for (int i = 0; i < array.length; ++i) { + Assert.assertEquals(array[i], + value(tree.getNotSmaller(new Integer(array[i] - 1)))); + Assert.assertEquals(array[i], + value(tree.getNotLarger(new Integer(array[i] + 1)))); + } + + checkOrder(tree); + + } + + @Test + public void testRepetition() { + int[] array = { 1, 1, 3, 3, 4, 5, 6, 7, 7, 7, 7, 7 }; + AVLTree tree = buildTree(array); + Assert.assertEquals(array.length, tree.size()); + + AVLTree.Node node = tree.getNotSmaller(new Integer(3)); + Assert.assertEquals(3, value(node)); + Assert.assertEquals(1, value(node.getPrevious())); + Assert.assertEquals(3, value(node.getNext())); + Assert.assertEquals(4, value(node.getNext().getNext())); + + node = tree.getNotLarger(new Integer(2)); + Assert.assertEquals(1, value(node)); + Assert.assertEquals(1, value(node.getPrevious())); + Assert.assertEquals(3, value(node.getNext())); + Assert.assertNull(node.getPrevious().getPrevious()); + + AVLTree.Node otherNode = tree.getNotSmaller(new Integer(1)); + Assert.assertTrue(node != otherNode); + Assert.assertEquals(1, value(otherNode)); + Assert.assertNull(otherNode.getPrevious()); + + node = tree.getNotLarger(new Integer(10)); + Assert.assertEquals(7, value(node)); + Assert.assertNull(node.getNext()); + node = node.getPrevious(); + Assert.assertEquals(7, value(node)); + node = node.getPrevious(); + Assert.assertEquals(7, value(node)); + node = node.getPrevious(); + Assert.assertEquals(7, value(node)); + node = node.getPrevious(); + Assert.assertEquals(7, value(node)); + node = node.getPrevious(); + Assert.assertEquals(6, value(node)); + + checkOrder(tree); + + } + + private AVLTree buildTree(int[] array) { + AVLTree tree = new AVLTree(); + for (int i = 0; i < array.length; ++i) { + tree.insert(new Integer(array[i])); + tree.insert(null); + } + return tree; + } + + private int value(AVLTree.Node node) { + return ((Integer) node.getElement()).intValue(); + } + + private void checkOrder(AVLTree tree) { + AVLTree.Node next = null; + for (AVLTree.Node node = tree.getSmallest(); + node != null; + node = next) { + next = node.getNext(); + if (next != null) { + Assert.assertTrue(node.getElement().compareTo(next.getElement()) <= 0); + } + } + } + +}