Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
import java.util.Optional;
import java.io.Serializable;
import javax.measure.Unit;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.LenientComparable;
import org.apache.sis.util.Utilities;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.opengis.referencing.operation.MathTransform1D;
Expand Down Expand Up @@ -85,7 +88,7 @@
*
* @since 1.0
*/
public class SampleDimension implements IdentifiedType, Serializable {
public class SampleDimension implements IdentifiedType, LenientComparable, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
Expand Down Expand Up @@ -498,6 +501,18 @@ public boolean equals(final Object object) {
return false;
}

@Override
public boolean equals(Object other, ComparisonMode mode) {
if (other == this) return true;
if (mode.isApproximate() && other instanceof SampleDimension) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should only test if the mode is STRICT, in which case the test should be done as in the current implementation of equals(Object), and unconditionally uses Utilities.deepEquals in all other cases. The reason is that since we need to make Category implements LenientComparable too, Category could apply whether policy it wants. In particular, it may compare the category name in ComparisonMode.BY_CONTRACT and ignore that name in ComparisonMode.IGNORE_METADATA.

final var otherDim = (SampleDimension) other;
return Utilities.deepEquals(this.transferFunction, otherDim.transferFunction, mode)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transfer function was not compared in equals(Object) because it was derived from categories. Therefore, comparing transferFunction was considered redundant with comparing categories. If we want the comparison to be tolerant to slight difference in transfer functions, then we need to make Category implements LenientComparable too, otherwise this method will actually behave as if a strict comparison was performed regardless the flexibility of Utilities.deepEquals(this.transferFunction, otherDim.transferFunction, mode).

&& Utilities.deepEquals(this.categories, otherDim.categories, mode)
&& Utilities.deepEquals(this.background, otherDim.background, mode);
}
return equals(other);
}

/**
* Returns a string representation of this sample dimension.
* This string is for debugging purpose only and may change in future version.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
package org.apache.sis.image;

import java.awt.Dimension;
import java.awt.image.SampleModel;
import java.util.Map;
import java.util.stream.IntStream;
import java.awt.Shape;
Expand All @@ -27,6 +29,8 @@

// Test dependencies
import org.junit.jupiter.api.Test;

import static org.apache.sis.feature.Assertions.assertPixelsEqual;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.sis.image.processing.isoline.IsolinesTest;
import org.apache.sis.test.TestCase;
Expand Down Expand Up @@ -110,4 +114,42 @@ public void testIsolines() {
IsolinesTest.verifyIsolineFromMultiCells(assertSingleton(r.values()));
} while ((parallel = !parallel) == true);
}

/**
* Verify that {@link ImageProcessor#reformat(RenderedImage, SampleModel) reformat} properly adapt tile size
* according to given parameters.
*/
@Test
public void changeTileSize() {
changeTileSize(12, 12, 4, 2);
changeTileSize(64, 64, 32, 32);
changeTileSize(50, 50, 5, 5);
}

