| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.metadata.iso.extent; |
| |
| import javax.xml.bind.annotation.XmlType; |
| import javax.xml.bind.annotation.XmlElement; |
| import javax.xml.bind.annotation.XmlRootElement; |
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; |
| import org.opengis.geometry.Envelope; |
| import org.opengis.util.FactoryException; |
| import org.opengis.referencing.crs.VerticalCRS; |
| import org.opengis.referencing.operation.MathTransform1D; |
| import org.opengis.referencing.operation.TransformException; |
| import org.opengis.metadata.extent.VerticalExtent; |
| import org.apache.sis.metadata.iso.ISOMetadata; |
| import org.apache.sis.internal.jaxb.gco.GO_Real; |
| import org.apache.sis.internal.metadata.ReferencingServices; |
| import org.apache.sis.math.MathFunctions; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.Utilities; |
| import org.apache.sis.xml.NilReason; |
| |
| |
| /** |
| * Vertical domain of dataset. |
| * The following properties are mandatory in a well-formed metadata according ISO 19115: |
| * |
| * <div class="preformat">{@code EX_VerticalExtent} |
| * {@code ├─minimumValue……} The lowest vertical extent contained in the dataset. |
| * {@code ├─maximumValue……} The highest vertical extent contained in the dataset. |
| * {@code └─verticalCRS………} Information about the vertical coordinate reference system to which the maximum and minimum elevation values are measured. The CRS identification includes unit of measure.</div> |
| * |
| * In addition to the standard properties, SIS provides the following methods: |
| * <ul> |
| * <li>{@link #setBounds(Envelope)} for setting the extent from the given envelope.</li> |
| * </ul> |
| * |
| * <h2>Limitations</h2> |
| * <ul> |
| * <li>Instances of this class are not synchronized for multi-threading. |
| * Synchronization, if needed, is caller's responsibility.</li> |
| * <li>Serialized objects of this class are not guaranteed to be compatible with future Apache SIS releases. |
| * Serialization support is appropriate for short term storage or RMI between applications running the |
| * same version of Apache SIS. For long term storage, use {@link org.apache.sis.xml.XML} instead.</li> |
| * <li>Coordinate Reference System can not be specified by identifier only; they have to be specified in full. |
| * See <a href="https://issues.apache.org/jira/browse/SIS-397">SIS-397</a>.</li> |
| * </ul> |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| * @author Touraïvane (IRD) |
| * @author Cédric Briançon (Geomatys) |
| * @version 1.0 |
| * @since 0.3 |
| * @module |
| */ |
| @XmlType(name = "EX_VerticalExtent_Type", propOrder = { |
| "minimumValue", |
| "maximumValue", |
| "verticalCRS" |
| }) |
| @XmlRootElement(name = "EX_VerticalExtent") |
| public class DefaultVerticalExtent extends ISOMetadata implements VerticalExtent { |
| /** |
| * Serial number for inter-operability with different versions. |
| */ |
| private static final long serialVersionUID = -1963873471175296153L; |
| |
| /** |
| * The lowest vertical extent contained in the dataset. |
| */ |
| private Double minimumValue; |
| |
| /** |
| * The highest vertical extent contained in the dataset. |
| */ |
| private Double maximumValue; |
| |
| /** |
| * Provides information about the vertical coordinate reference system to |
| * which the maximum and minimum elevation values are measured. The CRS |
| * identification includes unit of measure. |
| */ |
| private VerticalCRS verticalCRS; |
| |
| /** |
| * Constructs an initially empty vertical extent. |
| */ |
| public DefaultVerticalExtent() { |
| } |
| |
| /** |
| * Creates a vertical extent initialized to the specified values. |
| * |
| * @param minimumValue the lowest vertical extent contained in the dataset, or {@link Double#NaN} if none. |
| * @param maximumValue the highest vertical extent contained in the dataset, or {@link Double#NaN} if none. |
| * @param verticalCRS the information about the vertical coordinate reference system, or {@code null}. |
| */ |
| public DefaultVerticalExtent(final double minimumValue, |
| final double maximumValue, |
| final VerticalCRS verticalCRS) |
| { |
| if (!Double.isNaN(minimumValue)) this.minimumValue = minimumValue; |
| if (!Double.isNaN(maximumValue)) this.maximumValue = maximumValue; |
| this.verticalCRS = verticalCRS; |
| } |
| |
| /** |
| * Constructs a new instance initialized with the values from the specified metadata object. |
| * This is a <cite>shallow</cite> copy constructor, since the other metadata contained in the |
| * given object are not recursively copied. |
| * |
| * @param object the metadata to copy values from, or {@code null} if none. |
| * |
| * @see #castOrCopy(VerticalExtent) |
| */ |
| public DefaultVerticalExtent(final VerticalExtent object) { |
| super(object); |
| if (object != null) { |
| minimumValue = object.getMinimumValue(); |
| maximumValue = object.getMaximumValue(); |
| verticalCRS = object.getVerticalCRS(); |
| } |
| } |
| |
| /** |
| * Returns a SIS metadata implementation with the values of the given arbitrary implementation. |
| * This method performs the first applicable action in the following choices: |
| * |
| * <ul> |
| * <li>If the given object is {@code null}, then this method returns {@code null}.</li> |
| * <li>Otherwise if the given object is already an instance of |
| * {@code DefaultVerticalExtent}, then it is returned unchanged.</li> |
| * <li>Otherwise a new {@code DefaultVerticalExtent} instance is created using the |
| * {@linkplain #DefaultVerticalExtent(VerticalExtent) copy constructor} |
| * and returned. Note that this is a <cite>shallow</cite> copy operation, since the other |
| * metadata contained in the given object are not recursively copied.</li> |
| * </ul> |
| * |
| * @param object the object to get as a SIS implementation, or {@code null} if none. |
| * @return a SIS implementation containing the values of the given object (may be the |
| * given object itself), or {@code null} if the argument was null. |
| */ |
| public static DefaultVerticalExtent castOrCopy(final VerticalExtent object) { |
| if (object == null || object instanceof DefaultVerticalExtent) { |
| return (DefaultVerticalExtent) object; |
| } |
| return new DefaultVerticalExtent(object); |
| } |
| |
| /** |
| * Returns the lowest vertical extent contained in the dataset. |
| * |
| * @return the lowest vertical extent, or {@code null}. |
| */ |
| @Override |
| @XmlElement(name = "minimumValue", required = true) |
| @XmlJavaTypeAdapter(GO_Real.class) |
| public Double getMinimumValue() { |
| return minimumValue; |
| } |
| |
| /** |
| * Sets the lowest vertical extent contained in the dataset. |
| * |
| * @param newValue the new minimum value. |
| */ |
| public void setMinimumValue(final Double newValue) { |
| checkWritePermission(minimumValue); |
| minimumValue = newValue; |
| } |
| |
| /** |
| * Returns the highest vertical extent contained in the dataset. |
| * |
| * @return the highest vertical extent, or {@code null}. |
| */ |
| @Override |
| @XmlElement(name = "maximumValue", required = true) |
| @XmlJavaTypeAdapter(GO_Real.class) |
| public Double getMaximumValue() { |
| return maximumValue; |
| } |
| |
| /** |
| * Sets the highest vertical extent contained in the dataset. |
| * |
| * @param newValue the new maximum value. |
| */ |
| public void setMaximumValue(final Double newValue) { |
| checkWritePermission(maximumValue); |
| maximumValue = newValue; |
| } |
| |
| /** |
| * Provides information about the vertical coordinate reference system to |
| * which the maximum and minimum elevation values are measured. |
| * The CRS identification includes unit of measure. |
| * |
| * @return the vertical CRS, or {@code null}. |
| * |
| * @see <a href="https://issues.apache.org/jira/browse/SIS-397">SIS-397</a> |
| */ |
| @Override |
| @XmlElement(name = "verticalCRS") |
| public VerticalCRS getVerticalCRS() { |
| return verticalCRS; |
| } |
| |
| /** |
| * Sets the information about the vertical coordinate reference system to |
| * which the maximum and minimum elevation values are measured. |
| * |
| * @param newValue the new vertical CRS. |
| */ |
| public void setVerticalCRS(final VerticalCRS newValue) { |
| checkWritePermission(verticalCRS); |
| verticalCRS = newValue; |
| } |
| |
| /** |
| * Returns an arbitrary value, or {@code null} if both minimum and maximum are null. |
| * This is used for verifying if the bounds are already set or partially set. |
| */ |
| private Double value() { |
| return (minimumValue != null) ? minimumValue : maximumValue; |
| } |
| |
| /** |
| * Sets this vertical extent to values inferred from the specified envelope. The envelope can |
| * be multi-dimensional, in which case the {@linkplain Envelope#getCoordinateReferenceSystem() |
| * envelope CRS} must have a vertical component. |
| * |
| * <p><b>Note:</b> this method is available only if the referencing module is on the classpath.</p> |
| * |
| * @param envelope the envelope to use for setting this vertical extent. |
| * @throws UnsupportedOperationException if the referencing module is not on the classpath. |
| * @throws TransformException if the envelope can not be transformed to a vertical extent. |
| * |
| * @see DefaultExtent#addElements(Envelope) |
| * @see DefaultGeographicBoundingBox#setBounds(Envelope) |
| * @see DefaultTemporalExtent#setBounds(Envelope) |
| */ |
| public void setBounds(final Envelope envelope) throws TransformException { |
| checkWritePermission(value()); |
| ReferencingServices.getInstance().setBounds(envelope, this); |
| } |
| |
| /** |
| * Sets this vertical extent to the intersection of this extent with the specified one. |
| * The {@linkplain org.apache.sis.referencing.crs.DefaultVerticalCRS#getDatum() vertical datum} |
| * must be the same (ignoring metadata) for both extents; this method does not perform datum shift. |
| * However this method can perform unit conversions. |
| * |
| * <p>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.</p> |
| * |
| * @param other the vertical extent to intersect with this extent. |
| * @throws IllegalArgumentException 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 IllegalArgumentException { |
| checkWritePermission(value()); |
| 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 IllegalArgumentException(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().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}. |
| * |
| * <div class="note"><b>Note about alternatives:</b> |
| * 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.</div> |
| * |
| * @param cv the conversion computed by {@link #getConversionFrom(VerticalCRS)} (may be {@code null}). |
| * @param sample the minimum or the maximum value. |
| * @param other the minimum or maximum value at the opposite bound. |
| * @return {@code true} if the axis direction is reversed at the given value. |
| */ |
| private static boolean isReversing(final MathTransform1D cv, Double sample, final Double other) throws TransformException { |
| if (cv == null) { |
| return false; |
| } |
| if (sample == null || sample.isNaN()) { |
| sample = other; |
| } else if (other != null && !other.isNaN()) { |
| sample = (sample + other) / 2; |
| } |
| return MathFunctions.isNegative(cv.derivative(sample != null ? sample : Double.NaN)); |
| } |
| |
| /** |
| * Converts the given value with the given transform if non-null. This converter can generally |
| * not perform datum shift; the operation is merely unit conversion and change of axis direction. |
| */ |
| private static Double convert(MathTransform1D tr, Double value) throws TransformException { |
| return (tr != null) ? tr.transform(value) : value; |
| } |
| } |