This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit d40d2807255a170ea99724ed887c8a1336f2d8c8 Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Jul 31 18:18:43 2026 +0200 Add test case for `RenderingData`. --- .../sis/image/internal/shared/ReshapedImage.java | 115 ++++++++-- .../org/apache/sis/map/coverage/RenderingData.java | 24 +- .../apache/sis/map/coverage/RenderingDataTest.java | 251 +++++++++++++++++++++ 3 files changed, 356 insertions(+), 34 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java index 6c1469f3f0..d40d989d10 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java @@ -19,6 +19,7 @@ package org.apache.sis.image.internal.shared; import java.util.Vector; import java.util.Objects; import java.awt.Rectangle; +import java.awt.Image; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; @@ -31,7 +32,12 @@ import static java.lang.Math.subtractExact; import static java.lang.Math.multiplyFull; import static java.lang.Math.floorDiv; import static java.lang.Math.toIntExact; +import org.opengis.referencing.operation.TransformException; +import org.apache.sis.util.logging.Logging; +import org.apache.sis.coverage.grid.GridExtent; +import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.image.PlanarImage; +import static org.apache.sis.image.ResampledImage.POSITIONAL_CONSISTENCY_KEY; /** @@ -82,7 +88,8 @@ public final class ReshapedImage extends PlanarImage { private final int minTileX, minTileY; /** - * Creates an image with the following properties. + * Creates an image with the given properties. + * This constructor is invoked by the static factory methods. * * @throws ArithmeticException if image indices overflow 32-bits integer capacity. */ @@ -94,6 +101,9 @@ public final class ReshapedImage extends PlanarImage { { while (source instanceof ReshapedImage) { final var r = (ReshapedImage) source; + if ((offsetX | offsetY) == 0 && width >= r.width && height >= r.height) { + break; // Will be simplified by the caller through `simplify()`. + } offsetX = addExact(offsetX, r.offsetX); offsetY = addExact(offsetY, r.offsetY); source = r.source; @@ -123,15 +133,14 @@ public final class ReshapedImage extends PlanarImage { public static RenderedImage singleTile(final RenderedImage source, final int tileX, final int tileY) { final int tileWidth = source.getTileWidth(); final int tileHeight = source.getTileHeight(); - final var image = new ReshapedImage(source, + return new ReshapedImage(source, -(multiplyFull(tileX, tileWidth) + source.getTileGridXOffset()), // This negate cannot overflow. -(multiplyFull(tileY, tileHeight) + source.getTileGridYOffset()), 0, 0, tileWidth, tileHeight, tileX, - tileY); - return image.isIdentity() ? image.source : image; + tileY).simplify(); } /** @@ -148,7 +157,7 @@ public final class ReshapedImage extends PlanarImage { if ((offsetX | offsetY) == 0) { return source; } - final var image = new ReshapedImage( + return new ReshapedImage( source, offsetX, offsetY, @@ -157,8 +166,7 @@ public final class ReshapedImage extends PlanarImage { source.getWidth(), source.getHeight(), source.getMinTileX(), - source.getMinTileY()); - return image.isIdentity() ? image.source : image; + source.getMinTileY()).simplify(); } /** @@ -173,7 +181,10 @@ public final class ReshapedImage extends PlanarImage { * @return the relocated image. May be the given source or one of its sources. * @throws ArithmeticException if image indices would overflow 32-bits integer capacity. */ - public static RenderedImage relocate(final RenderedImage source, final long xmin, final long ymin, final long xmax, final long ymax) { + public static RenderedImage relocate(final RenderedImage source, + final long xmin, final long ymin, + final long xmax, final long ymax) + { /* * 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 @@ -206,27 +217,41 @@ public final class ReshapedImage extends PlanarImage { */ final long x = subtractExact(sx, xmin); final long y = subtractExact(sy, ymin); - final var image = new ReshapedImage(source, + return new ReshapedImage( + source, x - sx, y - sy, x, y, toIntExact(min(upperX, (maxTX + 1) * tw + xo) - sx), toIntExact(min(upperY, (maxTY + 1) * th + yo) - sy), toIntExact(minTX), - toIntExact(minTY)); - return image.isIdentity() ? image.source : image; + toIntExact(minTY)).simplify(); + } + + /** + * Applies to the given image the same translation and clip as this image. + * This is used for auxiliary images such as {@value #MASK_KEY} and {@value #POSITIONAL_ACCURACY_KEY}. + */ + private RenderedImage applySameChanges(final RenderedImage other) { + final Rectangle r = getBounds(); + ImageUtilities.clipBounds(other, r); + return new ReshapedImage( + source, offsetX, offsetY, + r.x, r.y, r.width, r.height, + ImageUtilities.pixelToTileX(other, r.x), + ImageUtilities.pixelToTileY(other, r.y)).simplify(); } /** - * Returns {@code true} if this image does not move and does not subset the wrapped image. + * Returns {@code source} if this image does not move and does not subset the wrapped image. * This is tested after construction in case that the result of unwrapping the source produces * an identity operation. * - * @return whether this image does not move and does not subset the wrapped image. + * @return a simplified version of this image, if possible. */ - private boolean isIdentity() { + private RenderedImage simplify() { // The use of >= is a paranoiac check, but the > case should never happen actually. - return offsetX == 0 && offsetY == 0 && width >= source.getWidth() && height >= source.getHeight(); + return (offsetX | offsetY) == 0 && width >= source.getWidth() && height >= source.getHeight() ? source : this; } /** @@ -235,23 +260,65 @@ public final class ReshapedImage extends PlanarImage { @Override @SuppressWarnings("UseOfObsoleteCollectionType") public Vector<RenderedImage> getSources() { - final Vector<RenderedImage> sources = new Vector<>(1); + final var sources = new Vector<RenderedImage>(1); sources.add(source); return sources; } /** - * Delegates to the wrapped image with no change. + * Returns the name of all supported properties, or {@code null} if none. + * Current implementation inherits all properties declared by the source, + * but some of them will need a translation. + */ + @Override + public String[] getPropertyNames() { + return source.getPropertyNames(); + } + + /** + * Returns the property of the given name. + * The property is inherited from the source image, + * potentially with a translation of pixel coordinates. * * @param name name of the property to get. * @return property value for the given name. */ - @Override public Object getProperty(String name) {return source.getProperty(name);} - @Override public String[] getPropertyNames() {return source.getPropertyNames();} - @Override public ColorModel getColorModel() {return source.getColorModel();} - @Override public SampleModel getSampleModel() {return source.getSampleModel();} - @Override public int getTileWidth() {return source.getTileWidth();} - @Override public int getTileHeight() {return source.getTileHeight();} + @Override + public Object getProperty(final String name) { + final Object property = source.getProperty(name); + switch (name) { + default: { + return property; + } + case GRID_GEOMETRY_KEY: { + if (property instanceof GridGeometry) try { + final var base = (GridGeometry) property; + return base.shiftGrid(offsetX, offsetY).relocate(new GridExtent(getBounds())); + } catch (TransformException e) { + // In most of Apache SIS, caller has a fallback for the case when this property is missing. + Logging.recoverableException(ImageUtilities.LOGGER, ReshapedImage.class, "getProperty", e); + } + break; + } + case POSITIONAL_CONSISTENCY_KEY: + case MASK_KEY: { + if (property instanceof RenderedImage) { + // Not cached for avoiding to retain memory. + return applySameChanges((RenderedImage) property); + } + break; + } + } + return Image.UndefinedProperty; + } + + /** + * Delegates to the wrapped image with no change. + */ + @Override public ColorModel getColorModel() {return source.getColorModel();} + @Override public SampleModel getSampleModel() {return source.getSampleModel();} + @Override public int getTileWidth() {return source.getTileWidth();} + @Override public int getTileHeight() {return source.getTileHeight();} /** * Returns properties determined at construction time. @@ -420,7 +487,7 @@ public final class ReshapedImage extends PlanarImage { @Override public boolean equals(final Object object) { if (object instanceof ReshapedImage) { - final ReshapedImage other = (ReshapedImage) object; + final var other = (ReshapedImage) object; return source.equals(other.source) && minX == other.minX && minY == other.minY && diff --git a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java index 68b9c18187..c3f4ce68b4 100644 --- a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java +++ b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java @@ -49,6 +49,7 @@ import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.ImageRenderer; import org.apache.sis.coverage.grid.PixelInCell; import org.apache.sis.coverage.grid.PixelTranslation; +import org.apache.sis.coverage.internal.shared.SampleDimensions; import org.apache.sis.geometry.AbstractEnvelope; import org.apache.sis.geometry.Envelope2D; import org.apache.sis.geometry.Shapes2D; @@ -58,26 +59,25 @@ import org.apache.sis.image.ErrorHandler; import org.apache.sis.image.ImageProcessor; import org.apache.sis.image.internal.shared.ColorModelType; import org.apache.sis.image.internal.shared.ImageUtilities; -import org.apache.sis.coverage.internal.shared.SampleDimensions; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.IdentifiedObjects; +import org.apache.sis.referencing.operation.transform.LinearTransform; +import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.referencing.operation.matrix.AffineTransforms2D; import org.apache.sis.referencing.internal.shared.WraparoundApplicator; import org.apache.sis.system.Modules; import org.apache.sis.util.Debug; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.internal.shared.CloneAccess; import org.apache.sis.util.internal.shared.Numerics; +import org.apache.sis.util.resources.Vocabulary; +import org.apache.sis.util.logging.Logging; import org.apache.sis.io.TableAppender; import org.apache.sis.math.Statistics; import org.apache.sis.measure.Quantities; import org.apache.sis.measure.Units; import org.apache.sis.metadata.iso.extent.Extents; -import org.apache.sis.referencing.operation.transform.LinearTransform; -import org.apache.sis.referencing.operation.transform.MathTransforms; -import org.apache.sis.referencing.operation.matrix.AffineTransforms2D; import org.apache.sis.map.internal.Resources; -import org.apache.sis.util.resources.Vocabulary; -import org.apache.sis.util.logging.Logging; import org.apache.sis.portrayal.PlanarCanvas; // For javadoc. @@ -123,7 +123,7 @@ public class RenderingData implements CloneAccess { * * @see #xyDimensions */ - private static final int BIDIMENSIONAL = 2; + protected static final int BIDIMENSIONAL = 2; /** * Whether to allow the creation of {@link java.awt.image.IndexColorModel}. This flag may be temporarily set @@ -367,8 +367,12 @@ public class RenderingData implements CloneAccess { * <p>Caller should invoke {@link #ensureImageLoaded(GridCoverage, GridExtent, boolean)} * after this method (this is not done automatically).</p> * + * <p>The {@code objectivePOI} argument should generally be non-null. But a null value is tolerated + * if the transform between the {@link GridCoverage} <abbr>CRS</abbr> and the argument given to the + * {@link #setObjectiveCRS(CoordinateReferenceSystem)} method is linear.</p> + * * @param objectiveToDisplay transform used for rendering the coverage on screen. - * @param objectivePOI point where to compute resolution, in coordinates of objective CRS. + * @param objectivePOI point where to compute resolution, in coordinates of objective <abbr>CRS</abbr>. * @return the loaded grid coverage, or {@code null} if no loading has been done * (which means that the coverage is unchanged, not that it does not exist). * @throws TransformException if an error occurred while computing resolution from given transforms. @@ -726,7 +730,7 @@ public class RenderingData implements CloneAccess { * * @param transform the transform to concatenate with a "wraparound" operation. * @param sourceMedian point of interest in the <em>source</em> CRS of given transform. - * @param targetMedian point of interest after wraparound. + * @param targetMedian point of interest after wraparound, or {@code null} if none. * @param targetCRS the target CRS of the given transform. */ private static MathTransform applyWraparound(final MathTransform transform, DirectPosition sourceMedian, @@ -905,7 +909,7 @@ public class RenderingData implements CloneAccess { * This is used for error reporting. */ private String getCRSName() { - if (dataGeometry.isDefined(GridGeometry.CRS)) { + if (isDefined(GridGeometry.CRS)) { String name = IdentifiedObjects.getDisplayName(dataGeometry.getCoordinateReferenceSystem(), locale); if (name != null) { return name; diff --git a/endorsed/src/org.apache.sis.portrayal/test/org/apache/sis/map/coverage/RenderingDataTest.java b/endorsed/src/org.apache.sis.portrayal/test/org/apache/sis/map/coverage/RenderingDataTest.java new file mode 100644 index 0000000000..7cf6ac1de4 --- /dev/null +++ b/endorsed/src/org.apache.sis.portrayal/test/org/apache/sis/map/coverage/RenderingDataTest.java @@ -0,0 +1,251 @@ +/* + * 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.map.coverage; + +import java.util.Map; +import java.util.Arrays; +import java.awt.Point; +import java.awt.color.ColorSpace; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import org.opengis.geometry.Envelope; +import org.opengis.geometry.DirectPosition; +import org.opengis.referencing.operation.TransformException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.coverage.grid.GridCoverage; +import org.apache.sis.coverage.grid.GridCoverage2D; +import org.apache.sis.coverage.grid.GridCoverageProcessor; +import org.apache.sis.coverage.grid.GridExtent; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.coverage.grid.ImageRenderer; +import org.apache.sis.coverage.grid.PixelInCell; +import org.apache.sis.geometry.DirectPosition2D; +import org.apache.sis.geometry.GeneralDirectPosition; +import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.image.ErrorHandler; +import org.apache.sis.image.PlanarImage; +import org.apache.sis.image.internal.shared.TiledImage; +import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.referencing.operation.transform.LinearTransform; +import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.referencing.internal.shared.AffineTransform2D; +import org.apache.sis.storage.MemoryGridCoverageResource; + +// Test dependencies +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static org.apache.sis.test.Assertions.assertSingleton; +import org.apache.sis.test.TestCase; + + +/** + * Headless reproduction of the {@code CoverageCanvas} display chain for a tiled image. + * Compares 2D resource against the same resource augmented with a temporal dimension. + */ +public final class RenderingDataTest extends TestCase { + /** + * Width and height (in pixels) of the rendered image produced in output. + */ + private static final int RENDERED_SIZE = 8; + + /** + * Width and height (in pixels) of the resource used as input. + */ + private static final int DATA_SIZE = 128; + + /** + * Tile height in pixels. There is no tile width as this test does not tile the image horizontally. + * It is sufficient to focus the test on the tiling in only one axis, which is <abbr>y</abbr>. + */ + private static final int TILE_HEIGHT = 16; + + /** + * Number of meters per pixel at full resolution. + */ + private static final double RESOLUTION = 10; + + /** + * The western-most coordinate of the limit of the resource extent. + * Unit is meters of the <abbr>UTM</abbr> zone 31 projection. + */ + private static final double WEST_BOUND = 400000; + + /** + * The northern-most coordinate of the limit of the resource extent. + * Unit is meters of the <abbr>UTM</abbr> zone 31 projection. + */ + private static final double NORTH_BOUND = 5000000; + + /** + * Creates a new text case. + */ + public RenderingDataTest() { + } + + /** + * Simulates the {@code CoverageCanvas} worker from the <abbr>GUI</abbr> module. + * The steps include: + * + * <ul> + * <li>pyramid loader,</li> + * <li>{@link RenderingData#ensureCoverageLoaded(LinearTransform, DirectPosition)},</li> + * <li>{@link RenderingData#ensureImageLoaded(GridCoverage, GridExtent, boolean)},</li> + * <li>{@link RenderingData#resampleAndConvert(RenderedImage, LinearTransform, DirectPosition)}.</li> + * </ul> + * + * @throws Exception if a data store, a factory or a transform exception occurred. + */ + @Test + public void testDisplayChain() throws Exception { + final GridCoverage coverage = createTiledCoverage(); + runDisplayChain(coverage); + runDisplayChain(addTemporalDimension(coverage)); + } + + /** + * Creates a two-dimensional coverage wrapping a tiled image with an <var>UTM</var> projection. + * The pixel values do not matter and are all zeros. What matter for this test is the tiling. + * Therefore, for saving memory and <abbr>CPU</abbr>, all tiles share the same data buffer. + */ + private static GridCoverage createTiledCoverage() { + final var gridToCRS = new AffineTransform2D(RESOLUTION, 0, 0, -RESOLUTION, WEST_BOUND, NORTH_BOUND); + final var crs = CommonCRS.WGS84.universal(45, 3); // UTM zone 31N. + final var gg = new GridGeometry(new GridExtent(DATA_SIZE, DATA_SIZE), PixelInCell.CELL_CORNER, gridToCRS, crs); + final var properties = Map.of(TiledImage.GRID_GEOMETRY_KEY, gg); + + final var colorSpace = ColorSpace.getInstance(ColorSpace.CS_GRAY); + final var colors = new ComponentColorModel(colorSpace, false, true, ComponentColorModel.OPAQUE, DataBuffer.TYPE_BYTE); + final var layout = new MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, DATA_SIZE, TILE_HEIGHT, 1); + final var buffer = new DataBufferByte(DATA_SIZE * TILE_HEIGHT / Byte.SIZE, 1); + final var tiles = new Raster[DATA_SIZE / TILE_HEIGHT]; + Arrays.setAll(tiles, (i) -> Raster.createRaster(layout, buffer, new Point(0, i * TILE_HEIGHT))); + final var image = new TiledImage(properties, colors, DATA_SIZE, DATA_SIZE, 0, 0, tiles); + return new GridCoverage2D(gg, null, image); + } + + /** + * Adds a temporal dimension to the given coverage. + */ + private static GridCoverage addTemporalDimension(final GridCoverage coverage) { + var timeCRS = CommonCRS.Temporal.UNIX.crs(); + var gridToTime = MathTransforms.linear(86400, 24*60*60); // 1 cell = 1 day. + var gridExtent = new GridExtent(null, null, new long[1], true); + var dimToAdd = new GridGeometry(gridExtent, PixelInCell.CELL_CORNER, gridToTime, timeCRS); + var processor = new GridCoverageProcessor(); + return processor.appendDimensions(coverage, dimToAdd); + } + + /** + * Simulates the {@code CoverageCanvas} worker from the <abbr>GUI</abbr> module. + * The steps include: + * + * <ul> + * <li>pyramid loader,</li> + * <li>{@link RenderingData#ensureCoverageLoaded(LinearTransform, DirectPosition)},</li> + * <li>{@link RenderingData#ensureImageLoaded(GridCoverage, GridExtent, boolean)},</li> + * <li>{@link RenderingData#resampleAndConvert(RenderedImage, LinearTransform, DirectPosition)}.</li> + * </ul> + */ + private static void runDisplayChain(final GridCoverage coverage) throws Exception { + final var resource = new MemoryGridCoverageResource(coverage); + /* + * Prepare a region of interest (ROI) in the middle of the resource domain. + * For this test, it is important that the ROI does not cover all tiles. + * Because the ROI is in the middle of the data domain, + * the envelope center should be invariant. + */ + GridGeometry domain = resource.getGridGeometry(); + final var zoomArea = new GeneralEnvelope(domain.getEnvelope()); + for (int i = 0; i < RenderingData.BIDIMENSIONAL; i++) { + final double margin = zoomArea.getSpan (i) / 4; + zoomArea.setRange(i, zoomArea.getLower(i) + margin, + zoomArea.getUpper(i) - margin); + } + verifyLocationOfCenter(domain); + domain = domain.derive().subgrid(zoomArea, null).build(); + verifyLocationOfCenter(domain); + final GridExtent sliceExtent = domain.getExtent(); + /* + * Simulate the fallback in `RenderingData.ensureImageLoaded(…)` which recompute + * the grid geometry using `ImageRenderer` when that information is not provided + * as a property. + */ + { + final var r = new ImageRenderer(coverage, sliceExtent); + verifyLocationOfCenter(r.getImageGeometry(RenderingData.BIDIMENSIONAL)); + assertArrayEquals(new int[] {0, 1}, r.getXYDimensions()); + } + domain = domain.selectDimensions(0, 1); + final double scale = ((double) RENDERED_SIZE) / (DATA_SIZE * RESOLUTION); + final var objectiveToDisplay = new AffineTransform2D(scale, 0, 0, -scale, WEST_BOUND * -scale, NORTH_BOUND * scale); + final CoordinateReferenceSystem objectiveCRS = domain.getCoordinateReferenceSystem(); + final var poi = new DirectPosition2D(objectiveCRS, WEST_BOUND + DATA_SIZE * RESOLUTION / 2, + NORTH_BOUND - DATA_SIZE * RESOLUTION / 2); + /* + * Render the request which has been prepared above. + * Get the result, but also the source of the result. + */ + final var render = new RenderingData(ErrorHandler.THROW); + render.coverageLoader = new MultiResolutionCoverageLoader(resource, null, null); + render.setImageSpace(domain, resource.getSampleDimensions(), new int[] {0, 1}); + render.setObjectiveCRS(objectiveCRS); + render.ensureImageLoaded(render.ensureCoverageLoaded(objectiveToDisplay, poi), sliceExtent, true); + final RenderedImage shown = render.resampleAndConvert(render.getSourceImage(), objectiveToDisplay, poi); + final RenderedImage source = assertSingleton(shown.getSources()); + /* + * Verify the source of the result before to verify the final result. + * The source should have the same size as the original data, + * but translated for having the request at coordinates (0,0). + */ + assertEquals(-DATA_SIZE / 4, source.getMinX()); // Difference relative to requested area. + assertEquals( DATA_SIZE, source.getWidth()); + assertEquals( 0, source.getMinY()); + assertEquals( DATA_SIZE / 2, source.getHeight()); + assertEquals(0, shown.getMinX()); + assertEquals(RENDERED_SIZE, shown.getWidth()); + assertEquals(RENDERED_SIZE, shown.getTileWidth()); + assertEquals(RENDERED_SIZE / 4, shown.getMinY()); + assertEquals(RENDERED_SIZE / 2, shown.getHeight()); + assertEquals(RENDERED_SIZE / 2, shown.getTileHeight()); + /* + * Verify georeferencing. + */ + verifyLocationOfCenter(assertInstanceOf(GridGeometry.class, source.getProperty(PlanarImage.GRID_GEOMETRY_KEY))); + } + + /** + * Verifies that the center of the given grid geometry is in the center of the test domain. + */ + private static void verifyLocationOfCenter(final GridGeometry domain) throws TransformException { + final Envelope envelope = domain.getEnvelope(); + assertEquals( WEST_BOUND + RESOLUTION * (DATA_SIZE / 2), envelope.getMedian(0)); + assertEquals(NORTH_BOUND - RESOLUTION * (DATA_SIZE / 2), envelope.getMedian(1)); + + final GridExtent extent = domain.getExtent(); + final var center = new GeneralDirectPosition(extent.getDimension()); + for (int i = center.getDimension(); --i >= 0;) { + center.setCoordinate(i, extent.getMedian(i)); + } + assertSame(center, (domain.getGridToCRS(PixelInCell.CELL_CORNER).transform(center, center))); + assertEquals( WEST_BOUND + RESOLUTION * (DATA_SIZE / 2), center.getCoordinate(0)); + assertEquals(NORTH_BOUND - RESOLUTION * (DATA_SIZE / 2), center.getCoordinate(1)); + } +}
