[ 
https://issues.apache.org/jira/browse/CASSJAVA-136?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18110972#comment-18110972
 ] 

Bret McGuire commented on CASSJAVA-136:
---------------------------------------

Gemini actually was helpful in analyzing this one.  My prompt first followed by 
Gemini's answer:


{noformat}
I have some Java code which implements equals() based on ESRI OGCGeometry 
classes derived from the input type.  The initial version 
(https://github.com/apache/cassandra-java-driver/blob/deffb9230cf5067e259a0ddee6434e2432155e47/core/src/main/java/com/datastax/dse/driver/internal/core/data/geometry/DefaultGeometry.java#L170-L180)
 implemented the comparison using the equals(OGCGeometry) method defined at 
https://github.com/Esri/geometry-api-java/blob/v1.2.1/src/main/java/com/esri/core/geometry/ogc/OGCGeometry.java#L213-L222.
  Let’s call this “Version 1”.  Note that at “Version 1” we were still using 
version 1.2.1 of the ESRI library.  The pull request at 
https://github.com/apache/cassandra-java-driver/pull/2077 update the ESRI 
library to 2.2.4. It also changed the DefaultGeometry.equals() method used to 
equals(Object) defined at 
https://github.com/Esri/geometry-api-java/blob/v2.2.4/src/main/java/com/esri/core/geometry/ogc/OGCGeometry.java#L809-L841.
 Let’s call this “Version 2”.  Finally the pull request at 
https://github.com/apache/cassandra-java-driver/pull/2093 changed the 
DefaultGeometry.equals() method to use the Equals(OGCGeometry) method 
introduced in ESRI 2.2.4 (see 
https://github.com/Esri/geometry-api-java/blob/v2.2.4/src/main/java/com/esri/core/geometry/ogc/OGCGeometry.java#L250-L269).
 Let’s call this “Version 3”. Note that the Equals() method is intended as an 
explicit replacement for the old equals(OGCGeometry) method in ESRI 1.2.1.  The 
integration test PointIT 
(https://github.com/apache/cassandra-java-driver/blob/4.19.3/integration-tests/src/test/java/com/datastax/dse/driver/api/core/data/geometry/PointIT.java)
 fails when run against Version 3 with the following exception:

[INFO] Running com.datastax.dse.driver.api.core.data.geometry.PointIT
[ERROR] Tests run: 11, Failures: 0, Errors: 1, Skipped: 2, Time elapsed: 0.287 
s <<< FAILURE! - in com.datastax.dse.driver.api.core.data.geometry.PointIT
[ERROR] 
com.datastax.dse.driver.api.core.data.geometry.PointIT.should_insert_as_map_keys
  Time elapsed: 0.001 s  <<< ERROR!
java.lang.IllegalArgumentException: Multiple entries with same key: POINT 
(1.7976931348623157e+308 4.9000000000000000e-324)=4 and POINT (0 0)=1
        at 
com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap.conflictException(ImmutableMap.java:382)
        at 
com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap.checkNoConflict(ImmutableMap.java:376)
        at 
com.datastax.oss.driver.shaded.guava.common.collect.RegularImmutableMap.checkNoConflictInKeyBucket(RegularImmutableMap.java:249)
        at 
com.datastax.oss.driver.shaded.guava.common.collect.RegularImmutableMap.fromEntryArrayCheckingBucketOverflow(RegularImmutableMap.java:136)
        at 
com.datastax.oss.driver.shaded.guava.common.collect.RegularImmutableMap.fromEntryArray(RegularImmutableMap.java:98)
        at 
com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap$Builder.build(ImmutableMap.java:579)
        at 
com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap$Builder.buildOrThrow(ImmutableMap.java:607)
        at 
com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap$Builder.build(ImmutableMap.java:594)
        at 
com.datastax.dse.driver.api.core.data.geometry.GeometryIT.should_insert_as_map_keys(GeometryIT.java:236)
        at 
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at 
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at 
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
             …

It’s fairly clear that when the test method at 
https://github.com/apache/cassandra-java-driver/blob/4.19.3/integration-tests/src/test/java/com/datastax/dse/driver/api/core/data/geometry/GeometryIT.java#L224-L248
 in the superclass runs we find that “Point.fromCoordinates(0, 0)” and 
“Point.fromCoordinates(Double.MAX_VALUE, Double.MIN_VALUE)” are determined to 
be equal to each other (and thus cannot be used as distinct map keys).  
Presumably this is because of the tolerances referenced in the comments around 
the Equals() method.

My question is why this test didn’t fail in the exact same way when using ESRI 
1.2.1 with Version 1.  The bodies of the methods used in Version 1 and Version 
3 are nearly identical… shouldn’t we see the exact same error with Version 1 
and, if not, why not?
{noformat}


{noformat}
The reason this test passed in **Version 1** (ESRI 1.2.1) but failed in 
**Version 3** (ESRI 2.2.4) lies in a critical internal change to ESRI’s 
`OGCGeometry.Equals()` implementation: **how coordinate tolerance/spatial 
references are determined when no SpatialReference is explicitly defined**.

While the logic inside `OGCGeometry.equals(OGCGeometry)` (v1.2.1) and 
`OGCGeometry.Equals(OGCGeometry)` (v2.2.4) looks nearly identical at first 
glance, the underlying behavior for empty or default spatial references shifted 
dramatically.

---

### The Underlying Difference

Both methods ultimately delegate equality checks to 
`OperatorEquals.local().execute(...)` using a **tolerance** calculated from the 
geometry's spatial reference:

```java
// Logic present in both ESRI v1.2.1 and v2.2.4
SpatialReference sr = getEsriSpatialReference();
return OperatorEquals.local().execute(
    getEsriGeometry(), 
    other.getEsriGeometry(), 
    sr, 
    null
);

```

The key divergence happens in how ESRI resolves `sr` when 
`getEsriSpatialReference()` returns `null` (which is default for driver `Point` 
instances created directly from raw coordinates):

#### 1. ESRI 1.2.1 (Version 1)

In v1.2.1, passing `sr = null` to `OperatorEquals` forced ESRI to use an 
**exact binary/coordinate comparison** (tolerance = `0.0`).

* `(0, 0)` and `(Double.MAX_VALUE, Double.MIN_VALUE)` evaluated to **`false`**.
* The geometries were considered distinct, generating distinct hash/bucket keys 
in Guava's `ImmutableMap.Builder`.
* The test **passed**.

#### 2. ESRI 2.2.4 (Version 3)

In v2.2.4, ESRI changed the fallback behavior when `sr = null` inside 
topological comparison operators. Instead of defaulting to exact exact-match 
tolerance (`0.0`), it derives a default tolerance dynamically based on the 
bounding envelope of the geometries being compared.

When comparing `(0, 0)` with `(Double.MAX_VALUE, Double.MIN_VALUE)`:

1. The combined bounding box envelope spans from `0` to `Double.MAX_VALUE` 
($\approx 1.79 \times 10^{308}$).
2. ESRI calculates a dynamic tolerance scaled to the envelope size 
($\text{Tolerance} \approx \text{Envelope Size} \times 10^{-8}$).
3. The computed tolerance becomes **enormous** ($\approx 10^{300}$).
4. Under a tolerance of $10^{300}$, the distance between $0$ and $1.79 \times 
10^{308}$ falls within the fuzzy threshold.
5. `OGCGeometry.Equals()` returns **`true`** for `(0, 0)` and `(MAX_VALUE, 
MIN_VALUE)`.

Because Guava's `ImmutableMap.Builder` checks key equality, both distinct 
points were determined to be duplicate keys, triggering:
`java.lang.IllegalArgumentException: Multiple entries with same key: POINT 
(1.7976931348623157e+308 4.9000000000000000e-324)=4 and POINT (0 0)=1`

---

### Comparison Matrix

| Version | ESRI Version | Method Called | Tolerance with `sr = null` | Result 
of `(0,0).equals(MAX, MIN)` |
| --- | --- | --- | --- | --- |
| **Version 1** | `1.2.1` | `equals(OGCGeometry)` | `0.0` (Strict exact match) 
| `false` (Test Passes) |
| **Version 2** | `2.2.4` | `equals(Object)` | N/A (Delegates to strict 
structural check) | `false` (Test Passes) |
| **Version 3** | `2.2.4` | `Equals(OGCGeometry)` | Dynamic envelope tolerance 
($\approx 10^{300}$) | **`true` (Test Fails)** |

---

### Solution / Takeaway

`OGCGeometry.Equals(OGCGeometry)` is designed for **topological spatial 
equivalence** under OGC standards (where geometries overlapping within a 
spatial tolerance are equal). It is generally unsafe for Java standard 
`equals()` contracts, `hashCode()` consistency, and Map key usage.

To fix Version 3, stick to structural/exact byte equality (like 
`equals(Object)` in Version 2) or explicit coordinate comparison, rather than 
OGC topological equivalence methods.
{noformat}


> PointIT is now failing after recent ninja fix
> ---------------------------------------------
>
>                 Key: CASSJAVA-136
>                 URL: https://issues.apache.org/jira/browse/CASSJAVA-136
>             Project: Apache Cassandra Java driver
>          Issue Type: Improvement
>            Reporter: Bret McGuire
>            Priority: Normal
>
> The ninja fix at https://github.com/apache/cassandra-java-driver/pull/2093 
> has introduced a regression in the PointIT integration test:
> {noformat}
> java.lang.IllegalArgumentException: Multiple entries with same key: POINT 
> (1.7976931348623157e+308 4.9000000000000000e-324)=4 and POINT (0 0)=1
>       at 
> com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap.conflictException(ImmutableMap.java:382)
>       at 
> com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap.checkNoConflict(ImmutableMap.java:376)
>       at 
> com.datastax.oss.driver.shaded.guava.common.collect.RegularImmutableMap.checkNoConflictInKeyBucket(RegularImmutableMap.java:249)
>       at 
> com.datastax.oss.driver.shaded.guava.common.collect.RegularImmutableMap.fromEntryArrayCheckingBucketOverflow(RegularImmutableMap.java:136)
>       at 
> com.datastax.oss.driver.shaded.guava.common.collect.RegularImmutableMap.fromEntryArray(RegularImmutableMap.java:98)
>       at 
> com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap$Builder.build(ImmutableMap.java:579)
>       at 
> com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap$Builder.buildOrThrow(ImmutableMap.java:607)
>       at 
> com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap$Builder.build(ImmutableMap.java:594)
>       at 
> com.datastax.dse.driver.api.core.data.geometry.GeometryIT.should_insert_as_map_keys(GeometryIT.java:236)
>       at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
>       at 
> sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
>       at 
> sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
>       at java.lang.reflect.Method.invoke(Method.java:498)
>       at 
> org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
>       at 
> org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
>       at 
> org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
>       at 
> org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
>       at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
>       at 
> org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
>       at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
>       at 
> org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
>       at 
> org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
>       at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
>       at 
> org.apache.maven.surefire.junitcore.pc.Scheduler$1.run(Scheduler.java:345)
>       at 
> org.apache.maven.surefire.junitcore.pc.InvokerStrategy.schedule(InvokerStrategy.java:47)
>       at 
> org.apache.maven.surefire.junitcore.pc.Scheduler.schedule(Scheduler.java:316)
>       at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
>       at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
>       at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
>       at 
> org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
>       at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:54)
>       at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:54)
>       at org.junit.rules.RunRules.evaluate(RunRules.java:20)
>       at org.junit.rules.RunRules.evaluate(RunRules.java:20)
>       at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
>       at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
>       at org.junit.runners.Suite.runChild(Suite.java:128)
>       at org.junit.runners.Suite.runChild(Suite.java:27)
>       at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
>       at 
> org.apache.maven.surefire.junitcore.pc.Scheduler$1.run(Scheduler.java:345)
>       at 
> java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
>       at java.util.concurrent.FutureTask.run(FutureTask.java:266)
>       at 
> java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
>       at 
> java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
>       at java.lang.Thread.run(Thread.java:750)
> {noformat}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to