/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. * * This package contains documentation from OpenGIS specifications. * OpenGIS consortium's work is fully acknowledged here. */ package org.geotools.metadata.iso.extent; import java.util.Locale; import java.awt.geom.Rectangle2D; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.UndeclaredThrowableException; import static java.lang.Double.doubleToLongBits; import org.opengis.metadata.extent.GeographicBoundingBox; import org.opengis.referencing.operation.TransformException; import org.opengis.geometry.Envelope; import org.geotools.util.Utilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * Geographic position of the dataset. This is only an approximate so specifying the coordinate * reference system is unnecessary. The CRS shall be geographic with Greenwich prime meridian, * but the datum doesn't need to be WGS84. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * @author Touraïvane */ public class GeographicBoundingBoxImpl extends GeographicExtentImpl implements GeographicBoundingBox { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -3278089380004172514L; /** * The method for constructing a bounding box from an envelope. * Will be obtained only when first needed. */ private static Method constructor; /** * The method for constructing a string representation of this box. * Will be obtained only when first needed. */ private static Method toString; /** * A bounding box ranging from 180°W to 180°E and 90°S to 90°N. * * @since 2.2 */ public static final GeographicBoundingBox WORLD; static { final GeographicBoundingBoxImpl world = new GeographicBoundingBoxImpl(-180, 180, -90, 90); world.freeze(); WORLD = world; } /** * The western-most coordinate of the limit of the dataset extent. * The value is expressed in longitude in decimal degrees (positive east). */ private double westBoundLongitude; /** * The eastern-most coordinate of the limit of the dataset extent. * The value is expressed in longitude in decimal degrees (positive east). */ private double eastBoundLongitude; /** * The southern-most coordinate of the limit of the dataset extent. * The value is expressed in latitude in decimal degrees (positive north). */ private double southBoundLatitude; /** * The northern-most, coordinate of the limit of the dataset extent. * The value is expressed in latitude in decimal degrees (positive north). */ private double northBoundLatitude; /** * Constructs an initially empty geographic bounding box. */ public GeographicBoundingBoxImpl() { } /** * Constructs a geographic bounding box initialized to the same values than the specified one. * * @param box The existing box to use for initializing this geographic bounding box. * * @since 2.2 */ public GeographicBoundingBoxImpl(final GeographicBoundingBox box) { /* * We could invokes super(box), but we will perform the assignations explicitly here * for performance reason. Warning: it may be a problem if the user creates a subclass * and relies on the default MetadataEntity(Object) behavior. Rather than bothering * the user with a javadoc warning, I would prefer to find some trick to avoid this * issue (todo). */ super(); setBounds(box); } /** * Constructs a geographic bounding box from the specified envelope. If the envelope contains * a CRS, then the bounding box may be projected to a geographic one. Otherwise, the envelope * is assumed already in appropriate CRS. * <p> * When coordinate transformation is required, the target geographic CRS is not necessarly * {@linkplain org.geotools.referencing.crs.DefaultGeographicCRS#WGS84 WGS84}. This method * preserves the same {@linkplain org.opengis.referencing.datum.Ellipsoid ellipsoid} than * in the envelope CRS when possible. This is because geographic bounding box are only * approximative and the ISO specification do not mandates a particular CRS, so we avoid * transformations that are not strictly necessary. * <p> * <strong>Note:</strong> This constructor is available only if the referencing module is * on the classpath. * * @param envelope The envelope to use for initializing this geographic bounding box. * @throws UnsupportedOperationException if the referencing module is not on the classpath. * @throws TransformException if the envelope can't be transformed. * * @since 2.2 */ public GeographicBoundingBoxImpl(final Envelope envelope) throws TransformException { super(true); if (constructor == null) { // No need to synchronize; not a big deal if we set this field twice. constructor = getMethod("copy", new Class[] {Envelope.class, GeographicBoundingBoxImpl.class}); } try { invoke(constructor, new Object[] {envelope, this}); } catch (InvocationTargetException exception) { final Throwable cause = exception.getTargetException(); if (cause instanceof TransformException) { throw (TransformException) cause; } throw new UndeclaredThrowableException(cause); } } /** * Constructs a geographic bounding box from the specified rectangle. * The rectangle is assumed in {@linkplain DefaultGeographicCRS#WGS84 WGS 84} CRS. * * @param bounds The rectangle to use for initializing this geographic bounding box. */ public GeographicBoundingBoxImpl(final Rectangle2D bounds) { this(bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(), bounds.getMaxY()); } /** * Creates a geographic bounding box initialized to the specified values. * <p> * <strong>Caution:</strong> Arguments are expected in the same order than they appear in the * ISO 19115 specification. This is different than the order commonly found in Java world, * which is rather (<var>x</var><sub>min</sub>, <var>y</var><sub>min</sub>, * <var>x</var><sub>max</sub>, <var>y</var><sub>max</sub>). * * @param westBoundLongitude The minimal <var>x</var> value. * @param eastBoundLongitude The maximal <var>x</var> value. * @param southBoundLatitude The minimal <var>y</var> value. * @param northBoundLatitude The maximal <var>y</var> value. */ public GeographicBoundingBoxImpl(final double westBoundLongitude, final double eastBoundLongitude, final double southBoundLatitude, final double northBoundLatitude) { super(true); setBounds(westBoundLongitude, eastBoundLongitude, southBoundLatitude, northBoundLatitude); } /** * Returns the western-most coordinate of the limit of the * dataset extent. The value is expressed in longitude in * decimal degrees (positive east). * * @return The western-most longitude between -180 and +180°. */ public double getWestBoundLongitude() { return westBoundLongitude; } /** * Set the western-most coordinate of the limit of the * dataset extent. The value is expressed in longitude in * decimal degrees (positive east). * * @param newValue The western-most longitude between -180 and +180°. */ public synchronized void setWestBoundLongitude(final double newValue) { checkWritePermission(); westBoundLongitude = newValue; } /** * Returns the eastern-most coordinate of the limit of the * dataset extent. The value is expressed in longitude in * decimal degrees (positive east). * * @return The eastern-most longitude between -180 and +180°. */ public double getEastBoundLongitude() { return eastBoundLongitude; } /** * Set the eastern-most coordinate of the limit of the * dataset extent. The value is expressed in longitude in * decimal degrees (positive east). * * @param newValue The eastern-most longitude between -180 and +180°. */ public synchronized void setEastBoundLongitude(final double newValue) { checkWritePermission(); eastBoundLongitude = newValue; } /** * Returns the southern-most coordinate of the limit of the * dataset extent. The value is expressed in latitude in * decimal degrees (positive north). * * @return The southern-most latitude between -90 and +90°. */ public double getSouthBoundLatitude() { return southBoundLatitude; } /** * Set the southern-most coordinate of the limit of the * dataset extent. The value is expressed in latitude in * decimal degrees (positive north). * * @param newValue The southern-most latitude between -90 and +90°. */ public synchronized void setSouthBoundLatitude(final double newValue) { checkWritePermission(); southBoundLatitude = newValue; } /** * Returns the northern-most, coordinate of the limit of the * dataset extent. The value is expressed in latitude in * decimal degrees (positive north). * * @return The northern-most latitude between -90 and +90°. */ public double getNorthBoundLatitude() { return northBoundLatitude; } /** * Set the northern-most, coordinate of the limit of the * dataset extent. The value is expressed in latitude in * decimal degrees (positive north). * * @param newValue The northern-most latitude between -90 and +90°. */ public synchronized void setNorthBoundLatitude(final double newValue) { checkWritePermission(); northBoundLatitude = newValue; } /** * Sets the bounding box to the specified values. * <p> * <strong>Caution:</strong> Arguments are expected in the same order than they appear in the * ISO 19115 specification. This is different than the order commonly found in Java world, * which is rather (<var>x</var><sub>min</sub>, <var>y</var><sub>min</sub>, * <var>x</var><sub>max</sub>, <var>y</var><sub>max</sub>). * * @param westBoundLongitude The minimal <var>x</var> value. * @param eastBoundLongitude The maximal <var>x</var> value. * @param southBoundLatitude The minimal <var>y</var> value. * @param northBoundLatitude The maximal <var>y</var> value. * * @since 2.5 */ public synchronized void setBounds(final double westBoundLongitude, final double eastBoundLongitude, final double southBoundLatitude, final double northBoundLatitude) { checkWritePermission(); this.westBoundLongitude = westBoundLongitude; this.eastBoundLongitude = eastBoundLongitude; this.southBoundLatitude = southBoundLatitude; this.northBoundLatitude = northBoundLatitude; } /** * Sets the bounding box to the same values than the specified box. * * @param box The geographic bounding box to use for setting the values of this box. * * @since 2.5 */ public void setBounds(final GeographicBoundingBox box) { ensureNonNull("box", box); setInclusion(box.getInclusion()); setBounds(box.getWestBoundLongitude(), box.getEastBoundLongitude(), box.getSouthBoundLatitude(), box.getNorthBoundLatitude()); } /** * Adds a geographic bounding box to this box. If the {@linkplain #getInclusion inclusion} * status is the same for this box and the box to be added, then the resulting bounding box * is the union of the two boxes. If the {@linkplain #getInclusion inclusion} status are * opposite (<cite>exclusion</cite>), then this method attempt to exclude some area of * specified box from this box. The resulting bounding box is smaller if the exclusion can * be performed without ambiguity. * * @param box The geographic bounding box to add to this box. * * @since 2.2 */ public synchronized void add(final GeographicBoundingBox box) { checkWritePermission(); final double xmin = box.getWestBoundLongitude(); final double xmax = box.getEastBoundLongitude(); final double ymin = box.getSouthBoundLatitude(); final double ymax = box.getNorthBoundLatitude(); /* * Reminder: 'inclusion' is a mandatory attribute, so it should never be null for a * valid metadata object. If the metadata object is invalid, it is better to get a * an exception than having a code doing silently some inappropriate work. */ final Boolean inc1 = getInclusion(); ensureNonNull("inclusion", inc1); final Boolean inc2 = box.getInclusion(); ensureNonNull("inclusion", inc2); if (inc1.booleanValue() == inc2.booleanValue()) { if (xmin < westBoundLongitude) westBoundLongitude = xmin; if (xmax > eastBoundLongitude) eastBoundLongitude = xmax; if (ymin < southBoundLatitude) southBoundLatitude = ymin; if (ymax > northBoundLatitude) northBoundLatitude = ymax; } else { if (ymin <= southBoundLatitude && ymax >= northBoundLatitude) { if (xmin > westBoundLongitude) westBoundLongitude = xmin; if (xmax < eastBoundLongitude) eastBoundLongitude = xmax; } if (xmin <= westBoundLongitude && xmax >= eastBoundLongitude) { if (ymin > southBoundLatitude) southBoundLatitude = ymin; if (ymax < northBoundLatitude) northBoundLatitude = ymax; } } } /** * Sets this bounding box to the intersection of this box with the specified one. * The {@linkplain #getInclusion inclusion} status must be the same for both boxes. * * @param box The geographic bounding box to intersect with this box. * * @since 2.5 */ public synchronized void intersect(final GeographicBoundingBox box) { checkWritePermission(); final Boolean inc1 = getInclusion(); ensureNonNull("inclusion", inc1); final Boolean inc2 = box.getInclusion(); ensureNonNull("inclusion", inc2); if (inc1.booleanValue() != inc2.booleanValue()) { throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$1, "box")); } final double xmin = box.getWestBoundLongitude(); final double xmax = box.getEastBoundLongitude(); final double ymin = box.getSouthBoundLatitude(); final double ymax = box.getNorthBoundLatitude(); if (xmin > westBoundLongitude) westBoundLongitude = xmin; if (xmax < eastBoundLongitude) eastBoundLongitude = xmax; if (ymin > southBoundLatitude) southBoundLatitude = ymin; if (ymax < northBoundLatitude) northBoundLatitude = ymax; if (westBoundLongitude > eastBoundLongitude) { westBoundLongitude = eastBoundLongitude = 0.5 * (westBoundLongitude + eastBoundLongitude); } if (southBoundLatitude > northBoundLatitude) { southBoundLatitude = northBoundLatitude = 0.5 * (southBoundLatitude + northBoundLatitude); } } /** * Returns {@code true} if this bounding box is empty. * * @return {@code true} if this box is empty. * * @since 2.5 */ public boolean isEmpty() { // Use '!' in order to catch NaN values. return !(eastBoundLongitude > westBoundLongitude && northBoundLatitude > southBoundLatitude); } /** * Compares this geographic bounding box with the specified object for equality. * * @param object The object to compare for equality. * @return {@code true} if the given object is equals to this box. */ @Override public synchronized boolean equals(final Object object) { if (object == this) { return true; } // Above code really requires GeographicBoundingBoxImpl.class, not getClass(). if (object!=null && object.getClass().equals(GeographicBoundingBoxImpl.class)) { final GeographicBoundingBoxImpl that = (GeographicBoundingBoxImpl) object; return Utilities.equals(this.getInclusion(), that.getInclusion()) && doubleToLongBits(this.southBoundLatitude) == doubleToLongBits(that.southBoundLatitude) && doubleToLongBits(this.northBoundLatitude) == doubleToLongBits(that.northBoundLatitude) && doubleToLongBits(this.eastBoundLongitude) == doubleToLongBits(that.eastBoundLongitude) && doubleToLongBits(this.westBoundLongitude) == doubleToLongBits(that.westBoundLongitude); } return super.equals(object); } /** * Returns a hash code value for this extent. * * @todo Consider relying on the default implementation, since it cache the hash code. */ @Override public synchronized int hashCode() { if (!getClass().equals(GeographicBoundingBoxImpl.class)) { return super.hashCode(); } final Boolean inclusion = getInclusion(); int code = (inclusion != null) ? inclusion.hashCode() : 0; code += hashCode(southBoundLatitude); code += hashCode(northBoundLatitude); code += hashCode(eastBoundLongitude); code += hashCode(westBoundLongitude); return code; } /** * Returns a hash code value for the specified {@code double}. */ private static int hashCode(final double value) { final long code = doubleToLongBits(value); return (int)code ^ (int)(code >>> 32); } /** * Returns a string representation of this extent using a default angle pattern. */ @Override public String toString() { return toString(this, "DD°MM'SS.s\"", null); } /** * Returns a string representation of the specified extent using the specified angle pattern * and locale. See {@link AngleFormat} for a description of angle patterns. * * @param box The bounding box to format. * @param pattern The angle pattern (e.g. {@code DD°MM'SS.s"}. * @param locale The locale, or {@code null} for the default one. * @return A string representation of the given box in the given locale. * * @since 2.2 */ public static String toString(final GeographicBoundingBox box, final String pattern, final Locale locale) { if (toString == null) { // No need to synchronize. toString = getMethod("toString", new Class[] { GeographicBoundingBox.class, String.class, Locale.class}); } try { return String.valueOf(invoke(toString, new Object[] {box, pattern, locale})); } catch (InvocationTargetException exception) { throw new UndeclaredThrowableException(exception.getTargetException()); } } /** * Returns a helper method which depends on the referencing module. We use reflection * since we can't have a direct dependency to this module. */ private static Method getMethod(final String name, final Class<?>[] arguments) { try { return Class.forName("org.geotools.resources.BoundingBoxes").getMethod(name, arguments); } catch (ClassNotFoundException exception) { throw new UnsupportedOperationException(Errors.format( ErrorKeys.MISSING_MODULE_$1, "referencing"), exception); } catch (NoSuchMethodException exception) { // Should never happen if we didn't broke our BoundingBoxes helper class. throw new AssertionError(exception); } } /** * Invokes the specified method with the specified arguments. */ private static Object invoke(final Method method, final Object[] arguments) throws InvocationTargetException { try { return method.invoke(null, arguments); } catch (IllegalAccessException exception) { // Should never happen if our BoundingBoxes helper class is not broken. throw new AssertionError(exception); } catch (InvocationTargetException exception) { final Throwable cause = exception.getTargetException(); if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } if (cause instanceof Error) { throw (Error) cause; } throw exception; } } }