blob: a437ffd0f2a58bdfbeddc6619971ee5dad110521 [file] [log] [blame]
/*
* 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.util.Map;
import java.util.List;
import java.util.Date;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.StringJoiner;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.io.IOException;
import java.time.Instant;
import javax.measure.Unit;
import org.opengis.util.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.GeodeticCRS;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.Conversion;
import org.opengis.referencing.operation.Matrix;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.privy.EllipsoidalHeightCombiner;
import org.apache.sis.referencing.cs.AbstractCS;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.referencing.crs.AbstractCRS;
import org.apache.sis.referencing.crs.DefaultGeographicCRS;
import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
import org.apache.sis.referencing.operation.provider.Equirectangular;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.netcdf.internal.Resources;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.measure.Units;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.referencing.operation.CoordinateOperationFactory;
/**
* Temporary object for building a coordinate reference system from the variables in a netCDF file.
* This class proceeds by inspecting the coordinate system axes. This is a different approach than
* {@link GridMapping}, which parses Well Known Text or EPSG codes declared in variable attributes.
*
* <p>Different instances are required for the geographic, vertical and temporal components of a CRS,
* or if a netCDF file uses different CRS for different variables. This builder is used as below:</p>
*
* <ol>
* <li>Invoke {@link #dispatch(List, Axis)} for all axes in a grid.
* Builders for CRS components will added in the given list.</li>
* <li>Invoke {@link #build(Decoder, boolean)} on each builder prepared in above step.</li>
* <li>Assemble the CRS components created in above step in a {@code CompoundCRS}.</li>
* </ol>
*
* The builder type is inferred from axes. The axes are identified by their abbreviations,
* which is a {@linkplain Axis#abbreviation controlled vocabulary} for this implementation.
*
* <h2>Exception handling</h2>
* {@link FactoryException} is handled as a warning by {@linkplain the caller Grid#getCoordinateReferenceSystem},
* while {@link DataStoreException} is handled as a fatal error. Warnings are stored in {@link #warnings} field.
*
* @author Martin Desruisseaux (Geomatys)
*/
abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
/**
* An arbitrary limit on the number of dimensions, for catching what may be malformed data.
* We rarely have more than 4 dimensions.
*/
private static final int MAXDIM = 1000;
/**
* The type of datum as a GeoAPI sub-interface of {@link Datum}.
* Used for verifying the type of cached datum at {@link #datumIndex}.
*/
private final Class<D> datumType;
/**
* Name of the datum on which the CRS is presumed to be based, or {@code ""}. This is used
* for building a datum name like <q>Unknown datum presumably based on GRS 1980</q>.
*/
private final String datumBase;
/**
* Index of the cached datum in a {@code Datum[]} array, from 0 inclusive to {@value #DATUM_CACHE_SIZE} exclusive.
* The datum type at that index must be an instance of {@link #datumType}. We cache only the datum because they do
* not depend on the netCDF file content in the common case where the CRS is not explicitly specified.
*/
private final int datumIndex;
/**
* Specify the range of valid number of dimensions, inclusive.
* The {@link #dimension} value shall be in that range.
*/
private final int minDim, maxDim;
/**
* Number of valid elements in the {@link #axes} array. The count should not be larger than 3,
* even if the netCDF file has more axes, because each {@code CRSBuilder} is only for a subset.
*/
private int dimension;
/**
* The axes to use for creating the coordinate reference system.
* They are information about netCDF axes, not yet ISO 19111 axes.
* The axis are listed in "natural" order (reverse of netCDF order).
* Only the {@link #dimension} first elements are valid.
*/
private Axis[] axes;
/**
* The datum created by {@link #createDatum(DatumFactory, Map)}.
*/
protected D datum;
/**
* The coordinate system created by {@link #createCS(CSFactory, Map, CoordinateSystemAxis[])}.
*/
protected CS coordinateSystem;
/**
* The coordinate reference system that may have been create by {@link #setPredefinedComponents(Decoder)}.
*/
protected SingleCRS referenceSystem;
/**
* Non-fatal exceptions that may occur while building the coordinate reference system.
* The same exception may be repeated many time, in which case we will report only the
* first one.
*
* @see #recoverableException(FactoryException)
*/
private FactoryException warnings;
/**
* Creates a new CRS builder based on datum of the given type.
* This constructor is invoked indirectly by {@link #dispatch(List, Axis)}.
*
* @param datumType the type of datum as a GeoAPI sub-interface of {@link Datum}.
* @param datumBase name of the datum on which the CRS is presumed to be based, or {@code ""}.
* @param datumIndex index of the cached datum in a {@code Datum[]} array.
* @param minDim minimum number of dimensions (usually 1, 2 or 3).
* @param maxDim maximum number of dimensions (usually 1, 2 or 3).
*/
private CRSBuilder(final Class<D> datumType, final String datumBase, final int datumIndex, final int minDim, final int maxDim) {
this.datumType = datumType;
this.datumBase = datumBase;
this.datumIndex = datumIndex;
this.minDim = minDim;
this.maxDim = maxDim;
this.axes = new Axis[3];
}
/**
* Infers a new CRS for a {@link Grid}.
*
* <h4>CRS replacements</h4>
* The {@code linearizations} argument allows to replace some CRSs inferred by this method by hard-coded CRSs.
* This is non-empty only when reading a netCDF file for a specific profile, i.e. a file decoded with a subclass
* of {@link Convention}. The CRS to be replaced is inferred from the axis directions.
*
* @param decoder the decoder of the netCDF from which the CRS are constructed.
* @param grid the grid for which the CRS are constructed.
* @param linearizations contains CRS to use instead of CRS inferred by this method, or null or empty if none.
* @param reorderGridToCRS an affine transform doing a final step in a "grid to CRS" transform for ordering axes.
* Not used by this method, but may be modified for taking in account axis order changes caused by replacements
* defined in {@code linearizations}. Ignored (can be null) if {@code linearizations} is null.
* @return coordinate reference system from the given axes, or {@code null}.
*/
public static CoordinateReferenceSystem assemble(final Decoder decoder, final Grid grid,
final List<GridCacheValue> linearizations, final Matrix reorderGridToCRS)
throws DataStoreException, FactoryException, IOException
{
final List<CRSBuilder<?,?>> builders = new ArrayList<>(4);
for (final Axis axis : grid.getAxes(decoder)) {
dispatch(builders, axis);
}
final SingleCRS[] components = new SingleCRS[builders.size()];
for (int i=0; i < components.length; i++) {
components[i] = builders.get(i).build(decoder, true);
}
/*
* If there is hard-coded CRS implied by `Convention.linearizers()`, use it now.
* We do not verify the datum; we assume that the linearizer that built the CRS
* was consistent with `Convention.defaultHorizontalCRS(false)`.
*/
if ((linearizations != null) && !linearizations.isEmpty()) {
Linearizer.replaceInCompoundCRS(components, linearizations, reorderGridToCRS);
}
switch (components.length) {
case 0: return null;
case 1: return components[0];
}
return new EllipsoidalHeightCombiner(decoder).createCompoundCRS(properties(grid.getName()), components);
}
/**
* Infers a new horizontal and vertical CRS for a {@link FeatureSet}.
* The CRS returned by this method does not include a temporal component.
* Instead the temporal component, if found, is stored in the {@code time} array.
* Note that the temporal component is not necessarily a {@link org.opengis.referencing.crs.TemporalCRS} instance;
* it can also be an {@link org.opengis.referencing.crs.EngineeringCRS} instance if the datum epoch is unknown.
*
* @param decoder the decoder of the netCDF from which the CRS are constructed.
* @param axes the axes to use for creating a CRS.
* @param time an array of length 1 where to store the temporal CRS.
* @return coordinate reference system from the given axes, or {@code null}.
*/
static CoordinateReferenceSystem assemble(final Decoder decoder, final Iterable<Variable> axes, final SingleCRS[] time)
throws DataStoreException, FactoryException, IOException
{
final List<CRSBuilder<?,?>> builders = new ArrayList<>(4);
for (final Variable axis : axes) {
dispatch(builders, new Axis(axis));
}
final SingleCRS[] components = new SingleCRS[builders.size()];
int n = 0;
for (final CRSBuilder<?, ?> cb : builders) {
final SingleCRS c = cb.build(decoder, false);
if (cb instanceof Temporal) {
time[0] = c;
} else {
components[n++] = c;
}
}
switch (n) {
case 0: return null;
case 1: return components[0];
}
return new EllipsoidalHeightCombiner(decoder).createCompoundCRS(ArraysExt.resize(components, n));
}
/**
* Dispatches the given axis to a {@code CRSBuilder} appropriate for the axis type. The axis type is determined
* from {@link Axis#abbreviation}, taken as a controlled vocabulary. If no suitable {@code CRSBuilder} is found
* in the given list, then a new one will be created and added to the list.
*
* @param components the list of builder where to dispatch the axis. May be modified by this method.
* @param axis the axis to add to a builder in the given list.
* @throws DataStoreContentException if the given axis cannot be added in a builder.
*/
@SuppressWarnings("fallthrough")
private static void dispatch(final List<CRSBuilder<?,?>> components, final Axis axis) throws DataStoreContentException {
final Class<? extends CRSBuilder<?,?>> addTo;
final Supplier<CRSBuilder<?,?>> constructor;
int alternative = -1;
switch (axis.abbreviation) {
case 'h': for (int i=components.size(); --i >= 0;) { // Can apply to either Geographic or Projected.
if (components.get(i) instanceof Projected) {
alternative = i;
break;
}
} // Fallthrough
case 'λ': case 'φ': addTo = Geographic.class; constructor = Geographic::new; break;
case 'θ': case 'Ω': case 'r': addTo = Spherical.class; constructor = Spherical::new; break;
case 'E': case 'N': addTo = Projected.class; constructor = Projected::new; break;
case 'H': case 'D': addTo = Vertical.class; constructor = Vertical::new; break;
case 't': addTo = Temporal.class; constructor = Temporal::new; break;
default: addTo = Engineering.class; constructor = Engineering::new; break;
}
/*
* If a builder of `addTo` class already exists, add the axis in the existing builder.
* We should have at most one builder of each class. But if we nevertheless have more,
* add to the most recently used builder. If there is no builder, create a new one.
*/
for (int i=components.size(); --i >= 0;) {
final CRSBuilder<?,?> builder = components.get(i);
if (addTo.isInstance(builder) || i == alternative) {
builder.add(axis);
return;
}
}
final CRSBuilder<?,?> builder = constructor.get();
/*
* Before to add the axis to a newly created builder, verify if we wrongly associated
* the ellipsoidal height to Geographic builder before. The issue is that ellipsoidal
* height can be associated to either Geographic or Projected CRS. If we do not have
* more information, our first bet is Geographic. If our bet appears to be wrong, the
* block below fixes it.
*/
if (addTo == Projected.class) {
previous: for (int i=components.size(); --i >= 0;) {
final CRSBuilder<?,?> replace = components.get(i);
for (final Axis a : replace.axes) {
if (a.abbreviation != 'h') {
continue previous; // Not a lonely ellipsoidal height in a Geographic CRS.
}
}
for (final Axis a : replace.axes) { // Should have exactly one element, but we are paranoiac.
builder.add(a);
}
components.remove(i);
break;
}
}
builder.add(axis);
components.add(builder); // Add only after we ensured that the builder contains at least one axis.
}
/**
* Adds an axis for the coordinate reference system to build. Adding more than 3 axes is usually an error,
* but this method nevertheless stores those extraneous axis references for building an error message later.
*
* @param axis the axis to add.
* @throws DataStoreContentException if the given axis cannot be added in this builder.
*/
private void add(final Axis axis) throws DataStoreContentException {
if (dimension > MAXDIM) {
throw new DataStoreContentException(getFirstAxis().coordinates.errors()
.getString(Errors.Keys.ExcessiveListSize_2, "axes", dimension));
}
if (dimension >= axes.length) {
axes = Arrays.copyOf(axes, dimension * 2); // Should not happen (see method javadoc).
}
axes[dimension++] = axis;
}
/**
* Returns whether the coordinate system has at least 3 axes.
*/
final boolean is3D() {
return dimension >= 3;
}
/**
* Returns the first axis. This method is invoked for coordinate reference systems that are known
* to contain only one axis, for example temporal coordinate systems.
*/
final Axis getFirstAxis() {
return axes[0];
}
/**
* Creates the coordinate reference system.
* This method can be invoked after all axes have been dispatched.
*
* @param decoder the decoder of the netCDF from which the CRS are constructed.
* @param grid {@code true} if building a CRS for a grid, or {@code false} for features.
*/
private SingleCRS build(final Decoder decoder, final boolean grid)
throws FactoryException, DataStoreException, IOException
{
if (dimension > maxDim) {
/*
* Reminder: FactoryException is handled as a warning by the `Grid` caller.
* By contrast, `DataStoreContentException` would be treated as a fatal error.
*/
final Variable axis = getFirstAxis().coordinates;
throw new FactoryException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4,
axis.getFilename(), getClass().getSimpleName(), dimension, NamedElement.listNames(axes, dimension, ", ")));
}
/*
* If the subclass can offer coordinate system and datum candidates based on a brief inspection of axes,
* set the datum, CS and CRS field values to those candidate. Those values do not need to be exact; they
* will be overwritten later if they do not match the netCDF file content.
*/
datum = datumType.cast(decoder.datumCache[datumIndex]); // Should be before `setPredefinedComponents` call.
setPredefinedComponents(decoder);
/*
* If `setPredefinedComponents(decoder)` offers a datum, we will used it as-is. Otherwise create the datum now.
* Datum are often not defined in netCDF files, so the above `setPredefinedComponents` method call may have set
* EPSG::6019 — "Not specified (based on GRS 1980 ellipsoid)". If not, we build a similar name.
*/
if (datum == null) {
// Not localized because stored as a String, possibly exported in WKT or GML, and `datumBase` is in English.
createDatum(decoder.getDatumFactory(), properties("Unknown datum presumably based upon ".concat(datumBase)));
}
decoder.datumCache[datumIndex] = datum;
/*
* We cannot go further if the number of dimensions is not valid for the coordinate system to build.
* This error may happen for example when the CRS type is geographic, but only the latitude axis has
* been declared (without longitude axis). It may happen for example with a (latitude, time) system.
* In such case, we can build an engineering CRS has a replacement.
*/
if (dimension < minDim) {
final CRSBuilder<EngineeringDatum, ?> eng = new CRSBuilder.Engineering();
System.arraycopy(axes, 0, eng.axes, 0, dimension);
eng.dimension = dimension;
eng.datum = decoder.getDatumFactory().createEngineeringDatum(
IdentifiedObjects.getProperties(datum, Datum.IDENTIFIERS_KEY));
eng.createFromDatum(decoder, grid);
return eng.referenceSystem;
}
/*
* Verify if a predefined coordinate system can be used. This is often the case, for example
* the EPSG::6424 coordinate system can be used for (longitude, latitude) axes in degrees.
* Using a predefined CS allows us to get more complete definitions (minimum and maximum values, etc.).
*/
if (coordinateSystem != null) {
for (int i=dimension; --i >= 0;) {
final Axis expected = axes[i];
if (expected == null || !expected.isSameUnitAndDirection(coordinateSystem.getAxis(i))) {
coordinateSystem = null;
referenceSystem = null;
break;
}
}
}
/*
* If `setPredefinedComponents(decoder)` did not proposed a coordinate system, or if it proposed a CS
* but its axes do not match the axes in the netCDF file, then create a new coordinate system here.
*/
if (referenceSystem == null) {
createFromDatum(decoder, grid);
}
/*
* Creates the coordinate reference system using current value of `datum` and `coordinateSystem` fields.
* The coordinate system initially have a [-180 … +180]° longitude range. If the actual coordinate values
* are outside that range, switch the longitude range to [0 … 360]°.
*/
if (grid) {
final CoordinateSystem cs = referenceSystem.getCoordinateSystem();
for (int i=cs.getDimension(); --i >= 0;) {
final CoordinateSystemAxis axis = cs.getAxis(i);
if (axis.getRangeMeaning() == RangeMeaning.WRAPAROUND) {
final NumberRange<?> range = axes[i].read().range(); // Vector is cached.
if (range != null) {
// Note: minimum/maximum are not necessarily first and last values in the vector.
if (range.getMinDouble() >= 0 && range.getMaxDouble() > axis.getMaximumValue()) {
referenceSystem = (SingleCRS) AbstractCRS.castOrCopy(referenceSystem)
.forConvention(AxesConvention.POSITIVE_RANGE);
coordinateSystem = null;
break;
}
}
}
}
}
if (warnings != null) {
decoder.listeners.warning(Level.FINE, null, warnings);
}
return referenceSystem;
}
/**
* Unconditionally creates a coordinate reference system, overwriting current {@link #referenceSystem} value.
* The {@link #datum} field must be initialized before to invoke this method.
*/
private void createFromDatum(final Decoder decoder, final boolean grid)
throws FactoryException, DataStoreException, IOException
{
final Map<String,?> properties;
if (coordinateSystem == null) {
// Fallback if the coordinate system is not predefined.
final StringJoiner joiner = new StringJoiner(" ");
final CSFactory csFactory = decoder.getCSFactory();
final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[dimension];
for (int i=0; i<iso.length; i++) {
final Axis axis = axes[i];
joiner.add(axis.getName());
iso[i] = axis.toISO(csFactory, i, grid);
}
createCS(csFactory, properties(joiner.toString()), iso);
properties = properties(coordinateSystem.getName());
} else {
properties = properties(NamedElement.listNames(axes, dimension, " "));
}
createCRS(decoder.getCRSFactory(), properties);
}
/**
* Reports a non-fatal exception that may occur during {@link #setPredefinedComponents(Decoder)}.
* In order to avoid repeating the same warning many times, this method collects the warnings
* together and reports them in a single log record after we finished creating the CRS.
*
* <p>The expected exception types are:</p>
* <ul>
* <li>{@link NoSuchAuthorityCodeException}</li>
* <li>{@link InvalidGeodeticParameterException}</li>
* </ul>
*/
final void recoverableException(final FactoryException e) {
if (warnings == null) warnings = e;
else warnings.addSuppressed(e);
}
/**
* Returns the properties to give to factory {@code create} methods.
*
* @param name name of the geodetic object (datum, coordinate system, …) to create.
*/
private static Map<String,?> properties(final Object name) {
return Map.of(IdentifiedObject.NAME_KEY, name);
}
/**
* Returns the EPSG code of a possible coordinate system from EPSG database. This method proceed by brief
* inspection of axis directions and units; there is no guarantee that the coordinate system returned by
* this method match the axes defined in the netCDF file. It is caller's responsibility to verify.
* This is a helper method for {@link #setPredefinedComponents(Decoder)} implementations.
*
* @param defaultUnit the unit to use if unit definition is missing in the netCDF file.
* @return EPSG code of a CS candidate, or {@code null} if none.
*
* @see Geodetic#isPredefinedCS(Unit)
*/
final Integer epsgCandidateCS(final Unit<?> defaultUnit) {
Unit<?> unit = getFirstAxis().getUnit();
if (unit == null) unit = defaultUnit;
final AxisDirection[] directions = new AxisDirection[dimension];
for (int i=0; i<directions.length; i++) {
directions[i] = axes[i].direction;
}
return CoordinateSystems.getEpsgCode(unit, directions);
}
/**
* If a brief inspection of unit and direction of the {@linkplain #getFirstAxis() first axis} suggests
* that a predefined coordinate system could be used, sets the {@link #coordinateSystem} field to that CS.
* The coordinate system does not need to be a full match since all axes will be verified by the caller.
* This method is invoked before to fallback on {@link #createCS(CSFactory, Map, CoordinateSystemAxis[])}.
*
* <p>This method may opportunistically set the {@link #datum} and {@link #referenceSystem} fields if it
* can propose a CRS candidate instead of only a CS candidate.</p>
*/
abstract void setPredefinedComponents(Decoder decoder) throws FactoryException;
/**
* Creates the datum for the coordinate reference system to build. The datum are generally not specified in netCDF files.
* To make that clearer, this method builds datum with names like <q>Unknown datum presumably based on GRS 1980</q>.
* The newly created datum is assigned to the {@link #datum} field.
*
* @param factory the factory to use for creating the datum.
* @param properties contains the name of the datum to create.
*/
abstract void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException;
/**
* Creates the coordinate system from the given axes. This method is invoked only after we
* verified that the number of axes is inside the {@link #minDim} … {@link #maxDim} range.
* The newly created coordinate system is assigned to the {@link #coordinateSystem} field.
*
* @param factory the factory to use for creating the coordinate system.
* @param properties contains the name of the coordinate system to create.
* @param axes the axes of the coordinate system.
*/
abstract void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException;
/**
* Creates the coordinate reference system from the values in {@link #datum} and {@link #coordinateSystem} fields.
* This method is invoked only if {@link #referenceSystem} is still {@code null} or its axes do not correspond to
* the expected axis. The newly created reference system is assigned to the {@link #referenceSystem} field.
*
* @param factory the factory to use for creating the coordinate reference system.
* @param properties contains the name of the coordinate reference system to create.
*/
abstract void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException;
/**
* Base classes of {@link Spherical}, {@link Geographic} and {@link Projected} builders.
* They all have in common to be based on a {@link GeodeticDatum}.
*/
private abstract static class Geodetic<CS extends CoordinateSystem> extends CRSBuilder<GeodeticDatum, CS> {
/**
* Index for the cache of datum in the {@link Decoder#datumCache} array.
*/
static final int CACHE_INDEX = 0;
/**
* The coordinate reference system which is presumed the basis of datum on netCDF files.
*/
protected CommonCRS defaultCRS;
/**
* Whether the coordinate system has longitude before latitude.
* This flag is set as a side-effect of {@link #isPredefinedCS(Unit)} method call.
*/
protected boolean isLongitudeFirst;
/**
* For subclasses constructors.
*
* @param minDim minimum number of dimensions (2 or 3).
*/
Geodetic(final int minDim) {
super(GeodeticDatum.class, "GRS 1980", CACHE_INDEX, minDim, 3);
}
/**
* Initializes this builder before {@link #build(Decoder, boolean)} execution.
*/
@Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
defaultCRS = decoder.convention().defaultHorizontalCRS(false);
}
/**
* Creates a {@link GeodeticDatum} for <q>Unknown datum presumably based on GRS 1980</q>.
* This method is invoked only if {@link #setPredefinedComponents(Decoder)} failed to create a datum.
*/
@Override final void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
final GeodeticDatum template = defaultCRS.datum();
datum = factory.createGeodeticDatum(properties, template.getEllipsoid(), template.getPrimeMeridian());
}
/**
* Returns {@code true} if the coordinate system may be one of the predefined CS. A returns value of {@code true}
* is not a guarantee that the coordinate system in the netCDF file matches the predefined CS; it only tells that
* this is reasonable chances to be the case based on a brief inspection of the first coordinate system axis.
* If {@code true}, then {@link #isLongitudeFirst} will have been set to an indication of axis order.
*
* @param expected the expected unit of measurement of the first axis.
*
* @see #epsgCandidateCS(Unit)
*/
final boolean isPredefinedCS(final Unit<?> expected) {
final Axis axis = getFirstAxis();
final Unit<?> unit = axis.getUnit();
if (unit == null || expected.equals(unit)) {
isLongitudeFirst = (axis.direction == AxisDirection.EAST);
if (isLongitudeFirst || (axis.direction == AxisDirection.NORTH)) {
return true;
}
}
return false;
}
}
/**
* Builder for geocentric CRS with (θ,Ω,r) axes.
*/
private static final class Spherical extends Geodetic<SphericalCS> {
/**
* Creates a new builder (invoked by lambda function).
*/
public Spherical() {
super(3);
}
/**
* Possibly sets {@link #datum} and {@link #coordinateSystem} to predefined objects
* matching the axes defined in the netCDF file.
*/
@Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
super.setPredefinedComponents(decoder);
if (isPredefinedCS(Units.DEGREE)) {
GeodeticCRS crs = defaultCRS.spherical();
if (isLongitudeFirst) {
crs = DefaultGeocentricCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
}
referenceSystem = crs;
coordinateSystem = (SphericalCS) crs.getCoordinateSystem();
datum = crs.getDatum();
} else {
datum = defaultCRS.datum();
}
}
/**
* Creates the two- or three-dimensional {@link SphericalCS} from given axes. This method is invoked only
* if {@link #setPredefinedComponents(Decoder)} failed to assign a CS or if {@link #build(Decoder, boolean)}
* found that the {@link #coordinateSystem} does not have compatible axes.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
if (axes.length > 2) {
coordinateSystem = factory.createSphericalCS(properties, axes[0], axes[1], axes[2]);
} else {
coordinateSystem = factory.createSphericalCS(properties, axes[0], axes[1]);
}
}
/**
* Creates the coordinate reference system from datum and coordinate system computed in previous steps.
* This method is invoked under conditions similar to the ones of above {@code createCS(…)} method.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
referenceSystem = factory.createGeodeticCRS(properties, datum, coordinateSystem);
}
}
/**
* Geographic CRS with (λ,φ,h) axes.
* The height, if present, is ellipsoidal height.
*/
private static final class Geographic extends Geodetic<EllipsoidalCS> {
/**
* Creates a new builder (invoked by lambda function).
*/
public Geographic() {
super(2);
}
/**
* Possibly sets {@link #datum}, {@link #coordinateSystem} and {@link #referenceSystem}
* to predefined objects matching the axes defined in the netCDF file.
*/
@Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
super.setPredefinedComponents(decoder);
if (isPredefinedCS(Units.DEGREE)) {
GeographicCRS crs;
if (is3D()) {
crs = defaultCRS.geographic3D();
if (isLongitudeFirst) {
crs = DefaultGeographicCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
}
} else if (isLongitudeFirst) {
crs = defaultCRS.normalizedGeographic();
} else {
crs = defaultCRS.geographic();
}
referenceSystem = crs;
coordinateSystem = crs.getCoordinateSystem();
datum = crs.getDatum();
} else {
datum = defaultCRS.datum();
final Integer epsg = epsgCandidateCS(Units.DEGREE);
if (epsg != null) try {
coordinateSystem = decoder.getCSAuthorityFactory().createEllipsoidalCS(epsg.toString());
} catch (NoSuchAuthorityCodeException e) {
recoverableException(e);
}
}
}
/**
* Creates the two- or three-dimensional {@link EllipsoidalCS} from given axes. This method is invoked only if
* {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder,
* boolean)} found that the {@link #coordinateSystem} does not have compatible axes.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
if (axes.length > 2) {
coordinateSystem = factory.createEllipsoidalCS(properties, axes[0], axes[1], axes[2]);
} else {
coordinateSystem = factory.createEllipsoidalCS(properties, axes[0], axes[1]);
}
}
/**
* Creates the coordinate reference system from datum and coordinate system computed in previous steps.
* This method is invoked under conditions similar to the ones of above {@code createCS(…)} method.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
referenceSystem = factory.createGeographicCRS(properties, datum, coordinateSystem);
}
}
/**
* Projected CRS with (E,N,h) axes. There is not enough information in a netCDF files for creating the right
* map projection, unless {@code "grid_mapping"} attributes are specified. If insufficient information, this
* class creates an unknown map projection based on Plate Carrée. Note that this map projection may be replaced
* by {@link GridMapping#crs} at a later stage.
*/
private static final class Projected extends Geodetic<CartesianCS> {
/**
* The spherical variant of {@link #defaultCRS}.
*/
private CommonCRS sphericalDatum;
/**
* Defining conversion for "Not specified (presumed Plate Carrée)". This conversion uses spherical formulas.
* Consequently, it should be used with {@link #sphericalDatum} instead of {@link #defaultCRS}.
*/
private static final Conversion UNKNOWN_PROJECTION;
static {
final CoordinateOperationFactory factory = DefaultCoordinateOperationFactory.provider();
try {
final OperationMethod method = factory.getOperationMethod(Equirectangular.NAME);
UNKNOWN_PROJECTION = factory.createDefiningConversion(
properties("Not specified (presumed Plate Carrée)"),
method, method.getParameters().createValue());
} catch (FactoryException e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* Creates a new builder (invoked by lambda function).
*/
public Projected() {
super(2);
}
/**
* Possibly sets {@link #datum}, {@link #coordinateSystem} and {@link #referenceSystem}
* to predefined objects matching the axes defined in the netCDF file.
*/
@Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
super.setPredefinedComponents(decoder);
sphericalDatum = decoder.convention().defaultHorizontalCRS(true);
datum = sphericalDatum.datum();
if (isPredefinedCS(Units.METRE)) {
coordinateSystem = decoder.getStandardProjectedCS();
}
}
/**
* Creates the two- or three-dimensional {@link CartesianCS} from given axes. This method is invoked only if
* {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder,
* boolean)} found that the {@link #coordinateSystem} does not have compatible axes.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
if (axes.length > 2) {
coordinateSystem = factory.createCartesianCS(properties, axes[0], axes[1], axes[2]);
} else {
coordinateSystem = factory.createCartesianCS(properties, axes[0], axes[1]);
}
}
/**
* Creates the coordinate reference system from datum and coordinate system computed in previous steps.
* The datum for this method is based on a sphere.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
final boolean is3D = (coordinateSystem.getDimension() >= 3);
GeographicCRS baseCRS = is3D ? sphericalDatum.geographic3D() : sphericalDatum.geographic();
if (!baseCRS.getDatum().equals(datum)) {
baseCRS = factory.createGeographicCRS(properties, datum, baseCRS.getCoordinateSystem());
}
referenceSystem = factory.createProjectedCRS(properties, baseCRS, UNKNOWN_PROJECTION, coordinateSystem);
}
}
/**
* Vertical CRS with (H) or (D) axis.
* Used for mean sea level (not for ellipsoidal height).
*/
private static final class Vertical extends CRSBuilder<VerticalDatum, VerticalCS> {
/**
* Index for the cache of datum in the {@link Decoder#datumCache} array.
*/
static final int CACHE_INDEX = Geodetic.CACHE_INDEX + 1;
/**
* Creates a new builder (invoked by lambda function).
*/
public Vertical() {
super(VerticalDatum.class, "Mean Sea Level", CACHE_INDEX, 1, 1);
}
/**
* Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file.
*/
@Override void setPredefinedComponents(final Decoder decoder) {
final Axis axis = getFirstAxis();
final Unit<?> unit = axis.getUnit();
final CommonCRS.Vertical predefined;
if (Units.METRE.equals(unit)) {
if (axis.direction == AxisDirection.UP) {
predefined = CommonCRS.Vertical.MEAN_SEA_LEVEL;
} else {
predefined = CommonCRS.Vertical.DEPTH;
}
} else if (Units.HECTOPASCAL.equals(unit)) {
predefined = CommonCRS.Vertical.BAROMETRIC;
} else {
return;
}
coordinateSystem = predefined.crs().getCoordinateSystem();
}
/**
* Creates a {@link VerticalDatum} for <q>Unknown datum based on Mean Sea Level</q>.
*/
@SuppressWarnings("deprecation")
@Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
datum = factory.createVerticalDatum(properties, RealizationMethod.GEOID);
}
/**
* Creates the one-dimensional {@link VerticalCS} from given axes. This method is invoked
* only if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system
* or if {@link #build(Decoder, boolean)} found that the axis or direction are not compatible.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
coordinateSystem = factory.createVerticalCS(properties, axes[0]);
}
/**
* Creates the coordinate reference system from datum and coordinate system computed in previous steps.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
referenceSystem = factory.createVerticalCRS(properties, datum, coordinateSystem);
}
}
/**
* Temporal CRS with (t) axis. Its datum need to be built
* in a special way since it contains the time origin.
*/
private static final class Temporal extends CRSBuilder<TemporalDatum, TimeCS> {
/**
* Index for the cache of datum in the {@link Decoder#datumCache} array.
*/
static final int CACHE_INDEX = Vertical.CACHE_INDEX + 1;
/**
* Creates a new builder (invoked by lambda function).
*/
public Temporal() {
super(TemporalDatum.class, "", CACHE_INDEX, 1, 1);
}
/**
* Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file.
*/
@Override void setPredefinedComponents(final Decoder decoder) {
final Axis axis = getFirstAxis();
final Unit<?> unit = axis.getUnit();
final CommonCRS.Temporal predefined;
if (Units.DAY.equals(unit)) {
predefined = CommonCRS.Temporal.JULIAN;
} else if (Units.SECOND.equals(unit)) {
predefined = CommonCRS.Temporal.UNIX;
} else if (Units.MILLISECOND.equals(unit)) {
predefined = CommonCRS.Temporal.JAVA;
} else {
return;
}
coordinateSystem = predefined.crs().getCoordinateSystem();
}
/**
* Creates a {@link TemporalDatum} for <q>Unknown datum based on …</q>.
* This method may left the datum to {@code null} if the epoch is unknown.
* In such case, {@link #createCRS createCRS(…)} will create an engineering CRS instead.
*/
@Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
final Axis axis = getFirstAxis();
axis.getUnit(); // Force epoch parsing if not already done.
final Instant epoch = axis.coordinates.epoch;
if (epoch != null) {
final CommonCRS.Temporal c = CommonCRS.Temporal.forEpoch(epoch);
if (c != null) {
datum = c.datum();
} else {
properties = properties("Time since " + epoch);
datum = factory.createTemporalDatum(properties, Date.from(epoch));
}
}
}
/**
* Creates the one-dimensional {@link TimeCS} from given axes. This method is invoked only
* if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if
* {@link #build(Decoder, boolean)} found that the axis or direction are not compatible.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
coordinateSystem = factory.createTimeCS(properties, axes[0]);
}
/**
* Creates the coordinate reference system from datum and coordinate system computed in previous steps.
* It should be a temporal CRS. But if the temporal datum cannot be created because epoch was unknown,
* this method fallbacks on an engineering CRS.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
properties = properties(getFirstAxis().coordinates.getUnitsString());
if (datum != null) {
referenceSystem = factory.createTemporalCRS(properties, datum, coordinateSystem);
} else {
referenceSystem = factory.createEngineeringCRS(properties,
CommonCRS.Engineering.TIME.datum(), coordinateSystem);
}
}
}
/**
* Unknown CRS with (x,y,z) axes.
*/
private static final class Engineering extends CRSBuilder<EngineeringDatum, CoordinateSystem> {
/**
* Index for the cache of datum in the {@link Decoder#datumCache} array.
*/
static final int CACHE_INDEX = Temporal.CACHE_INDEX + 1;
/**
* Creates a new builder (invoked by lambda function).
*/
public Engineering() {
super(EngineeringDatum.class, "affine coordinate system", CACHE_INDEX, 1, 3);
}
/**
* No-op since we have no predefined engineering CRS.
*/
@Override void setPredefinedComponents(final Decoder decoder) {
}
/**
* Creates a {@link VerticalDatum} for <q>Unknown datum based on affine coordinate system</q>.
*/
@Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
datum = factory.createEngineeringDatum(properties);
}
/**
* Creates two- or three-dimensional coordinate system (usually {@link AffineCS}) from given axes.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
try {
switch (axes.length) {
case 0: break; // Should never happen but we are paranoiac.
case 1: coordinateSystem = factory.createParametricCS(properties, axes[0]); return;
case 2: coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1]); return;
default: coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1], axes[2]); return;
}
} catch (InvalidGeodeticParameterException e) {
/*
* Unknown Coordinate System type, for example because of unexpected units of measurement for a
* Cartesian or affine coordinate system. The fallback object created below is not abstract in
* the Java sense, but in the sense that we don't have more specific information on the CS type.
*/
recoverableException(e);
}
coordinateSystem = new AbstractCS(properties, axes);
}
/**
* Creates the coordinate reference system from datum and coordinate system computed in previous steps.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
referenceSystem = factory.createEngineeringCRS(properties, datum, coordinateSystem);
}
}
/**
* Maximal {@link #datumIndex} value +1. The maximal value can be seen in the call to {@code super(…)} constructor
* in the last inner class defined above.
*/
static final int DATUM_CACHE_SIZE = Engineering.CACHE_INDEX + 1;
}