Skip to content

Add incidentEdgeTracker and indexCellData types and tests #180

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

rsned
Copy link
Collaborator

@rsned rsned commented May 15, 2025

Add incidentEdgeTracker and indexCellData types and tests.
Update LaxPolygon to support proper vertex ordering on holes and shells.
Fix comments and variable names in LaxPolygon.
Add more of the missing LaxPolygon tests to validate fixes.

These are ports of the corresponding C++ s2/internal/ types that are needed for ValidationQuery for Loops and Polygons.

This PR works towards issues #72, #108.

rsned added 3 commits May 15, 2025 13:07
These are ports of the C++ s2/internal/ types that are needed for ValidationQuery for Loops and Polygons.

Work for issues golang#72, golang#108.
Clean up some comments and variable names.
Update some of the methods to match current C++ logic.
Add more unit tests for multiloop polygons to validate that holes are inverted ordering.
@alan-strohm alan-strohm self-assigned this May 20, 2025
Copy link
Collaborator

@alan-strohm alan-strohm left a comment

Choose a reason for hiding this comment

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

Sending some initial thoughts since you've been waiting a while and some comments might need discussion. I'll look more tomorrow. Thanks for doing this!

@@ -0,0 +1,173 @@
// Copyright 2025 The S2 Geometry Project Authors. All rights reserved.
Copy link
Collaborator

@alan-strohm alan-strohm May 27, 2025

Choose a reason for hiding this comment

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

I assume we need to resolve this discussion about what to do with Copyrights before proceeding: https://github.com/golang/geo/pull/178/files#r2094005567

}

// vertexEdge is a tuple of vertex and edgeID for processing incident edges.
type vertexEdge struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In C++ this concept is private to the tracker. The s2 package namespace seems pretty big and "vertexEdge" very generic. Do we want to be more careful about adding concepts to it?

Options:

  1. Leave it be until we get a collision.
  2. Switch to an anonymous struct. addEdge becomes pretty painful but maybe it's worth it.
  3. use a longer name vertexEdgeForIncidentTracking?

I lean towards 2. but I don't feel strongly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is also coming up in index_cell_data.go I think we should consider a 4th option as well:

Move this code to an "internal" package so that it can have a public interface without being a public part of the s2 package.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are a ton of these little pair typedefs throughout the C++, many of which end up as int, int or int32, int32.

// 7 std::pair<Distance, Id <--- typedef int
// 1 std::pair<double, double
// 1 std::pair<InputVertexId, InputVertexId <--- typedef int32_t
// 2 std::pair<int32_t, int32_t
// 1 std::pair<int, bool
// 1 std::pair<int, Candidate*
// 1 std::pair<int, EdgeId <--- typedef int32_t
// 1 std::pair<int, int
// 1 std::pair<int, uint64_t
// 1 std::pair<S2CellId, InputVertexId <--- typedef int32_t
// 14 std::pair<S2CellId, S2CellId
// 1 std::pair<S2Point, int
// 5 std::pair<S2Point, S2Point
// 2 std::pair<ShapeEdgeId, ShapeEdgeId <--- typedef int32_t
// 1 std::pair<SiteId, SiteId <--- typedef int32_t
// 4 std::pair<TouchType, TouchType <--- typedef int
// 8 std::pair<S2Shape, int64_t
// 1 std::pair<VertexId, VertexId <--- typedef int32_t

Would it be easier to define a simple generic type "Pair" and then use that where its needed instead of making these 30+ one-off types? e.g.,

type pair[T1, T2 any] struct {
first T1
second T2
}

...

type incidentEdgeTracker struct {
currentShapeID int32
nursery []pair[Point, int32] <--- (was nursery []pointEdgeSomething)
}

And then

    // Add non-degenerate edges to the nursery.
    t.nursery = append(t.nursery, pair[Point, int32]{first: e.V0, second: edgeID})
    if !e.IsDegenerate() {
            t.nursery = append(t.nursery, pair[Point, int32]{first: e.V1, second: edgeID})
    }

....

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The problem with moving this to "internal" is that it still requires all the types and test helpers from the s2 package which would make circular dependency chain.

$ go build .

github.com/golang/geo/s2/internal

./incident_edge_tracker.go:20:10: undefined: Point
./incident_edge_tracker.go:45:9: undefined: Point
./incident_edge_tracker.go:89:17: undefined: Point
./incident_edge_tracker.go:115:55: undefined: Edge
./index_cell_data.go:40:2: undefined: Edge
./index_cell_data.go:85:13: undefined: ShapeIndex
./index_cell_data.go:86:13: undefined: ShapeIndexCell
./index_cell_data.go:87:12: undefined: CellID
./index_cell_data.go:93:13: undefined: Cell
./index_cell_data.go:95:13: undefined: Point

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ack. Let's take option 4 off the table. I'm guessing we want one of 1-3 for this pull request. I don't feel strongly between them. As long as you thought through all 3 options and are happy with your recommendations, let's go with your recommendation.

How many of those pairs already have struct versions due to previous porting? If there are still a lot of pairs remaining, I can start a discussion documenting options 1-3 and seeking feedback on your suggestion of a generic as a more ergonomic form of 2.


package s2

// incidentEdgeKey is a tuple of (shape id, vertex) that compares by shape id.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: id -> ID throughout.

