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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new e93c5f2 If a GeographicCRS has only one axe, replace it by an EngineeringCRS. If we still fail to build the CRS, handle as a warning instead of an error. Various documentation editions and method renamings in attempt to clarify a little bit the chain of operations when building a grid geometry. e93c5f2 is described below commit e93c5f23147baad06b5c7c7e577639383e488d66 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Feb 25 20:47:55 2022 +0100 If a GeographicCRS has only one axe, replace it by an EngineeringCRS. If we still fail to build the CRS, handle as a warning instead of an error. Various documentation editions and method renamings in attempt to clarify a little bit the chain of operations when building a grid geometry. --- .../org/apache/sis/internal/netcdf/CRSBuilder.java | 102 ++++++++++++++++----- .../org/apache/sis/internal/netcdf/Decoder.java | 16 +++- .../org/apache/sis/internal/netcdf/Dimension.java | 2 +- .../java/org/apache/sis/internal/netcdf/Grid.java | 6 +- .../apache/sis/internal/netcdf/GridAdjustment.java | 8 +- .../org/apache/sis/internal/netcdf/Variable.java | 18 ++-- .../sis/internal/netcdf/impl/ChannelDecoder.java | 4 +- .../apache/sis/internal/netcdf/impl/GridInfo.java | 6 +- .../sis/internal/netcdf/impl/VariableInfo.java | 20 ++-- .../sis/internal/netcdf/ucar/DecoderWrapper.java | 43 ++++++--- .../sis/internal/netcdf/ucar/GridWrapper.java | 5 +- .../sis/internal/netcdf/ucar/VariableWrapper.java | 20 ++-- .../apache/sis/storage/netcdf/MetadataReader.java | 36 +++++--- .../apache/sis/storage/netcdf/package-info.java | 2 +- .../org/apache/sis/internal/netcdf/GridTest.java | 8 +- 15 files changed, 195 insertions(+), 101 deletions(-) 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 ee2f90b..bb1191a 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 @@ -43,6 +43,7 @@ import org.opengis.referencing.operation.OperationMethod; import org.opengis.referencing.operation.Conversion; import org.opengis.referencing.operation.Matrix; import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.referencing.cs.AbstractCS; import org.apache.sis.referencing.cs.AxesConvention; import org.apache.sis.referencing.cs.CoordinateSystems; @@ -79,6 +80,10 @@ import org.apache.sis.measure.Units; * The builder type is inferred from axes. The axes are identified by their abbreviations, * which is a {@linkplain Axis#abbreviation controlled vocabulary} for this implementation. * + * <h2>Exception handling</h2> + * {@link FactoryException} is handled as a warning by {@linkplain the caller Grid#getCoordinateReferenceSystem}, + * while {@link DataStoreException} is handled as a fatal error. Warnings are stored in {@link #warnings} field. + * * @author Martin Desruisseaux (Geomatys) * @version 1.2 * @since 1.0 @@ -362,9 +367,13 @@ previous: for (int i=components.size(); --i >= 0;) { private SingleCRS build(final Decoder decoder, final boolean grid) throws FactoryException, DataStoreException, IOException { - if (dimension < minDim || dimension > maxDim) { + if (dimension > maxDim) { + /* + * Reminder: FactoryException is handled as a warning by the `Grid` caller. + * By contrast, `DataStoreContentException` would be treated as a fatal error. + */ final Variable axis = getFirstAxis().coordinates; - throw new DataStoreContentException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4, + throw new FactoryException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4, axis.getFilename(), getClass().getSimpleName(), dimension, NamedElement.listNames(axes, dimension, ", "))); } /* @@ -385,6 +394,21 @@ previous: for (int i=components.size(); --i >= 0;) { } decoder.datumCache[datumIndex] = datum; /* + * We can not go further if the number of dimensions is not valid for the coordinate system to build. + * This error may happen for example when the CRS type is geographic, but only the latitude axis has + * been declared (without longitude axis). It may happen for example with a (latitude, time) system. + * In such case, we can build an engineering CRS has a replacement. + */ + if (dimension < minDim) { + final CRSBuilder<EngineeringDatum, ?> eng = new CRSBuilder.Engineering(); + System.arraycopy(axes, 0, eng.axes, 0, dimension); + eng.dimension = dimension; + eng.datum = decoder.getDatumFactory().createEngineeringDatum( + IdentifiedObjects.getProperties(datum, Datum.IDENTIFIERS_KEY)); + eng.createFromDatum(decoder, grid); + return eng.referenceSystem; + } + /* * Verify if a pre-defined coordinate system can be used. This is often the case, for example * the EPSG::6424 coordinate system can be used for (longitude, latitude) axes in degrees. * Using a pre-defined CS allows us to get more complete definitions (minimum and maximum values, etc.). @@ -404,23 +428,7 @@ previous: for (int i=components.size(); --i >= 0;) { * 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; - if (coordinateSystem == null) { - // Fallback if the coordinate system is not predefined. - final StringJoiner joiner = new StringJoiner(" "); - final CSFactory csFactory = decoder.getCSFactory(); - final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[dimension]; - for (int i=0; i<iso.length; i++) { - final Axis axis = axes[i]; - joiner.add(axis.getName()); - iso[i] = axis.toISO(csFactory, i, grid); - } - createCS(csFactory, properties(joiner.toString()), iso); - properties = properties(coordinateSystem.getName()); - } else { - properties = properties(NamedElement.listNames(axes, dimension, " ")); - } - createCRS(decoder.getCRSFactory(), properties); + createFromDatum(decoder, grid); } /* * Creates the coordinate reference system using current value of `datum` and `coordinateSystem` fields. @@ -451,6 +459,32 @@ previous: for (int i=components.size(); --i >= 0;) { } /** + * Unconditionally creates a coordinate reference system, overwriting current {@link #referenceSystem} value. + * The {@link #datum} field must be initialized before to invoke this method. + */ + private void createFromDatum(final Decoder decoder, final boolean grid) + throws FactoryException, DataStoreException, IOException + { + final Map<String,?> properties; + if (coordinateSystem == null) { + // Fallback if the coordinate system is not predefined. + final StringJoiner joiner = new StringJoiner(" "); + final CSFactory csFactory = decoder.getCSFactory(); + final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[dimension]; + for (int i=0; i<iso.length; i++) { + final Axis axis = axes[i]; + joiner.add(axis.getName()); + iso[i] = axis.toISO(csFactory, i, grid); + } + createCS(csFactory, properties(joiner.toString()), iso); + properties = properties(coordinateSystem.getName()); + } else { + properties = properties(NamedElement.listNames(axes, dimension, " ")); + } + createCRS(decoder.getCRSFactory(), properties); + } + + /** * 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. @@ -547,6 +581,11 @@ previous: for (int i=components.size(); --i >= 0;) { */ private abstract static class Geodetic<CS extends CoordinateSystem> extends CRSBuilder<GeodeticDatum, CS> { /** + * Index for the cache of datum in the {@link Decoder#datumCache} array. + */ + static final int CACHE_INDEX = 0; + + /** * The coordinate reference system which is presumed the basis of datum on netCDF files. */ protected CommonCRS defaultCRS; @@ -563,7 +602,7 @@ previous: for (int i=components.size(); --i >= 0;) { * @param minDim minimum number of dimensions (2 or 3). */ Geodetic(final int minDim) { - super(GeodeticDatum.class, "GRS 1980", 0, minDim, 3); + super(GeodeticDatum.class, "GRS 1980", CACHE_INDEX, minDim, 3); } /** @@ -813,10 +852,15 @@ previous: for (int i=components.size(); --i >= 0;) { */ private static final class Vertical extends CRSBuilder<VerticalDatum, VerticalCS> { /** + * Index for the cache of datum in the {@link Decoder#datumCache} array. + */ + static final int CACHE_INDEX = Geodetic.CACHE_INDEX + 1; + + /** * Creates a new builder (invoked by lambda function). */ public Vertical() { - super(VerticalDatum.class, "Mean Sea Level", 1, 1, 1); + super(VerticalDatum.class, "Mean Sea Level", CACHE_INDEX, 1, 1); } /** @@ -873,10 +917,15 @@ previous: for (int i=components.size(); --i >= 0;) { */ private static final class Temporal extends CRSBuilder<TemporalDatum, TimeCS> { /** + * Index for the cache of datum in the {@link Decoder#datumCache} array. + */ + static final int CACHE_INDEX = Vertical.CACHE_INDEX + 1; + + /** * Creates a new builder (invoked by lambda function). */ public Temporal() { - super(TemporalDatum.class, "", 2, 1, 1); + super(TemporalDatum.class, "", CACHE_INDEX, 1, 1); } /** @@ -951,10 +1000,15 @@ previous: for (int i=components.size(); --i >= 0;) { */ private static final class Engineering extends CRSBuilder<EngineeringDatum, CoordinateSystem> { /** + * Index for the cache of datum in the {@link Decoder#datumCache} array. + */ + static final int CACHE_INDEX = Temporal.CACHE_INDEX + 1; + + /** * Creates a new builder (invoked by lambda function). */ public Engineering() { - super(EngineeringDatum.class, "affine coordinate system", 3, 1, 3); + super(EngineeringDatum.class, "affine coordinate system", CACHE_INDEX, 1, 3); } /** @@ -1004,5 +1058,5 @@ previous: for (int i=components.size(); --i >= 0;) { * Maximal {@link #datumIndex} value +1. The maximal value can be seen in the call to {@code super(…)} constructor * in the last inner class defined above. */ - static final int DATUM_CACHE_SIZE = 4; + static final int DATUM_CACHE_SIZE = Engineering.CACHE_INDEX + 1; } 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 e5cfd4a..5cee19c 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 @@ -110,6 +110,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 GRS 1980". + * Index in the cache are one of the {@code CACHE_INDEX} constants declared in {@link CRSBuilder}. * * @see CRSBuilder#build(Decoder, boolean) */ @@ -128,9 +129,10 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo final Map<Object,GridMapping> gridMapping; /** - * Cache of localization grids created for a given pair of (<var>x</var>,<var>y</var>) axes. Localization grids - * are expensive to compute and consume a significant amount of memory. The {@link Grid} instances returned by - * {@link #getGrids()} share localization grids only between variables using the exact same list of dimensions. + * Cache of localization grids created for a given pair of (<var>x</var>,<var>y</var>) axes. + * Localization grids are expensive to compute and consume a significant amount of memory. + * The {@link Grid} instances returned by {@link #getGridCandidates()} share localization + * grids only between variables using the exact same list of dimensions. * This {@code localizationGrids} cache allows to cover other cases. * * <div class="note"><b>Example:</b> @@ -407,11 +409,15 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo * Returns all grid geometries (related to coordinate systems) found in the netCDF file. * This method may return a direct reference to an internal array - do not modify. * + * <p>The number of grid geometries returned by this method may be greater that the actual number of + * grids in the file. A more extensive analysis is done by {@link Variable#findGrid(GridAdjustment)}, + * which may result in some grid candidates being filtered out.</p> + * * @return all grid geometries, or an empty array if none. * @throws IOException if an I/O operation was necessary but failed. * @throws DataStoreException if a logical error occurred. */ - public abstract Grid[] getGrids() throws IOException, DataStoreException; + public abstract Grid[] getGridCandidates() throws IOException, DataStoreException; /** * Returns for information purpose only the Coordinate Reference Systems present in this file. @@ -437,7 +443,7 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo */ if (list.isEmpty()) { final List<Exception> warnings = new ArrayList<>(); // For internal usage by Grid. - for (final Grid grid : getGrids()) { + for (final Grid grid : getGridCandidates()) { addIfNotPresent(list, grid.getCoordinateReferenceSystem(this, warnings, null, null)); } } diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Dimension.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Dimension.java index f04687f..6a30071 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Dimension.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Dimension.java @@ -28,7 +28,7 @@ import org.apache.sis.util.resources.Vocabulary; * and {@code Dimension.equals(object)} must return {@code true} if two {@code Dimension} * instances represent the same netCDF dimensions. This may require subclasses to override * {@link #hashCode()} and {@link #equals(Object)} if uniqueness is not guaranteed. - * This is needed by {@link Variable#getGrid} default implementation.</p> + * This is needed by {@link Variable#findGrid(GridAdjustment)} default implementation.</p> * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) 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 374d625..71af070 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 @@ -50,7 +50,7 @@ import org.apache.sis.util.ArraysExt; * @author Martin Desruisseaux (Geomatys) * @version 1.2 * - * @see Decoder#getGrids() + * @see Decoder#getGridCandidates() * * @since 0.3 * @module @@ -117,7 +117,7 @@ public abstract class Grid extends NamedElement { /** * Returns a localization grid having the same dimensions than this grid but in a different order. - * This method is invoked by {@link Variable#getGrid} when the localization grids created by + * This method is invoked by {@link Variable#findGrid(GridAdjustment)} when the localization grids created by * {@link Decoder} subclasses are not sufficient and must be tailored for a particular variable. * Subclasses shall verify that the given {@code dimensions} array met the following conditions: * @@ -164,7 +164,7 @@ public abstract class Grid extends NamedElement { * The list length should be equal to {@link #getSourceDimensions()}. * * <p>This list is usually equal to the {@link Variable#getGridDimensions()} list for all variables - * that are {@linkplain Variable#getGrid associated to this grid}. But those lists can also differ + * that are {@linkplain Variable#findGrid associated to this grid}. But those lists can also differ * in the following aspects:</p> * * <ul> diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridAdjustment.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridAdjustment.java index 595db87..17f521c 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridAdjustment.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridAdjustment.java @@ -32,12 +32,12 @@ import org.apache.sis.referencing.operation.transform.MathTransforms; /** - * Contains information computed together with {@link Variable#getGrid(GridAdjustment)} but which are still specific + * Contains information computed together with {@link Variable#findGrid(GridAdjustment)} but which are still specific * to the variable. Those information are kept in a class separated from {@link Grid} because the same {@code Grid} * instance may apply to many variables while {@code GridAdjustment} may contain amendments that are specific to a * particular {@link Variable} instance. * - * <p>Instance is created by {@link Variable#getGridGeometry()} and updated by {@link Variable#getGrid(GridAdjustment)}. + * <p>Instance is created by {@link Variable#getGridGeometry()} and updated by {@link Variable#findGrid(GridAdjustment)}. * Subclasses of {@link Variable} do not need to know the details of this class; they just need to pass it verbatim * to their parent class.</p> * @@ -53,7 +53,7 @@ public final class GridAdjustment { * be non-null if the localization grid has shorter dimensions than the dimensions of the variable, as documented * in {@link Convention#nameOfDimension(Variable, int)} javadoc. * - * <p>Created by {@link Variable#getGrid(GridAdjustment)} and is consumed by {@link Variable#getGridGeometry()}. + * <p>Created by {@link Variable#findGrid(GridAdjustment)} and is consumed by {@link Variable#getGridGeometry()}. * Some values may be {@link Double#NaN} if the {@code "resampling_interval"} attribute was not found. * This array may be longer than necessary.</p> * @@ -107,7 +107,7 @@ public final class GridAdjustment { * @param axes all axes in the netCDF file (not only the variable axes). * @param toGridDimensions in input, the dimensions to accept. In output, "label → grid dimension" entries. * @param convention convention for getting dimension labels. - * @return {@code true} if the {@code Variable.getGrid(…)} caller should abort. + * @return {@code true} if the {@code Variable.findGrid(…)} caller should abort. * * @see Convention#nameOfDimension(Variable, int) */ diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java index 9de8f12..f002f85 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java @@ -530,13 +530,13 @@ public abstract class Variable extends Node { * * <p>The default implementation provided in this {@code Variable} base class could be sufficient, but subclasses * are encouraged to override with a more efficient implementation or by exploiting information not available to this - * base class (for example UCAR {@link ucar.nc2.dataset.CoordinateSystem} objects) and invoke {@code super.getGrid(…)} + * base class (for example UCAR {@link ucar.nc2.dataset.CoordinateSystem} objects) and invoke {@code super.findGrid(…)} * as a fallback. The default implementation tries to build a grid in the following ways:</p> * * <ol class="verbose"> * <li><b>Grid of same dimension than this variable:</b> - * iterate over {@linkplain Decoder#getGrids() all localization grids} and search for an element having the - * same dimensions than this variable, i.e. where {@link Grid#getDimensions()} contains the same elements + * iterate over {@link Decoder#getGridCandidates() all localization grids} and search for an element having + * the same dimensions than this variable, i.e. where {@link Grid#getDimensions()} contains the same elements * than {@link #getGridDimensions()} (not necessarily in the same order). The {@link Grid#forDimensions(Dimension[])} * method will be invoked for reordering dimensions in the right order.</li> * @@ -558,12 +558,12 @@ public abstract class Variable extends Node { * Typically, subclasses will handle case #1 in above list and this implementation is invoked for case #2. * This method should be invoked only once, so subclasses do not need to cache the value. * - * @param adjustment subclasses shall ignore and pass verbatim to {@code super.getGrid(adjustment)}. + * @param adjustment subclasses shall ignore and pass verbatim to {@code super.findGrid(adjustment)}. * @return the grid geometry for this variable, or {@code null} if none. * @throws IOException if an error occurred while reading the data. * @throws DataStoreException if a logical error occurred. */ - protected Grid getGrid(final GridAdjustment adjustment) throws IOException, DataStoreException { + protected Grid findGrid(final GridAdjustment adjustment) throws IOException, DataStoreException { final Convention convention = decoder.convention(); /* * Collect all axis dimensions, in no particular order. We use this map for determining @@ -648,7 +648,7 @@ public abstract class Variable extends Node { Grid fallback = null; boolean fallbackMatches = false; final String[] axisNames = convention.namesOfAxisVariables(this); // Usually null. - for (final Grid candidate : decoder.getGrids()) { + for (final Grid candidate : decoder.getGridCandidates()) { final Grid grid = candidate.forDimensions(dimensions); if (grid != null) { final int gridDimension = grid.getSourceDimensions(); @@ -688,7 +688,7 @@ public abstract class Variable extends Node { gridDetermined = true; // Set first so we don't try twice in case of failure. final GridMapping gridMapping = GridMapping.forVariable(this); final GridAdjustment adjustment = new GridAdjustment(); - final Grid info = getGrid(adjustment); + final Grid info = findGrid(adjustment); if (info != null) { /* * This variable may have more dimensions than the grid. We need to reduce the list to the same @@ -814,10 +814,10 @@ public abstract class Variable extends Node { * this information is used for completing ISO 19115 metadata, providing a default implementation of * {@link Convention#roleOf(Variable)} method or for building string representation of this variable * among others. Those tasks are mostly for information purpose, except if {@code Variable} subclass - * failed to create a grid and we must rely on {@link #getGrid(GridAdjustment)} default implementation. + * failed to create a grid and we must rely on {@link #findGrid(GridAdjustment)} default implementation. * For actual georeferencing, use {@link #getGridGeometry()} instead.</div> * - * If {@link #getGrid(GridAdjustment)} returns a non-null value, then the list returned by this method should + * If {@link #findGrid(GridAdjustment)} returns a non-null value, then the list returned by this method should * contain all dimensions returned by {@link Grid#getDimensions()}. It may contain more dimension however. * Those additional dimensions can be considered as bands. Furthermore the dimensions of the {@code Grid} * may have a different {@linkplain Dimension#length() length} than the dimensions returned by this method. diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java index e0d9792..9fcfecb 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java @@ -217,7 +217,7 @@ public final class ChannelDecoder extends Decoder { /** * The grid geometries, created when first needed. * - * @see #getGrids() + * @see #getGridCandidates() */ private transient Grid[] gridGeometries; @@ -963,7 +963,7 @@ public final class ChannelDecoder extends Decoder { */ @Override @SuppressWarnings("ReturnOfCollectionOrArrayField") - public Grid[] getGrids() { + public Grid[] getGridCandidates() { if (gridGeometries == null) { /* * First, find all variables which are used as coordinate system axis. The keys in the map are 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 a6c3162..27c97fd 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 @@ -192,8 +192,8 @@ next: for (final String name : axisNames) { } } /* - * In this method, 'sourceDim' and 'targetDim' are relative to "grid to CRS" conversion. - * So 'sourceDim' is the grid (domain) dimension and 'targetDim' is the CRS (range) dimension. + * In this method, `sourceDim` and `targetDim` are relative to "grid to CRS" conversion. + * So `sourceDim` is the grid (domain) dimension and `targetDim` is the CRS (range) dimension. */ final Axis[] axes = new Axis[range.length]; for (final SortedMap.Entry<VariableInfo,Integer> entry : variables.entrySet()) { @@ -225,7 +225,7 @@ next: for (final String name : axisNames) { /** * Returns a hash code for this grid. A map of {@code GridInfo} is used by - * {@link ChannelDecoder#getGrids()} for sharing existing instances. + * {@link ChannelDecoder#getGridCandidates()} for sharing existing instances. */ @Override public int hashCode() { diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java index 97b8fc0..17534ce 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java @@ -160,12 +160,12 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { private final DataType dataType; /** - * The grid geometry associated to this variable, computed by {@link ChannelDecoder#getGrids()} when first needed. + * The grid geometry associated to this variable, computed by {@link ChannelDecoder#getGridCandidates()} when first needed. * May stay {@code null} if the variable is not a data cube. We do not need disambiguation between the case where * the grid has not yet been computed and the case where the computation has been done with {@code null} result, - * because {@link #getGrid(GridAdjustment)} should be invoked only once per variable. + * because {@link #findGrid(GridAdjustment)} should be invoked only once per variable. * - * @see #getGrid(GridAdjustment) + * @see #findGrid(GridAdjustment) */ GridInfo grid; @@ -509,17 +509,17 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { /** * Returns a builder for the grid geometry of this variable, or {@code null} if this variable is not a data cube. * The grid geometry builders are opportunistically cached in {@code VariableInfo} instances after they have been - * computed by {@link ChannelDecoder#getGrids()}. This method delegates to the super-class method only if the grid - * requires more analysis than the one performed by {@link ChannelDecoder}. + * computed by {@link ChannelDecoder#getGridCandidates()}. This method delegates to the super-class method only + * if the grid requires more analysis than the one performed by {@link ChannelDecoder}. * - * @see ChannelDecoder#getGrids() + * @see ChannelDecoder#getGridCandidates() */ @Override - protected Grid getGrid(final GridAdjustment adjustment) throws IOException, DataStoreException { + protected Grid findGrid(final GridAdjustment adjustment) throws IOException, DataStoreException { if (grid == null) { - decoder.getGrids(); // Force calculation of grid geometries if not already done. - if (grid == null) { // May have been computed as a side-effect of decoder.getGrids(). - grid = (GridInfo) super.getGrid(adjustment); // Non-null if grid dimensions are different than this variable. + decoder.getGridCandidates(); // Force calculation of grid geometries if not already done. + if (grid == null) { // May have been computed as a side-effect of getGridCandidates(). + grid = (GridInfo) super.findGrid(adjustment); // Non-null if grid dimensions are different than this variable. } } return grid; diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java index bdb3093..594713c 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java @@ -36,6 +36,7 @@ import ucar.units.UnitException; import ucar.nc2.time.Calendar; import ucar.nc2.time.CalendarDate; import ucar.nc2.time.CalendarDateFormatter; +import ucar.nc2.dt.GridDataset; import ucar.nc2.ft.FeatureDataset; import ucar.nc2.ft.FeatureDatasetPoint; import ucar.nc2.ft.FeatureDatasetFactoryManager; @@ -90,7 +91,7 @@ public final class DecoderWrapper extends Decoder implements CancelTask { private transient VariableWrapper[] variables; /** - * The discrete sampling features, or {@code null} if none. + * The discrete sampling features or grids found by UCAR library, or {@code null} if none. * This reference is kept for making possible to close it in {@link #close()}. * * @see #getDiscreteSampling(Object) @@ -100,7 +101,7 @@ public final class DecoderWrapper extends Decoder implements CancelTask { /** * The grid geometries, computed when first needed. * - * @see #getGrids() + * @see #getGridCandidates() */ private transient Grid[] geometries; @@ -430,6 +431,21 @@ public final class DecoderWrapper extends Decoder implements CancelTask { } /** + * Returns the set of features found by UCAR, or {@code null} if none. + * May be an instance of {@link FeatureDataset} or {@link GridDataset} among others. + * + * <p>Note that invoking this method may be costly. It seems that the UCAR library + * attemps to read at least the coordinate values of coordinate system axes.</p> + */ + private FeatureDataset getFeatureDataSet() throws IOException { + if (features == null && file instanceof NetcdfDataset) { + features = FeatureDatasetFactoryManager.wrap(null, (NetcdfDataset) file, this, + new Formatter(new LogAdapter(listeners), listeners.getLocale())); + } + return features; + } + + /** * If this decoder can handle the file content as features, returns handlers for them. * * @param lock the lock to use in {@code synchronized(lock)} statements. @@ -440,10 +456,7 @@ public final class DecoderWrapper extends Decoder implements CancelTask { @Override @SuppressWarnings("null") public DiscreteSampling[] getDiscreteSampling(final Object lock) throws IOException, DataStoreException { - if (features == null && file instanceof NetcdfDataset) { - features = FeatureDatasetFactoryManager.wrap(null, (NetcdfDataset) file, this, - new Formatter(new LogAdapter(listeners), listeners.getLocale())); - } + final FeatureDataset features = getFeatureDataSet(); if (features instanceof FeatureDatasetPoint) { final List<DsgFeatureCollection> fc = ((FeatureDatasetPoint) features).getPointFeatureCollectionList(); if (fc != null && !fc.isEmpty()) { @@ -468,22 +481,30 @@ public final class DecoderWrapper extends Decoder implements CancelTask { * Returns all grid geometries (related to coordinate systems) found in the netCDF file. * This method returns a direct reference to an internal array - do not modify. * + * <p>In the case of those wrappers, this method may return more grid geometries than + * what the actual number of rasters (or data cubes) in the file. This is because an + * {@linkplain VariableWrapper#findGrid additional filtering is done by the variable}. + * Consequently this method is not completely reliable for determining if the file + * contains grids.</p> + * * @return all grid geometries, or an empty array if none. * @throws IOException if an I/O operation was necessary but failed. */ @Override @SuppressWarnings({"ReturnOfCollectionOrArrayField"}) - public Grid[] getGrids() throws IOException { + public Grid[] getGridCandidates() throws IOException { if (geometries == null) { List<CoordinateSystem> systems = Collections.emptyList(); if (file instanceof NetcdfDataset) { + /* + * We take all coordinate systems as associated to a grid. As an alternative, + * we tried to invoke `getFeatureDataSet()` and cast to UCAR `GridDataset`, + * but it causes the loading of large data for an end result often the same. + */ final NetcdfDataset ds = (NetcdfDataset) file; systems = ds.getCoordinateSystems(); } - geometries = new Grid[systems.size()]; - for (int i=0; i<geometries.length; i++) { - geometries[i] = new GridWrapper(systems.get(i)); - } + geometries = systems.stream().map(GridWrapper::new).toArray(Grid[]::new); } return geometries; } 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 b31df7f..2d0cde9 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 @@ -41,6 +41,9 @@ import org.apache.sis.util.ArraysExt; * However the UCAR model takes a different point of view where the coordinate system holds some * of the grid geometry information. * + * <p>{@code GridWrapper} instances do not contain data; they are only about the geometry of grids. + * Many netCDF variables may be associated to the same {@code GridWrapper} instance.</p> + * * @author Martin Desruisseaux (Geomatys) * @version 1.1 * @since 0.3 @@ -122,7 +125,7 @@ final class GridWrapper extends Grid { /** * Returns a localization grid having the same dimensions than this grid but in a different order. - * This method is invoked by {@link VariableWrapper#getGrid} when the localization grids created + * This method is invoked by {@link VariableWrapper#findGrid} when the localization grids created * by {@link DecoderWrapper} are not sufficient and must be tailored for a particular variable. * Returns {@code null} if this grid contains a dimension not in the given list. */ diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java index d821066..9bae8f6 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java @@ -249,26 +249,26 @@ final class VariableWrapper extends org.apache.sis.internal.netcdf.Variable { /** * Returns a builder for the grid geometry of this variable, or {@code null} if this variable is not a data cube. - * This method searches for a grid previously computed by {@link DecoderWrapper#getGrids()}, keeping in mind that - * the UCAR library sometime builds {@link CoordinateSystem} instances with axes in different order than what we - * would expect. This method delegates to the super-class method only if the grid requires a different analysis - * than the one performed by UCAR library. + * This method searches for a grid previously computed by {@link DecoderWrapper#getGridCandidates()}, + * keeping in mind that the UCAR library sometime builds {@link CoordinateSystem} instances with axes + * in different order than what we would expect. This method delegates to the super-class method only + * if the grid requires a different analysis than the one performed by UCAR library. * * <p>This method should be invoked by {@link #getGridGeometry()} only once. * For that reason, it does not need to cache the value.</p> * - * @see DecoderWrapper#getGrids() + * @see DecoderWrapper#getGridCandidates() */ @Override - protected Grid getGrid(final GridAdjustment adjustment) throws IOException, DataStoreException { + protected Grid findGrid(final GridAdjustment adjustment) throws IOException, DataStoreException { /* * In some netCDF files, more than one grid could be associated to a variable. If the names of the * variables to use as coordinate system axes have been specified, use those names for filtering. * Otherwise no filtering is applied (which is the common case). If more than one grid fit, take * the first grid having the largest number of dimensions. * - * This block duplicates work done in super.getGrid(…), except that it focuses on the set of coordinate - * systems identified by UCAR for this variable while super.getGrid(…) inspects all dimensions found in + * This block duplicates work done in super.findGrid(…), except that it focuses on the set of coordinate + * systems identified by UCAR for this variable while super.findGrid(…) inspects all dimensions found in * the file. Note that those coordinate systems may have been set by the user. */ if (variable instanceof VariableDS) { @@ -276,7 +276,7 @@ final class VariableWrapper extends org.apache.sis.internal.netcdf.Variable { if (!systems.isEmpty()) { GridWrapper grid = null; final String[] axisNames = decoder.convention().namesOfAxisVariables(this); - for (final Grid candidate : decoder.getGrids()) { + for (final Grid candidate : decoder.getGridCandidates()) { final GridWrapper ordered = ((GridWrapper) candidate).forVariable(variable, systems, axisNames); if (ordered != null && (grid == null || ordered.getSourceDimensions() > grid.getSourceDimensions())) { grid = ordered; @@ -293,7 +293,7 @@ final class VariableWrapper extends org.apache.sis.internal.netcdf.Variable { * can map to the variable dimension using attribute values. This mechanism is described * in Convention.nameOfDimension(…). */ - return (GridWrapper) super.getGrid(adjustment); + return (GridWrapper) super.findGrid(adjustment); } /** diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java index cff4023..3be066d 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java @@ -112,7 +112,7 @@ import static org.apache.sis.internal.util.CollectionsExt.first; * @author Martin Desruisseaux (Geomatys) * @author Thi Phuong Hao Nguyen (VNSC) * @author Alexis Manin (Geomatys) - * @version 1.0 + * @version 1.2 * @since 0.3 * @module */ @@ -182,6 +182,11 @@ final class MetadataReader extends MetadataBuilder { private VerticalCRS verticalCRS; /** + * Whether at least one grid coverage has been found during iteration over variables. + */ + private boolean hasGridCoverages; + + /** * Creates a new <cite>netCDF to ISO</cite> mapper for the given source. * * @param decoder the source of netCDF attributes. @@ -225,7 +230,7 @@ final class MetadataReader extends MetadataBuilder { final List<String> items = new ArrayList<>(); int start = 0; // Index of the first character of the next item to add in the list. int end; // Index after the last character of the next item to add in the list. - int next; // Index of the next separator (comma) after 'end'. + int next; // Index of the next separator (comma) after `end`. final int length = CharSequences.skipTrailingWhitespaces(value, 0, value.length()); split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, length)) < length) { if (value.charAt(start) == QUOTE) { @@ -461,7 +466,7 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt role = isPointOfContact ? Role.POINT_OF_CONTACT : keys.DEFAULT_ROLE; } /* - * Verify if we can share the existing 'pointOfContact' instance. This is often the case in practice. + * Verify if we can share the existing `pointOfContact` instance. This is often the case in practice. * If we can not share the whole existing instance, we usually can share parts of it like the address. */ Responsibility responsibility = pointOfContact; @@ -639,9 +644,10 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt } /* * Add spatial representation type only if it was not explicitly given in the metadata. - * The call to getGrids() may be relatively costly, so we don't want to invoke it without necessity. + * The call to getGridCandidates() may be relatively costly, so we don't want to invoke + * it without necessity. */ - if (!hasDataType && decoder.getGrids().length != 0) { + if (!hasDataType && decoder.getGridCandidates().length != 0) { addSpatialRepresentation(SpatialRepresentationType.GRID); } /* @@ -673,7 +679,7 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt setFormat(NetcdfStoreProvider.NAME); id = null; } catch (MetadataStoreException e) { - // Will add 'id' at the end of this method. + // Will add `id` at the end of this method. warning(e); } if (format.length >= 2) { @@ -682,7 +688,7 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt setFormatEdition(format[2]); } } - addFormatName(id); // Do nothing is 'id' is null. + addFormatName(id); // Do nothing is `id` is null. } /** @@ -693,13 +699,18 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt * @throws ArithmeticException if the size of an axis exceeds {@link Integer#MAX_VALUE}, or other overflow occurs. */ private void addSpatialRepresentationInfo(final Grid cs) throws IOException, DataStoreException { + /* + * We work on grid axes instead of Coordinate Reference System axes because + * `metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionSize` + * seems to imply that. + */ final Axis[] axes = cs.getAxes(decoder); for (int i=0; i<axes.length; i++) { final Axis axis = axes[i]; /* * Axes usually have exactly one dimension. However some netCDF axes are backed by a two-dimensional * conversion grid. In such case, our Axis constructor should have ensured that the first element in - * the 'sourceDimensions' and 'sourceSizes' arrays are for the grid dimension which is most closely + * the `sourceDimensions` and `sourceSizes` arrays are for the grid dimension which is most closely * oriented toward the axis direction. */ final int d = i; // Because lambda expressions want final variable. @@ -885,6 +896,7 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt names[i] = dimensions.get(i).getName(); } CollectionsExt.addToMultiValuesMap(contents, Arrays.asList(names), variable); + hasGridCoverages = true; } } final String processingLevel = stringValue(PROCESSING_LEVEL); @@ -1032,19 +1044,17 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt * Add the dimension information, if any. This metadata node * is built from the netCDF CoordinateSystem objects. */ - boolean hasGrids = false; - for (final Grid cs : decoder.getGrids()) { + for (final Grid cs : decoder.getGridCandidates()) { if (cs.getSourceDimensions() >= Grid.MIN_DIMENSION && cs.getTargetDimensions() >= Grid.MIN_DIMENSION) { addSpatialRepresentationInfo(cs); - hasGrids = true; } } - setISOStandards(hasGrids); + setISOStandards(hasGridCoverages); addFileIdentifier(); /* - * Deperture: UnidataDD2MI.xsl puts the source in Metadata.dataQualityInfo.lineage.statement. + * Departure: UnidataDD2MI.xsl puts the source in Metadata.dataQualityInfo.lineage.statement. * However since ISO 19115:2014, Metadata.resourceLineage.statement seems a more appropriate place. * See https://issues.apache.org/jira/browse/SIS-361 */ diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/package-info.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/package-info.java index 491fc02..2955c72 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/package-info.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/package-info.java @@ -40,7 +40,7 @@ * Care must be taken for avoiding confusion when using SIS and UCAR libraries together. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.1 + * @version 1.2 * @since 0.3 * @module */ 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 e681a20..f9f487d 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 @@ -70,12 +70,12 @@ public strictfp class GridTest extends TestCase { */ @Test public void testDimensions() throws IOException, DataStoreException { - Grid geometry = getSingleton(filter(selectDataset(TestData.NETCDF_2D_GEOGRAPHIC).getGrids())); + Grid geometry = getSingleton(filter(selectDataset(TestData.NETCDF_2D_GEOGRAPHIC).getGridCandidates())); assertEquals("getSourceDimensions()", 2, geometry.getSourceDimensions()); assertEquals("getTargetDimensions()", 2, geometry.getTargetDimensions()); final int n = includeRuntimeDimension ? 5 : 4; - geometry = getSingleton(filter(selectDataset(TestData.NETCDF_4D_PROJECTED).getGrids())); + geometry = getSingleton(filter(selectDataset(TestData.NETCDF_4D_PROJECTED).getGridCandidates())); assertEquals("getSourceDimensions()", 4, geometry.getSourceDimensions()); assertEquals("getTargetDimensions()", n, geometry.getTargetDimensions()); } @@ -89,7 +89,7 @@ public strictfp class GridTest extends TestCase { @Test @DependsOnMethod("testDimensions") public void testAxes2D() throws IOException, DataStoreException { - final Axis[] axes = getSingleton(filter(selectDataset(TestData.NETCDF_2D_GEOGRAPHIC).getGrids())).getAxes(decoder()); + final Axis[] axes = getSingleton(filter(selectDataset(TestData.NETCDF_2D_GEOGRAPHIC).getGridCandidates())).getAxes(decoder()); assertEquals(2, axes.length); final Axis x = axes[0]; final Axis y = axes[1]; @@ -113,7 +113,7 @@ public strictfp class GridTest extends TestCase { @Test @DependsOnMethod("testDimensions") public void testAxes4D() throws IOException, DataStoreException { - final Axis[] axes = getSingleton(filter(selectDataset(TestData.NETCDF_4D_PROJECTED).getGrids())).getAxes(decoder()); + final Axis[] axes = getSingleton(filter(selectDataset(TestData.NETCDF_4D_PROJECTED).getGridCandidates())).getAxes(decoder()); assertEquals(includeRuntimeDimension ? 5 : 4, axes.length); final Axis x = axes[0]; final Axis y = axes[1];