Skip to content

Commit db74c8c

Browse files
committed
Add shapeutil_visit_crossing_edge_pairs
1 parent 0b6e08c commit db74c8c

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package s2
2+
3+
import "fmt"
4+
5+
type ShapeEdgeVector []ShapeEdge
6+
7+
type EdgePairVisitor func(a, b ShapeEdge, isInterior bool) bool
8+
9+
// getShapeEdges returns all edges in the given S2ShapeIndexCell.
10+
func getShapeEdges(index *ShapeIndex, cell *ShapeIndexCell) ShapeEdgeVector {
11+
var shapeEdges ShapeEdgeVector
12+
for _, clipped := range cell.shapes {
13+
shape := index.Shape(clipped.shapeID)
14+
for _, edgeID := range clipped.edges {
15+
shapeEdges = append(shapeEdges, ShapeEdge{
16+
ID: ShapeEdgeID{
17+
ShapeID: clipped.shapeID,
18+
EdgeID: int32(edgeID),
19+
},
20+
Edge: shape.Edge(edgeID),
21+
})
22+
}
23+
}
24+
return shapeEdges
25+
}
26+
27+
// VisitCrossings finds and processes all crossing edge pairs.
28+
func visitCrossings(shapeEdges ShapeEdgeVector, crossingType CrossingType, needAdjacent bool, visitor EdgePairVisitor) bool {
29+
minCrossingSign := MaybeCross
30+
if crossingType == CrossingTypeInterior {
31+
minCrossingSign = Cross
32+
}
33+
for i := 0; i < len(shapeEdges) - 1; i++ {
34+
a := shapeEdges[i]
35+
j := i + 1
36+
// A common situation is that an edge AB is followed by an edge BC. We
37+
// only need to visit such crossings if "needAdjacent" is true (even if
38+
// AB and BC belong to different edge chains).
39+
if !needAdjacent && a.Edge.V1 == shapeEdges[j].Edge.V0 {
40+
j++
41+
if j >= len(shapeEdges) {
42+
break
43+
}
44+
}
45+
crosser := NewEdgeCrosser(a.Edge.V0, a.Edge.V1)
46+
for ; j < len(shapeEdges); j++ {
47+
b := shapeEdges[j]
48+
if crosser.c != b.Edge.V0 {
49+
crosser.RestartAt(b.Edge.V0)
50+
}
51+
sign := crosser.ChainCrossingSign(b.Edge.V1)
52+
// missinglink: enum ordering is reversed compared to C++
53+
if sign <= minCrossingSign {
54+
if !visitor(a, b, sign == Cross) {
55+
return false
56+
}
57+
}
58+
}
59+
}
60+
return true
61+
}
62+
63+
// Visits all pairs of crossing edges in the given S2ShapeIndex, terminating
64+
// early if the given EdgePairVisitor function returns false (in which case
65+
// VisitCrossings returns false as well). "type" indicates whether all
66+
// crossings should be visited, or only interior crossings.
67+
//
68+
// If "needAdjacent" is false, then edge pairs of the form (AB, BC) may
69+
// optionally be ignored (even if the two edges belong to different edge
70+
// chains). This option exists for the benefit of FindSelfIntersection(),
71+
// which does not need such edge pairs (see below).
72+
func VisitCrossings(index *ShapeIndex, crossingType CrossingType, needAdjacent bool, visitor EdgePairVisitor) bool {
73+
// TODO(b/262264880): Use brute force if the total number of edges is small
74+
// enough (using a larger threshold if the S2ShapeIndex is not constructed
75+
// yet).
76+
for it := index.Iterator(); !it.Done(); it.Next() {
77+
shapeEdges := getShapeEdges(index, it.cell)
78+
if !visitCrossings(shapeEdges, crossingType, needAdjacent, visitor) {
79+
return false
80+
}
81+
}
82+
return true
83+
}
84+
85+
// VisitCrossingEdgePairs finds all crossing edge pairs in an index.
86+
func VisitCrossingEdgePairs(index *ShapeIndex, crossingType CrossingType, visitor EdgePairVisitor) bool {
87+
needAdjacent := crossingType == CrossingTypeAll
88+
for it := index.Iterator(); !it.Done(); it.Next() {
89+
shapeEdges := getShapeEdges(index, it.cell)
90+
if !visitCrossings(shapeEdges, crossingType, needAdjacent, visitor) {
91+
return false
92+
}
93+
}
94+
return true
95+
}
96+
97+
func FindCrossingError(shape Shape, a, b ShapeEdge, isInterior bool) error {
98+
ap := shape.ChainPosition(int(a.ID.EdgeID))
99+
bp := shape.ChainPosition(int(b.ID.EdgeID))
100+
101+
if isInterior {
102+
if ap.ChainID != bp.ChainID {
103+
return fmt.Errorf(
104+
"Loop %d edge %d crosses loop %d edge %d",
105+
ap.ChainID, ap.Offset, bp.ChainID, bp.Offset)
106+
}
107+
return fmt.Errorf("Edge %d crosses edge %d", ap, bp)
108+
}
109+
110+
// Loops are not allowed to have duplicate vertices, and separate loops
111+
// are not allowed to share edges or cross at vertices. We only need to
112+
// check a given vertex once, so we also require that the two edges have
113+
// the same end vertex
114+
if a.Edge.V1 != b.Edge.V1 {
115+
return nil
116+
}
117+
118+
if ap.ChainID == bp.ChainID {
119+
return fmt.Errorf("Edge %d has duplicate vertex with edge %d", ap, bp)
120+
}
121+
122+
aLen := shape.Chain(ap.ChainID).Length
123+
bLen := shape.Chain(bp.ChainID).Length
124+
aNext := ap.Offset + 1
125+
if aNext == aLen {
126+
aNext = 0
127+
}
128+
129+
bNext := bp.Offset + 1
130+
if bNext == bLen {
131+
bNext = 0
132+
}
133+
134+
a2 := shape.ChainEdge(ap.ChainID, aNext).V1
135+
b2 := shape.ChainEdge(bp.ChainID, bNext).V1
136+
137+
if a.Edge.V0 == b.Edge.V0 || a.Edge.V0 == b2 {
138+
// The second edge index is sometimes off by one, hence "near".
139+
return fmt.Errorf(
140+
"Loop %d edge %d has duplicate near loop %d edge %d",
141+
ap.ChainID, ap.Offset, bp.ChainID, bp.Offset)
142+
}
143+
144+
// Since S2ShapeIndex loops are oriented such that the polygon interior is
145+
// always on the left, we need to handle the case where one wedge contains
146+
// the complement of the other wedge. This is not specifically detected by
147+
// GetWedgeRelation, so there are two cases to check for.
148+
//
149+
// Note that we don't need to maintain any state regarding loop crossings
150+
// because duplicate edges are detected and rejected above.
151+
if WedgeRelation(a.Edge.V0, a.Edge.V1, a2, b.Edge.V0, b2) == WedgeProperlyOverlaps &&
152+
WedgeRelation(a.Edge.V0, a.Edge.V1, a2, b2, b.Edge.V0) == WedgeProperlyOverlaps {
153+
return fmt.Errorf(
154+
"Loop %d edge %d crosses loop %d edge %d",
155+
ap.ChainID, ap.Offset, bp.ChainID, bp.Offset)
156+
}
157+
158+
return nil
159+
}
160+
161+
func FindSelfIntersection(index *ShapeIndex) bool {
162+
if len(index.shapes) == 0 {
163+
return false
164+
}
165+
shape := index.Shape(0)
166+
167+
// Visit all crossing pairs except possibly for ones of the form (AB, BC),
168+
// since such pairs are very common and FindCrossingError() only needs pairs
169+
// of the form (AB, AC).
170+
return !VisitCrossings(
171+
index, CrossingTypeAll, false,
172+
func(a, b ShapeEdge, isInterior bool) bool {
173+
return FindCrossingError(shape, a, b, isInterior) == nil
174+
},
175+
)
176+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package s2
2+
3+
import (
4+
"slices"
5+
"sort"
6+
"testing"
7+
)
8+
9+
type EdgePair struct {
10+
A, B ShapeEdgeID
11+
}
12+
13+
// A set of edge pairs within an S2ShapeIndex.
14+
type EdgePairVector []EdgePair
15+
16+
// Get crossings in one index.
17+
func getCrossings(index *ShapeIndex, crossingType CrossingType) EdgePairVector {
18+
edgePairs := EdgePairVector{}
19+
VisitCrossingEdgePairs(index, crossingType, func(a, b ShapeEdge, _ bool) bool {
20+
edgePairs = append(edgePairs, EdgePair{a.ID, b.ID})
21+
return true // Continue visiting.
22+
})
23+
if len(edgePairs) > 1 {
24+
sort.Slice(edgePairs, func(i, j int) bool {
25+
return edgePairs[i].A.Cmp(edgePairs[j].A) == -1 || edgePairs[i].B.Cmp(edgePairs[j].B) == -1
26+
})
27+
slices.Compact(edgePairs)
28+
}
29+
return edgePairs
30+
}
31+
32+
// Brute force crossings in one index.
33+
func getCrossingEdgePairsBruteForce(index *ShapeIndex, crossingType CrossingType) EdgePairVector {
34+
var result EdgePairVector
35+
minSign := Cross
36+
if crossingType == CrossingTypeAll {
37+
minSign = MaybeCross
38+
}
39+
40+
for aIter := NewEdgeIterator(index); !aIter.Done(); aIter.Next() {
41+
a := aIter.Edge()
42+
bIter := EdgeIterator{
43+
index: aIter.index,
44+
shapeID: aIter.shapeID,
45+
numEdges: aIter.numEdges,
46+
edgeID: aIter.edgeID,
47+
}
48+
for bIter.Next(); !bIter.Done(); bIter.Next() {
49+
b := bIter.Edge()
50+
// missinglink: enum ordering is reversed compared to C++
51+
if CrossingSign(a.V0, a.V1, b.V0, b.V1) <= minSign {
52+
result = append(result, EdgePair{
53+
aIter.ShapeEdgeID(),
54+
bIter.ShapeEdgeID(),
55+
})
56+
}
57+
}
58+
}
59+
return result
60+
}
61+
62+
func TestGetCrossingEdgePairs(t *testing.T) {
63+
var index ShapeIndex
64+
if len(getCrossings(&index, CrossingTypeAll)) != 0 {
65+
t.Error("Expected 0 crossings in empty index")
66+
}
67+
if len(getCrossings(&index, CrossingTypeInterior)) != 0 {
68+
t.Error("Expected 0 interior crossings in empty index")
69+
}
70+
}
71+
72+
func TestGetCrossingEdgePairsGrid(t *testing.T) {
73+
kGridSize := 10.0
74+
epsilon := 1e-10
75+
76+
// There are 11 horizontal and 11 vertical lines. The expected number of
77+
// interior crossings is 9x9, plus 9 "touching" intersections along each of
78+
// the left, right, and bottom edges. "epsilon" is used to make the interior
79+
// lines slightly longer so the "touches" actually cross, otherwise 3 of the
80+
// 27 touches are not considered intersecting.
81+
// However, the vertical lines do not reach the top line as it curves on the
82+
// surface of the sphere: despite "epsilon" those 9 are not even very close
83+
// to intersecting. Thus 9 * 12 = 108 interior and four more at the corners
84+
// when CrossingType::ALL is used.
85+
86+
index := NewShapeIndex()
87+
shape := edgeVectorShape{}
88+
89+
for i := 0.0; i <= kGridSize; i++ {
90+
var e = epsilon
91+
if i == 0 || i == kGridSize {
92+
e = 0
93+
}
94+
95+
shape.Add(PointFromLatLng(LatLngFromDegrees(-e, i)), PointFromLatLng(LatLngFromDegrees(kGridSize + e, i)));
96+
shape.Add(PointFromLatLng(LatLngFromDegrees(i, -e)), PointFromLatLng(LatLngFromDegrees(i, kGridSize + e)));
97+
}
98+
99+
index.Add(&shape)
100+
if len(getCrossingEdgePairsBruteForce(index, CrossingTypeAll)) != 112 {
101+
t.Errorf("Fail")
102+
}
103+
if len(getCrossingEdgePairsBruteForce(index, CrossingTypeInterior)) != 108 {
104+
t.Errorf("Fail")
105+
}
106+
}
107+
108+
func testHasCrossingPermutations(t *testing.T, loops []*Loop, i int, hasCrossing bool) {
109+
if i == len(loops) {
110+
index := NewShapeIndex()
111+
polygon := PolygonFromLoops(loops)
112+
index.Add(polygon)
113+
114+
if hasCrossing != FindSelfIntersection(index) {
115+
t.Error("Test failed: expected and actual crossing results do not match")
116+
}
117+
return
118+
}
119+
120+
origLoop := loops[i]
121+
for j := 0; j < origLoop.NumVertices(); j++ {
122+
vertices := make([]Point, origLoop.NumVertices())
123+
for k := 0; k < origLoop.NumVertices(); k++ {
124+
vertices[k] = origLoop.Vertex((j + k) % origLoop.NumVertices())
125+
}
126+
127+
loops[i] = LoopFromPoints(vertices)
128+
testHasCrossingPermutations(t, loops, i+1, hasCrossing)
129+
}
130+
loops[i] = origLoop
131+
}
132+
133+
func TestHasCrossing(t *testing.T) {
134+
// Coordinates are (lat,lng), which can be visualized as (y,x).
135+
cases := []struct {
136+
polygonStr string
137+
hasCrossing bool
138+
}{
139+
{"0:0, 0:1, 0:2, 1:2, 1:1, 1:0", false},
140+
{"0:0, 0:1, 0:2, 1:2, 0:1, 1:0", true}, // duplicate vertex
141+
{"0:0, 0:1, 1:0, 1:1", true}, // edge crossing
142+
{"0:0, 1:1, 0:1; 0:0, 1:1, 1:0", true}, // duplicate edge
143+
{"0:0, 1:1, 0:1; 1:1, 0:0, 1:0", true}, // reversed edge
144+
{"0:0, 0:2, 2:2, 2:0; 1:1, 0:2, 3:1, 2:0", true}, // vertex crossing
145+
}
146+
for _, tc := range cases {
147+
polygon := makePolygon(tc.polygonStr, true)
148+
testHasCrossingPermutations(t, polygon.loops, 0, tc.hasCrossing)
149+
}
150+
}

0 commit comments

Comments
 (0)