This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 556ca7b Consolidation of the way pixel interleaved sample models are
constructed for netCDF variables where one dimension is interpreted as bands.
https://issues.apache.org/jira/browse/SIS-449
556ca7b is described below
commit 556ca7bc4b7d533621f19f7240b385bc8a639e28
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Mar 26 00:33:39 2019 +0100
Consolidation of the way pixel interleaved sample models are constructed
for netCDF variables where one dimension is interpreted as bands.
https://issues.apache.org/jira/browse/SIS-449
---
.../java/org/apache/sis/coverage/CategoryList.java | 19 ++-
.../org/apache/sis/coverage/SampleDimension.java | 2 +-
.../apache/sis/coverage/grid/ImageRenderer.java | 130 ++++++++++++-----
.../sis/internal/raster/ColorModelFactory.java | 21 ++-
.../apache/sis/internal/raster/RasterFactory.java | 27 ++--
.../org/apache/sis/coverage/CategoryListTest.java | 12 +-
.../org/apache/sis/internal/netcdf/Raster.java | 15 +-
.../apache/sis/internal/netcdf/RasterResource.java | 161 +++++++++++++--------
.../org/apache/sis/internal/netcdf/Variable.java | 14 ++
.../sis/internal/storage/AbstractGridResource.java | 2 +-
.../sis/storage/DataStoreReferencingException.java | 10 +-
.../org/apache/sis/storage/WritableAggregate.java | 10 +-
12 files changed, 286 insertions(+), 137 deletions(-)
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
index 4242da6..37def06 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
@@ -159,13 +159,12 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
* The {@code categories} array should contain at least one element,
* otherwise the {@link #EMPTY} constant should be used.
*
- * @param categories the list of categories. May be empty, but can not
be null.
- * This array is not cloned and is modified in-place.
+ * @param categories the list of categories. This array is not cloned
and is modified in-place.
* @param converse if we are creating the list of categories after
conversion from samples to real values,
* the original list before conversion. Otherwise
{@code null}.
* @throws IllegalArgumentException if two or more categories have
overlapping sample value range.
*/
- CategoryList(final Category[] categories, CategoryList converse) {
+ private CategoryList(final Category[] categories, CategoryList converse) {
this.categories = categories;
final int count = categories.length;
/*
@@ -307,6 +306,20 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
/**
+ * Constructs a category list using the specified array of categories.
+ * The {@code categories} array should contain at least one element,
+ * otherwise the {@link #EMPTY} constant should be used.
+ *
+ * <p>This is defined as a static method for allowing the addition of a
caching mechanism in the future if desired.</p>
+ *
+ * @param categories the list of categories. This array is not cloned
and is modified in-place.
+ * @throws IllegalArgumentException if two or more categories have
overlapping sample value range.
+ */
+ static CategoryList create(final Category[] categories) {
+ return new CategoryList(categories, null);
+ }
+
+ /**
* Returns the <cite>transfer function</cite> from sample values to real
values, including conversion
* of "no data" values to NaNs. Callers shall ensure that there is at
least one quantitative category
* before to invoke this method.
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index 8d3e220..3f80221 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -168,7 +168,7 @@ public class SampleDimension implements Serializable {
if (categories.isEmpty()) {
list = CategoryList.EMPTY;
} else {
- list = new CategoryList(categories.toArray(new
Category[categories.size()]), null);
+ list = CategoryList.create(categories.toArray(new
Category[categories.size()]));
}
this.name = name;
this.background = background;
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
index b46fdd3..b505598 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -93,7 +93,7 @@ public class ImageRenderer {
*
* <div class="note"><b>Note:</b> if those offsets exceed 32 bits integer
capacity, then it may not be possible to build
* an image for given {@code sliceExtent} from a single {@link
DataBuffer}, because accessing sample values would exceed
- * the* capacity of index in Java arrays. In those cases the image needs
to be tiled.</div>
+ * the capacity of index in Java arrays. In those cases the image needs to
be tiled.</div>
*/
private final long offsetX, offsetY;
@@ -126,11 +126,19 @@ public class ImageRenderer {
private final int height;
/**
- * Number of data elements between two samples in the data {@link
#buffer}. This value is implicitly 1 in Java2D since
- * {@link java.awt.image} supports <cite>pixel stride</cite> and
<cite>scanline stride</cite> in {@link SampleModel},
- * but does not support stride at the {@link DataBuffer} level. This is
theoretically not needed since "sample stride"
- * can be represented as {@link #pixelStride}. We allow this concept for
the convenience of this builder, but at the
- * end this value is incorporated into the pixel stride.
+ * Number of data elements between two samples in the data {@link
#buffer}. A <cite>sample stride</cite> is defined
+ * in this class as the number of data elements between two samples in the
data {@link #buffer}. This is implicitly
+ * 1 in Java2D because {@link java.awt.image} supports <cite>pixel
stride</cite> and <cite>scanline stride</cite>
+ * in {@link SampleModel}, but does not support stride in {@link
DataBuffer} banks. This is theoretically not needed
+ * because "sample stride" can be represented as {@link #pixelStride}.
This is what we do in the end, but the concept
+ * of "sample stride" needs to exist temporarily in this builder before
the final pixel stride is computed.
+ *
+ * <div class="note"><b>Note:</b>
+ * this stride is <strong>not</strong> equivalent to applying a
subsampling on the image, because we do not divide
+ * the image width or height by the given stride. This field should be
used only for describing a particular layout
+ * of data in the buffers.</div>
+ *
+ * @see #setInterleavedPixelOffsets(int, int[])
*/
private int sampleStride;
@@ -141,7 +149,7 @@ public class ImageRenderer {
*
* @see java.awt.image.ComponentSampleModel#pixelStride
*/
- private int pixelStride;
+ private final int pixelStride;
/**
* Number of data elements between a given sample and the corresponding
sample in the same column of the next line.
@@ -150,7 +158,7 @@ public class ImageRenderer {
*
* @see java.awt.image.ComponentSampleModel#scanlineStride
*/
- private int scanlineStride;
+ private final int scanlineStride;
/**
* The sample dimensions, to be used for defining the bands.
@@ -158,6 +166,22 @@ public class ImageRenderer {
private final SampleDimension[] bands;
/**
+ * Offset to add to index of sample values in each band in order to reach
the value in the {@link DataBuffer} bank.
+ * This is closely related to {@link
java.awt.image.ComponentSampleModel#bandOffsets} but not identical, because of
+ * the following differences:
+ *
+ * <ul>
+ * <li>Another offset for {@link #offsetX} and {@link #offsetY} may need
to be added
+ * before to give the {@code bandOffsets} to {@link SampleModel}
constructor.</li>
+ * <li>If null, a default value is inferred depending on whether the
{@link SampleModel}
+ * to construct is banded or interleaved.</li>
+ * </ul>
+ *
+ * @see #setInterleavedPixelOffsets(int, int[])
+ */
+ private int[] bandOffsets;
+
+ /**
* Bank indices for each band, or {@code null} for 0, 1, 2, 3….
* If non-null, this array length must be equal to {@link #bands} array
length.
*/
@@ -222,7 +246,6 @@ public class ImageRenderer {
* At this point, the RenderedImage properties have been computed on
the assumption
* that the returned image will be a single tile. Now compute
SampleModel properties.
*/
- this.sampleStride = 1;
long pixelStride = 1;
for (int i=0; i<xd; i++) {
pixelStride = Math.multiplyExact(pixelStride, source.getSize(i));
@@ -247,11 +270,15 @@ public class ImageRenderer {
/**
* Ensures that the given number is equals to the expected number of bands.
+ * The given number shall be either 1 (case of interleaved sample model) or
+ * {@link #getNumBands()} (case of banded sample model).
*/
- private void ensureExpectedBandCount(final int n) {
- final int e = getNumBands();
- if (n != e) {
- throw new
MismatchedCoverageRangeException(Resources.format(Resources.Keys.UnexpectedNumberOfBands_2,
e, n));
+ private void ensureExpectedBandCount(final int n, final boolean acceptOne)
{
+ if (!(n == 1 & acceptOne)) {
+ final int e = getNumBands();
+ if (n != e) {
+ throw new
MismatchedCoverageRangeException(Resources.format(Resources.Keys.UnexpectedNumberOfBands_2,
e, n));
+ }
}
}
@@ -277,7 +304,7 @@ public class ImageRenderer {
*/
public void setData(final DataBuffer data) {
ArgumentChecks.ensureNonNull("data", data);
- ensureExpectedBandCount(data.getNumBanks());
+ ensureExpectedBandCount(data.getNumBanks(), true);
buffer = data;
}
@@ -310,7 +337,7 @@ public class ImageRenderer {
*/
public void setData(final int dataType, final Buffer... data) {
ArgumentChecks.ensureNonNull("data", data);
- ensureExpectedBandCount(data.length);
+ ensureExpectedBandCount(data.length, true);
final DataBuffer banks = RasterFactory.wrap(dataType, data);
if (banks == null) {
throw new
IllegalArgumentException(Resources.format(Resources.Keys.UnknownDataType_1,
dataType));
@@ -336,7 +363,7 @@ public class ImageRenderer {
*/
public void setData(final Vector... data) {
ArgumentChecks.ensureNonNull("data", data);
- ensureExpectedBandCount(data.length);
+ ensureExpectedBandCount(data.length, true);
final Buffer[] buffers = new Buffer[data.length];
int dataType = DataBuffer.TYPE_UNDEFINED;
for (int i=0; i<data.length; i++) {
@@ -355,26 +382,28 @@ public class ImageRenderer {
}
/**
- * Specifies the number of data elements between two samples in the
vectors specified by {@code setData(…)} methods.
- * The default value is 1. A value of 2 (for example) instructs {@code
ImageRenderer} to use the first value of the
- * given data vectors, skip a value, use the next value, <i>etc.</i> In
other words, this method applies a subsampling
- * on the vectors specified to {@link #setData(Vector...)} or {@link
#setData(int, Buffer...)}.
+ * Specifies the offsets to add to sample index in each band in order to
reach the sample value in the {@link DataBuffer} bank.
+ * This method should be invoked when the data given to {@code setData(…)}
contains only one {@link Vector}, {@link Buffer} or
+ * {@link DataBuffer} bank, and the bands in that unique bank are
interleaved.
*
- * @param stride the number of data elements between each sample values
in the data vectors.
- * @throws ArithmeticException if the given stride is too large.
+ * <div class="note"><b>Example:</b>
+ * for an image having three bands named Red (R), Green (G) and Blue (B),
if the sample values are stored in a single bank in a
+ * R₀,G₀,B₀, R₁,G₁,B₁, R₂,G₂,B₂, R₃,G₃,B₃, <i>etc.</i> fashion, then this
method should be invoked as below:
*
- * @see java.awt.image.ComponentSampleModel#pixelStride
- * @see java.awt.image.ComponentSampleModel#scanlineStride
+ * {@preformat java
+ * setInterleavedPixelOffsets(3, new int[] {0, 1, 2});
+ * }
+ * </div>
+ *
+ * @param pixelStride the number of data elements between each pixel in
the data vector or buffer.
+ * @param bandOffsets offsets to add to sample index in each band. This
is typically {0, 1, 2, …}.
*/
- public void setSampleStride(final int stride) {
- if (stride != sampleStride) {
- ArgumentChecks.ensureStrictlyPositive("stride", stride);
- // Division by 'dataStride' is for cancelling effect of previous
calls.
- scanlineStride = Math.multiplyExact(scanlineStride / sampleStride,
stride);
- // If above operation did not fail, then following operation can
not fail.
- pixelStride = (pixelStride / sampleStride) * stride;
- sampleStride = stride;
- }
+ public void setInterleavedPixelOffsets(final int pixelStride, final int[]
bandOffsets) {
+ ArgumentChecks.ensureStrictlyPositive("pixelStride", pixelStride);
+ ArgumentChecks.ensureNonNull("bandOffsets", bandOffsets);
+ ensureExpectedBandCount(bandOffsets.length, false);
+ this.sampleStride = pixelStride;
+ this.bandOffsets = bandOffsets.clone();
}
/**
@@ -390,14 +419,35 @@ public class ImageRenderer {
if (buffer == null) {
throw new
IllegalStateException(Resources.format(Resources.Keys.UnspecifiedRasterData));
}
- // Number of data elements from the first element of the bank to the
first sample of the band.
- final int[] bandOffsets = new int[getNumBands()];
- Arrays.fill(bandOffsets, Math.toIntExact(Math.addExact(
- Math.multiplyExact(offsetX, pixelStride),
- Math.multiplyExact(offsetY, scanlineStride))));
-
+ int ps = pixelStride;
+ int ls = scanlineStride;
+ if (bandOffsets != null) {
+ ls = Math.multiplyExact(ls, sampleStride);
+ ps *= sampleStride; // Can not fail if
above operation did not fail.
+ }
+ /*
+ * Number of data elements from the first element of the bank to the
first sample of the band.
+ * This is usually 0 for all bands, unless the upper-left corner
(minX, minY) is not (0,0).
+ */
+ final int[] offsets = new int[getNumBands()];
+ Arrays.fill(offsets, Math.toIntExact(Math.addExact(
+ Math.multiplyExact(offsetX, ps),
+ Math.multiplyExact(offsetY, ls))));
+ /*
+ * Add the offset specified by the user (if any), or the default
offset. The default is 0, 1, 2…
+ * for interleaved sample model (all bands in one bank) and 0, 0, 0…
for banded sample model.
+ */
+ if (bandOffsets != null) {
+ for (int i=0; i<offsets.length; i++) {
+ offsets[i] = Math.addExact(offsets[i], bandOffsets[i]);
+ }
+ } else if (buffer.getNumBanks() == 1) {
+ for (int i=1; i<offsets.length; i++) {
+ offsets[i] = Math.addExact(offsets[i], i);
+ }
+ }
final Point location = new Point(imageX, imageY);
- return RasterFactory.createRaster(buffer, width, height, pixelStride,
scanlineStride, bankIndices, bandOffsets, location);
+ return RasterFactory.createRaster(buffer, width, height, ps, ls,
bankIndices, offsets, location);
}
/**
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
index e6ce4b8..cfe9d19 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
@@ -231,16 +231,21 @@ public final class ColorModelFactory {
* Interpolates the colors in the color palette. Colors that do not
fall
* in the range of a category will be set to a transparent color.
*/
- final int[] colorMap = new int[pieceStarts[categoryCount]];
+ final int[] colorMap;
int transparent = -1;
- for (int i=0; i<categoryCount; i++) {
- final int[] colors = ARGB[i];
- final int lower = pieceStarts[i ];
- final int upper = pieceStarts[i+1];
- if (transparent < 0 && colors.length == 0) {
- transparent = lower;
+ if (categoryCount <= 0) {
+ colorMap = ArraysExt.range(0, 256);
+ } else {
+ colorMap = new int[pieceStarts[categoryCount]];
+ for (int i=0; i<categoryCount; i++) {
+ final int[] colors = ARGB[i];
+ final int lower = pieceStarts[i ];
+ final int upper = pieceStarts[i+1];
+ if (transparent < 0 && colors.length == 0) {
+ transparent = lower;
+ }
+ expand(colors, colorMap, lower, upper);
}
- expand(colors, colorMap, lower, upper);
}
return createIndexColorModel(colorMap, numBands, visibleBand,
transparent);
}
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
index a16822b..3d877d2 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
@@ -57,6 +57,13 @@ public final class RasterFactory extends Static {
/**
* Wraps the given data buffer in a raster.
* The sample model type is selected according the number of bands and the
pixel stride.
+ * The number of bands is determined by {@code bandOffsets.length}, which
should be one of followings:
+ *
+ * <ul>
+ * <li>For banded sample model, all {@code bandOffsets} can be zero.</li>
+ * <li>For interleaved sample model ({@code buffer.getNumBanks()} = 1),
each band needs a different offset.
+ * They may be 0, 1, 2, 3….</li>
+ * </ul>
*
* @param buffer buffer that contains the sample values.
* @param width raster width in pixels.
@@ -64,7 +71,7 @@ public final class RasterFactory extends Static {
* @param pixelStride number of data elements between two samples for
the same band on the same line.
* @param scanlineStride number of data elements between a given sample
and the corresponding sample in the same column of the next line.
* @param bankIndices bank indices for each band, or {@code null} for
0, 1, 2, 3….
- * @param bandOffsets number of data elements from the first element
of the bank to the first sample of the band, or {@code null} for all 0.
+ * @param bandOffsets number of data elements from the first element
of the bank to the first sample of the band.
* @param location the upper-left corner of the raster, or {@code
null} for (0,0).
* @return a raster built from given properties.
* @throws NullPointerException if {@code buffer} is {@code null}.
@@ -76,15 +83,12 @@ public final class RasterFactory extends Static {
@SuppressWarnings("fallthrough")
public static WritableRaster createRaster(final DataBuffer buffer,
final int width, final int height, final int pixelStride, final
int scanlineStride,
- int[] bankIndices, int[] bandOffsets, final Point location)
+ int[] bankIndices, final int[] bandOffsets, final Point location)
{
/*
* We do not verify the argument validity. Since this class is
internal, caller should have done verification
* itself. Furthermore those arguments are verified by WritableRaster
constructors anyway.
*/
- if (bandOffsets == null) {
- bandOffsets = new int[buffer.getNumBanks()];
- }
final int dataType = buffer.getDataType();
/*
* This SampleModel variable is a workaround for WritableRaster static
methods not supporting all data types.
@@ -96,9 +100,9 @@ public final class RasterFactory extends Static {
final SampleModel model;
if (buffer.getNumBanks() == 1 && (bankIndices == null ||
bankIndices[0] == 0)) {
/*
- * Sample data are stored for all bands in a single bank of the
DataBuffer.
- * Each sample of a pixel occupies one data element of the
DataBuffer.
- * The number of bands is inferred from bandOffsets.length.
+ * Sample data are stored for all bands in a single bank of the
DataBuffer, in an interleaved fashion.
+ * Each sample of a pixel occupies one data element of the
DataBuffer, with a different offset since
+ * the buffer beginning. The number of bands is inferred from
bandOffsets.length.
*/
switch (dataType) {
case DataBuffer.TYPE_BYTE:
@@ -119,13 +123,14 @@ public final class RasterFactory extends Static {
}
}
} else {
+ /*
+ * Sample data are stored in different banks (arrays) for each
band. If all pixels are consecutive (pixelStride = 1),
+ * we have the classical banded sample model. Otherwise the type
is not well identified; neither interleaved or banded.
+ */
if (bankIndices == null) {
bankIndices = ArraysExt.range(0, bandOffsets.length);
}
if (pixelStride == 1) {
- /*
- * All pixels are consecutive (pixelStride = 1) but may be on
many bands.
- */
switch (dataType) {
case DataBuffer.TYPE_BYTE:
case DataBuffer.TYPE_USHORT:
diff --git
a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
index 5cc8347..d702247 100644
---
a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
+++
b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
@@ -82,7 +82,7 @@ public final strictfp class CategoryListTest extends TestCase
{
new Category("Again", NumberRange.create(10, true, 10, true),
null, null, toNaN) // Range overlaps.
};
try {
- assertNotConverted(new CategoryList(categories.clone(), null));
+ assertNotConverted(CategoryList.create(categories.clone()));
fail("Should not have accepted range overlap.");
} catch (IllegalArgumentException exception) {
// This is the expected exception.
@@ -92,7 +92,7 @@ public final strictfp class CategoryListTest extends TestCase
{
}
// Removes the wrong category. Now, construction should succeed.
categories = Arrays.copyOf(categories, categories.length - 1);
- assertNotConverted(new CategoryList(categories, null));
+ assertNotConverted(CategoryList.create(categories));
assertSorted(Arrays.asList(categories));
}
@@ -182,7 +182,7 @@ public final strictfp class CategoryListTest extends
TestCase {
*/
@Test
public void testRanges() {
- final CategoryList list = new CategoryList(categories(), null);
+ final CategoryList list = CategoryList.create(categories());
assertSorted(list);
assertTrue ("isMinIncluded", list.range.isMinIncluded());
assertFalse ("isMaxIncluded", list.range.isMaxIncluded());
@@ -205,7 +205,7 @@ public final strictfp class CategoryListTest extends
TestCase {
@DependsOnMethod("testBinarySearch")
public void testSearch() {
final Category[] categories = categories();
- final CategoryList list = new CategoryList(categories.clone(), null);
+ final CategoryList list = CategoryList.create(categories.clone());
assertTrue("containsAll", list.containsAll(Arrays.asList(categories)));
/*
* Checks category searches for values that are insides the range of a
category.
@@ -253,7 +253,7 @@ public final strictfp class CategoryListTest extends
TestCase {
@DependsOnMethod("testSearch")
public void testTransform() throws TransformException {
final Random random = TestUtilities.createRandomNumberGenerator();
- final CategoryList list = new CategoryList(categories(), null);
+ final CategoryList list = CategoryList.create(categories());
/*
* Checks conversions. We verified in 'testSearch()' that correct
categories are found for those values.
*/
@@ -340,7 +340,7 @@ public final strictfp class CategoryListTest extends
TestCase {
for (int i=0; i<categories.length; i++) {
categories[i] = categories[i].converse;
}
- final CategoryList list = new CategoryList(categories, null);
+ final CategoryList list = CategoryList.create(categories);
assertSorted(list);
for (int i=list.size(); --i >= 0;) {
final Category category = list.get(i);
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
index 7f5b979..54fc45d 100644
---
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
+++
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
@@ -55,6 +55,12 @@ final class Raster extends GridCoverage {
private final int pixelStride;
/**
+ * Offsets to add to sample index in each band, or {@code null} if none.
+ * This is non-null only if a variable dimension is used for the bands.
+ */
+ private final int[] bandOffsets;
+
+ /**
* Name to display in error messages. Not to be used for processing.
*/
private final String label;
@@ -62,13 +68,14 @@ final class Raster extends GridCoverage {
/**
* Creates a new raster from the given resource.
*/
- Raster(final GridGeometry domain, final List<SampleDimension> range, final
DataBuffer data, final int pixelStride,
- final String label)
+ Raster(final GridGeometry domain, final List<SampleDimension> range, final
DataBuffer data,
+ final int pixelStride, final int[] bandOffsets, final String label)
{
super(domain, range);
this.data = data;
this.label = label;
this.pixelStride = pixelStride;
+ this.bandOffsets = bandOffsets;
}
/**
@@ -80,7 +87,9 @@ final class Raster extends GridCoverage {
try {
final ImageRenderer renderer = new ImageRenderer(this, target);
renderer.setData(data);
- renderer.setSampleStride(pixelStride);
+ if (bandOffsets != null) {
+ renderer.setInterleavedPixelOffsets(pixelStride, bandOffsets);
+ }
return renderer.image();
} catch (IllegalArgumentException | ArithmeticException |
RasterFormatException e) {
throw new
CannotEvaluateException(Resources.format(Resources.Keys.CanNotRender_2, label,
e), e);
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index bd8135d..96259f8 100644
---
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -18,7 +18,6 @@ package org.apache.sis.internal.netcdf;
import java.util.Map;
import java.util.List;
-import java.util.Arrays;
import java.util.ArrayList;
import java.io.IOException;
import java.nio.file.Path;
@@ -30,6 +29,7 @@ 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.raster.RasterFactory;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.grid.GridExtent;
@@ -111,16 +111,20 @@ public final class RasterResource extends
AbstractGridResource implements Resour
private final SampleDimension[] ranges;
/**
- * The netCDF variable wrapped by this resource. The length of this array
shall be equal to {@code ranges.length}.
- * The same variable may be repeated if it contains many bands, in which
case the bands are in dimension at index
- * {@link #bandDimension}.
+ * 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 the {@code
bandDimension} value:
+ * 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>
@@ -152,27 +156,22 @@ public final class RasterResource extends
AbstractGridResource implements Resour
* @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, or -1 for using {@code
bands.length}.
- * @param inner if one of {@link #data} dimension provides values for
different bands, that dimension index. Otherwise -1.
+ * @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 inner, final Object lock) throws
IOException, DataStoreException
+ final int numBands, final int bandDim, final Object lock)
{
super(decoder.listeners);
- data = bands.toArray(new Variable[numBands >= 0 ? numBands :
bands.size()]);
- for (int i=data.length; --i >= 0;) {
- if (data[i] != null) {
- Arrays.fill(data, i+1, data.length, data[i]);
// Repeat the last variable for all bands.
- break;
- }
- }
- ranges = new SampleDimension[data.length];
+ data = bands.toArray(new Variable[bands.size()]);
+ ranges = new SampleDimension[numBands];
identifier = decoder.nameFactory.createLocalName(decoder.namespace,
name);
location = decoder.location;
gridGeometry = grid;
- bandDimension = inner;
+ bandDimension = bandDim;
this.lock = lock;
+ assert data.length == (bandDimension >= 0 ? 1 : ranges.length);
}
/**
@@ -216,13 +215,19 @@ public final class RasterResource extends
AbstractGridResource implements Resour
final int gridDimension = grid.getDimension();
final int bandDimension, numBands;
if (dataDimension != gridDimension) {
- if (dataDimension != gridDimension + 1) {
- throw new
DataStoreContentException(Resources.forLocale(decoder.listeners.getLocale())
- .getString(Resources.Keys.UnmappedDimensions_4,
name, decoder.getFilename(), 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;
@@ -236,7 +241,6 @@ public final class RasterResource extends
AbstractGridResource implements Resour
* name is the same and the grid geometries are the same.
*/
bandDimension = -1;
// No dimension to be interpreted as bands.
- numBands = -1;
// To be determined by siblings.size().
final DataType type = variable.getDataType();
for (final String keyword : VECTOR_COMPONENT_NAMES) {
final int prefixLength = name.indexOf(keyword);
@@ -284,6 +288,7 @@ public final class RasterResource extends
AbstractGridResource implements Resour
}
}
}
+ numBands = siblings.size();
}
resources.add(new RasterResource(decoder, name.trim(), grid,
siblings, numBands, bandDimension, lock));
siblings.clear();
@@ -310,6 +315,14 @@ public final class RasterResource extends
AbstractGridResource implements Resour
}
/**
+ * 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".
@@ -324,7 +337,7 @@ public final class RasterResource extends
AbstractGridResource implements Resour
for (int i=0; i<ranges.length; i++) {
if (ranges[i] == null) {
if (builder == null) builder = new
SampleDimension.Builder();
- ranges[i] = createSampleDimension(builder, data[i]);
+ ranges[i] = createSampleDimension(builder,
getVariable(i), i);
builder.clear();
}
}
@@ -340,9 +353,12 @@ public final class RasterResource extends
AbstractGridResource implements Resour
*
* @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 TransformException if an error occurred while using the
transfer function.
*/
- private SampleDimension createSampleDimension(final
SampleDimension.Builder builder, final Variable band) throws TransformException
{
+ 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
@@ -427,7 +443,16 @@ public final class RasterResource extends
AbstractGridResource implements Resour
}
builder.addQualitative(name, n, n);
}
- return builder.setName(band.getName()).build();
+ /*
+ * 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();
}
/**
@@ -444,7 +469,7 @@ public final class RasterResource extends
AbstractGridResource implements Resour
if (domain == null) {
domain = gridGeometry;
}
- final Variable first = data[rangeIndices.getFirstSpecified()];
+ 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++) {
@@ -465,56 +490,70 @@ public final class RasterResource extends
AbstractGridResource implements Resour
*/
final DataBuffer imageBuffer;
final SampleDimension[] bands = new
SampleDimension[rangeIndices.getNumBands()];
+ int[] bandOffsets = null;
// By default, all bands start at index 0.
try {
- final Buffer[] sampleValues = new Buffer[bands.length];
- final GridDerivation targetGeometry =
gridGeometry.derive().subgrid(domain);
- GridExtent areaOfInterest = targetGeometry.getIntersection();
- final int[] scales = targetGeometry.getSubsamplings();
- int[] subsamplings = scales;
+ GridDerivation targetGeometry =
gridGeometry.derive().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.subsample(subsamplings).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.
+ * 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 values = null;
+ Buffer[] sampleValues = new Buffer[numBuffers];
synchronized (lock) {
for (int i=0; i<bands.length; i++) {
- final int r = rangeIndices.getSourceIndex(i);
// In strictly increasing order.
- final Variable variable = data[r];
- SampleDimension sd = ranges[r];
- if (sd == null) {
- ranges[r] = sd =
createSampleDimension(rangeIndices.builder(), variable);
+ 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);
}
- if (bandDimension > 0) {
- // TODO: adjust 'areaOfInterest'.
- throw new UnsupportedOperationException();
+ bands[indexInRaster] = b;
+ if (bandOffsets != null) {
+ bandOffsets[indexInRaster] = i;
+ indexInRaster = 0; // Pixels
interleaved in one bank: sampleValues.length = 1.
}
- if (bandDimension != 0 || values == null) {
+ if (i < numBuffers) {
// Optional.orElseThrow() below should never fail
since Variable.read(…) wraps primitive array.
- values = variable.read(areaOfInterest,
subsamplings).buffer().get();
+ sampleValues[indexInRaster] =
variable.read(areaOfInterest, subsamplings).buffer().get();
}
- if (bandDimension == 0) {
- /*
- * This block is executed only if the band dimension
is first, in which case we have interleaved
- * values like (band0, band1) for each pixel. Those
values were read once for all in above block.
- * This block sets the offset of the first value to
read, together with the buffer limit in order
- * to ensure that all buffers have the same number of
remaining elements. The pixel stride is not
- * specified here; it will be specified later, at
java.awt.image.SampleModel construction time.
- */
- if (i != 0) values = JDK9.duplicate(values);
- final int p = rangeIndices.getSubsampledIndex(i);
- values.position(p).limit(values.capacity() -
bands.length + p + 1);
+ }
+ }
+ /*
+ * 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));
}
- final int p = rangeIndices.getTargetIndex(i);
- sampleValues[p] = values;
- bands[p] = sd;
+ sampleValues[i] = values;
}
}
- domain = targetGeometry.subsample(scales).build();
+ /*
+ * Convert NIO Buffer into Java2D DataBuffer. May throw various
RuntimeException.
+ */
imageBuffer = RasterFactory.wrap(dataType.rasterDataType,
sampleValues);
} catch (IOException e) {
throw new DataStoreException(e);
@@ -524,13 +563,15 @@ public final class RasterResource extends
AbstractGridResource implements Resour
final Throwable cause = e.getCause();
if (cause instanceof TransformException) {
throw new DataStoreReferencingException(cause);
+ } else {
+ throw new DataStoreContentException(e);
}
- 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(), first.getStandardName());
+ return new Raster(domain, UnmodifiableArrayList.wrap(bands),
imageBuffer,
+ rangeIndices.getPixelStride(), bandOffsets,
first.getStandardName());
}
/**
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
index fb3e8ec..89eaca0 100644
---
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
+++
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
@@ -134,6 +134,7 @@ public abstract class Variable extends NamedElement {
* If {@link #gridGeometry} has less dimensions than this variable, index
of a grid dimension to take as raster bands.
* Otherwise this field is left uninitialized. If set, the index is
relative to "natural" order (reverse of netCDF order).
*
+ * @see #getBandStride()
* @see RasterResource#bandDimension
*/
int bandDimension;
@@ -736,6 +737,19 @@ public abstract class Variable extends NamedElement {
}
/**
+ * Returns the number of sample values between two bands.
+ * This method is meaningful only if {@link #bandDimension} ≧ 0.
+ */
+ final long getBandStride() throws IOException, DataStoreException {
+ long length = 1;
+ final GridExtent extent = getGridGeometry().getExtent();
+ for (int i=bandDimension; --i >= 0;) {
+ length = Math.multiplyExact(length, extent.getSize(i));
+ }
+ return length;
+ }
+
+ /**
* Returns the dimensions of this variable in the order they are declared
in the netCDF file.
* The dimensions are those of the grid, not the dimensions of the
coordinate system.
* In ISO 19123 terminology, {@link Dimension#length()} on each dimension
give the upper corner
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
index 39817aa..66f9ec9 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
@@ -227,7 +227,7 @@ public abstract class AbstractGridResource extends
AbstractResource implements G
* subsamplings = rangeIndices.insertSubsampling (subsamplings,
bandDimension);
* data = myReadMethod(areaOfInterest, subsamplings);
* for (int i=0; i<numBands; i++) {
- * int bandIndexInTheDataWeJustRead = getSubsampledIndex(i);
+ * int bandIndexInTheDataWeJustRead =
rangeIndices.getSubsampledIndex(i);
* }
* }
*
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreReferencingException.java
b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreReferencingException.java
index 7d7965a..bb23d6c 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreReferencingException.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreReferencingException.java
@@ -17,13 +17,14 @@
package org.apache.sis.storage;
import java.util.Locale;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.TransformException;
/**
* Thrown when a data store failed to construct the coordinate reference
system (CRS)
* or other positioning information. This exception is typically (but not
necessarily)
- * caused by {@link org.opengis.util.FactoryException} or
- * {@link org.opengis.referencing.operation.TransformException}.
+ * caused by {@link FactoryException} or {@link TransformException}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
@@ -47,6 +48,7 @@ public class DataStoreReferencingException extends
DataStoreException {
/**
* Creates an exception with the specified cause and no details message.
+ * The given cause should (but is not required to) be a {@link
FactoryException} or {@link TransformException}.
*
* @param cause the cause for this exception.
*/
@@ -56,6 +58,7 @@ public class DataStoreReferencingException extends
DataStoreException {
/**
* Creates an exception with the specified details message and cause.
+ * The given cause should (but is not required to) be a {@link
FactoryException} or {@link TransformException}.
*
* @param message the detail message.
* @param cause the cause for this exception.
@@ -69,6 +72,9 @@ public class DataStoreReferencingException extends
DataStoreException {
* Location in the file where the error occurred while be fetched from the
given {@code store}
* argument if possible. If the given store is not recognized, then it
will be ignored.
*
+ * <p>This constructor should be followed by a call to {@link
#initCause(Throwable)}
+ * with a {@link FactoryException} or {@link TransformException} cause.</p>
+ *
* @param locale the locale of the message to be returned by {@link
#getLocalizedMessage()}, or {@code null}.
* @param format short name or abbreviation of the data format (e.g.
"CSV", "GML", "WKT", <i>etc</i>).
* @param filename name of the file or data store where the error
occurred.
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableAggregate.java
b/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableAggregate.java
index 45b9d3a..e8a4caf 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableAggregate.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableAggregate.java
@@ -54,9 +54,15 @@ public interface WritableAggregate extends Aggregate {
/**
* Removes a {@code Resource} from this {@code Aggregate}.
- * This operation is destructive: the {@link Resource} and it's related
data will be removed.
+ * The given resource should be one of the instances returned by {@link
#components()}.
+ * This operation is destructive in two aspects:
*
- * @param resource child resource to remove, should not be null.
+ * <ul>
+ * <li>The {@link Resource} and it's data will be deleted from the
{@link DataStore}.</li>
+ * <li>The given resource may become invalid and should not be used
anymore after this method call.</li>
+ * </ul>
+ *
+ * @param resource child resource to remove from this {@code Aggregate}.
* @throws DataStoreException if the given resource could not be removed.
*/
void remove(Resource resource) throws DataStoreException;