[
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]