blob: d3047c81b45248370713076735af8536ff2bae0b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.core.partitioning;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeSet;
import org.apache.commons.geometry.core.Point;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
/** Abstract class for all regions, independent of geometry type or dimension.
* @param <P> Point type defining the space
* @param <S> Point type defining the sub-space
*/
public abstract class AbstractRegion<P extends Point<P>, S extends Point<S>> implements Region<P> {
/** Inside/Outside BSP tree. */
private BSPTree<P> tree;
/** Precision context used to determine floating point equality. */
private final DoublePrecisionContext precision;
/** Size of the instance. */
private double size;
/** Barycenter. */
private P barycenter;
/** Build a region representing the whole space.
* @param precision precision context used to compare floating point numbers
*/
protected AbstractRegion(final DoublePrecisionContext precision) {
this.tree = new BSPTree<>(Boolean.TRUE);
this.precision = precision;
}
/** Build a region from an inside/outside BSP tree.
* <p>The leaf nodes of the BSP tree <em>must</em> have a
* {@code Boolean} attribute representing the inside status of
* the corresponding cell (true for inside cells, false for outside
* cells). In order to avoid building too many small objects, it is
* recommended to use the predefined constants
* {@code Boolean.TRUE} and {@code Boolean.FALSE}. The
* tree also <em>must</em> have either null internal nodes or
* internal nodes representing the boundary as specified in the
* {@link #getTree getTree} method).</p>
* @param tree inside/outside BSP tree representing the region
* @param precision precision context used to compare floating point values
*/
protected AbstractRegion(final BSPTree<P> tree, final DoublePrecisionContext precision) {
this.tree = tree;
this.precision = precision;
}
/** Build a Region from a Boundary REPresentation (B-rep).
* <p>The boundary is provided as a collection of {@link
* SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
* interior part of the region on its minus side and the exterior on
* its plus side.</p>
* <p>The boundary elements can be in any order, and can form
* several non-connected sets (like for example polygons with holes
* or a set of disjoints polyhedrons considered as a whole). In
* fact, the elements do not even need to be connected together
* (their topological connections are not used here). However, if the
* boundary does not really separate an inside open from an outside
* open (open having here its topological meaning), then subsequent
* calls to the {@link #checkPoint(Point) checkPoint} method will not be
* meaningful anymore.</p>
* <p>If the boundary is empty, the region will represent the whole
* space.</p>
* @param boundary collection of boundary elements, as a
* collection of {@link SubHyperplane SubHyperplane} objects
* @param precision precision context used to compare floating point values
*/
protected AbstractRegion(final Collection<SubHyperplane<P>> boundary, final DoublePrecisionContext precision) {
this.precision = precision;
if (boundary.size() == 0) {
// the tree represents the whole space
tree = new BSPTree<>(Boolean.TRUE);
} else {
// sort the boundary elements in decreasing size order
// (we don't want equal size elements to be removed, so
// we use a trick to fool the TreeSet)
final TreeSet<SubHyperplane<P>> ordered = new TreeSet<>(new Comparator<SubHyperplane<P>>() {
/** {@inheritDoc} */
@Override
public int compare(final SubHyperplane<P> o1, final SubHyperplane<P> o2) {
final double size1 = o1.getSize();
final double size2 = o2.getSize();
return (size2 < size1) ? -1 : ((o1 == o2) ? 0 : +1);
}
});
ordered.addAll(boundary);
// build the tree top-down
tree = new BSPTree<>();
insertCuts(tree, ordered);
// set up the inside/outside flags
tree.visit(new BSPTreeVisitor<P>() {
/** {@inheritDoc} */
@Override
public Order visitOrder(final BSPTree<P> node) {
return Order.PLUS_SUB_MINUS;
}
/** {@inheritDoc} */
@Override
public void visitInternalNode(final BSPTree<P> node) {
}
/** {@inheritDoc} */
@Override
public void visitLeafNode(final BSPTree<P> node) {
if (node.getParent() == null || node == node.getParent().getMinus()) {
node.setAttribute(Boolean.TRUE);
} else {
node.setAttribute(Boolean.FALSE);
}
}
});
}
}
/** Build a convex region from an array of bounding hyperplanes.
* @param hyperplanes array of bounding hyperplanes (if null, an
* empty region will be built)
* @param precision precision context used to compare floating point values
*/
public AbstractRegion(final Hyperplane<P>[] hyperplanes, final DoublePrecisionContext precision) {
this.precision = precision;
if ((hyperplanes == null) || (hyperplanes.length == 0)) {
tree = new BSPTree<>(Boolean.FALSE);
} else {
// use the first hyperplane to build the right class
tree = hyperplanes[0].wholeSpace().getTree(false);
// chop off parts of the space
BSPTree<P> node = tree;
node.setAttribute(Boolean.TRUE);
for (final Hyperplane<P> hyperplane : hyperplanes) {
if (node.insertCut(hyperplane)) {
node.setAttribute(null);
node.getPlus().setAttribute(Boolean.FALSE);
node = node.getMinus();
node.setAttribute(Boolean.TRUE);
}
}
}
}
/** {@inheritDoc} */
@Override
public abstract AbstractRegion<P, S> buildNew(BSPTree<P> newTree);
/** Get the object used to determine floating point equality for this region.
* @return the floating point precision context for the instance
*/
public DoublePrecisionContext getPrecision() {
return precision;
}
/** Recursively build a tree by inserting cut sub-hyperplanes.
* @param node current tree node (it is a leaf node at the beginning
* of the call)
* @param boundary collection of edges belonging to the cell defined
* by the node
*/
private void insertCuts(final BSPTree<P> node, final Collection<SubHyperplane<P>> boundary) {
final Iterator<SubHyperplane<P>> iterator = boundary.iterator();
// build the current level
Hyperplane<P> inserted = null;
while ((inserted == null) && iterator.hasNext()) {
inserted = iterator.next().getHyperplane();
if (!node.insertCut(inserted.copySelf())) {
inserted = null;
}
}
if (!iterator.hasNext()) {
return;
}
// distribute the remaining edges in the two sub-trees
final ArrayList<SubHyperplane<P>> plusList = new ArrayList<>();
final ArrayList<SubHyperplane<P>> minusList = new ArrayList<>();
while (iterator.hasNext()) {
final SubHyperplane<P> other = iterator.next();
final SubHyperplane.SplitSubHyperplane<P> split = other.split(inserted);
switch (split.getSide()) {
case PLUS:
plusList.add(other);
break;
case MINUS:
minusList.add(other);
break;
case BOTH:
plusList.add(split.getPlus());
minusList.add(split.getMinus());
break;
default:
// ignore the sub-hyperplanes belonging to the cut hyperplane
}
}
// recurse through lower levels
insertCuts(node.getPlus(), plusList);
insertCuts(node.getMinus(), minusList);
}
/** {@inheritDoc} */
@Override
public AbstractRegion<P, S> copySelf() {
return buildNew(tree.copySelf());
}
/** {@inheritDoc} */
@Override
public boolean isEmpty() {
return isEmpty(tree);
}
/** {@inheritDoc} */
@Override
public boolean isEmpty(final BSPTree<P> node) {
// we use a recursive function rather than the BSPTreeVisitor
// interface because we can stop visiting the tree as soon as we
// have found an inside cell
if (node.getCut() == null) {
// if we find an inside node, the region is not empty
return !((Boolean) node.getAttribute());
}
// check both sides of the sub-tree
return isEmpty(node.getMinus()) && isEmpty(node.getPlus());
}
/** {@inheritDoc} */
@Override
public boolean isFull() {
return isFull(tree);
}
/** {@inheritDoc} */
@Override
public boolean isFull(final BSPTree<P> node) {
// we use a recursive function rather than the BSPTreeVisitor
// interface because we can stop visiting the tree as soon as we
// have found an outside cell
if (node.getCut() == null) {
// if we find an outside node, the region does not cover full space
return (Boolean) node.getAttribute();
}
// check both sides of the sub-tree
return isFull(node.getMinus()) && isFull(node.getPlus());
}
/** {@inheritDoc} */
@Override
public boolean contains(final Region<P> region) {
return new RegionFactory<P>().difference(region, this).isEmpty();
}
/** {@inheritDoc}
*/
@Override
public BoundaryProjection<P> projectToBoundary(final P point) {
final BoundaryProjector<P, S> projector = new BoundaryProjector<>(point);
getTree(true).visit(projector);
return projector.getProjection();
}
/** {@inheritDoc} */
@Override
public Location checkPoint(final P point) {
return checkPoint(tree, point);
}
/** Check a point with respect to the region starting at a given node.
* @param node root node of the region
* @param point point to check
* @return a code representing the point status: either {@link
* Region.Location#INSIDE INSIDE}, {@link Region.Location#OUTSIDE
* OUTSIDE} or {@link Region.Location#BOUNDARY BOUNDARY}
*/
protected Location checkPoint(final BSPTree<P> node, final P point) {
final BSPTree<P> cell = node.getCell(point, precision);
if (cell.getCut() == null) {
// the point is in the interior of a cell, just check the attribute
return ((Boolean) cell.getAttribute()) ? Location.INSIDE : Location.OUTSIDE;
}
// the point is on a cut-sub-hyperplane, is it on a boundary ?
final Location minusCode = checkPoint(cell.getMinus(), point);
final Location plusCode = checkPoint(cell.getPlus(), point);
return (minusCode == plusCode) ? minusCode : Location.BOUNDARY;
}
/** {@inheritDoc} */
@Override
public BSPTree<P> getTree(final boolean includeBoundaryAttributes) {
if (includeBoundaryAttributes && (tree.getCut() != null) && (tree.getAttribute() == null)) {
// compute the boundary attributes
tree.visit(new BoundaryBuilder<P>());
}
return tree;
}
/** {@inheritDoc} */
@Override
public double getBoundarySize() {
final BoundarySizeVisitor<P> visitor = new BoundarySizeVisitor<>();
getTree(true).visit(visitor);
return visitor.getSize();
}
/** {@inheritDoc} */
@Override
public double getSize() {
if (barycenter == null) {
computeGeometricalProperties();
}
return size;
}
/** Set the size of the instance.
* @param size size of the instance
*/
protected void setSize(final double size) {
this.size = size;
}
/** {@inheritDoc} */
@Override
public P getBarycenter() {
if (barycenter == null) {
computeGeometricalProperties();
}
return barycenter;
}
/** Set the barycenter of the instance.
* @param barycenter barycenter of the instance
*/
protected void setBarycenter(final P barycenter) {
this.barycenter = barycenter;
}
/** Compute some geometrical properties.
* <p>The properties to compute are the barycenter and the size.</p>
*/
protected abstract void computeGeometricalProperties();
/** {@inheritDoc} */
@Override
public SubHyperplane<P> intersection(final SubHyperplane<P> sub) {
return recurseIntersection(tree, sub);
}
/** Recursively compute the parts of a sub-hyperplane that are
* contained in the region.
* @param node current BSP tree node
* @param sub sub-hyperplane traversing the region
* @return filtered sub-hyperplane
*/
private SubHyperplane<P> recurseIntersection(final BSPTree<P> node, final SubHyperplane<P> sub) {
if (node.getCut() == null) {
return (Boolean) node.getAttribute() ? sub.copySelf() : null;
}
final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
final SubHyperplane.SplitSubHyperplane<P> split = sub.split(hyperplane);
if (split.getPlus() != null) {
if (split.getMinus() != null) {
// both sides
final SubHyperplane<P> plus = recurseIntersection(node.getPlus(), split.getPlus());
final SubHyperplane<P> minus = recurseIntersection(node.getMinus(), split.getMinus());
if (plus == null) {
return minus;
} else if (minus == null) {
return plus;
} else {
return plus.reunite(minus);
}
} else {
// only on plus side
return recurseIntersection(node.getPlus(), sub);
}
} else if (split.getMinus() != null) {
// only on minus side
return recurseIntersection(node.getMinus(), sub);
} else {
// on hyperplane
return recurseIntersection(node.getPlus(),
recurseIntersection(node.getMinus(), sub));
}
}
/** Transform a region.
* <p>Applying a transform to a region consist in applying the
* transform to all the hyperplanes of the underlying BSP tree and
* of the boundary (and also to the sub-hyperplanes embedded in
* these hyperplanes) and to the barycenter. The instance is not
* modified, a new instance is built.</p>
* @param transform transform to apply
* @return a new region, resulting from the application of the
* transform to the instance
*/
public AbstractRegion<P, S> applyTransform(final Transform<P, S> transform) {
// transform the tree, except for boundary attribute splitters
final Map<BSPTree<P>, BSPTree<P>> map = new HashMap<>();
final BSPTree<P> transformedTree = recurseTransform(getTree(false), transform, map);
// set up the boundary attributes splitters
for (final Map.Entry<BSPTree<P>, BSPTree<P>> entry : map.entrySet()) {
if (entry.getKey().getCut() != null) {
@SuppressWarnings("unchecked")
BoundaryAttribute<P> original = (BoundaryAttribute<P>) entry.getKey().getAttribute();
if (original != null) {
@SuppressWarnings("unchecked")
BoundaryAttribute<P> transformed = (BoundaryAttribute<P>) entry.getValue().getAttribute();
for (final BSPTree<P> splitter : original.getSplitters()) {
transformed.getSplitters().add(map.get(splitter));
}
}
}
}
return buildNew(transformedTree);
}
/** Recursively transform an inside/outside BSP-tree.
* @param node current BSP tree node
* @param transform transform to apply
* @param map transformed nodes map
* @return a new tree
*/
@SuppressWarnings("unchecked")
private BSPTree<P> recurseTransform(final BSPTree<P> node, final Transform<P, S> transform,
final Map<BSPTree<P>, BSPTree<P>> map) {
final BSPTree<P> transformedNode;
if (node.getCut() == null) {
transformedNode = new BSPTree<>(node.getAttribute());
} else {
final SubHyperplane<P> sub = node.getCut();
final SubHyperplane<P> tSub = ((AbstractSubHyperplane<P, S>) sub).applyTransform(transform);
BoundaryAttribute<P> attribute = (BoundaryAttribute<P>) node.getAttribute();
if (attribute != null) {
final SubHyperplane<P> tPO = (attribute.getPlusOutside() == null) ?
null : ((AbstractSubHyperplane<P, S>) attribute.getPlusOutside()).applyTransform(transform);
final SubHyperplane<P> tPI = (attribute.getPlusInside() == null) ?
null : ((AbstractSubHyperplane<P, S>) attribute.getPlusInside()).applyTransform(transform);
// we start with an empty list of splitters, it will be filled in out of recursion
attribute = new BoundaryAttribute<>(tPO, tPI, new NodesSet<P>());
}
transformedNode = new BSPTree<>(tSub,
recurseTransform(node.getPlus(), transform, map),
recurseTransform(node.getMinus(), transform, map),
attribute);
}
map.put(node, transformedNode);
return transformedNode;
}
}