| /* |
| * 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); |
| } |
| } |