-
Notifications
You must be signed in to change notification settings - Fork 186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement shapeutil_visit_crossing_edge_pairs
#135
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package s2 | ||
|
||
import "fmt" | ||
|
||
type ShapeEdgeVector []ShapeEdge | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Go slice types of types should generally be named as the plural of the type. e.g. shapeEdges []ShapeEdge. (also Vector is used through s2 already as an X,Y,Z object, so the name would be somewhat confusing that the defined type has no ShapeEdge but no Vector part.) |
||
|
||
type EdgePairVisitor func(a, b ShapeEdge, isInterior bool) bool | ||
|
||
// getShapeEdges returns all edges in the given S2ShapeIndexCell. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace all C++ type names with the Go counterparts in comments. e.g. S2ShapeIndexCell -> ShapeIndexCell |
||
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++ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what to get out of "missinglink:"? Is this
? |
||
if sign <= minCrossingSign { | ||
if !visitor(a, b, sign == Cross) { | ||
return false | ||
} | ||
} | ||
} | ||
} | ||
return true | ||
} | ||
|
||
// Visits all pairs of crossing edges in the given S2ShapeIndex, terminating | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://go.dev/doc/comment#func Func comments should start with the name of the function. (here and all others) |
||
// 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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand why this doesn't call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two lines below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It calls |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be private and needs the corresponding doc comments from c++ |
||
ap := shape.ChainPosition(int(a.ID.EdgeID)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason not to have is_polygon in the error messages? |
||
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here and elsewhere: Error messages should start with lower-case, since they may be wrapped with more context (https://google.github.io/styleguide/go/decisions.html#error-strings) |
||
ap.ChainID, ap.Offset, bp.ChainID, bp.Offset) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a TODO to eventually add the improved loop error message. |
||
return fmt.Errorf("Edge %d crosses edge %d", ap, bp) | ||
} | ||
|
||
// Loops are not allowed to have duplicate vertices, and separate loops | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code has not been gofmt'd. |
||
// 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the C++, error could already have been initialized earlier in the process even if this method did not add anything to the error chain. Returning nil here takes away the ability to differentiate the case where this happens like here. The check on line 173 makes this a true instead of a false. |
||
} | ||
|
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make private and include the doc comments from C++ |
||
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 | ||
}, | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package s2 | ||
|
||
import ( | ||
"slices" | ||
"sort" | ||
"testing" | ||
) | ||
|
||
type EdgePair struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be private |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. const gridSize = 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CrossingTypeAll |
||
|
||
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. error messages should be more descriptive than just 'fail' |
||
} | ||
if len(getCrossingEdgePairsBruteForce(index, CrossingTypeInterior)) != 108 { | ||
t.Errorf("Fail") | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are none of the two index versions of the tests. Can you add a TODO for them to be added? |
||
func testHasCrossingPermutations(t *testing.T, loops []*Loop, i int, hasCrossing bool) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add the c++ function comments to this |
||
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idiomatic Go style is like if got := FindSelfIntersection(index); got != hasCrossing {
t.Errorf("FindSelfIntersection got %v, want %v", got, hasCrossing)
} It would be nice to include something about the loops ( |
||
} | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follow the naming conventions in other test files here and incorporate some of the file / type name into the test name so it can more easily by traced if there are errors. (this file name is long so don't need to go full java but something more then TestHasCrossing. |
||
// Coordinates are (lat,lng), which can be visualized as (y,x). | ||
cases := []struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. match the style in other files where the tests cases are named tests := []struct.... for _, test := range tests ... |
||
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) | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do these test cases match up with the C++ coverage? I don't see a
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be package private since it's only used in private methods