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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@
* {@link SameOuterHoleOverlay}, {@link DifferentOuterHoleOverlay},
* {@link HalfDiscOverlay} (complementary / sectors / collinear),
* {@link TwoShellClip} (0 / 1 / 2 / even-n / odd-n with a tangent
* as a degenerate NSpan), or a two-node walk vs a disc or plain
* polygon via {@link TwoNodeClip}. A hole that straddles the
* other shell, or two holes that cross, stay {@code null} (bite
* / noder, not a kit). A 0-node mixed shell vs a circular disc
* as a degenerate NSpan), {@link BiteVsHole} (straddling hole:
* new edge ⊂ other.shell is a bite, not a punch), or a two-node
* walk vs a disc or plain polygon via {@link TwoNodeClip}. Two
* holes that cross stay {@code null} ({@code H-SHELL-HOLE-X}).
* A 0-node mixed shell vs a circular disc
* ({@code CC-NEST-ANNULUS}) is not a punch. A miss is {@code null}.
*/
final class CompoundCurveShellOverlay {
Expand All @@ -57,6 +58,10 @@ static Geometry overlay(Geometry a, Geometry b, int opCode) {
if (differentHole != null) {
return differentHole;
}
Geometry bite = BiteVsHole.overlay(a, b, opCode);
if (bite != null) {
return bite;
}
CurvePolygon shellA = compoundCurveShell(a);
CurvePolygon shellB = compoundCurveShell(b);
if (shellA != null && shellB != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ static Coordinate[] nodes(List<CurveSegmentString> a,

/**
* Shared runs of a circular pair. Empty is no interval and no
* pinch. {@code null} is a hole (P2.3 / P2.4) or a pair the
* pinch. {@code null} is a holed pair (the hole ring is walked
* as strings by {@link BiteVsHole}, not here) or a pair the
* strings cannot name. Does not assemble a face.
*/
static List<CurveSegmentString> edges(Geometry a, Geometry b) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ boolean isDegenerate() {
/**
* Exterior pieces of a hole-free circular / compound / plain ring,
* or the pieces of a lineal CircularString / CompoundCurve /
* LineString (R-LL). Holes stay {@code null} (P2.3 / P2.4).
* LineString (R-LL). A holed CurvePolygon stays {@code null};
* pass the hole ring itself as a LineString. Two holes are P2.4.
*/
static List<CurveSegmentString> of(Geometry g) {
Geometry geom = unwrap(g);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
* and if it is strictly outside ignore it for CAP (keep it on the
* holed side). A hole that meets or crosses the CAP shares the
* clip edge: subtracting hole ∩ other is a bite, not an interior
* punch ({@code H-SHELL-HOLE-CROSS}). Two holes that cross
* ({@code H-SHELL-HOLE-X}) are a noder. Both stay {@code null}.
* punch ({@code H-SHELL-HOLE-CROSS}). That pair is
* {@link BiteVsHole}. Two holes that cross
* ({@code H-SHELL-HOLE-X}) stay {@code null}.
*/
final class DifferentOuterHoleOverlay {

Expand Down Expand Up @@ -173,7 +174,8 @@ else if (loc == TwoNodeClip.IN) {
}
}
// Both sides of the CAP: the hole crosses the other shell.
// hole ∩ CAP shares the clip edge (H-SHELL-HOLE-CROSS).
// hole ∩ CAP shares the clip edge -- BiteVsHole decides
// bite versus hole. This kit stays MIXED.
if (mixed || (sawIn && sawOut)) return TwoNodeClip.MIXED;
if (sawIn) return TwoNodeClip.IN;
if (sawOut) return TwoNodeClip.OUT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@
* is the holed / unholed / hole polygon. A different-outer hole
* whose outers already clip composes: hole strictly inside the
* outer CAP is punched, hole strictly outside is ignored on
* CAP. A hole that meets or crosses the other outer shares
* the clip edge (a bite, not an interior punch). Two holes
* that cross are a noder. Collinear overlap, mixed labels,
* CAP. A hole that crosses the other outer shares the clip
* edge: if that new edge is a subset of the other shell it
* is a bite, not an interior punch. Two holes that cross
* stay a named miss. Collinear overlap, mixed labels,
* or a line-only shell return {@code null} without paying
* this path.</li>
* <li><b>R-LL</b> -- one operand is a {@link org.locationtech.jts.geom.curve.CircularString}
Expand All @@ -133,8 +134,9 @@
* R1.5–R1.7 share package-private {@code TwoNodeClip} for the two-node
* walk (hits, ring / member walk, CAP / CUP / SUB / XOR). Even-n
* assemble is {@code NSpanClip}. R1.7 dispatch is
* {@code CompoundCurveShellOverlay} (hole / half-disc / two-shell /
* vs disc or polygon). R-LL and R-AA reuse the same intersection
* {@code CompoundCurveShellOverlay} (hole / bite-vs-hole /
* half-disc / two-shell / vs disc or polygon). R-LL and R-AA
* reuse the same intersection
* primitives. Each rung keeps its own shape dispatch.
* The distinction in R0/R1 is the one that matters: an exact answer chosen by a
* tolerance-bounded decision is still exact, but the decision can be wrong for
Expand Down Expand Up @@ -238,7 +240,9 @@ public OverlayNGCurve(Geometry a, Geometry b) {
* whose only non-alternation is a tangent (degenerate NSpan),
* a same-outer
* hole-inside pair, a different-outer hole composed from a
* certified outer clip, and an even 4+ line–circle cut of a disc
* certified outer clip, a straddling hole whose new edge is a
* subset of the other shell (a bite, not a punch), and an even
* 4+ line–circle cut of a disc
* by a plain polygon. In
* the R1 case the <em>answer</em> is exact even though the <em>decision</em>
* to return it was made on densified copies.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,9 @@ public void testHFourArealCutsAreNNodeAssemble() throws Exception {
* with the touch as a zero-length span. A same-outer hole-inside
* pair is the holed cell. A different-outer hole composes when
* it sits strictly inside or outside a certified outer CAP.
* Collinear overlap, mixed labels, and a hole that meets or
* crosses the other outer stay refused.
* Collinear overlap, mixed labels, and a hole that meets the
* other diameter stay refused. A hole that straddles the other
* shell is a bite when the new edge ⊂ that shell.
*/
public void testHShellComplementaryHalfDiscsAreTheDisc() throws Exception {
Geometry upper = readCurve(HALF_UPPER);
Expand Down Expand Up @@ -378,10 +379,12 @@ public void testHShellComplementaryHalfDiscsAreTheDisc() throws Exception {
CompoundCurveShellOverlay.overlay(holed, right, OverlayNG.INTERSECTION));
Geometry straddle = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (-1 1, 1 1, 1 2, -1 2, -1 1))");
// The hole crosses the other outer, not the other hole. hole ∩
// other shares the clip edge, so a punch would guess a bite.
assertNull("H-SHELL-HOLE-CROSS: hole straddles the other shell",
CompoundCurveShellOverlay.overlay(straddle, right, OverlayNG.INTERSECTION));
OverlayNGCurve crossCap = new OverlayNGCurve(straddle, right);
Geometry bite = crossCap.getResult(OverlayNG.INTERSECTION);
assertFalse("H-SHELL-HOLE-CROSS: new edge ⊂ other.shell is a bite",
crossCap.isApproximate());
assertEquals("Q1 minus the right half-rectangle", 6.25 * Math.PI - 1.0,
bite.getArea(), EXACT);
Geometry holeX = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5))");
// Two holes that cross each other is a noder, not a kit.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
package org.locationtech.jts.operation.overlayng.curve;

import org.locationtech.jts.algorithm.distance.DiscreteHausdorffDistance;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.curve.CircularString;
Expand All @@ -36,11 +37,12 @@
* hole-inside pair is the holed cell. A different-outer hole
* composes when it sits strictly inside or outside a certified
* outer CAP. A hole that straddles the other shell shares the
* clip edge, so subtracting hole ∩ other is a bite, not an
* interior punch. Two holes that cross are a noder. A
* three-point LineString is not an arc. Collinear overlap,
* mixed labels, and those named hole misses stay {@code null}
* so OverlayNGCurve can take R2 without paying this path first.
* clip edge: if that new edge is a subset of the other shell
* it is a bite, not an interior punch. Two holes that cross
* are a noder. A three-point LineString is not an arc.
* Collinear overlap, mixed labels, and the two-hole miss stay
* {@code null} so OverlayNGCurve can take R2 without paying
* this path first.
*/
public class CompoundCurveShellOverlayTest extends GeometryTestCase {

Expand Down Expand Up @@ -602,6 +604,69 @@ public void testDifferentOuterHoleComplementaryIsDiscMinusHole()
assertEquals("disc minus the rectangle", DISC - 1.0, both.getArea(), EXACT);
}

/**
* H-SHELL-HOLE-CROSS: the hole straddles the other shell. Even-n
* hole ∩ other shares the clip edge (0,1)–(0,2) on the vertical
* diameter. That new edge ⊂ other.shell, so the walk says bite,
* not a punched hole.
*/
public void testHoleStraddleIsABiteNotAHole() throws Exception {
Geometry straddle = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (-1 1, 1 1, 1 2, -1 2, -1 1))");
Geometry right = readCurve(HALF_RIGHT);
assertEquals("new edge ⊂ other.shell is a bite", BiteVsHole.BITE,
BiteVsHole.decide(straddle, right));
CurveSegmentString clip = BiteVsHole.clipEdge(straddle, right);
assertNotNull("clip edge is the shared diameter run", clip);
assertFalse(clip.isArc());
assertFalse(clip.isDegenerate());
assertEquals(1.0, clip.length(), EXACT);
assertTrue("clip edge (0 1)–(0 2)",
(clip.getStart().distance(new Coordinate(0, 1)) <= EXACT
&& clip.getEnd().distance(new Coordinate(0, 2)) <= EXACT)
|| (clip.getStart().distance(new Coordinate(0, 2)) <= EXACT
&& clip.getEnd().distance(new Coordinate(0, 1)) <= EXACT));

OverlayNGCurve cap = new OverlayNGCurve(straddle, right);
Geometry q = cap.getResult(OverlayNG.INTERSECTION);
assertFalse("H-SHELL-HOLE-CROSS CAP is exact (bite, not hole)",
cap.isApproximate());
assertEquals("Q1 minus the right half-rectangle", 6.25 * Math.PI - 1.0,
q.getArea(), EXACT);
assertEquals("bite is a shell, not an interior ring", 0,
((CurvePolygon) q).getNumInteriorRing());
assertArcAndLineShell(q);
assertParity(straddle, right, OverlayNG.INTERSECTION, q);

OverlayNGCurve cup = new OverlayNGCurve(straddle, right);
Geometry u = cup.getResult(OverlayNG.UNION);
assertFalse("H-SHELL-HOLE-CROSS CUP is exact", cup.isApproximate());
assertEquals("three-quarter minus the leftover hole",
18.75 * Math.PI - 1.0, u.getArea(), EXACT);
assertEquals("leftover is a hole (new edge not on the CUP shell)",
1, ((CurvePolygon) u).getNumInteriorRing());

OverlayNGCurve sub = new OverlayNGCurve(straddle, right);
Geometry ears = sub.getResult(OverlayNG.DIFFERENCE);
assertFalse("H-SHELL-HOLE-CROSS SUB is exact", sub.isApproximate());
assertEquals("Q2 minus the left half-rectangle", 6.25 * Math.PI - 1.0,
ears.getArea(), EXACT);
assertEquals("SUB bite is a shell", 0,
((CurvePolygon) ears).getNumInteriorRing());

OverlayNGCurve rev = new OverlayNGCurve(right, straddle);
Geometry other = rev.getResult(OverlayNG.DIFFERENCE);
assertFalse("H-SHELL-HOLE-CROSS reverse SUB is exact",
rev.isApproximate());
assertEquals("Q4 plus the right half-rectangle", 6.25 * Math.PI + 1.0,
other.getArea(), EXACT);

OverlayNGCurve xor = new OverlayNGCurve(straddle, right);
Geometry x = xor.getResult(OverlayNG.SYMDIFFERENCE);
assertFalse("H-SHELL-HOLE-CROSS XOR is exact", xor.isApproximate());
assertEquals("both bites", 12.5 * Math.PI, x.getArea(), EXACT);
}

public void testNotThisCellReturnsNull() throws Exception {
Geometry half = readCurve(HALF_DISC);
Geometry disc = readCurve(CIRCLE_5);
Expand All @@ -612,19 +677,12 @@ public void testNotThisCellReturnsNull() throws Exception {
Geometry right = readCurve(HALF_RIGHT);
Geometry onDiameter = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-1 1, 0 2, 1 1), (1 1, 1 0), (1 0, -1 0), (-1 0, -1 1)))");
Geometry straddle = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (-1 1, 1 1, 1 2, -1 2, -1 1))");
assertNull("two discs stay on R1.5",
CompoundCurveShellOverlay.overlay(disc, other, OverlayNG.INTERSECTION));
assertNull("plain vs plain",
CompoundCurveShellOverlay.overlay(square, square, OverlayNG.UNION));
assertNull("H-SHELL-HOLE-OUTER: hole meets the other diameter",
CompoundCurveShellOverlay.overlay(holed, right, OverlayNG.INTERSECTION));
// The hole crosses the other outer (the vertical diameter), not
// the other hole. hole ∩ other shares that clip edge, so a punch
// would guess a bite versus a hole.
assertNull("H-SHELL-HOLE-CROSS: hole straddles the other shell",
CompoundCurveShellOverlay.overlay(straddle, right, OverlayNG.INTERSECTION));
Geometry holeX = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5))");
// Two holes that cross each other is a noder, not a kit.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.curve.CurveGeometryFactory;
import org.locationtech.jts.geom.curve.CurvePolygon;
import org.locationtech.jts.io.curve.CurveWKTReader;

import junit.textui.TestRunner;
Expand All @@ -25,7 +26,8 @@
/**
* P2.1 nodes plus P2.2 overlap-as-edge. {@link CurveSegmentString}
* is the unit; {@link CurveSegmentNoder} emits the discrete node
* set or a shared run as an edge (interval). No faces. Not N-SS.
* set or a shared run as an edge (interval). P2.3 walks a hole
* ring as strings into one face decision (bite vs hole). Not N-SS.
*/
public class CurveSegmentStringTest extends GeometryTestCase {

Expand Down Expand Up @@ -385,20 +387,38 @@ public void testRllCollinearOverlapIsAnEdge() throws Exception {
assertEquals(8.0, run.length(), EXACT);
}

public void testPinchAndHolesStayNamedMiss() throws Exception {
public void testHoleStraddleNodesAreTheClipPair() throws Exception {
Geometry right = readCurve(HALF_RIGHT);
Geometry straddle = readCurve(HOLE_STRADDLE);
assertNull("H-SHELL-HOLE-CROSS: holes are P2.3",
assertNull("H-SHELL-HOLE-CROSS: Geometry pair still has a hole",
CurveSegmentNoder.nodes(straddle, right));
assertNull("H-SHELL-HOLE-CROSS: edges stay null",
assertNull("H-SHELL-HOLE-CROSS: Geometry edges stay on the outers",
CurveSegmentNoder.edges(straddle, right));

CurvePolygon cp = (CurvePolygon) straddle;
List<CurveSegmentString> hole = CurveSegmentString.of(
cp.getInteriorCurveN(0));
List<CurveSegmentString> shell = CurveSegmentString.of(right);
Coordinate[] nodes = CurveSegmentNoder.nodes(hole, shell, 10.0);
assertEquals("H-SHELL-HOLE-CROSS: even-n on the hole ring", 2,
nodes.length);
assertHas(nodes, 0.0, 1.0);
assertHas(nodes, 0.0, 2.0);
assertEquals("new edge ⊂ other.shell ⇒ bite", BiteVsHole.BITE,
BiteVsHole.decide(straddle, right));
CurveSegmentString clip = BiteVsHole.clipEdge(straddle, right);
assertNotNull(clip);
assertEquals(1.0, clip.length(), EXACT);
assertTrue(sameEnds(clip, 0.0, 1.0, 0.0, 2.0));

Geometry holed = readCurve(HALF_HOLED);
Geometry holeX = readCurve(HOLE_X);
assertNull("H-SHELL-HOLE-X: two holes are P2.4",
CurveSegmentNoder.nodes(holed, holeX));
assertNull("H-SHELL-HOLE-X: edges stay null",
CurveSegmentNoder.edges(holed, holeX));
assertEquals("H-SHELL-HOLE-X: walk does not invent a face",
BiteVsHole.MISS, BiteVsHole.decide(holed, holeX));
}

public void testNoderDoesNotAssembleFaces() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,26 @@ public void testDifferentOuterHoleLensCapNotSlowerThanChord()
() -> chordOverlay(a, b, OverlayNGCurve.INTERSECTION));
}

public void testHoleStraddleBiteCapNotSlowerThanChord() throws Exception {
Geometry a = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (-1 1, 1 1, 1 2, -1 2, -1 1))");
Geometry b = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 -5, 5 0, 0 5), (0 5, 0 -5)))");
assertLaserNotSlower("hole-straddle bite CAP",
() -> OverlayNGCurve.intersection(a, b),
() -> chordOverlay(a, b, OverlayNGCurve.INTERSECTION));
}

