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 4f66c86c4592cdc01d4bc79ea3a10cb0f4c1ed83 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Sep 11 19:27:10 2024 +0200 Refactor the handling of the HYCOM special case, which is replaced by `VariableTransformer`. The special case was about converting an axis of encoded "year month day" values to instants. We need to handle another case used in AML IWC. The difficulty is that they are climatological data (no particular year) with time values encoded as scharacter strings instead of numbers. --- .../org/apache/sis/storage/netcdf/base/Axis.java | 18 +- .../apache/sis/storage/netcdf/base/CRSBuilder.java | 16 +- .../apache/sis/storage/netcdf/base/Convention.java | 8 +- .../apache/sis/storage/netcdf/base/Decoder.java | 16 +- .../apache/sis/storage/netcdf/base/FeatureSet.java | 26 +- .../sis/storage/netcdf/base/GridMapping.java | 11 +- .../org/apache/sis/storage/netcdf/base/HYCOM.java | 124 -------- .../org/apache/sis/storage/netcdf/base/Node.java | 21 +- .../apache/sis/storage/netcdf/base/Variable.java | 86 +++--- .../storage/netcdf/base/VariableTransformer.java | 312 +++++++++++++++++++++ .../sis/storage/netcdf/classic/ChannelDecoder.java | 12 +- .../sis/storage/netcdf/classic/GridInfo.java | 22 +- .../sis/storage/netcdf/classic/VariableInfo.java | 36 +-- .../sis/storage/netcdf/ucar/GridWrapper.java | 3 +- .../sis/storage/netcdf/ucar/VariableWrapper.java | 1 + 15 files changed, 455 insertions(+), 257 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Axis.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Axis.java index 8bff2968de..dca2c1dd06 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Axis.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Axis.java @@ -122,7 +122,7 @@ public final class Axis extends NamedElement { * In particular, this field has no meaning for CRS of geometries in a {@link FeatureSet}. * See {@link #Axis(Variable)} for a list of methods than cannot be used in such case.</p> * - * @suu #getNumDimensions() + * @see #getNumDimensions() * @see #getMainDirection() */ final int[] gridDimensionIndices; @@ -157,7 +157,7 @@ public final class Axis extends NamedElement { /** * Creates an axis for a {@link FeatureSet}. This constructor leaves the {@link #gridDimensionIndices} - * and {@link #gridSizes} array to {@code null}, which forbid the use of following methods: + * and {@link #gridSizes} array to {@code null}, which forbids the use of following methods: * * <ul> * <li>{@link #mainDimensionFirst(Axis[], int)}</li> @@ -178,21 +178,24 @@ public final class Axis extends NamedElement { } /** - * Constructs a new axis associated to an arbitrary number of grid dimension. The given arrays are stored + * Constructs a new axis associated to an arbitrary number of grid dimensions. The given arrays are stored * as-in (not cloned) and their content may be modified after construction by {@link Grid#getAxes(Decoder)}. * * @param abbreviation axis abbreviation, also identifying its type. This is a controlled vocabulary. * @param direction direction of positive values ("up" or "down"), or {@code null} if unknown. * @param gridDimensionIndices indices of grid dimension associated to this axis, initially in netCDF order. * @param gridSizes number of cell elements along above grid dimensions, as unsigned integers. + * @param dimension number of valid elements in {@code gridDimensionIndices} and {@code gridSizes}. * @param coordinates coordinates of the localization grid used by this axis. * @throws IOException if an I/O operation was necessary but failed. * @throws DataStoreException if a logical error occurred. * @throws ArithmeticException if the size of an axis exceeds {@link Integer#MAX_VALUE}, or other overflow occurs. */ - public Axis(final char abbreviation, final String direction, final int[] gridDimensionIndices, final int[] gridSizes, - final Variable coordinates) throws IOException, DataStoreException + public Axis(final char abbreviation, final String direction, int[] gridDimensionIndices, int[] gridSizes, + int dimension, final Variable coordinates) throws IOException, DataStoreException { + gridDimensionIndices = ArraysExt.resize(gridDimensionIndices, dimension); + gridSizes = ArraysExt.resize(gridSizes, dimension); /* * Try to get the axis direction from one of the following sources, * in preference order (unless an inconsistency is detected): @@ -385,7 +388,7 @@ public final class Axis extends NamedElement { /** * Returns the number of dimension of the localization grid used by this axis. - * This method returns 2 if this axis if backed by a localization grid having 2 or more dimensions. + * This method returns 2 if this axis is backed by a localization grid having 2 or more dimensions. * In the netCDF UCAR library, such axes are handled by a {@link ucar.nc2.dataset.CoordinateAxis2D}. * * @return number of dimension of the localization grid used by this axis. @@ -917,7 +920,8 @@ public final class Axis extends NamedElement { data = data.transform(tr.getScale(), tr.getOffset()); // Apply scale and offset attributes, if any. return data; } else { - throw new DataStoreException(coordinates.resources().getString(Resources.Keys.CanNotUseAxis_1, getName())); + throw new DataStoreException(coordinates.decoder.resources() + .getString(Resources.Keys.CanNotUseAxis_1, getName())); } } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java index b3d4d844fd..5f1ce20493 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java @@ -202,11 +202,11 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> { final List<GridCacheValue> linearizations, final Matrix reorderGridToCRS) throws DataStoreException, FactoryException, IOException { - final List<CRSBuilder<?,?>> builders = new ArrayList<>(4); + final var builders = new ArrayList<CRSBuilder<?,?>>(4); for (final Axis axis : grid.getAxes(decoder)) { dispatch(builders, axis); } - final SingleCRS[] components = new SingleCRS[builders.size()]; + final var components = new SingleCRS[builders.size()]; for (int i=0; i < components.length; i++) { components[i] = builders.get(i).build(decoder, true); } @@ -240,11 +240,11 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> { static CoordinateReferenceSystem assemble(final Decoder decoder, final Iterable<Variable> axes, final SingleCRS[] time) throws DataStoreException, FactoryException, IOException { - final List<CRSBuilder<?,?>> builders = new ArrayList<>(4); + final var builders = new ArrayList<CRSBuilder<?,?>>(4); for (final Variable axis : axes) { dispatch(builders, new Axis(axis)); } - final SingleCRS[] components = new SingleCRS[builders.size()]; + final var components = new SingleCRS[builders.size()]; int n = 0; for (final CRSBuilder<?, ?> cb : builders) { final SingleCRS c = cb.build(decoder, false); @@ -310,7 +310,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> { * block below fixes it. */ if (addTo == Projected.class) { -previous: for (int i=components.size(); --i >= 0;) { +previous: for (int i = components.size(); --i >= 0;) { final CRSBuilder<?,?> replace = components.get(i); for (final Axis a : replace.axes) { if (a.abbreviation != 'h') { @@ -377,7 +377,7 @@ previous: for (int i=components.size(); --i >= 0;) { * By contrast, `DataStoreContentException` would be treated as a fatal error. */ final Variable axis = getFirstAxis().coordinates; - throw new FactoryException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4, + throw new FactoryException(axis.decoder.resources().getString(Resources.Keys.UnexpectedAxisCount_4, axis.getFilename(), getClass().getSimpleName(), dimension, NamedElement.listNames(axes, dimension, ", "))); } /* @@ -475,7 +475,7 @@ previous: for (int i=components.size(); --i >= 0;) { // Fallback if the coordinate system is not predefined. final StringJoiner joiner = new StringJoiner(" "); final CSFactory csFactory = decoder.getCSFactory(); - final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[dimension]; + final var iso = new CoordinateSystemAxis[dimension]; for (int i=0; i<iso.length; i++) { final Axis axis = axes[i]; joiner.add(axis.getName()); @@ -528,7 +528,7 @@ previous: for (int i=components.size(); --i >= 0;) { final Integer epsgCandidateCS(final Unit<?> defaultUnit) { Unit<?> unit = getFirstAxis().getUnit(); if (unit == null) unit = defaultUnit; - final AxisDirection[] directions = new AxisDirection[dimension]; + final var directions = new AxisDirection[dimension]; for (int i=0; i<directions.length; i++) { directions[i] = axes[i].direction; } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java index 07912902d8..1e5449b5c3 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java @@ -478,7 +478,7 @@ public class Convention { if (method == null) { return null; } - final Map<String,Object> definition = new HashMap<>(); + final var definition = new HashMap<String,Object>(); definition.put(CF.GRID_MAPPING_NAME, method); for (final String name : node.getAttributeNames()) try { final String ln = name.toLowerCase(Decoder.DATA_LOCALE); @@ -493,7 +493,7 @@ public class Convention { */ final Vector values = node.getAttributeAsVector(name); if (values == null || values.size() < 3) continue; - final BursaWolfParameters bp = new BursaWolfParameters(CommonCRS.WGS84.datum(), null); + final var bp = new BursaWolfParameters(CommonCRS.WGS84.datum(), null); bp.setValues(values.doubleValues()); value = bp; break; @@ -709,7 +709,7 @@ public class Convention { * @return no-data values with bitmask of their roles or textual descriptions. */ public Map<Number,Object> nodataValues(final Variable data) { - final Map<Number,Object> pads = new LinkedHashMap<>(); + final var pads = new LinkedHashMap<Number,Object>(); for (int i=0; i < NODATA_ATTRIBUTES.length; i++) { final String name = NODATA_ATTRIBUTES[i]; final Vector values = data.getAttributeAsVector(name); @@ -748,7 +748,7 @@ public class Convention { * If scale_factor and/or add_offset variable attributes are present, then this is * a "packed" variable. Otherwise the transfer function is the identity transform. */ - final TransferFunction tr = new TransferFunction(); + final var tr = new TransferFunction(); final double scale = data.getAttributeAsDouble(CDM.SCALE_FACTOR); final double offset = data.getAttributeAsDouble(CDM.ADD_OFFSET); if (!Double.isNaN(scale)) tr.setScale (scale); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java index b559329826..545f8c968b 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java @@ -27,7 +27,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.Level; -import java.time.ZoneId; import java.time.ZoneOffset; import java.time.temporal.Temporal; import java.io.IOException; @@ -199,7 +198,10 @@ public abstract class Decoder extends ReferencingFactoryContainer { * @throws DataStoreException if an error occurred while interpreting the netCDF file content. */ public final void applyOtherConventions() throws IOException, DataStoreException { - HYCOM.convert(this, getVariables()); + final var t = new VariableTransformer(this); + for (Variable variable : getVariables()) { + t.analyze(variable); + } } /** @@ -328,7 +330,7 @@ public abstract class Decoder extends ReferencingFactoryContainer { * @param value the illegal value. * @param e the exception, or {@code null} if none. */ - final void illegalAttributeValue(final String name, final String value, final NumberFormatException e) { + final void illegalAttributeValue(final String name, final String value, final Exception e) { listeners.warning(resources().getString(Resources.Keys.IllegalAttributeValue_3, getFilename(), name, value), e); } @@ -355,7 +357,7 @@ public abstract class Decoder extends ReferencingFactoryContainer { * * @return the timezone for dates. */ - public ZoneId getTimeZone() { + public ZoneOffset getTimeZone() { return ZoneOffset.UTC; } @@ -440,7 +442,7 @@ public abstract class Decoder extends ReferencingFactoryContainer { * @throws DataStoreException if a logical error occurred. */ public final List<CoordinateReferenceSystem> getReferenceSystemInfo() throws IOException, DataStoreException { - final List<CoordinateReferenceSystem> list = new ArrayList<>(); + final var list = new ArrayList<CoordinateReferenceSystem>(); for (final Variable variable : getVariables()) { final GridMapping m = GridMapping.forVariable(variable); if (m != null) { @@ -453,7 +455,7 @@ public abstract class Decoder extends ReferencingFactoryContainer { * Consequently, if such information is present, grid CRS may be inaccurate. */ if (list.isEmpty()) { - final List<Exception> warnings = new ArrayList<>(); // For internal usage by Grid. + final var warnings = new ArrayList<Exception>(); // For internal usage by Grid. for (final Grid grid : getGridCandidates()) { addIfNotPresent(list, grid.getCoordinateReferenceSystem(this, warnings, null, null)); } @@ -530,7 +532,7 @@ public abstract class Decoder extends ReferencingFactoryContainer { * * @return the localized error resource bundle. */ - final Resources resources() { + public final Resources resources() { return Resources.forLocale(getLocale()); } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java index 66fe896ce5..11b224b32d 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java @@ -209,26 +209,26 @@ final class FeatureSet extends DiscreteSampling { * - Trajectory as a geometric object, potentially with a time characteristic. * - Time-varying properties (i.e. properties having a value per instant). */ - final FeatureTypeBuilder builder = new FeatureTypeBuilder( - decoder.nameFactory, decoder.geomlib, decoder.listeners.getLocale()); + final var builder = new FeatureTypeBuilder(decoder.nameFactory, decoder.geomlib, decoder.listeners.getLocale()); /* * Identifier and other static properties (one value per feature instance). */ for (int i = getReferencingDimension(false); i < properties.length; i++) { final Variable v = properties[i]; - final Class<?> type; + final Class<?> t; if (v.getEnumeration() != null) { - type = String.class; + t = String.class; } else { - type = v.getDataType().getClass(v.getNumDimensions() > 1); + t = v.getDataType().getClass(v.getNumDimensions() > 1); } - describe(v, builder.addAttribute(type)); + describe(v, builder.addAttribute(t)); } /* * Geometry object as a single point or a trajectory, associated with: * - A Coordinate Reference System (CRS) characteristic. * - A "datetimes" characteristic if a time axis exists. */ + @SuppressWarnings("LocalVariableHidesMemberVariable") DefaultTemporalCRS timeCRS = null; if (referencingDimension != 0) { final AttributeTypeBuilder<?> geometry; @@ -256,8 +256,8 @@ final class FeatureSet extends DiscreteSampling { */ for (int i = getReferencingDimension(true); i < dynamicProperties.length; i++) { final Variable v = dynamicProperties[i]; - final Class<?> type = (v.getEnumeration() != null || v.isString()) ? String.class : Number.class; - describe(v, builder.addAttribute(type).setMaximumOccurs(Integer.MAX_VALUE)); + final Class<?> t = (v.getEnumeration() != null || v.isString()) ? String.class : Number.class; + describe(v, builder.addAttribute(t).setMaximumOccurs(Integer.MAX_VALUE)); } /* * By default, `name` is a netCDF dimension name (see method javadoc), usually all lower-cases. @@ -302,8 +302,8 @@ final class FeatureSet extends DiscreteSampling { */ static FeatureSet[] create(final Decoder decoder, final DataStore lock) throws IOException, DataStoreException { assert Thread.holdsLock(lock); - final List<FeatureSet> features = new ArrayList<>(3); // Will usually contain at most one element. - final Map<Dimension,Boolean> done = new HashMap<>(); // Whether a dimension has already been used. + final var features = new ArrayList<FeatureSet>(3); // Will usually contain at most one element. + final var done = new HashMap<Dimension,Boolean>(); // Whether a dimension has already been used. for (final Variable v : decoder.getVariables()) { if (v.getRole() != VariableRole.FEATURE_PROPERTY) { continue; @@ -444,7 +444,7 @@ final class FeatureSet extends DiscreteSampling { * support mixing both modes (e.g. X and Y coordinates as static properties and T as dynamic property). * The variables are reordered for making sure that X, Y, Z, T are first and in that order. */ - final Reorder r = new Reorder(); + final var r = new Reorder(); features.add(new FeatureSet(decoder, featureName, (counts != null) ? counts.read() : null, r.toArray(properties, coordinates, false), @@ -908,8 +908,8 @@ makeGeom: if (!isEmpty) { } final Map<Integer,String> enumeration = p.getEnumeration(); if (enumeration != null && value instanceof Vector) { - final Vector data = (Vector) value; - final String[] meanings = new String[data.size()]; + final var data = (Vector) value; + final var meanings = new String[data.size()]; for (int j=0; j<meanings.length; j++) { String m = enumeration.get(data.intValue(j)); meanings[j] = (m != null) ? m : ""; diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java index 609cc18f5f..4bdf40c8ea 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java @@ -457,8 +457,7 @@ final class GridMapping { if (c.length == 6) { gridToCRS = new AffineTransform2D(c[1], c[4], c[2], c[5], c[0], c[3]); // X_DIMENSION, Y_DIMENSION } else { - canNotCreate(mapping, message, new DataStoreContentException( - Errors.forLocale(mapping.getLocale()) + canNotCreate(mapping, message, new DataStoreContentException(mapping.errors() .getString(Errors.Keys.UnexpectedArrayLength_2, 6, c.length))); } } @@ -519,9 +518,9 @@ final class GridMapping { * The WKT is presumed to use the GDAL flavor of WKT 1, and warnings are redirected to decoder listeners. */ private static CoordinateReferenceSystem createFromWKT(final Node node, final String wkt) throws ParseException { - final WKTFormat f = new WKTFormat(Decoder.DATA_LOCALE, TimeZone.getTimeZone(node.decoder.getTimeZone())); + final var f = new WKTFormat(Decoder.DATA_LOCALE, TimeZone.getTimeZone(node.decoder.getTimeZone())); f.setConvention(org.apache.sis.io.wkt.Convention.WKT1_COMMON_UNITS); - final CoordinateReferenceSystem crs = (CoordinateReferenceSystem) f.parseObject(wkt); + final var crs = (CoordinateReferenceSystem) f.parseObject(wkt); final Warnings warnings = f.getWarnings(); if (warnings != null) { final LogRecord record = new LogRecord(Level.WARNING, warnings.toString()); @@ -643,8 +642,8 @@ final class GridMapping { explicitG2C = implicitG2C; } else try { int count = 0; - MathTransform[] components = new MathTransform[3]; - final TransformSeparator sep = new TransformSeparator(implicitG2C, variable.decoder.getMathTransformFactory()); + var components = new MathTransform[3]; + final var sep = new TransformSeparator(implicitG2C, variable.decoder.getMathTransformFactory()); if (firstAffectedCoordinate != 0) { sep.addTargetDimensionRange(0, firstAffectedCoordinate); components[count++] = sep.separate(); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/HYCOM.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/HYCOM.java deleted file mode 100644 index 543f9f12e4..0000000000 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/HYCOM.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sis.storage.netcdf.base; - -import java.io.IOException; -import java.time.Instant; -import java.util.TimeZone; -import java.util.GregorianCalendar; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.apache.sis.math.Vector; -import org.apache.sis.measure.Units; -import org.apache.sis.storage.DataStoreException; -import org.apache.sis.util.privy.Constants; - - -/** - * Handles particularity of HYCOM format. It is not yet clear whether those particularities are used elsewhere or not. - * We handle them in a separated class for now and may refactor later in a more general mechanism for providing extensions. - * - * @author Martin Desruisseaux (Geomatys) - * - * @see <a href="https://issues.apache.org/jira/browse/SIS-315">SIS-315</a> - */ -final class HYCOM { - /** - * The pattern to use for identifying temporal units of the form "day as %Y%m%d.%f". - * "%Y" is year formatted as at least four digits, "%m" is month formatted as two digits, - * and "%d" is day of month formatted as two digits. - * - * Example: 20181017.0000 for 2018-10-17. - */ - private static final Pattern DATE_PATTERN = Pattern.compile("days?\\s+as\\s+(?-i)%Y%m%d.*", Pattern.CASE_INSENSITIVE); - - /** - * Do not allow instantiation of this class. - */ - private HYCOM() { - } - - /** - * If any variable uses the "day as %Y%m%d.%f" pseudo-units, converts to a number of days since the epoch. - * The epoch is taken from the unit of the dimension. Example of netCDF file header: - * - * <pre class="text"> - * dimensions: - * MT = UNLIMITED ; // (1 currently) - * Y = 3298 ; - * X = 4500 ; - * variables: - * double MT(MT) ; - * MT:long_name = "time" ; - * MT:units = "days since 1900-12-31 00:00:00" ; - * MT:calendar = "standard" ; - * MT:axis = "T" ; - * double Date(MT) ; - * Date:long_name = "date" ; - * Date:units = "day as %Y%m%d.%f" ; - * Date:C_format = "%13.4f" ; - * Date:FORTRAN_format = "(f13.4)" ; - * data: - * MT = 43024 ; - * Date = 20181017.0000 ;</pre> - * - * In this example, the real units of {@code Date(MT)} will be taken from {@code MT(MT)}, which is - * "days since 1900-12-31 00:00:00". - */ - static void convert(final Decoder decoder, final Variable[] variables) throws IOException, DataStoreException { - Matcher matcher = null; - for (final Variable variable : variables) { - if (variable.getNumDimensions() == 1) { - final String units = variable.getUnitsString(); - if (units != null) { - if (matcher == null) { - matcher = DATE_PATTERN.matcher(units); - } else { - matcher.reset(units); - } - if (matcher.matches()) { - final Dimension dimension = variable.getGridDimensions().get(0); - Instant epoch = variable.setUnit(decoder.findVariable(dimension.getName()), Units.DAY); - if (epoch == null) { - epoch = Instant.EPOCH; - } - final long origin = epoch.toEpochMilli(); - /* - * Convert all dates into numbers of days since the epoch. - */ - Vector values = variable.read(); - final double[] times = new double[values.size()]; - final GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone(decoder.getTimeZone()), Decoder.DATA_LOCALE); - calendar.clear(); - for (int i=0; i<times.length; i++) { - double time = values.doubleValue(i); // Date encoded as a double (e.g. 20181017) - long date = (long) time; // Round toward zero. - time -= date; // Fractional part of the day. - int day = (int) (date % 100); date /= 100; - int month = (int) (date % 100); date /= 100; - calendar.set(Math.toIntExact(date), month - 1, day, 0, 0, 0); - date = calendar.getTimeInMillis() - origin; // Milliseconds since epoch. - time += date / (double) Constants.MILLISECONDS_PER_DAY; - times[i] = time; - } - variable.setValues(times); - } - } - } - } - } -} diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java index fded643327..4c56220944 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java @@ -16,7 +16,6 @@ */ package org.apache.sis.storage.netcdf.base; -import java.util.Locale; import java.util.Collection; import org.apache.sis.math.Vector; import org.apache.sis.math.DecimalFunctions; @@ -252,31 +251,13 @@ public abstract class Node extends NamedElement { } } - /** - * Returns the locale to use for warnings and error messages. - * - * @return the locale for warnings and error messages. - */ - protected final Locale getLocale() { - return decoder.listeners.getLocale(); - } - - /** - * Returns the resources to use for warnings or error messages. - * - * @return the resources for the locales specified to the decoder. - */ - protected final Resources resources() { - return Resources.forLocale(getLocale()); - } - /** * Returns the resources to use for error messages. * * @return the resources for error messages using the locales specified to the decoder. */ final Errors errors() { - return Errors.forLocale(getLocale()); + return Errors.forLocale(decoder.getLocale()); } /** diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java index 8153d7dced..7288cdb144 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.ArrayList; import java.util.regex.Pattern; +import java.lang.reflect.Array; import java.io.IOException; import java.time.Instant; import ucar.nc2.constants.CDM; // String constants are copied by the compiler with no UCAR reference left. @@ -65,7 +66,7 @@ public abstract class Variable extends Node { * <p>All shared vectors shall be considered read-only.</p> * * @see #read() - * @see #setValues(Object) + * @see #setValues(Object, boolean) */ private static final WeakHashSet<Vector> SHARED_VECTORS = new WeakHashSet<>(Vector.class); @@ -166,7 +167,7 @@ public abstract class Variable extends Node { * (for example the values in coordinate system axes). * * @see #read() - * @see #setValues(Object) + * @see #setValues(Object, boolean) */ private transient Vector values; @@ -176,12 +177,12 @@ public abstract class Variable extends Node { * This is a different instance if this variable is a two-dimensional character array, in which case this field * is an instance of {@code List<String>}. * - * The difference between {@code values} and {@code valuesAnyType} is that {@code values.get(i)} may throw - * {@link NumberFormatException} because it always try to return its elements as {@link Number} instances, - * while {@code valuesAnyType.get(i)} can return {@link String} instances. + * <p>The difference between {@code values} and {@code valuesAnyType} is that {@code values.get(i)} may throw + * {@link NumberFormatException} because it always tries to return its elements as {@link Number} instances, + * while {@code valuesAnyType.get(i)} can return {@link String} instances.</p> * * @see #readAnyType() - * @see #setValues(Object) + * @see #setValues(Object, boolean) */ private transient List<?> valuesAnyType; @@ -356,26 +357,18 @@ public abstract class Variable extends Node { protected abstract Unit<?> parseUnit(String symbols) throws Exception; /** - * Sets the unit of measurement and the epoch to the same value as the given variable. - * This method is not used in CF-compliant files; it is reserved for the handling of some - * particular conventions, for example {@link HYCOM}. + * Sets the unit of measurement to the given value. This method is not used in CF-compliant files. + * It is reserved for the handling of some particular conventions, for example HYCOM. * - * @param other the variable from which to copy unit and epoch, or {@code null} if none. - * @param overwrite if non-null, set to the given unit instead of the unit of {@code other}. - * @return the epoch (may be {@code null}). + * @param unit the new unit of measurement. + * @param epich the epoch if the unit is temporal, or {@code null} otherwise. * * @see #getUnit() */ - final Instant setUnit(final Variable other, Unit<?> overwrite) { - if (other != null) { - unit = other.getUnit(); // May compute the epoch as a side effect. - epoch = other.epoch; - } - if (overwrite != null) { - unit = overwrite; - } + final void setUnit(final Unit<?> unit, final Instant epoch) { + this.unit = unit; + this.epoch = epoch; unitParsed = true; - return epoch; } /** @@ -483,7 +476,7 @@ public abstract class Variable extends Node { * @see #STRING_DIMENSION */ final boolean isString() { - return getDataType() == DataType.CHAR && getNumDimensions() >= STRING_DIMENSION; + return getNumDimensions() >= STRING_DIMENSION && getDataType() == DataType.CHAR; } /** @@ -1004,7 +997,7 @@ public abstract class Variable extends Node { @SuppressWarnings("ReturnOfCollectionOrArrayField") public final Vector read() throws IOException, DataStoreException { if (values == null) { - setValues(readFully()); + setValues(readFully(), false); } return values; } @@ -1012,9 +1005,12 @@ public abstract class Variable extends Node { /** * Reads all the data for this variable and returns them as a list of any object. * The difference between {@code read()} and {@code readAnyType()} is that {@code vector.get(i)} may throw - * {@link NumberFormatException} because it always try to return its elements as {@link Number} instances, + * {@link NumberFormatException} because it always tries to return its elements as {@link Number} instances, * while {@code list.get(i)} can return {@link String} instances. * + * @todo Consider extending to {@link java.time} objects as well. It would be useful in particular for + * climatological data, where objects may be {@link java.time.Month} or {@link java.time.MonthDay}. + * * @return the data as a list of numbers or strings. * @throws IOException if an error occurred while reading the data. * @throws DataStoreException if a logical error occurred. @@ -1023,7 +1019,7 @@ public abstract class Variable extends Node { @SuppressWarnings("ReturnOfCollectionOrArrayField") public final List<?> readAnyType() throws IOException, DataStoreException { if (valuesAnyType == null) { - setValues(readFully()); + setValues(readFully(), false); } return valuesAnyType; } @@ -1056,6 +1052,8 @@ public abstract class Variable extends Node { * Reads a subsampled sub-area of the variable and returns them as a list of any object. * Elements in the returned list may be {@link Number} or {@link String} instances. * + * @todo Consider extending to {@link java.time} objects as well. + * * @param area indices of cell values to read along each dimension, in "natural" order. * @param subsampling subsampling along each dimension, or {@code null} if none. * @return the data as a list of {@link Number} or {@link String} instances. @@ -1076,28 +1074,44 @@ public abstract class Variable extends Node { protected abstract Object readFully() throws IOException, DataStoreException; /** - * Sets the values in this variable. The values are normally read from the netCDF file by the {@link #read()} method, - * but this {@code setValues(Object)} method may also be invoked if the caller wants to overwrite those values. + * Sets the values in this variable. The values are normally read from the netCDF file by {@link #read()}, + * but this {@code setValues(…)} method may also be invoked if the caller wants to overwrite those values. * - * @param array the values as an array of primitive type (for example {@code float[]}. + * @param array the values as an array of primitive type (for example {@code float[]}. + * @param forceNumerics whether to force the replacement of character strings by real numbers. * @throws ArithmeticException if the dimensions of this variable are too large. */ - final void setValues(final Object array) { + final void setValues(final Object array, final boolean forceNumerics) { final DataType dataType = getDataType(); if (dataType == DataType.CHAR) { int n = getNumDimensions(); if (n >= STRING_DIMENSION) { final List<Dimension> dimensions = getGridDimensions(); - final int length = Math.toIntExact(dimensions.get(--n).length()); - long count = dimensions.get(--n).length(); - while (n > 0) { + final int length = Math.toIntExact(dimensions.get(--n).length()); // Number of characters per value. + long count = dimensions.get(--n).length(); // Number of values. + while (n > 0) { // In case of matrix of strings. count = Math.multiplyExact(count, dimensions.get(--n).length()); } - final String[] strings = createStringArray(array, Math.toIntExact(count), length); /* - * Following method calls take the array reference without cloning it. + * The character strings may have been replaced by real numbers by the caller. + * In such case, we need to take the vector as-is. + */ + if (forceNumerics) { + assert Array.getLength(array) == count : getName(); + values = SHARED_VECTORS.unique(Vector.create(array, false)); + valuesAnyType = values; + return; + } + /* + * Standard case. The `createStringArray(…)` method expects either `byte[]` or `char[]`, + * depending on the subclass. It may throw `ClassCastException` if the array is not of + * the expected class, but it should not happen unless there is a bug in our algorithm. + * + * The `Vector.create(…)` and `wrap(…)` method calls take the array reference without cloning it. * Consequently, creating those two objects now (even if we may not use them) is reasonably cheap. */ + assert Array.getLength(array) == count * length : getName(); + final String[] strings = createStringArray(array, Math.toIntExact(count), length); values = Vector.create(strings, false); valuesAnyType = UnmodifiableArrayList.wrap(strings); return; @@ -1162,7 +1176,7 @@ public abstract class Variable extends Node { protected final List<String> createStringList(final Object chars, final GridExtent area) { final int length = Math.toIntExact(area.getSize(0)); long count = area.getSize(1); - for (int i = area.getDimension(); --i >= 2;) { // As a safety, but should never enter in this loop. + for (int i = area.getDimension(); --i >= STRING_DIMENSION;) { // As a safety, but should never enter in this loop. count = Math.multiplyExact(count, area.getSize(i)); } return UnmodifiableArrayList.wrap(createStringArray(chars, Math.toIntExact(count), length)); @@ -1272,7 +1286,7 @@ public abstract class Variable extends Node { * @return the exception to throw. */ protected final DataStoreContentException canNotComputePosition(final ArithmeticException cause) { - return new DataStoreContentException(resources().getString( + return new DataStoreContentException(decoder.resources().getString( Resources.Keys.CanNotComputeVariablePosition_2, getFilename(), getName()), cause); } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/VariableTransformer.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/VariableTransformer.java new file mode 100644 index 0000000000..3a15424bb4 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/VariableTransformer.java @@ -0,0 +1,312 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.netcdf.base; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.measure.Unit; +import javax.measure.UnitConverter; +import javax.measure.quantity.Time; +import ucar.nc2.constants.CDM; +import org.apache.sis.math.Vector; +import org.apache.sis.measure.Units; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.util.privy.Constants; + + +/** + * Helper class for transforming a variable from one type to another type. + * This is used mostly for converting dates encoded in some calendar form + * to a number of temporal units since an epoch. Use cases: + * + * <ul> + * <li>Time axis with temporal coordinates encoded as character strings.</li> + * <li>Temporal coordinates encoded as numbers with "day as %Y%m%d.%f" pattern.</li> + * </ul> + * + * In the current implementation, the special cases handled by this class + * are detected solely from the unit of measurement. + * + * @author Martin Desruisseaux (Geomatys) + * + * @see <a href="https://issues.apache.org/jira/browse/SIS-315">SIS-315</a> + */ +final class VariableTransformer { + /** + * Patterns of unit of measurements that need to be handled in a special way. + * This enumeration contains special cases that we have met in practice. + * The pattern are tested in the order of enumeration values. + */ + private enum UnitPattern { + /** + * The pattern to use for identifying temporal units of the form "day as %Y%m%d.%f". + * "%Y" is year formatted as at least four digits, "%m" is month formatted as two digits, + * and "%d" is day of month formatted as two digits. + * For example, 20181017.0000 stands for 2018-10-17. + * + * <p>In HYCOM files, the transformations needs to be applied on "ordinary" variables. + * It should not be restricted to variables that are coordinate system axes.</p> + */ + HYCOM("days?\\s+as\\s+(?-i)%Y%m%d.*", Units.DAY, false, (matcher) -> new int[] {100, 100}), + + /** + * The pattern to use for identifying temporal units of the form "CCYYMMDDHHMMSS". + * The "CC" prefix identifies climatological time, for data repeated every year. + * The number of characters in each group determines the number of digits. + * The actual values are sometime stored as character strings. + * + * <p>The transformation will be applied on variables that are coordinate system axes.</p> + * + * @see <a href="https://www.admiralty.co.uk/defence/additional-military-layers#Specifications">Additional + * Military Layers (<abbr>AML</abbr>)</a> — Integrated water column (<abbr>IWC</abbr>) Annex C + */ + CLIMATOLOGICAL("C+Y+(M+)?(D+)?(H+)?(M+)?(S+)?", null, true, (matcher) -> { + final String order = "MDHMS"; + final int count = matcher.groupCount(); + final int[] bases = new int[count]; + for (int i=0; i<count; i++) { + String value = matcher.group(i+1); + int n = value.length(); + if (--n < 0 || Character.toUpperCase(value.charAt(0)) != order.charAt(i)) { + return null; // Fields out of order. + } + int base = 10; + while (--n >= 0) { + if ((base *= 10) < 0) return null; // Return null if overflow. + } + bases[i] = base; + } + return bases; + }); + + /** + * The compiled pattern. + */ + final Pattern pattern; + + /** + * Units of measurement implied by the pattern, + * or {@code null} if more analysis is needed for determining the unit. + */ + final Unit<Time> unit; + + /** + * A function providing values of 10ⁿ where <var>n</var> is the number of digits for the month, day, hour, + * minute and second fields, in that order. The year field is omitted because implicit. If the dates have + * no time of day, then the array length should be 2 instead of 5. Intermediate lengths are also allowed. + * Returns {@code null} if the match is invalid. + */ + final Function<Matcher, int[]> fieldBases; + + /** + * Whether to restrict the match to variables that are identified as time axis. This is a safety for avoiding + * false positive when the unit does not contain a clear keyword such as "days". We do not require time axis + * in all cases because {@link #datesToTimeSinceEpoch}, for example, needs to be applied on "ordinary" + * variables (i.e., variables that are not coordinate system axes). + */ + final boolean requireTimeAxis; + + /** + * Creates a new enumeration value. + * + * @param regex the regular expression to compile. + * @param unit units of measurement implied by the pattern, or {@code null}. + */ + private UnitPattern(final String regex, final Unit<Time> unit, final boolean requireTimeAxis, + final Function<Matcher, int[]> fieldBases) + { + pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + this.requireTimeAxis = requireTimeAxis; + this.fieldBases = fieldBases; + this.unit = unit; + } + } + + /** + * The decoder that produced the variables to transform. + */ + private final Decoder decoder; + + /** + * The matcher for each pattern to test, created when first needed. + */ + private Matcher[] matchers; + + /** + * Creates a new transformer for the given variable. + * + * @param decoder the decoder that produced the variables to transform. + */ + VariableTransformer(final Decoder decoder) { + this.decoder = decoder; + } + + /** + * Analyzes and (if needed) transforms the given variable. + * This method applies heuristic rules for detecting the transformation to apply. + * + * @param variable the variable to analyze and (if needed) to transform. + */ + final void analyze(final Variable variable) throws IOException, DataStoreException { + /* + * Heuristic rule #1: accept only one-dimensional variables. + * Character strings are two dimensional, but considered as + * one dimensional after each string is parsed as a number. + */ + switch (variable.getNumDimensions()) { + default: return; + case 1: break; + case Variable.STRING_DIMENSION: { + if (variable.getDataType() != DataType.CHAR) return; + break; + } + } + /* + * Heuristic rule #2: identify the special case from the unit of measurement + * and the axis type. The axis type is itself determined from the axis name. + */ + final String units = variable.getUnitsString(); + if (units != null) { + final UnitPattern[] candidates = UnitPattern.values(); + if (matchers == null) { + matchers = new Matcher[candidates.length]; + } + for (final UnitPattern candidate : candidates) { + if (!candidate.requireTimeAxis || AxisType.valueOf(variable) == AxisType.T) { + Matcher matcher = matchers[candidate.ordinal()]; + if (matcher == null) { + matcher = candidate.pattern.matcher(units); + matchers[candidate.ordinal()] = matcher; + } else { + matcher.reset(units); + } + if (matcher.matches()) { + final int[] fieldBases = candidate.fieldBases.apply(matcher); + if (fieldBases != null) { + datesToTimeSinceEpoch(variable, candidate.unit, fieldBases, + candidate == UnitPattern.CLIMATOLOGICAL); + } + } + } + } + } + } + + /** + * Converts from "Year Month Day Hours Minutes Seconds" pseudo-units to an amounts of time since the epoch. + * The {@code fieldBases} argument gives the values of 10ⁿ where <var>n</var> is the number of digits for + * the month, day, hour, minute and second fields, in that order. Trailing fields that do no exist should + * be omitted or have their value set to 1. + * + * @param variable the variable to transform. + * @param unit the target unit, or {@code null} keeping the current unit unchanged. + * @param fieldBases number of digits in month, day, hour, minute, second fields, as powers of 10. + * @param climatological whether a climatological calendar is used (data repeated each year). + */ + private void datesToTimeSinceEpoch(final Variable variable, Unit<Time> unit, final int[] fieldBases, + final boolean climatological) throws IOException, DataStoreException + { + final Unit<Time> unitOfLast; + switch (fieldBases.length) { + case 2: unitOfLast = Units.DAY; break; + case 3: unitOfLast = Units.HOUR; break; + case 4: unitOfLast = Units.MINUTE; break; + case 5: unitOfLast = Units.SECOND; break; + default: return; + } + /* + * The unit and epoch sometime need to be taken from another variable. + * In the following example from HYCOM, the real unit of `Date(MT)` will be taken from `MT(MT)`: + * + * variables: + * double MT(MT) + * MT:long_name = "time" + * MT:units = "days since 1900-12-31 00:00:00" + * MT:axis = "T" + * double Date(MT) + * Date:long_name = "date" + * Date:units = "day as %Y%m%d.%f" + * data: + * MT = 43024 + * Date = 20181017.0000 + */ + Instant epoch = Instant.EPOCH; + final Variable axis = decoder.findVariable(variable.getGridDimensions().get(0).getName()); + if (axis != variable && axis != null) { + Unit<?> t = axis.getUnit(); // Unconditional call because computes `epoch` as a side-effect. + if (unit == null) try { + unit = t.asType(Time.class); + } catch (ClassCastException e) { + decoder.illegalAttributeValue(CDM.UNITS, axis.getUnitsString(), e); + } + if (axis.epoch != null) { + epoch = axis.epoch; // Need to be after the call to `getUnit()`. + } + } + if (unit == null) { + unit = Units.DAY; + } + variable.setUnit(unit, epoch); + /* + * Prepares the conversion factors. The offset takes in account the change of epoch from Unix epoch + * to the target epoch of the variable, including timezone shift. The `year` field is relevant only + * if a climatological calendar is used, in which case we need a pseudo-year. + */ + final int year = climatological ? LocalDate.ofInstant(epoch, ZoneOffset.UTC).getYear() : 0; + double offset = epoch.toEpochMilli() + Constants.MILLIS_PER_SECOND * decoder.getTimeZone().getTotalSeconds(); + offset = Units.MILLISECOND.getConverterTo(unit).convert(offset); + final UnitConverter toUnit = unitOfLast.getConverterTo(unit); + /* + * Convert all dates to amounts of time since the epoch. This code takes in account change + * of epoch from the Unix epoch to the target epoch of the variable, including timezone. + */ + int[] fields = new int[fieldBases.length]; + final Vector values = variable.read(); + final var times = new double[values.size()]; + for (int i=0; i < times.length; i++) { + double value = values.doubleValue(i); // Date encoded as a double (e.g. 20181017) + long time = (long) value; // Intentional rounding toward zero. + value -= time; // Fractional part to be added at the end. + for (int j = fields.length; --j >= 0;) { + final int base = fieldBases[j]; + fields[j] = Math.abs((int) (time % base)); // Will apply the minus sign to the year field only. + time /= base; + } + /* + * After completion of the loop, `time` should be the year (potentially negative). + * Convert to a number of days since the epoch, then add hours, minutes and seconds. + * + * TODO: if we provide an API for returning `java.time` objects, then we should build + * instances of `Month` or `MonthDay` below in the case of climatological data. + */ + var date = LocalDate.of(Math.toIntExact(year + time), fields[0], fields[1]); + time = date.toEpochDay(); + for (int j=2; j < fields.length; j++) { + time *= (j == 2) ? 24 : 60; + time += fields[j]; + } + value += time - offset; + times[i] = toUnit.convert(value); + } + variable.setValues(times, true); + } +} diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java index dfb99b4786..e920decb07 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java @@ -592,6 +592,7 @@ public final class ChannelDecoder extends Decoder { if (allDimensions == null) { throw malformedHeader(); // May happen if readDimensions(…) has not been invoked. } + @SuppressWarnings("LocalVariableHidesMemberVariable") final VariableInfo[] variables = new VariableInfo[nelems]; for (int j=0; j<nelems; j++) { final String name = readName(); @@ -965,8 +966,7 @@ public final class ChannelDecoder extends Decoder { for (final VariableInfo variable : variables) { switch (variable.getRole()) { case COVERAGE: - case DISCRETE_COVERAGE: - { + case DISCRETE_COVERAGE: { // If Convention.roleOf(…) overwrote the value computed by VariableInfo, // remember the new value for avoiding to ask again in next loops. variable.isCoordinateSystemAxis = false; @@ -984,9 +984,9 @@ public final class ChannelDecoder extends Decoder { * For each variable, get its list of axes. More than one variable may have the same list of axes, * so we remember the previously created instances in order to share the grid geometry instances. */ - final Set<VariableInfo> axes = new LinkedHashSet<>(8); - final Set<DimensionInfo> usedDimensions = new HashSet<>(8); - final Map<GridInfo,GridInfo> shared = new LinkedHashMap<>(); + final var axes = new LinkedHashSet<VariableInfo>(8); + final var usedDimensions = new HashSet<DimensionInfo>(8); + final var shared = new LinkedHashMap<GridInfo,GridInfo>(); nextVar: for (final VariableInfo variable : variables) { if (variable.isCoordinateSystemAxis || variable.dimensions.length == 0) { continue; @@ -1072,7 +1072,7 @@ nextVar: for (final VariableInfo variable : variables) { */ @Override public String toString() { - final StringBuilder buffer = new StringBuilder(); + final var buffer = new StringBuilder(); buffer.append("SIS driver: “").append(getFilename()).append('”'); if (!input.channel.isOpen()) { buffer.append(" (closed)"); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/GridInfo.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/GridInfo.java index c0250c6d4e..1596467eac 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/GridInfo.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/GridInfo.java @@ -19,8 +19,8 @@ package org.apache.sis.storage.netcdf.classic; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.TreeMap; -import java.util.SortedMap; import ucar.nc2.constants.CF; // String constants are copied by the compiler with no UCAR reference left. import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.storage.DataStoreException; @@ -30,7 +30,6 @@ import org.apache.sis.storage.netcdf.base.Grid; import org.apache.sis.storage.netcdf.base.Decoder; import org.apache.sis.storage.netcdf.base.Dimension; import org.apache.sis.storage.netcdf.internal.Resources; -import org.apache.sis.util.ArraysExt; import org.apache.sis.util.privy.UnmodifiableArrayList; @@ -180,11 +179,12 @@ next: for (final String name : axisNames) { * This is often the reverse order of range indices, but not necessarily. The intent is to reduce the * number of disk seek operations. Data loading may happen in this method through Axis constructor. */ - final SortedMap<VariableInfo,Integer> variables = new TreeMap<>(); + final var variables = new TreeMap<VariableInfo,Integer>(); for (int i=0; i<range.length; i++) { final VariableInfo v = range[i]; if (variables.put(v, i) != null) { - throw new DataStoreContentException(Resources.format(Resources.Keys.DuplicatedAxis_2, getFilename(), v.getName())); + throw new DataStoreContentException(decoder.resources().getString( + Resources.Keys.DuplicatedAxis_2, getFilename(), v.getName())); } } /* @@ -192,13 +192,21 @@ next: for (final String name : axisNames) { * 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()) { + for (final Map.Entry<VariableInfo,Integer> entry : variables.entrySet()) { final int targetDim = entry.getValue(); final VariableInfo axis = entry.getKey(); /* * Get the grid dimensions (part of the "domain" in UCAR terminology) used for computing * the coordinate values along the current axis. There is exactly 1 such grid dimension in * straightforward netCDF files. However, some more complex files may have 2 dimensions. + * + * An axis may have two-dimensions but be conceptually one-dimensional if the coordinate values + * are given as character strings. While rare, this is sometime observed in practice for dates. + * In such case, the dimension used for indexing the characters in each value should not appear + * in the `domain` field of this grid. Therefore, that artificial dimension should be discarded + * by the loop below. This is okay because either the strings are automatically parsed as numbers + * by `org.apache.sis.math.ArrayVector.ASCII`, or either that variable have already been converted + * to a one-dimensional vector of numbers by `VariableTransformer`. */ int i = 0; final DimensionInfo[] axisDomain = axis.dimensions; @@ -213,8 +221,8 @@ next: for (final String name : axisNames) { } } } - axes[targetDim] = new Axis(AxisType.abbreviation(axis), axis.getAttributeAsString(CF.POSITIVE), - ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i), axis); + final char abbreviation = AxisType.abbreviation(axis); + axes[targetDim] = new Axis(abbreviation, axis.getAttributeAsString(CF.POSITIVE), indices, sizes, i, axis); } return axes; } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java index 5473325f93..3aa85d6251 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java @@ -62,7 +62,7 @@ import org.apache.sis.math.Vector; /** * Description of a variable found in a netCDF file. * The natural ordering of {@code VariableInfo} is the order in which the variables appear in the stream of bytes - * that make the netCDF file. Reading variables in natural order reduces the number of channel seek operations. + * that makes the netCDF file. Reading variables in natural order reduces the number of channel seek operations. * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) @@ -218,7 +218,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { offsetToNextRecord = Math.multiplyExact(offsetToNextRecord, dim.length()); } else if (i != 0) { // Unlimited dimension, if any, must be first in a netCDF 3 classic format. - throw new DataStoreContentException(getLocale(), Decoder.FORMAT_NAME, input.filename, null); + throw new DataStoreContentException(decoder.getLocale(), Decoder.FORMAT_NAME, input.filename, null); } } reader = new HyperRectangleReader(dataType.number, input); @@ -228,7 +228,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { } /* * If the value that we computed ourselves does not match the value declared in the netCDF file, - * maybe for some reason the writer used a different layout. For example, maybe it inserted some + * maybe for some reason the writer used a different layout. For example, maybe it inserted some * additional padding. */ if (size != -1) { // Maximal unsigned value, means possible overflow. @@ -249,21 +249,23 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { * as its dimension. But the "_CoordinateAxisType" attribute is often used for making explicit that a * variable is an axis. We check that case before to check variable name. */ - isCoordinateSystemAxis = (dimensions.length == 1 || dimensions.length == 2) && (getAxisType() != null); - /* - * If the "_CoordinateAliasForDimension" attribute is defined, then its value will be used - * instead of the variable name when determining if the variable is a coordinate system axis. - * "_CoordinateVariableAlias" seems to be a legacy attribute name for the same purpose. - */ - if (!isCoordinateSystemAxis && dimensions.length == 1) { - Object value = getAttributeValue(_Coordinate.AliasForDimension, "_coordinatealiasfordimension"); - if (value == null) { - value = getAttributeValue("_CoordinateVariableAlias", "_coordinatevariablealias"); + if (dimensions.length == 1 || dimensions.length == 2) { + isCoordinateSystemAxis = (getAxisType() != null); + if (!isCoordinateSystemAxis && (dimensions.length == 1 || dataType == DataType.CHAR)) { + /* + * If the "_CoordinateAliasForDimension" attribute is defined, then its value will be used + * instead of the variable name when determining if the variable is a coordinate system axis. + * "_CoordinateVariableAlias" seems to be a legacy attribute name for the same purpose. + */ + Object value = getAttributeValue(_Coordinate.AliasForDimension, "_coordinatealiasfordimension"); if (value == null) { - value = name; + value = getAttributeValue("_CoordinateVariableAlias", "_coordinatevariablealias"); + if (value == null) { + value = name; + } } + isCoordinateSystemAxis = dimensions[0].name.equals(value); } - isCoordinateSystemAxis = dimensions[0].name.equals(value); } /* * Rewrite the enumeration names as an array for avoiding to parse the string if this information @@ -736,7 +738,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { subsampling = new long[dimension]; Arrays.fill(subsampling, 1); } - final Region region = new Region(size, lower, upper, subsampling); + final var region = new Region(size, lower, upper, subsampling); /* * If this variable uses the unlimited dimension, we have to skip the records of all other unlimited variables * before to reach the next record of this variable. Current implementation can do that only if the number of @@ -835,7 +837,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { * Returns the error message for an unknown data type. */ private String unknownType() { - return resources().getString(Resources.Keys.UnsupportedDataType_3, getFilename(), name, dataType); + return decoder.resources().getString(Resources.Keys.UnsupportedDataType_3, getFilename(), name, dataType); } /** diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/GridWrapper.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/GridWrapper.java index 89e988064c..e893ac60e0 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/GridWrapper.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/GridWrapper.java @@ -304,8 +304,7 @@ next: for (final String name : axisNames) { */ } if (i != 0) { // Variables with 0 dimensions sometimes happen. - axes[axisCount++] = new Axis(abbreviation, axis.getPositive(), - ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i), wrapper); + axes[axisCount++] = new Axis(abbreviation, axis.getPositive(), indices, sizes, i, wrapper); } } return ArraysExt.resize(axes, axisCount); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/VariableWrapper.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/VariableWrapper.java index fba9001d16..ed90e30614 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/VariableWrapper.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/VariableWrapper.java @@ -369,6 +369,7 @@ final class VariableWrapper extends org.apache.sis.storage.netcdf.base.Variable /** * Returns the single value or vector of values for the given attribute, or {@code null} if none. * The returned value can be an instance of {@link String}, {@link Number}, {@link Vector} or {@code String[]}. + * The search is case-insensitive. * * @param attributeName the name of the attribute for which to get the values. * @return value(s) for the named attribute, or {@code null} if none.