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 641cbee Specifies the meaning of RenderedImage.getMinX()/getMinY()
resulting from a call to GridCoverage.render(…). Throw an exception if the
given extent is out of bounds. Fix ImageRenderer.setSampleStrid(int) method
contract.
641cbee is described below
commit 641cbeea81ad2db652033728bf4b0cfbb8603427
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Mar 23 18:12:26 2019 +0100
Specifies the meaning of RenderedImage.getMinX()/getMinY() resulting from a
call to GridCoverage.render(…).
Throw an exception if the given extent is out of bounds. Fix
ImageRenderer.setSampleStrid(int) method contract.
---
.../sis/coverage/grid/DisjointExtentException.java | 2 +-
.../org/apache/sis/coverage/grid/GridCoverage.java | 45 ++++---
.../apache/sis/coverage/grid/GridDerivation.java | 2 +
.../org/apache/sis/coverage/grid/GridExtent.java | 2 +-
.../apache/sis/coverage/grid/ImageRenderer.java | 144 +++++++++++++--------
.../org/apache/sis/internal/netcdf/Raster.java | 13 +-
6 files changed, 130 insertions(+), 78 deletions(-)
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/DisjointExtentException.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/DisjointExtentException.java
index 9860c05..dbfb587 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/DisjointExtentException.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/DisjointExtentException.java
@@ -21,7 +21,7 @@ import org.apache.sis.internal.raster.Resources;
/**
* Thrown when operations on a {@link GridGeometry} result in an area which
- * do not intersect anymore the {@link GridExtent} of the {@link GridGeometry}.
+ * does not intersect anymore the {@link GridExtent} of the {@link
GridGeometry}.
*
* @author Johann Sorel (Geomatys)
* @version 1.0
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index cd6998c..5a6b2fb 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -35,7 +35,6 @@ import org.apache.sis.util.Debug;
// Branch-specific imports
import org.opengis.coverage.CannotEvaluateException;
-import org.opengis.coverage.PointOutsideCoverageException;
/**
@@ -133,16 +132,15 @@ public abstract class GridCoverage {
* <i>(<var>z</var>,<var>t</var>)</i> coordinates of the desired slice.
Those coordinates are specified in a grid extent
* where {@linkplain GridExtent#getLow(int) low coordinate} = {@linkplain
GridExtent#getHigh(int) high coordinate} in the
* <var>z</var> and <var>t</var> dimensions. The two dimensions of the
data to be shown (<var>x</var> and <var>y</var>
- * in our example) shall be the only dimensions with a {@linkplain
GridExtent#getSize(int) size} greater than 1 cell.
+ * in our example) shall be the only dimensions having a {@linkplain
GridExtent#getSize(int) size} greater than 1 cell.
*
* <p>If the {@code sliceExtent} argument is {@code null}, then the
default value is
* <code>{@linkplain #getGridGeometry()}.{@linkplain
GridGeometry#getExtent() getExtent()}</code>.
* This means that {@code gridExtent} is optional for two-dimensional grid
coverages or grid coverages where all dimensions
* except two have a size of 1 cell. If the grid extent contains more than
2 dimensions with a size greater than one cell,
- * then a {@link SubspaceNotSpecifiedException} is thrown. If some {@code
sliceExtent} coordinates are outside the extent
- * of this grid coverage, then a {@link PointOutsideCoverageException} is
thrown.</p>
+ * then a {@link SubspaceNotSpecifiedException} is thrown.</p>
*
- * <div class="section">Computing a slice extent from a slice point in
"real world" coordinates</div>
+ * <div class="note"><p><b>How to compute a slice extent from a slice
point in "real world" coordinates</b></p>
* The {@code sliceExtent} is specified to this method as grid indices. If
the <var>z</var> and <var>t</var> values
* are not grid indices but are relative to some Coordinate Reference
System (CRS) instead, then the slice extent can
* be computed as below. First, a <cite>slice point</cite> containing the
<var>z</var> and <var>t</var> coordinates
@@ -158,19 +156,34 @@ public abstract class GridCoverage {
*
* <blockquote><code>sliceExtent = {@linkplain #getGridGeometry()}.{@link
GridGeometry#derive()
* derive()}.{@linkplain GridDerivation#slice(DirectPosition)
- * slice}(slicePoint).{@linkplain GridDerivation#build()
build()};</code></blockquote>
+ * slice}(slicePoint).{@linkplain GridDerivation#getIntersection()
getIntersection()};</code></blockquote>
*
* If the {@code slicePoint} CRS is different than this grid coverage CRS
(except for the number of dimensions),
- * a coordinate transformation will be applied as needed.
+ * a coordinate transformation will be applied as needed.</div>
+ *
+ * <div class="section">Characteristics of the returned image</div>
+ * Image dimensions <var>x</var> and <var>y</var> map to the first and
second dimension respectively of
+ * the two-dimensional {@code sliceExtent} {@linkplain
GridExtent#getSubspaceDimensions(int) subspace}.
+ * The coordinates given by {@link RenderedImage#getMinX()} and {@link
RenderedImage#getMinY() getMinY()}
+ * will be the image location <em>relative to</em> the location specified
in {@code sliceExtent}
+ * {@linkplain GridExtent#getLow(int) low coordinates}.
+ * For example in the case of image {@linkplain RenderedImage#getMinX()
minimum X coordinate}:
+ *
+ * <ul class="verbose">
+ * <li>A value of 0 means that the image left border is exactly where
requested by {@code sliceExtent.getLow(xDimension)}.</li>
+ * <li>A positive value means that the returned image is shifted to the
right compared to specified extent.
+ * This implies that the image has less data than requested on left
side.
+ * It may happen if the specified extent is partially outside grid
coverage extent.</li>
+ * <li>A negative value means that the returned image is shifted to the
left compared to specified extent.
+ * This implies that the image has more data than requested on left
side. It may happen if the image is tiled,
+ * the specified {@code sliceExtent} covers many tiles, and
expanding the specified extent is necessary
+ * for returning an integer amount of tiles.</li>
+ * </ul>
*
- * <div class="section">Rendered image properties</div>
+ * Similar discussion applies to the {@linkplain RenderedImage#getMinY()
minimum Y coordinate}.
* The {@linkplain RenderedImage#getWidth() image width} and {@linkplain
RenderedImage#getHeight() height} will be
- * the {@code sliceExtent} {@linkplain GridExtent#getSize(int) sizes} in
the first and second dimension respectively
- * of the two-dimensional {@code sliceExtent} {@linkplain
GridExtent#getSubspaceDimensions(int) subspace}.
- * The image location ({@linkplain RenderedImage#getMinX() x}, {@linkplain
RenderedImage#getMinY() y}) can be any point;
- * that location may not be the same as the {@code sliceExtent}
{@linkplain GridExtent#getLow(int) low} coordinates
- * since conversion from {@code long} to {@code int} primitive type may
cause lost of precision, and some implementations
- * like {@link java.awt.image.BufferedImage} restrict that location to
(0,0).
+ * the {@code sliceExtent} {@linkplain GridExtent#getSize(int) sizes} if
this method can honor exactly the request,
+ * or otherwise may be adjusted for the same reasons than <var>x</var> and
<var>y</var> location discussed above.
*
* <p>Implementations should return a view as much as possible, without
copying sample values.
* {@code GridCoverage} subclasses can use the {@link ImageRenderer} class
as a helper tool for that purpose.
@@ -179,9 +192,9 @@ public abstract class GridCoverage {
*
* @param sliceExtent a subspace of this grid coverage extent where all
dimensions except two have a size of 1 cell.
* May be {@code null} if this grid coverage has only two
dimensions with a size greater than 1 cell.
- * @return the grid slice as a rendered image.
- * @throws PointOutsideCoverageException if the given slice extent
contains illegal coordinates.
+ * @return the grid slice as a rendered image. Image location is relative
to {@code sliceExtent}.
* @throws SubspaceNotSpecifiedException if the given argument is not
sufficient for reducing the grid to a two-dimensional slice.
+ * @throws DisjointExtentException if the given extent does not intersect
this grid coverage.
* @throws CannotEvaluateException if this method can not produce the
rendered image for another reason.
*/
public abstract RenderedImage render(GridExtent sliceExtent) throws
CannotEvaluateException;
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
index 1521e52..c3eea61 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
@@ -803,6 +803,8 @@ public class GridDerivation {
* Builds a grid geometry with the configuration specified by the other
methods in this {@code GridDerivation} class.
*
* @return the modified grid geometry. May be the {@link #base} grid
geometry if no change apply.
+ *
+ * @see #getIntersection()
*/
public GridGeometry build() {
/*
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index 04af16a..4ebbe97 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -734,7 +734,7 @@ public class GridExtent implements Serializable {
* @param index index of the dimension as stored in this grid
extent.
* @param indexShown index to write in the message. Often the same as
{@code index}.
*/
- private Object getAxisIdentification(final int index, final int
indexShown) {
+ final Object getAxisIdentification(final int index, final int indexShown) {
if (types != null) {
final DimensionNameType type = types[index];
if (type != null) {
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 7263498..b46fdd3 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
@@ -19,8 +19,10 @@ package org.apache.sis.coverage.grid;
import java.util.Arrays;
import java.nio.Buffer;
import java.awt.Point;
+import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
+import java.awt.image.SampleModel;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
@@ -86,12 +88,24 @@ import org.apache.sis.math.Vector;
*/
public class ImageRenderer {
/**
- * Pixel coordinates of the image upper-left location.
- * This is often left to zero since {@link BufferedImage} has this
constraint.
+ * Location of the first image pixel relative to the grid coverage extent.
The (0,0) offset means that the first pixel
+ * in the {@code sliceExtent} (specified at construction time) is the
first pixel in the whole {@link GridCoverage}.
*
- * @see #setLocation(int, int)
+ * <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>
*/
- private int x, y;
+ private final long offsetX, offsetY;
+
+ /**
+ * Pixel coordinates of the image upper-left corner, as an offset relative
to the {@code sliceExtent}.
+ * This is initially zero (unless {@code sliceExtent} is partially outside
the grid coverage extent),
+ * but a different value may be used if the given data are tiled.
+ *
+ * @see RenderedImage#getMinX()
+ * @see RenderedImage#getMinY()
+ */
+ private final int imageX, imageY;
/**
* Width (number of pixels in a row) of the image to render.
@@ -112,6 +126,15 @@ 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.
+ */
+ private int sampleStride;
+
+ /**
* Number of data elements between two samples for the same band on the
same line.
* This is set to the product of {@linkplain GridExtent#getSize(int) grid
sizes} of enclosing
* {@code GridCoverage} in all dimensions before the dimension of image
{@linkplain #width}.
@@ -141,12 +164,6 @@ public class ImageRenderer {
private int[] bankIndices;
/**
- * Number of data elements from the first element of the bank to the first
sample of the band, or {@code null} for all 0.
- * If non-null, this array length must be equal to {@link #bands} array
length.
- */
- private final int[] bandOffsets;
-
- /**
* The band to be made visible (usually 0). All other bands, if any will
be ignored.
*/
private int visibleBand;
@@ -158,11 +175,12 @@ public class ImageRenderer {
private DataBuffer buffer;
/**
- * Creates a new image renderer for the given slice extent. The image will
have only one tile.
+ * Creates a new image renderer for the given slice extent.
*
* @param coverage the grid coverage for which to build an image.
* @param sliceExtent the grid geometry from which to create an image,
or {@code null} for the {@code coverage} extent.
* @throws SubspaceNotSpecifiedException if this method can not infer a
two-dimensional slice from {@code sliceExtent}.
+ * @throws DisjointExtentException if the given extent does not intersect
this grid coverage.
* @throws ArithmeticException if a stride calculation overflows the 32
bits integer capacity.
*/
public ImageRenderer(final GridCoverage coverage, GridExtent sliceExtent) {
@@ -179,19 +197,33 @@ public class ImageRenderer {
sliceExtent = source;
}
final int[] dimensions = sliceExtent.getSubspaceDimensions(2);
- int xd = dimensions[0];
- int yd = dimensions[1];
- long xo = sliceExtent.getLow(xd);
- long yo = sliceExtent.getLow(yd);
- width = Math.toIntExact(sliceExtent.getSize(xd));
- height = Math.toIntExact(sliceExtent.getSize(yd));
+ final int xd = dimensions[0];
+ final int yd = dimensions[1];
+ final long xcov = source.getLow(xd);
+ final long ycov = source.getLow(yd);
+ final long xreq = sliceExtent.getLow(xd);
+ final long yreq = sliceExtent.getLow(yd);
+ final long xmin = Math.max(xreq, xcov);
+ final long ymin = Math.max(yreq, ycov);
+ final long xmax = Math.min(sliceExtent.getHigh(xd),
source.getHigh(xd));
+ final long ymax = Math.min(sliceExtent.getHigh(yd),
source.getHigh(yd));
+ if (xmax <= xmin || ymax <= ymin) {
+ final int d = (xmax <= xmin) ? xd : yd;
+ throw new DisjointExtentException(source.getAxisIdentification(d,
d),
+ source.getLow(d), source.getHigh(d),
sliceExtent.getLow(d), sliceExtent.getHigh(d));
+ }
+ width = Math.incrementExact(Math.toIntExact(xmax - xmin));
+ height = Math.incrementExact(Math.toIntExact(ymax - ymin));
+ imageX = Math.toIntExact(Math.subtractExact(xreq, xmin));
+ imageY = Math.toIntExact(Math.subtractExact(yreq, ymin));
+ offsetX = Math.subtractExact(xmin, xcov);
+ offsetY = Math.subtractExact(ymin, ycov);
/*
- * After this point, xd and yd should be indices relative to source
extent.
- * For now we keep them unchanged on the assumption that the two grid
extents have the same dimensions.
+ * 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.
*/
- xo = Math.subtractExact(xo, source.getLow(xd));
- yo = Math.subtractExact(yo, source.getLow(yd));
- long pixelStride = 1;
+ this.sampleStride = 1;
+ long pixelStride = 1;
for (int i=0; i<xd; i++) {
pixelStride = Math.multiplyExact(pixelStride, source.getSize(i));
}
@@ -201,8 +233,6 @@ public class ImageRenderer {
}
this.pixelStride = Math.toIntExact(pixelStride);
this.scanlineStride = Math.toIntExact(scanlineStride);
- this.bandOffsets = new int[getNumBands()];
- Arrays.fill(bandOffsets, Math.toIntExact(xo + Math.multiplyExact(yo,
scanlineStride)));
}
/**
@@ -226,30 +256,12 @@ public class ImageRenderer {
}
/**
- * Sets the pixel coordinates of the upper-left corner of the rendered
image to create.
- * If this method is not invoked, then the default value is (0,0).
- * That default value is often suitable since {@link BufferedImage}
constraints that corner to (0,0).
+ * Returns the location of the image upper-left corner together with the
image size.
*
- * @param x the minimum <var>x</var> coordinate (inclusive) of the
rendered image.
- * @param y the minimum <var>y</var> coordinate (inclusive) of the
rendered image.
- *
- * @see RenderedImage#getMinX()
- * @see RenderedImage#getMinY()
+ * @return the rendered image location and size (never null).
*/
- public void setLocation(final int x, final int y) {
- this.x = x;
- this.y = y;
- }
-
- /**
- * Returns the location of the image upper-left corner.
- * This is the last value set by a call to {@link #setLocation(int, int)}.
- * The default value is (0,0).
- *
- * @return the image location (never null).
- */
- public final Point getLocation() {
- return new Point(x,y);
+ public final Rectangle getBounds() {
+ return new Rectangle(imageX, imageY, width, height);
}
/**
@@ -343,41 +355,59 @@ public class ImageRenderer {
}
/**
- * Applies a subsampling between pixels. Invoking this method multiplies
the <cite>pixel stride</cite>
- * by the given amount. This method is cumulative: invoking it many times
is equivalent to invoking it
- * once with the product of all argument values.
+ * 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...)}.
+ *
+ * @param stride the number of data elements between each sample values
in the data vectors.
+ * @throws ArithmeticException if the given stride is too large.
*
- * @param subsampling the subsampling between pixels in each row.
+ * @see java.awt.image.ComponentSampleModel#pixelStride
+ * @see java.awt.image.ComponentSampleModel#scanlineStride
*/
- public void subsampleX(final int subsampling) {
- ArgumentChecks.ensureStrictlyPositive("subsampling", subsampling);
- scanlineStride = Math.multiplyExact(scanlineStride, subsampling);
- pixelStride *= subsampling; // If above operation did not fail,
then this operation can not fail.
+ 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;
+ }
}
/**
* Creates a raster with the data specified by the last call to a {@code
setData(…)} method.
- * The raster upper-left corner is located at the position given by {@link
#getLocation()}.
+ * The raster upper-left corner is located at the position given by {@link
#getBounds()}.
*
* @return the raster.
* @throws IllegalStateException if no {@code setData(…)} method has been
invoked before this method call.
* @throws RasterFormatException if a call to a {@link WritableRaster}
factory method failed.
+ * @throws ArithmeticException if a property of the raster to construct
exceeds the capacity of 32 bits integers.
*/
public WritableRaster raster() {
if (buffer == null) {
throw new
IllegalStateException(Resources.format(Resources.Keys.UnspecifiedRasterData));
}
- final Point location = ((x | y) != 0) ? new Point(x,y) : null;
+ // 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))));
+
+ final Point location = new Point(imageX, imageY);
return RasterFactory.createRaster(buffer, width, height, pixelStride,
scanlineStride, bankIndices, bandOffsets, location);
}
/**
* Creates an image with the data specified by the last call to a {@code
setData(…)} method.
- * The image upper-left corner is located at the position given by {@link
#getLocation()}.
+ * The image upper-left corner is located at the position given by {@link
#getBounds()}.
*
* @return the image.
* @throws IllegalStateException if no {@code setData(…)} method has been
invoked before this method call.
* @throws RasterFormatException if a call to a {@link WritableRaster}
factory method failed.
+ * @throws ArithmeticException if a property of the image to construct
exceeds the capacity of 32 bits integers.
*/
public RenderedImage image() {
WritableRaster raster = raster();
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 cf29b44..7f5b979 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
@@ -29,7 +29,9 @@ import org.apache.sis.coverage.grid.ImageRenderer;
/**
- * Data loaded from a {@link RasterResource}.
+ * Data loaded from a {@link RasterResource} and potentially shown as {@link
RenderedImage}.
+ * The rendered image is usually mono-banded, but may be multi-banded in some
special cases
+ * handled by {@link RasterResource#read(GridGeometry, int...)}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
@@ -38,7 +40,12 @@ import org.apache.sis.coverage.grid.ImageRenderer;
*/
final class Raster extends GridCoverage {
/**
- * The sample values.
+ * The sample values, potentially multi-banded. If there is more than one
band to put in the rendered image,
+ * then each band is a {@linkplain DataBuffer#getNumBanks() separated
bank} in the buffer, even if two banks
+ * are actually wrapping the same arrays with different offsets.
+ *
+ * <div class="note">The later case is better represented by {@link
java.awt.image.PixelInterleavedSampleModel},
+ * but it is {@link ImageRenderer} responsibility to perform this
substitution as an optimization.</div>
*/
private final DataBuffer data;
@@ -73,7 +80,7 @@ final class Raster extends GridCoverage {
try {
final ImageRenderer renderer = new ImageRenderer(this, target);
renderer.setData(data);
- renderer.subsampleX(pixelStride);
+ renderer.setSampleStride(pixelStride);
return renderer.image();
} catch (IllegalArgumentException | ArithmeticException |
RasterFormatException e) {
throw new
CannotEvaluateException(Resources.format(Resources.Keys.CanNotRender_2, label,
e), e);