This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit f5fa5b8779ddc94f6747f9356f41133b94d9267e Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Apr 6 17:30:09 2019 +0200 If the CRS in a netCDF file seems to be a map projection but the Grid class does not have enough information for building the right map projection, creates an "Not specified" projection. This "not specified" projection may be replaced by a projection parsed from Well Known Text at a later stage during the netCDF reading process, but creating an instance in in Grid class allow us to preserve information like mapping from CRS axes to grid axes. --- .../referencing/provider/Equirectangular.java | 11 +- .../referencing/operation/DefaultConversion.java | 2 +- .../org/apache/sis/internal/util/Constants.java | 14 - .../java/org/apache/sis/internal/netcdf/Axis.java | 35 +- .../org/apache/sis/internal/netcdf/CRSBuilder.java | 377 ++++++++++++++------- .../org/apache/sis/internal/netcdf/Decoder.java | 15 +- .../java/org/apache/sis/internal/netcdf/Grid.java | 11 +- .../apache/sis/internal/netcdf/GridMapping.java | 5 +- .../apache/sis/internal/netcdf/impl/GridInfo.java | 14 +- .../internal/netcdf/ucar/CSBuilderFallback.java | 5 +- .../sis/internal/netcdf/ucar/GridWrapper.java | 6 +- .../org/apache/sis/internal/netcdf/GridTest.java | 4 +- 12 files changed, 327 insertions(+), 172 deletions(-) diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Equirectangular.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Equirectangular.java index ea2d742..033a241 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Equirectangular.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Equirectangular.java @@ -61,7 +61,7 @@ import static java.lang.Math.*; * * @author John Grange * @author Martin Desruisseaux (Geomatys) - * @version 0.8 + * @version 1.0 * * @see PseudoPlateCarre * @see <a href="http://geotiff.maptools.org/proj_list/equirectangular.html">GeoTIFF parameters for Equirectangular</a> @@ -76,6 +76,13 @@ public final class Equirectangular extends AbstractProvider { */ private static final long serialVersionUID = -278288251842178001L; + /** + * Name of this projection in EPSG geodetic dataset. + * + * @todo Remove with JDK9 after we introduce {@code getInstance()} method. + */ + public static final String NAME = "Equidistant Cylindrical (Spherical)"; + /* * ACCESS POLICY: Only formal EPSG parameters shall be public. * Parameters that we add ourselves should be package-privated. @@ -182,7 +189,7 @@ public final class Equirectangular extends AbstractProvider { // Do not declare the ESRI "Equidistant_Cylindrical" projection name below, // for avoiding confusion with EPSG "Equidistant Cylindrical" ellipsoidal projection. PARAMETERS = addIdentifierAndLegacy(builder, "1029", "9823") // 9823 uses deprecated parameter names - .addName( "Equidistant Cylindrical (Spherical)") + .addName( NAME) .addName( "Plate Carrée") // Not formally defined by EPSG, but cited in documentation. .addName(Citations.OGC, "Equirectangular") .addName(Citations.ESRI, "Plate_Carree") diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java index ca98dd6..4e1ebd0 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java @@ -190,7 +190,7 @@ public class DefaultConversion extends AbstractSingleOperation implements Conver * The semi-major and semi-minor parameter values will be set automatically when the * {@link #specialize specialize(…)} method will be invoked.</p> * - * <p>If both the {@code transform} and {@code parameters} arguments are non-null, then the later should describes + * <p>If both the {@code transform} and {@code parameters} arguments are non-null, then the later should describe * the parameters used for creating the transform. Those parameters will be stored for information purpose and can * be given back by the {@link #getParameterValues()} method.</p> * diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java index d089889..afd1b0d 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java +++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java @@ -241,20 +241,6 @@ public final class Constants extends Static { public static final short EPSG_ANTARCTIC_POLAR_STEREOGRAPHIC = 3031; /** - * EPSG code of "Unknown datum based upon the WGS 84 ellipsoid". - * This is a two-dimensional geographic CRS. - * Note that the EPSG database defines unknown CRS for many other ellipsoids. - * For now only the WGS 84 case is used by Apache SIS. - */ - public static final short EPSG_UNKNOWN_CRS = 4030; - - /** - * EPSG code of "Not specified (based upon the WGS 84 ellipsoid)". - * This is a geodetic datum. - */ - public static final short EPSG_UNKNOWN_DATUM = 6030; - - /** * Do not allow instantiation of this class. */ private Constants() { diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java index f034fa4..67e602b 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java @@ -384,7 +384,7 @@ public final class Axis extends NamedElement { /** * Returns {@code true} if the given axis specifies the same direction and unit of measurement than this axis. - * This is used for testing is a predefined axis can be used instead than invoking {@link #toISO(CSFactory)}. + * This is used for testing if a predefined axis can be used instead than invoking {@link #toISO(CSFactory, int)}. */ final boolean isSameUnitAndDirection(final CoordinateSystemAxis axis) { if (!axis.getDirection().equals(direction)) { @@ -467,9 +467,10 @@ public final class Axis extends NamedElement { * Creates an ISO 19111 axis from the information stored in this netCDF axis. * * @param factory the factory to use for creating the coordinate system axis. + * @param order 0 if creating the first axis, 1 if creating the second axis, <i>etc</i>. * @return the ISO axis. */ - final CoordinateSystemAxis toISO(final CSFactory factory) throws FactoryException { + final CoordinateSystemAxis toISO(final CSFactory factory, final int order) throws FactoryException { /* * The axis name is stored without namespace, because the variable name in a netCDF file can be anything; * this is not controlled vocabulary. However the standard name, if any, is stored with "NetCDF" namespace @@ -505,28 +506,40 @@ public final class Axis extends NamedElement { /* * Axis abbreviation, direction and unit of measurement are mandatory. If any of them is null, * creation of CoordinateSystemAxis is likely to fail with an InvalidGeodeticParameterException. - * We provide default values for the most well-accepted values and leave other values to null. + * We provide default values for the most well-identified axes and leave other values to null. * Those null values can be accepted if users specify their own factory. + * + * The default values are SI base units except degrees, which is the usually angular units for netCDF files. + * Providing default units is a little bit dangerous, but we can not create CRS otherwise. Note that wrong + * defaults become harmless if the CRS is overwritten by GridMapping attributes in Variable.getGridGeometry(). */ Unit<?> unit = getUnit(); if (unit == null) { switch (abbreviation) { - /* - * TODO: consider moving those default values in a separated class, - * for example a netCDF-specific CSFactory, for allowing users to override. - */ - case 'λ': case 'φ': unit = Units.DEGREE; break; + case 'λ': case 'φ': // Geodetic longitude and latitude. + case 'θ': case 'Ω': unit = Units.DEGREE; break; // Spherical longitude and latitude. + case 'r': case 'D': // Depth and radius. + case 'H': case 'h': // Gravity-related and ellipsoidal height. + case 'E': case 'N': unit = Units.METRE; break; // Projected easting and northing. + case 't': unit = Units.SECOND; break; // Time. + } + } + AxisDirection dir = direction; + if (dir == null) { + switch (order) { + case 0: dir = AxisDirection.COLUMN_POSITIVE; break; + case 1: dir = AxisDirection.ROW_POSITIVE; break; } } final String abbr; if (abbreviation != 0) { abbr = Character.toString(abbreviation).intern(); - } else if (direction != null && unit != null) { - abbr = AxisDirections.suggestAbbreviation(name, direction, unit); + } else if (dir != null && unit != null) { + abbr = AxisDirections.suggestAbbreviation(name, dir, unit); } else { abbr = null; } - return factory.createCoordinateSystemAxis(properties, abbr, direction, unit); + return factory.createCoordinateSystemAxis(properties, abbr, dir, unit); } /** diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java index f097c65..3a62463 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java @@ -33,17 +33,22 @@ import org.opengis.referencing.datum.*; import org.opengis.referencing.crs.SingleCRS; import org.opengis.referencing.crs.CRSFactory; import org.opengis.referencing.crs.GeographicCRS; +import org.opengis.referencing.crs.GeocentricCRS; import org.opengis.referencing.NoSuchAuthorityCodeException; +import org.opengis.referencing.operation.CoordinateOperationFactory; +import org.opengis.referencing.operation.OperationMethod; +import org.opengis.referencing.operation.Conversion; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.referencing.cs.AxesConvention; import org.apache.sis.referencing.cs.CoordinateSystems; -import org.apache.sis.referencing.cs.DefaultSphericalCS; -import org.apache.sis.referencing.cs.DefaultEllipsoidalCS; import org.apache.sis.referencing.crs.AbstractCRS; -import org.apache.sis.storage.DataStoreException; -import org.apache.sis.storage.DataStoreContentException; +import org.apache.sis.referencing.crs.DefaultGeographicCRS; +import org.apache.sis.referencing.crs.DefaultGeocentricCRS; +import org.apache.sis.internal.referencing.provider.Equirectangular; +import org.apache.sis.internal.system.DefaultFactories; import org.apache.sis.internal.util.TemporalUtilities; -import org.apache.sis.internal.util.Constants; +import org.apache.sis.storage.DataStoreContentException; +import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.resources.Errors; import org.apache.sis.measure.Units; import org.apache.sis.math.Vector; @@ -75,9 +80,13 @@ import org.apache.sis.math.Vector; abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> { /** * The coordinate reference system which is presumed the basis of datum on netCDF files. - * Note: if this default is changed, search also for "WGS 84" strings in this class. + * Note: if this default is changed, search also for "GRS 1980" strings in this class. + * + * <div class="note"><b>Note:</b> we use GRS 1980 instead than WGS 84 because the CRS name + * clearly said "Unknown datum based upon the GRS 1980 ellipsoid" and for consistency with + * {@link CommonCRS#SPHERE}, which also use GRS 1980.</div> */ - private static final CommonCRS DEFAULT = CommonCRS.WGS84; + private static final CommonCRS DEFAULT = CommonCRS.GRS1980; /** * The type of datum as a GeoAPI sub-interface of {@link Datum}. @@ -87,7 +96,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> { /** * Name of the datum on which the CRS is presumed to be based, or {@code ""}. This is used - * for building a datum name like <cite>"Unknown datum presumably based on WGS 84"</cite>. + * for building a datum name like <cite>"Unknown datum presumably based on GRS 1980"</cite>. */ private final String datumBase; @@ -121,17 +130,17 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> { /** * The datum created by {@link #createDatum(DatumFactory, Map)}. */ - D datum; + protected D datum; /** * The coordinate system created by {@link #createCS(CSFactory, Map, CoordinateSystemAxis[])}. */ - CS coordinateSystem; + protected CS coordinateSystem; /** - * The coordinate reference system that may have been create by {@link #candidate(Decoder)}. + * The coordinate reference system that may have been create by {@link #setPredefinedComponents(Decoder)}. */ - SingleCRS referenceSystem; + protected SingleCRS referenceSystem; /** * Non-fatal exceptions that may occur while building the coordinate reference system. @@ -278,12 +287,11 @@ previous: for (int i=components.size(); --i >= 0;) { * set the datum, CS and CRS field values to those candidate. Those values do not need to be exact; they * will be overwritten later if they do not match the netCDF file content. */ - datum = datumType.cast(decoder.datumCache[datumIndex]); // Should be initialized before 'candidate' call. - candidate(decoder); + datum = datumType.cast(decoder.datumCache[datumIndex]); // Should be before 'setPredefinedComponents' call. + setPredefinedComponents(decoder); /* - * If 'candidate(decoder)' offers a datum, we will used it as-is. Otherwise create the datum now. - * Datum are often not defined in netCDF files, so it will usually be EPSG::6030 — "Not specified - * (based on WGS 84 ellipsoid)". + * If 'setPredefinedComponents(decoder)' offers a datum, we will used it as-is. Otherwise create the datum now. + * Datum are often not defined in netCDF files, so we use EPSG::6019 — "Not specified (based on GRS 1980 ellipsoid)". */ if (datum == null) { // Not localized because stored as a String, possibly exported in WKT or GML, and 'datumBase' is in English. @@ -306,8 +314,8 @@ previous: for (int i=components.size(); --i >= 0;) { } } /* - * If 'candidate(decoder)' did not proposed a coordinate system, or if it proposed a CS but its - * axes do not match the axes in the netCDF file, then create a new coordinate system here. + * If 'setPredefinedComponents(decoder)' did not proposed a coordinate system, or if it proposed a CS + * but its axes do not match the axes in the netCDF file, then create a new coordinate system here. */ if (referenceSystem == null) { final Map<String,?> properties; @@ -319,7 +327,7 @@ previous: for (int i=components.size(); --i >= 0;) { for (int i=0; i<iso.length; i++) { final Axis axis = axes[i]; joiner.add(axis.getName()); - iso[i] = axis.toISO(csFactory); + iso[i] = axis.toISO(csFactory, i); } createCS(csFactory, properties(joiner.toString()), iso); properties = properties(coordinateSystem.getName()); @@ -356,9 +364,9 @@ previous: for (int i=components.size(); --i >= 0;) { } /** - * Reports a non-fatal exception that may occur when processing the value returned by {@link #epsgCandidate(Unit)}. - * In order to avoid repeating the same warning many times, this method collects the warnings together and reports - * them in a single log record after we finished creating the CRS. + * Reports a non-fatal exception that may occur during {@link #setPredefinedComponents(Decoder)}. + * In order to avoid repeating the same warning many times, this method collects the warnings + * together and reports them in a single log record after we finished creating the CRS. */ final void recoverableException(final NoSuchAuthorityCodeException e) { if (warnings == null) warnings = e; @@ -378,12 +386,14 @@ previous: for (int i=components.size(); --i >= 0;) { * Returns the EPSG code of a possible coordinate system from EPSG database. This method proceed by brief * inspection of axis directions and units; there is no guarantees that the coordinate system returned by * this method match the axes defined in the netCDF file. It is caller's responsibility to verify. - * This is a helper method for {@link #candidate(Decoder)} implementations. + * This is a helper method for {@link #setPredefinedComponents(Decoder)} implementations. * * @param defaultUnit the unit to use if unit definition is missing in the netCDF file. * @return EPSG code of a CS candidate, or {@code null} if none. + * + * @see Geodetic#isPredefinedCS(Unit) */ - final Integer epsgCandidate(final Unit<?> defaultUnit) { + final Integer epsgCandidateCS(final Unit<?> defaultUnit) { Unit<?> unit = getFirstAxis().getUnit(); if (unit == null) unit = defaultUnit; final AxisDirection[] directions = new AxisDirection[dimension]; @@ -402,11 +412,11 @@ previous: for (int i=components.size(); --i >= 0;) { * <p>This method may opportunistically set the {@link #datum} and {@link #referenceSystem} fields if it * can propose a CRS candidate instead than only a CS candidate.</p> */ - abstract void candidate(Decoder decoder) throws FactoryException; + abstract void setPredefinedComponents(Decoder decoder) throws FactoryException; /** * Creates the datum for the coordinate reference system to build. The datum are generally not specified in netCDF files. - * To make that clearer, this method builds datum with names like <cite>"Unknown datum presumably based on WGS 84"</cite>. + * To make that clearer, this method builds datum with names like <cite>"Unknown datum presumably based on GRS 1980"</cite>. * The newly created datum is assigned to the {@link #datum} field. * * @param factory the factory to use for creating the datum. @@ -435,20 +445,33 @@ previous: for (int i=components.size(); --i >= 0;) { */ abstract void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException; + + + /** * Base classes of {@link Spherical}, {@link Geographic} and {@link Projected} builders. * They all have in common to be based on a {@link GeodeticDatum}. */ private abstract static class Geodetic<CS extends CoordinateSystem> extends CRSBuilder<GeodeticDatum, CS> { - /** Whether the coordinate system has longitude before latitude. */ - boolean isLongitudeFirst; + /** + * Whether the coordinate system has longitude before latitude. + * This flag is set as a side-effect of {@link #isPredefinedCS(Unit)} method call. + */ + protected boolean isLongitudeFirst; - /** For subclasses constructors. */ + /** + * For subclasses constructors. + * + * @param minDim minimum number of dimensions (2 or 3). + */ Geodetic(final byte minDim) { - super(GeodeticDatum.class, "WGS 84", (byte) 0, minDim, (byte) 3); + super(GeodeticDatum.class, "GRS 1980", (byte) 0, minDim, (byte) 3); } - /** Creates a {@link GeodeticDatum} for <cite>"Unknown datum based on WGS 84"</cite>. */ + /** + * Creates a {@link GeodeticDatum} for <cite>"Unknown datum based on GRS 1980"</cite>. + * This method is invoked only if {@link #setPredefinedComponents(Decoder)} failed to create a datum. + */ @Override final void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException { final GeodeticDatum template = DEFAULT.datum(); datum = factory.createGeodeticDatum(properties, template.getEllipsoid(), template.getPrimeMeridian()); @@ -461,8 +484,10 @@ previous: for (int i=components.size(); --i >= 0;) { * If {@code true}, then {@link #isLongitudeFirst} will have been set to an indication of axis order. * * @param expected the expected unit of measurement of the first axis. + * + * @see #epsgCandidateCS(Unit) */ - final boolean isPredefined(final Unit<?> expected) { + final boolean isPredefinedCS(final Unit<?> expected) { final Axis axis = getFirstAxis(); final Unit<?> unit = axis.getUnit(); if (unit == null || expected.equals(unit)) { @@ -473,103 +498,109 @@ previous: for (int i=components.size(); --i >= 0;) { } return false; } - - /** - * If the {@link #datum} field is not already set, initialize it to "Not specified (based upon the WGS 84 ellipsoid)". - * Subclasses should override this method for setting also the {@link #coordinateSystem} and {@link #referenceSystem} - * fields if possible. - */ - @Override void candidate(final Decoder decoder) throws FactoryException { - if (datum == null) try { - datum = decoder.getDatumAuthorityFactory().createGeodeticDatum(String.valueOf(Constants.EPSG_UNKNOWN_DATUM)); - } catch (NoSuchAuthorityCodeException e) { - recoverableException(e); - } - } } + + + /** * Builder for geocentric CRS with (θ,Ω,r) axes. */ private static final class Spherical extends Geodetic<SphericalCS> { - /** Creates a new builder (invoked by lambda function). */ + /** + * Creates a new builder (invoked by lambda function). + */ public Spherical() { super((byte) 3); } - /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */ - @Override void candidate(final Decoder decoder) throws FactoryException { - super.candidate(decoder); - final Integer epsg = epsgCandidate(Units.DEGREE); - if (epsg != null) try { - coordinateSystem = decoder.getCSAuthorityFactory().createSphericalCS(epsg.toString()); - return; - } catch (NoSuchAuthorityCodeException e) { - recoverableException(e); - } - if (isPredefined(Units.DEGREE)) { - coordinateSystem = (SphericalCS) DEFAULT.spherical().getCoordinateSystem(); + /** + * Possibly sets {@link #datum} and {@link #coordinateSystem} to predefined objects + * matching the axes defined in the netCDF file. + */ + @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException { + if (isPredefinedCS(Units.DEGREE)) { + GeocentricCRS crs = DEFAULT.spherical(); if (isLongitudeFirst) { - coordinateSystem = DefaultSphericalCS.castOrCopy(coordinateSystem).forConvention(AxesConvention.RIGHT_HANDED); + crs = DefaultGeocentricCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED); } + referenceSystem = crs; + coordinateSystem = (SphericalCS) crs.getCoordinateSystem(); + datum = crs.getDatum(); + } else { + datum = DEFAULT.datum(); } } - /** Creates the three-dimensional {@link SphericalCS} from given axes. */ + /** + * Creates the three-dimensional {@link SphericalCS} from given axes. This method is invoked only + * if {@link #setPredefinedComponents(Decoder)} failed to assign a CS or if {@link #build(Decoder)} + * found that the {@link #coordinateSystem} does not have compatible axes. + */ @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException { coordinateSystem = factory.createSphericalCS(properties, axes[0], axes[1], axes[2]); } - /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */ + /** + * Creates the coordinate reference system from datum and coordinate system computed in previous steps. + * This method is invoked under conditions similar to the ones of above {@code createCS(…)} method. + */ @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException { referenceSystem = factory.createGeocentricCRS(properties, datum, coordinateSystem); } - }; + } + + + /** * Geographic CRS with (λ,φ,h) axes. * The height, if present, is ellipsoidal height. */ private static final class Geographic extends Geodetic<EllipsoidalCS> { - /** Creates a new builder (invoked by lambda function). */ + /** + * Creates a new builder (invoked by lambda function). + */ public Geographic() { super((byte) 2); } - /** Tries to creates the coordinate system from EPSG code. */ - private boolean tryEPSG(final Decoder decoder) throws FactoryException { - super.candidate(decoder); // Initialize the datum. - final Integer epsg = epsgCandidate(Units.DEGREE); - if (epsg != null) try { - coordinateSystem = decoder.getCSAuthorityFactory().createEllipsoidalCS(epsg.toString()); - return true; - } catch (NoSuchAuthorityCodeException e) { - recoverableException(e); - } - return false; - } - - /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */ - @Override void candidate(final Decoder decoder) throws FactoryException { - if (isPredefined(Units.DEGREE)) { - if (!is3D()) { - GeographicCRS crs = decoder.getCRSAuthorityFactory().createGeographicCRS(String.valueOf(Constants.EPSG_UNKNOWN_CRS)); - coordinateSystem = crs.getCoordinateSystem(); - datum = crs.getDatum(); - referenceSystem = crs; - } else if (!tryEPSG(decoder)) { - coordinateSystem = DEFAULT.geographic3D().getCoordinateSystem(); - } - if (isLongitudeFirst) { - coordinateSystem = DefaultEllipsoidalCS.castOrCopy(coordinateSystem).forConvention(AxesConvention.RIGHT_HANDED); - referenceSystem = null; + /** + * Possibly sets {@link #datum}, {@link #coordinateSystem} and {@link #referenceSystem} + * to predefined objects matching the axes defined in the netCDF file. + */ + @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException { + if (isPredefinedCS(Units.DEGREE)) { + GeographicCRS crs; + if (is3D()) { + crs = DEFAULT.geographic3D(); + if (isLongitudeFirst) { + crs = DefaultGeographicCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED); + } + } else if (isLongitudeFirst) { + crs = DEFAULT.normalizedGeographic(); + } else { + crs = DEFAULT.geographic(); } + referenceSystem = crs; + coordinateSystem = crs.getCoordinateSystem(); + datum = crs.getDatum(); } else { - tryEPSG(decoder); + datum = DEFAULT.datum(); + final Integer epsg = epsgCandidateCS(Units.DEGREE); + if (epsg != null) try { + coordinateSystem = decoder.getCSAuthorityFactory().createEllipsoidalCS(epsg.toString()); + } catch (NoSuchAuthorityCodeException e) { + recoverableException(e); + } } } - /** Creates the two- or three-dimensional {@link EllipsoidalCS} from given axes. */ + /** + * Creates the two- or three-dimensional {@link EllipsoidalCS} from given axes. This method is invoked only if + * {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder)} + * found that the {@link #coordinateSystem} does not have compatible axes. + */ @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException { if (axes.length > 2) { coordinateSystem = factory.createEllipsoidalCS(properties, axes[0], axes[1], axes[2]); @@ -578,29 +609,71 @@ previous: for (int i=components.size(); --i >= 0;) { } } - /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */ + /** + * Creates the coordinate reference system from datum and coordinate system computed in previous steps. + * This method is invoked under conditions similar to the ones of above {@code createCS(…)} method. + */ @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException { - referenceSystem = factory.createGeographicCRS(properties, datum, coordinateSystem); + referenceSystem = factory.createGeographicCRS(properties, datum, coordinateSystem); } - }; + } + + + /** - * Projected CRS with (E,N,h) axes. + * Projected CRS with (E,N,h) axes. There is not enough information in a netCDF files for creating the right + * map projection, unless {@code "grid_mapping"} attributes are specified. If insufficient information, this + * class creates an unknown map projection based on Plate Carrée. Note that this map projection may be replaced + * by {@link GridMapping#crs} at a later stage. */ private static final class Projected extends Geodetic<CartesianCS> { - /** Creates a new builder (invoked by lambda function). */ + /** + * The spherical variant of {@link CRSBuilder#DEFAULT}. + * Currently based upon the GRS 1980 Authalic Sphere. + */ + private static final CommonCRS SPHERICAL = CommonCRS.SPHERE; + + /** + * Defining conversion for "Not specified (presumed Plate Carrée)". This conversion use spherical formulas. + * Consequently it should be used with {@link #SPHERICAL} instead of {@link CommonCRS#DEFAULT}. + */ + private static final Conversion UNKNOWN_PROJECTION; + static { + final CoordinateOperationFactory factory = DefaultFactories.forBuildin(CoordinateOperationFactory.class); + try { + final OperationMethod method = factory.getOperationMethod(Equirectangular.NAME); + UNKNOWN_PROJECTION = factory.createDefiningConversion( + properties("Not specified (presumed Plate Carrée)"), + method, method.getParameters().createValue()); + } catch (FactoryException e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Creates a new builder (invoked by lambda function). + */ public Projected() { super((byte) 2); } - /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */ - @Override void candidate(final Decoder decoder) { - if (isPredefined(Units.METRE)) { - coordinateSystem = DEFAULT.universal(0,0).getCoordinateSystem(); + /** + * Possibly sets {@link #datum}, {@link #coordinateSystem} and {@link #referenceSystem} + * to predefined objects matching the axes defined in the netCDF file. + */ + @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException { + datum = SPHERICAL.datum(); + if (isPredefinedCS(Units.METRE)) { + coordinateSystem = CommonCRS.WGS84.universal(0,0).getCoordinateSystem(); } } - /** Creates the two- or three-dimensional {@link CartesianCS} from given axes. */ + /** + * Creates the two- or three-dimensional {@link CartesianCS} from given axes. This method is invoked only if + * {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder)} + * found that the {@link #coordinateSystem} does not have compatible axes. + */ @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException { if (axes.length > 2) { coordinateSystem = factory.createCartesianCS(properties, axes[0], axes[1], axes[2]); @@ -609,24 +682,38 @@ previous: for (int i=components.size(); --i >= 0;) { } } - /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */ + /** + * Creates the coordinate reference system from datum and coordinate system computed in previous steps. + * The datum for this method is based upon the GRS 1980 Authalic Sphere. + */ @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException { - throw new UnsupportedOperationException(); // TODO + GeographicCRS baseCRS = (coordinateSystem.getDimension() >= 3) ? SPHERICAL.geographic3D() : SPHERICAL.geographic(); + if (!baseCRS.getDatum().equals(datum)) { + baseCRS = factory.createGeographicCRS(properties, datum, baseCRS.getCoordinateSystem()); + } + referenceSystem = factory.createProjectedCRS(properties, baseCRS, UNKNOWN_PROJECTION, coordinateSystem); } - }; + } + + + /** * Vertical CRS with (H) or (D) axis. * Used for mean sea level (not for ellipsoidal height). */ private static final class Vertical extends CRSBuilder<VerticalDatum, VerticalCS> { - /** Creates a new builder (invoked by lambda function). */ + /** + * Creates a new builder (invoked by lambda function). + */ public Vertical() { super(VerticalDatum.class, "Mean Sea Level", (byte) 1, (byte) 1, (byte) 1); } - /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */ - @Override void candidate(final Decoder decoder) { + /** + * Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. + */ + @Override void setPredefinedComponents(final Decoder decoder) { final Axis axis = getFirstAxis(); final Unit<?> unit = axis.getUnit(); final CommonCRS.Vertical predefined; @@ -644,34 +731,49 @@ previous: for (int i=components.size(); --i >= 0;) { coordinateSystem = predefined.crs().getCoordinateSystem(); } - /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on Mean Sea Level"</cite>. */ + /** + * Creates a {@link VerticalDatum} for <cite>"Unknown datum based on Mean Sea Level"</cite>. + */ @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException { datum = factory.createVerticalDatum(properties, VerticalDatumType.GEOIDAL); } - /** Creates the one-dimensional {@link VerticalCS} from given axes. */ + /** + * Creates the one-dimensional {@link VerticalCS} from given axes. This method is invoked + * only if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system + * or if {@link #build(Decoder)} found that the axis or direction are not compatible. + */ @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException { coordinateSystem = factory.createVerticalCS(properties, axes[0]); } - /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */ + /** + * Creates the coordinate reference system from datum and coordinate system computed in previous steps. + */ @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException { referenceSystem = factory.createVerticalCRS(properties, datum, coordinateSystem); } - }; + } + + + /** * Temporal CRS with (t) axis. Its datum need to be built * in a special way since it contains the time origin. */ private static final class Temporal extends CRSBuilder<TemporalDatum, TimeCS> { - /** Creates a new builder (invoked by lambda function). */ + /** + * Creates a new builder (invoked by lambda function). + */ public Temporal() { super(TemporalDatum.class, "", (byte) 2, (byte) 1, (byte) 1); } - /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */ - @Override void candidate(final Decoder decoder) { + /** + * Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. + */ + @Override void setPredefinedComponents(final Decoder decoder) { final Axis axis = getFirstAxis(); final Unit<?> unit = axis.getUnit(); final CommonCRS.Temporal predefined; @@ -687,7 +789,9 @@ previous: for (int i=components.size(); --i >= 0;) { coordinateSystem = predefined.crs().getCoordinateSystem(); } - /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on …"</cite>. */ + /** + * Creates a {@link TemporalDatum} for <cite>"Unknown datum based on …"</cite>. + */ @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException { final Axis axis = getFirstAxis(); axis.getUnit(); // Force epoch parsing if not already done. @@ -701,37 +805,54 @@ previous: for (int i=components.size(); --i >= 0;) { } } - /** Creates the one-dimensional {@link TimeCS} from given axes. */ + /** + * Creates the one-dimensional {@link TimeCS} from given axes. This method is invoked only + * if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if + * {@link #build(Decoder)} found that the axis or direction are not compatible. + */ @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException { coordinateSystem = factory.createTimeCS(properties, axes[0]); } - /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */ + /** + * Creates the coordinate reference system from datum and coordinate system computed in previous steps. + */ @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException { properties = properties(getFirstAxis().coordinates.getUnitsString()); referenceSystem = factory.createTemporalCRS(properties, datum, coordinateSystem); } - }; + } + + + /** * Unknown CRS with (x,y,z) axes. */ private static final class Engineering extends CRSBuilder<EngineeringDatum, AffineCS> { - /** Creates a new builder (invoked by lambda function). */ + /** + * Creates a new builder (invoked by lambda function). + */ public Engineering() { super(EngineeringDatum.class, "affine coordinate system", (byte) 3, (byte) 2, (byte) 3); } - /** No-op since we have no predefined engineering CRS. */ - @Override void candidate(final Decoder decoder) { + /** + * No-op since we have no predefined engineering CRS. + */ + @Override void setPredefinedComponents(final Decoder decoder) { } - /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on affine coordinate system"</cite>. */ + /** + * Creates a {@link VerticalDatum} for <cite>"Unknown datum based on affine coordinate system"</cite>. + */ @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException { datum = factory.createEngineeringDatum(properties); } - /** Creates two- or three-dimensional {@link AffineCS} from given axes. */ + /** + * Creates two- or three-dimensional {@link AffineCS} from given axes. + */ @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException { if (axes.length > 2) { coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1], axes[2]); @@ -740,11 +861,13 @@ previous: for (int i=components.size(); --i >= 0;) { } } - /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */ + /** + * Creates the coordinate reference system from datum and coordinate system computed in previous steps. + */ @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException { referenceSystem = factory.createEngineeringCRS(properties, datum, coordinateSystem); } - }; + } /** * Maximal {@link #datumIndex} value +1. The maximal value can be seen in the call to {@code super(…)} constructor diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java index 3084826..1732715 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java @@ -96,7 +96,7 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo /** * The geodetic datum, created when first needed. The datum are generally not specified in netCDF files. - * To make that clearer, we will build datum with names like "Unknown datum presumably based on WGS 84". + * To make that clearer, we will build datum with names like "Unknown datum presumably based on GRS 1980". * * @see CRSBuilder#build(Decoder) */ @@ -332,8 +332,8 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo /** * Returns for information purpose only the Coordinate Reference Systems present in this file. - * The CRS returned by this method may not be exactly the CRS to be used by variables. - * This method is provided for metadata purposes. + * The CRS returned by this method may not be exactly the same than the ones used by variables. + * For example, axis order is not guaranteed. This method is provided for metadata purposes. * * @return coordinate reference systems present in this file. * @throws IOException if an I/O operation was necessary but failed. @@ -347,6 +347,11 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo addIfNotPresent(list, m.crs); } } + /* + * Add the CRS computed by grids only if we did not found any grid mapping information. + * This is because grid mapping information override the CRS inferred by Grid from axes. + * Consequently if such information is present, grid CRS may be inaccurate. + */ if (list.isEmpty()) { for (final Grid grid : getGrids()) { addIfNotPresent(list, grid.getCoordinateReferenceSystem(this)); @@ -357,7 +362,9 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo /** * Adds the given coordinate reference system to the given list, provided that an equivalent CRS - * (ignoring axes) is not already present. + * (ignoring axes) is not already present. We ignore axes because the same CRS may be repeated + * with different axis order if values in the localization grid do not vary at the same speed in + * the same directions. */ private static void addIfNotPresent(final List<CoordinateReferenceSystem> list, final CoordinateReferenceSystem crs) { if (crs != null) { diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java index 7cc2401..461cdf8 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java @@ -260,8 +260,15 @@ public abstract class Grid extends NamedElement { protected abstract boolean containsAllNamedAxes(String[] axisNames); /** - * Returns the coordinate reference system, or {@code null} if none. - * This method creates the CRS the first time it is invoked and cache the result. + * Returns the coordinate reference system inferred from axes, or {@code null} if none. + * This method creates the CRS the first time it is invoked and caches the result, + * for allowing {@link Decoder#getReferenceSystemInfo()} to be cheaper. + * + * <p>This CRS is inferred only from analysis of grid axes. It does not take in account {@link GridMapping} information. + * This CRS may be overwritten by another CRS parsed from Well Known Text or other attributes. This overwriting is done + * by {@link Variable#getGridGeometry()}. But even if the CRS is going to be overwritten, we still need to create it in + * this method because this CRS will be used for adjusting axis order or for completion if grid mapping does not include + * information for all dimensions.</p> * * @param decoder the decoder for which CRS are constructed. * @return the CRS for this grid geometry, or {@code null}. diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java index 3a0b235..09de0b2 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java @@ -58,7 +58,10 @@ import ucar.nc2.constants.CF; * * @author Martin Desruisseaux (Geomatys) * @version 1.0 - * @since 1.0 + * + * @see <a href="https://www.unidata.ucar.edu/software/thredds/current/netcdf-java/reference/StandardCoordinateTransforms.html">UCAR projections</a> + * + * @since 1.0 * @module */ final class GridMapping { diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java index 46bd7ca..5f02906 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java @@ -54,18 +54,24 @@ final class GridInfo extends Grid { * Mapping from values of the {@code "_CoordinateAxisType"} attribute or axis name to the abbreviation. * Keys are lower cases and values are controlled vocabulary documented in {@link Axis#abbreviation}. * + * <div class="note">"GeoX" and "GeoY" stands for projected coordinates, not geocentric coordinates + * (<a href="https://www.unidata.ucar.edu/software/thredds/current/netcdf-java/reference/CoordinateAttributes.html#AxisTypes">source</a>). + * </div> + * * @see #getAxisType(String) */ private static final Map<String,Character> AXIS_TYPES = new HashMap<>(26); static { addAxisTypes('λ', "longitude", "lon", "long"); addAxisTypes('φ', "latitude", "lat"); - addAxisTypes('H', "pressure", "height", "altitude", "elevation", "elev"); + addAxisTypes('H', "pressure", "height", "altitude", "elevation", "elev", "geoz"); addAxisTypes('D', "depth"); + addAxisTypes('E', "geox"); + addAxisTypes('N', "geoy"); addAxisTypes('t', "t", "time", "runtime"); - addAxisTypes('x', "x", "geox"); - addAxisTypes('y', "y", "geoy"); - addAxisTypes('z', "z", "geoz"); + addAxisTypes('x', "x"); + addAxisTypes('y', "y"); + addAxisTypes('z', "z"); } /** diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/CSBuilderFallback.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/CSBuilderFallback.java index 581304e..fb88041 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/CSBuilderFallback.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/CSBuilderFallback.java @@ -45,7 +45,10 @@ import org.apache.sis.util.CharSequences; * * @author Martin Desruisseaux (Geomatys) * @version 1.0 - * @since 1.0 + * + * @see <a href="https://www.unidata.ucar.edu/software/thredds/current/netcdf-java/tutorial/CoordSysBuilder.html">UCAR tutorial</a> + * + * @since 1.0 * @module */ final class CSBuilderFallback extends CoordSysBuilder { diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java index 767d13e..37d56e4 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java @@ -241,9 +241,9 @@ next: for (final String name : axisNames) { char abbreviation = 0; final AxisType type = axis.getAxisType(); if (type != null) switch (type) { - case GeoX: abbreviation = 'x'; break; - case GeoY: abbreviation = 'y'; break; - case GeoZ: abbreviation = 'z'; break; + case GeoX: abbreviation = netcdfCS.isGeoXY() ? 'E' : 'x'; break; + case GeoY: abbreviation = netcdfCS.isGeoXY() ? 'N' : 'y'; break; + case GeoZ: abbreviation = netcdfCS.isGeoXY() ? 'H' : 'z'; break; case Lon: abbreviation = 'λ'; break; case Lat: abbreviation = 'φ'; break; case Pressure: // Fallthrough: consider as Height diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java index 27d7e8a..c754a42 100644 --- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java +++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java @@ -110,8 +110,8 @@ public strictfp class GridTest extends TestCase { final Axis z = axes[2]; final Axis t = axes[3]; - assertEquals('x', x.abbreviation); - assertEquals('y', y.abbreviation); + assertEquals('E', x.abbreviation); + assertEquals('N', y.abbreviation); assertEquals('H', z.abbreviation); assertEquals('t', t.abbreviation);
