blob: d632b669c3fb46d34c2e91fb2b3c8c9b86f296e3 [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.HashMap;
import java.util.ArrayList;
import java.util.Optional;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.Buffer;
import java.awt.image.DataBuffer;
import org.opengis.util.GenericName;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.internal.storage.AbstractGridResource;
import org.apache.sis.internal.storage.ResourceOnFileSystem;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.internal.util.Strings;
import org.apache.sis.internal.coverage.RasterFactory;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridDerivation;
import org.apache.sis.coverage.grid.GridRoundingMode;
import org.apache.sis.coverage.IllegalSampleDimensionException;
import org.apache.sis.storage.netcdf.AttributeNames;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreReferencingException;
import org.apache.sis.storage.Resource;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.measure.MeasurementRange;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.math.Vector;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.internal.jdk9.JDK9;
/**
* A grid coverage in a netCDF file. We create one resource for each variable,
* unless we determine that two variables should be handled together (for example
* the <var>u</var> and <var>v</var> components of wind vectors).
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
* @author Alexis Manin (Geomatys)
* @version 1.1
* @since 1.0
* @module
*/
public final class RasterResource extends AbstractGridResource implements ResourceOnFileSystem {
/**
* Words used in standard (preferred) or long (if no standard) variable names which suggest
* that the variable is a component of a vector. Those words are used in heuristic rules
* for deciding if two variables should be stored in a single {@code Coverage} instance.
* For example the eastward (u) and northward (v) components of oceanic current vectors
* should be stored as two sample dimensions of a single "Current" coverage.
* Example of standard variable names:
*
* <ul>
* <li>{@code baroclinic_eastward_sea_water_velocity}</li>
* <li>{@code baroclinic_northward_sea_water_velocity}</li>
* <li>{@code eastward_atmosphere_water_transport_across_unit_distance}</li>
* <li><i>etc.</i></li>
* </ul>
*
* One element to note is that direction (e.g. "eastward") is not necessarily at the beginning
* of variable name.
*
* @see <a href="http://cfconventions.org/Data/cf-standard-names/current/build/cf-standard-name-table.html">Standard name table</a>
*/
private static final String[] VECTOR_COMPONENT_NAMES = {
"eastward", "westward", "northward", "southward", "upward", "downward"
};
/**
* The identifier of this grid resource. This is {@link Variable#getStandardName()}. We prefer netCDF standard name instead
* than variable name because the former is controlled vocabulary. The use of controlled vocabulary for identifiers increases
* the chances of stability or consistency between similar products.
*
* <p>The value set by constructor may be updated by {@link #resolveNameCollision(RasterResource, Decoder)},
* but should not be modified after that point.</p>
*
* @see #getIdentifier()
*/
private GenericName identifier;
/**
* The grid geometry (size, CRS…) of the {@linkplain #data} cube.
* This defines the "domain" in "coverage function" terminology.
*
* @see #getGridGeometry()
*/
private final GridGeometry gridGeometry;
/**
* The sample dimension for the {@link #data} variables.
* This defines the "range" in "coverage function" terminology.
* All elements are initially {@code null} and created when first needed.
*
* @see #getSampleDimensions()
*/
private final SampleDimension[] ranges;
/**
* The netCDF variables for each sample dimensions. The length of this array shall be equal to {@code ranges.length},
* except if bands are stored as one variable dimension ({@link #bandDimension} ≧ 0) in which case the length shall
* be exactly 1. Accesses to this array need to take in account that the length may be only 1. Example:
*
* {@preformat java
* Variable v = data[bandDimension >= 0 ? 0 : index];
* }
*/
private final Variable[] data;
/**
* If one of {@link #data} dimension provides values for different bands, that dimension index. Otherwise -1.
* This is an index in a list of dimensions in "natural" order (reverse of netCDF order).
* There is three ways to read the data, determined by this {@code bandDimension} value:
*
* <ul>
* <li>{@code (bandDimension < 0)}: one variable per band (usual case).</li>
* <li>{@code (bandDimension = 0)}: one variable containing all bands, with bands in the first dimension.</li>
* <li>{@code (bandDimension > 0)}: one variable containing all bands, with bands in the last dimension.</li>
* </ul>
*
* @see Variable#bandDimension
*/
private final int bandDimension;
/**
* Path to the netCDF file for information purpose, or {@code null} if unknown.
*
* @see #getComponentFiles()
*/
private final Path location;
/**
* The object to use for synchronization. For now we use a {@code synchronized} statement,
* but it may be changed to {@link java.util.concurrent.locks.Lock} in a future version.
*/
private final Object lock;
/**
* Creates a new resource. All variables in the {@code data} list shall have the same domain and the same grid geometry.
*
* @param decoder the implementation used for decoding the netCDF file.
* @param name the name for the resource.
* @param grid the grid geometry (size, CRS…) of the {@linkplain #data} cube.
* @param bands the variables providing actual data. Shall contain at least one variable.
* @param numBands the number of bands. Shall be {@code bands.size()} except if {@code bandsDimension} ≧ 0.
* @param bandDim if one of {@link #data} dimension provides values for different bands, that dimension index. Otherwise -1.
* @param lock the lock to use in {@code synchronized(lock)} statements.
*/
private RasterResource(final Decoder decoder, final String name, final GridGeometry grid, final List<Variable> bands,
final int numBands, final int bandDim, final Object lock)
{
super(decoder.listeners);
data = bands.toArray(new Variable[bands.size()]);
ranges = new SampleDimension[numBands];
identifier = decoder.nameFactory.createLocalName(decoder.namespace, name);
location = decoder.location;
gridGeometry = grid;
bandDimension = bandDim;
this.lock = lock;
assert data.length == (bandDimension >= 0 ? 1 : ranges.length);
}
/**
* Creates all grid coverage resources from the given decoder.
* This method shall be invoked in a method synchronized on {@link #lock}.
*
* @param decoder the implementation used for decoding the netCDF file.
* @param lock the lock to use in {@code synchronized(lock)} statements.
* @return all grid coverage resources.
* @throws IOException if an I/O operation was required and failed.
* @throws DataStoreException if a logical error occurred.
*/
public static List<Resource> create(final Decoder decoder, final Object lock) throws IOException, DataStoreException {
assert Thread.holdsLock(lock);
final Variable[] variables = decoder.getVariables().clone(); // Needs a clone because may be modified.
final List<Variable> siblings = new ArrayList<>(4); // Usually has only 1 element, sometime 2.
final List<Resource> resources = new ArrayList<>(variables.length); // The raster resources to be returned.
final Map<GenericName,RasterResource> firstOfName = new HashMap<>(); // For detecting name collisions.
for (int i=0; i<variables.length; i++) {
final Variable variable = variables[i];
if (variable == null || variable.getRole() != VariableRole.COVERAGE) {
continue; // Skip variables that are not grid coverages.
}
final GridGeometry grid = variable.getGridGeometry();
if (grid == null) {
continue; // Skip variables that are not grid coverages.
}
siblings.add(variable); // Variable will the first band of raster.
String name = variable.getStandardName();
/*
* At this point we found a variable for which to create a resource. Most of the time, there is nothing else to do;
* the resource will have a single variable and the same name than that unique variable. The resulting raster will
* have only one band (sample dimension). However in some cases the raster should have more than one band:
*
* 1) if the variable has an extra dimension compared to the grid geometry;
* 2) of if two or more variables should be grouped together.
*
* The following if {…} else {…} blocks implement those two cases.
*/
final List<Dimension> gridDimensions = variable.getGridDimensions();
final int dataDimension = gridDimensions.size();
final int gridDimension = grid.getDimension();
final int bandDimension, numBands;
if (dataDimension != gridDimension) {
bandDimension = variable.bandDimension; // One variable dimension is interpreted as bands.
Dimension dim = gridDimensions.get(dataDimension - 1 - bandDimension); // Note: "natural" → netCDF index conversion.
numBands = Math.toIntExact(dim.length());
if (dataDimension != gridDimension + 1 || (bandDimension > 0 && bandDimension != gridDimension)) {
/*
* One of the following restrictions it not met for the requested data:
*
* - Only 1 dimension can be used for bands. Variables with 2 or more band dimensions are not supported.
* - The dimension for bands shall be either the first or the last dimension; it can not be in the middle.
*/
throw new DataStoreContentException(Resources.forLocale(decoder.listeners.getLocale())
.getString(Resources.Keys.UnmappedDimensions_4, name, decoder.getFilename(), dataDimension, gridDimension));
}
} else {
/*
* At this point we found a variable where all dimensions are in the CRS. This is the usual case;
* there is no band explicitly declared in the netCDF file. However in some cases, we should put
* other variables together with the one we just found. Example:
*
* 1) baroclinic_eastward_sea_water_velocity
* 2) baroclinic_northward_sea_water_velocity
*
* We use the "eastward" and "northward" keywords for recognizing such pairs, providing that everything else in the
* name is the same and the grid geometries are the same.
*/
bandDimension = -1; // No dimension to be interpreted as bands.
final DataType type = variable.getDataType();
for (final String keyword : VECTOR_COMPONENT_NAMES) {
final int prefixLength = name.indexOf(keyword);
if (prefixLength >= 0) {
int suffixStart = prefixLength + keyword.length();
int suffixLength = name.length() - suffixStart;
for (int j=i; ++j < variables.length;) {
final Variable candidate = variables[j];
if (candidate == null || candidate.getRole() != VariableRole.COVERAGE) {
variables[j] = null; // For avoiding to revisit that variable again.
continue;
}
final String cn = candidate.getStandardName();
if (cn.regionMatches(cn.length() - suffixLength, name, suffixStart, suffixLength) &&
cn.regionMatches(0, name, 0, prefixLength) && candidate.getDataType() == type &&
grid.equals(candidate.getGridGeometry()))
{
/*
* Found another variable with the same name except for the keyword. Verify that the
* keyword is replaced by another word in the vector component keyword list. If this
* is the case, then we consider that those two variables should be kept together.
*/
for (final String k : VECTOR_COMPONENT_NAMES) {
if (cn.regionMatches(prefixLength, k, 0, k.length())) {
siblings.add(candidate);
variables[j] = null;
break;
}
}
}
}
/*
* If we have more than one variable, omit the keyword from the name. For example instead
* of "baroclinic_eastward_sea_water_velocity", construct "baroclinic_sea_water_velocity".
* Note that we may need to remove duplicated '_' character after keyword removal.
*/
if (siblings.size() > 1) {
if (suffixLength != 0) {
final int c = name.codePointAt(suffixStart);
if ((prefixLength != 0) ? (c == name.codePointBefore(prefixLength)) : (c == '_')) {
suffixStart += Character.charCount(c);
}
}
name = new StringBuilder(name).delete(prefixLength, suffixStart).toString();
}
}
}
numBands = siblings.size();
}
final RasterResource r = new RasterResource(decoder, name.trim(), grid, siblings, numBands, bandDimension, lock);
r.resolveNameCollision(firstOfName.putIfAbsent(r.identifier, r), decoder);
resources.add(r);
siblings.clear();
}
return resources;
}
/**
* If the given resource is non-null, modifies the name of this resource for avoiding name collision.
* The {@code other} resource shall be non-null when the caller detected that there is a name collision
* with that resource.
*
* @param other the other resource for which there is a name collision, or {@code null} if no collision.
*/
private void resolveNameCollision(final RasterResource other, final Decoder decoder) {
if (other != null) {
if (identifier.equals(other.identifier)) {
other.resolveNameCollision(decoder);
}
resolveNameCollision(decoder);
}
}
/**
* Invoked when the name of this resource needs to be changed because it collides with the name of another resource.
* This method appends the variable name, which should be unique in each netCDF file.
*/
private void resolveNameCollision(final Decoder decoder) {
String name = identifier + " (" + data[0].getName() + ')';
identifier = decoder.nameFactory.createLocalName(decoder.namespace, name);
}
/**
* Returns the variable name as an identifier of this resource.
*/
@Override
public Optional<GenericName> getIdentifier() {
return Optional.of(identifier);
}
/**
* Returns an object containing the grid size, the CRS and the conversion from grid indices to CRS coordinates.
*
* @return extent of grid coordinates together with their mapping to "real world" coordinates.
*/
@Override
public GridGeometry getGridGeometry() {
return gridGeometry;
}
/**
* Returns the variable at the given index. This method can be invoked when the caller has not verified
* if we are in the special case where all bands are in the same variable ({@link #bandDimension} ≧ 0).
*/
private Variable getVariable(final int i) {
return data[bandDimension >= 0 ? 0 : i];
}
/**
* Returns the ranges of sample values together with the conversion from samples to real values.
*
* @return ranges of sample values together with their mapping to "real values".
* @throws DataStoreException if an error occurred while reading definitions from the underlying data store.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public List<SampleDimension> getSampleDimensions() throws DataStoreException {
SampleDimension.Builder builder = null;
try {
synchronized (lock) {
for (int i=0; i<ranges.length; i++) {
if (ranges[i] == null) {
if (builder == null) {
builder = new SampleDimension.Builder();
}
try {
ranges[i] = createSampleDimension(builder, getVariable(i), i);
} catch (TransformException | IllegalSampleDimensionException e) {
builder.categories().clear();
ranges[i] = builder.build();
warning(e);
}
builder.clear();
}
}
}
} catch (RuntimeException e) {
throw new DataStoreContentException(e);
}
return UnmodifiableArrayList.wrap(ranges);
}
/**
* Creates a single sample dimension for the given variable.
*
* @param builder the builder to use for creating the sample dimension.
* @param band the data for which to create a sample dimension.
* @param index index in the variable dimension identified by {@link #bandDimension}.
* @throws IllegalSampleDimensionException if a sample dimension has overlapping ranges.
* @throws TransformException if an error occurred while using the transfer function.
*/
private SampleDimension createSampleDimension(final SampleDimension.Builder builder, final Variable band, final int index)
throws TransformException
{
/*
* Take the minimum and maximum values as determined by Apache SIS through the Convention class. The UCAR library
* is used only as a fallback. We give precedence to the range computed by Apache SIS instead than the range given
* by UCAR because we need the range of packed values instead than the range of converted values.
*/
NumberRange<?> range;
if (!createEnumeration(builder, band, index) && (range = band.getValidRange()) != null) {
final MathTransform1D mt = band.getTransferFunction().getTransform();
if (!mt.isIdentity() && range instanceof MeasurementRange<?>) {
/*
* Heuristic rule defined in UCAR documentation (see EnhanceScaleMissing interface):
* if the type of the range is equal to the type of the scale, and the type of the
* data is not wider, then assume that the minimum and maximum are real values.
* This is identified in Apache SIS by the range given as a MeasurementRange.
*/
final MathTransform1D inverse = mt.inverse();
boolean isMinIncluded = range.isMinIncluded();
boolean isMaxIncluded = range.isMaxIncluded();
double minimum = inverse.transform(range.getMinDouble());
double maximum = inverse.transform(range.getMaxDouble());
if (maximum < minimum) {
final double swap = maximum;
maximum = minimum;
minimum = swap;
final boolean sb = isMaxIncluded;
isMaxIncluded = isMinIncluded;
isMinIncluded = sb;
}
if (band.getDataType().number <= Numbers.LONG && minimum >= Long.MIN_VALUE && maximum <= Long.MAX_VALUE) {
range = NumberRange.create(Math.round(minimum), isMinIncluded, Math.round(maximum), isMaxIncluded);
} else {
range = NumberRange.create(minimum, isMinIncluded, maximum, isMaxIncluded);
}
}
/*
* Range may be empty if min/max values declared in the netCDF files are erroneous,
* or if we have not read them correctly (edu.ucar:cdm:4.6.13 sometime confuses an
* unsigned integer with a signed one).
*/
if (range.isEmpty()) {
band.warning(RasterResource.class, "getSampleDimensions", Resources.Keys.IllegalValueRange_4,
band.getFilename(), band.getName(), range.getMinValue(), range.getMaxValue());
} else {
String name = band.getDescription();
if (name == null) name = band.getName();
builder.addQuantitative(name, range, mt, band.getUnit());
}
}
/*
* Adds the "missing value" or "fill value" as qualitative categories. If a value has both roles, use "missing value"
* as category name. If the sample values are already real values, then the "no data" values have been replaced by NaN
* values by Variable.replaceNaN(Object). The qualitative categories constructed below must be consistent with the NaN
* values created by 'replaceNaN'.
*/
boolean setBackground = true;
int ordinal = band.hasRealValues() ? 0 : -1;
final CharSequence[] names = new CharSequence[2];
for (final Map.Entry<Number,Object> entry : band.getNodataValues().entrySet()) {
final Number n;
if (ordinal >= 0) {
n = MathFunctions.toNanFloat(ordinal++); // Must be consistent with Variable.replaceNaN(Object).
} else {
n = entry.getKey(); // Should be real number, made unique by the HashMap.
}
CharSequence name;
final Object label = entry.getValue();
if (label instanceof Integer) {
final int role = (Integer) label; // Bit 0 set (value 1) = pad value, bit 1 set = missing value.
final int i = (role == 1) ? 1 : 0; // i=1 if role is only pad value, i=0 otherwise.
name = names[i];
if (name == null) {
name = Vocabulary.formatInternational(i == 0 ? Vocabulary.Keys.MissingValue : Vocabulary.Keys.FillValue);
names[i] = name;
}
if (setBackground & (role & 1) != 0) {
setBackground = false; // Declare only one fill value.
builder.setBackground(name, n);
continue;
}
} else {
name = (CharSequence) label;
}
builder.addQualitative(name, n, n);
}
/*
* At this point we have the list of all categories to put in the sample dimension.
* Now create the sample dimension using the variable short name as dimension name.
* The index is appended to the name only if bands are all in the same variable.
*/
String name = band.getName();
if (bandDimension >= 0) {
name = Strings.toIndexed(name, index);
}
return builder.setName(name).build();
}
/**
* Appends qualitative categories in the given builder for {@code "flag_values"} or {@code "flag_masks"} attribute.
*
* @param builder the builder to use for creating the sample dimension.
* @param band the data for which to create a sample dimension.
* @param index index in the variable dimension identified by {@link #bandDimension}.
* @return {@code true} if flag attributes have been found, or {@code false} otherwise.
*/
private static boolean createEnumeration(final SampleDimension.Builder builder, final Variable band, final int index) {
CharSequence[] names = band.getAttributeAsStrings(AttributeNames.FLAG_NAMES, ' ');
if (names == null) {
names = band.getAttributeAsStrings(AttributeNames.FLAG_MEANINGS, ' ');
if (names == null) return false;
}
Vector values = band.getAttributeAsVector(AttributeNames.FLAG_VALUES);
if (values == null) {
values = band.getAttributeAsVector(AttributeNames.FLAG_MASKS);
if (values == null) return false;
}
final int length = values.size();
for (int i=0; i<length; i++) {
final Number value = values.get(i);
final CharSequence name;
if (i < names.length) {
name = names[i];
} else {
name = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed);
}
builder.addQualitative(name, value, value);
}
return true;
}
/**
* Loads a subset of the grid coverage represented by this resource.
*
* @param domain desired grid extent and resolution, or {@code null} for reading the whole domain.
* @param range 0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
* @return the grid coverage for the specified domain and range.
* @throws DataStoreException if an error occurred while reading the grid coverage data.
*/
@Override
public GridCoverage read(GridGeometry domain, final int... range) throws DataStoreException {
final RangeArgument rangeIndices = validateRangeArgument(ranges.length, range);
if (domain == null) {
domain = gridGeometry;
}
final Variable first = data[bandDimension >= 0 ? 0 : rangeIndices.getFirstSpecified()];
final DataType dataType = first.getDataType();
if (bandDimension < 0) {
for (int i=0; i<rangeIndices.getNumBands(); i++) {
final Variable variable = data[rangeIndices.getSourceIndex(i)];
if (!dataType.equals(variable.getDataType())) {
throw new DataStoreContentException(Resources.forLocale(getLocale()).getString(
Resources.Keys.MismatchedVariableType_3, getFilename(), first.getName(), variable.getName()));
}
}
}
/*
* At this point the arguments and the state of this resource have been validated.
* There is three ways to read the data, determined by `bandDimension` value:
*
* • (bandDimension < 0): one variable per band (usual case).
* • (bandDimension = 0): one variable containing all bands, with bands in the first dimension.
* • (bandDimension > 0): one variable containing all bands, with bands in the last dimension.
*/
final DataBuffer imageBuffer;
final SampleDimension[] bands = new SampleDimension[rangeIndices.getNumBands()];
int[] bandOffsets = null; // By default, all bands start at index 0.
try {
final GridDerivation targetGeometry = gridGeometry.derive()
.rounding(GridRoundingMode.ENCLOSING)
.subgrid(domain);
GridExtent areaOfInterest = targetGeometry.getIntersection(); // Pixel indices of data to read.
int[] subsamplings = targetGeometry.getSubsamplings(); // Slice to read or subsampling to apply.
int numBuffers = bands.length; // By default, one variable per band.
domain = targetGeometry.build(); // Adjust user-specified domain to data geometry.
if (bandDimension >= 0) {
areaOfInterest = rangeIndices.insertBandDimension(areaOfInterest, bandDimension);
subsamplings = rangeIndices.insertSubsampling (subsamplings, bandDimension);
if (bandDimension == 0) {
bandOffsets = new int[numBuffers]; // Will be set to non-zero values later.
}
numBuffers = 1; // One variable for all bands.
}
/*
* Iterate over netCDF variables in the order they appear in the file, not in the order requested
* by the 'range' argument. The intent is to perform sequential I/O as much as possible, without
* seeking backward. In the (uncommon) case where bands are one of the variable dimension instead
* than different variables, the reading of the whole variable occurs during the first iteration.
*/
Buffer[] sampleValues = new Buffer[numBuffers];
synchronized (lock) {
for (int i=0; i<bands.length; i++) {
int indexInResource = rangeIndices.getSourceIndex(i); // In strictly increasing order.
int indexInRaster = rangeIndices.getTargetIndex(i);
Variable variable = getVariable(indexInResource);
SampleDimension b = ranges[indexInResource];
if (b == null) {
ranges[indexInResource] = b = createSampleDimension(rangeIndices.builder(), variable, i);
}
bands[indexInRaster] = b;
if (bandOffsets != null) {
bandOffsets[indexInRaster] = i;
indexInRaster = 0; // Pixels interleaved in one bank: sampleValues.length = 1.
}
if (i < numBuffers) try {
// Optional.orElseThrow() below should never fail since Variable.read(…) wraps primitive array.
sampleValues[indexInRaster] = variable.read(areaOfInterest, subsamplings).buffer().get();
} catch (ArithmeticException e) {
throw variable.canNotComputePosition(e);
}
}
}
/*
* The following block is executed only if all bands are in a single variable, and the bands dimension is
* the last one (in "natural" order). In such case, the sample model to construct is a BandedSampleModel.
* Contrarily to PixelInterleavedSampleModel (the case when the band dimension is first), banded sample
* model force us to split the buffer in a buffer for each band.
*/
if (bandDimension > 0) { // Really > 0, not >= 0.
final int stride = Math.toIntExact(data[0].getBandStride());
Buffer values = sampleValues[0].limit(stride);
sampleValues = new Buffer[bands.length];
for (int i=0; i<sampleValues.length; i++) {
if (i != 0) {
values = JDK9.duplicate(values);
final int p = values.limit();
values.position(p).limit(Math.addExact(p, stride));
}
sampleValues[i] = values;
}
}
/*
* Convert NIO Buffer into Java2D DataBuffer. May throw various RuntimeException.
*/
imageBuffer = RasterFactory.wrap(dataType.rasterDataType, sampleValues);
} catch (IOException e) {
throw new DataStoreException(e);
} catch (TransformException e) {
throw new DataStoreReferencingException(e);
} catch (RuntimeException e) { // Many exceptions thrown by RasterFactory.wrap(…).
final Throwable cause = e.getCause();
if (cause instanceof TransformException) {
throw new DataStoreReferencingException(cause);
} else {
throw new DataStoreContentException(e);
}
}
if (imageBuffer == null) {
throw new DataStoreContentException(Errors.getResources(getLocale()).getString(Errors.Keys.UnsupportedType_1, dataType.name()));
}
return new Raster(domain, UnmodifiableArrayList.wrap(bands), imageBuffer,
rangeIndices.getPixelStride(), bandOffsets, String.valueOf(identifier));
}
/**
* Returns the name of the netCDF file. This is used for error messages.
*/
private String getFilename() {
if (location != null) {
return location.getFileName().toString();
} else {
return Vocabulary.getResources(getLocale()).getString(Vocabulary.Keys.Unnamed);
}
}
/**
* Gets the paths to files used by this resource, or an empty array if unknown.
*/
@Override
public Path[] getComponentFiles() {
return (location != null) ? new Path[] {location} : new Path[0];
}
}