You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2020/10/16 18:09:28 UTC
[sis] branch geoapi-4.0 updated: Share more code between
`FeatureSet` and `Grid` regarding axis type inference.
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 7a1fec6 Share more code between `FeatureSet` and `Grid` regarding axis type inference.
7a1fec6 is described below
commit 7a1fec6d1752af9a493bcb82f1f611a8363247d1
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Oct 16 20:08:13 2020 +0200
Share more code between `FeatureSet` and `Grid` regarding axis type inference.
---
.../apache/sis/feature/EnvelopeOperationTest.java | 2 +-
.../java/org/apache/sis/internal/util/Strings.java | 9 +-
.../org/apache/sis/internal/util/StringsTest.java | 8 +-
.../storage/earthobservation/LandsatReader.java | 2 +-
.../org/apache/sis/storage/geotiff/CRSBuilder.java | 2 +-
.../org/apache/sis/internal/netcdf/AxisType.java | 180 +++++++++++++++++
.../org/apache/sis/internal/netcdf/FeatureSet.java | 217 ++++++++++++---------
.../org/apache/sis/internal/netcdf/Resources.java | 10 +-
.../sis/internal/netcdf/Resources.properties | 2 +-
.../sis/internal/netcdf/Resources_fr.properties | 2 +-
.../org/apache/sis/internal/netcdf/Variable.java | 7 +
.../apache/sis/internal/netcdf/impl/GridInfo.java | 109 +----------
.../sis/internal/netcdf/impl/VariableInfo.java | 3 +-
.../sis/internal/netcdf/ucar/GridWrapper.java | 10 +-
.../sis/internal/netcdf/ucar/VariableWrapper.java | 17 ++
15 files changed, 363 insertions(+), 217 deletions(-)
diff --git a/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java b/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
index d58e018..a554a37 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
@@ -86,7 +86,7 @@ public final strictfp class EnvelopeOperationTest extends TestCase {
* if the corresponding {@code declareCRS} flag is true. The first geometry will be the default one.
*
* @param defaultCRS1 default CRS of first property (may be {@code null}).
- * @param defaultCRS1 default CRS of second property (may be {@code null}).
+ * @param defaultCRS2 default CRS of second property (may be {@code null}).
* @param asCharacteristic1 whether to declare CRS 1 as a characteristic of first property.
* @param asCharacteristic2 whether to declare CRS 2 as a characteristic of second property.
*/
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
index 1167d1f..2acad54 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
@@ -196,9 +196,11 @@ public final class Strings extends Static {
*
* @param text the text to filter.
* @param filter the filter to apply.
+ * @param all {@code true} for making all the string in upper-cases,
+ * or {@code false} for changing only the first character.
* @return the filtered text.
*/
- public static String toUpperCase(final String text, final Characters.Filter filter) {
+ public static String toUpperCase(final String text, final Characters.Filter filter, final boolean all) {
final int length = text.length();
int c, i = 0;
while (true) {
@@ -206,7 +208,8 @@ public final class Strings extends Static {
return text;
}
c = text.codePointAt(i);
- if (!filter.contains(c) || Character.toUpperCase(c) != c) {
+ if (!filter.contains(c)) break;
+ if ((i == 0 | all) && Character.toUpperCase(c) != c) {
break;
}
i += Character.charCount(c);
@@ -219,7 +222,7 @@ public final class Strings extends Static {
while (i < length) {
c = text.codePointAt(i);
if (filter.contains(c)) {
- buffer.appendCodePoint(Character.toUpperCase(c));
+ buffer.appendCodePoint((i == 0 | all) ? Character.toUpperCase(c) : c);
}
i += Character.charCount(c);
}
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
index 3b8e76f..96d12c6 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
@@ -41,13 +41,13 @@ public final strictfp class StringsTest extends TestCase {
}
/**
- * Tests the {@link Strings#toUpperCase(String, Characters.Filter)} method.
+ * Tests the {@link Strings#toUpperCase(String, Characters.Filter, boolean)} method.
*/
@Test
public void testToUpperCase() {
final String expected = "WGS84";
- assertSame (expected, Strings.toUpperCase(expected, Characters.Filter.LETTERS_AND_DIGITS));
- assertEquals(expected, Strings.toUpperCase("WGS 84", Characters.Filter.LETTERS_AND_DIGITS));
- assertEquals(expected, Strings.toUpperCase("wgs 84", Characters.Filter.LETTERS_AND_DIGITS));
+ assertSame (expected, Strings.toUpperCase(expected, Characters.Filter.LETTERS_AND_DIGITS, true));
+ assertEquals(expected, Strings.toUpperCase("WGS 84", Characters.Filter.LETTERS_AND_DIGITS, true));
+ assertEquals(expected, Strings.toUpperCase("wgs 84", Characters.Filter.LETTERS_AND_DIGITS, true));
}
}
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
index 0e50216..58983f1 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
@@ -748,7 +748,7 @@ final class LandsatReader extends MetadataBuilder {
* We ignore the "ELLIPSOID" attribute because it is implied by the datum.
*/
case "DATUM": {
- datum = CommonCRS.valueOf(Strings.toUpperCase(value, Characters.Filter.LETTERS_AND_DIGITS));
+ datum = CommonCRS.valueOf(Strings.toUpperCase(value, Characters.Filter.LETTERS_AND_DIGITS, true));
break;
}
/*
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
index 125df3d..82d1dd6 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
@@ -995,7 +995,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
final Ellipsoid ellipsoid = createEllipsoid(names, linearUnit);
final PrimeMeridian meridian = createPrimeMeridian(names, angularUnit);
final GeodeticDatum datum = getDatumFactory().createGeodeticDatum(properties(name), ellipsoid, meridian);
- name = Strings.toUpperCase(name, Characters.Filter.LETTERS_AND_DIGITS);
+ name = Strings.toUpperCase(name, Characters.Filter.LETTERS_AND_DIGITS, true);
lastName = datum.getName();
try {
final GeodeticDatum predefined = CommonCRS.valueOf(name).datum();
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/AxisType.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/AxisType.java
new file mode 100644
index 0000000..2333557
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/AxisType.java
@@ -0,0 +1,180 @@
+/*
+ * 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.internal.netcdf;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Locale;
+import org.opengis.referencing.cs.AxisDirection;
+import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.measure.Units;
+import javax.measure.Unit;
+import ucar.nc2.constants.CF;
+
+
+/**
+ * Type of coordinate system axis, in the order they should appears for a "normalized" coordinate reference system.
+ * The enumeration name matches the name of the {@code "axis"} attribute in CF-convention.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public enum AxisType {
+ /**
+ * X (usually longitude) coordinate axis.
+ */
+ X,
+
+ /**
+ * Y (usually latitude) coordinate axis.
+ */
+ Y,
+
+ /**
+ * Z (usually height) coordinate axis.
+ */
+ Z,
+
+ /**
+ * Time coordinate axis.
+ */
+ T;
+
+ /**
+ * Mapping from values of the {@code "_CoordinateAxisType"} attribute or axis name to the abbreviation.
+ * Keys are lower cases and values are controlled vocabulary documented in {@link Axis#abbreviation}.
+ *
+ * <div class="note">"GeoX" and "GeoY" stands for projected coordinates, not geocentric coordinates
+ * (<a href="https://www.unidata.ucar.edu/software/thredds/current/netcdf-java/reference/CoordinateAttributes.html#AxisTypes">source</a>).
+ * </div>
+ *
+ * @see abbreviation(String)
+ */
+ private static final Map<String,Character> TYPES = new HashMap<>(26);
+
+ /**
+ * The enumeration values for given abbreviations.
+ */
+ private static final Map<Character,AxisType> VALUES = new HashMap<>(13);
+ static {
+ addAxisTypes(X, 'λ', "longitude", "lon", "long");
+ addAxisTypes(Y, 'φ', "latitude", "lat");
+ addAxisTypes(Z, 'H', "pressure", "height", "altitude", "barometric_altitude", "elevation", "elev", "geoz");
+ addAxisTypes(Z, 'D', "depth", "depth_below_geoid");
+ addAxisTypes(X, 'E', "geox", "projection_x_coordinate");
+ addAxisTypes(Y, 'N', "geoy", "projection_y_coordinate");
+ addAxisTypes(T, 't', "t", "time", "runtime");
+ addAxisTypes(X, 'x', "x");
+ addAxisTypes(Y, 'y', "y");
+ addAxisTypes(Z, 'z', "z");
+ }
+
+ /**
+ * Adds a sequence of axis types or variable names for the given abbreviation.
+ */
+ private static void addAxisTypes(final AxisType value, final char abbreviation, final String... names) {
+ final Character c = abbreviation;
+ for (final String name : names) {
+ TYPES.put(name, c);
+ }
+ VALUES.put(c, value);
+ }
+
+ /**
+ * Returns the axis type (identified by its abbreviation) for an axis of the given name, or null if unknown.
+ * The returned code is one of the controlled vocabulary documented in {@link Axis#abbreviation}.
+ *
+ * @param type the {@code "_CoordinateAxisType"} attribute value or another description used as fallback.
+ * @return axis abbreviation for the given type or name, or {@code null} if none.
+ */
+ private static Character abbreviation(final String type) {
+ return (type != null) ? TYPES.get(type.toLowerCase(Locale.US)) : null;
+ }
+
+ /**
+ * Returns the axis type (identified by its abbreviation) for the given axis, or 0 if unknown.
+ * The returned code is one of the controlled vocabulary documented in {@link Axis#abbreviation}.
+ *
+ * @param axis axis for which to get an abbreviation.
+ * @return abbreviation for the given axis, or 0 if none.
+ */
+ public static char abbreviation(final Variable axis) {
+ /*
+ * In Apache SIS implementation, the abbreviation determines the axis type. If a "_CoordinateAxisType" attribute
+ * exists, il will have precedence over all other heuristic rules in this method because it is the most specific
+ * information about axis type. Otherwise the "standard_name" attribute is our first fallback since valid values
+ * are standardized to "longitude" and "latitude" among others.
+ */
+ Character abbreviation = abbreviation(axis.getAxisType());
+ if (abbreviation == null) {
+ abbreviation = abbreviation(axis.getAttributeAsString(CF.STANDARD_NAME)); // No fallback on variable name.
+ /*
+ * If the abbreviation is still unknown, look at the "long_name", "description" or "title" attribute. Those
+ * attributes are not standardized, so they are less reliable than "standard_name". But they are still more
+ * reliable that the variable name since the long name may be "Longitude" or "Latitude" while the variable
+ * name is only "x" or "y".
+ */
+ if (abbreviation == null) {
+ abbreviation = abbreviation(axis.getDescription());
+ if (abbreviation == null) {
+ /*
+ * Actually the "degree_east" and "degree_north" units of measurement are the most reliable way to
+ * identify geographic system, but we nevertheless check them almost last because the direction is
+ * already verified by Axis constructor. By checking the variable attributes first, we give a chance
+ * to Axis constructor to report a warning if there is an inconsistency.
+ */
+ if (Units.isAngular(axis.getUnit())) {
+ final AxisDirection direction = AxisDirections.absolute(Axis.direction(axis.getUnitsString()));
+ if (AxisDirection.EAST.equals(direction)) {
+ return 'λ';
+ } else if (AxisDirection.NORTH.equals(direction)) {
+ return 'φ';
+ }
+ }
+ /*
+ * We test the variable name last because that name is more at risk of being an uninformative "x" or "y" name.
+ * If even the variable name is not sufficient, we use some easy to recognize units.
+ */
+ if (abbreviation == null) {
+ abbreviation = abbreviation(axis.getName());
+ if (abbreviation == null) {
+ final Unit<?> unit = axis.getUnit();
+ if (Units.isTemporal(unit)) {
+ return 't';
+ } else if (Units.isPressure(unit)) {
+ return 'z';
+ } else {
+ return 0;
+ }
+ }
+ }
+ }
+ }
+ }
+ return abbreviation;
+ }
+
+ /**
+ * Returns the enumeration value for the given variable, or {@code null} if none.
+ */
+ static AxisType valueOf(final Variable axis) {
+ final char abbreviation = abbreviation(axis);
+ return (abbreviation != 0) ? VALUES.get(abbreviation) : null;
+ }
+}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
index aa68567..c076d2c 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
@@ -20,6 +20,8 @@ import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Spliterator;
import java.util.stream.Stream;
@@ -31,11 +33,13 @@ import org.opengis.metadata.acquisition.GeometryType;
import org.apache.sis.math.Vector;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.internal.feature.MovingFeatures;
+import org.apache.sis.internal.util.Strings;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.feature.builder.AttributeRole;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.feature.builder.AttributeTypeBuilder;
import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.Characters;
import ucar.nc2.constants.CF;
// Branch-dependent imports
@@ -55,7 +59,8 @@ import org.opengis.feature.FeatureType;
*/
final class FeatureSet extends DiscreteSampling {
/**
- * Kind of features supported by this class. Also used as property name.
+ * Value of {@code "featureType"} global attribute for netCDF files that this class can handle.
+ * Also used as property name for the geometry object.
*/
static final String TRAJECTORY = "trajectory";
@@ -108,7 +113,12 @@ final class FeatureSet extends DiscreteSampling {
* Creates a new discrete sampling parser for features identified by the given variable.
* All arrays given to this method are stored by direct reference (they are not cloned).
*
+ * <p>The {@code name} argument can be anything. A not-too-bad choice (when nothing better is available)
+ * is the name of the first dimension of {@code coordinates} and {@code properties} variables. All those
+ * variables should have that first dimension in common, because {@code create(…)} uses that criterion.</p>
+ *
* @param decoder the source of the features to create.
+ * @param name name to give to the feature type.
* @param counts the count of instances per feature, or {@code null} if none.
* @param identifiers the feature identifiers, possibly with other singleton properties.
* @param hasTime whether the {@code coordinates} array contains a temporal variable.
@@ -116,8 +126,8 @@ final class FeatureSet extends DiscreteSampling {
* @param properties the variables that contain custom time-varying properties.
* @throws IllegalArgumentException if the given library is non-null but not available.
*/
- private FeatureSet(final Decoder decoder, final Vector counts, final Variable[] identifiers,
- final boolean hasTime, final Variable[] coordinates, final Variable[] properties)
+ private FeatureSet(final Decoder decoder, String name, final Vector counts, final Variable[] identifiers,
+ final boolean hasTime, final Variable[] coordinates, final Variable[] properties)
{
super(decoder.geomlib, decoder.listeners);
this.counts = counts;
@@ -153,11 +163,18 @@ final class FeatureSet extends DiscreteSampling {
final Class<?> type = v.isEnumeration() ? String.class : Number.class;
describe(v, builder.addAttribute(type).setMaximumOccurs(Integer.MAX_VALUE), hasTime);
}
- type = builder.setName("Features").build(); // TODO: get a better name.
+ /*
+ * By default, `name` is a netCDF dimension name (see method javadoc), usually all lower-cases.
+ * Make the first letter upper-case for consistency with SIS convention used for feature types.
+ */
+ name = Strings.toUpperCase(name, Characters.Filter.UNICODE_IDENTIFIER, false);
+ type = builder.setName(name).build();
}
/**
* Sets the attribute name, and potentially its definition, from the given variable.
+ * If the variable has a {@code "cf_role"} attribute set {@code "trajectory_id"},
+ * then the attribute will also be declared as an identifier.
*
* @param variable the variable from which to get metadata.
* @param attribute the attribute to configure with variable metadata.
@@ -173,19 +190,15 @@ final class FeatureSet extends DiscreteSampling {
if (hasTime) {
attribute.addCharacteristic(MovingFeatures.TIME);
}
- }
-
- /**
- * Returns {@code true} if the given attribute value is one of the {@code cf_role} attribute values
- * supported by this implementation.
- */
- private static boolean hasSupportedRole(final Variable variable) {
- return CF.TRAJECTORY_ID.equalsIgnoreCase(variable.getAttributeAsString(CF.CF_ROLE));
+ if (CF.TRAJECTORY_ID.equalsIgnoreCase(variable.getAttributeAsString(CF.CF_ROLE))) {
+ attribute.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+ }
}
/**
* Creates new discrete sampling parsers from the attribute values found in the given decoder.
*
+ * @param decoder the source of the features to create.
* @throws IllegalArgumentException if the geometric object library is not available.
* @throws ArithmeticException if the size of a variable exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
*/
@@ -196,7 +209,7 @@ search: for (final Variable counts : decoder.getVariables()) {
* Any one-dimensional integer variable having a "sample_dimension" attribute string value
* will be taken as an indication that we have Discrete Sampling Geometries. That variable
* shall be counting the number of feature instances, and another variable having the same
- * dimension (optionally plus a character dimension) shall give the feature identifiers.
+ * dimension (optionally plus a character dimension) should give the feature identifiers.
* Example:
*
* dimensions:
@@ -208,90 +221,112 @@ search: for (final Variable counts : decoder.getVariables()) {
* int counts(identifiers);
* counts:sample_dimension = "points";
*/
- if (counts.getNumDimensions() != 1 || !counts.getDataType().isInteger) {
- continue;
- }
- final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
- if (sampleDimName == null) {
- continue;
+ if (counts.getNumDimensions() == 1 && counts.getDataType().isInteger) {
+ final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
+ if (sampleDimName != null) {
+ final Dimension sampleDimension = decoder.findDimension(sampleDimName);
+ if (sampleDimension != null) {
+ addFeatureSet(features, decoder, counts, counts.getGridDimensions().get(0), sampleDimension);
+ } else {
+ decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DimensionNotFound_3,
+ decoder.getFilename(), counts.getName(), sampleDimName));
+ }
+ }
}
- final Dimension featureDimension = counts.getGridDimensions().get(0);
- final Dimension sampleDimension = decoder.findDimension(sampleDimName);
- if (sampleDimension == null) {
- decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DimensionNotFound_3,
- decoder.getFilename(), counts.getName(), sampleDimName));
+ }
+ return features.toArray(new FeatureSet[features.size()]);
+ }
+
+ /**
+ * Searches all variables having the expected feature dimension or sample dimension.
+ * Those variable contains the actual data. For example if the sample dimension name
+ * is "points", then we may have:
+ *
+ * {@preformat text
+ * double longitude(points);
+ * longitude:axis = "X";
+ * longitude:standard_name = "longitude";
+ * longitude:units = "degrees_east";
+ * double latitude(points);
+ * latitude:axis = "Y";
+ * latitude:standard_name = "latitude";
+ * latitude:units = "degrees_north";
+ * double time(points);
+ * time:axis = "T";
+ * time:standard_name = "time";
+ * time:units = "minutes since 2014-11-29 00:00:00";
+ * short myCustomProperty(points);
+ * }
+ *
+ * @param features where to add the {@code FeatureSet} instance.
+ * @param decoder the source of the features to create.
+ * @param counts the count of instances per feature, or {@code null} if none.
+ * @param featureDimension dimension of properties having a single value per feature instance.
+ * @param sampleDimension dimension of properties having multiple values per feature instance.
+ * @return whether a {@code FeatureSet} has been added to the {@code features} collection.
+ */
+ private static boolean addFeatureSet(final List<FeatureSet> features, final Decoder decoder, final Variable counts,
+ final Dimension featureDimension, final Dimension sampleDimension) throws IOException, DataStoreException
+ {
+ final String featureName = featureDimension.getName();
+ final boolean isPointSet = sampleDimension.equals(featureDimension);
+ final List<Variable> singletons = isPointSet ? Collections.emptyList() : new ArrayList<>();
+ final List<Variable> properties = new ArrayList<>();
+ final Map<AxisType,Variable> coordinates = new LinkedHashMap<>();
+ for (final Variable data : decoder.getVariables()) {
+ if (data.equals(counts)) {
continue;
}
/*
- * We should have another variable of the same name than the feature dimension name
- * ("identifiers" in above example). That variable should have a "cf_role" attribute
- * set to one of the values known to current implementation. If we do not find such
- * variable, search among other variables before to give up. That second search is not
- * part of CF convention and will be accepted only if there is no ambiguity.
+ * We should have another variable of the same name than the feature dimension name.
+ * In SIS implementation, this variable is optional. But if present, it should have
+ * the expected dimension. According CF convention that variable should also have a
+ * "cf_role" attribute set to "trajectory_id", but this is not required by SIS.
*/
- Variable identifiers = decoder.findVariable(featureDimension.getName());
- if (identifiers != null && hasSupportedRole(identifiers)) {
- if (!isScalarOrString(identifiers, featureDimension, decoder)) {
- continue;
- }
- } else {
- identifiers = null;
- for (final Variable alt : decoder.getVariables()) {
- if (hasSupportedRole(alt) && isScalarOrString(alt, featureDimension, null)) {
- if (identifiers != null) {
- identifiers = null;
- break; // Ambiguity found: consider that we found no replacement.
- }
- identifiers = alt;
- }
+ if (featureName.equalsIgnoreCase(data.getName())) {
+ if (isScalarOrString(data, featureDimension, decoder)) {
+ (isPointSet ? properties : singletons).add(data);
}
- if (identifiers == null) {
- decoder.listeners.warning(decoder.resources().getString(Resources.Keys.VariableNotFound_2,
- decoder.getFilename(), featureDimension.getName()));
- continue;
- }
- }
- /*
- * At this point, all information have been verified as valid. Now search all variables having
- * the expected sample dimension. Those variable contains the actual data. For example if the
- * sample dimension name is "points", then we may have:
- *
- * double longitude(points);
- * longitude:axis = "X";
- * longitude:standard_name = "longitude";
- * longitude:units = "degrees_east";
- * double latitude(points);
- * latitude:axis = "Y";
- * latitude:standard_name = "latitude";
- * latitude:units = "degrees_north";
- * double time(points);
- * time:axis = "T";
- * time:standard_name = "time";
- * time:units = "minutes since 2014-11-29 00:00:00";
- * short myCustomProperty(points);
- */
- final Map<String,Variable> coordinates = new LinkedHashMap<>();
- final List<Variable> properties = new ArrayList<>();
- for (final Variable data : decoder.getVariables()) {
- if (isScalarOrString(data, sampleDimension, null)) {
- final String axisType = data.getAttributeAsString(CF.AXIS);
- if (axisType == null) {
- properties.add(data);
- } else if (coordinates.put(axisType, data) != null) {
- continue search; // Two axes of the same type: abort.
+ } else if (!isPointSet && isScalarOrString(data, featureDimension, null)) {
+ /*
+ * Feature property other than identifiers. Should rarely happen.
+ */
+ singletons.add(data);
+ } else if (isScalarOrString(data, sampleDimension, null)) {
+ /*
+ * All other sample property (i.e. property having a value for each temporal value).
+ * If `isPointSet` is true, then sample properties are actually feature properties.
+ */
+ final AxisType axisType = AxisType.valueOf(data);
+ if (axisType != null) {
+ final Variable previous = coordinates.putIfAbsent(axisType, data);
+ if (previous != null) {
+ decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DuplicatedAxisType_4,
+ decoder.getFilename(), axisType, previous.getName(), data.getName()));
+ // TODO: give precedence to which axis?
}
+ } else {
+ properties.add(data);
}
}
- final Variable time = coordinates.remove("T");
- if (time != null) {
- coordinates.put("T", time); // Make sure that time is last.
- features.add(new FeatureSet(decoder, counts.read(),
- new Variable[] {identifiers}, true,
- coordinates.values().toArray(new Variable[coordinates.size()]),
- properties.toArray(new Variable[properties.size()])));
- }
}
- return features.toArray(new FeatureSet[features.size()]);
+ final Variable time = coordinates.remove(AxisType.T);
+ if (time != null) {
+ coordinates.put(AxisType.T, time); // Make sure that time is last.
+ }
+ return features.add(new FeatureSet(decoder, featureName,
+ (counts != null) ? counts.read() : null,
+ toArray(singletons), time != null,
+ toArray(coordinates.values()),
+ toArray(properties)));
+
+ }
+
+ /**
+ * Returns the content of given collection as an array.
+ */
+ private static Variable[] toArray(final Collection<Variable> variables) {
+ return variables.toArray(new Variable[variables.size()]);
}
/**
@@ -305,9 +340,9 @@ search: for (final Variable counts : decoder.getVariables()) {
*/
@SuppressWarnings("fallthrough")
private static boolean isScalarOrString(final Variable data, final Dimension featureDimension, final Decoder decoder) {
- final List<Dimension> dimensions = data.getGridDimensions();
+ List<Dimension> dimensions = null;
final int unexpectedDimension;
- switch (dimensions.size()) {
+ switch (data.getNumDimensions()) {
default: { // Too many dimensions
unexpectedDimension = 2;
break;
@@ -320,6 +355,7 @@ search: for (final Variable counts : decoder.getVariables()) {
// Fall through for checking the first dimension.
}
case 1: {
+ dimensions = data.getGridDimensions();
if (featureDimension.equals(dimensions.get(0))) {
return true;
}
@@ -331,6 +367,9 @@ search: for (final Variable counts : decoder.getVariables()) {
}
}
if (decoder != null) {
+ if (dimensions == null) {
+ dimensions = data.getGridDimensions();
+ }
decoder.listeners.warning(decoder.resources().getString(
Resources.Keys.UnexpectedDimensionForVariable_4,
decoder.getFilename(), data.getName(),
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
index f6acc63..9f9c4ee 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
@@ -116,6 +116,11 @@ public final class Resources extends IndexedResourceBundle {
public static final short DimensionNotFound_3 = 1;
/**
+ * Axes “{2}” and “{3}” have the same type “{1}” in netCDF file “{0}”.
+ */
+ public static final short DuplicatedAxisType_4 = 3;
+
+ /**
* Duplicated axis “{1}” in a grid of netCDF file “{0}”.
*/
public static final short DuplicatedAxis_2 = 7;
@@ -179,11 +184,6 @@ public final class Resources extends IndexedResourceBundle {
* NetCDF file “{0}” uses unsupported data type {2} for variable “{1}”.
*/
public static final short UnsupportedDataType_3 = 5;
-
- /**
- * Variable “{1}” is not found in the “{0}” file.
- */
- public static final short VariableNotFound_2 = 3;
}
/**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
index 62b864e..e6a6457 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
@@ -31,6 +31,7 @@ CanNotUseUCAR = Can not use UCAR library for netCDF format.
ComputeLocalizationGrid_2 = Computed localization grid for \u201c{0}\u201d in {1} seconds.
DimensionNotFound_3 = Dimension \u201c{2}\u201d declared by attribute \u201c{1}\u201d is not found in the \u201c{0}\u201d file.
DuplicatedAxis_2 = Duplicated axis \u201c{1}\u201d in a grid of netCDF file \u201c{0}\u201d.
+DuplicatedAxisType_4 = Axes \u201c{2}\u201d and \u201c{3}\u201d have the same type \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
IllegalAttributeValue_3 = Illegal value \u201c{2}\u201d for attribute \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
IllegalValueRange_4 = Illegal value range {2,number} \u2026 {3,number} for variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
MismatchedAttributeLength_5 = Attributes \u201c{1}\u201d and \u201c{2}\u201d on variable \u201c{0}\u201d have different lengths: {3} and {4} respectively.
@@ -42,4 +43,3 @@ UnexpectedAxisCount_4 = Reference system of type \u2018{1}\u2019 can
UnexpectedDimensionForVariable_4 = Variable \u201c{1}\u201d in file \u201c{0}\u201d has a dimension \u201c{3}\u201d while we expected \u201c{2}\u201d.
UnmappedDimensions_4 = Variable \u201c{1}\u201d in file \u201c{0}\u201d has {2,number} dimensions but only {3,number} can be associated to a coordinate reference system.
UnsupportedDataType_3 = NetCDF file \u201c{0}\u201d uses unsupported data type {2} for variable \u201c{1}\u201d.
-VariableNotFound_2 = Variable \u201c{1}\u201d is not found in the \u201c{0}\u201d file.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
index e9dd81c..624d10e 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
@@ -36,6 +36,7 @@ CanNotUseUCAR = Ne peut pas utiliser la biblioth\u00e8que de
ComputeLocalizationGrid_2 = Grille de localisation de \u00ab\u202f{0}\u202f\u00bb calcul\u00e9e en {1} secondes.
DimensionNotFound_3 = La dimension \u00ab\u202f{2}\u202f\u00bb d\u00e9clar\u00e9e par l\u2019attribut \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans le fichier \u00ab\u202f{0}\u202f\u00bb.
DuplicatedAxis_2 = Axe \u00ab\u202f{1}\u202f\u00bb dupliqu\u00e9 dans une grille du fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
+DuplicatedAxisType_4 = Les axes \u00ab\u202f{2}\u202f\u00bb et \u00ab\u202f{3}\u202f\u00bb d\u00e9clarent le m\u00eame type \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
IllegalAttributeValue_3 = La valeur \u00ab\u202f{2}\u202f\u00bb est ill\u00e9gale pour l\u2019attribut \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
IllegalValueRange_4 = Plage de valeurs {2,number} \u2026 {3,number} ill\u00e9gale pour la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
MismatchedAttributeLength_5 = Les attributs \u201c{1}\u201d et \u201c{2}\u201d de la variable \u201c{0}\u201d ont des longueurs diff\u00e9rentes\u00a0: {3} et {4} respectivement.
@@ -47,4 +48,3 @@ UnexpectedAxisCount_4 = Les syst\u00e8mes de r\u00e9f\u00e9rence de
UnexpectedDimensionForVariable_4 = La variable \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb a une dimension \u00ab\u202f{3}\u202f\u00bb alors qu\u2019on attendait \u00ab\u202f{2}\u202f\u00bb.
UnmappedDimensions_4 = La variable \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb a {2,number} dimensions mais seulement {3,number} peuvent \u00eatre associ\u00e9es \u00e0 un syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es.
UnsupportedDataType_3 = Le fichier netCDF \u00ab\u202f{0}\u202f\u00bb utilise un type de donn\u00e9es non-support\u00e9 {2} pour la variable \u00ab\u202f{1}\u202f\u00bb.
-VariableNotFound_2 = La variable \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans le fichier \u00ab\u202f{0}\u202f\u00bb.
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 6285bd9..8f19cae 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
@@ -440,6 +440,13 @@ public abstract class Variable extends Node {
protected abstract boolean isCoordinateSystemAxis();
/**
+ * Returns the value of {@code "_CoordinateAxisType"} attribute.
+ *
+ * @return Value of {@code "_CoordinateAxisType"} attribute, or {@code null} if none.
+ */
+ protected abstract String getAxisType();
+
+ /**
* Returns a builder for the grid geometry of this variable, or {@code null} if this variable is not a data cube.
* Not all variables have a grid geometry. For example collections of features do not have such grid.
* This method should be invoked only once per variable, but the same builder may be returned by different variables.
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 e62ddbf..76ca226 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
@@ -19,23 +19,17 @@ package org.apache.sis.internal.netcdf.impl;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.HashMap;
import java.util.TreeMap;
import java.util.SortedMap;
-import javax.measure.Unit;
-import org.opengis.referencing.cs.AxisDirection;
import org.apache.sis.internal.netcdf.Axis;
+import org.apache.sis.internal.netcdf.AxisType;
import org.apache.sis.internal.netcdf.Grid;
import org.apache.sis.internal.netcdf.Decoder;
import org.apache.sis.internal.netcdf.Dimension;
import org.apache.sis.internal.netcdf.Resources;
-import org.apache.sis.internal.referencing.AxisDirections;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.measure.Units;
import org.apache.sis.util.ArraysExt;
import ucar.nc2.constants.CF;
@@ -54,40 +48,6 @@ import ucar.nc2.constants.CF;
*/
final class GridInfo extends Grid {
/**
- * Mapping from values of the {@code "_CoordinateAxisType"} attribute or axis name to the abbreviation.
- * Keys are lower cases and values are controlled vocabulary documented in {@link Axis#abbreviation}.
- *
- * <div class="note">"GeoX" and "GeoY" stands for projected coordinates, not geocentric coordinates
- * (<a href="https://www.unidata.ucar.edu/software/thredds/current/netcdf-java/reference/CoordinateAttributes.html#AxisTypes">source</a>).
- * </div>
- *
- * @see #getAxisType(String)
- */
- private static final Map<String,Character> AXIS_TYPES = new HashMap<>(26);
- static {
- addAxisTypes('λ', "longitude", "lon", "long");
- addAxisTypes('φ', "latitude", "lat");
- addAxisTypes('H', "pressure", "height", "altitude", "barometric_altitude", "elevation", "elev", "geoz");
- addAxisTypes('D', "depth", "depth_below_geoid");
- addAxisTypes('E', "geox", "projection_x_coordinate");
- addAxisTypes('N', "geoy", "projection_y_coordinate");
- addAxisTypes('t', "t", "time", "runtime");
- addAxisTypes('x', "x");
- addAxisTypes('y', "y");
- addAxisTypes('z', "z");
- }
-
- /**
- * Adds a sequence of axis types or variable names for the given abbreviation.
- */
- private static void addAxisTypes(final char abbreviation, final String... names) {
- final Character c = abbreviation;
- for (final String name : names) {
- AXIS_TYPES.put(name, c);
- }
- }
-
- /**
* Describes the input values expected by the function converting grid indices to geodetic coordinates.
* They are the dimensions of the grid (<strong>not</strong> the dimensions of the CRS).
* Dimensions are listed in the order they appear in netCDF file (reverse of "natural" order).
@@ -152,21 +112,6 @@ final class GridInfo extends Grid {
}
/**
- * Returns the axis type for an axis of the given name, or 0 if unknown.
- * If non-zero, then the returned code is one of the controlled vocabulary
- * documented in {@link Axis#abbreviation}.
- */
- private static char getAxisType(final String name) {
- if (name != null) {
- final Character abbreviation = AXIS_TYPES.get(name.toLowerCase(Locale.US));
- if (abbreviation != null) {
- return abbreviation;
- }
- }
- return 0;
- }
-
- /**
* Returns the number of dimensions of source coordinates in the <cite>"grid to CRS"</cite> conversion.
* This is the number of dimensions of the <em>grid</em>.
*/
@@ -255,56 +200,6 @@ next: for (final String name : axisNames) {
final int targetDim = entry.getValue();
final VariableInfo axis = entry.getKey();
/*
- * In Apache SIS implementation, the abbreviation determines the axis type. If a "_coordinateaxistype" attribute
- * exists, il will have precedence over all other heuristic rules in this method because it is the most specific
- * information about axis type. Otherwise the "standard_name" attribute is our first fallback since valid values
- * are standardized to "longitude" and "latitude" among others.
- */
- char abbreviation = getAxisType(axis.getAxisType());
- if (abbreviation == 0) {
- abbreviation = getAxisType(axis.getAttributeAsString(CF.STANDARD_NAME)); // No fallback on variable name.
- /*
- * If the abbreviation is still unknown, look at the "long_name", "description" or "title" attribute. Those
- * attributes are not standardized, so they are less reliable than "standard_name". But they are still more
- * reliable that the variable name since the long name may be "Longitude" or "Latitude" while the variable
- * name is only "x" or "y".
- */
- if (abbreviation == 0) {
- abbreviation = getAxisType(axis.getDescription());
- if (abbreviation == 0) {
- /*
- * Actually the "degree_east" and "degree_north" units of measurement are the most reliable way to
- * identify geographic system, but we nevertheless check them almost last because the direction is
- * already verified by Axis constructor. By checking the variable attributes first, we give a chance
- * to Axis constructor to report a warning if there is an inconsistency.
- */
- if (Units.isAngular(axis.getUnit())) {
- final AxisDirection direction = AxisDirections.absolute(Axis.direction(axis.getUnitsString()));
- if (AxisDirection.EAST.equals(direction)) {
- abbreviation = 'λ';
- } else if (AxisDirection.NORTH.equals(direction)) {
- abbreviation = 'φ';
- }
- }
- /*
- * We test the variable name last because that name is more at risk of being an uninformative "x" or "y" name.
- * If even the variable name is not sufficient, we use some easy to recognize units.
- */
- if (abbreviation == 0) {
- abbreviation = getAxisType(axis.getName());
- if (abbreviation == 0) {
- final Unit<?> unit = axis.getUnit();
- if (Units.isTemporal(unit)) {
- abbreviation = 't';
- } else if (Units.isPressure(unit)) {
- abbreviation = 'z';
- }
- }
- }
- }
- }
- }
- /*
* 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.
@@ -322,7 +217,7 @@ next: for (final String name : axisNames) {
}
}
}
- axes[targetDim] = new Axis(abbreviation, axis.getAttributeAsString(CF.POSITIVE),
+ axes[targetDim] = new Axis(AxisType.abbreviation(axis), axis.getAttributeAsString(CF.POSITIVE),
ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i), axis);
}
return axes;
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 4f3f8c8..aab54bd 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
@@ -430,7 +430,8 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
/**
* Returns the value of the {@code "_CoordinateAxisType"} attribute, or {@code null} if none.
*/
- final String getAxisType() {
+ @Override
+ protected String getAxisType() {
final Object value = getAttributeValue(_Coordinate.AxisType, "_coordinateaxistype");
return (value instanceof String) ? (String) value : null;
}
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 b8b6df1..bde4422 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
@@ -30,6 +30,7 @@ import ucar.nc2.dataset.CoordinateSystem;
import org.apache.sis.internal.netcdf.Axis;
import org.apache.sis.internal.netcdf.Grid;
import org.apache.sis.internal.netcdf.Decoder;
+import org.apache.sis.internal.netcdf.Variable;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.ArraysExt;
@@ -42,7 +43,7 @@ import org.apache.sis.util.ArraysExt;
* of the grid geometry information.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 0.3
* @module
*/
@@ -234,6 +235,7 @@ next: for (final String name : axisNames) {
final Axis[] axes = new Axis[targetDim];
while (--targetDim >= 0) {
final CoordinateAxis axis = range.get(targetDim);
+ final Variable wrapper = ((DecoderWrapper) decoder).getWrapperFor(axis);
/*
* The AttributeNames are for ISO 19115 metadata. They are not used for locating grid cells
* on Earth, but we nevertheless get them now for making MetadataReader work easier.
@@ -254,6 +256,9 @@ next: for (final String name : axisNames) {
case RadialElevation: abbreviation = 'Ω'; break; // Spherical latitude
case RadialDistance: abbreviation = 'r'; break; // Geocentric radius
}
+ if (abbreviation == 0) {
+ abbreviation = org.apache.sis.internal.netcdf.AxisType.abbreviation(wrapper);
+ }
/*
* 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
@@ -276,8 +281,7 @@ next: for (final String name : axisNames) {
*/
}
axes[targetDim] = new Axis(abbreviation, axis.getPositive(),
- ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i),
- ((DecoderWrapper) decoder).getWrapperFor(axis));
+ ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i), wrapper);
}
/*
* We want axes in "natural" order. But the netCDF UCAR library sometime provides axes already
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 e447373..95ee819 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
@@ -29,12 +29,14 @@ import ucar.nc2.Attribute;
import ucar.nc2.VariableIF;
import ucar.nc2.dataset.Enhancements;
import ucar.nc2.dataset.VariableEnhanced;
+import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.CoordinateAxis2D;
import ucar.nc2.dataset.CoordinateSystem;
import ucar.nc2.dataset.EnhanceScaleMissing;
import ucar.nc2.units.SimpleUnit;
import ucar.nc2.units.DateUnit;
+import ucar.nc2.constants._Coordinate;
import org.opengis.referencing.operation.Matrix;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.math.Vector;
@@ -50,6 +52,7 @@ import org.apache.sis.storage.netcdf.AttributeNames;
import org.apache.sis.measure.MeasurementRange;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.measure.Units;
+import ucar.nc2.constants.AxisType;
/**
@@ -255,6 +258,20 @@ final class VariableWrapper extends Variable {
}
/**
+ * Returns the value of the {@code "_CoordinateAxisType"} attribute, or {@code null} if none.
+ */
+ @Override
+ protected String getAxisType() {
+ if (variable instanceof CoordinateAxis) {
+ final AxisType type = ((CoordinateAxis) variable).getAxisType();
+ if (type != null) {
+ return type.name();
+ }
+ }
+ return getAttributeAsString(_Coordinate.AxisType);
+ }
+
+ /**
* 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