Skip to content

Commit 40abfda

Browse files
committed
Add incidentEdgeTracker and indexCellData types and tests
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.
1 parent 0a13e5a commit 40abfda

4 files changed

+778
-0
lines changed

s2/incident_edge_tracker.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2025 The S2 Geometry Project Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package s2
16+
17+
// incidentEdgeKey is a tuple of (shape id, vertex) that compares by shape id.
18+
type incidentEdgeKey struct {
19+
shapeID int32
20+
vertex Point
21+
}
22+
23+
// We need a strict ordering to be a valid key for an ordered container, but
24+
// we don't actually care about the ordering of the vertices (as long as
25+
// they're grouped by shape id). Vertices are 3D points so they don't have a
26+
// natural ordering, so we'll just compare them lexicographically.
27+
func (i incidentEdgeKey) Cmp(o incidentEdgeKey) int {
28+
if i.shapeID < o.shapeID {
29+
return -1
30+
}
31+
if i.shapeID > o.shapeID {
32+
return 1
33+
}
34+
35+
return i.vertex.Cmp(o.vertex.Vector)
36+
}
37+
38+
// vertexEdge is a tuple of vertex and edgeID for processing incident edges.
39+
type vertexEdge struct {
40+
vertex Point
41+
edgeID int32
42+
}
43+
44+
// incidentEdgeTracker is a used for detecting and tracking shape edges that
45+
// are incident on the same vertex. Edges of multiple shapes may be tracked,
46+
// but lookup is by shape id and vertex: there is no facility to get all
47+
// edges of all shapes at a vertex. Edge vertices must compare exactly equal
48+
// to be considered the same vertex, no tolerance is applied as this isn't
49+
// intended for e.g.: snapping shapes together, which Builder does better
50+
// and more robustly.
51+
//
52+
// To use, instantiate and then add edges with one or more sequences of calls,
53+
// where each sequence begins with startShape(), followed by addEdge() calls to
54+
// add edges for that shape, and ends with finishShape(). Those sequences do
55+
// not need to visit shapes or edges in order. Then, call incidentEdges() to get
56+
// the resulting map from incidentEdgeKeys (which are shapeId, vertex pairs) to
57+
// a set of edgeIds of the shape that are incident to that vertex..
58+
//
59+
// This works on a block of edges at a time, meaning that to detect incident
60+
// edges on a particular vertex, we must have at least three edges incident
61+
// at that vertex when finishShape() is called. We don't maintain partial
62+
// information between calls. However, subject to this constraint, a single
63+
// shape's edges may be defined with multiple sequences of startShape(),
64+
// addEdge()... , finishShape() calls.
65+
//
66+
// The reason for this is simple: most edges don't have more than two incident
67+
// edges (the incoming and outgoing edge). If we had to maintain information
68+
// between calls, we'd end up with a map that contains every vertex, to no
69+
// benefit. Instead, when finishShape() is called, we discard vertices that
70+
// contain two or fewer incident edges.
71+
//
72+
// In principle this isn't a real limitation because generally we process a
73+
// ShapeIndex cell at a time, and, if a vertex has multiple edges, we'll see
74+
// all the edges in the same cell as the vertex, and, in general, it's possible
75+
// to aggregate edges before calling.
76+
//
77+
// The tracker maintains incident edges until it's cleared. If you call it with
78+
// each cell from an ShapeIndex, then at the end you will have all the
79+
// incident edge information for the whole index. If only a subset is needed,
80+
// call reset() when you're done.
81+
type incidentEdgeTracker struct {
82+
currentShapeID int32
83+
84+
nursery []vertexEdge
85+
86+
// We can and do encounter the same edges multiple times, so we need to
87+
// deduplicate edges as they're inserted.
88+
edgeMap map[incidentEdgeKey]map[int32]bool
89+
}
90+
91+
// newIncidentEdgeTracker returns a new tracker.
92+
func newIncidentEdgeTracker() *incidentEdgeTracker {
93+
return &incidentEdgeTracker{
94+
currentShapeID: -1,
95+
nursery: []vertexEdge{},
96+
edgeMap: make(map[incidentEdgeKey]map[int32]bool),
97+
}
98+
}
99+
100+
// startShape is used to start adding edges to the edge tracker. After calling,
101+
// any vertices with multiple (> 2) incident edges will appear in the
102+
// incident edge map.
103+
func (t *incidentEdgeTracker) startShape(id int32) {
104+
t.currentShapeID = id
105+
t.nursery = t.nursery[:0]
106+
}
107+
108+
// addEdge adds the given edges start to the nursery, and if not degenerate,
109+
// adds it second endpoint as well.
110+
func (t *incidentEdgeTracker) addEdge(edgeID int32, e Edge) {
111+
if t.currentShapeID < 0 {
112+
return
113+
}
114+
115+
// Add non-degenerate edges to the nursery.
116+
t.nursery = append(t.nursery, vertexEdge{vertex: e.V0, edgeID: edgeID})
117+
if !e.IsDegenerate() {
118+
t.nursery = append(t.nursery, vertexEdge{vertex: e.V1, edgeID: edgeID})
119+
}
120+
}
121+
122+
func (t *incidentEdgeTracker) finishShape() {
123+
// We want to keep any vertices with more than two incident edges. We could
124+
// sort the array by vertex and remove any with fewer, but that would require
125+
// shifting the array and could turn quadratic quickly.
126+
//
127+
// Instead we'll scan forward from each vertex, swapping entries with the same
128+
// vertex into a contiguous range. Once we've done all the swapping we can
129+
// just make sure that we have at least three edges in the range.
130+
nurserySize := len(t.nursery)
131+
for start := 0; start < nurserySize; {
132+
end := start + 1
133+
134+
// Scan to the end of the array, swap entries so that entries with
135+
// the same vertex as the start are adjacent.
136+
next := start
137+
currVertex := t.nursery[start].vertex
138+
for next+1 < nurserySize {
139+
next++
140+
if t.nursery[next].vertex == currVertex {
141+
t.nursery[next], t.nursery[end] = t.nursery[end], t.nursery[next]
142+
end++
143+
}
144+
}
145+
146+
// Most vertices will have two incident edges (the incoming edge and the
147+
// outgoing edge), which aren't interesting, skip them.
148+
numEdges := end - start
149+
if numEdges <= 2 {
150+
start = end
151+
continue
152+
}
153+
154+
key := incidentEdgeKey{
155+
shapeID: t.currentShapeID,
156+
vertex: t.nursery[start].vertex,
157+
}
158+
159+
// If we don't have this key yet, create it manually.
160+
if _, ok := t.edgeMap[key]; !ok {
161+
t.edgeMap[key] = map[int32]bool{}
162+
}
163+
164+
for ; start != end; start++ {
165+
t.edgeMap[key][t.nursery[start].edgeID] = true
166+
}
167+
}
168+
}
169+
170+
// reset removes all incident edges from the tracker.
171+
func (t *incidentEdgeTracker) reset() {
172+
t.edgeMap = make(map[incidentEdgeKey]map[int32]bool)
173+
}

