package de.blau.android.osm;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import org.xmlpull.v1.XmlSerializer;
import android.support.annotation.Nullable;
import android.util.Log;
import de.blau.android.Map;
import de.blau.android.exception.OsmException;
import de.blau.android.util.GeoMath;
import de.blau.android.util.rtree.BoundedObject;
/**
* BoundingBox represents a bounding box for a selection of an area. All values
* are in decimal-degree (WGS84), multiplied by 1E7.
*
* @author mb
*/
public class BoundingBox implements Serializable, JosmXmlSerializable, BoundedObject {
private static final long serialVersionUID = -2708721312405863618L;
/**
* left border of the bounding box, multiplied by 1E7
*/
private int left;
/**
* bottom border of the bounding box, multiplied by 1E7
*/
private int bottom;
/**
* right border of the bounding box, multiplied by 1E7
*/
private int right;
/**
* top border of the bounding box, multiplied by 1E7
*/
private int top;
/**
* The width of the bounding box. Always positive.
*/
private long width;
/**
* The height of the bounding box. Always positive.
*/
private int height;
/**
* Mercator value for the bottom of the BBos
*/
//TODO experimental code for using non-approx. projections
private double bottomMercator;
/**
* Delimiter for the bounding box as String representation.
*/
private static final String STRING_DELIMITER = ",";
/**
* The name of the tag in the OSM-XML file.
*/
public static final String NAME = "bounds";
/**
* Default zoom in factor. Must be greater than 0.
*/
private static final float ZOOM_IN = 0.125f;
/**
* Default zoom out factor. Must be less than 0.
*/
private static final float ZOOM_OUT = -0.16666666f;
/**
* The maximum difference between two borders of the bounding box for the
* OSM-API. {@link http://wiki.openstreetmap.org/index.php/Getting_Data#Construct_an_URL_for_the_HTTP_API }
*/
public static final int API_MAX_DEGREE_DIFFERENCE = 5000000;
/**
* Maximum latitude ({@link GeoMath#MAX_LAT}) in 1E7.
*/
public static final int MAX_LAT_E7 = (int) (GeoMath.MAX_LAT * 1E7);
/**
* Maximum Longitude.
*/
public static final int MAX_LON_E7 = (int) (GeoMath.MAX_LON * 1E7);
/**
* Minimum width to zoom in.
*/
private static final int MIN_ZOOM_WIDTH = 1000; // roughly 110m at the equator
/**
* Maximum width to zoom out.
*/
// private static final long MAX_ZOOM_WIDTH = 500000000L;
private static final long MAX_ZOOM_WIDTH = 3599999999L;
private static final String DEBUG_TAG = BoundingBox.class.getSimpleName();
/**
* The ratio of this BoundingBox. Only needed when it's used as a viewbox.
*/
private float ratio = 1;
/**
* @return a BoundingBox initialized to the maximum extent of mercator projection
*/
public static BoundingBox getMaxMercatorExtent() {
BoundingBox box = new BoundingBox();
box.left = (int) (-180*1E7);
box.bottom = (int) (-GeoMath.MAX_LAT*1E7);
box.right = (int) (180*1E7);
box.top = (int) (GeoMath.MAX_LAT*1E7);
box.calcDimensions();
box.calcBottomMercator();
return box;
}
/**
* Creates a new bounding box with coordinates initialized to zero
* Careful: will fail validation
*/
public BoundingBox() {
left = 0;
bottom = 0;
right = 0;
top = 0;
width = 0;
height = 0;
}
/**
* Creates a degenerated BoundingBox with the corners set to the node coordinates
* validate will cause an exception if called on this
*
* @param lonE7 longitude of the node
* @param latE7 latitude of the node
*/
public BoundingBox(int lonE7, int latE7) {
resetTo(lonE7, latE7);
}
/**
* Resets to a degenerated BoundingBox with the corners set to the node coordinates
* validate will cause an exception if called on a box after this has been called
*
* @param lonE7 longitude of the node
* @param latE7 latitude of the node
*/
public void resetTo(int lonE7, int latE7) {
left = lonE7;
bottom = latE7;
right = lonE7;
top = latE7;
width = 0;
height = 0;
}
/**
* Generates a bounding box with the given borders. Of course, left must be
* left to right and top must be top of bottom.
*
* @param left degree of the left Border, multiplied by 1E7
* @param bottom degree of the bottom Border, multiplied by 1E7
* @param right degree of the right Border, multiplied by 1E7
* @param top degree of the top Border, multiplied by 1E7
* @throws OsmException when the borders are mixed up or outside of
* {@link #MAX_LAT_E7}/{@link #MAX_LON_E7} (!{@link #isValid()})
*/
public BoundingBox(final int left, final int bottom, final int right, final int top) throws OsmException {
this.left = left;
this.bottom = bottom;
this.right = right;
this.top = top;
calcDimensions();
calcBottomMercator();
validate();
}
/**
* Generates a bounding box with the given borders.
*
* @param left degree of the left Border
* @param bottom degree of the bottom Border
* @param right degree of the right Border
* @param top degree of the top Border
* @throws OsmException see {@link #BoundingBox(int, int, int, int)}
*/
public BoundingBox(final double left, final double bottom, final double right, final double top)
throws OsmException {
this((int) (left * 1E7), (int) (bottom * 1E7), (int) (right * 1E7), (int) (top * 1E7));
}
/**
* Generates a bounding box with a given radius and a center-position.
*
* @param centerLat latitude of the center
* @param centerLon longitude of the center
* @param radius radius in degree
* @throws OsmException see {@link #BoundingBox(int, int, int, int)}
*/
private BoundingBox(final double centerLat, final double centerLon, final double radius) throws OsmException {
this(centerLon - radius, centerLat - radius, centerLon + radius, centerLat + radius);
}
/**
* Copy-Constructor.
*
* @param box box with the new borders.
*/
public BoundingBox(final BoundingBox box) {
// this(box.left, box.bottom, box.right, box.top); not good, forces a recalc of everything
this.left = box.left;
this.bottom = box.bottom;
this.right = box.right;
this.top = box.top;
this.width = box.width;
this.height = box.height;
this.bottomMercator = box.bottomMercator;
}
/**
* @return returns a copy of this object.
*/
public BoundingBox copy() {
return new BoundingBox(this);
}
/**
* Checks if the bounding box is valid for the OSM API.
*
* @return true, if the bbox is smaller than 0.5*0.5 (here multiplied by
* 1E7) degree.
*/
public boolean isValidForApi() {
return isValid() && (width < API_MAX_DEGREE_DIFFERENCE) && (height < API_MAX_DEGREE_DIFFERENCE);
}
/**
* @return true if left is less than right and bottom is less than top.
*/
private boolean isValid() {
return (left < right) && (bottom < top) && (left >= -MAX_LON_E7) && (right <= MAX_LON_E7) && (top <= MAX_LAT_E7)
&& (bottom >= -MAX_LAT_E7);
}
/**
* @return a String, representing the bounding box. Format:
* "left,bottom,right,top" in decimal degrees.
*/
public String toApiString() {
return "" + left / 1E7 + STRING_DELIMITER + bottom / 1E7 + STRING_DELIMITER + right / 1E7 + STRING_DELIMITER
+ top / 1E7;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "(" + left + STRING_DELIMITER + bottom + STRING_DELIMITER + right + STRING_DELIMITER + top + ")";
}
/**
* Get the left (western-most) side of the box.
* @return The 1E7 longitude of the left side of the box.
*/
public int getLeft() {
return left;
}
/**
* Get the bottom (southern-most) side of the box.
* @return The 1E7 latitude of the bottom side of the box.
*/
public int getBottom() {
return bottom;
}
/**
* Get the right (eastern-most) side of the box.
* @return The 1E7 longitude of the right side of the box.
*/
public int getRight() {
return right;
}
/**
* Get the top (northern-most) side of the box.
* @return The 1E7 latitude of the top side of the box.
*/
public int getTop() {
return top;
}
/**
* Get the width of the box.
* @return The difference in 1E7 degrees between the right and left sides.
*/
public long getWidth() {
return width;
}
/**
* Get the height of the box.
* @return The difference in 1E7 degrees between the top and bottom sides.
*/
public int getHeight() {
return height;
}
/**
* Checks if lat/lon is in this bounding box.
*
* @param latE7
* @param lonE7
* @return true if lat/lon is inside this bounding box.
*/
public boolean isIn(final int latE7, final int lonE7) {
return lonE7 >= left && lonE7 <= right && latE7 >= bottom && latE7 <= top;
}
/**
* Checks if a line between lat/lon and lat2/lon2 may intersect with this
* bounding box.
*
* @param lat
* @param lon
* @param lat2
* @param lon2
* @return true, when at least one lat/lon is inside, or a intersection
* could not be excluded.
*/
public boolean intersects(final int lat, final int lon, final int lat2, final int lon2) {
return isIn(lat, lon) || isIn(lat2, lon2) || isIntersectionPossible(lat, lon, lat2, lon2);
}
public boolean intersects(final BoundingBox b) {
if (right < b.left) return false; // a is left of b
if (left > b.right) return false; // a is right of b
if (top < b.bottom) return false; // a is above b
if (bottom > b.top) return false; // a is below b
return true; // boxes overlap
}
/**
* Java Rect compatibility
* Return true if the boxes intersect
* @param box2
* @param box
* @return
*/
public static boolean intersects(BoundingBox box2, BoundingBox box) {
return box2.intersects(box);
}
/**
* Checks if an intersection with a line between lat/lon and lat2/lon2 is
* impossible. If two coordinates (lat/lat2 or lon/lon2) are outside of a
* border, no intersection is possible.
*
* @param lat
* @param lon
* @param lat2
* @param lon2
* @return true, when an intersection is possible.
*/
private boolean isIntersectionPossible(final int lat, final int lon, final int lat2, final int lon2) {
return !(lat > top && lat2 > top || lat < bottom && lat2 < bottom ||
lon > right && lon2 > right || lon < left && lon2 < left);
}
/**
* Calculates the dimensions width and height of this bounding box.
*/
private void calcDimensions() {
int t;
if (right < left) {
t = right;
right = left;
left = t;
}
width = (long)right - (long)left;
if (top < bottom) {
t = top;
top = bottom;
bottom = t;
}
height = top - bottom;
// Log.d("BoundingBox", "calcdimensions width " + width + " height " + height);
}
/**
*/
private void calcBottomMercator() {
bottomMercator = GeoMath.latE7ToMercator(bottom);
}
/**
* Changes the dimensions of this bounding box to fit the given ratio.
* Ratio is width divided by height. The smallest dimension will remain,
* the larger one will be resized to fit ratio.
*
* @param map an instance
* @param ratio The new aspect ratio.
*/
public void setRatio(Map map, final float ratio) throws OsmException {
setRatio(map, ratio, false);
}
/**
* Changes the dimensions of this bounding box to fit the given ratio.
* @param ratio The new aspect ratio.
* @param preserveZoom If true, maintains the current level of zoom by
* creating a new boundingbox at the required ratio at the same center. If
* false, the new bounding box is sized such that the currently visible
* area is still visible with the new aspect ratio applied.
*/
public void setRatio(Map map, final float ratio, final boolean preserveZoom) throws OsmException {
long mTop = GeoMath.latE7ToMercatorE7(top); // note long or else we get an int overflow on calculating the center
long mBottom = GeoMath.latE7ToMercatorE7(bottom);
long mHeight = mTop - mBottom;
if (width <= 0 || mHeight <=0) {
// should't happen, but just in case
Log.d("BoundingBox","Width or height zero: " + width + "/" + height);
BoundingBox bbox = GeoMath.createBoundingBoxForCoordinates(GeoMath.mercatorE7ToLat((int) (mBottom+mHeight/2)), GeoMath.mercatorE7ToLat((int) (left+width/2)), 10.0f, true);
left = bbox.left;
bottom = bbox.bottom;
right = bbox.right;
top = bbox.top;
calcDimensions();
mTop = GeoMath.latE7ToMercatorE7(top); // note long or else we get an int overflow on calculating the center
mBottom = GeoMath.latE7ToMercatorE7(bottom);
mHeight = mTop - mBottom;
}
//Log.d("BoundingBox","current ratio " + this.ratio + " new ratio " + ratio);
if ((ratio > 0) && !Float.isNaN(ratio)) {
if (preserveZoom) {
// Apply the new aspect ratio, but preserve the level of zoom
// so that for example, rotating portrait<-->landscape won't
// zoom out
long centerX = left + width / 2L; // divide first to stay < 2^32
long centerY = mBottom + mHeight / 2L;
long newHeight2 = 0;
long newWidth2 = 0;
if (ratio <= 1.0) { // portrait and square
if (width <= mHeight) {
newHeight2 = (long)((width / 2L) / ratio);
newWidth2 = width / 2L;
} else { // switch landscape --> portrait
float pixelDeg = (float)map.getHeight()/(float)width; // height was the old width
newWidth2 = (long)(map.getWidth() / pixelDeg)/2L;
newHeight2 = (long)(newWidth2 / ratio );
}
} else { // landscape
if (width < mHeight) { // switch portrait -> landscape
float pixelDeg = (float)map.getHeight()/(float)width; // height was the old width
newWidth2 = (long)(map.getWidth() / pixelDeg)/2L;
newHeight2 = (long)(newWidth2 / ratio );
} else {
newHeight2 =(long)((width / 2L) / ratio);
newWidth2 = width / 2L;
}
}
if (centerX + newWidth2 > MAX_LON_E7) {
right = MAX_LON_E7;
left = (int) Math.max(-MAX_LON_E7, MAX_LON_E7 - 2*newWidth2);
} else if (centerX - newWidth2 < -MAX_LON_E7) {
left = -MAX_LON_E7;
right = (int) Math.min(MAX_LON_E7, centerX + 2*newWidth2);
} else {
left = (int) (centerX - newWidth2);
right = (int) (centerX + newWidth2);
}
//
if ((centerY + newHeight2) > GeoMath.MAX_MLAT_E7) {
mTop = GeoMath.MAX_MLAT_E7;
mBottom = Math.max(-GeoMath.MAX_MLAT_E7, GeoMath.MAX_MLAT_E7 - 2*newHeight2);
} else if ((centerY - newHeight2) < -GeoMath.MAX_MLAT_E7) {
mBottom = -GeoMath.MAX_MLAT_E7;
mTop = Math.min(GeoMath.MAX_MLAT_E7, -GeoMath.MAX_MLAT_E7 + 2*newHeight2);
} else {
mTop = centerY + newHeight2;
mBottom = centerY - newHeight2;
}
} else {
int singleBorderMovement;
// Ensure currently visible area is entirely visible in the new box
if ((width / (mHeight)) < ratio) {
// The actual box is wider than it should be.
/* Here comes the math:
* width/height = ratio
* width = ratio * height
* newWidth = width - ratio * height
*/
singleBorderMovement = Math.round((width - ratio * mHeight) / 2);
left += singleBorderMovement;
right -= singleBorderMovement;
} else {
// The actual box is more narrow than it should be.
/* Same in here, only different:
* width/height = ratio
* height = width/ratio
* newHeight = height - width/ratio
*/
singleBorderMovement = Math.round((mHeight - width / ratio) / 2);
mBottom += singleBorderMovement;
mTop -= singleBorderMovement;
}
}
top = GeoMath.mercatorE7ToLatE7((int)mTop);
bottom = GeoMath.mercatorE7ToLatE7((int)mBottom);
// border-sizes changed. So we have to recalculate the dimensions.
calcDimensions();
calcBottomMercator();
this.ratio = ratio;
validate();
}
}
/**
* Performs a translation so the center of this bounding box will be at
* (lonCenter|latCenter).
*
* @param map current map view
* @param lonCenter the absolute longitude for the center (deg*1E7)
* @param latCenter the absolute latitude for the center (deg*1E7)
*/
public void moveTo(Map map, final int lonCenter, final int latCenter) {
// new middle in mercator
double mLatCenter = GeoMath.latE7ToMercator(latCenter);
double mTop = GeoMath.latE7ToMercator(top);
int newBottom = GeoMath.mercatorToLatE7(mLatCenter - (mTop - bottomMercator)/2);
try {
translate(map, (lonCenter - left - (int)(width / 2L)), newBottom - bottom);
} catch (OsmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Relative translation.
*
* Note clamping based on direction of movement can cause problems, always check that we are in bounds
*
* @param map instance of the current map view
* @param lon the relative longitude change.
* @param lat the relative latitude change.
*/
public void translate(@Nullable Map map, int lon, int lat) throws OsmException {
if ((long)right + (long)lon > (long)MAX_LON_E7) {
lon = MAX_LON_E7 - right;
} else if ((long)left + (long)lon < (long)-MAX_LON_E7) {
lon = -MAX_LON_E7 - left;
}
if (top + lat > MAX_LAT_E7) {
lat = MAX_LAT_E7 - top;
} else if (bottom + lat < -MAX_LAT_E7) {
lat = -MAX_LAT_E7 - bottom;
}
left += lon;
right += lon;
top += lat;
bottom += lat;
if (map != null) {
setRatio(map, ratio, true); //TODO slightly expensive likely to be better to do everything in mercator
}
validate();
}
/** Calculate the largest zoom-in factor that can be applied to the current
* view.
* @return The largest allowable zoom-in factor.
*/
private float zoomInLimit() {
return (width - MIN_ZOOM_WIDTH) / 2f / width;
}
/**
* Calculate the largest zoom-out factor that can be applied to the current
* view.
* @return The largest allowable zoom-out factor.
*/
private float zoomOutLimit() {
long mTop = GeoMath.latE7ToMercatorE7(top);
long mBottom = GeoMath.latE7ToMercatorE7(bottom);
long mHeight = mTop - mBottom;
return -Math.min((MAX_ZOOM_WIDTH - width) / 2f / width, ((2L*(long)GeoMath.MAX_MLAT_E7) - mHeight) / 2f /mHeight);
}
/**
* Test if the box can be zoomed in.
* @return true if the box can be zoomed in, false if it can't.
*/
public boolean canZoomIn() {
return (ZOOM_IN < zoomInLimit());
}
/**
* Test if the box can be zoomed out.
* @return true if the box can be zoomed out, false if it can't.
*/
public boolean canZoomOut() {
// return (ZOOM_OUT > zoomOutLimit());
return zoomOutLimit() < -3.1E-9; // determined experimental
}
/**
* Reduces this bounding box by the ZOOM_IN factor. The ratio of width and
* height remains.
*/
public void zoomIn() {
zoom(ZOOM_IN);
}
/**
* Enlarges this bounding box by the ZOOM_OUT factor. The ratio of width
* and height remains.
*/
public void zoomOut() {
zoom(ZOOM_OUT);
}
/**
* Enlarges/reduces the borders by zoomFactor.
*
* @param zoomFactor factor enlarge/reduce the borders.
*/
public void zoom(float zoomFactor) {
// Log.d("BoundingBox","zoom " + this.toString());
zoomFactor = Math.min(zoomInLimit(), zoomFactor);
zoomFactor = Math.max(zoomOutLimit(), zoomFactor);
long mTop = GeoMath.latE7ToMercatorE7(top);
long mBottom = GeoMath.latE7ToMercatorE7(bottom);
long mHeight = mTop - mBottom;
long horizontalChange = (long)(width * zoomFactor);
long verticalChange = (long)(mHeight * zoomFactor);
long tmpLeft=left;
long tmpRight=right;
//
if (tmpLeft + horizontalChange < (long)-MAX_LON_E7) {
long rest = left + horizontalChange + (long)MAX_LON_E7;
tmpLeft = -MAX_LON_E7;
tmpRight = tmpRight - rest;
} else {
tmpLeft = tmpLeft + horizontalChange;
}
if (tmpRight - horizontalChange > (long)MAX_LON_E7) {
long rest = tmpRight - horizontalChange - (long)MAX_LON_E7;
tmpRight = MAX_LON_E7;
tmpLeft = Math.max((long)-MAX_LON_E7,tmpLeft + rest);
} else {
tmpRight = tmpRight - horizontalChange;
}
left = (int)tmpLeft;
right = (int)tmpRight;
// left = Math.max(-MAX_LON, left + (int)horizontalChange);
// right = Math.min(MAX_LON, right - (int)horizontalChange);
if ((mBottom + verticalChange) < -GeoMath.MAX_MLAT_E7) {
long rest = mBottom + (long)verticalChange + (long)GeoMath.MAX_MLAT_E7;
mBottom = - GeoMath.MAX_MLAT_E7;
mTop = mTop - rest;
} else {
mBottom = mBottom + verticalChange;
}
if ((mTop - verticalChange) > (long)GeoMath.MAX_MLAT_E7) {
long rest = mTop - verticalChange - (long)GeoMath.MAX_MLAT_E7;
mTop = GeoMath.MAX_MLAT_E7;
mBottom = Math.max(-GeoMath.MAX_MLAT_E7,mBottom - rest);
} else {
mTop = mTop - verticalChange;
}
bottom = GeoMath.mercatorE7ToLatE7((int)mBottom);
top = GeoMath.mercatorE7ToLatE7((int)mTop);
// bottom = Math.max(-MAX_LAT_E7, GeoMath.mercatorE7ToLatE7((int)(mBottom + (long)verticalChange)));
// top = Math.min(MAX_LAT_E7, GeoMath.mercatorE7ToLatE7((int)(mTop - (long)verticalChange)));
// setRatio(ratio, true);
calcDimensions(); // need to do this or else centering will not work
calcBottomMercator();
}
/**
* set current zoom level to a tile zoom level equivalent, powers of 2 assuming 256x256 tiles
* maintain center of bounding box
* @param tileZoomLevel The TMS zoom level to zoom to (from 0 for the whole world to about 19 for small areas).
*/
public void setZoom(Map map, int tileZoomLevel) {
// setting an exact zoom level implies one screen pixel == one tile pixel
// calculate one pixel in degrees (mercator) at this zoom level
double degE7PerPixel = 3600000000.0d / (256*Math.pow(2, tileZoomLevel));
double wDegE7 = map.getWidth() * degE7PerPixel;
double hDegE7 = map.getHeight() * degE7PerPixel;
long centerLon = left + width/2;
left = (int) (centerLon - wDegE7/2);
right = (int) (left + wDegE7);
long mBottom = GeoMath.latE7ToMercatorE7(bottom);
long mTop = GeoMath.latE7ToMercatorE7(top);
long centerLat = mBottom + (mTop-mBottom)/2;
bottom = GeoMath.mercatorE7ToLatE7((int)(centerLat - hDegE7/2));
top = GeoMath.mercatorE7ToLatE7((int)(centerLat + hDegE7/2));
calcDimensions(); //
calcBottomMercator();
}
/**
* Sets the borders to the ones of newBox. Recalculates dimensions to fit the current ratio (that of the window)
* and maintains zoom level
*
* @param map current map view
* @param newBox box with the new borders.
*/
public void setBorders(Map map, final BoundingBox newBox) {
setBorders(map, newBox, this.ratio);
}
/**
* Sets the borders to the ones of newBox. Recalculates dimensions to fit the ratio and maintains zoom level
*
* @param map current map view
* @param newBox
* @param ratio
*/
private void setBorders(Map map, final BoundingBox newBox, float ratio) {
setBorders(map, newBox, ratio, true);
}
/**
* Sets the borders to the ones of newBox. Recalculates dimensions to fit the current ratio (that of the window)
* and maintains zoom level depending on the value of preserveZoom
*
* @param map current map view
* @param newBox new bounding box
* @param preserveZoom maintain current zoom level
*/
public void setBorders(final Map map, final BoundingBox newBox, boolean preserveZoom) {
setBorders(map, newBox, this.ratio, preserveZoom);
}
/**
* Sets the borders to the ones of newBox. Recalculates dimensions to fit the current ratio (that of the window)
* and maintains zoom level depending on the value of preserveZoom
*
* @param map map current map view
* @param newBox new bounding box
* @param ratio current window ratio
* @param preserveZoom maintain current zoom level
*/
public void setBorders(final Map map, final BoundingBox newBox, float ratio, boolean preserveZoom) {
left = newBox.left;
right = newBox.right;
top = newBox.top;
bottom = newBox.bottom;
Log.d("BoundingBox","setBorders " + newBox.toString() + " ratio is " + ratio);
try {
calcDimensions(); // neede to recalc width
setRatio(map, ratio, preserveZoom);
validate();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} //TODO slightly expensive likely to be better to do everything in mercator
}
/**
* Make the bounding box a valid request for the API, shrinking into its center if necessary.
*/
public void makeValidForApi() throws OsmException {
if (!isValidForApi()) {
int centerx = (left / 2 + right / 2); // divide first to stay < 2^32
int centery = (top + bottom) / 2;
left = centerx - API_MAX_DEGREE_DIFFERENCE / 2;
right = centerx + API_MAX_DEGREE_DIFFERENCE / 2;
top = centery + API_MAX_DEGREE_DIFFERENCE / 2;
bottom = centery - API_MAX_DEGREE_DIFFERENCE / 2;
calcDimensions();
calcBottomMercator();
}
validate();
}
private void validate() throws OsmException {
if (!isValid()) {
Log.e(DEBUG_TAG, toString());
throw new OsmException("left must be less than right and bottom must be less than top");
}
}
public boolean contains(BoundingBox bb) {
return (bb.bottom >= bottom) && (bb.top <= top) && (bb.left >= left) && (bb.right <= right);
}
/**
* Returns true if the coordinates are in the box
* Right and top coordinate are considered inside
* @param lonE7
* @param latE7
* @return
*/
public boolean contains(int lonE7, int latE7) {
return left <= lonE7 && lonE7 <= right && bottom <= latE7 && latE7 <= top;
}
/**
* Return pre-caclulated meracator value of bottom of the bounding box
* @return
*/
public double getBottomMercator() {
return bottomMercator;
}
/**
* The setters are private since without calling calcDimensions the BB will be left in an inconsistent state
* @param latE7
*/
private void setTop(int latE7) {
this.top = latE7;
}
private void setBottom(int latE7) {
this.bottom = latE7;
}
private void setRight(int lonE7) {
this.right = lonE7;
}
private void setLeft(int lonE7) {
this.left = lonE7;
}
/**
* Return lat value of the center of the bounding box
* @return
*/
public double getCenterLat() {
int mBottom = GeoMath.latE7ToMercatorE7(bottom);
int mHeight = GeoMath.latE7ToMercatorE7(top) - mBottom;
return GeoMath.mercatorE7ToLat(mBottom + mHeight/2);
}
/**
* Given a list of existing bounding boxes and a new bbox. Return a list of pieces of the new bbox that complete the coverage
* @param existing
* @param newBox
* @return
* @throws OsmException
*/
public static ArrayList<BoundingBox> newBoxes(ArrayList<BoundingBox> existing, BoundingBox newBox) {
ArrayList<BoundingBox> result = new ArrayList<BoundingBox>();
result.add(newBox);
for (BoundingBox b:existing) {
ArrayList<BoundingBox> temp = new ArrayList<BoundingBox>();
for (BoundingBox rb:result) {
if (b.intersects(rb)) {
try {
// higher than b
if (rb.top > b.top) {
temp.add(new BoundingBox(rb.left, b.top, rb.right, rb.top));
rb.setTop(b.top);
}
// lower than b
if (rb.bottom < b.bottom) {
temp.add(new BoundingBox(rb.left, rb.bottom, rb.right, b.bottom));
rb.setBottom(b.bottom);
}
// left
if (rb.left < b.left && rb.bottom != rb.top) {
temp.add(new BoundingBox(rb.left, rb.bottom, b.left, rb.top));
rb.setLeft(b.left);
}
// right
if (rb.right > b.right && rb.bottom != rb.top) {
temp.add(new BoundingBox(b.right, rb.bottom, rb.right, rb.top));
rb.setRight(b.right);
}
rb.calcDimensions();
rb.calcBottomMercator();
} catch (OsmException e) {
Log.d("BoundingBox", "Exception " + e.getMessage());
}
} else {
temp.add(rb);
}
}
result = temp;
}
return result;
}
@Override
public void toJosmXml(final XmlSerializer s)
throws IllegalArgumentException, IllegalStateException, IOException {
s.startTag("", "bounds");
s.attribute("", "origin", "");
s.attribute("", "maxlon", Double.toString((right / 1E7)));
s.attribute("", "maxlat", Double.toString((top / 1E7)));
s.attribute("", "minlon", Double.toString((left / 1E7)));
s.attribute("", "minlat", Double.toString((bottom / 1E7)));
s.endTag("", "bounds");
}
@Override
public BoundingBox getBounds() {
return this;
}
/**
* Set corners to same values as b
* CAREFUL does not update other fields
* @param b
*/
public void set(BoundingBox b) {
left=b.left;
bottom=b.bottom;
right=b.right;
top=b.top;
width = b.width;
height = b.height;
}
/**
* grow this box so that it covers the point
* @param lonE7
* @param latE7
*/
public void union(int lonE7, int latE7) {
if (lonE7 < left) {
left = lonE7;
} else if (lonE7 > right) {
right = lonE7;
}
if (latE7 < bottom) {
bottom = latE7;
} else if (latE7 > top) {
top = latE7;
}
width = right - left;
height = top - bottom;
}
/**
* grow this box so that it covers b
* @param b
*/
public void union(BoundingBox b) {
if (b.left < left) {
left = b.left;
}
if (b.right > right) {
right = b.right;
}
if (b.bottom < bottom) {
bottom = b.bottom;
}
if (b.top > top) {
top = b.top;
}
width = right - left;
height = top - bottom;
}
/**
* Return true if box is empty
* @return
*/
public boolean isEmpty() {
return left == right && top == bottom;
}
}