// This software is released into the Public Domain. See copying.txt for details. package org.openstreetmap.osmosis.core.domain.v0_6; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedList; import org.openstreetmap.osmosis.core.store.StoreClassRegister; import org.openstreetmap.osmosis.core.store.StoreReader; import org.openstreetmap.osmosis.core.store.StoreWriter; /** * A data class representing an OSM data bound element. * * @author Karl Newman */ public class Bound extends Entity implements Comparable<Bound> { private static final double MIN_LATITUDE = -90.0; private static final double MAX_LATITUDE = 90.0; private static final double MIN_LONGITUDE = -180.0; private static final double MAX_LONGITUDE = 180.0; private double right; private double left; private double top; private double bottom; private String origin; /** * Creates a new instance which covers the entire planet. * * @param origin * The origin (source) of the data, typically a URI * */ public Bound(String origin) { this(MAX_LONGITUDE, MIN_LONGITUDE, MAX_LATITUDE, MIN_LATITUDE, origin); } /** * Creates a new instance with the specified boundaries. * * @param right * The longitude coordinate of the right (East) edge of the bound * @param left * The longitude coordinate of the left (West) edge of the bound * @param top * The latitude coordinate of the top (North) edge of the bound * @param bottom * The latitude coordinate of the bottom (South) edge of the bound * @param origin * The origin (source) of the data, typically a URI */ public Bound(double right, double left, double top, double bottom, String origin) { super(new CommonEntityData(0, 0, new Date(), OsmUser.NONE, 0)); // minimal underlying entity // Check if any coordinates are out of bounds if (Double.compare(right, MAX_LONGITUDE + 1.0d) > 0 || Double.compare(right, MIN_LONGITUDE - 1.0d) < 0 || Double.compare(left, MAX_LONGITUDE + 1.0d) > 0 || Double.compare(left, MIN_LONGITUDE - 1.0d) < 0 || Double.compare(top, MAX_LATITUDE + 1.0d) > 0 || Double.compare(top, MIN_LATITUDE - 1.0d) < 0 || Double.compare(bottom, MAX_LATITUDE + 1.0d) > 0 || Double.compare(bottom, MIN_LATITUDE - 1.0d) < 0) { throw new IllegalArgumentException("Bound coordinates outside of valid range"); } if (Double.compare(top, bottom) < 0) { throw new IllegalArgumentException("Bound top < bottom"); } this.right = right; this.left = left; this.top = top; this.bottom = bottom; this.origin = origin; } /** * Creates a new instance. * * @param sr * The store to read state from. * @param scr * Maintains the mapping between classes and their identifiers within the store. */ public Bound(StoreReader sr, StoreClassRegister scr) { super(sr, scr); this.right = sr.readDouble(); this.left = sr.readDouble(); this.top = sr.readDouble(); this.bottom = sr.readDouble(); this.origin = sr.readString(); } /** * {@inheritDoc} */ @Override public void store(StoreWriter sw, StoreClassRegister scr) { super.store(sw, scr); sw.writeDouble(right); sw.writeDouble(left); sw.writeDouble(top); sw.writeDouble(bottom); sw.writeString(origin); } /** * {@inheritDoc} */ @Override public EntityType getType() { return EntityType.Bound; } /** * @return The right (East) bound longitude */ public double getRight() { return right; } /** * @return The left (West) bound longitude */ public double getLeft() { return left; } /** * @return The top (North) bound latitude */ public double getTop() { return top; } /** * @return The bottom (South) bound latitude */ public double getBottom() { return bottom; } /** * @return the origin */ public String getOrigin() { return origin; } /** * Calculate the intersected area of this with the specified bound. * * @param intersectingBound * Bound element with which to calculate the intersection * @return Bound Resultant intersection of the two bound object */ public Bound intersect(Bound intersectingBound) { String newOrigin; double newRight = 0.0, newLeft = 0.0, newTop, newBottom; boolean intersect180, this180; // flags to indicate bound cross antimeridian if (intersectingBound == null) { return null; // no intersection } // first check the vertical intersection newTop = Math.min(this.getTop(), intersectingBound.getTop()); newBottom = Math.max(this.getBottom(), intersectingBound.getBottom()); if (Double.compare(newBottom, newTop) >= 0) { // no north-south intersecting region return null; } intersect180 = (Double.compare(intersectingBound.getLeft(), intersectingBound.getRight()) > 0); this180 = (Double.compare(this.getLeft(), this.getRight()) > 0); if ((intersect180 && this180) || !(intersect180 || this180)) { // if both or neither cross the antimeridian, use the simple case newRight = Math.min(this.getRight(), intersectingBound.getRight()); newLeft = Math.max(this.getLeft(), intersectingBound.getLeft()); if (!(intersect180 || this180) && (Double.compare(newLeft, newRight) >= 0)) { /* * This is only applicable for the case where neither cross the antimeridian, * because if both cross, they must intersect. */ return null; // no intersecting area } } else { Bound b1, b2; // stand-ins for this and intersectingBound if (intersect180 && !this180) { // passed parameter Bound crosses the antimeridian, this Bound doesn't b1 = this; b2 = intersectingBound; } else { // this Bound crosses the antimeridian, passed parameter Bound doesn't b1 = intersectingBound; b2 = this; } if (Double.compare(b1.getRight(), b2.getLeft()) > 0 && Double.compare(b1.getLeft(), b2.getRight()) < 0) { // intersects on both sides of the antimeridian--just pick the smaller of the // two Double diff1 = b1.getRight() - b1.getLeft(); Double diff2 = b2.getRight() - MIN_LONGITUDE + MAX_LONGITUDE - b2.getLeft(); if (Double.compare(diff1, diff2) <= 0) { newRight = b1.getRight(); newLeft = b1.getLeft(); } else { newRight = b2.getRight(); newLeft = b2.getLeft(); } } else if (Double.compare(b1.getRight(), b2.getLeft()) > 0) { // intersects on the East side of the antimeridian newRight = b1.getRight(); newLeft = b2.getLeft(); } else if (Double.compare(b1.getLeft(), b2.getRight()) < 0) { // intersects on the West side of the antimeridian newRight = b2.getRight(); newLeft = b1.getLeft(); } } if (Double.compare(newRight, newLeft) == 0) { return null; } // Keep the origin string from this if it's not blank, otherwise use the origin string from // the intersecting Bound if (origin != "") { newOrigin = origin; } else { newOrigin = intersectingBound.origin; } return new Bound(newRight, newLeft, newTop, newBottom, newOrigin); } /** * Calculate the union area of this with the specified bound. Not a strict mathematical union, * but the smallest rectangular area which includes both bound. Thus, result may include areas * not contained in the original bound. * * @param unionBound * Bound element with which to calculate the union * @return Bound Resultant union of the two bound objects */ public Bound union(Bound unionBound) { double newRight = 0.0, newLeft = 0.0, newTop, newBottom; String newOrigin; if (unionBound == null) { return this; // nothing to compute a union with } // First compute the vertical union newTop = Math.max(this.getTop(), unionBound.getTop()); newBottom = Math.min(this.getBottom(), unionBound.getBottom()); if (Double.compare(newBottom, newTop) >= 0) { // no north-south intersecting region return null; } // Next check the (likely) common case where one of the bound covers the planet if ((Double.compare(this.getLeft(), MIN_LONGITUDE) == 0 && Double.compare( this.getRight(), MAX_LONGITUDE) == 0) || (Double.compare(unionBound.getLeft(), MIN_LONGITUDE) == 0 && Double.compare( unionBound.getRight(), MAX_LONGITUDE) == 0)) { newRight = MAX_LONGITUDE; newLeft = MIN_LONGITUDE; } else { boolean union180, this180; // flags to indicate bound cross antimeridian double size1, size2; // resulting union sizes for comparison union180 = (Double.compare(unionBound.getLeft(), unionBound.getRight()) > 0); this180 = (Double.compare(this.getLeft(), this.getRight()) > 0); if (union180 && this180) { // if both cross the antimeridian, then the union will cross, too. newRight = Math.max(this.getRight(), unionBound.getRight()); newLeft = Math.min(this.getLeft(), unionBound.getLeft()); } else if (!(union180 || this180)) { // neither cross the antimeridian, but the union might // first calculate the size of a simple union which doesn't cross the antimeridian size1 = Math.max(this.getRight(), unionBound.getRight()) - Math.min(this.getLeft(), unionBound.getLeft()); // then calculate the size of the resulting union which does cross the antimeridian size2 = (Math.min(this.getRight(), unionBound.getRight()) - MIN_LONGITUDE) + (MAX_LONGITUDE - Math.max(this.getLeft(), unionBound.getLeft())); // now pick the smaller of the two if (Double.compare(size1, size2) <= 0) { newRight = Math.max(this.getRight(), unionBound.getRight()); newLeft = Math.min(this.getLeft(), unionBound.getLeft()); } else { newRight = Math.min(this.getRight(), unionBound.getRight()); newLeft = Math.max(this.getLeft(), unionBound.getLeft()); } } else { // One of the Bound crosses the antimeridian, the other doesn't Bound b1, b2; if (union180 && !this180) { // passed parameter Bound crosses the antimeridian, this Bound doesn't b1 = unionBound; b2 = this; } else { // this Bound crosses the antimeridian, passed parameter Bound doesn't b1 = this; b2 = unionBound; } // check for the case where the two Bound overlap on both edges such that the union // covers the planet. if (Double.compare(b1.getRight(), b2.getLeft()) >= 0 && Double.compare(b1.getLeft(), b2.getRight()) <= 0) { newLeft = MIN_LONGITUDE; newRight = MAX_LONGITUDE; } else { // first calculate the size of a union with the simple bound added to the left size1 = (Math.max(b1.getRight(), b2.getRight()) - MIN_LONGITUDE) + (MAX_LONGITUDE - b1.getLeft()); // first calculate the size of a union with the simple bound added to the right size2 = (b1.getRight() - MIN_LONGITUDE) + (MAX_LONGITUDE - Math.min(b1.getLeft(), b2.getLeft())); // now pick the smaller of the two if (Double.compare(size1, size2) <= 0) { newRight = Math.max(b1.getRight(), b2.getRight()); newLeft = b1.getLeft(); } else { newRight = b1.getRight(); newLeft = Math.min(b1.getLeft(), b2.getLeft()); } } } } if (Double.compare(newRight, newLeft) == 0) { return null; } // Keep the origin string from this if it's not blank, otherwise use the origin string from // the union Bound if (this.getOrigin() != null && !this.getOrigin().equals("")) { newOrigin = getOrigin(); } else { newOrigin = unionBound.getOrigin(); } return new Bound(newRight, newLeft, newTop, newBottom, newOrigin); } /** * Retrieve a collection of Bound objects which collectively comprise the entirety of this * Bound but individually do not cross the antimeridian and thus can be used in simple area * operations. The degenerate case will return this Bound. * * @return Iterable collection of Bound elements */ public Iterable<Bound> toSimpleBound() { Collection<Bound> c = new LinkedList<Bound>(); if (Double.compare(this.getLeft(), this.getRight()) < 0) { // simple case, just return this c.add(this); } else { // split the bound into two parts--one on either side of the antimeridian c.add(new Bound( MAX_LONGITUDE, this.getLeft(), this.getTop(), this.getBottom(), this.getOrigin())); c.add(new Bound( this.getRight(), MIN_LONGITUDE, this.getTop(), this.getBottom(), this.getOrigin())); } return Collections.unmodifiableCollection(c); } /** * Compares this bound to the specified bound. The bound comparison is based * on a comparison of area, latitude, and longitude in that order. * * @param comparisonBound * The bound to compare to. * @return 0 if equal, < 0 if this sorts before comparison (this is * "smaller"), and > 0 if this sorts before comparison (this is * "bigger") */ public int compareTo(Bound comparisonBound) { double areaT = 0.0, areaC = 0.0; int result; /* * This is a very simple "area" calculation just using the coordinate values, not accounting * for any projections. */ for (Bound b : this.toSimpleBound()) { areaT += (b.getRight() - b.getLeft()) * (b.getTop() - b.getBottom()); } for (Bound b : comparisonBound.toSimpleBound()) { areaC += (b.getRight() - b.getLeft()) * (b.getTop() - b.getBottom()); } // Use Double.compare (instead of < and >) to catch unique border cases result = Double.compare(areaT, areaC); if (result != 0) { return result; } result = Double.compare(this.getTop(), comparisonBound.getTop()); if (result != 0) { return result; } result = Double.compare(this.getBottom(), comparisonBound.getBottom()); if (result != 0) { return result; } result = Double.compare(this.getLeft(), comparisonBound.getLeft()); if (result != 0) { return result; } result = Double.compare(this.getRight(), comparisonBound.getRight()); if (result != 0) { return result; } String myOrigin = this.getOrigin(); String otherOrigin = comparisonBound.getOrigin(); // null origin is considered "less" than non-null origin if (myOrigin == null) { if (otherOrigin == null) { return 0; } else { return -1; } } else { if (otherOrigin == null) { return 1; } else { return myOrigin.compareTo(otherOrigin); } } } /** * {@inheritDoc} */ @Override public boolean equals(Object o) { if (o instanceof Bound) { return compareTo((Bound) o) == 0; } else { return false; } } /** * {@inheritDoc} */ @Override public int hashCode() { /* * As per the hashCode definition, this doesn't have to be unique it * just has to return the same value for any two objects that compare * equal. Using both id and version will provide a good distribution of * values but is simple to calculate. */ return (int) getId() + getVersion(); } /** * {@inheritDoc} */ @Override public Bound getWriteableInstance() { return this; } /** * ${@inheritDoc}. */ @Override public String toString() { return "Bound(top=" + getTop() + ", bottom=" + getBottom() + ", left=" + getLeft() + ", right=" + getRight() + ")"; } }