public void testHoleStraddleBiteSubNotSlowerThanChord() throws Exception {
Geometry a = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (-1 1, 1 1, 1 2, -1 2, -1 1))");
Geometry b = readCurve(
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 -5, 5 0, 0 5), (0 5, 0 -5)))");
assertLaserNotSlower("hole-straddle bite SUB",
() -> OverlayNGCurve.difference(a, b),
() -> chordOverlay(a, b, OverlayNGCurve.DIFFERENCE));
}

public void testFourCutCapNotSlowerThanChord() throws Exception {
Geometry a = readCurve(CIRCLE_5);
Geometry b = readCurve("POLYGON ((-8 -1, 8 -1, 8 1, -8 1, -8 -1))");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@
* Complementary half-discs are 0EEE. Perpendicular same-circle
* half-discs, a two-node two-shell clip, collinear same-side halves,
* nested halves, and a 1-node touch are exact. A four-cut two-shell
* n-span, a same-outer hole-inside pair, and a different-outer hole
* that sits strictly inside or outside a certified outer CAP are
* n-span, a same-outer hole-inside pair, a different-outer hole
* that sits strictly inside or outside a certified outer CAP, and
* a straddling hole whose new edge ⊂ the other shell (a bite) are
* exact. A four-cut disc vs a band is EEEE.
*/
public class OverlayNGCurveRatchetTest extends GeometryTestCase {
Expand Down Expand Up @@ -107,6 +108,8 @@ public class OverlayNGCurveRatchetTest extends GeometryTestCase {
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-1 -1, 0 -2, 1 -1), (1 -1, 1 6), CIRCULARSTRING (1 6, 0 7, -1 6), (-1 6, -1 -1)))";
private static final String HALF_HOLED =
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (0 1, 1 1, 1 2, 0 2, 0 1))";
private static final String HOLE_STRADDLE =
"CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, -5 0)), (-1 1, 1 1, 1 2, -1 2, -1 1))";

private static final int[] OPS = { OverlayNGCurve.INTERSECTION, OverlayNGCurve.UNION,
OverlayNGCurve.DIFFERENCE, OverlayNGCurve.SYMDIFFERENCE };
Expand Down Expand Up @@ -310,6 +313,14 @@ public void testMatrix_fourCut() throws Exception {
assertRow("four-cut disc ∩ band", CIRCLE_5, BAND_FOUR, "EEEE");
}

public void testMatrix_holeStraddleBite() throws Exception {
assertRow("hole-straddle bite", HOLE_STRADDLE, HALF_RIGHT, "EEEE");
}

public void testMatrix_holeStraddleBiteReverse() throws Exception {
assertRow("hole-straddle bite reverse", HALF_RIGHT, HOLE_STRADDLE, "EEEE");
}

// -- the disjoint CUP/XOR result, not just its exactness -----------------

/**
Expand Down
Loading