s2/incident_edge_tracker_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2025 The S2 Geometry Project Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package s2
16+
17+
import (
18+
"testing"
19+
)
20+
21+
func TestIncidentEdgeTrackerBasic(t *testing.T) {
22+
tests := []struct {
23+
index string
24+
want int
25+
}{
26+
// These shapeindex strings came from validation query's test
27+
// corpus to determine which ones ended up actually getting
28+
// tracked edges.
29+
{
30+
// Has 0 tracked edges
31+
index: "## 0:0, 1:1",
32+
want: 0,
33+
},
34+
{
35+
// Has 1 tracked edges
36+
index: "## 2:0, 0:-2, -2:0, 0:2; 2:0, 0:-1, -1:0, 0:1",
37+
want: 1,
38+
},
39+
{
40+
// Has 2 tracked edges
41+
index: "## 2:0, 0:-2, -2:0, 0:2; 2:0, 0:-1, -2:0, 0:1",
42+
want: 2,
43+
},
44+
}
45+
46+
for _, test := range tests {
47+
index := makeShapeIndex(test.index)
48+
index.Build()
49+
50+
iter := index.Iterator()
51+
celldata := newIndexCellData()
52+
celldata.loadCell(index, iter.CellID(), iter.IndexCell())
53+
54+
tracker := newIncidentEdgeTracker()
55+
56+
for _, clipped := range celldata.indexCell.shapes {
57+
shapeID := clipped.shapeID
58+
tracker.startShape(shapeID)
59+
for _, e := range celldata.shapeEdges(shapeID) {
60+
tracker.addEdge(e.ID, e.Edge)
61+
}
62+
tracker.finishShape()
63+
}
64+
65+
if got := len(tracker.edgeMap); got != test.want {
66+
t.Errorf("incidentEdgeTracker should have %d edges, got %d",
67+
test.want, got)
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)