blob: 37275e7cc3edbaa708185b1ab3804a4b30d8bfb9 [file] [log] [blame]
/*
* 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.io.wkt;
import java.util.Map;
import javax.measure.Unit;
import javax.measure.quantity.Length;
import org.opengis.util.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.cs.CSFactory;
import org.opengis.referencing.cs.VerticalCS;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.VerticalCRS;
import org.apache.sis.metadata.privy.AxisNames;
import org.apache.sis.metadata.iso.extent.DefaultExtent;
import org.apache.sis.metadata.iso.extent.DefaultVerticalExtent;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.referencing.datum.RealizationMethod;
/**
* Stores temporary information needed for completing the construction of an {@link DefaultVerticalExtent} instance.
* WKT of vertical extents looks like:
*
* {@snippet lang="wkt" :
* VERTICALEXTENT[-1000, 0, LENGTHUNIT[“metre”, 1]]
* }
*
* But {@code DefaultVerticalExtent} has no {@code unit} property. Instead, {@code DefaultVerticalExtent} has a
* {@code verticalCRS} property. The WKT specification said that heights are positive toward up and relative to
* an unspecified mean sea level, but we will try to use the parsed vertical CRS instance if we find a suitable
* one (i.e. one that defines gravity-related heights or depths), on the assumption that the vertical extent is
* likely to be defined in the same vertical CRS.
*
* <p>This class can be understood as the converse of
* {@link org.apache.sis.metadata.iso.extent.Extents#getVerticalRange}</p>
*
* @author Martin Desruisseaux (Geomatys)
*/
final class VerticalInfo {
/**
* The next instance to resolve. This form a chained list.
*/
private VerticalInfo next;
/**
* The vertical extent pending completion.
*/
private final DefaultVerticalExtent extent;
/**
* The unit specified in the {@code VERTICALEXTENT} WKT element.
*/
final Unit<Length> unit;
/**
* If a vertical CRS could be used pending only a change of units, that CRS.
* Otherwise {@code null}.
*/
private VerticalCRS compatibleCRS;
/**
* Adds to the chained list a new {@code DefaultVerticalExtent} instance pending completion.
*
* @param next the existing {@code VerticalInfo} instance. Will become the next instance
* to process after {@code this} in a chain of {@code VerticalInfo}.
* @param extents where to add the vertical extent.
* @param unit the unit to assign to the {@code extent}. Cannot be null.
*/
VerticalInfo(final VerticalInfo next, final DefaultExtent extents, final double minimum, final double maximum, final Unit<Length> unit) {
this.next = next;
this.unit = unit;
this.extent = new DefaultVerticalExtent(minimum, maximum, null);
extents.getVerticalElements().add(extent);
}
/**
* If the pending {@code DefaultVerticalExtent} can use the given CRS, completes the extent now.
* This method invokes {@link DefaultVerticalExtent#setVerticalCRS(VerticalCRS)} with the given CRS if:
*
* <ul>
* <li>realization method is {@link RealizationMethod#GEOID},</li>
* <li>increasing height values are up, and</li>
* <li>axis unit of measurement is the given linear unit.</li>
* </ul>
*
* This method processes also all other {@code VerticalInfo} instances in the chained list.
*
* @return the new head of the chained list (may be {@code this}), or {@code null} if the list
* became empty as a result of this operation.
*/
final VerticalInfo resolve(final VerticalCRS crs) {
if (crs != null && crs.getDatum().getRealizationMethod().orElse(null) == RealizationMethod.GEOID) {
return resolve(crs, crs.getCoordinateSystem().getAxis(0));
}
return this;
}
/**
* Implementation of {@link #resolve(VerticalCRS, CoordinateSystemAxis)} to be invoked recursively,
* after we checked the datum type and fetched the axis once for all.
*/
private VerticalInfo resolve(final VerticalCRS crs, final CoordinateSystemAxis axis) {
if (next != null) {
next = next.resolve(crs, axis);
}
final Unit<?> crsUnit = axis.getUnit();
if (axis.getDirection() == AxisDirection.UP && unit.equals(crsUnit)) {
extent.setVerticalCRS(crs);
return next;
} else if (unit.isCompatible(crsUnit)) {
compatibleCRS = crs;
}
return this;
}
/**
* Completes the extent with a new CRS using the units specified at construction time.
* The CRS created by this method is implementation-dependent. The only guarantees are:
*
* <ul>
* <li>realization method is {@link RealizationMethod#GEOID},</li>
* <li>increasing height values are up, and</li>
* <li>axis unit of measurement is the given linear unit.</li>
* </ul>
*
* If this method cannot propose a suitable CRS, then it returns {@code this}.
*/
final VerticalInfo complete(final CRSFactory crsFactory, final CSFactory csFactory) throws FactoryException {
if (next != null) {
next = next.complete(crsFactory, csFactory);
}
if (compatibleCRS == null) {
return this;
}
final Object name;
final String abbreviation;
CoordinateSystemAxis axis = compatibleCRS.getCoordinateSystem().getAxis(0);
final boolean isUP = (axis.getDirection() == AxisDirection.UP);
if (isUP) {
name = axis.getName();
abbreviation = axis.getAbbreviation();
} else {
name = AxisNames.GRAVITY_RELATED_HEIGHT;
abbreviation = "H";
}
axis = csFactory.createCoordinateSystemAxis(properties(name), abbreviation, AxisDirection.UP, unit);
/*
* Naming policy (based on usage of names in the EPSG database):
*
* - We can reuse the old axis name if (and only if) the direction is the same, because the axis
* names are constrained by the ISO 19111 specification in a way that do not include the units
* of measurement. Examples: "Gravity-related height", "Depth".
*
* - We cannot reuse the previous Coordinate System name, because it often contains the axis
* abbreviation and unit. Examples: "Vertical CS. Axis: height (H). Orientation: up. UoM: m.".
* Since we are lazy, we will reuse the axis name instead, which is more neutral.
*
* - We generally can reuse the CRS name because those names tend to refer to the datum (which is
* unchanged) rather than the coordinate system. Examples: "Low Water depth", "NGF Lallemand height",
* "JGD2011 (vertical) height". However, we make an exception if the direction is down, because in such
* cases the previous name may contain terms like "depth", which are not appropriate for our new CRS.
*/
final VerticalCS cs = csFactory.createVerticalCS (properties(axis.getName()), axis);
extent.setVerticalCRS(crsFactory.createVerticalCRS(
properties((isUP ? compatibleCRS : axis).getName()), compatibleCRS.getDatum(), cs));
return next;
}
/**
* Convenience method for creating the map of properties to give to the factory method.
*/
private static Map<String,?> properties(final Object name) {
return Map.of(IdentifiedObject.NAME_KEY, name);
}
}