diff --git a/s2/shapeutil_visit_crossing_edge_pairs.go b/s2/shapeutil_visit_crossing_edge_pairs.go new file mode 100644 index 00000000..3488577b --- /dev/null +++ b/s2/shapeutil_visit_crossing_edge_pairs.go @@ -0,0 +1,176 @@ +package s2 + +import "fmt" + +type ShapeEdgeVector []ShapeEdge + +type EdgePairVisitor func(a, b ShapeEdge, isInterior bool) bool + +// getShapeEdges returns all edges in the given S2ShapeIndexCell. +func getShapeEdges(index *ShapeIndex, cell *ShapeIndexCell) ShapeEdgeVector { + var shapeEdges ShapeEdgeVector + for _, clipped := range cell.shapes { + shape := index.Shape(clipped.shapeID) + for _, edgeID := range clipped.edges { + shapeEdges = append(shapeEdges, ShapeEdge{ + ID: ShapeEdgeID{ + ShapeID: clipped.shapeID, + EdgeID: int32(edgeID), + }, + Edge: shape.Edge(edgeID), + }) + } + } + return shapeEdges +} + +// VisitCrossings finds and processes all crossing edge pairs. +func visitCrossings(shapeEdges ShapeEdgeVector, crossingType CrossingType, needAdjacent bool, visitor EdgePairVisitor) bool { + minCrossingSign := MaybeCross + if crossingType == CrossingTypeInterior { + minCrossingSign = Cross + } + for i := 0; i < len(shapeEdges) - 1; i++ { + a := shapeEdges[i] + j := i + 1 + // A common situation is that an edge AB is followed by an edge BC. We + // only need to visit such crossings if "needAdjacent" is true (even if + // AB and BC belong to different edge chains). + if !needAdjacent && a.Edge.V1 == shapeEdges[j].Edge.V0 { + j++ + if j >= len(shapeEdges) { + break + } + } + crosser := NewEdgeCrosser(a.Edge.V0, a.Edge.V1) + for ; j < len(shapeEdges); j++ { + b := shapeEdges[j] + if crosser.c != b.Edge.V0 { + crosser.RestartAt(b.Edge.V0) + } + sign := crosser.ChainCrossingSign(b.Edge.V1) + // missinglink: enum ordering is reversed compared to C++ + if sign <= minCrossingSign { + if !visitor(a, b, sign == Cross) { + return false + } + } + } + } + return true +} + +// Visits all pairs of crossing edges in the given S2ShapeIndex, terminating +// early if the given EdgePairVisitor function returns false (in which case +// VisitCrossings returns false as well). "type" indicates whether all +// crossings should be visited, or only interior crossings. +// +// If "needAdjacent" is false, then edge pairs of the form (AB, BC) may +// optionally be ignored (even if the two edges belong to different edge +// chains). This option exists for the benefit of FindSelfIntersection(), +// which does not need such edge pairs (see below). +func VisitCrossings(index *ShapeIndex, crossingType CrossingType, needAdjacent bool, visitor EdgePairVisitor) bool { + // TODO(b/262264880): Use brute force if the total number of edges is small + // enough (using a larger threshold if the S2ShapeIndex is not constructed + // yet). + for it := index.Iterator(); !it.Done(); it.Next() { + shapeEdges := getShapeEdges(index, it.cell) + if !visitCrossings(shapeEdges, crossingType, needAdjacent, visitor) { + return false + } + } + return true +} + +// VisitCrossingEdgePairs finds all crossing edge pairs in an index. +func VisitCrossingEdgePairs(index *ShapeIndex, crossingType CrossingType, visitor EdgePairVisitor) bool { + needAdjacent := crossingType == CrossingTypeAll + for it := index.Iterator(); !it.Done(); it.Next() { + shapeEdges := getShapeEdges(index, it.cell) + if !visitCrossings(shapeEdges, crossingType, needAdjacent, visitor) { + return false + } + } + return true +} + +func FindCrossingError(shape Shape, a, b ShapeEdge, isInterior bool) error { + ap := shape.ChainPosition(int(a.ID.EdgeID)) + bp := shape.ChainPosition(int(b.ID.EdgeID)) + + if isInterior { + if ap.ChainID != bp.ChainID { + return fmt.Errorf( + "Loop %d edge %d crosses loop %d edge %d", + ap.ChainID, ap.Offset, bp.ChainID, bp.Offset) + } + return fmt.Errorf("Edge %d crosses edge %d", ap, bp) + } + + // Loops are not allowed to have duplicate vertices, and separate loops + // are not allowed to share edges or cross at vertices. We only need to + // check a given vertex once, so we also require that the two edges have + // the same end vertex + if a.Edge.V1 != b.Edge.V1 { + return nil + } + + if ap.ChainID == bp.ChainID { + return fmt.Errorf("Edge %d has duplicate vertex with edge %d", ap, bp) + } + + aLen := shape.Chain(ap.ChainID).Length + bLen := shape.Chain(bp.ChainID).Length + aNext := ap.Offset + 1 + if aNext == aLen { + aNext = 0 + } + + bNext := bp.Offset + 1 + if bNext == bLen { + bNext = 0 + } + + a2 := shape.ChainEdge(ap.ChainID, aNext).V1 + b2 := shape.ChainEdge(bp.ChainID, bNext).V1 + + if a.Edge.V0 == b.Edge.V0 || a.Edge.V0 == b2 { + // The second edge index is sometimes off by one, hence "near". + return fmt.Errorf( + "Loop %d edge %d has duplicate near loop %d edge %d", + ap.ChainID, ap.Offset, bp.ChainID, bp.Offset) + } + + // Since S2ShapeIndex loops are oriented such that the polygon interior is + // always on the left, we need to handle the case where one wedge contains + // the complement of the other wedge. This is not specifically detected by + // GetWedgeRelation, so there are two cases to check for. + // + // Note that we don't need to maintain any state regarding loop crossings + // because duplicate edges are detected and rejected above. + if WedgeRelation(a.Edge.V0, a.Edge.V1, a2, b.Edge.V0, b2) == WedgeProperlyOverlaps && + WedgeRelation(a.Edge.V0, a.Edge.V1, a2, b2, b.Edge.V0) == WedgeProperlyOverlaps { + return fmt.Errorf( + "Loop %d edge %d crosses loop %d edge %d", + ap.ChainID, ap.Offset, bp.ChainID, bp.Offset) + } + + return nil +} + +func FindSelfIntersection(index *ShapeIndex) bool { + if len(index.shapes) == 0 { + return false + } + shape := index.Shape(0) + + // Visit all crossing pairs except possibly for ones of the form (AB, BC), + // since such pairs are very common and FindCrossingError() only needs pairs + // of the form (AB, AC). + return !VisitCrossings( + index, CrossingTypeAll, false, + func(a, b ShapeEdge, isInterior bool) bool { + return FindCrossingError(shape, a, b, isInterior) == nil + }, + ) +} diff --git a/s2/shapeutil_visit_crossing_edge_pairs_test.go b/s2/shapeutil_visit_crossing_edge_pairs_test.go new file mode 100644 index 00000000..6fe59b5f --- /dev/null +++ b/s2/shapeutil_visit_crossing_edge_pairs_test.go @@ -0,0 +1,150 @@ +package s2 + +import ( + "slices" + "sort" + "testing" +) + +type EdgePair struct { + A, B ShapeEdgeID +} + +// A set of edge pairs within an S2ShapeIndex. +type EdgePairVector []EdgePair + +// Get crossings in one index. +func getCrossings(index *ShapeIndex, crossingType CrossingType) EdgePairVector { + edgePairs := EdgePairVector{} + VisitCrossingEdgePairs(index, crossingType, func(a, b ShapeEdge, _ bool) bool { + edgePairs = append(edgePairs, EdgePair{a.ID, b.ID}) + return true // Continue visiting. + }) + if len(edgePairs) > 1 { + sort.Slice(edgePairs, func(i, j int) bool { + return edgePairs[i].A.Cmp(edgePairs[j].A) == -1 || edgePairs[i].B.Cmp(edgePairs[j].B) == -1 + }) + slices.Compact(edgePairs) + } + return edgePairs +} + +// Brute force crossings in one index. +func getCrossingEdgePairsBruteForce(index *ShapeIndex, crossingType CrossingType) EdgePairVector { + var result EdgePairVector + minSign := Cross + if crossingType == CrossingTypeAll { + minSign = MaybeCross + } + + for aIter := NewEdgeIterator(index); !aIter.Done(); aIter.Next() { + a := aIter.Edge() + bIter := EdgeIterator{ + index: aIter.index, + shapeID: aIter.shapeID, + numEdges: aIter.numEdges, + edgeID: aIter.edgeID, + } + for bIter.Next(); !bIter.Done(); bIter.Next() { + b := bIter.Edge() + // missinglink: enum ordering is reversed compared to C++ + if CrossingSign(a.V0, a.V1, b.V0, b.V1) <= minSign { + result = append(result, EdgePair{ + aIter.ShapeEdgeID(), + bIter.ShapeEdgeID(), + }) + } + } + } + return result +} + +func TestGetCrossingEdgePairs(t *testing.T) { + var index ShapeIndex + if len(getCrossings(&index, CrossingTypeAll)) != 0 { + t.Error("Expected 0 crossings in empty index") + } + if len(getCrossings(&index, CrossingTypeInterior)) != 0 { + t.Error("Expected 0 interior crossings in empty index") + } +} + +func TestGetCrossingEdgePairsGrid(t *testing.T) { + kGridSize := 10.0 + epsilon := 1e-10 + + // There are 11 horizontal and 11 vertical lines. The expected number of + // interior crossings is 9x9, plus 9 "touching" intersections along each of + // the left, right, and bottom edges. "epsilon" is used to make the interior + // lines slightly longer so the "touches" actually cross, otherwise 3 of the + // 27 touches are not considered intersecting. + // However, the vertical lines do not reach the top line as it curves on the + // surface of the sphere: despite "epsilon" those 9 are not even very close + // to intersecting. Thus 9 * 12 = 108 interior and four more at the corners + // when CrossingType::ALL is used. + + index := NewShapeIndex() + shape := edgeVectorShape{} + + for i := 0.0; i <= kGridSize; i++ { + var e = epsilon + if i == 0 || i == kGridSize { + e = 0 + } + + shape.Add(PointFromLatLng(LatLngFromDegrees(-e, i)), PointFromLatLng(LatLngFromDegrees(kGridSize + e, i))); + shape.Add(PointFromLatLng(LatLngFromDegrees(i, -e)), PointFromLatLng(LatLngFromDegrees(i, kGridSize + e))); + } + + index.Add(&shape) + if len(getCrossingEdgePairsBruteForce(index, CrossingTypeAll)) != 112 { + t.Errorf("Fail") + } + if len(getCrossingEdgePairsBruteForce(index, CrossingTypeInterior)) != 108 { + t.Errorf("Fail") + } +} + +func testHasCrossingPermutations(t *testing.T, loops []*Loop, i int, hasCrossing bool) { + if i == len(loops) { + index := NewShapeIndex() + polygon := PolygonFromLoops(loops) + index.Add(polygon) + + if hasCrossing != FindSelfIntersection(index) { + t.Error("Test failed: expected and actual crossing results do not match") + } + return + } + + origLoop := loops[i] + for j := 0; j < origLoop.NumVertices(); j++ { + vertices := make([]Point, origLoop.NumVertices()) + for k := 0; k < origLoop.NumVertices(); k++ { + vertices[k] = origLoop.Vertex((j + k) % origLoop.NumVertices()) + } + + loops[i] = LoopFromPoints(vertices) + testHasCrossingPermutations(t, loops, i+1, hasCrossing) + } + loops[i] = origLoop +} + +func TestHasCrossing(t *testing.T) { + // Coordinates are (lat,lng), which can be visualized as (y,x). + cases := []struct { + polygonStr string + hasCrossing bool + }{ + {"0:0, 0:1, 0:2, 1:2, 1:1, 1:0", false}, + {"0:0, 0:1, 0:2, 1:2, 0:1, 1:0", true}, // duplicate vertex + {"0:0, 0:1, 1:0, 1:1", true}, // edge crossing + {"0:0, 1:1, 0:1; 0:0, 1:1, 1:0", true}, // duplicate edge + {"0:0, 1:1, 0:1; 1:1, 0:0, 1:0", true}, // reversed edge + {"0:0, 0:2, 2:2, 2:0; 1:1, 0:2, 3:1, 2:0", true}, // vertex crossing + } + for _, tc := range cases { + polygon := makePolygon(tc.polygonStr, true) + testHasCrossingPermutations(t, polygon.loops, 0, tc.hasCrossing) + } +}