blob: 2416f84019f0de9fe8e12020ae79e923ce864372 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.coverage.grid;
import java.util.Arrays;
import java.util.Random;
import java.util.EnumSet;
import java.util.stream.IntStream;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import static org.opengis.referencing.datum.PixelInCell.CELL_CENTER;
import org.apache.sis.geometry.Envelope2D;
import org.apache.sis.geometry.DirectPosition2D;
import org.apache.sis.geometry.ImmutableEnvelope;
import org.apache.sis.image.Interpolation;
import org.apache.sis.coverage.privy.TiledImage;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.privy.Formulas;
import org.apache.sis.referencing.privy.AffineTransform2D;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.TransformSeparator;
// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.sis.test.TestCase;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.image.TiledImageMock;
import org.apache.sis.referencing.crs.HardCodedCRS;
import org.apache.sis.referencing.operation.HardCodedConversions;
import static org.apache.sis.referencing.Assertions.assertEnvelopeEquals;
import static org.apache.sis.feature.Assertions.assertValuesEqual;
import static org.apache.sis.feature.Assertions.assertPixelsEqual;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import static org.opengis.test.Assertions.assertSampleValuesEqual;
import static org.opengis.test.Assertions.assertAxisDirectionsEqual;
/**
* Tests the {@link ResampledGridCoverage} implementation.
* The tests in this class does not verify interpolation results
* (this is {@link org.apache.sis.image.ResampledImageTest} job).
* Instead, it focuses on the grid geometry inferred by the operation.
*
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
* @author Johann Sorel (Geomatys)
*/
public final class ResampledGridCoverageTest extends TestCase {
/**
* The random number generator used for generating some grid coverage values.
* Created only if needed.
*/
private Random random;
/**
* Arbitrary non-zero grid coordinate for the <var>z</var> dimensions.
*/
private int gridZ;
/**
* Creates a new test case.
*/
public ResampledGridCoverageTest() {
}
/**
* Creates a small grid coverage with arbitrary data. The rendered image will
* have only one tile because testing tiling is not the purpose of this class.
* This simple coverage is two-dimensional.
*/
private GridCoverage2D createCoverage2D() {
random = TestUtilities.createRandomNumberGenerator();
final int width = random.nextInt(8) + 3;
final int height = random.nextInt(8) + 3;
final TiledImageMock image = new TiledImageMock(
DataBuffer.TYPE_USHORT, 2, // dataType and numBands
random.nextInt(32) - 10, // minX (no effect on tests)
random.nextInt(32) - 10, // minY (no effect on tests)
width, height, // Image size
width, height, // Tile size
random.nextInt(32) - 10, // minTileX
random.nextInt(32) - 10, // minTileY
random.nextBoolean()); // Banded or interleaved sample model
image.validate();
image.initializeAllTiles(0);
final int x = random.nextInt(32) - 10;
final int y = random.nextInt(32) - 10;
final GridGeometry gg = new GridGeometry(
new GridExtent(null, new long[] {x, y}, new long[] {x+width, y+height}, false),
new Envelope2D(HardCodedCRS.WGS84, 20, 15, 60, 62), GridOrientation.HOMOTHETY);
return new GridCoverage2D(gg, null, image);
}
/**
* Size of a quadrant in the coverage created by {@link #createCoverageND(boolean)}.
* The total image width and height are {@code 2*Q}.
*/
private static final int QS = 3;
/**
* Low values for the grid extent created by {@link #createCoverage2D()}.
*/
private static final int LX = 3, LY = -2;
/**
* Creates a coverage in {@linkplain HardCodedCRS#WGS84_3D OGC:CRS:84 + elevation} reference system.
* If the {@code withTime} argument is {@code true}, then the coverage will also include a temporal
* dimension. The grid coverage characteristics are:
* <ul>
* <li>Dimension is 6×6.</li>
* <li>Grid extent starts at arbitrary non-zero low values.</li>
* <li>Envelope is arbitrary but stable (no random values).</li>
* <li>Display oriented (origin is in upper-left corner).</li>
* <li>3 byte bands for RGB coloration.</li>
* <li>Each quarter of the overall image is filled with a plain color:
* <table style="color:white;border-collapse:collapse;">
* <tbody style="border:none">
* <tr>
* <td style="width:50%; background-color:black">Black</td>
* <td style="width:50%; background-color:red">Red</td>
* </tr>
* <tr>
* <td style="width:50%; background-color:green">Green</td>
* <td style="width:50%; background-color:blue">Blue</td>
* </tr>
* </tbody>
* </table>
* </li>
* </ul>
*
* @param withTime {@code false} for a three-dimensional coverage, or {@code true} for adding a temporal dimension.
* @return a new three- or four-dimensional RGB Grid Coverage.
*/
private GridCoverage createCoverageND(final boolean withTime) {
random = TestUtilities.createRandomNumberGenerator();
final BufferedImage image = new BufferedImage(2*QS, 2*QS, BufferedImage.TYPE_3BYTE_BGR);
final int[] color = new int[QS*QS];
/* Upper-left quarter */ // Keep default value, which is black.
/* Upper-right quarter */ Arrays.fill(color, Color.RED .getRGB()); image.setRGB(QS, 0, QS, QS, color, 0, QS);
/* Lower-left quarter */ Arrays.fill(color, Color.GREEN.getRGB()); image.setRGB( 0, QS, QS, QS, color, 0, QS);
/* Lower-right quarter */ Arrays.fill(color, Color.BLUE .getRGB()); image.setRGB(QS, QS, QS, QS, color, 0, QS);
/*
* Create an image with origin between -2 and +2. We use a random image location for more
* complete testing, but actually the tests in this class are independent of image origin.
* Note that grid extent origin does not need to be the same as image origin.
*/
final int minX = random.nextInt(5) - 2;
final int minY = random.nextInt(5) - 2;
GridGeometry gg = createGridGeometryND(withTime ? HardCodedCRS.WGS84_4D : HardCodedCRS.WGS84_3D, 0, 1, 2, 3, false);
final TiledImage shiftedImage = new TiledImage(null,
image.getColorModel(),
image.getWidth(), image.getHeight(), // Image size
random.nextInt(32) - 10, // minTileX
random.nextInt(32) - 10, // minTileY
image.getRaster().createTranslatedChild(minX, minY));
return new GridCoverage2D(gg, null, shiftedImage);
}
/**
* Creates the grid geometry associated with {@link #createCoverageND(boolean)}, optionally with swapped
* horizontal axes and flipped Y axis. The given CRS shall have 3 or 4 dimensions.
*
* @param crs the coordinate reference system to assign to the grid geometry.
* @param x dimension of <var>x</var> coordinates (typically 0).
* @param y dimension of <var>y</var> coordinates (typically 1).
* @param z dimension of <var>z</var> coordinates (typically 2).
* @param t dimension of <var>t</var> coordinates (typically 3). Ignored if the CRS is not four-dimensional.
* @param flipY whether to flip the <var>y</var> axis.
*/
private GridGeometry createGridGeometryND(final CoordinateReferenceSystem crs,
final int x, final int y, final int z, final int t, final boolean flipY)
{
final int dim = crs.getCoordinateSystem().getDimension();
final long[] lower = new long[dim];
final long[] upper = new long[dim];
lower[x] = LX; upper[x] = LX + 2*QS - 1;
lower[y] = LY; upper[y] = LY + 2*QS - 1;
final MatrixSIS gridToCRS = Matrices.createIdentity(dim + 1);
gridToCRS.setElement(x, x, 44./(2*QS)); // X scale
gridToCRS.setElement(x, dim, -50./(2*QS)); // X translation
gridToCRS.setElement(y, y, -3.5); // Y scale
gridToCRS.setElement(y, dim, -0.75); // Y translation
gridToCRS.setElement(z, dim, -100);
lower[z] = upper[z] = gridZ = 7; // Arbitrary non-zero position in the grid.
if (t < dim) {
gridToCRS.setElement(t, dim, 48055);
lower[t] = upper[t] = 12;
}
if (flipY) {
/*
* Lower Y coordinate before flip: Ty₁ + scale × LY
* Upper Y coordinate after flip: Ty₂ − scale × (LY+2×QS−1)
* Condition Ty₁ = Ty₂ gives: Ty₂ = Ty₁ + scale × (2(QS+LY)−1)
*/
gridToCRS.setElement(y, y, 3.5); // Inverse sign.
gridToCRS.setElement(y, dim, -0.75 + -3.5 * (2*(QS+LY) - 1));
}
return new GridGeometry(new GridExtent(null, lower, upper, true),
CELL_CENTER, MathTransforms.linear(gridToCRS), crs);
}
/**
* Verifies that the given target coverage has the same pixel values as the source coverage.
* This method opportunistically verifies that the target {@link GridCoverage} instance has a
* {@link GridCoverage#render(GridExtent)} implementation conforms to the specification, i.e.
* that requesting only a sub-area results in an image where pixel coordinate (0,0) corresponds
* to cell coordinates in the lower corner of specified {@code sliceExtent}.
*/
private void assertContentEquals(final GridCoverage source, final GridCoverage target) {
final int tx = random.nextInt(3);
final int ty = random.nextInt(3);
final GridExtent sourceExtent = source.gridGeometry.getExtent();
final int newWidth = StrictMath.toIntExact(sourceExtent.getSize(0) - tx);
final int newHeight = StrictMath.toIntExact(sourceExtent.getSize(1) - ty);
GridExtent subExtent = new GridExtent(
StrictMath.toIntExact(sourceExtent.getLow(0) + tx),
StrictMath.toIntExact(sourceExtent.getLow(1) + ty),
newWidth,
newHeight
);
assertPixelsEqual(source.render(null), new Rectangle(tx, ty, newWidth, newHeight),
target.render(subExtent), new Rectangle(newWidth, newHeight));
}
/**
* Returns a resampled coverage using processor with default configuration.
* We use processor instead of instantiating {@link ResampledGridCoverage} directly in order
* to test {@link GridCoverageProcessor#resample(GridCoverage, GridGeometry)} method as well.
*
* <p>{@link GridCoverageProcessor.Optimization#REPLACE_OPERATION} is disabled for avoiding to
* test another operation than the resampling one.</p>
*/
private static GridCoverage resample(final GridCoverage source, final GridGeometry target) throws TransformException {
final GridCoverageProcessor processor = new GridCoverageProcessor();
processor.setOptimizations(EnumSet.of(GridCoverageProcessor.Optimization.REPLACE_SOURCE));
processor.setInterpolation(Interpolation.NEAREST);
return processor.resample(source, target);
}
/**
* Tests application of an identity transform computed from an explicitly given "grid to CRS" transform.
* We expect the source coverage to be returned unchanged.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testExplicitIdentity() throws TransformException {
final GridCoverage2D source = createCoverage2D();
GridGeometry gg = source.getGridGeometry();
gg = new GridGeometry(null, CELL_CENTER, gg.getGridToCRS(CELL_CENTER), gg.getCoordinateReferenceSystem());
final GridCoverage target = resample(source, gg);
assertSame(source, target, "Identity transform should result in same coverage.");
assertContentEquals(source, target);
}
/**
* Tests application of an identity transform without specifying explicitly the desired grid geometry.
* This test is identical to {@link #testExplicitIdentity()} except that the "grid to CRS" transform
* specified to the {@code resample(…)} operation is null.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testImplicitIdentity() throws TransformException {
final GridCoverage2D source = createCoverage2D();
GridGeometry gg = source.getGridGeometry();
gg = new GridGeometry(null, CELL_CENTER, null, gg.getCoordinateReferenceSystem());
final GridCoverage target = resample(source, gg);
assertSame(source, target, "Identity transform should result in same coverage.");
assertContentEquals(source, target);
}
/**
* Tests resampling with a transform which is only a translation by integer values.
* This test verifies that an optimized path (much cheaper than real resampling) is taken.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testIntegerTranslation() throws TransformException {
final GridCoverageProcessor processor = new GridCoverageProcessor(); // With all optimization enabled.
final GridCoverage source = createCoverage2D();
final GridGeometry sourceGG = source.getGridGeometry();
final GridGeometry targetGG = sourceGG.shiftGrid(-10, 15);
final GridCoverage target = processor.resample(source, targetGG);
assertInstanceOf(TranslatedGridCoverage.class, target, "Expected fast path.");
assertSame(targetGG, target.getGridGeometry());
assertEnvelopeEquals(sourceGG.getEnvelope(), targetGG.getEnvelope(), STRICT);
/*
* The envelope is BOX(20 15, 80 77). Evaluate a single point inside that envelope.
* The result for identical "real world" coordinates should be the same for both coverages.
*/
final DirectPosition2D p = new DirectPosition2D(sourceGG.getCoordinateReferenceSystem(), 50, 30);
assertArrayEquals(source.evaluator().apply(p),
target.evaluator().apply(p));
}
/**
* Tests application of axis swapping in a two-dimensional coverage.
* This test verifies the envelope of resampled coverage.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testAxisSwap() throws TransformException {
final GridCoverage2D source = createCoverage2D();
GridGeometry gg = new GridGeometry(null, CELL_CENTER, null, HardCodedCRS.WGS84_LATITUDE_FIRST);
final GridCoverage target = resample(source, gg);
/*
* We expect the same image since `ResampledGridCoverage` should have been
* able to apply the operation with only a change of `gridToCRS` transform.
*/
assertNotSame(source, target);
assertSame(unwrap(source.render(null)),
unwrap(target.render(null)));
/*
* As an easy way to check that axis swapping has happened, check the envelopes.
*/
final ImmutableEnvelope se = source.getGridGeometry().envelope;
final ImmutableEnvelope te = target.getGridGeometry().envelope;
assertEquals(se.getLower(0), te.getLower(1), Formulas.ANGULAR_TOLERANCE);
assertEquals(se.getLower(1), te.getLower(0), Formulas.ANGULAR_TOLERANCE);
assertEquals(se.getUpper(0), te.getUpper(1), Formulas.ANGULAR_TOLERANCE);
assertEquals(se.getUpper(1), te.getUpper(0), Formulas.ANGULAR_TOLERANCE);
}
/**
* Unwraps the given image if it is an instance of {@link ReshapedImage}.
*/
private static RenderedImage unwrap(final RenderedImage image) {
assertEquals(0, image.getMinX(), "GridCoverage.render(null) should have their origin at (0,0).");
assertEquals(0, image.getMinY(), "GridCoverage.render(null) should have their origin at (0,0).");
return (image instanceof ReshapedImage) ? ((ReshapedImage) image).source : image;
}
/**
* Tests application of axis swapping in a three-dimensional coverage, together with an axis flip.
* This test verifies that the pixel values of resampled coverage are found in expected quadrant.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testAxisSwapAndFlip() throws TransformException {
final GridCoverage source = createCoverageND(false);
final GridGeometry target = createGridGeometryND(CommonCRS.WGS84.geographic3D(), 1, 0, 2, 3, true);
final GridCoverage result = resample(source, target);
final RenderedImage sourceImage = source.render(null);
final RenderedImage targetImage = result.render(null);
assertEquals(target, result.getGridGeometry());
assertEquals(0, sourceImage.getMinX()); // As per GridCoverage.render(…) contract.
assertEquals(0, sourceImage.getMinY());
assertEquals(0, targetImage.getMinX());
assertEquals(0, targetImage.getMinY());
assertPixelsEqual(sourceImage, new Rectangle( 0, QS, QS, QS),
targetImage, new Rectangle( 0, 0, QS, QS)); // Green should be top-left.
assertPixelsEqual(sourceImage, new Rectangle( 0, 0, QS, QS),
targetImage, new Rectangle(QS, 0, QS, QS)); // Black should be upper-right.
assertPixelsEqual(sourceImage, new Rectangle(QS, QS, QS, QS),
targetImage, new Rectangle( 0, QS, QS, QS)); // Blue should be lower-left.
assertPixelsEqual(sourceImage, new Rectangle(QS, 0, QS, QS),
targetImage, new Rectangle(QS, QS, QS, QS)); // Red should be lower-right.
}
/**
* Tests an operation moving the dimension of temporal axis in a four-dimensional coverage.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testTemporalAxisMoved() throws TransformException {
final GridCoverage source = createCoverageND(true);
final GridGeometry target = createGridGeometryND(HardCodedCRS.WGS84_4D_TIME_FIRST, 1, 2, 3, 0, false);
final GridCoverage result = resample(source, target);
assertAxisDirectionsEqual(result.getGridGeometry().getCoordinateReferenceSystem().getCoordinateSystem(),
new AxisDirection[] {AxisDirection.FUTURE, AxisDirection.EAST, AxisDirection.NORTH, AxisDirection.UP},
"Expected (t,λ,φ,H) axes.");
assertSampleValuesEqual(source.render(null), result.render(null), STRICT, null);
}
/**
* Tests resampling in a sub-region specified by a grid extent. This method uses a three-dimensional coverage,
* which implies that this method also tests the capability to identify which slice needs to be resampled.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testSubGridExtent() throws TransformException {
final GridCoverage source = createCoverageND(false);
final GridGeometry sourceGeom = source.getGridGeometry();
final GridGeometry targetGeom = new GridGeometry(
new GridExtent(null, new long[] {LX+2, LY+2, gridZ},
new long[] {LX+5, LY+5, gridZ}, true),
CELL_CENTER, sourceGeom.gridToCRS,
sourceGeom.getCoordinateReferenceSystem());
final GridCoverage result = resample(source, targetGeom);
assertEquals(targetGeom, result.getGridGeometry());
/*
* Verify that the target coverage contains all pixel values of the source coverage.
* Iteration over source pixels needs to be restricted to the `targetGeom` extent.
*/
final RenderedImage sourceImage = source.render(null);
RenderedImage targetImage = result.render(null);
assertPixelsEqual(sourceImage, new Rectangle(2, 2, 4, 4),
targetImage, null);
/*
* Verify GridCoverage.render(GridExtent) contract: the origin of the returned image
* shall be the lower-left corner of `sliceExtent`, which is (3,3) in this test.
*/
targetImage = result.render(new GridExtent(null,
new long[] {LX+3, LY+3, gridZ},
new long[] {LX+4, LY+4, gridZ}, true));
assertPixelsEqual(sourceImage, new Rectangle(3, 3, 2, 2),
targetImage, new Rectangle(0, 0, 2, 2));
}
/**
* Tests resampling in a sub-region specified by a grid extent spanning a single column.
* When trying to optimize resampling by dropping dimensions, it can happen that transform dimensions
* are reduced to 1D. However, it is a problem for image case which requires 2D coordinates.
* So we must ensure that resample conversion keeps at least two dimensions.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testSubGridExtentColumnar() throws TransformException {
final GridCoverage2D source = createCoverage2D();
final GridGeometry sourceGeom = source.getGridGeometry();
final GridExtent sourceExtent = sourceGeom.getExtent();
final GridExtent targetExtent = new GridExtent(null,
new long[] {sourceExtent.getLow(0), sourceExtent.getLow (1)},
new long[] {sourceExtent.getLow(0), sourceExtent.getHigh(1)}, true);
final GridGeometry targetGeom = new GridGeometry(
targetExtent, CELL_CENTER,
sourceGeom.getGridToCRS(CELL_CENTER),
sourceGeom.getCoordinateReferenceSystem());
final GridCoverage result = resample(source, targetGeom);
final int height = (int) targetExtent.getSize(1);
assertPixelsEqual(source.render(null), new Rectangle(0, 0, 1, height), result.render(null), null);
}
/**
* Tests resampling in a sub-region specified by an envelope.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testSubGeographicArea() throws TransformException {
final GridCoverage2D source = createCoverage2D(); // Envelope2D(20, 15, 60, 62)
final GridGeometry gg = new GridGeometry(null,
new Envelope2D(HardCodedCRS.WGS84, 18, 20, 17, 31), GridOrientation.HOMOTHETY);
final GridCoverage target = resample(source, gg);
final GridExtent sourceExtent = source.getGridGeometry().getExtent();
final GridExtent targetExtent = target.getGridGeometry().getExtent();
assertTrue(sourceExtent.getSize(0) > targetExtent.getSize(0));
assertTrue(sourceExtent.getSize(1) > targetExtent.getSize(1));
}
/**
* Tests application of a non-linear transform.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testReprojection() throws TransformException {
final GridCoverage2D source = createCoverage2D();
GridGeometry gg = new GridGeometry(null, CELL_CENTER, null, HardCodedConversions.mercator());
final GridCoverage target = resample(source, gg);
assertTrue(target.getGridGeometry().getExtent().startsAtZero());
/*
* Mercator projection does not change pixel width, but change pixel height.
*/
final GridExtent sourceExtent = source.getGridGeometry().getExtent();
final GridExtent targetExtent = target.getGridGeometry().getExtent();
assertEquals(sourceExtent.getSize(0), targetExtent.getSize(0));
assertTrue (sourceExtent.getSize(1) <= targetExtent.getSize(1));
}
/**
* Tests application of a three-dimensional transform which cannot be reduced to a two-dimensional transform.
* It happens for example when transformation of <var>x</var> or <var>y</var> coordinate depends on <var>z</var>
* coordinate value. In such case we cannot separate the 3D transform into (2D + 1D) transforms. This method
* verifies that {@link ResampledGridCoverage} nevertheless manages to do its work even in that situation.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testNonSeparableGridToCRS() throws TransformException {
final GridCoverage source = createCoverageND(false);
final MatrixSIS nonSeparableMatrix = Matrices.createDiagonal(4, 4);
nonSeparableMatrix.setElement(0, 2, 1); // Make X dependent of Z.
nonSeparableMatrix.setElement(1, 2, 1); // Make Y dependent of Z.
final MathTransform nonSeparableG2C = MathTransforms.concatenate(
source.getGridGeometry().getGridToCRS(CELL_CENTER),
MathTransforms.linear(nonSeparableMatrix));
{
/*
* The test in this block is not a `ResampleGridCoverage` test, but rather a
* check for a condition that we need for the test performed in this method.
*/
final TransformSeparator separator = new TransformSeparator(nonSeparableG2C);
separator.addSourceDimensions(0, 1);
separator.addTargetDimensions(0, 1);
assertNotNull(assertThrows(FactoryException.class, () -> separator.separate(),
"Test requires a non-separable transform, but separation succeed."));
}
final GridGeometry targetGeom = new GridGeometry(
null, // Let the resample operation compute the extent automatically.
CELL_CENTER, nonSeparableG2C,
source.getCoordinateReferenceSystem());
/*
* Real test is below (above code was only initialization).
* Target image should be 6×6 pixels, like source image.
*/
final GridCoverage result = resample(source, targetGeom);
assertSampleValuesEqual(source.render(null), result.render(null), STRICT, null);
}
/**
* Tests the addition of a temporal axis. The value to insert in the temporal coordinate can be computed
* from the four-dimensional "grid to CRS" transform given in argument to the {@code resample(…)} method,
* combined with the source grid extent.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testDimensionalityIncrease() throws TransformException {
final GridCoverage source3D = createCoverageND(false);
final GridGeometry target4D = createGridGeometryND(HardCodedCRS.WGS84_4D, 0, 1, 2, 3, false);
final GridCoverage result = resample(source3D, target4D);
assertEquals(target4D, result.getGridGeometry());
assertSampleValuesEqual(source3D.render(null), result.render(null), STRICT, null);
}
/**
* Tests the removal of temporal axis.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testDimensionalityReduction() throws TransformException {
final GridGeometry target3D = createGridGeometryND(HardCodedCRS.WGS84_3D, 0, 1, 2, 3, false);
final GridCoverage source4D = createCoverageND(true);
final GridCoverage result = resample(source4D, target3D);
assertEquals(target3D, result.getGridGeometry());
assertSampleValuesEqual(source4D.render(null), result.render(null), STRICT, null);
}
/**
* Tests resampling with a target domain larger than the source domain.
* Pixel outside the source domain shall be set to fill value, which is 0.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*
* @see <a href="https://issues.apache.org/jira/browse/SIS-495">SIS-495</a>
*/
@Test
public void testDomainIncrease() throws TransformException {
final int size = 2;
final CoordinateReferenceSystem crs = HardCodedCRS.WGS84;
final BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_BYTE_GRAY);
image.getRaster().setDataElements(0, 0, size, size, new byte[] {10, 12, 16, 14});
final AffineTransform2D gridToCRS = new AffineTransform2D(1, 0, 0, -1, 0, 0);
final GridGeometry sourceGrid = new GridGeometry(null, CELL_CENTER, gridToCRS, crs);
final GridGeometry targetGrid = new GridGeometry(new GridExtent(4, 4), CELL_CENTER, gridToCRS, crs);
final GridCoverage source = new GridCoverage2D(sourceGrid, null, image);
final GridCoverage target = resample(source, targetGrid);
assertValuesEqual(target.render(null), 0, new double[][] {
{10, 12, 0, 0},
{16, 14, 0, 0},
{ 0, 0, 0, 0},
{ 0, 0, 0, 0}
});
}
/**
* Tests resampling of an image associated to a coordinate system using the 0 to 360° range of longitude.
* The image crosses the 180° longitude. The resampling does not involve map projection.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testLongitude360() throws TransformException {
final int width = 32;
final int height = 3;
final GridGeometry source = new GridGeometry(
new GridExtent(width, height), CELL_CENTER,
new AffineTransform2D(1, 0, 0, -1, 164, 0),
HardCodedCRS.WGS84.forConvention(AxesConvention.POSITIVE_RANGE));
/*
* Above grid extent is [164 … 195]° in longitude. The part that exceed 180° is equivalent to
* a [-180 … -165]° range. The extent below requests only a part of it, namely [-173 … -167]°.
* The first pixel of resampled image is the 23th pixel of original image.
*/
final GridGeometry target = new GridGeometry(
new GridExtent(7, height), CELL_CENTER,
new AffineTransform2D(1, 0, 0, -1, -173, 0),
HardCodedCRS.WGS84);
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
image.getRaster().setPixels(0, 0, width, height, IntStream.range(100, 100 + width*height).toArray());
final GridCoverage2D coverage = new GridCoverage2D(source, null, image);
final GridCoverage resampled = resample(coverage, target);
assertValuesEqual(resampled.render(null), 0, new double[][] {
{123, 124, 125, 126, 127, 128, 129},
{155, 156, 157, 158, 159, 160, 161},
{187, 188, 189, 190, 191, 192, 193}
});
}
/**
* Tests map reprojection of an image associated to a coordinate system using the 0 to 360° range of longitude.
*
* @throws TransformException if some coordinates cannot be transformed to the target grid geometry.
*/
@Test
public void testReprojectionFromLongitude360() throws TransformException {
/*
* Longitudes from 91°E to 235°E (in WGS84 geographic CRS), which is equivalent to 91°E to 125°W.
* Latitude range is not important for this test.
*/
final int width = 8;
final int height = 5;
final GridGeometry source = new GridGeometry(
new GridExtent(null, null, new long[] {width, height}, false), CELL_CENTER,
new AffineTransform2D(18, 0, 0, 19, 100, -20),
HardCodedCRS.WGS84.forConvention(AxesConvention.POSITIVE_RANGE));
/*
* 180°W to 180″E (the world) and 80°S to 80°N in Mercator projection.
* Latitude range is about the same as source grid geometry.
*/
final double xmin = -2.0037508342789244E7;
final GridGeometry target = new GridGeometry(
new GridExtent(null, null, new long[] {2*width, height}, false), CELL_CENTER,
new AffineTransform2D(-xmin/width, 0, 0, 2610000, xmin, -2376500),
HardCodedConversions.mercator());
/*
* Resample the image by specifying fully the target grid geometry.
* The grid coverage should have the exact same instance.
*/
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
image.getRaster().setPixels(0, 0, width, height, IntStream.range(100, 100 + width*height).toArray());
final GridCoverage2D coverage = new GridCoverage2D(source, null, image);
final GridCoverage resampled = resample(coverage, target);
assertSame(target, resampled.getGridGeometry());
/*
* Sample values 100, 101, 102, … should be distributed on both sides of the image.
*/
assertValuesEqual(resampled.render(null), 0, new double[][] {
{104, 106, 107, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 102, 103},
{112, 114, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 109, 110, 111},
{120, 122, 123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 118, 119},
{128, 130, 131, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 125, 126, 127},
{136, 138, 139, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 133, 134, 135},
});
}
/**
* Returns an image with only the queries part of the given image.
* This is an helper tools which can be invoked during debugging
* session in IDE capable to display images.
*
* <h4>Usage</h4>
* Add a new watch calling this method on wanted image.
*
* <h4>Limitations</h4>
* <ul>
* <li>If given image color-model is null, this method assumes 3 byte/RGB image.</li>
* <li>Works only with single-tile images.</li>
* </ul>
*
* @param source the image to display.
* @param extent if non-null, crop rendering to the rectangle defined by given extent,
* assuming extent low coordinate matches source image (0,0) coordinate.
* @return the image directly displayable through debugger.
*/
private static BufferedImage debug(final RenderedImage source, final GridExtent extent) {
Raster tile = source.getTile(source.getMinTileX(), source.getMinTileY());
final int width, height;
if (extent == null) {
tile = tile.createTranslatedChild(0, 0);
width = tile.getWidth();
height = tile.getHeight();
} else {
width = StrictMath.toIntExact(extent.getSize(0));
height = StrictMath.toIntExact(extent.getSize(1));
tile = tile.createChild(0, 0, width, height, 0, 0, null);
}
final BufferedImage view;
if (source.getColorModel() == null) {
view = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
view.getRaster().setRect(tile);
} else {
final WritableRaster wr = tile.createCompatibleWritableRaster(0, 0, width, height);
wr.setRect(tile);
view = new BufferedImage(source.getColorModel(), wr, false, null);
}
return view;
}
}