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 c39230c Move RelocatedImage in the org.apache.sis.coverage.grid
package and make it more specific to GridCoverage2D purpose. In particular we
add the capability to produce a smaller image by retaining only the tiles
needed for the request.
c39230c is described below
commit c39230c3fae7a0d80a4679a8ade982e55dc6fbf7
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Dec 28 16:00:49 2019 +0100
Move RelocatedImage in the org.apache.sis.coverage.grid package and make it
more specific to GridCoverage2D purpose.
In particular we add the capability to produce a smaller image by retaining
only the tiles needed for the request.
---
.../apache/sis/coverage/grid/GridCoverage2D.java | 54 ++--
.../apache/sis/coverage/grid/ImageRenderer.java | 6 +-
.../apache/sis/coverage/grid/RelocatedImage.java | 287 +++++++++++++++++++++
.../java/org/apache/sis/image/ImageOperations.java | 60 -----
.../java/org/apache/sis/image/RelocatedImage.java | 246 ------------------
.../sis/internal/coverage/j2d/PlanarImage.java | 92 +++++--
.../grid}/RelocatedImageTest.java | 46 ++--
.../sis/internal/coverage/j2d/PlanarImageTest.java | 1 +
.../apache/sis/test/suite/FeatureTestSuite.java | 2 +-
9 files changed, 416 insertions(+), 378 deletions(-)
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
index 31a35dd..ad0e279 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -23,7 +23,6 @@ import java.text.NumberFormat;
import java.text.FieldPosition;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
@@ -36,7 +35,6 @@ import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.image.ImageOperations;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.internal.coverage.j2d.ConvertedGridCoverage;
import org.apache.sis.internal.feature.Resources;
@@ -49,6 +47,11 @@ import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.Debug;
+import static java.lang.Math.min;
+import static java.lang.Math.addExact;
+import static java.lang.Math.subtractExact;
+import static java.lang.Math.toIntExact;
+
// Branch-specific imports
import org.opengis.coverage.CannotEvaluateException;
import org.opengis.coverage.PointOutsideCoverageException;
@@ -184,8 +187,8 @@ public class GridCoverage2D extends GridCoverage {
}
xDimension = imageAxes[0];
yDimension = imageAxes[1];
- gridToImageX = Math.subtractExact(data.getMinX(),
extent.getLow(xDimension));
- gridToImageY = Math.subtractExact(data.getMinY(),
extent.getLow(yDimension));
+ gridToImageX = subtractExact(data.getMinX(),
extent.getLow(xDimension));
+ gridToImageY = subtractExact(data.getMinY(),
extent.getLow(yDimension));
/*
* Verifiy that the domain is consistent with image size.
* We do not verify image location; it can be anywhere.
@@ -443,8 +446,8 @@ public class GridCoverage2D extends GridCoverage {
try {
final FractionalGridCoordinates gc = toGridCoordinates(point);
try {
- final int x =
Math.toIntExact(Math.addExact(gc.getCoordinateValue(xDimension), gridToImageX));
- final int y =
Math.toIntExact(Math.addExact(gc.getCoordinateValue(yDimension), gridToImageY));
+ final int x =
toIntExact(addExact(gc.getCoordinateValue(xDimension), gridToImageX));
+ final int y =
toIntExact(addExact(gc.getCoordinateValue(yDimension), gridToImageY));
return evaluate(data, x, y, buffer);
} catch (ArithmeticException | IndexOutOfBoundsException |
DisjointExtentException ex) {
throw (PointOutsideCoverageException) new
PointOutsideCoverageException(
@@ -483,35 +486,37 @@ public class GridCoverage2D extends GridCoverage {
}
final GridExtent extent = gridGeometry.extent;
if (extent != null) {
- for (int i = Math.min(sliceExtent.getDimension(),
extent.getDimension()); --i >= 0;) {
+ for (int i = min(sliceExtent.getDimension(),
extent.getDimension()); --i >= 0;) {
if (i != xDimension && i != yDimension) {
- if (sliceExtent.getLow(i) < extent.getLow(i) ||
sliceExtent.getHigh(i) > extent.getHigh(i)) {
+ if (sliceExtent.getHigh(i) < extent.getLow(i) ||
sliceExtent.getLow(i) > extent.getHigh(i)) {
throw new DisjointExtentException(extent, sliceExtent,
i);
}
}
}
}
try {
- final Rectangle bounds = ImageUtilities.getBounds(data);
- final long x = Math.addExact(sliceExtent.getLow(xDimension),
gridToImageX);
- final long y = Math.addExact(sliceExtent.getLow(yDimension),
gridToImageY);
/*
- * The following code clamp values to 32 bits integers without
throwing ArithmeticException
- * because any value that overflow 32 bits are sure to be outside
the RenderedImage bounds.
- * In such case, clamping should not change the result.
+ * Convert the coordinates from this grid coverage coordinate
system to the image coordinate system.
+ * The coverage coordinates may require 64 bits integers, but
after translation the (x,y) coordinates
+ * should be in 32 bits integers range. Do not cast to 32 bits now
however; this will be done later.
*/
- final Rectangle request = bounds.intersection(new Rectangle(
- (int) Math.min(Integer.MAX_VALUE,
Math.max(Integer.MIN_VALUE, x)),
- (int) Math.min(Integer.MAX_VALUE,
Math.max(Integer.MIN_VALUE, y)),
- (int) Math.min(Integer.MAX_VALUE,
sliceExtent.getSize(xDimension)),
- (int) Math.min(Integer.MAX_VALUE,
sliceExtent.getSize(yDimension))));
+ final long xmin = addExact(sliceExtent.getLow (xDimension),
gridToImageX);
+ final long ymin = addExact(sliceExtent.getLow (yDimension),
gridToImageY);
+ final long xmax = addExact(sliceExtent.getHigh(xDimension),
gridToImageX);
+ final long ymax = addExact(sliceExtent.getHigh(yDimension),
gridToImageY);
/*
* BufferedImage.getSubimage() returns a new image with upper-left
coordinate at (0,0),
- * which is exactly what this method contract is requesting.
+ * which is exactly what this method contract is requesting
provided that the requested
+ * upper-left point is inside the image.
*/
if (data instanceof BufferedImage) {
- final BufferedImage image = (BufferedImage) data;
- return image.getSubimage(request.x, request.y, request.width,
request.height);
+ final long ix = data.getMinX();
+ final long iy = data.getMinY();
+ if (xmin >= ix && ymin >= iy) {
+ return ((BufferedImage)
data).getSubimage(toIntExact(xmin), toIntExact(ymin),
+ toIntExact(min(xmax + 1, ix + data.getWidth() -
1) - xmin),
+ toIntExact(min(ymax + 1, iy + data.getHeight() -
1) - ymin));
+ }
}
/*
* Return the backing image almost as-is (with potentially just a
wrapper) for avoiding to copy data.
@@ -519,9 +524,8 @@ public class GridCoverage2D extends GridCoverage {
* and actual region of the returned image. For example if the
user requested an image starting at
* (5,5) but the image to return starts at (1,1), then we need to
set its location to (-4,-4).
*/
- return ImageOperations.moveTo(data,
- Math.toIntExact(Math.subtractExact(bounds.x, x)),
- Math.toIntExact(Math.subtractExact(bounds.y, y)));
+ final RelocatedImage r = new RelocatedImage(data, xmin, ymin,
xmax, ymax);
+ return r.isIdentity() ? data : r;
} catch (ArithmeticException e) {
throw new CannotEvaluateException(e.getMessage(), e);
}
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
index 22eb573..30d43c9 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -208,10 +208,10 @@ public class ImageRenderer {
/**
* 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.
+ * @param coverage the source coverage for which to build an image.
+ * @param sliceExtent the domain 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 DisjointExtentException if the given extent does not intersect
the given coverage.
* @throws ArithmeticException if a stride calculation overflows the 32
bits integer capacity.
*/
public ImageRenderer(final GridCoverage coverage, GridExtent sliceExtent) {
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/RelocatedImage.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/RelocatedImage.java
new file mode 100644
index 0000000..3396556
--- /dev/null
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/RelocatedImage.java
@@ -0,0 +1,287 @@
+/*
+ * 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.coverage.grid;
+
+import java.util.Vector;
+import java.awt.Rectangle;
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import java.awt.image.SampleModel;
+import java.awt.image.ColorModel;
+import java.awt.image.WritableRaster;
+import org.apache.sis.internal.coverage.j2d.PlanarImage;
+
+import static java.lang.Math.min;
+import static java.lang.Math.max;
+import static java.lang.Math.addExact;
+import static java.lang.Math.subtractExact;
+import static java.lang.Math.floorDiv;
+import static java.lang.Math.toIntExact;
+
+
+/**
+ * A view over another image with the origin relocated to a new position.
+ * Only the pixel coordinates are changed; the tile indices stay the same.
+ * However the image view may expose less tiles than the wrapped image.
+ * This wrapper does not change image size otherwise than by an integer amount
of tiles.
+ *
+ * @author Johann Sorel (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class RelocatedImage extends PlanarImage {
+ /**
+ * The image to translate.
+ */
+ private final RenderedImage image;
+
+ /**
+ * Value to add for converting a column index from the coordinate system
of the wrapped image
+ * to the coordinate system of this image. For a conversion in opposite
direction, that value
+ * shall be subtracted.
+ */
+ private final int offsetX;
+
+ /**
+ * Value to add for converting a row index from the coordinate system of
the wrapped image to
+ * the coordinate system of this image. For a conversion in opposite
direction, that value
+ * shall be subtracted.
+ */
+ private final int offsetY;
+
+ /**
+ * The image size in pixels. May be smaller than {@link #image} size by an
integer amount of tiles.
+ */
+ private final int width, height;
+
+ /**
+ * Coordinate of the upper-left pixel.
+ * Computed at construction time in order to detect integer overflows
early.
+ */
+ private final int minX, minY;
+
+ /**
+ * Index in tile matrix of the upper-left tile.
+ * Computed at construction time in order to detect integer overflows
early.
+ */
+ private final int minTileX, minTileY;
+
+ /**
+ * Creates a new image with the same data than the given image but located
at different coordinates.
+ * In addition, this constructor can reduce the number of tiles.
+ *
+ * @param image the image to move.
+ * @param xmin minimal <var>x</var> coordinate of the requested region,
inclusive.
+ * @param ymin minimal <var>y</var> coordinate of the requested region,
inclusive.
+ * @param xmax maximal <var>x</var> coordinate of the requested region,
inclusive.
+ * @param ymax maximal <var>y</var> coordinate of the requested region,
inclusive.
+ * @throws ArithmeticException if image indices would overflow 32 bits
integer capacity.
+ */
+ RelocatedImage(final RenderedImage image, final long xmin, final long
ymin, final long xmax, final long ymax) {
+ this.image = image;
+ /*
+ * Compute indices of all tiles to retain in this image. All local
fields are `long` in order to force
+ * 64-bits integer arithmetic, because may have temporary 32-bits
integer overflow during intermediate
+ * calculation but still have a final result representable as an
`int`. The use of `min` and `max` are
+ * paranoiac safety against long integer overflow; real clamping will
be done later.
+ */
+ final long lowerX = image.getMinX(); // Lower
source index (inclusive)
+ final long lowerY = image.getMinY();
+ final long upperX = image.getWidth() + lowerX; // Upper
image index (exclusive).
+ final long upperY = image.getHeight() + lowerY;
+ final long tw = image.getTileWidth();
+ final long th = image.getTileHeight();
+ final long xo = image.getTileGridXOffset();
+ final long yo = image.getTileGridYOffset();
+ final long minTX = floorDiv(max(lowerX, xmin) - xo, tw); // Indices
of the first tile to retain.
+ final long minTY = floorDiv(max(lowerY, ymin) - yo, th);
+ final long maxTX = floorDiv(min(upperX, xmax) - xo, tw); // Indices
of the last tile to retain (inclusive).
+ final long maxTY = floorDiv(min(upperY, ymax) - yo, th);
+ /*
+ * Coordinates in source image of the first pixel to show in this
relocated image.
+ * They are the coordinates of the upper-left corner of the first tile
to retain,
+ * clamped to image bounds if needed. This is not yet coordinates of
this image.
+ */
+ final long sx = max(lowerX, minTX * tw + xo);
+ final long sy = max(lowerY, minTY * th + yo);
+ /*
+ * As per GridCoverage2D contract, we shall set the (x,y) location to
the difference between
+ * requested region and actual region of this image. For example if
the user requested image
+ * starting at (5,5) but the data starts at (1,1), then we need to set
location to (-4,-4).
+ */
+ final long x = subtractExact(sx, xmin);
+ final long y = subtractExact(sy, ymin);
+ minX = toIntExact(x);
+ minY = toIntExact(y);
+ width = toIntExact(min(upperX, (maxTX + 1) * tw + xo) - sx);
+ height = toIntExact(min(upperY, (maxTY + 1) * th + yo) - sy);
+ offsetX = toIntExact(x - lowerX);
+ offsetY = toIntExact(y - lowerY);
+ minTileX = toIntExact(minTX);
+ minTileY = toIntExact(minTY);
+ }
+
+ /**
+ * Returns {@code true} if this image does not move and does not subset
the wrapped image.
+ */
+ final boolean isIdentity() {
+ return offsetX == 0 && offsetY == 0 &&
+ minX == image.getMinX() && width == image.getWidth() &&
+ minY == image.getMinY() && height == image.getHeight();
+ }
+
+ /**
+ * Returns the immediate source of this image.
+ */
+ @Override
+ @SuppressWarnings("UseOfObsoleteCollectionType")
+ public Vector<RenderedImage> getSources() {
+ final Vector<RenderedImage> sources = new Vector<>(1);
+ sources.add(image);
+ return sources;
+ }
+
+ /**
+ * Delegates to the wrapped image with no change.
+ */
+ @Override public Object getProperty(String name) {return
image.getProperty(name);}
+ @Override public String[] getPropertyNames() {return
image.getPropertyNames();}
+ @Override public ColorModel getColorModel() {return
image.getColorModel();}
+ @Override public SampleModel getSampleModel() {return
image.getSampleModel();}
+ @Override public int getTileWidth() {return
image.getTileWidth();}
+ @Override public int getTileHeight() {return
image.getTileHeight();}
+
+ /**
+ * Returns properties determined at construction time.
+ */
+ @Override public int getMinX() {return minX;}
+ @Override public int getMinY() {return minY;}
+ @Override public int getWidth() {return width;}
+ @Override public int getHeight() {return height;}
+ @Override public int getMinTileX() {return minTileX;}
+ @Override public int getMinTileY() {return minTileY;}
+
+ /**
+ * Returns the <var>x</var> coordinate of the upper-left pixel of tile (0,
0).
+ * That tile (0, 0) may not actually exist.
+ */
+ @Override
+ public int getTileGridXOffset() {
+ return addExact(image.getTileGridXOffset(), offsetX);
+ }
+
+ /**
+ * Returns the <var>y</var> coordinate of the upper-left pixel of tile (0,
0).
+ * That tile (0, 0) may not actually exist.
+ */
+ @Override
+ public int getTileGridYOffset() {
+ return addExact(image.getTileGridYOffset(), offsetY);
+ }
+
+ /**
+ * Returns a raster with the same data than the given raster but with
coordinates translated
+ * from the coordinate system of the wrapped image to the coordinate
system of this image.
+ * The returned raster will have the given raster as its parent.
+ */
+ private Raster offset(final Raster data) {
+ return data.createTranslatedChild(addExact(data.getMinX(), offsetX),
+ addExact(data.getMinY(), offsetY));
+ }
+
+ /**
+ * Returns the tile at the given tile indices (not to be confused with
pixel indices).
+ *
+ * @param tileX the <var>x</var> index of the requested tile in the tile
array.
+ * @param tileY the <var>y</var> index of the requested tile in the tile
array.
+ * @return the tile specified by the specified indices.
+ */
+ @Override
+ public Raster getTile(final int tileX, final int tileY) {
+ return offset(image.getTile(tileX, tileY));
+ }
+
+ /**
+ * Returns a copy of this image as one large tile.
+ * The returned raster will not be updated if this image is changed.
+ *
+ * @return a copy of this image as one large tile.
+ */
+ @Override
+ public Raster getData() {
+ return offset(image.getData());
+ }
+
+ /**
+ * Returns a copy of an arbitrary region of this image.
+ * The returned raster will not be updated if this image is changed.
+ *
+ * @param aoi the region of this image to copy.
+ * @return a copy of this image in the given area of interest.
+ */
+ @Override
+ public Raster getData(Rectangle aoi) {
+ aoi = new Rectangle(aoi);
+ aoi.x = subtractExact(aoi.x, offsetX); // Convert coordinate from
this image to wrapped image.
+ aoi.y = subtractExact(aoi.y, offsetY);
+ final Raster data = image.getData(aoi);
+ return data.createTranslatedChild(addExact(data.getMinX(), offsetX),
+ addExact(data.getMinY(), offsetY));
+ }
+
+ /**
+ * Copies an arbitrary rectangular region of this image to the supplied
writable raster.
+ * The region to be copied is determined from the bounds of the supplied
raster.
+ *
+ * @param raster the raster to hold a copy of this image, or {@code
null}.
+ * @return the given raster if it was not-null, or a new raster otherwise.
+ */
+ @Override
+ public WritableRaster copyData(final WritableRaster raster) {
+ WritableRaster data;
+ if (raster != null) {
+ data = raster.createWritableTranslatedChild(
+ subtractExact(raster.getMinX(), offsetX),
+ subtractExact(raster.getMinY(), offsetY));
+ } else {
+ data = null;
+ }
+ data = image.copyData(data);
+ if (data.getWritableParent() == raster) {
+ return raster;
+ }
+ return data.createWritableTranslatedChild(addExact(data.getMinX(),
offsetX),
+ addExact(data.getMinY(),
offsetY));
+ }
+
+ /**
+ * Verifies whether image layout information are consistent.
+ */
+ @Override
+ public String verify() {
+ final String error = super.verify();
+ if (error == null) {
+ if (getMinX() != image.getMinX() + offsetX) return "minX";
+ if (getMinY() != image.getMinY() + offsetY) return "minY";
+ if (getTileGridXOffset() != super.getTileGridXOffset()) return
"tileGridXOffset";
+ if (getTileGridYOffset() != super.getTileGridYOffset()) return
"tileGridYOffset";
+ }
+ return error;
+ }
+}
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
b/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
deleted file mode 100644
index f8ae272..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.image;
-
-import java.awt.image.RenderedImage;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.Static;
-
-
-/**
- * Provides static methods working on images. Some of those methods create
cheap <em>views</em>
- * sharing the same pixels storage than the original image, while some other
methods may create
- * new tiles holding computation results. See the javadoc of each method for
details.
- *
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since 1.1
- * @module
- */
-public final class ImageOperations extends Static {
- /**
- * Do not allow instantiation of this class.
- */
- private ImageOperations() {
- }
-
- /**
- * Returns an image with the same data than the given image but located at
given coordinates.
- * The returned image is a <em>view</em>, i.e. this method does not copy
any pixel.
- * Changes in the original image are reflected immediately in the returned
image.
- * This method may return the given image directly if it is already
located at the given position.
- *
- * @param image the image to move.
- * @param minX new <var>x</var> coordinate of upper-left pixel.
- * @param minY new <var>y</var> coordinate of upper-left pixel.
- * @return image with the same data but at the given coordinates.
- */
- public static RenderedImage moveTo(final RenderedImage image, final int
minX, final int minY) {
- ArgumentChecks.ensureNonNull("image", image);
- if (minX == image.getMinX() && minY == image.getMinY()) {
- // Condition verified here for avoiding RelocatedImage class
loading when not needed.
- return image;
- }
- return RelocatedImage.moveTo(image, minX, minY);
- }
-}
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/RelocatedImage.java
b/core/sis-feature/src/main/java/org/apache/sis/image/RelocatedImage.java
deleted file mode 100644
index 0c3e805..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/image/RelocatedImage.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.image;
-
-import java.util.Vector;
-import java.awt.Rectangle;
-import java.awt.image.Raster;
-import java.awt.image.RenderedImage;
-import java.awt.image.SampleModel;
-import java.awt.image.ColorModel;
-import java.awt.image.WritableRaster;
-import org.apache.sis.internal.coverage.j2d.PlanarImage;
-
-
-/**
- * A view over another image with the origin relocated to a new position.
- * If the image is tiled, this wrapper may also reduce the number of tiles.
- * This wrapper does not change image size otherwise than by an integer amount
of tiles.
- *
- * @author Johann Sorel (Geomatys)
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since 1.1
- * @module
- */
-final class RelocatedImage extends PlanarImage {
- /**
- * The image to translate.
- */
- private final RenderedImage image;
-
- /**
- * Coordinate of the upper-left pixel.
- * Computed at construction time in order to detect integer overflows
early.
- */
- private final int minX, minY;
-
- /**
- * Creates a new image with the same data than the given image but located
at given coordinates.
- *
- * @param image the image to move.
- * @param minX <var>x</var> coordinate of upper-left pixel.
- * @param minY <var>y</var> coordinate of upper-left pixel.
- */
- private RelocatedImage(final RenderedImage image, final int minX, final
int minY) {
- this.image = image;
- this.minX = minX;
- this.minY = minY;
- }
-
- /**
- * Returns an image with the same data than the given image but located at
given coordinates.
- * Caller should verify that the given image is not null and not already
at the given location.
- *
- * @param image the image to move.
- * @param minX <var>x</var> coordinate of upper-left pixel.
- * @param minY <var>y</var> coordinate of upper-left pixel.
- * @return image with the same data but at the given coordinates.
- */
- static RenderedImage moveTo(RenderedImage image, final int minX, final int
minY) {
- if (image instanceof RelocatedImage) {
- image = (RelocatedImage) image;
- if (minX == image.getMinX() && minY == image.getMinY()) {
- return image;
- }
- }
- return new RelocatedImage(image, minX, minY);
- }
-
- /**
- * Returns the immediate source of this image.
- */
- @Override
- @SuppressWarnings("UseOfObsoleteCollectionType")
- public Vector<RenderedImage> getSources() {
- final Vector<RenderedImage> sources = new Vector<>(1);
- sources.add(image);
- return sources;
- }
-
- /**
- * Delegates to the wrapped image with no change.
- */
- @Override public Object getProperty(String name) {return
image.getProperty(name);}
- @Override public String[] getPropertyNames() {return
image.getPropertyNames();}
- @Override public ColorModel getColorModel() {return
image.getColorModel();}
- @Override public SampleModel getSampleModel() {return
image.getSampleModel();}
- @Override public int getWidth() {return
image.getWidth();}
- @Override public int getHeight() {return
image.getHeight();}
- @Override public int getNumXTiles() {return
image.getNumXTiles();}
- @Override public int getNumYTiles() {return
image.getNumYTiles();}
- @Override public int getMinTileX() {return
image.getMinTileX();}
- @Override public int getMinTileY() {return
image.getMinTileY();}
- @Override public int getTileWidth() {return
image.getTileWidth();}
- @Override public int getTileHeight() {return
image.getTileHeight();}
-
- /**
- * Returns the minimum <var>x</var> coordinate (inclusive) specified at
construction time.
- * This coordinate may differ from the coordinate of the wrapped image.
- */
- @Override
- public int getMinX() {
- return minX;
- }
-
- /**
- * Returns the minimum <var>y</var> coordinate (inclusive) specified at
construction time.
- * This coordinate may differ from the coordinate of the wrapped image.
- */
- @Override
- public int getMinY() {
- return minY;
- }
-
- /**
- * Returns the <var>x</var> coordinate of the upper-left pixel of tile (0,
0).
- * That tile (0, 0) may not actually exist.
- */
- @Override
- public int getTileGridXOffset() {
- return offsetX(image.getTileGridXOffset());
- }
-
- /**
- * Returns the <var>y</var> coordinate of the upper-left pixel of tile (0,
0).
- * That tile (0, 0) may not actually exist.
- */
- @Override
- public int getTileGridYOffset() {
- return offsetY(image.getTileGridYOffset());
- }
-
- /**
- * Converts a column index from the coordinate system of the wrapped image
- * to the coordinate system of this image.
- *
- * @param x a column index of the wrapped image.
- * @return the corresponding column index in this image.
- */
- private int offsetX(final int x) {
- return Math.toIntExact(x + (minX - (long) image.getMinX()));
- }
-
- /**
- * Converts a row index from the coordinate system of the wrapped image
- * to the coordinate system of this image.
- *
- * @param y a row index of the wrapped image.
- * @return the corresponding row index in this image.
- */
- private int offsetY(final int y) {
- return Math.toIntExact(y + (minY - (long) image.getMinY()));
- }
-
- /**
- * Returns a raster with the same data than the given raster but with
coordinates translated
- * from the coordinate system of the wrapped image to the coordinate
system of this image.
- * The returned raster will have the given raster as its parent.
- */
- private Raster offset(final Raster data) {
- return data.createTranslatedChild(offsetX(data.getMinX()),
offsetY(data.getMinY()));
- }
-
- /**
- * Returns the tile at the given tile indices (not to be confused with
pixel indices).
- *
- * @param tileX the <var>x</var> index of the requested tile in the tile
array.
- * @param tileY the <var>y</var> index of the requested tile in the tile
array.
- * @return the tile specified by the specified indices.
- */
- @Override
- public Raster getTile(final int tileX, final int tileY) {
- return offset(image.getTile(tileX, tileY));
- }
-
- /**
- * Returns a copy of this image as one large tile.
- * The returned raster will not be updated if this image is changed.
- *
- * @return a copy of this image as one large tile.
- */
- @Override
- public Raster getData() {
- return offset(image.getData());
- }
-
- /**
- * Returns a copy of an arbitrary region of this image.
- * The returned raster will not be updated if this image is changed.
- *
- * @param aoi the region of this image to copy.
- * @return a copy of this image in the given area of interest.
- */
- @Override
- public Raster getData(Rectangle aoi) {
- final long offsetX = minX - (long) image.getMinX();
- final long offsetY = minY - (long) image.getMinY();
- aoi = new Rectangle(aoi);
- aoi.x = Math.toIntExact(aoi.x - offsetX); // Inverse of
offsetX(int).
- aoi.y = Math.toIntExact(aoi.y - offsetY);
- final Raster data = image.getData(aoi);
- return data.createTranslatedChild(Math.toIntExact(data.getMinX() +
offsetX),
- Math.toIntExact(data.getMinY() +
offsetY));
- }
-
- /**
- * Copies an arbitrary rectangular region of this image to the supplied
writable raster.
- * The region to be copied is determined from the bounds of the supplied
raster.
- *
- * @param raster the raster to hold a copy of this image, or {@code
null}.
- * @return the given raster if it was not-null, or a new raster otherwise.
- */
- @Override
- public WritableRaster copyData(final WritableRaster raster) {
- final long offsetX = minX - (long) image.getMinX();
- final long offsetY = minY - (long) image.getMinY();
- WritableRaster data;
- if (raster != null) {
- data = raster.createWritableTranslatedChild(
- Math.toIntExact(raster.getMinX() - offsetX),
- Math.toIntExact(raster.getMinY() - offsetY));
- } else {
- data = null;
- }
- data = image.copyData(data);
- if (data.getWritableParent() == raster) {
- return raster;
- }
- return
data.createWritableTranslatedChild(Math.toIntExact(data.getMinX() + offsetX),
-
Math.toIntExact(data.getMinY() + offsetY));
- }
-}
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PlanarImage.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PlanarImage.java
index 1f27177..27e13dd 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PlanarImage.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PlanarImage.java
@@ -36,16 +36,31 @@ import org.apache.sis.util.Classes;
/**
* Base class of {@link RenderedImage} implementations in Apache SIS.
- * Current implementation does not hold any state.
+ * The "Planar" part in the class name emphases that this image is a
representation
+ * of two-dimensional data and should not represent three-dimensional effects.
+ * Planar images can be used as data storage for {@link
org.apache.sis.coverage.grid.GridCoverage2D}.
*
* <div class="note"><b>Note: inspirational source</b>
- * <p>This class takes some inspiration from the {@code
javax.media.jai.PlanarImage} class
- * defined in <cite>Java Advanced Imaging</cite> (JAI).
+ * <p>This class takes some inspiration from the {@code
javax.media.jai.PlanarImage}
+ * class defined in the <cite>Java Advanced Imaging</cite> (<abbr>JAI</abbr>)
library.
* That excellent library was maybe 20 years in advance over common imaging
frameworks,
* but unfortunately does not seems to be maintained anymore.
* We do not try to reproduce the full set of JAI functionalities here, but we
progressively
* reproduce some little bits of functionalities as they are needed by Apache
SIS.</p></div>
*
+ * <p>Subclasses need to implement the following methods:</p>
+ * <ul>
+ * <li>{@link #getMinX()} — the minimum <var>x</var> coordinate
(inclusive) of the image.</li>
+ * <li>{@link #getMinY()} — the minimum <var>y</var> coordinate
(inclusive) of the image.</li>
+ * <li>{@link #getWidth()} — the image width in pixels.</li>
+ * <li>{@link #getHeight()} — the image height in pixels.</li>
+ * <li>{@link #getMinTileX()} — the minimum tile index in the
<var>x</var> direction.</li>
+ * <li>{@link #getMinTileY()} — the minimum tile index in the
<var>y</var> direction.</li>
+ * <li>{@link #getTileWidth()} — the tile width in pixels.</li>
+ * <li>{@link #getTileHeight()} — the tile height in pixels.</li>
+ * <li>{@link #getTile(int,int)} — the tile at given tile indices.</li>
+ * </ul>
+ *
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
@@ -109,23 +124,31 @@ public abstract class PlanarImage implements
RenderedImage {
}
/**
- * Returns the number of tiles in the X direction.
+ * Returns the number of tiles in the <var>x</var> direction.
*
- * <p>The default implementation computes this value from {@link
#getWidth()} and {@link #getTileWidth()}.</p>
+ * <p>The default implementation computes this value from {@link
#getWidth()} and {@link #getTileWidth()}
+ * on the assumption that {@link #getMinX()} is the coordinate of the
leftmost pixels of tiles located at
+ * {@link #getMinTileX()} index. This assumption can be verified by {@link
#verify()}.</p>
*
- * @return returns the number of tiles in the X direction.
+ * @return returns the number of tiles in the <var>x</var> direction.
*/
@Override
public int getNumXTiles() {
+ /*
+ * If assumption documented in javadoc does not hold, the calculation
performed here would need to be
+ * more complicated: compute tile index of minX, compute tile index of
maxX, return difference plus 1.
+ */
return Numerics.ceilDiv(getWidth(), getTileWidth());
}
/**
- * Returns the number of tiles in the Y direction.
+ * Returns the number of tiles in the <var>y</var> direction.
*
- * <p>The default implementation computes this value from {@link
#getHeight()} and {@link #getTileHeight()}.</p>
+ * <p>The default implementation computes this value from {@link
#getHeight()} and {@link #getTileHeight()}
+ * on the assumption that {@link #getMinY()} is the coordinate of the
uppermost pixels of tiles located at
+ * {@link #getMinTileY()} index. This assumption can be verified by {@link
#verify()}.</p>
*
- * @return returns the number of tiles in the Y direction.
+ * @return returns the number of tiles in the <var>y</var> direction.
*/
@Override
public int getNumYTiles() {
@@ -133,31 +156,32 @@ public abstract class PlanarImage implements
RenderedImage {
}
/**
- * Returns the X coordinate of the upper-left pixel of tile (0, 0).
+ * Returns the <var>x</var> coordinate of the upper-left pixel of tile (0,
0).
* That tile (0, 0) may not actually exist.
*
* <p>The default implementation computes this value from {@link
#getMinX()},
* {@link #getMinTileX()} and {@link #getTileWidth()}.</p>
*
- * @return the X offset of the tile grid relative to the origin.
+ * @return the <var>x</var> offset of the tile grid relative to the origin.
*/
@Override
public int getTileGridXOffset() {
- return Math.subtractExact(getMinX(), Math.multiplyExact(getMinTileX(),
getTileWidth()));
+ // We may have temporary `int` overflow after multiplication but exact
result after addition.
+ return Math.toIntExact(getMinX() - getMinTileX() * ((long)
getTileWidth()));
}
/**
- * Returns the Y coordinate of the upper-left pixel of tile (0, 0).
+ * Returns the <var>y</var> coordinate of the upper-left pixel of tile (0,
0).
* That tile (0, 0) may not actually exist.
*
* <p>The default implementation computes this value from {@link
#getMinY()},
* {@link #getMinTileY()} and {@link #getTileHeight()}.</p>
*
- * @return the Y offset of the tile grid relative to the origin.
+ * @return the <var>y</var> offset of the tile grid relative to the origin.
*/
@Override
public int getTileGridYOffset() {
- return Math.subtractExact(getMinY(), Math.multiplyExact(getMinTileY(),
getTileHeight()));
+ return Math.toIntExact(getMinY() - getMinTileY() * ((long)
getTileHeight()));
}
/**
@@ -293,8 +317,38 @@ public abstract class PlanarImage implements RenderedImage
{
}
/**
+ * Verifies whether image layout information are consistent. This method
verifies that the coordinates
+ * of image upper-left corner are equal to the coordinates of the
upper-left corner of the tile in the
+ * upper-left corner, and that image size is equal to the sum of the sizes
of all tiles. Compatibility
+ * of sample model and color model is also verified.
+ *
+ * @return {@code null} if image layout information are consistent, or
name of inconsistent property
+ * if a problem is found.
+ */
+ public String verify() {
+ final int tileWidth = getTileWidth();
+ final int tileHeight = getTileHeight();
+ final SampleModel sm = getSampleModel();
+ if (sm != null) {
+ if (sm.getWidth() != tileWidth) return "tileWidth";
+ if (sm.getHeight() != tileHeight) return "tileHeight";
+ final ColorModel cm = getColorModel();
+ if (cm != null) {
+ if (!cm.isCompatibleSampleModel(sm)) return "SampleModel";
+ }
+ }
+ if (((long) getMinTileX()) * tileWidth + getTileGridXOffset() !=
getMinX()) return "tileX";
+ if (((long) getMinTileY()) * tileHeight + getTileGridYOffset() !=
getMinY()) return "tileY";
+ if (((long) getNumXTiles()) * tileWidth != getWidth()) return
"numXTiles";
+ if (((long) getNumYTiles()) * tileHeight != getHeight()) return
"numYTiles";
+ return null;
+ }
+
+ /**
* Returns a string representation of this image for debugging purpose.
* This string representation may change in any future SIS version.
+ *
+ * @return a string representation of this image for debugging purpose
only.
*/
@Override
public String toString() {
@@ -338,14 +392,18 @@ colors: if (cm != null) {
buffer.append("; ").append(transparency);
}
/*
- * Tiling information last because it is usually a secondary aspect
compared
- * to above information.
+ * Tiling information last because it is usually a secondary aspect
compared to above information.
+ * If a warning is emitted, it will usually be a tiling problem so it
is useful to keep it close.
*/
final int tx = getNumXTiles();
final int ty = getNumYTiles();
if (tx != 1 || ty != 1) {
buffer.append("; ").append(tx).append(" × ").append(ty).append("
tiles");
}
+ final String error = verify();
+ if (error != null) {
+ buffer.append("; ⚠ mismatched ").append(error);
+ }
return buffer.append(']').toString();
}
}
diff --git
a/core/sis-feature/src/test/java/org/apache/sis/image/RelocatedImageTest.java
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/RelocatedImageTest.java
similarity index 52%
rename from
core/sis-feature/src/test/java/org/apache/sis/image/RelocatedImageTest.java
rename to
core/sis-feature/src/test/java/org/apache/sis/coverage/grid/RelocatedImageTest.java
index cbdefd1..a5dfac3 100644
---
a/core/sis-feature/src/test/java/org/apache/sis/image/RelocatedImageTest.java
+++
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/RelocatedImageTest.java
@@ -14,51 +14,45 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.image;
+package org.apache.sis.coverage.grid;
-import java.awt.Point;
import java.awt.image.BufferedImage;
-import java.awt.image.RenderedImage;
-import org.opengis.coverage.grid.SequenceType;
+import java.awt.image.WritableRaster;
import org.apache.sis.test.TestCase;
import org.junit.Test;
import static org.junit.Assert.*;
+import static org.apache.sis.test.FeatureAssert.assertValuesEqual;
/**
* Tests the {@link RelocatedImage} implementation.
*
* @author Johann Sorel (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 1.1
* @module
*/
public final strictfp class RelocatedImageTest extends TestCase {
-
+ /**
+ * Tests with a request starting on the left and on top of data.
+ */
@Test
- public void iteratorTest() {
+ public void testRequestBefore() {
final BufferedImage image = new BufferedImage(2, 2,
BufferedImage.TYPE_BYTE_GRAY);
- image.getRaster().setSample(0, 0, 0, 1);
- image.getRaster().setSample(1, 0, 0, 2);
- image.getRaster().setSample(0, 1, 0, 3);
- image.getRaster().setSample(1, 1, 0, 4);
-
- final RenderedImage trs = RelocatedImage.moveTo(image, -10, -20);
+ final WritableRaster raster = image.getRaster();
+ raster.setSample(0, 0, 0, 1);
+ raster.setSample(1, 0, 0, 2);
+ raster.setSample(0, 1, 0, 3);
+ raster.setSample(1, 1, 0, 4);
- final PixelIterator ite = new
PixelIterator.Builder().setIteratorOrder(SequenceType.LINEAR).create(trs);
- assertTrue(ite.next());
- assertEquals(new Point(-10, -20), ite.getPosition());
- assertEquals(1, ite.getSample(0));
- assertTrue(ite.next());
- assertEquals(new Point(-9, -20), ite.getPosition());
- assertEquals(2, ite.getSample(0));
- assertTrue(ite.next());
- assertEquals(new Point(-10, -19), ite.getPosition());
- assertEquals(3, ite.getSample(0));
- assertTrue(ite.next());
- assertEquals(new Point(-9, -19), ite.getPosition());
- assertEquals(4, ite.getSample(0));
- assertFalse(ite.next());
+ final RelocatedImage trs = new RelocatedImage(image, -1, -2, 4, 4);
+ assertEquals(1, trs.getMinX());
+ assertEquals(2, trs.getMinY());
+ assertValuesEqual(trs.getData(), 0, new int[][] {
+ {1, 2},
+ {3, 4}
+ });
}
}
diff --git
a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/PlanarImageTest.java
b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/PlanarImageTest.java
index 6b99f49..b5d8c89 100644
---
a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/PlanarImageTest.java
+++
b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/PlanarImageTest.java
@@ -108,6 +108,7 @@ public final strictfp class PlanarImageTest extends
TestCase {
}
}
assertEquals(tiles.length, i);
+ assertNull(verify());
}
/**
diff --git
a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 48c1e07..f7af982 100644
---
a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++
b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -76,7 +76,6 @@ import org.junit.runners.Suite;
// Rasters
org.apache.sis.image.DefaultIteratorTest.class,
org.apache.sis.image.LinearIteratorTest.class,
- org.apache.sis.image.RelocatedImageTest.class,
org.apache.sis.coverage.CategoryTest.class,
org.apache.sis.coverage.CategoryListTest.class,
org.apache.sis.coverage.SampleDimensionTest.class,
@@ -86,6 +85,7 @@ import org.junit.runners.Suite;
org.apache.sis.coverage.grid.GridGeometryTest.class,
org.apache.sis.coverage.grid.GridDerivationTest.class,
org.apache.sis.coverage.grid.FractionalGridCoordinates.class,
+ org.apache.sis.coverage.grid.RelocatedImageTest.class,
org.apache.sis.coverage.grid.GridCoverage2DTest.class,
org.apache.sis.internal.coverage.j2d.ImageUtilitiesTest.class,
org.apache.sis.internal.coverage.j2d.ScaledColorSpaceTest.class,