diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/Category.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/Category.java
index f88ed0593d9..04706fc0622 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/Category.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/Category.java
@@ -23,6 +23,9 @@
import java.io.Serializable;
import static java.lang.Double.doubleToRawLongBits;
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.InternationalString;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
@@ -71,7 +74,7 @@
* @version 1.1
* @since 1.0
*/
-public class Category implements Serializable {
+public class Category implements LenientComparable, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
@@ -507,29 +510,41 @@ public int hashCode() {
* @param object the object to compare with.
* @return {@code true} if the given object is equal to this category.
*/
+ public final boolean equals(final Object object) {
+ return equals(object, ComparisonMode.STRICT);
+ }
+
+ /**
+ * Compares this category with the given object for equality at the given level of strictness.
+ *
+ *
+ * - {@link ComparisonMode#STRICT}: same implementation class, same name, exact range and transfer function.
+ * - {@link ComparisonMode#BY_CONTRACT}: same name, exact range and transfer function; class may differ.
+ * - {@link ComparisonMode#IGNORE_METADATA} and more lenient: range and transfer function only (name is ignored).
+ * - {@link ComparisonMode#APPROXIMATE}: range and transfer function approximately equal.
+ *
+ *
+ * @param object the object to compare with.
+ * @param mode the comparison strictness level.
+ * @return {@code true} if both objects are equal according the given comparison mode.
+ */
@Override
- public boolean equals(final Object object) {
- if (object == this) {
- // Slight optimization
- return true;
- }
- if (object != null && getClass().equals(object.getClass())) {
- final Category that = (Category) object;
- if (name.equals(that.name)) {
- final NumberRange> other = that.range;
- /*
- * The NumberRange.equals(Object) comparison is not sufficient because it considers all NaN values as equal.
- * For the purpose of Category, we need to distinguish the different NaN values.
- */
- if (range == other || (range.equals(other)
- && doubleToRawLongBits(range.getMinDouble()) == doubleToRawLongBits(other.getMinDouble())
- && doubleToRawLongBits(range.getMaxDouble()) == doubleToRawLongBits(other.getMaxDouble())))
- {
- return toConverse.equals(that.toConverse);
- }
+ public boolean equals(final Object object, final ComparisonMode mode) {
+ if (object == this) return true;
+ if (!(object instanceof Category)) return false;
+ final Category that = (Category) object;
+
+ switch (mode) {
+ case STRICT: {
+ if (!getClass().equals(that.getClass())) return false;
+ }
+ case BY_CONTRACT: {
+ if (!name.equals(that.name)) return false;
}
+ default:
+ return Utilities.deepEquals(range, that.range, mode)
+ && Utilities.deepEquals(toConverse, that.toConverse, mode);
}
- return false;
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/CategoryList.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/CategoryList.java
index bba233abf13..75cda8ad26a 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/CategoryList.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/CategoryList.java
@@ -32,6 +32,8 @@
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.LenientComparable;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.math.MathFunctions;
@@ -69,7 +71,7 @@
*
* @author Martin Desruisseaux (IRD, Geomatys)
*/
-final class CategoryList extends AbstractList implements MathTransform1D, Serializable {
+final class CategoryList extends AbstractList implements MathTransform1D, LenientComparable, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
@@ -825,16 +827,34 @@ public final Category get(final int i) {
* Compares the specified object with this category list for equality.
*/
@Override
- public boolean equals(final Object object) {
+ public final boolean equals(final Object object) {
if (object instanceof CategoryList) {
- final CategoryList that = (CategoryList) object;
- if (Arrays.equals(categories, that.categories)) {
- assert Arrays.equals(minimums, that.minimums);
- } else {
+ return equals(object, ComparisonMode.STRICT);
+ }
+ return super.equals(object);
+ }
+
+ /**
+ * Compares this category list with the given object for equality at the given level of strictness.
+ * The comparison is performed element-wise using {@link Category#equals(Object, ComparisonMode)}.
+ *
+ * @param object the object to compare with.
+ * @param mode the comparison strictness level.
+ * @return {@code true} if both lists are equal according the given comparison mode.
+ */
+ @Override
+ public boolean equals(final Object object, final ComparisonMode mode) {
+ if (object == this) return true;
+ if (!(object instanceof CategoryList)) return false;
+ final CategoryList that = (CategoryList) object;
+ final int count = categories.length;
+ if (that.categories.length != count) return false;
+ for (int i = 0; i < count; i++) {
+ if (!categories[i].equals(that.categories[i], mode)) {
return false;
}
}
- return super.equals(object);
+ return true;
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/SampleDimension.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/SampleDimension.java
index 4826bb42dea..90c462ca0d2 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/SampleDimension.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/SampleDimension.java
@@ -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;
@@ -79,13 +82,13 @@
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.5
+ * @version 1.7
*
* @see org.opengis.metadata.content.SampleDimension
*
* @since 1.0
*/
-public class SampleDimension implements IdentifiedType, Serializable {
+public class SampleDimension implements IdentifiedType, LenientComparable, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
@@ -487,15 +490,29 @@ public int hashCode() {
* @return {@code true} if the given object is equal to this sample dimension.
*/
@Override
- public boolean equals(final Object object) {
- if (object == this) {
- return true;
- }
- if (object instanceof SampleDimension) {
- final SampleDimension that = (SampleDimension) object;
- return name.equals(that.name) && Objects.equals(background, that.background) && categories.equals(that.categories);
+ public final boolean equals(final Object object) {
+ return equals(object, ComparisonMode.STRICT);
+ }
+
+ @Override
+ public boolean equals(Object other, ComparisonMode mode) {
+ if (other == this) return true;
+ if (!(other instanceof SampleDimension)) return false;
+
+ switch (mode) {
+ case STRICT: {
+ if (other.getClass() == getClass()) {
+ final SampleDimension that = (SampleDimension) other;
+ return name.equals(that.name) && Objects.equals(background, that.background) && categories.equals(that.categories);
+ }
+ }
+ default: {
+ final var otherDim = (SampleDimension) other;
+ return Utilities.deepEquals(this.transferFunction, otherDim.transferFunction, mode)
+ && Utilities.deepEquals(this.categories, otherDim.categories, mode)
+ && Utilities.deepEquals(this.background, otherDim.background, mode);
+ }
}
- return false;
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/CategoryListTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/CategoryListTest.java
index ea32bb2589f..4e2ecfeb640 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/CategoryListTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/CategoryListTest.java
@@ -24,6 +24,7 @@
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.measure.NumberRange;
+import org.apache.sis.util.ComparisonMode;
// Test dependencies
import org.junit.jupiter.api.Test;
@@ -414,4 +415,116 @@ public void testFromConvertedCategories() {
assertTrue(category.toConverse.isIdentity());
}
}
+
+ /**
+ * Tests {@link CategoryList#equals(Object, ComparisonMode)} for all comparison modes.
+ * Covers null/incompatible-type rejection, empty list, element-wise comparison,
+ * size mismatch, NaN-ordinal differences, name-only differences, and approximate
+ * transfer-function tolerance.
+ */
+ @Test
+ public void testLenientEquality() {
+ final CategoryList list1 = CategoryList.create(categories(), null);
+
+ // ------------------------------------------------------------------
+ // Null and incompatible type.
+ // ------------------------------------------------------------------
+ assertFalse(list1.equals(null, ComparisonMode.STRICT), "null/STRICT");
+ assertFalse(list1.equals("not a list", ComparisonMode.APPROXIMATE), "String/APPROXIMATE");
+
+ // ------------------------------------------------------------------
+ // Empty list: same reference → true.
+ // ------------------------------------------------------------------
+ assertTrue(CategoryList.EMPTY.equals(CategoryList.EMPTY, ComparisonMode.STRICT), "empty/STRICT");
+
+ // ------------------------------------------------------------------
+ // Two lists built from independent category arrays with the same
+ // configuration. All modes must return true.
+ // ------------------------------------------------------------------
+ final CategoryList list2 = CategoryList.create(categories(), null);
+ assertTrue(list1.equals(list2, ComparisonMode.STRICT), "same-cfg/STRICT");
+ assertTrue(list1.equals(list2, ComparisonMode.BY_CONTRACT), "same-cfg/BY_CONTRACT");
+ assertTrue(list1.equals(list2, ComparisonMode.IGNORE_METADATA), "same-cfg/IGNORE_METADATA");
+ assertTrue(list1.equals(list2, ComparisonMode.APPROXIMATE), "same-cfg/APPROXIMATE");
+
+ // ------------------------------------------------------------------
+ // Different size: list of 5 categories vs 3 categories.
+ // ------------------------------------------------------------------
+ final ToNaN toNaN = new ToNaN();
+ final Category[] fewer = {
+ new Category("No data", NumberRange.create( 0, true, 0, true), null, null, toNaN),
+ new Category("Land", NumberRange.create( 7, true, 7, true), null, null, toNaN),
+ new Category("Temperature", NumberRange.create( 10, true, 100, false),
+ (MathTransform1D) MathTransforms.linear(0.1, 5), null, toNaN)
+ };
+ final CategoryList shortList = CategoryList.create(fewer, null);
+ assertFalse(list1.equals(shortList, ComparisonMode.STRICT), "diff-size/STRICT");
+ assertFalse(list1.equals(shortList, ComparisonMode.APPROXIMATE), "diff-size/APPROXIMATE");
+
+ // ------------------------------------------------------------------
+ // Different NaN ordinal in a qualitative category.
+ // Replace sample value 0 with a category that forces ordinal 99.
+ // ------------------------------------------------------------------
+ final Category[] diffNaN = {
+ new Category("No data", NumberRange.create( 0, true, 0, true), null, null, (v) -> 99),
+ new Category("Land", NumberRange.create( 7, true, 7, true), null, null, toNaN),
+ new Category("Clouds", NumberRange.create( 3, true, 3, true), null, null, toNaN),
+ new Category("Temperature", NumberRange.create( 10, true, 100, false),
+ (MathTransform1D) MathTransforms.linear(0.1, 5), null, toNaN),
+ new Category("Foo", NumberRange.create(100, true, 120, false),
+ (MathTransform1D) MathTransforms.linear(-1, 3), null, toNaN)
+ };
+ final CategoryList listDiffNaN = CategoryList.create(diffNaN, null);
+ assertFalse(list1.equals(listDiffNaN, ComparisonMode.STRICT), "diff-NaN/STRICT");
+ assertTrue(list1.equals(listDiffNaN, ComparisonMode.APPROXIMATE), "diff-NaN/APPROXIMATE");
+
+ // ------------------------------------------------------------------
+ // Different name only in the qualitative "No data" category.
+ // ------------------------------------------------------------------
+ final Category[] diffName = {
+ new Category("Renamed", NumberRange.create( 0, true, 0, true), null, null, toNaN),
+ new Category("Land", NumberRange.create( 7, true, 7, true), null, null, toNaN),
+ new Category("Clouds", NumberRange.create( 3, true, 3, true), null, null, toNaN),
+ new Category("Temperature", NumberRange.create( 10, true, 100, false),
+ (MathTransform1D) MathTransforms.linear(0.1, 5), null, toNaN),
+ new Category("Foo", NumberRange.create(100, true, 120, false),
+ (MathTransform1D) MathTransforms.linear(-1, 3), null, toNaN)
+ };
+ final CategoryList listDiffName = CategoryList.create(diffName, null);
+ assertFalse(list1.equals(listDiffName, ComparisonMode.STRICT), "diff-name/STRICT");
+ assertTrue (list1.equals(listDiffName, ComparisonMode.IGNORE_METADATA), "diff-name/IGNORE_METADATA");
+
+ // ------------------------------------------------------------------
+ // Significantly different transfer function (scale 0.1 → 0.2).
+ // ------------------------------------------------------------------
+ final Category[] bigDiff = {
+ new Category("No data", NumberRange.create( 0, true, 0, true), null, null, toNaN),
+ new Category("Land", NumberRange.create( 7, true, 7, true), null, null, toNaN),
+ new Category("Clouds", NumberRange.create( 3, true, 3, true), null, null, toNaN),
+ new Category("Temperature", NumberRange.create( 10, true, 100, false),
+ (MathTransform1D) MathTransforms.linear(0.2, 5), null, toNaN),
+ new Category("Foo", NumberRange.create(100, true, 120, false),
+ (MathTransform1D) MathTransforms.linear(-1, 3), null, toNaN)
+ };
+ final CategoryList listBigDiff = CategoryList.create(bigDiff, null);
+ assertFalse(list1.equals(listBigDiff, ComparisonMode.STRICT), "big-diff/STRICT");
+ assertFalse(list1.equals(listBigDiff, ComparisonMode.APPROXIMATE), "big-diff/APPROXIMATE");
+
+ // ------------------------------------------------------------------
+ // Tiny transfer function difference: offset 5.0 vs 5.0 - 1e-13.
+ // Relative threshold for offset ≈ 5 is 5E-13 > 1E-13, so within tolerance.
+ // ------------------------------------------------------------------
+ final Category[] tinyDiff = {
+ new Category("No data", NumberRange.create( 0, true, 0, true), null, null, toNaN),
+ new Category("Land", NumberRange.create( 7, true, 7, true), null, null, toNaN),
+ new Category("Clouds", NumberRange.create( 3, true, 3, true), null, null, toNaN),
+ new Category("Temperature", NumberRange.create( 10, true, 100, false),
+ (MathTransform1D) MathTransforms.linear(0.1, 5.0 - 1e-13), null, toNaN),
+ new Category("Foo", NumberRange.create(100, true, 120, false),
+ (MathTransform1D) MathTransforms.linear(-1, 3), null, toNaN)
+ };
+ final CategoryList listTinyDiff = CategoryList.create(tinyDiff, null);
+ assertFalse(list1.equals(listTinyDiff, ComparisonMode.STRICT), "tiny-diff/STRICT");
+ assertTrue (list1.equals(listTinyDiff, ComparisonMode.APPROXIMATE), "tiny-diff/APPROXIMATE");
+ }
}
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/CategoryTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/CategoryTest.java
index 51f7bf25e89..4313bc79f35 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/CategoryTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/CategoryTest.java
@@ -20,8 +20,11 @@
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.measure.MeasurementRange;
import org.apache.sis.measure.NumberRange;
+import org.apache.sis.measure.Units;
import org.apache.sis.math.MathFunctions;
+import org.apache.sis.util.ComparisonMode;
// Test dependencies
import org.junit.jupiter.api.Test;
@@ -240,4 +243,106 @@ public void testCategoryNaN() {
assertTrue (category.toConverse.isIdentity());
assertFalse (category.isQuantitative());
}
+
+ /**
+ * Tests {@link Category#equals(Object, ComparisonMode)} for all comparison modes.
+ * Covers qualitative and quantitative categories, name differences, NaN ordinal
+ * differences, approximate transform comparison, and category-vs-converse inequality.
+ */
+ @Test
+ public void testLenientEquality() {
+ final Category qualA = new Category("Water", NumberRange.create(1, true, 1, true), null, null, new ToNaN());
+ final Category qualB = new Category("Water", NumberRange.create(1, true, 1, true), null, null, new ToNaN());
+
+ // Null / incompatible type — all modes must return false.
+ assertFalse(qualA.equals(null, ComparisonMode.STRICT), "null/STRICT");
+ assertFalse(qualA.equals("not a category", ComparisonMode.APPROXIMATE), "String/APPROXIMATE");
+
+ // Self-equality — all modes must return true.
+ assertTrue(qualA.equals(qualA, ComparisonMode.STRICT), "self/STRICT");
+ assertTrue(qualA.equals(qualA, ComparisonMode.BY_CONTRACT), "self/BY_CONTRACT");
+ assertTrue(qualA.equals(qualA, ComparisonMode.IGNORE_METADATA), "self/IGNORE_METADATA");
+ assertTrue(qualA.equals(qualA, ComparisonMode.APPROXIMATE), "self/APPROXIMATE");
+
+ // Qualitative: same configuration (fresh ToNaN, same sample → same NaN ordinal).
+ assertTrue(qualA.equals(qualB, ComparisonMode.STRICT), "qual-same/STRICT");
+ assertTrue(qualA.equals(qualB, ComparisonMode.BY_CONTRACT), "qual-same/BY_CONTRACT");
+ assertTrue(qualA.equals(qualB, ComparisonMode.IGNORE_METADATA), "qual-same/IGNORE_METADATA");
+ assertTrue(qualA.equals(qualB, ComparisonMode.APPROXIMATE), "qual-same/APPROXIMATE");
+
+ // Qualitative: same sample range but different NaN ordinal.
+ // Force ordinal 99 for qualC; standard ToNaN for sample value 1 yields ordinal 1.
+ final Category qualC = new Category("Water", NumberRange.create(1, true, 1, true), null, null, (v) -> 99);
+ assertFalse(qualA.equals(qualC, ComparisonMode.STRICT), "qual-diffNaN/STRICT");
+ assertTrue(qualA.equals(qualC, ComparisonMode.APPROXIMATE), "qual-diffNaN/APPROXIMATE");
+
+ // Qualitative: same NaN ordinal but different name.
+ final Category qualD = new Category("Land", NumberRange.create(1, true, 1, true), null, null, new ToNaN());
+ assertFalse(qualA.equals(qualD, ComparisonMode.STRICT), "qual-diffName/STRICT");
+ assertFalse(qualA.equals(qualD, ComparisonMode.BY_CONTRACT), "qual-diffName/BY_CONTRACT");
+ assertTrue (qualA.equals(qualD, ComparisonMode.IGNORE_METADATA), "qual-diffName/IGNORE_METADATA");
+ assertTrue (qualA.equals(qualD, ComparisonMode.APPROXIMATE), "qual-diffName/APPROXIMATE");
+
+ // Category vs its converse (ConvertedCategory): must be false in all modes.
+ final Category qualConverse = qualA.converse;
+ assertNotSame(qualA, qualConverse);
+ assertFalse(qualA.equals(qualConverse, ComparisonMode.STRICT), "vs-converse/STRICT");
+ assertFalse(qualA.equals(qualConverse, ComparisonMode.BY_CONTRACT), "vs-converse/BY_CONTRACT");
+ assertFalse(qualA.equals(qualConverse, ComparisonMode.IGNORE_METADATA), "vs-converse/IGNORE_METADATA");
+ assertFalse(qualA.equals(qualConverse, ComparisonMode.APPROXIMATE), "vs-converse/APPROXIMATE");
+
+ // ------------------------------------------------------------------
+ // Quantitative categories with linear transfer function.
+ // ------------------------------------------------------------------
+ final MathTransform1D tf1 = (MathTransform1D) MathTransforms.linear(0.1, 5.0);
+ final Category quantA = new Category("Temperature", NumberRange.create(10, true, 100, false), tf1, null, null);
+ final Category quantB = new Category("Temperature", NumberRange.create(10, true, 100, false), tf1, null, null);
+
+ // Same configuration — all modes must return true.
+ assertTrue(quantA.equals(quantB, ComparisonMode.STRICT), "quant-same/STRICT");
+ assertTrue(quantA.equals(quantB, ComparisonMode.BY_CONTRACT), "quant-same/BY_CONTRACT");
+ assertTrue(quantA.equals(quantB, ComparisonMode.IGNORE_METADATA), "quant-same/IGNORE_METADATA");
+ assertTrue(quantA.equals(quantB, ComparisonMode.APPROXIMATE), "quant-same/APPROXIMATE");
+
+ // Different name only.
+ final Category quantC = new Category("Renamed", NumberRange.create(10, true, 100, false), tf1, null, null);
+ assertFalse(quantA.equals(quantC, ComparisonMode.STRICT), "quant-diffName/STRICT");
+ assertFalse(quantA.equals(quantC, ComparisonMode.BY_CONTRACT), "quant-diffName/BY_CONTRACT");
+ assertTrue (quantA.equals(quantC, ComparisonMode.IGNORE_METADATA), "quant-diffName/IGNORE_METADATA");
+ assertTrue (quantA.equals(quantC, ComparisonMode.APPROXIMATE), "quant-diffName/APPROXIMATE");
+
+ // Significantly different transfer function: scale 0.1 vs 0.2.
+ final MathTransform1D tf2 = (MathTransform1D) MathTransforms.linear(0.2, 5.0);
+ final Category quantD = new Category("Temperature", NumberRange.create(10, true, 100, false), tf2, null, null);
+ assertFalse(quantA.equals(quantD, ComparisonMode.STRICT), "quant-bigDiff/STRICT");
+ assertFalse(quantA.equals(quantD, ComparisonMode.APPROXIMATE), "quant-bigDiff/APPROXIMATE");
+
+ // Tiny transfer function difference: offset 5.0 vs 5.0 - 1e-13.
+ // Relative threshold for offset ≈ 5.0 is 5.0 * 1E-13 = 5E-13 > 1E-13, so within tolerance.
+ final MathTransform1D tf3 = (MathTransform1D) MathTransforms.linear(0.1, 5.0 - 1e-13);
+ final Category quantE = new Category("Temperature", NumberRange.create(10, true, 100, false), tf3, null, null);
+ assertFalse(quantA.equals(quantE, ComparisonMode.STRICT), "quant-tinyDiff/STRICT");
+ assertTrue (quantA.equals(quantE, ComparisonMode.APPROXIMATE), "quant-tinyDiff/APPROXIMATE");
+
+ // ------------------------------------------------------------------
+ // Category with MeasurementRange (identity transform, unit-bearing range).
+ // ------------------------------------------------------------------
+ final MathTransform1D identity = (MathTransform1D) MathTransforms.identity(1);
+ final Category measA = new Category("Celsius",
+ MeasurementRange.create(10f, true, 30f, true, Units.CELSIUS),
+ identity, Units.CELSIUS, null);
+ final Category measB = new Category("Celsius",
+ MeasurementRange.create(10f, true, 30f, true, Units.CELSIUS),
+ identity, Units.CELSIUS, null);
+
+ // Same unit — equal in STRICT.
+ assertTrue(measA.equals(measB, ComparisonMode.STRICT), "meas-sameUnit/STRICT");
+
+ // Different units: CELSIUS vs KELVIN — ranges differ, must be false.
+ final Category measK = new Category("Kelvin",
+ MeasurementRange.create(10f, true, 30f, true, Units.KELVIN),
+ identity, Units.KELVIN, null);
+ assertFalse(measA.equals(measK, ComparisonMode.STRICT), "meas-diffUnit/STRICT");
+ assertFalse(measA.equals(measK, ComparisonMode.APPROXIMATE), "meas-diffUnit/APPROXIMATE");
+ }
}
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/SampleDimensionTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/SampleDimensionTest.java
index a2d38f06285..5587a1cae5f 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/SampleDimensionTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/SampleDimensionTest.java
@@ -25,6 +25,7 @@
import org.apache.sis.math.MathFunctions;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.measure.Units;
+import org.apache.sis.util.ComparisonMode;
// Test dependencies
import org.junit.jupiter.api.Test;
@@ -223,4 +224,125 @@ public void testBuilder() {
assertEquals("Clouds", categories.remove(1).getName().toString());
assertEquals(2, categories.size());
}
+
+ @Test
+ public void testNullAndIncompatibleTypesAreNeverEqual() {
+ final var base = new SampleDimension.Builder().setName("base").build();
+ assertFalse(base.equals(null, ComparisonMode.STRICT));
+ assertFalse(base.equals("other", ComparisonMode.STRICT));
+ assertFalse(base.equals(null));
+ assertFalse(base.equals("other"));
+ assertFalse(base.equals(null, ComparisonMode.APPROXIMATE));
+ assertFalse(base.equals("other", ComparisonMode.APPROXIMATE));
+ }
+
+ /**
+ * Verify strict equality behavior, and ensure standard {@link Object#equals(Object)} is consistent with strict equality.
+ *
+ * @see org.apache.sis.util.LenientComparable#equals(Object, ComparisonMode)
+ * @see ComparisonMode#STRICT
+ */
+ @Test
+ public void testStrictEquality() {
+ final var base = new SampleDimension.Builder()
+ .addQualitative("Clouds", 1)
+ .addQualitative("Lands", 2)
+ .setName("base")
+ .build();
+ final var strictlyEqToBase = new SampleDimension.Builder()
+ .addQualitative("Clouds", 1)
+ .addQualitative("Lands", 2)
+ .setName("base")
+ .build();
+
+ // Same categories and background as dim1, but different dimension name.
+ final var renamed = new SampleDimension.Builder()
+ .addQualitative("Clouds", 1)
+ .addQualitative("Lands", 2)
+ .setName("Different name")
+ .build();
+
+ // Ensure a sample dimension is strictly equal to itself
+ assertTrue (base.equals(base, ComparisonMode.STRICT), "A sample dimension must be strictly equal to itself");
+ assertTrue (base.equals(base), "A sample dimension must be equal to itself");
+
+ // Ensure strict comparison work as expected
+ assertTrue (base.equals(strictlyEqToBase, ComparisonMode.STRICT), "identical dimensions must be equal");
+ assertTrue (strictlyEqToBase.equals(base, ComparisonMode.STRICT), "equality must be symmetric");
+ assertEquals(base.hashCode(), strictlyEqToBase.hashCode(), "hashCode must be consistent with equals");
+
+ // Dimensions that differ only in name must NOT be equal under STRICT.
+ assertFalse(base.equals(renamed, ComparisonMode.STRICT), "different names must produce inequality");
+ assertFalse(renamed.equals(base, ComparisonMode.STRICT), "inequality must be symmetric");
+ }
+
+ /**
+ * Verifies the {@link org.apache.sis.util.LenientComparable#equals(Object, ComparisonMode)} contract
+ * for {@link ComparisonMode#APPROXIMATE}.
+ * In approximate mode the dimension name is not compared, so two dimensions that differ
+ * only by name must be considered equal, while a different transfer function must not.
+ */
+ @Test
+ public void testEqualsApproximate() {
+ final var base = new SampleDimension.Builder()
+ .setBackground(null, 0)
+ .addQualitative("Clouds", 1)
+ .addQuantitative("Temperature", 10, 200, 0.1, 5.0, Units.CELSIUS)
+ .setName("Base")
+ .build();
+ final var strictlyEqualToBase = new SampleDimension.Builder()
+ .setBackground(null, 0)
+ .addQualitative("Clouds", 1)
+ .addQuantitative("Temperature", 10, 200, 0.1, 5.0, Units.CELSIUS)
+ .setName("Base")
+ .build();
+ // Same structure as base but with an explicit different dimension name.
+ final SampleDimension renamed = new SampleDimension.Builder()
+ .setBackground(null, 0)
+ .addQualitative("Clouds", 1)
+ .addQuantitative("Temperature", 10, 200, 0.1, 5.0, Units.CELSIUS)
+ .setName("Renamed")
+ .build();
+
+ // Different scale in the transfer function — must not be equal even approximately.
+ final SampleDimension differentScale = new SampleDimension.Builder()
+ .setBackground(null, 0)
+ .addQualitative("Clouds", 1)
+ .addQuantitative("Temperature", 10, 200, 0.2, 5.0, Units.CELSIUS)
+ .build();
+
+ // Tiny difference in offset (differs by less than the APPROXIMATE threshold): should be equal.
+ final SampleDimension tinyOffsetDiff = new SampleDimension.Builder()
+ .setBackground(null, 0)
+ .addQualitative("Clouds", 1)
+ .addQuantitative("Temperature", 10, 200, 0.1, 5.0 - 1e-13, Units.CELSIUS)
+ .build();
+
+ assertTrue(base.equals(base, ComparisonMode.APPROXIMATE),
+ "A sample dimension should be approximately equal to itself");
+ // Different dimension name is ignored under APPROXIMATE — must be equal.
+ assertTrue (base.equals(strictlyEqualToBase, ComparisonMode.APPROXIMATE),
+ "two strictly equal sample dimensions should also be approximately equal");
+ assertTrue (strictlyEqualToBase.equals(base, ComparisonMode.APPROXIMATE),
+ "two strictly equal sample dimensions should also be approximately equal");
+
+ // Different dimension name is ignored under APPROXIMATE — must be equal.
+ assertTrue (base.equals(renamed, ComparisonMode.APPROXIMATE),
+ "name difference should be ignored on Sample dimension approximate equality");
+ assertTrue (renamed.equals(base, ComparisonMode.APPROXIMATE),
+ "name difference should be ignored on Sample dimension approximate equality");
+
+ // The same pair must NOT be equal under STRICT because names differ.
+ assertFalse(base.equals(renamed, ComparisonMode.STRICT), "STRICT must detect name difference");
+
+ // Different scale in the transfer function → not equal even approximately.
+ assertFalse(base.equals(differentScale, ComparisonMode.APPROXIMATE), "different scale must produce inequality");
+ assertFalse(differentScale.equals(base, ComparisonMode.APPROXIMATE), "different scale must produce inequality");
+
+ // A very little difference in transfer function offset should still mark both dimensions as approximately equal
+ assertTrue (base.equals(tinyOffsetDiff, ComparisonMode.APPROXIMATE),
+ "A tiny offset difference should not fail approximate equality");
+ assertTrue (tinyOffsetDiff.equals(base, ComparisonMode.APPROXIMATE),
+ "A tiny offset difference should not fail approximate equality");
+ }
}
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageProcessorTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageProcessorTest.java
index bf2fa4c1fea..f817f65f400 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageProcessorTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageProcessorTest.java
@@ -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;
@@ -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;
@@ -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);
+ }
}
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
index 5d495e9544b..ecad02e598c 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
@@ -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;
@@ -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;
@@ -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);
}
/**
@@ -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 not accepted as is by geotiff.
+ * 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
@@ -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.
+ *
+ * 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);
}
}
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/MeasurementRange.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/MeasurementRange.java
index 4c36100b25a..8b0638edbf6 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/MeasurementRange.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/MeasurementRange.java
@@ -21,7 +21,9 @@
import javax.measure.UnitConverter;
import javax.measure.IncommensurableException;
import org.apache.sis.math.NumberType;
+import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Numbers;
+import org.apache.sis.util.Utilities;
import org.apache.sis.util.resources.Errors;
@@ -479,8 +481,9 @@ public Range[] subtract(final Range range) throws IllegalArgumentException
* @return {@inheritDoc}
*/
@Override
- public boolean equals(final Object object) {
- return super.equals(object) && Objects.equals(unit, ((MeasurementRange>) object).unit);
+ public boolean equals(final Object object, ComparisonMode mode) {
+ return super.equals(object, mode)
+ && Utilities.deepEquals(unit, ((MeasurementRange>) object).unit, mode);
}
/**
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/Range.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/Range.java
index 016ff2169c1..ab67f954a7a 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/Range.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/Range.java
@@ -25,7 +25,10 @@
import org.apache.sis.math.NumberType;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArgumentCheckByAssertion;
+import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Emptiable;
+import org.apache.sis.util.LenientComparable;
+import org.apache.sis.util.Utilities;
import org.apache.sis.util.internal.shared.Strings;
import org.apache.sis.util.collection.CheckedContainer;
@@ -90,7 +93,7 @@
*
* @since 0.3
*/
-public class Range> implements CheckedContainer, Formattable, Emptiable, Serializable {
+public class Range> implements CheckedContainer, Formattable, Emptiable, LenientComparable, Serializable {
/**
* For cross-version compatibility.
*/
@@ -639,23 +642,8 @@ private int compareMaxTo(final E value, int position) {
* @return {@code true} if the given object is equal to this range.
*/
@Override
- public boolean equals(final Object object) {
- if (object == this) {
- return true;
- }
- if (object != null && object.getClass() == getClass()) {
- final Range> other = (Range>) object;
- if (Objects.equals(elementType, other.elementType)) {
- if (isEmpty()) {
- return other.isEmpty();
- }
- return Objects.equals(minValue, other.minValue) &&
- Objects.equals(maxValue, other.maxValue) &&
- isMinIncluded == other.isMinIncluded &&
- isMaxIncluded == other.isMaxIncluded;
- }
- }
- return false;
+ public final boolean equals(final Object object) {
+ return equals(object, ComparisonMode.STRICT);
}
/**
@@ -673,6 +661,41 @@ public int hashCode() {
return hash ^ (int) serialVersionUID;
}
+
+ @Override
+ public boolean equals(Object object, ComparisonMode mode) {
+ if (object == this) {
+ return true;
+ }
+ if (!(object instanceof Range)) return false;
+ final Range> other = (Range>) object;
+ switch (mode) {
+ case STRICT:
+ case BY_CONTRACT:
+ case IGNORE_METADATA: {
+ if (other.getClass() != getClass()) return false;
+ if (!Objects.equals(elementType, other.elementType)) return false;
+ if (isEmpty()) return other.isEmpty();
+ return Objects.equals(minValue, other.minValue)
+ && Objects.equals(maxValue, other.maxValue)
+ && isMinIncluded == other.isMinIncluded
+ && isMaxIncluded == other.isMaxIncluded;
+ }
+ default: {
+ return Utilities.deepEquals(minValue, other.minValue, mode)
+ && Utilities.deepEquals(maxValue, other.maxValue, mode)
+ /* TODO: we might want to improve this in the future to allow mixing different boundary types.
+ * For example, (0..256) is equivalent to [1..255] in the case of a discrete integer space,
+ * but not in continuous real number space.
+ * As it is difficult for now to properly manage such corner cases, we start strict,
+ * so we can improve this step by step in the future.
+ */
+ && isMinIncluded == other.isMinIncluded
+ && isMaxIncluded == other.isMaxIncluded;
+ }
+ }
+ }
+
/**
* Returns {@code true} if the given number is formatted with only one character.
* We will use less space if the minimum and maximum values are formatted using
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Utilities.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Utilities.java
index 4587b715721..4cdf227ea3a 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Utilities.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Utilities.java
@@ -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;
/**
@@ -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);
}
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
index 3a03b0fe2f3..bd7b23c21ba 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
@@ -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);
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a4413138c96..aaaabb3cb9f 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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