/*
* Copyright (C) 2014 Dr. John Lindsay <jlindsay@uoguelph.ca>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package whitebox.algorithms;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.algorithm.ConvexHull;
/**
*
* @author johnlindsay
*/
public class MinimumBoundingRectangle {
public enum MinimizationCriterion {
AREA, PERIMETER;
}
double[][] coordinates;
final double rightAngle = Math.PI / 2.0;
GeometryFactory factory = new GeometryFactory();
boolean minimizeArea = true;
double boxCentreX;
double boxCentreY;
double shortAxis;
double longAxis;
double longAxisOrientation;
double slope;
boolean boxCalculated = false;
public MinimumBoundingRectangle() {
}
public MinimumBoundingRectangle(MinimizationCriterion criterion) {
minimizeArea = criterion == MinimizationCriterion.AREA;
}
public MinimumBoundingRectangle(double[][] coordinates) {
this.coordinates = coordinates.clone();
}
public MinimumBoundingRectangle(double[][] coordinates, MinimizationCriterion criterion) {
this.coordinates = coordinates.clone();
minimizeArea = criterion == MinimizationCriterion.AREA;
}
public void setCoordinates(double[][] coordinates) {
this.coordinates = coordinates.clone();
boxCalculated = false;
}
public void setMinimizationCriterion(MinimizationCriterion criterion) {
minimizeArea = criterion == MinimizationCriterion.AREA;
}
public double[] getBoxCentrePoint() throws Exception {
if (!boxCalculated) {
getBoundingBox();
}
return new double[]{boxCentreX, boxCentreY};
}
public double getLongAxisLength() throws Exception {
if (!boxCalculated) {
getBoundingBox();
}
return longAxis;
}
public double getShortAxisLength() throws Exception {
if (!boxCalculated) {
getBoundingBox();
}
return shortAxis;
}
public double getElongationRatio() throws Exception {
if (!boxCalculated) {
getBoundingBox();
}
return 1 - shortAxis / longAxis;
}
public double getLongAxisOrientation() throws Exception {
if (!boxCalculated) {
getBoundingBox();
}
return 90 + Math.toDegrees(Math.atan(Math.tan(-slope)));
}
public double getShortAxisOrientation() throws Exception {
if (!boxCalculated) {
getBoundingBox();
}
double orient = getLongAxisOrientation();
return (orient >= 90) ? orient - 90 : orient + 90;
}
public double[][] getBoundingBox() throws Exception {
double[][] ret = new double[5][2];
int numPoints = coordinates.length;
Coordinate[] coords = new Coordinate[numPoints];
double east = Double.NEGATIVE_INFINITY;
double west = Double.POSITIVE_INFINITY;
double north = Double.NEGATIVE_INFINITY;
double south = Double.POSITIVE_INFINITY;
for (int i = 0; i < numPoints; i++) {
if (coordinates[i][0] > east) {
east = coordinates[i][0];
}
if (coordinates[i][0] < west) {
west = coordinates[i][0];
}
if (coordinates[i][1] > north) {
north = coordinates[i][1];
}
if (coordinates[i][1] < south) {
south = coordinates[i][1];
}
coords[i] = new Coordinate(coordinates[i][0], coordinates[i][1]);
}
double midX = west + (east - west) / 2.0;
double midY = south + (north - south) / 2.0;
ConvexHull hull = new ConvexHull(coords, factory);
Coordinate[] hullPoints = hull.getConvexHull().getCoordinates();
int numHullPoints = hullPoints.length;
double[][] verticesRotated = new double[numHullPoints][2];
double[] newBoundingBox = new double[4];
double[] axes = new double[2];
axes[0] = 9999999;
axes[1] = 9999999;
double x, y;
slope = 0;
boxCentreX = 0;
boxCentreY = 0;
// Rotate the hull points to align with the orientation of each side in order.
for (int m = 0; m < numHullPoints - 1; m++) {
double xDiff = hullPoints[m + 1].x - hullPoints[m].x;
double yDiff = hullPoints[m + 1].y - hullPoints[m].y;
double psi = -Math.atan2(yDiff, xDiff);
// Rotate each edge cell in the array by m degrees.
for (int n = 0; n < numHullPoints; n++) {
x = hullPoints[n].x - midX;
y = hullPoints[n].y - midY;
verticesRotated[n][0] = (x * Math.cos(psi)) - (y * Math.sin(psi));
verticesRotated[n][1] = (x * Math.sin(psi)) + (y * Math.cos(psi));
}
// calculate the minimum area bounding box in this coordinate
// system and see if it is less
newBoundingBox[0] = Double.MAX_VALUE; // west
newBoundingBox[1] = Double.MIN_VALUE; // east
newBoundingBox[2] = Double.MAX_VALUE; // north
newBoundingBox[3] = Double.MIN_VALUE; // south
for (int n = 0; n < numHullPoints; n++) {
x = verticesRotated[n][0];
y = verticesRotated[n][1];
if (x < newBoundingBox[0]) {
newBoundingBox[0] = x;
}
if (x > newBoundingBox[1]) {
newBoundingBox[1] = x;
}
if (y < newBoundingBox[2]) {
newBoundingBox[2] = y;
}
if (y > newBoundingBox[3]) {
newBoundingBox[3] = y;
}
}
double newXAxis = Math.abs(newBoundingBox[1] - newBoundingBox[0]);
double newYAxis = Math.abs(newBoundingBox[3] - newBoundingBox[2]);
double newValue = minimizeArea ? newXAxis * newYAxis : (newXAxis + newYAxis) * 2;
double currentValue = minimizeArea ? axes[0] * axes[1] : (axes[0] + axes[1]) * 2;
if (newValue < currentValue) { // minimize the metric of the bounding box.
axes[0] = newXAxis;
axes[1] = newYAxis;
if (axes[0] > axes[1]) {
slope = -psi;
} else {
slope = -(rightAngle + psi);
}
x = newBoundingBox[0] + newXAxis / 2;
y = newBoundingBox[2] + newYAxis / 2;
boxCentreX = midX + (x * Math.cos(-psi)) - (y * Math.sin(-psi));
boxCentreY = midY + (x * Math.sin(-psi)) + (y * Math.cos(-psi));
}
}
longAxis = Math.max(axes[0], axes[1]);
shortAxis = Math.min(axes[0], axes[1]);
double[][] axesEndPoints = new double[4][2];
axesEndPoints[0][0] = boxCentreX + longAxis / 2.0 * Math.cos(slope);
axesEndPoints[0][1] = boxCentreY + longAxis / 2.0 * Math.sin(slope);
axesEndPoints[1][0] = boxCentreX - longAxis / 2.0 * Math.cos(slope);
axesEndPoints[1][1] = boxCentreY - longAxis / 2.0 * Math.sin(slope);
axesEndPoints[2][0] = boxCentreX + shortAxis / 2.0 * Math.cos(rightAngle + slope);
axesEndPoints[2][1] = boxCentreY + shortAxis / 2.0 * Math.sin(rightAngle + slope);
axesEndPoints[3][0] = boxCentreX - shortAxis / 2.0 * Math.cos(rightAngle + slope);
axesEndPoints[3][1] = boxCentreY - shortAxis / 2.0 * Math.sin(rightAngle + slope);
ret[0][0] = axesEndPoints[0][0] + shortAxis / 2.0 * Math.cos(rightAngle + slope);
ret[0][1] = axesEndPoints[0][1] + shortAxis / 2.0 * Math.sin(rightAngle + slope);
ret[1][0] = axesEndPoints[0][0] - shortAxis / 2.0 * Math.cos(rightAngle + slope);
ret[1][1] = axesEndPoints[0][1] - shortAxis / 2.0 * Math.sin(rightAngle + slope);
ret[2][0] = axesEndPoints[1][0] - shortAxis / 2.0 * Math.cos(rightAngle + slope);
ret[2][1] = axesEndPoints[1][1] - shortAxis / 2.0 * Math.sin(rightAngle + slope);
ret[3][0] = axesEndPoints[1][0] + shortAxis / 2.0 * Math.cos(rightAngle + slope);
ret[3][1] = axesEndPoints[1][1] + shortAxis / 2.0 * Math.sin(rightAngle + slope);
ret[4][0] = axesEndPoints[0][0] + shortAxis / 2.0 * Math.cos(rightAngle + slope);
ret[4][1] = axesEndPoints[0][1] + shortAxis / 2.0 * Math.sin(rightAngle + slope);
boxCalculated = true;
return ret;
}
}