/*
* Copyright (c) 2016 Fraunhofer IGD
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Fraunhofer IGD <http://www.igd.fraunhofer.de/>
*/
package de.fhg.igd.geom;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import de.fhg.igd.geom.util.BlochHashCode;
import de.fhg.igd.geom.util.MathHelper;
/**
* This class represents a Bounding Box in 3D, as defined by the lower left
* minimum height and the upper right maximum height corners.
*
* @author Thorsten Reitz
*/
public final class BoundingBox implements Serializable, Localizable, Cloneable {
/**
* The class' serial version UID
*/
private static final long serialVersionUID = 713293043787375488L;
/**
* The lower left bottom corner's x value.
*/
private double minX;
/**
* The lower left bottom corner's x value.
*/
private double minY;
/**
* The lower left bottom corner's z value.
*/
private double minZ;
/**
* The upper right top corner's x value.
*/
private double maxX;
/**
* The upper right top corner's y value.
*/
private double maxY;
/**
* The upper right top corner's z value.
*/
private double maxZ;
/**
* Default constructor
*/
public BoundingBox() {
reset();
}
/**
* Construct a BB from two points defined by three double values.
*
* @param x1 - xmin
* @param y1 - ymin
* @param z1 - zmin
* @param x2 - xmax
* @param y2 - ymax
* @param z2 - zmax
*/
public BoundingBox(double x1, double y1, double z1, double x2, double y2, double z2) {
this.minX = x1;
this.minY = y1;
this.minZ = z1;
this.maxX = x2;
this.maxY = y2;
this.maxZ = z2;
}
/**
* Copy constructor
*
* @param boundingBox The source bounding box
*/
public BoundingBox(BoundingBox boundingBox) {
this(boundingBox.minX, boundingBox.minY, boundingBox.minZ, boundingBox.maxX,
boundingBox.maxY, boundingBox.maxZ);
}
// functional methods
// .......................................................
/**
* Resets the bounding box
*/
public void reset() {
this.minX = Double.POSITIVE_INFINITY;
this.minY = Double.POSITIVE_INFINITY;
this.minZ = Double.POSITIVE_INFINITY;
this.maxX = Double.NEGATIVE_INFINITY;
this.maxY = Double.NEGATIVE_INFINITY;
this.maxZ = Double.NEGATIVE_INFINITY;
}
/**
* @see Localizable#getBoundingBox()
*/
@Override
public BoundingBox getBoundingBox() {
return this;
}
/**
* This method expands this BoundingBox in volume by factor stepsize. If the
* original extent is 0.0, stepsize will be used as absolute value.
*
* @param stepsize the size factor
* @return the extended BoundingBox (this)
* @see Localizable#getBoundingBox()
*/
public BoundingBox expand(double stepsize) {
double delta_x = this.maxX - this.minX;
double delta_y = this.maxY - this.minY;
double delta_z = this.maxZ - this.minZ;
if (delta_x == 0.0) {
delta_x = stepsize;
}
else {
delta_x = delta_x * (1 + stepsize);
}
if (delta_y == 0.0) {
delta_y = stepsize;
}
else {
delta_y = delta_y * (1 + stepsize);
}
if (delta_z == 0.0) {
delta_z = stepsize;
}
else {
delta_z = delta_z * (1 + stepsize);
}
double min_temp = (this.maxX + this.minX) / 2 - delta_x / 2;
this.maxX = (this.maxX + this.minX) / 2 + delta_x / 2;
this.minX = min_temp;
min_temp = (this.maxY + this.minY) / 2 - delta_y / 2;
this.maxY = (this.maxY + this.minY) / 2 + delta_y / 2;
this.minY = min_temp;
min_temp = (this.maxZ + this.minZ) / 2 - delta_z / 2;
this.maxZ = (this.maxZ + this.minZ) / 2 + delta_z / 2;
this.minZ = min_temp;
return this;
}
/**
* This method will add the given Bounding Box to this Bounding Box, thus
* possibly enlarging it.
*
* @param bbox the BoundingBox to add
*/
public void add(BoundingBox bbox) {
if (bbox == null) {
return;
}
Point3D center = bbox.getCenter();
if (!(Double.isNaN(center.getX()) || Double.isNaN(center.getY())
|| Double.isNaN(center.getZ()))) {
if (bbox.covers(this)) {
this.setMinX(bbox.getMinX());
this.setMaxX(bbox.getMaxX());
this.setMinY(bbox.getMinY());
this.setMaxY(bbox.getMaxY());
this.setMinZ(bbox.getMinZ());
this.setMaxZ(bbox.getMaxZ());
}
else if (this.covers(bbox)) {
// do nothing
}
else {
if (this.minX > bbox.minX) {
this.minX = bbox.minX;
}
if (this.minY > bbox.minY) {
this.minY = bbox.minY;
}
if (this.minZ > bbox.minZ) {
this.minZ = bbox.minZ;
}
if (this.maxX < bbox.maxX) {
this.maxX = bbox.maxX;
}
if (this.maxY < bbox.maxY) {
this.maxY = bbox.maxY;
}
if (this.maxZ < bbox.maxZ) {
this.maxZ = bbox.maxZ;
}
}
}
}
/**
* @return the center of this BB as a Point3D.
*/
public Point3D getCenter() {
return new Point3D((this.maxX + this.minX) / 2, (this.maxY + this.minY) / 2,
(this.maxZ + this.minZ) / 2);
}
/**
* @param bb the BoundingBox that may have any relation to this one
* @return true if the given BoundingBox has any spatial relation to this
* one.
*/
public boolean any(BoundingBox bb) {
return (this.intersects(bb) || this.covers(bb) || bb.covers(this) || bb.equals(this)
|| bb.touches(this));
}
/**
* This checks if this Bounding Box completely covers the parameter Bounding
* Box.
*
* @param bbox the other BoundingBox
* @return true if this Box covers bbox, false otherwise
*/
public boolean covers(BoundingBox bbox) {
return (this.getMinX() <= bbox.getMinX() && this.getMaxX() >= bbox.getMaxX()
&& this.getMinY() <= bbox.getMinY() && this.getMaxY() >= bbox.getMaxY()
&& this.getMinZ() <= bbox.getMinZ() && this.getMaxZ() >= bbox.getMaxZ());
}
/**
* This checks if this Bounding Box completely contains the parameter
* Bounding Box.
*
* @param bbox the other BoundingBox
* @return true if this Box contains bbox, false otherwise
*/
private boolean contains(BoundingBox bbox) {
return (this.getMinX() < bbox.getMinX() && this.getMaxX() > bbox.getMaxX()
&& this.getMinY() < bbox.getMinY() && this.getMaxY() > bbox.getMaxY()
&& this.getMinZ() < bbox.getMinZ() && this.getMaxZ() > bbox.getMaxZ());
}
/**
* @param point a point that may touch this BoundingBox
* @return true if the given Point touches the BoundingBox
*/
private boolean touches(Point3D point) {
// We keep the "unsafe" FP comparison since we prefer a proper touches()
// over
// something which cannot hold basic topological properties. That
// touches isn't the most stable criteria is another thing.
if (point.getX() == this.minX || point.getX() == this.maxX) {
if (point.getY() <= this.maxY && point.getY() >= this.minY && point.getZ() <= this.maxZ
&& point.getZ() >= this.minZ) {
return true;
}
}
if (point.getY() == this.minY || point.getY() == this.maxY) {
if (point.getX() <= this.maxX && point.getX() >= this.minX && point.getZ() <= this.maxZ
&& point.getZ() >= this.minZ) {
return true;
}
}
if (point.getZ() == this.minZ || point.getZ() == this.maxZ) {
if (point.getX() <= this.maxX && point.getX() >= this.minX && point.getY() <= this.maxY
&& point.getY() >= this.minY) {
return true;
}
}
return false;
}
/**
* @param bb the BoundingBox that may touch this one
* @return true if the given BoundingBox has corners on an edge of this one
*/
private boolean touchesHelper(BoundingBox bb) {
int count = 0;
if (this.touches(new Point3D(bb.minX, bb.minY, bb.minZ))) {
count++;
}
if (this.touches(new Point3D(bb.minX, bb.minY, bb.maxZ))) {
count++;
}
if (this.touches(new Point3D(bb.minX, bb.maxY, bb.minZ))) {
count++;
}
if (this.touches(new Point3D(bb.minX, bb.maxY, bb.maxZ))) {
count++;
}
if (this.touches(new Point3D(bb.maxX, bb.minY, bb.minZ))) {
count++;
}
if (this.touches(new Point3D(bb.maxX, bb.minY, bb.maxZ))) {
count++;
}
if (this.touches(new Point3D(bb.maxX, bb.maxY, bb.minZ))) {
count++;
}
if (this.touches(new Point3D(bb.maxX, bb.maxY, bb.maxZ))) {
count++;
}
return count > 0 && count < 5;
}
/**
* @param bb the BoundingBox that may touch this one
* @return true if the given BoundingBox touches this one
*/
private boolean touches(BoundingBox bb) {
return (touchesHelper(bb) || bb.touchesHelper(this)) && !this.intersects(bb)
&& !this.covers(bb) && !bb.covers(this);
}
/**
* Test emptiness, i.e. whether at least one axis has zero width and the box
* is regular.
*
* @return whether the bounding box is empty.
*/
public boolean isEmpty() {
return (getWidth() == 0 || getHeight() == 0 || getDepth() == 0) && isRealValued();
}
/**
* Test regularity, i.e. whether all axes have zero or positive length.
*
* @return whether the bounding box is regular.
* @see #checkIntegrity()
*/
private boolean isRegular() {
return getWidth() >= 0 && getHeight() >= 0 && getDepth() >= 0;
}
/**
* This checks if this Bounding Box intersects with another Bounding Box.
*
* @param bb the other BoundingBox
* @return true if the Boxes intersect, false otherwise
*/
public boolean intersects(BoundingBox bb) {
if (this.getMinX() >= bb.getMaxX() || this.getMaxX() <= bb.getMinX()) {
return false;
}
else if (this.getMinY() >= bb.getMaxY() || this.getMaxY() <= bb.getMinY()) {
return false;
}
else if (this.getMinZ() >= bb.getMaxZ() || this.getMaxZ() <= bb.getMinZ()) {
return false;
}
return (!this.contains(bb) && !this.equals(bb));
}
/**
* Checks if this BoundingBox intersects with or covers the given
* BoundingBox bb
*
* @param bb the BoundingBox to check against
* @return true if this BoundingBox intersects with or covers bb, false
* otherwise
*/
public boolean intersectsOrCovers(BoundingBox bb) {
return (this.intersects(bb) || this.covers(bb));
}
/**
* This method is used internally to add a point to a given extent
*
* @param x the x ordinate of the point to add
* @param y the y ordinate
* @param result an array containing the current extent. This array will be
* updated by this method. The structure is as follows:<br />
* result[0] - maximum x<br />
* result[1] - minimum x<br />
* result[2] - maximum y<br />
* result[3] - minimum y
*/
private static void computeInternal(double x, double y, double[] result) {
if (x > result[0]) {
result[0] = x;
}
if (x < result[1]) {
result[1] = x;
}
if (y > result[2]) {
result[2] = y;
}
if (y < result[3]) {
result[3] = y;
}
}
/**
* This method is used internally to add a point to a given bounding box
*
* @param x the x ordinate of the point to add
* @param y the y ordinate
* @param z the z ordinate
* @param result an array containing the current bounding box. This array
* will be updated by this method. The structure is as follows:
* <br />
* result[0] - maximum x<br />
* result[1] - minimum x<br />
* result[2] - maximum y<br />
* result[3] - minimum y<br />
* result[4] - maximum z<br />
* result[5] - minimum z<br />
*/
private static void computeInternal(double x, double y, double z, double[] result) {
computeInternal(x, y, result);
if (z > result[4]) {
result[4] = z;
}
if (z < result[5]) {
result[5] = z;
}
}
/**
* This static method will return a BoundingBox for a given Array of
* Localizables.
*
* @param locs the Localizables to add to the BoundingBox
* @return a BoundingBox that contains all Localizables
*/
public static BoundingBox compute(Localizable[] locs) {
return computeLocalizable(Arrays.asList(locs));
}
/**
* This static method will return a BoundingBox for a given collection of
* Point2D objects
*
* @param points a collection with Point3D objects
* @return a BoundingBox containing all points
*/
public static BoundingBox computePoint2D(Collection<Point2D> points) {
double[] result = new double[4];
result[0] = Double.NEGATIVE_INFINITY;
result[1] = Double.POSITIVE_INFINITY;
result[2] = Double.NEGATIVE_INFINITY;
result[3] = Double.POSITIVE_INFINITY;
for (Point2D p2d : points) {
computeInternal(p2d.getX(), p2d.getY(), result);
}
BoundingBox bb = new BoundingBox();
bb.setMaxX(result[0]);
bb.setMinX(result[1]);
bb.setMaxY(result[2]);
bb.setMinY(result[3]);
bb.setMinZ(0.0);
bb.setMaxZ(0.0);
return bb;
}
/**
* This static method will return a BoundingBox for a given collection of
* Point3D objects
*
* @param points a collection with Point3D objects
* @return a BoundingBox containing all points
*/
private static BoundingBox computePoint3D(Collection<Point3D> points) {
double[] result = new double[6];
result[0] = Double.NEGATIVE_INFINITY;
result[1] = Double.POSITIVE_INFINITY;
result[2] = Double.NEGATIVE_INFINITY;
result[3] = Double.POSITIVE_INFINITY;
result[4] = Double.NEGATIVE_INFINITY;
result[5] = Double.POSITIVE_INFINITY;
for (Point3D p3d : points) {
computeInternal(p3d.getX(), p3d.getY(), p3d.getZ(), result);
}
BoundingBox bb = new BoundingBox();
bb.setMaxX(result[0]);
bb.setMinX(result[1]);
bb.setMaxY(result[2]);
bb.setMinY(result[3]);
bb.setMaxZ(result[4]);
bb.setMinZ(result[5]);
return bb;
}
/**
* This static method will return a BoundingBox for a given collection of
* Localizable objects
*
* @param locs a collection of Localizable objects
* @return a BoundingBox containing all Localizables
*/
private static BoundingBox computeLocalizable(Iterable<? extends Localizable> locs) {
double[] result = new double[6];
result[0] = Double.NEGATIVE_INFINITY;
result[1] = Double.POSITIVE_INFINITY;
result[2] = Double.NEGATIVE_INFINITY;
result[3] = Double.POSITIVE_INFINITY;
result[4] = Double.NEGATIVE_INFINITY;
result[5] = Double.POSITIVE_INFINITY;
for (Localizable loc : locs) {
BoundingBox locbb = loc.getBoundingBox();
computeInternal(locbb.minX, locbb.minY, locbb.minZ, result);
computeInternal(locbb.maxX, locbb.maxY, locbb.maxZ, result);
}
BoundingBox bb = new BoundingBox();
bb.setMaxX(result[0]);
bb.setMinX(result[1]);
bb.setMaxY(result[2]);
bb.setMinY(result[3]);
bb.setMaxZ(result[4]);
bb.setMinZ(result[5]);
return bb;
}
/**
* This static method will return a BoundingBox for a given Array of Point3D
* objects.
*
* @param points the array of points
* @return a BoundingBox containing all points
*/
public static BoundingBox compute(Point3D[] points) {
return computePoint3D(Arrays.asList(points));
}
/**
* This static method will return a BoundingBox for a given Array of Point2D
* objects.
*
* @param points the array of points
* @return a BoundingBox containing all points
*/
public static BoundingBox compute(Point2D[] points) {
return computePoint2D(Arrays.asList(points));
}
/**
* This static method will return a BoundingBox for a given Array of
* {@link Coordinate}s objects.
*
* @param points the array of coordinates
* @return a BoundingBox containing all points
*/
public static BoundingBox compute2D(Coordinate[] points) {
Double maxX = null, maxY = null, minX = null, minY = null;
for (Coordinate point : points) {
// maximum x ordinate
if (maxX == null) {
maxX = point.x;
}
else {
maxX = Math.max(maxX, point.x);
}
// maximum y ordinate
if (maxY == null) {
maxY = point.y;
}
else {
maxY = Math.max(maxY, point.y);
}
// minimum x ordinate
if (minX == null) {
minX = point.x;
}
else {
minX = Math.min(minX, point.x);
}
// minimum y ordinate
if (minY == null) {
minY = point.y;
}
else {
minY = Math.min(minY, point.y);
}
}
if (maxX != null && maxY != null && minX != null && minY != null) {
return new BoundingBox(minX, minY, 0, maxX, maxY, 0);
}
else {
return null;
}
}
/**
* checks if min* and max* are actually in the expected relation. If a pair
* is real and in the wrong order, it is swapped.
*/
public void normalize() {
if (MathHelper.isReal(minX) && MathHelper.isReal(maxX) && maxX < minX) {
double tmp = minX;
minX = maxX;
maxX = tmp;
}
if (MathHelper.isReal(minY) && MathHelper.isReal(maxY) && maxY < minY) {
double tmp = minY;
minY = maxY;
maxY = tmp;
}
if (MathHelper.isReal(minZ) && MathHelper.isReal(maxZ) && maxZ < minZ) {
double tmp = minZ;
minZ = maxZ;
maxZ = tmp;
}
}
/**
* This method will return a 2D Extent that uses only the X and Y
* coordinates of this BoundingBox.
*
* @return Extent
*/
public Extent toExtent() {
Extent result = new Extent();
result.setMinX(this.minX);
result.setMinY(this.minY);
result.setMaxX(this.maxX);
result.setMaxY(this.maxY);
return result;
}
/**
* @return this Bounding Box' Width (along x axis).
*/
public double getWidth() {
return this.maxX - this.minX;
}
/**
* @return this Bounding Box' Height (along y axis).
*/
public double getHeight() {
return this.maxY - this.minY;
}
/**
* @return this Bounding Box' Depth (along z axis).
*/
public double getDepth() {
return this.maxZ - this.minZ;
}
// canonical java methods
// ...................................................
/**
* Two BoundingBoxes are defined as being equal when their LLB and URT
* coordinates are equal.
*
* @see Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o != null && o instanceof BoundingBox) {
BoundingBox other = (BoundingBox) o;
if (this.getMinX() == other.getMinX() && this.getMinY() == other.getMinY()
&& this.getMinZ() == other.getMinZ() && this.getMaxX() == other.getMaxX()
&& this.getMaxY() == other.getMaxY() && this.getMaxZ() == other.getMaxZ()) {
return true;
}
}
return false;
}
/**
* Provides a hashCode so that x.hashCode() == y.hashCode() when x.equals(y)
* == true
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int hash = BlochHashCode.addFieldToHash(BlochHashCode.HASH_CONSTANT, this.getMinX());
hash = BlochHashCode.addFieldToHash(hash, this.getMinY());
hash = BlochHashCode.addFieldToHash(hash, this.getMinZ());
hash = BlochHashCode.addFieldToHash(hash, this.getMaxX());
hash = BlochHashCode.addFieldToHash(hash, this.getMaxY());
return BlochHashCode.addFieldToHash(hash, this.getMaxZ());
}
/**
* Standard issue toString method.
*
* @see Object#toString()
*/
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append("BoundingBox[");
buffer.append("maxX = ").append(maxX);
buffer.append(" maxY = ").append(maxY);
buffer.append(" maxZ = ").append(maxZ);
buffer.append(" minX = ").append(minX);
buffer.append(" minY = ").append(minY);
buffer.append(" minZ = ").append(minZ);
buffer.append("]");
return buffer.toString();
}
/**
* @return true if all limits of this bb have been initialized in a
* meaningful way (i.e. not NaN, Infinity or NegativeInfinty) and is
* regular.
* @see #isRegular()
*/
public boolean checkIntegrity() {
return isRealValued() && isRegular();
}
/**
* @return true if all limits of this bb have been initialized in a
* meaningful way (i.e. not NaN, Infinity or NegativeInfinty)
*/
private boolean isRealValued() {
double[] values = new double[] { minX, minY, minZ, maxX, maxY, maxZ };
boolean result = true;
for (int i = 0; i < values.length && result == true; i++) {
if (Double.isNaN(values[i]) || Double.isInfinite(values[i])) {
result = false;
}
}
return result;
}
// getter / setter methods
// ..................................................
/**
* Access method for the minX property.
*
* @return the current value of the minX property
*/
public double getMinX() {
return minX;
}
/**
* Sets the value of the minX property.
*
* @param aMinX the new value of the minX property
*/
public void setMinX(double aMinX) {
minX = aMinX;
}
/**
* Access method for the minY property.
*
* @return the current value of the minY property
*/
public double getMinY() {
return minY;
}
/**
* Sets the value of the minY property.
*
* @param aMinY the new value of the minY property
*/
public void setMinY(double aMinY) {
minY = aMinY;
}
/**
* Access method for the minZ property.
*
* @return the current value of the minZ property
*/
public double getMinZ() {
return minZ;
}
/**
* Sets the value of the minZ property.
*
* @param aMinZ the new value of the minZ property
*/
public void setMinZ(double aMinZ) {
minZ = aMinZ;
}
/**
* Access method for the maxX property.
*
* @return the current value of the maxX property
*/
public double getMaxX() {
return maxX;
}
/**
* Sets the value of the maxX property.
*
* @param aMaxX the new value of the maxX property
*/
public void setMaxX(double aMaxX) {
maxX = aMaxX;
}
/**
* Access method for the maxY property.
*
* @return the current value of the maxY property
*/
public double getMaxY() {
return maxY;
}
/**
* Sets the value of the maxY property.
*
* @param aMaxY the new value of the maxY property
*/
public void setMaxY(double aMaxY) {
maxY = aMaxY;
}
/**
* Access method for the maxZ property.
*
* @return the current value of the maxZ property
*/
public double getMaxZ() {
return maxZ;
}
/**
* Sets the value of the maxZ property.
*
* @param aMaxZ the new value of the maxZ property
*/
public void setMaxZ(double aMaxZ) {
maxZ = aMaxZ;
}
/**
* @see Object#clone()
*/
@Override
public Object clone() {
BoundingBox boundingBox = new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ);
return boundingBox;
}
/**
* Determine the bounding box for a geometry.
*
* @param geometry the geometry
* @return the bounding box or <code>null</code> if it is either an empty
* geometry or the bounding box cannot be determined
*/
public static BoundingBox compute(Geometry geometry) {
Geometry envelope = geometry.getEnvelope();
if (envelope instanceof Point) {
Point point = (Point) envelope;
if (!point.isEmpty()) { // not an empty geometry
// a bounding box representing the point
return new BoundingBox(point.getX(), point.getY(), 0, point.getX(), point.getY(),
0);
}
}
else if (envelope instanceof LineString) {
LineString line = (LineString) envelope;
if (!line.isEmpty()) {
return compute2D(line.getCoordinates());
}
}
else if (envelope instanceof Polygon) {
Polygon rect = (Polygon) envelope;
if (!rect.isEmpty()) {
return compute2D(rect.getCoordinates());
}
}
return null;
}
}