Skip to content
Merged
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 @@ -35,7 +35,7 @@
* a Point or MultiPoint (PIP and DE-9IM), a disc against a LineString
* (DE-9IM from line–circle nodes), a disc against a plain Polygon
* (DE-9IM from vertices, edge nodes, and mid-arc PIP), or two circular
* discs (DE-9IM from radical-axis nodes and {@link #locatePoint}).
* discs (DE-9IM from {@code d²} vs {@code (r1±r2)²} in {@code R²}).
* Package-private -- not a new public API.
* {@link CurveOps} takes these only when they can answer; anything else
* goes straight to the chord baseline. Trying and falling through would
Expand Down Expand Up @@ -201,9 +201,9 @@ static Boolean covers(Geometry curve, Geometry other) {
* lineal and polygonal use
* {@link CircularArcDensifier#intersectSegmentCircle}
* ({@code t ∈ [0,1]}, the R1.6 / {@code ARC_SEGMENT_XY} quadratic)
* plus endpoint / vertex location. Two discs reuse
* {@link CircularArcDensifier#intersectCircles} (radical axis) and
* locate each centre in the other disc. A polygonal miss also samples
* plus endpoint / vertex location. Two discs classify in {@code R²}
* ({@code d²} vs {@code (r1±r2)²}); the kiss coordinates are not
* needed for the matrix. A polygonal miss also samples
* mid-arc points in the polygon (jts-core PIP). Mixed MultiPoint
* location classes, a multi-member MultiLineString or MultiSurface,
* a holed or non-disc curve polygon return {@code null} so the caller
Expand Down Expand Up @@ -342,10 +342,14 @@ private static LineString plainLine(Geometry g) {

/**
* Location classes of two circular discs. Envelope miss is disjoint.
* Node count is {@link CircularArcDensifier#intersectCircles} (empty
* when {@code d == 0}, {@code d > r1+r2}, or {@code d < |r1-r2|});
* each centre is then located in the other disc. A non-disc
* {@code CurvePolygon} never reaches this method.
* The class is decided in {@code R²}: {@code d²} against
* {@code (r1+r2)²} and {@code (r1-r2)²}. No {@code hypot} and no
* radical-axis node for the decision (V1, before noding). Public
* numbers may take {@code sqrt}; this matrix does not need the kiss
* coordinates. T-ext is {@link #IM_AREA_EXT_TANGENT} {@code FF2F01212}
* — one kiss, no shared flesh — ISO/IEC 13249-3 {@code ST_Touches}
* (DE-9IM {@code FT*******} / {@code F**T*****} / {@code F***T****}).
* A non-disc {@code CurvePolygon} never reaches this method.
*/
private static IntersectionMatrix relateDisc(CircularArcDensifier.Circle a,
CircularArcDensifier.Circle b) {
Expand All @@ -354,30 +358,33 @@ private static IntersectionMatrix relateDisc(CircularArcDensifier.Circle a,
if (!ea.intersects(eb)) {
return new IntersectionMatrix(IM_AREA_DISJOINT);
}
Coordinate[] nodes = CircularArcDensifier.intersectCircles(a, b);
Coordinate ca = new Coordinate(a.cx, a.cy);
Coordinate cb = new Coordinate(b.cx, b.cy);
double r2a = a.r * a.r;
double r2b = b.r * b.r;
if (nodes.length == 2) {
return new IntersectionMatrix(IM_AREA_OVERLAP);
}
int locAinB = locatePoint(b, ca, r2b);
int locBinA = locatePoint(a, cb, r2a);
if (nodes.length == 1) {
if (locAinB == Location.EXTERIOR && locBinA == Location.EXTERIOR) {
return new IntersectionMatrix(IM_AREA_EXT_TANGENT);
}
double dx = a.cx - b.cx;
double dy = a.cy - b.cy;
double d2 = dx * dx + dy * dy;
double sum = a.r + b.r;
double sum2 = sum * sum;
double diff = a.r - b.r;
double diff2 = diff * diff;
if (d2 == 0.0 && a.r == b.r) {
return new IntersectionMatrix(IM_AREA_EQUAL);
}
if (d2 > sum2) {
return new IntersectionMatrix(IM_AREA_DISJOINT);
}
if (d2 == sum2) {
return new IntersectionMatrix(IM_AREA_EXT_TANGENT);
}
if (d2 == diff2) {
return a.r > b.r
? new IntersectionMatrix(IM_AREA_INT_TANGENT)
: new IntersectionMatrix(IntersectionMatrix.transpose(IM_AREA_INT_TANGENT));
}
if (locAinB == Location.EXTERIOR && locBinA == Location.EXTERIOR) {
return new IntersectionMatrix(IM_AREA_DISJOINT);
if (d2 < diff2) {
return a.r > b.r
? new IntersectionMatrix(IM_AREA_COVERS)
: new IntersectionMatrix(IM_AREA_COVEREDBY);
}
if (a.r > b.r) return new IntersectionMatrix(IM_AREA_COVERS);
if (a.r < b.r) return new IntersectionMatrix(IM_AREA_COVEREDBY);
return new IntersectionMatrix(IM_AREA_EQUAL);
return new IntersectionMatrix(IM_AREA_OVERLAP);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ public void testNestedOffsetCovers() throws Exception {
assertTrue(inner.coveredBy(big));
}

/**
* Axis-aligned pair kisses at the shared control {@code (5, 0)}.
* R.1 ({@link CurveExactRelateTouchTest}) pins the same matrix on a
* 3-4-5 pair whose kiss is not a control vertex.
*/
public void testExternalTangent() throws Exception {
Geometry a = readCurve(CIRCLE_5);
Geometry b = readCurve(CIRCLE_EXT_TAN);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright (c) 2026 grootstebozewolf
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.locationtech.jts.geom.curve;

import java.lang.reflect.Field;

import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.IntersectionMatrix;
import org.locationtech.jts.io.curve.CurveWKTReader;
import org.locationtech.jts.operation.overlayng.curve.OverlayNGCurve;
import org.locationtech.jts.operation.relate.RelateOp;

import junit.textui.TestRunner;
import test.jts.GeometryTestCase;

/**
* R.1 TOUCH laser cell. Two externally tangent circular discs:
* one kiss, no shared flesh. T-ext DE-9IM {@code FF2F01212}.
* <p>
* ISO/IEC 13249-3 {@code ST_Touches}: the only points in common lie
* on the boundaries (DE-9IM {@code FT*******} / {@code F**T*****} /
* {@code F***T****}). No DOI. Stamp: EXACT. V1 before noding — the
* class is {@code d² == (r1+r2)²} in {@code R²}, not a hypot
* quotient and not a noder. Public {@link Geometry#touches} is the
* RelateOp predicate ({@link IntersectionMatrix#isTouches}) on that
* matrix. No {@code OverlayNGCurve.TOUCH}. No fifth overlay op.
* <p>
* The axis-aligned pair in {@link CurveExactRelateDiscTest} kisses at
* a shared control vertex, so the control diamonds also touch. This
* cell pins a 3-4-5 pair whose kiss {@code (4, 3)} is not a control:
* RelateOp on the control polygons is the lie (disjoint).
*/
public class CurveExactRelateTouchTest extends GeometryTestCase {

private static final String CIRCLE_5 =
"CURVEPOLYGON (CIRCULARSTRING (-5 0, 0 5, 5 0, 0 -5, -5 0))";
/** Centre (8, 6), r=5. Externally tangent to CIRCLE_5; kiss (4, 3). */
private static final String CIRCLE_3_4_5 =
"CURVEPOLYGON (CIRCULARSTRING (13 6, 8 11, 3 6, 8 1, 13 6))";
private static final String MULTI_5 = "MULTISURFACE (" + CIRCLE_5 + ")";
private static final String MULTI_345 = "MULTISURFACE (" + CIRCLE_3_4_5 + ")";
private static final String CONTROL_5 =
"POLYGON ((-5 0, 0 5, 5 0, 0 -5, -5 0))";
private static final String CONTROL_345 =
"POLYGON ((13 6, 8 11, 3 6, 8 1, 13 6))";

/** T-ext: II=F, BB=0. One kiss, no shared flesh. */
private static final String IM_EXT = CurveExact.IM_AREA_EXT_TANGENT;

public static void main(String[] args) {
TestRunner.run(CurveExactRelateTouchTest.class);
}

public CurveExactRelateTouchTest(String name) { super(name); }

private static Geometry readCurve(String wkt) throws Exception {
return new CurveWKTReader(new CurveGeometryFactory()).read(wkt);
}

public void testMatrixIsFF2F01212() {
assertEquals("FF2F01212", IM_EXT);
assertTrue("SFS / RelateOp Touch on T-ext",
new IntersectionMatrix(IM_EXT).isTouches(2, 2));
}

public void testR2KissIsExactEquality() throws Exception {
Geometry a = readCurve(CIRCLE_5);
Geometry b = readCurve(CIRCLE_3_4_5);
CircularArcDensifier.Circle da = CurveExact.circularDisc(a);
CircularArcDensifier.Circle db = CurveExact.circularDisc(b);
assertNotNull(da);
assertNotNull(db);
double dx = da.cx - db.cx;
double dy = da.cy - db.cy;
double d2 = dx * dx + dy * dy;
double sum2 = (da.r + db.r) * (da.r + db.r);
assertEquals("3-4-5: d² = 8²+6² = 100", 100.0, d2, 0.0);
assertEquals("(r1+r2)² = 10²", 100.0, sum2, 0.0);
assertEquals("T-ext is d² == (r1+r2)², no hypot",
Double.doubleToRawLongBits(d2), Double.doubleToRawLongBits(sum2));
}

public void testPublicRelateAndTouches() throws Exception {
Geometry a = readCurve(CIRCLE_5);
Geometry b = readCurve(CIRCLE_3_4_5);
assertEquals(IM_EXT, CurveExact.relate(a, b).toString());
assertEquals(IM_EXT, CurveExact.relate(b, a).toString());
assertEquals(IM_EXT, a.relate(b).toString());
assertEquals(IM_EXT, b.relate(a).toString());
assertTrue("Geometry.touches via RelateOp isTouches", a.touches(b));
assertTrue(b.touches(a));
assertTrue(a.relate(b).isTouches(a.getDimension(), b.getDimension()));
assertTrue(a.relate(b, IM_EXT));
assertTrue(a.intersects(b));
assertFalse("no shared flesh", a.overlaps(b));
assertFalse(a.contains(b));
assertFalse(a.covers(b));
assertFalse(a.crosses(b));
assertFalse(a.equalsTopo(b));
}

public void testControlPolygonRelateOpIsTheLie() throws Exception {
Geometry a = readCurve(CIRCLE_5);
Geometry b = readCurve(CIRCLE_3_4_5);
Geometry diamondA = read(CONTROL_5);
Geometry diamondB = read(CONTROL_345);
String lie = RelateOp.relate(diamondA, diamondB).toString();
assertFalse("control diamonds miss the (4, 3) kiss",
IM_EXT.equals(lie));
assertFalse("RelateOp on controls is not Touch",
diamondA.touches(diamondB));
assertEquals("laser is not the control-polygon matrix",
IM_EXT, a.relate(b).toString());
assertTrue(a.touches(b));
}

public void testSingleMemberMultiSurface() throws Exception {
Geometry ma = readCurve(MULTI_5);
Geometry mb = readCurve(MULTI_345);
Geometry b = readCurve(CIRCLE_3_4_5);
assertEquals(IM_EXT, CurveExact.relate(ma, b).toString());
assertEquals(IM_EXT, ma.relate(b).toString());
assertEquals(IM_EXT, CurveExact.relate(ma, mb).toString());
assertEquals(IM_EXT, ma.relate(mb).toString());
assertTrue(ma.touches(b));
assertTrue(ma.touches(mb));
}

public void testNoOverlayNGCurveTouch() {
Field[] fields = OverlayNGCurve.class.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
assertFalse("DE-9IM Touch only — no OverlayNGCurve.TOUCH",
"TOUCH".equals(fields[i].getName()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ public class OverlayNGCurvePerfGateTest extends GeometryTestCase {
"CURVEPOLYGON (CIRCULARSTRING (2 0, 7 5, 12 0, 7 -5, 2 0))";
private static final String CIRCLE_EXT_TAN =
"CURVEPOLYGON (CIRCULARSTRING (5 0, 10 5, 15 0, 10 -5, 5 0))";
/** R.1 T-ext kiss at (4, 3), not a control vertex. */
private static final String CIRCLE_3_4_5 =
"CURVEPOLYGON (CIRCULARSTRING (13 6, 8 11, 3 6, 8 1, 13 6))";
private static final String EMPTY = "CURVEPOLYGON EMPTY";
private static final String POINT_INSIDE = "POINT (3 3)";
private static final String POINT_FAR = "POINT (100 100)";
Expand Down Expand Up @@ -762,6 +765,17 @@ public void testRelateExtTangentDiscsNotSlowerThanChord() throws Exception {
() -> CurveOps.linearise(a).relate(CurveOps.linearise(tan)));
}

public void testRelateR1TouchKissNotSlowerThanChord() throws Exception {
Geometry a = readCurve(CIRCLE_5);
Geometry kiss = readCurve(CIRCLE_3_4_5);
assertLaserNotSlower("R.1 T-ext 3-4-5 relate",
() -> a.relate(kiss),
() -> CurveOps.linearise(a).relate(CurveOps.linearise(kiss)));
assertLaserNotSlower("R.1 T-ext 3-4-5 touches",
() -> a.touches(kiss),
() -> CurveOps.linearise(a).touches(CurveOps.linearise(kiss)));
}

public void testRelateCrossingDiscsReverseNotSlowerThanChord() throws Exception {
Geometry a = readCurve(CIRCLE_5);
Geometry cross = readCurve(CIRCLE_CROSSING);
Expand Down
Loading