private void changeTileSize(int sourceImageWidth, int sourceImageHeight, int targetTileWidth, int targetTileHeight) {
// Fill source image
final var image = new BufferedImage(sourceImageWidth, sourceImageHeight, BufferedImage.TYPE_BYTE_GRAY);
final var canvas = image.getRaster();
for (int y = 0 ; y < image.getHeight() ; y++) {
for (int x = 0 ; x < image.getWidth() ; x++) {
canvas.setSample(x, y, 0, x*y);
}
}

// Prepare target image layout
final var tileModel = image.getSampleModel().createCompatibleSampleModel(targetTileWidth, targetTileHeight);
final var preferredTileSize = new Dimension(tileModel.getWidth(), tileModel.getHeight());
processor.setImageLayout(new ImageLayout(tileModel, preferredTileSize, true, false, true, null));

// Execute and verify twice: sequential then parallel
boolean parallel = false;
final var imageBounds = new Rectangle(0, 0, image.getWidth(), image.getHeight());
do {
processor.setExecutionMode(parallel ? ImageProcessor.Mode.SEQUENTIAL : ImageProcessor.Mode.PARALLEL);
final RenderedImage reformatted = processor.reformat(image, null);
assertPixelsEqual(image, imageBounds, reformatted, imageBounds);
assertEquals(tileModel.getWidth(), reformatted.getTileWidth(), "Reformatted image tile width");
assertEquals(tileModel.getHeight(), reformatted.getTileHeight(), "Reformatted image tile height");
} while ((parallel = !parallel) == true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@
*/
package org.apache.sis.storage.geotiff;

import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Files;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.util.Utilities;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.geometry.Envelopes;
Expand All @@ -44,10 +45,13 @@
import org.apache.sis.referencing.operation.matrix.Matrix4;

// Test dependencies
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.apache.sis.test.Assertions.assertSingleton;
import static org.apache.sis.feature.Assertions.assertGridToCornerEquals;
import static org.apache.sis.feature.Assertions.assertPixelsEqual;
import org.apache.sis.test.TestCase;
import org.apache.sis.referencing.crs.HardCodedCRS;
import org.apache.sis.referencing.operation.HardCodedConversions;
Expand Down Expand Up @@ -138,7 +142,7 @@ public void testNonLinearVerticalTransform() throws Exception {
*/
@Test
public void testWriteUntiled() throws Exception {
testWrite(UNTILED, new Rectangle(32, 16), null, 1054);
testWrite(new Rectangle(32, 16), null);
}

/**
Expand All @@ -149,19 +153,31 @@ public void testWriteUntiled() throws Exception {
@Test
public void testWriteTiled() throws Exception {
final var tileSize = new Dimension(16, 16); // TIFF tile size must be multiple of 16.
testWrite(TILED, new Rectangle(tileSize.width * 3, tileSize.height * 2), tileSize, 2334);
testWrite(new Rectangle(tileSize.width * 3, tileSize.height * 2), tileSize);
}

/**
* Writes an image and compare with the {@code "tiled.tiff"} file.
* This test differs from {@link #testWriteTiled()} because it requests a tile size that is not accepted as is by
* geotiff.
* </br>
* The aim of this test is to ensure that Geotiff writer will adapt tile size according to the Tiff standard.
* It requests tiles of size 19, and expect the Geotiff writer to adapt request to write tiles of size 16 or 32.
*/
@Test
public void testWriteTiledAdapted() throws Exception {
final var tileSize = new Dimension(7, 7);
testWrite(new Rectangle(64, 64), tileSize);
}

/**
* Implementation of {@link #testWriteUntiled()} and {@link #testWriteTiled()}.
*
* @param filename name of the file which contain the expected image.
* @param bounds bounds of the image to create.
* @param tileSize size of the tiles, or {@code null} for the image size.
* @param length expected length in bytes.
*/
private static void testWrite(final String filename, final Rectangle bounds, final Dimension tileSize, final int length)
throws TransformException, DataStoreException, IOException
private static void testWrite(final Rectangle bounds, final Dimension tileSize)
throws TransformException, DataStoreException
{
/*
* We need a CRS which has no EPSG code for ensuring that the test write the same GeoTIFF keys
Expand All @@ -177,17 +193,99 @@ private static void testWrite(final String filename, final Rectangle bounds, fin
.flipGridAxis(1)
.build();

final var buffer = new ByteArrayOutputStream(length);
final var buffer = new ByteArrayOutputStream();
try (DataStore ds = DataStores.openWritable(buffer, "geotiff")) {
assertInstanceOf(GeoTiffStore.class, ds).append(coverage, null);
}

final byte[] actual = buffer.toByteArray();
final byte[] expected;
try (InputStream in = GeoTiffStoreTest.class.getResourceAsStream(filename)) {
assertNotNull(in, filename);
expected = in.readAllBytes();
try (var store = new GeoTiffStore(new GeoTiffStoreProvider(), new StorageConnector(ByteBuffer.wrap(actual)))) {
var coverageToValidate = store.components().get(0).read(null);
final var expectedGridGeom = coverage.getGridGeometry();
final var actualGridGeom = coverageToValidate.getGridGeometry();
assertTrue(
Utilities.equalsApproximately(expectedGridGeom, actualGridGeom),
() -> String.format(
"Written grid geometry differs from original one.%nOriginal:%n%s%nWritten:%n%s%n",
expectedGridGeom, actualGridGeom
)
);

assertTrue(
Utilities.equalsApproximately(expectedGridGeom, actualGridGeom),
() -> String.format(
"Written grid geometry differs from original one.%nOriginal:%n%s%nWritten:%n%s%n",
expectedGridGeom, actualGridGeom
)
);

final var expectedSampleDims = coverage.getSampleDimensions();
final var actualSampleDims = coverageToValidate.getSampleDimensions();
assertTrue(
Utilities.equalsApproximately(expectedSampleDims, actualSampleDims),
() -> String.format(
"Written Sample dimensions differ from original one.%nOriginal:%n%s%nWritten:%n%s%n",
expectedSampleDims, actualSampleDims
)
);

final var actualRendering = coverageToValidate.render(null);
assertPixelsEqual(coverage.render(null), null, actualRendering, null);
// If user requested a tiled dataset, we must ensure the written Geotiff file has been tiled
if (tileSize != null && (tileSize.getWidth() < bounds.getWidth() || tileSize.getHeight() < bounds.getHeight())) {
assertTiling(actualRendering, tileSize, 16);
}
}
}

/**
* Represent the side of the tile being evaluated. Either width (X) or height (Y).
*/
private enum TileAxis { width, height }

/**
* Verify that given image tiling respects user tiling request, modulo a given restriction.
* The restriction maps Tiff standard requirement for tile size to be multiple of a given factor.
* </br>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a small detail, but the rest of SIS rather uses <p>...</p> instead of <br/> (the slash also appears to be in the wrong side in this line). This is for semantic reason (the same reason why we use <i>, <em>, <var> or <dfn> in different contexts, even if they all appear in italic in the browser): the semantic is that we want to start a new paragraph.

* It means that if user requests a tile size of 3, but the restriction factor is 2,
* then we expect the image to use a tile size of either 2 or 4,
* which are the nearest enclosing multiples of 2 for request 3.
*
* @param actualRendering The image to control tiling on.
* @param tileSize The tile size requested by user.
* @param tileSizeMultiple A factor to use to adapted requested tile size.
*/
private static void assertTiling(RenderedImage actualRendering, Dimension tileSize, int tileSizeMultiple) {
assertTileSize(TileAxis.width, actualRendering.getWidth(), actualRendering.getTileWidth(), tileSize.width, tileSizeMultiple);
assertTileSize(TileAxis.height, actualRendering.getHeight(), actualRendering.getTileHeight(), tileSize.height, tileSizeMultiple);
}

/**
* Test a specific tile side according to requirements expressed by {@link #assertTiling(RenderedImage, Dimension, int)}.
*
* @param axis Which side of the tiling is being tested. Used for assertion error message formatting.
* @param imgSize The image actual size along tested side (its {@link RenderedImage#getWidth() width} or {@link RenderedImage#getHeight() height}).
* @param imgActualTileSize The image actual tile size along tested side (its {@link RenderedImage#getTileWidth() tile width} or {@link RenderedImage#getTileHeight() tile height}).
* @param requestedTileSize User request tile size along the side to test.
* @param tileSizeMultiple The restriction factor: actual tile size must be a multiple of this value, independently of the user request.
*/
private static void assertTileSize(TileAxis axis, int imgSize, int imgActualTileSize, int requestedTileSize, int tileSizeMultiple) {
if (imgSize > requestedTileSize) {
final int modulo = requestedTileSize % tileSizeMultiple;
if (modulo == 0) {
assertEquals(requestedTileSize, imgActualTileSize, () -> "Tile " + axis);
} else if (requestedTileSize < tileSizeMultiple) {
assertEquals(tileSizeMultiple, imgActualTileSize, () -> "Tile " + axis);
} else {
final var minTileSize = requestedTileSize - modulo;
final var maxTileSize = requestedTileSize + (tileSizeMultiple - modulo);
assertTrue(imgActualTileSize == minTileSize || imgActualTileSize == maxTileSize,
() -> String.format(
"Tile %s should be either %d or %d (because it must be a multiple of %d), but it is %d",
axis, minTileSize, maxTileSize, tileSizeMultiple, imgActualTileSize
)
);
}
}
assertArrayEquals(expected, actual);
assertEquals(length, actual.length);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Objects;
import java.util.Optional;
import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.util.internal.shared.Numerics;


/**
Expand Down Expand Up @@ -204,6 +205,16 @@ assert isNotDebug(mode) : ((object1 != null) ? object1.getClass()
}
return true;
}

if (object1 instanceof Number && object2 instanceof Number) {
final Number n1 = (Number) object1;
final Number n2 = (Number) object2;
return (n1 == n2 || (
(n1 instanceof Double || n1 instanceof Float || n2 instanceof Double || n2 instanceof Float)
&& Numerics.epsilonEqual(n1.doubleValue(), n2.doubleValue(), mode)
));
}

return Objects.deepEquals(object1, object2);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,11 @@ public static boolean epsilonEqual(final double v1, final double v2, final doubl
public static boolean epsilonEqual(final double v1, final double v2, final ComparisonMode mode) {
if (mode.isApproximate()) {
final double mg = max(abs(v1), abs(v2));
if (mg != Double.POSITIVE_INFINITY) {
/*
* If one of the numbers to compare is not finite, it is not possible to compare them using an epsilon.
* In such cases, we must fall back to the standard equal to check if their bit representation is the same.
*/
if (Double.isFinite(mg)) {
return epsilonEqual(v1, v2, COMPARISON_THRESHOLD * mg);
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down