001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2004-2006, GeoTools Project Managment Committee (PMC)
005: * (C) 2004, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation;
010: * version 2.1 of the License.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * This package contains documentation from OpenGIS specifications.
018: * OpenGIS consortium's work is fully acknowledged here.
019: */
020: package org.geotools.metadata.iso.extent;
021:
022: // J2SE dependencies
023: import java.util.Locale;
024: import java.awt.geom.Rectangle2D;
025: import java.lang.reflect.Method;
026: import java.lang.reflect.InvocationTargetException;
027: import java.lang.reflect.UndeclaredThrowableException;
028:
029: // OpenGIS dependencies
030: import org.opengis.metadata.extent.GeographicBoundingBox;
031: import org.opengis.referencing.operation.TransformException;
032: import org.opengis.geometry.Envelope;
033:
034: // Geotools dependencies
035: import org.geotools.resources.Utilities;
036: import org.geotools.resources.i18n.Errors;
037: import org.geotools.resources.i18n.ErrorKeys;
038:
039: /**
040: * Geographic position of the dataset. This is only an approximate
041: * so specifying the co-ordinate reference system is unnecessary.
042: *
043: * @since 2.1
044: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/metadata/iso/extent/GeographicBoundingBoxImpl.java $
045: * @version $Id: GeographicBoundingBoxImpl.java 29091 2008-02-05 19:27:31Z desruisseaux $
046: * @author Martin Desruisseaux
047: * @author Touraïvane
048: */
049: public class GeographicBoundingBoxImpl extends GeographicExtentImpl
050: implements GeographicBoundingBox {
051: /**
052: * Serial number for interoperability with different versions.
053: */
054: private static final long serialVersionUID = -3278089380004172514L;
055:
056: /**
057: * The method for constructing a bounding box from an envelope.
058: * Will be obtained only when first needed.
059: */
060: private static Method constructor;
061:
062: /**
063: * The method for constructing a string representation of this box.
064: * Will be obtained only when first needed.
065: */
066: private static Method toString;
067:
068: /**
069: * A bounding box ranging from 180°W to 180°E and 90°S to 90°N.
070: *
071: * @since 2.2
072: */
073: public static final GeographicBoundingBox WORLD;
074: static {
075: final GeographicBoundingBoxImpl world = new GeographicBoundingBoxImpl(
076: -180, 180, -90, 90);
077: world.freeze();
078: WORLD = world;
079: }
080:
081: /**
082: * The western-most coordinate of the limit of the dataset extent.
083: * The value is expressed in longitude in decimal degrees (positive east).
084: */
085: private double westBoundLongitude;
086:
087: /**
088: * The eastern-most coordinate of the limit of the dataset extent.
089: * The value is expressed in longitude in decimal degrees (positive east).
090: */
091: private double eastBoundLongitude;
092:
093: /**
094: * The southern-most coordinate of the limit of the dataset extent.
095: * The value is expressed in latitude in decimal degrees (positive north).
096: */
097: private double southBoundLatitude;
098:
099: /**
100: * The northern-most, coordinate of the limit of the dataset extent.
101: * The value is expressed in latitude in decimal degrees (positive north).
102: */
103: private double northBoundLatitude;
104:
105: /**
106: * Constructs an initially empty geographic bounding box.
107: */
108: public GeographicBoundingBoxImpl() {
109: }
110:
111: /**
112: * Constructs a geographic bounding box initialized to the same values than the specified one.
113: * <p>
114: * <strong>Caution:</strong> Arguments are expected in the same order than they appear in the
115: * ISO 19115 specification. This is different than the order commonly found in Java world,
116: * which is rather (<var>x</var><sub>min</sub>, <var>y</var><sub>min</sub>,
117: * <var>x</var><sub>max</sub>, <var>y</var><sub>max</sub>).
118: *
119: * @since 2.2
120: */
121: public GeographicBoundingBoxImpl(final GeographicBoundingBox box) {
122: /*
123: * We could invokes super(box), but we will perform the assignations explicitly here
124: * for performance reason. Warning: it may be a problem if the user creates a subclass
125: * and relies on the default MetadataEntity(Object) behavior. Rather than bothering
126: * the user with a javadoc warning, I would prefer to find some trick to avoid this
127: * issue (todo).
128: */
129: super ();
130: setInclusion(box.getInclusion());
131: setWestBoundLongitude(box.getWestBoundLongitude());
132: setEastBoundLongitude(box.getEastBoundLongitude());
133: setSouthBoundLatitude(box.getSouthBoundLatitude());
134: setNorthBoundLatitude(box.getNorthBoundLatitude());
135: }
136:
137: /**
138: * Constructs a geographic bounding box from the specified envelope. If the envelope contains
139: * a CRS, then the bounding box will be projected to the {@linkplain DefaultGeographicCRS#WGS84
140: * WGS 84} CRS. Otherwise, the envelope is assumed already in WGS 84 CRS.
141: * <p>
142: * <strong>Note:</strong> This method is available only if the referencing module is
143: * on the classpath.
144: *
145: * @param envelope The envelope to use for initializing this geographic bounding box.
146: * @throws UnsupportedOperationException if the referencing module is not on the classpath.
147: * @throws TransformException if the envelope can't be transformed.
148: *
149: * @since 2.2
150: */
151: public GeographicBoundingBoxImpl(final Envelope envelope)
152: throws TransformException {
153: super (true);
154: if (constructor == null) {
155: // No need to synchronize; not a big deal if we set this field twice.
156: constructor = getMethod("copy", new Class[] {
157: Envelope.class, GeographicBoundingBoxImpl.class });
158: }
159: try {
160: invoke(constructor, new Object[] { envelope, this });
161: } catch (InvocationTargetException exception) {
162: final Throwable cause = exception.getTargetException();
163: if (cause instanceof TransformException) {
164: throw (TransformException) cause;
165: }
166: throw new UndeclaredThrowableException(cause);
167: }
168: }
169:
170: /**
171: * Constructs a geographic bounding box from the specified rectangle.
172: * The rectangle is assumed in {@linkplain DefaultGeographicCRS#WGS84 WGS 84} CRS.
173: */
174: public GeographicBoundingBoxImpl(final Rectangle2D bounds) {
175: this (bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(),
176: bounds.getMaxY());
177: }
178:
179: /**
180: * Creates a geographic bounding box initialized to the specified values.
181: */
182: public GeographicBoundingBoxImpl(final double westBoundLongitude,
183: final double eastBoundLongitude,
184: final double southBoundLatitude,
185: final double northBoundLatitude) {
186: super (true);
187: setWestBoundLongitude(westBoundLongitude);
188: setEastBoundLongitude(eastBoundLongitude);
189: setSouthBoundLatitude(southBoundLatitude);
190: setNorthBoundLatitude(northBoundLatitude);
191: }
192:
193: /**
194: * Returns the western-most coordinate of the limit of the
195: * dataset extent. The value is expressed in longitude in
196: * decimal degrees (positive east).
197: *
198: * @return The western-most longitude between -180 and +180°.
199: */
200: public double getWestBoundLongitude() {
201: return westBoundLongitude;
202: }
203:
204: /**
205: * Set the western-most coordinate of the limit of the
206: * dataset extent. The value is expressed in longitude in
207: * decimal degrees (positive east).
208: */
209: public synchronized void setWestBoundLongitude(final double newValue) {
210: checkWritePermission();
211: westBoundLongitude = newValue;
212: }
213:
214: /**
215: * Returns the eastern-most coordinate of the limit of the
216: * dataset extent. The value is expressed in longitude in
217: * decimal degrees (positive east).
218: *
219: * @return The eastern-most longitude between -180 and +180°.
220: */
221: public double getEastBoundLongitude() {
222: return eastBoundLongitude;
223: }
224:
225: /**
226: * Set the eastern-most coordinate of the limit of the
227: * dataset extent. The value is expressed in longitude in
228: * decimal degrees (positive east).
229: */
230: public synchronized void setEastBoundLongitude(final double newValue) {
231: checkWritePermission();
232: eastBoundLongitude = newValue;
233: }
234:
235: /**
236: * Returns the southern-most coordinate of the limit of the
237: * dataset extent. The value is expressed in latitude in
238: * decimal degrees (positive north).
239: *
240: * @return The southern-most latitude between -90 and +90°.
241: */
242: public double getSouthBoundLatitude() {
243: return southBoundLatitude;
244: }
245:
246: /**
247: * Set the southern-most coordinate of the limit of the
248: * dataset extent. The value is expressed in latitude in
249: * decimal degrees (positive north).
250: */
251: public synchronized void setSouthBoundLatitude(final double newValue) {
252: checkWritePermission();
253: southBoundLatitude = newValue;
254: }
255:
256: /**
257: * Returns the northern-most, coordinate of the limit of the
258: * dataset extent. The value is expressed in latitude in
259: * decimal degrees (positive north).
260: *
261: * @return The northern-most latitude between -90 and +90°.
262: */
263: public double getNorthBoundLatitude() {
264: return northBoundLatitude;
265: }
266:
267: /**
268: * Set the northern-most, coordinate of the limit of the
269: * dataset extent. The value is expressed in latitude in
270: * decimal degrees (positive north).
271: */
272: public synchronized void setNorthBoundLatitude(final double newValue) {
273: checkWritePermission();
274: northBoundLatitude = newValue;
275: }
276:
277: /**
278: * Adds a geographic bounding box to this box. If the {@linkplain #getInclusion inclusion}
279: * status is the same for this box and the box to be added, then the resulting bounding box
280: * is the union of the two boxes. If the {@linkplain #getInclusion inclusion} status are
281: * opposite (<cite>exclusion</cite>), then this method attempt to exclude the some area of
282: * specified box from this box. The resulting bounding box is smaller if the exclusion can
283: * be performed without ambiguity.
284: *
285: * @since 2.2
286: */
287: public synchronized void add(final GeographicBoundingBox box) {
288: checkWritePermission();
289: final double xmin = box.getWestBoundLongitude();
290: final double xmax = box.getEastBoundLongitude();
291: final double ymin = box.getSouthBoundLatitude();
292: final double ymax = box.getNorthBoundLatitude();
293: /*
294: * Reminder: 'inclusion' is a mandatory attribute, so it should never be null for a
295: * valid metadata object. If the metadata object is invalid, it is better to get a
296: * an exception than having a code doing silently some inappropriate work.
297: */
298: final Boolean inc1 = getInclusion();
299: ensureNonNull("inclusion", inc1);
300: final Boolean inc2 = box.getInclusion();
301: ensureNonNull("inclusion", inc2);
302: if (inc1.booleanValue() == inc2.booleanValue()) {
303: if (xmin < westBoundLongitude)
304: westBoundLongitude = xmin;
305: if (xmax > eastBoundLongitude)
306: eastBoundLongitude = xmax;
307: if (ymin < southBoundLatitude)
308: southBoundLatitude = ymin;
309: if (ymax > northBoundLatitude)
310: northBoundLatitude = ymax;
311: } else {
312: if (ymin <= southBoundLatitude
313: && ymax >= northBoundLatitude) {
314: if (xmin > westBoundLongitude)
315: westBoundLongitude = xmin;
316: if (xmax < eastBoundLongitude)
317: eastBoundLongitude = xmax;
318: }
319: if (xmin <= westBoundLongitude
320: && xmax >= eastBoundLongitude) {
321: if (ymin > southBoundLatitude)
322: southBoundLatitude = ymin;
323: if (ymax < northBoundLatitude)
324: northBoundLatitude = ymax;
325: }
326: }
327: }
328:
329: /**
330: * Compares this geographic bounding box with the specified object for equality.
331: */
332: public synchronized boolean equals(final Object object) {
333: if (object == this ) {
334: return true;
335: }
336: // Above code really requires GeographicBoundingBoxImpl.class, not getClass().
337: if (object != null
338: && object.getClass().equals(
339: GeographicBoundingBoxImpl.class)) {
340: final GeographicBoundingBoxImpl that = (GeographicBoundingBoxImpl) object;
341: return Utilities.equals(this .getInclusion(), that
342: .getInclusion())
343: && Double.doubleToLongBits(this .southBoundLatitude) == Double
344: .doubleToLongBits(that.southBoundLatitude)
345: && Double.doubleToLongBits(this .northBoundLatitude) == Double
346: .doubleToLongBits(that.northBoundLatitude)
347: && Double.doubleToLongBits(this .eastBoundLongitude) == Double
348: .doubleToLongBits(that.eastBoundLongitude)
349: && Double.doubleToLongBits(this .westBoundLongitude) == Double
350: .doubleToLongBits(that.westBoundLongitude);
351: }
352: return super .equals(object);
353: }
354:
355: /**
356: * Returns a hash code value for this extent.
357: *
358: * @todo Consider relying on the default implementation, since it cache the hash code.
359: */
360: public synchronized int hashCode() {
361: if (!getClass().equals(GeographicBoundingBoxImpl.class)) {
362: return super .hashCode();
363: }
364: final Boolean inclusion = getInclusion();
365: int code = (inclusion != null) ? inclusion.hashCode() : 0;
366: code += hashCode(southBoundLatitude);
367: code += hashCode(northBoundLatitude);
368: code += hashCode(eastBoundLongitude);
369: code += hashCode(westBoundLongitude);
370: return code;
371: }
372:
373: /**
374: * Returns a hash code value for the specified {@code double}.
375: */
376: private static int hashCode(final double value) {
377: final long code = Double.doubleToLongBits(value);
378: return (int) code ^ (int) (code >>> 32);
379: }
380:
381: /**
382: * Returns a string representation of this extent using a default angle pattern.
383: */
384: public String toString() {
385: return toString(this , "DD°MM'SS.s\"", null);
386: }
387:
388: /**
389: * Returns a string representation of the specified extent using the specified angle pattern
390: * and locale. See {@link AngleFormat} for a description of angle patterns.
391: *
392: * @param box The bounding box to format.
393: * @param pattern The angle pattern (e.g. {@code DD°MM'SS.s"}.
394: * @param locale The locale, or {@code null} for the default one.
395: *
396: * @since 2.2
397: */
398: public static String toString(final GeographicBoundingBox box,
399: final String pattern, final Locale locale) {
400: if (toString == null) {
401: // No need to synchronize.
402: toString = getMethod("toString", new Class[] {
403: GeographicBoundingBox.class, String.class,
404: Locale.class });
405: }
406: try {
407: return String.valueOf(invoke(toString, new Object[] { box,
408: pattern, locale }));
409: } catch (InvocationTargetException exception) {
410: throw new UndeclaredThrowableException(exception
411: .getTargetException());
412: }
413: }
414:
415: /**
416: * Returns a helper method which depends on the referencing module. We use reflection
417: * since we can't have a direct dependency to this module.
418: */
419: private static Method getMethod(final String name,
420: final Class[] arguments) {
421: try {
422: return Class
423: .forName("org.geotools.resources.BoundingBoxes")
424: .getMethod(name, arguments);
425: } catch (ClassNotFoundException exception) {
426: // Simplify when we will be allowed to compile for J2SE 1.5.
427: UnsupportedOperationException e = new UnsupportedOperationException(
428: Errors.format(ErrorKeys.MISSING_MODULE_$1,
429: "referencing"));
430: e.initCause(exception);
431: throw e;
432: } catch (NoSuchMethodException exception) {
433: // Should never happen if we didn't broke our BoundingBoxes helper class.
434: throw new AssertionError(exception);
435: }
436: }
437:
438: /**
439: * Invokes the specified method with the specified arguments.
440: */
441: private static Object invoke(final Method method,
442: final Object[] arguments) throws InvocationTargetException {
443: try {
444: return method.invoke(null, arguments);
445: } catch (IllegalAccessException exception) {
446: // Should never happen if our BoundingBoxes helper class is not broken.
447: throw new AssertionError(exception);
448: } catch (InvocationTargetException exception) {
449: final Throwable cause = exception.getTargetException();
450: if (cause instanceof RuntimeException) {
451: throw (RuntimeException) cause;
452: }
453: if (cause instanceof Error) {
454: throw (Error) cause;
455: }
456: throw exception;
457: }
458: }
459: }
|