blob: 7d3ed5f433d4aaf2273204e604cf8a8618744371 [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.internal.netcdf;
import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.Collections;
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.cs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.GeocentricCRS;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.Conversion;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.referencing.crs.AbstractCRS;
import org.apache.sis.referencing.crs.DefaultGeographicCRS;
import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
import org.apache.sis.internal.referencing.provider.Equirectangular;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.util.TemporalUtilities;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.measure.Units;
import org.apache.sis.math.Vector;
/**
* 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)} 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.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 1.0
* @module
*/
abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
/**
* 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 <cite>"Unknown datum presumably based on GRS 1980"</cite>.
*/
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 byte datumIndex;
/**
* Specify the range of valid number of dimensions, inclusive.
* The {@link #dimension} value shall be in that range.
*/
private final byte 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 byte 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(NoSuchAuthorityCodeException)
*/
private NoSuchAuthorityCodeException 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 byte datumIndex, final byte minDim, final byte maxDim) {
this.datumType = datumType;
this.datumBase = datumBase;
this.datumIndex = datumIndex;
this.minDim = minDim;
this.maxDim = maxDim;
this.axes = new Axis[3];
}
/**
* 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 can not be added in a builder.
*/
@SuppressWarnings("fallthrough")
public 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 can not be added in this builder.
*/
private void add(final Axis axis) throws DataStoreContentException {
if (dimension == Byte.MAX_VALUE) {
throw new DataStoreContentException(getFirstAxis().coordinates.errors()
.getString(Errors.Keys.ExcessiveListSize_2, "axes", (short) (Byte.MAX_VALUE + 1)));
}
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.
*/
public final SingleCRS build(final Decoder decoder) throws FactoryException, DataStoreException, IOException {
if (dimension < minDim || dimension > maxDim) {
final Variable axis = getFirstAxis().coordinates;
throw new DataStoreContentException(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;
/*
* Verify if a pre-defined coordinate system can be used. This is often the case, for example
* the EPSG::6424 coordinate system can be used for (longitude, latitude) axes in degrees.
* Using a pre-defined CS allows us to get more complete definitions (minimum and maximum values, etc.).
*/
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) {
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);
}
createCS(csFactory, properties(joiner.toString()), iso);
properties = properties(coordinateSystem.getName());
} else {
properties = properties(NamedElement.listNames(axes, dimension, " "));
}
createCRS(decoder.getCRSFactory(), properties);
}
/*
* 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]°.
*/
final CoordinateSystem cs = referenceSystem.getCoordinateSystem();
for (int i=cs.getDimension(); --i >= 0;) {
final CoordinateSystemAxis axis = cs.getAxis(i);
if (RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
final Vector coordinates = axes[i].read(); // Typically a cached vector.
final int length = coordinates.size();
if (length != 0) {
final double first = coordinates.doubleValue(0);
final double last = coordinates.doubleValue(length - 1);
if (Math.min(first, last) >= 0 && Math.max(first, last) > axis.getMaximumValue()) {
referenceSystem = (SingleCRS) AbstractCRS.castOrCopy(referenceSystem).forConvention(AxesConvention.POSITIVE_RANGE);
break;
}
}
}
}
if (warnings != null) {
decoder.listeners.warning(Level.FINE, null, warnings);
}
return referenceSystem;
}
/**
* Reports a non-fatal exception that may occur during {@link #setPredefinedComponents(Decoder)}.
* In order to avoid repeating the same warning many times, this method collects the warnings
* together and reports them in a single log record after we finished creating the CRS.
*/
final void recoverableException(final NoSuchAuthorityCodeException e) {
if (warnings == null) warnings = e;
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 Collections.singletonMap(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 guarantees that the coordinate system returned by
* this method match the axes defined in the netCDF file. It is caller's responsibility to verify.
* This is a helper method for {@link #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 than 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 <cite>"Unknown datum presumably based on GRS 1980"</cite>.
* The newly created datum is assigned to the {@link #datum} field.
*
* @param factory the factory to use for creating the datum.
* @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> {
/**
* 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 byte minDim) {
super(GeodeticDatum.class, "GRS 1980", (byte) 0, minDim, (byte) 3);
}
/**
* Initializes this builder before {@link #build(Decoder)} execution.
*/
@Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
defaultCRS = decoder.convention().defaultHorizontalCRS(false);
}
/**
* Creates a {@link GeodeticDatum} for <cite>"Unknown datum presumably based on GRS 1980"</cite>.
* This method is invoked only if {@link #setPredefinedComponents(Decoder)} failed to create a datum.
*/
@Override final void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
final GeodeticDatum template = 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 = AxisDirection.EAST.equals(axis.direction);
if (isLongitudeFirst || AxisDirection.NORTH.equals(axis.direction)) {
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((byte) 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)) {
GeocentricCRS 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 three-dimensional {@link SphericalCS} from given axes. This method is invoked only
* if {@link #setPredefinedComponents(Decoder)} failed to assign a CS or if {@link #build(Decoder)}
* found that the {@link #coordinateSystem} does not have compatible axes.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
coordinateSystem = factory.createSphericalCS(properties, axes[0], axes[1], axes[2]);
}
/**
* Creates the coordinate reference system from datum and coordinate system computed in previous steps.
* This method is invoked under conditions similar to the ones of above {@code createCS(…)} method.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
referenceSystem = factory.createGeocentricCRS(properties, datum, coordinateSystem);
}
}
/**
* Geographic CRS with (λ,φ,h) axes.
* The height, if present, is ellipsoidal height.
*/
private static final class Geographic extends Geodetic<EllipsoidalCS> {
/**
* Creates a new builder (invoked by lambda function).
*/
public Geographic() {
super((byte) 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)}
* 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 use spherical formulas.
* Consequently it should be used with {@link #sphericalDatum} instead of {@link #defaultCRS}.
*/
private static final Conversion UNKNOWN_PROJECTION;
static {
final CoordinateOperationFactory factory = DefaultFactories.forBuildin(CoordinateOperationFactory.class);
try {
final OperationMethod method = factory.getOperationMethod(Equirectangular.NAME);
UNKNOWN_PROJECTION = factory.createDefiningConversion(
properties("Not specified (presumed Plate Carrée)"),
method, method.getParameters().createValue());
} catch (FactoryException e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* Creates a new builder (invoked by lambda function).
*/
public Projected() {
super((byte) 2);
}
/**
* Possibly sets {@link #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)}
* 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> {
/**
* Creates a new builder (invoked by lambda function).
*/
public Vertical() {
super(VerticalDatum.class, "Mean Sea Level", (byte) 1, (byte) 1, (byte) 1);
}
/**
* Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file.
*/
@Override void setPredefinedComponents(final Decoder decoder) {
final Axis axis = getFirstAxis();
final Unit<?> unit = axis.getUnit();
final CommonCRS.Vertical predefined;
if (Units.METRE.equals(unit)) {
if (AxisDirection.UP.equals(axis.direction)) {
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 <cite>"Unknown datum based on Mean Sea Level"</cite>.
*/
@Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
datum = factory.createVerticalDatum(properties, VerticalDatumType.GEOIDAL);
}
/**
* Creates the one-dimensional {@link VerticalCS} from given axes. This method is invoked
* only if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system
* or if {@link #build(Decoder)} found that the axis or direction are not compatible.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
coordinateSystem = factory.createVerticalCS(properties, axes[0]);
}
/**
* Creates the coordinate reference system from datum and coordinate system computed in previous steps.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
referenceSystem = factory.createVerticalCRS(properties, datum, coordinateSystem);
}
}
/**
* Temporal CRS with (t) axis. Its datum need to be built
* in a special way since it contains the time origin.
*/
private static final class Temporal extends CRSBuilder<TemporalDatum, TimeCS> {
/**
* Creates a new builder (invoked by lambda function).
*/
public Temporal() {
super(TemporalDatum.class, "", (byte) 2, (byte) 1, (byte) 1);
}
/**
* Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file.
*/
@Override void 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 <cite>"Unknown datum based on …"</cite>.
*/
@Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
final Axis axis = getFirstAxis();
axis.getUnit(); // Force epoch parsing if not already done.
Instant epoch = axis.coordinates.epoch;
final CommonCRS.Temporal c = CommonCRS.Temporal.forEpoch(epoch);
if (c != null) {
datum = c.datum();
} else {
properties = properties("Time since " + epoch);
datum = factory.createTemporalDatum(properties, TemporalUtilities.toDate(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)} 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.
*/
@Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
properties = properties(getFirstAxis().coordinates.getUnitsString());
referenceSystem = factory.createTemporalCRS(properties, datum, coordinateSystem);
}
}
/**
* Unknown CRS with (x,y,z) axes.
*/
private static final class Engineering extends CRSBuilder<EngineeringDatum, AffineCS> {
/**
* Creates a new builder (invoked by lambda function).
*/
public Engineering() {
super(EngineeringDatum.class, "affine coordinate system", (byte) 3, (byte) 2, (byte) 3);
}
/**
* No-op since we have no predefined engineering CRS.
*/
@Override void setPredefinedComponents(final Decoder decoder) {
}
/**
* Creates a {@link VerticalDatum} for <cite>"Unknown datum based on affine coordinate system"</cite>.
*/
@Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
datum = factory.createEngineeringDatum(properties);
}
/**
* Creates two- or three-dimensional {@link AffineCS} from given axes.
*/
@Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
if (axes.length > 2) {
coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1], axes[2]);
} else {
coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1]);
}
}
/**
* 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 = 4;
}