Skip to content
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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
176 changes: 176 additions & 0 deletions s2/shapeutil_visit_crossing_edge_pairs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package s2

import "fmt"

type ShapeEdgeVector []ShapeEdge
Copy link
Collaborator

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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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++
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what to get out of "missinglink:"? Is this

// Note that enum ordering is reversed compared to C++
// TODO(missinglink): Make them match.

?

if sign <= minCrossingSign {
if !visitor(a, b, sign == Cross) {
return false
}
}
}
}
return true
}

// Visits all pairs of crossing edges in the given S2ShapeIndex, terminating
Copy link
Collaborator

Choose a reason for hiding this comment

The 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() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two lines below.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It calls visitCrossings in a loop. Could this be a single call to VisitCrossings, like the C++ does? It looks like you're reimplementing VisitCrossings.

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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))
Copy link
Collaborator

Choose a reason for hiding this comment

The 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",
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
},
)
}
150 changes: 150 additions & 0 deletions s2/shapeutil_visit_crossing_edge_pairs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package s2

import (
"slices"
"sort"
"testing"
)

type EdgePair struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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")
Copy link
Collaborator

Choose a reason for hiding this comment

The 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")
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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")
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 (FindSelfIntersection(%s)), though there isn't a loopToString textformat function yet.

}
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 FindSelfIntersection test.

TEST(GetCrossingEdgePairs, NoIntersectionsOneIndex) {
TEST(GetCrossingEdgePairs, NoIntersectionsTwoIndexes) {
TEST(GetCrossingEdgePairs, EdgeGridOneIndex) {
TEST(GetCrossingEdgePairs, EdgeGridTwoIndexes) {
TEST(FindSelfIntersection, Basic) {