If there is no intersection between the two extents, then this method sets both minimum and + * maximum values to {@linkplain Double#NaN}. If either this extent or the specified extent has NaN + * bounds, then the corresponding bounds of the intersection result will also be NaN.

+ * + * @param other the vertical extent to intersect with this extent. + * @throws MismatchedReferenceSystemException if the two extents do not use the same datum, ignoring metadata. + * + * @see Extents#intersection(VerticalExtent, VerticalExtent) + * @see org.apache.sis.geometry.GeneralEnvelope#intersect(Envelope) + * + * @since 0.8 + */ + public void intersect(final VerticalExtent other) throws MismatchedReferenceSystemException { + checkWritePermission(); + ArgumentChecks.ensureNonNull("other", other); + Double min = other.getMinimumValue(); + Double max = other.getMaximumValue(); + try { + final MathTransform1D cv = getConversionFrom(other.getVerticalCRS()); + if (isReversing(cv, min, max)) { + Double tmp = min; + min = max; + max = tmp; + } + /* + * If minimumValue is NaN, keep it unchanged (because x > minimumValue is false) + * in order to preserve the NilReason. Conversely if min is NaN, then we want to + * take it without conversion for preserving its NilReason. + */ + if (min != null) { + if (minimumValue == null || min.isNaN() || (min = convert(cv, min)) > minimumValue) { + minimumValue = min; + } + } + if (max != null) { + if (maximumValue == null || max.isNaN() || (max = convert(cv, max)) < maximumValue) { + maximumValue = max; + } + } + } catch (UnsupportedOperationException | FactoryException | ClassCastException | TransformException e) { + throw new MismatchedReferenceSystemException(Errors.format(Errors.Keys.IncompatiblePropertyValue_1, "verticalCRS"), e); + } + if (minimumValue != null && maximumValue != null && minimumValue > maximumValue) { + minimumValue = maximumValue = NilReason.MISSING.createNilObject(Double.class); + } + } + + /** + * Returns the conversion from the given CRS to the CRS of this extent, or {@code null} if none or unknown. + * The returned {@code MathTransform1D} may apply unit conversions or axis direction reversal, but usually + * not datum shift. + * + * @param source the CRS from which to perform the conversions, or {@code null} if unknown. + * @return the conversion from {@code source}, or {@code null} if none or unknown. + * @throws UnsupportedOperationException if the {@code sis-referencing} module is not on the classpath. + * @throws FactoryException if the coordinate operation factory is not available. + * @throws ClassCastException if the conversion is not an instance of {@link MathTransform1D}. + */ + private MathTransform1D getConversionFrom(final VerticalCRS source) throws FactoryException { + if (!Utilities.equalsIgnoreMetadata(verticalCRS, source) && verticalCRS != null && source != null) { + final MathTransform1D cv = (MathTransform1D) ReferencingServices.getInstance() + .getCoordinateOperationFactory(null, null, null, null) + .createOperation(source, verticalCRS).getMathTransform(); + if (!cv.isIdentity()) { + return cv; + } + } + return null; + } + + /** + * Returns {@code true} if the given conversion seems to change the axis direction. + * This happen for example with conversions from "Elevation" axis to "Depth" axis. + * In case of doubt, this method returns {@code false}. + * + *
Note about alternatives: + * we could compare axis directions instead, but it would not work with user-defined directions + * or user-defined unit conversions with negative scale factor (should never happen, but we are + * paranoiac). We could compare the minimum and maximum values after conversions, but it would + * not work if one or both values are {@code null} or {@code NaN}. Since we want to preserve + * {@link NilReason}, we still need to know if axes are reversed in order to put the nil reason + * in the right location.
- *
• {@link #getGeographicBoundingBox(Extent)}, {@link #getVerticalRange(Extent)} - * and {@link #getDate(Extent, double)} - * for fetching geographic or temporal components in a convenient form.
• - *
• Methods for computing {@linkplain #intersection intersection} of bounding boxes - * and {@linkplain #area area} estimations.
• + *
• {@linkplain #getGeographicBoundingBox Fetching geographic}, + * {@linkplain #getVerticalRange vertical} or + * {@linkplain #getDate temporal components} in a convenient form.
• + *
• Computing {@linkplain #intersection intersection} of bounding boxes
• + *
• Computing {@linkplain #area area} estimations.
• *
* * @author Martin Desruisseaux (Geomatys) @@ -436,33 +439,6 @@ public final class Extents extends Stati } /** - * Returns the intersection of the given geographic bounding boxes. If any of the arguments is {@code null}, - * then this method returns the other argument (which may be null). Otherwise this method returns a box which - * is the intersection of the two given boxes. - * - *

This method never modify the given boxes, but may return directly one of the given arguments if it - * already represents the intersection result.

- * - * @param b1 the first bounding box, or {@code null}. - * @param b2 the second bounding box, or {@code null}. - * @return the intersection (may be any of the {@code b1} or {@code b2} argument if unchanged), - * or {@code null} if the two given boxes are null. - * @throws IllegalArgumentException if the {@linkplain DefaultGeographicBoundingBox#getInclusion() inclusion status} - * is not the same for both boxes. - * - * @see DefaultGeographicBoundingBox#intersect(GeographicBoundingBox) - * - * @since 0.4 - */ - public static GeographicBoundingBox intersection(final GeographicBoundingBox b1, final GeographicBoundingBox b2) { - if (b1 == null) return b2; - if (b2 == null || b2 == b1) return b1; - final DefaultGeographicBoundingBox box = new DefaultGeographicBoundingBox(b1); - box.intersect(b2); - return box; - } - - /** * Returns an estimation of the area (in square metres) of the given bounding box. * Since {@code GeographicBoundingBox} provides only approximative information (for example * it does not specify the datum), the value returned by this method is also approximative. @@ -496,4 +472,145 @@ public final class Extents extends Stati max(0, sin(toRadians(box.getNorthBoundLatitude())) - sin(toRadians(box.getSouthBoundLatitude()))); } + + /** + * Returns the intersection of the given geographic bounding boxes. If any of the arguments is {@code null}, + * then this method returns the other argument (which may be null). Otherwise this method returns a box which + * is the intersection of the two given boxes. + * + *

This method never modify the given boxes, but may return directly one of the given arguments if it + * already represents the intersection result.

+ * + * @param b1 the first bounding box, or {@code null}. + * @param b2 the second bounding box, or {@code null}. + * @return the intersection (may be any of the {@code b1} or {@code b2} argument if unchanged), + * or {@code null} if the two given boxes are null. + * @throws IllegalArgumentException if the {@linkplain DefaultGeographicBoundingBox#getInclusion() inclusion status} + * is not the same for both boxes. + * + * @see DefaultGeographicBoundingBox#intersect(GeographicBoundingBox) + * + * @since 0.4 + */ + public static GeographicBoundingBox intersection(final GeographicBoundingBox b1, final GeographicBoundingBox b2) { + if (b1 == null) return b2; + if (b2 == null || b2 == b1) return b1; + final DefaultGeographicBoundingBox box = new DefaultGeographicBoundingBox(b1); + box.intersect(b2); + if (box.equals(b1, ComparisonMode.BY_CONTRACT)) return b1; + if (box.equals(b2, ComparisonMode.BY_CONTRACT)) return b2; + return box; + } + + /** + * May compute an intersection between the given geographic extents. + * Current implementation supports only {@link GeographicBoundingBox}; + * all other kinds are handled as if they were {@code null}. + * + *

We may improve this method in future Apache SIS version, but it is not yet clear how. + * For example how to handle {@link GeographicDescription} or {@link BoundingPolygon}? + * This method should not be public before we find a better contract.

+ */ + static GeographicExtent intersection(final GeographicExtent e1, final GeographicExtent e2) { + return intersection(e1 instanceof GeographicBoundingBox ? (GeographicBoundingBox) e1 : null, + e2 instanceof GeographicBoundingBox ? (GeographicBoundingBox) e2 : null); + } + + /** + * Returns the intersection of the given vertical extents. If any of the arguments is {@code null}, + * then this method returns the other argument (which may be null). Otherwise this method returns a + * vertical extent which is the intersection of the two given extents. + * + *

This method never modify the given extents, but may return directly one of the given arguments + * if it already represents the intersection result.

+ * + *
Advantage and inconvenient of this method
+ * This method can not intersect extents defined with different datums because height transformations + * generally require the geodetic positions (latitudes and longitudes) of the heights to transform. + * For more general transformations, it is better to convert all extent components into a single envelope, + * then {@linkplain org.apache.sis.geometry.Envelopes#transform(CoordinateOperation, Envelope) transform + * the envelope at once}. On the other hand, this {@code intersect(…)} method preserves better + * the {@link org.apache.sis.xml.NilReason} (if any). + * + * @param e1 the first extent, or {@code null}. + * @param e2 the second extent, or {@code null}. + * @return the intersection (may be any of the {@code e1} or {@code e2} argument if unchanged), + * or {@code null} if the two given extents are null. + * @throws MismatchedReferenceSystemException if the two extents do not use the same datum, ignoring metadata. + * + * @see DefaultVerticalExtent#intersect(VerticalExtent) + * + * @since 0.8 + */ + public static VerticalExtent intersection(final VerticalExtent e1, final VerticalExtent e2) { + if (e1 == null) return e2; + if (e2 == null || e2 == e1) return e1; + final DefaultVerticalExtent extent = new DefaultVerticalExtent(e1); + extent.intersect(e2); + if (extent.equals(e1, ComparisonMode.BY_CONTRACT)) return e1; + if (extent.equals(e2, ComparisonMode.BY_CONTRACT)) return e2; + return extent; + } + + /** + * Returns the intersection of the given temporal extents. If any of the arguments is {@code null}, + * then this method returns the other argument (which may be null). Otherwise this method returns a + * temporal extent which is the intersection of the two given extents. + * + *

This method never modify the given extents, but may return directly one of the given arguments + * if it already represents the intersection result.

+ * + * @param e1 the first extent, or {@code null}. + * @param e2 the second extent, or {@code null}. + * @return the intersection (may be any of the {@code e1} or {@code e2} argument if unchanged), + * or {@code null} if the two given extents are null. + * @throws UnsupportedOperationException if no implementation of {@code TemporalFactory} has been found + * on the classpath. + * + * @see DefaultTemporalExtent#intersect(TemporalExtent) + * + * @since 0.8 + */ + public static TemporalExtent intersection(final TemporalExtent e1, final TemporalExtent e2) { + if (e1 == null) return e2; + if (e2 == null || e2 == e1) return e1; + final DefaultTemporalExtent extent = new DefaultTemporalExtent(e1); + extent.intersect(e2); + if (extent.equals(e1, ComparisonMode.BY_CONTRACT)) return e1; + if (extent.equals(e2, ComparisonMode.BY_CONTRACT)) return e2; + return extent; + } + + /** + * Returns the intersection of the given extents. If any of the arguments is {@code null}, + * then this method returns the other argument (which may be null). Otherwise this method + * returns an extent which is the intersection of all geographic, vertical and temporal + * elements in the two given extents. + * + *

This method never modify the given extents, but may return directly one of the given + * arguments if it already represents the intersection result.

• The schema name must be {@code "metadata"}, as this is the name used unquoted in SQL scripts.