// add edges for that shape, and ends with finishShape(). Those sequences do
// not need to visit shapes or edges in order. Then, call incidentEdges() to get
// the resulting map from incidentEdgeKeys (which are shapeId, vertex pairs) to
// a set of edgeIds of the shape that are incident to that vertex..
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: remove duplicate period.

// shape's edges may be defined with multiple sequences of startShape(),
// addEdge()... , finishShape() calls.
//
// The reason for this is simple: most edges don't have more than two incident
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this supposed to read "most vertices don't have more than two incident edges"? I'm having a hard time following these two paragraphs and I don't think it' s just my lack of subject-matter experience...

If we're OK with more divergence with the C++, I think it'd be clearer to make the 3rd paragraph about how we only look for vertices with 3 or more incident edges and that we don't remember vertices with fewer incident edges after a call to finishShape. Then in the 4th (and 5th if necessary) paragraph, I'd talk about how "a single shape's edges may be defined with multiple sequences..." as long as the caller ensures that all the edges on a given vertex are handled in the same sequence of calls via something like ShapeIndex cells.

But if you want to stick to correcting obvious errors, that seems like a very reasonable choice.

// To use, instantiate and then add edges with one or more sequences of calls,
// where each sequence begins with startShape(), followed by addEdge() calls to
// add edges for that shape, and ends with finishShape(). Those sequences do
// not need to visit shapes or edges in order. Then, call incidentEdges() to get
Copy link
Collaborator

Choose a reason for hiding this comment

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

"Then, call incidentEdges()"... there's no function with that name. Update the sentence to describe golang usage. "Then, read the edgeMap member to get..."


nursery []vertexEdge

// We can and do encounter the same edges multiple times, so we need to
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment should describe the data structure first. The C++ says "Map from keys of (shape id, vertex) to a set of edge_ids." That works fine for me. However, I'd find it helpful to also mention that this only includes vertices with 3 or more incident edge IDs.

// all the edges in the same cell as the vertex, and, in general, it's possible
// to aggregate edges before calling.
//
// The tracker maintains incident edges until it's cleared. If you call it with
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: s/clear/reset/ since that's the name of the function and the word "clear" doesn't appear anywhere else in this file.

// adds it second endpoint as well.
func (t *incidentEdgeTracker) addEdge(edgeID int32, e Edge) {
if t.currentShapeID < 0 {
return
Copy link
Collaborator

Choose a reason for hiding this comment

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

The error handling across these functions is pretty bad. Here, you silently drop edges. There's no checking to ensure that calls are made in the right sequence. Should we re-examine the API to make erroneous usage more difficult? All the internal calls to the C++ version look like:

      incident_edge_tracker_.StartShape(shape_id);
      for (const auto& edge : CurrentCell().shape_edges(shape_id)) {
        incident_edge_tracker_.AddEdge(edge.id, edge);
      }
      incident_edge_tracker_.FinishShape();

So we could have an interface that looks like:

// Add all edges to the tracker.   After calling, any vertices with multiple (> 2) incident
// edges will appear in the incident edge map.  Returns an error if any edges have a different
// shape ID than shapeID.
func (t *incidentEdgeTracker) addShapeEdges(shapeID int32, edges []ShapeEdge) error

This would also remove the need for a "nursery" and the associated type, resolving one of my other comments.

}
}

// startShape is used to start adding edges to the edge tracker. After calling,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment is copied from the C++ where the scope is ambiguous. "After calling" more accurately describes finishShape. If we stick to this API, let's move the comment there.

Copy link
Collaborator

@alan-strohm alan-strohm left a comment

Choose a reason for hiding this comment

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

OK, this should be a complete set of comments.

index string
want int
}{
// These shapeindex strings came from validation query's test
Copy link
Collaborator

Choose a reason for hiding this comment

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

// cell returns the S2 Cell for the current index cell, loading it if it
// was not already set.
func (d *indexCellData) cell() Cell {
if !d.s2CellSet.Load() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

}

// vertexEdge is a tuple of vertex and edgeID for processing incident edges.
type vertexEdge struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is also coming up in index_cell_data.go I think we should consider a 4th option as well:

Move this code to an "internal" package so that it can have a public interface without being a public part of the s2 package.

edge := shape.ChainEdge(position.ChainID, position.Offset)
d.edges = append(d.edges, edgeAndIDChain{
Edge: edge,
ID: int32(edgeID),
Copy link
Collaborator

Choose a reason for hiding this comment

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

why are you using int32 when the shape APIs are returning int?

Comment on lines +70 to +71
data.dimWanted[j] = test.dimWanted[j]
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
data.dimWanted[j] = test.dimWanted[j]
}
data.setDimWanted(j, test.dimWanted[j])
}

dimEmpty [3]bool
}{
{
// Check that we get all dimensions by default.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't actually test the default behavior since it forces the dimWanted to match the test case.

}
}

// newindexCellDataFromCell creates a new indexCellData and loads the given
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// newindexCellDataFromCell creates a new indexCellData and loads the given
// newIndexCellDataFromCell creates a new indexCellData and loads the given

Comment on lines +75 to +76
func LaxPolygonFromLoops(l []Loop) *LaxPolygon {
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.

Was this intentional?


for _, edge := range edges {
if shape.ChainPosition(edge.e) != (ChainPosition{edge.i, edge.j}) {
t.Errorf("addasdaa")
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 real failure message (also for the next condition in this loop).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants