Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ S2Cap | ✅
S2Cell | ✅
S2CellId | ✅
S2CellIdVector | ❌
S2CellIndex | 🟡
S2CellIndex |
S2CellUnion | ✅
S2Coords | ✅
S2DensityTree | ❌
Expand Down
110 changes: 99 additions & 11 deletions s2/cell_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type cellIndexNode struct {
// newCellIndexNode returns a node with the appropriate default values.
func newCellIndexNode() cellIndexNode {
return cellIndexNode{
cellID: 0,
cellID: CellID(0),
label: cellIndexDoneContents,
parent: -1,
}
Expand All @@ -49,15 +49,56 @@ type rangeNode struct {
contents int32 // Contents of this node (an index within the cell tree).
}

type labels []int32

func (l *labels) Normalize() {
encountered := make(map[int32]struct{})

for i := range *l {
encountered[(*l)[i]] = struct{}{}
}

*l = (*l)[:0]
for key := range encountered {
*l = append(*l, key)
}
}

type cellVisitor func(cellID CellID, label int32) bool

// CellIndexIterator is an iterator that visits the entire set of indexed
// (CellID, label) pairs in an unspecified order.
type CellIndexIterator struct {
// TODO(roberts): Implement
nodes []cellIndexNode
pos int
}

// NewCellIndexIterator creates an iterator for the given CellIndex.
func NewCellIndexIterator(index *CellIndex) *CellIndexIterator {
return &CellIndexIterator{}
return &CellIndexIterator{
nodes: index.cellTree,
pos: 0,
}
}

func (c *CellIndexIterator) CellID() CellID {
return c.nodes[c.pos].cellID
}

func (c *CellIndexIterator) Label() int32 {
return c.nodes[c.pos].label
}

func (c *CellIndexIterator) Begin() {
c.pos = 0
}

func (c *CellIndexIterator) Done() bool {
return c.pos == len(c.nodes)
}

func (c *CellIndexIterator) Next() {
c.pos++
}

// CellIndexRangeIterator is an iterator that seeks and iterates over a set of
Expand Down Expand Up @@ -202,7 +243,6 @@ func (c *CellIndexRangeIterator) Seek(target CellID) {

// Nonempty needs to find the next non-empty entry.
for c.nonEmpty && c.IsEmpty() && !c.Done() {
// c.Next()
c.pos++
}
}
Expand Down Expand Up @@ -246,7 +286,7 @@ type CellIndexContentsIterator struct {
func NewCellIndexContentsIterator(index *CellIndex) *CellIndexContentsIterator {
it := &CellIndexContentsIterator{
cellTree: index.cellTree,
prevStartID: 0,
prevStartID: CellID(0),
nodeCutoff: -1,
nextNodeCutoff: -1,
node: cellIndexNode{label: cellIndexDoneContents},
Expand All @@ -256,7 +296,7 @@ func NewCellIndexContentsIterator(index *CellIndex) *CellIndexContentsIterator {

// Clear clears all state with respect to which range(s) have been visited.
func (c *CellIndexContentsIterator) Clear() {
c.prevStartID = 0
c.prevStartID = CellID(0)
c.nodeCutoff = -1
c.nextNodeCutoff = -1
c.node.label = cellIndexDoneContents
Expand Down Expand Up @@ -482,7 +522,8 @@ func (c *CellIndex) Build() {
c.cellTree = append(c.cellTree, cellIndexNode{
cellID: deltas[i].cellID,
label: deltas[i].label,
parent: contents})
parent: contents,
})
contents = int32(len(c.cellTree) - 1)
} else if deltas[i].cellID == SentinelCellID {
contents = c.cellTree[contents].parent
Expand All @@ -492,7 +533,54 @@ func (c *CellIndex) Build() {
}
}

// TODO(roberts): Differences from C++
// IntersectingLabels
// VisitIntersectingCells
// CellIndexIterator
// IntersectingLabels is a convenience function that returns
// the labels of all indexed cells that intersect the given CellUnion "target".
func (c *CellIndex) IntersectingLabels(target CellUnion) []int32 {
var labels labels
c.VisitIntersectingCells(target, func(cellID CellID, label int32) bool {
labels = append(labels, label)
return true
})

labels.Normalize()
return labels
}

func (c *CellIndex) VisitIntersectingCells(target CellUnion, visitor cellVisitor) bool {
if len(target) == 0 {
return true
}

pos := 0
contents := NewCellIndexContentsIterator(c)
rangeIter := NewCellIndexRangeIterator(c)
rangeIter.Begin()

for ok := true; ok; ok = pos != len(target) {
if rangeIter.LimitID() <= target[pos].RangeMin() {
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 was if rangeIter.LimitID() <= rangeIter.RangeMin() { is there a reason it's not like that here?

Copy link
Author

Choose a reason for hiding this comment

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

In C++ it was range.limit_id() <= it->range_min() where it is an iterator derived from target. Because there's no iterator I manually iterate over the cell ids.

Copy link
Collaborator

Choose a reason for hiding this comment

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

rangeIter.Seek(target[pos].RangeMin())
}

for ; rangeIter.StartID() <= target[pos].RangeMax(); rangeIter.Next() {
for contents.StartUnion(rangeIter); !contents.Done(); contents.Next() {
if !visitor(contents.CellID(), contents.Label()) {
return false
}
}
}

// Check whether the next target cell is also contained by the leaf cell
// range that we just processed. If so, we can skip over all such cells
// using binary search. This speeds up benchmarks by between 2x and 10x
// when the average number of intersecting cells is small (< 1).
pos++
if pos != len(target) && target[pos].RangeMax() < rangeIter.StartID() {
pos = target.lowerBound(pos+1, len(target), rangeIter.StartID())
if target[pos-1].RangeMax() >= rangeIter.StartID() {
pos--
}
}
}

return true
}
189 changes: 170 additions & 19 deletions s2/cell_index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@ func cellIndexNodesEqual(a, b []cellIndexNode) bool {
sort.Slice(b, func(i, j int) bool {
return b[i].less(b[j])
})
return reflect.DeepEqual(a, b)
Copy link
Author

Choose a reason for hiding this comment

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

parent doesn't need to be equal and checked. (See LabelledCell in C++)


for i := range a {
if a[i].cellID != b[i].cellID && a[i].label != b[i].label {
return false
}
}

return true
}

// copyCellIndexNodes creates a copy of the nodes so that sorting and other tests
Expand All @@ -61,19 +67,16 @@ func copyCellIndexNodes(in []cellIndexNode) []cellIndexNode {
}

func verifyCellIndexCellIterator(t *testing.T, desc string, index *CellIndex) {
// TODO(roberts): Once the plain iterator is implemented, add this check.
/*
var actual []cellIndexNode
iter := NewCellIndexIterator(index)
for iter.Begin(); !iter.Done(); iter.Next() {
actual = append(actual, cellIndexNode{iter.StartID(), iter.Label())
}
var actual []cellIndexNode
iter := NewCellIndexIterator(index)
for iter.Begin(); !iter.Done(); iter.Next() {
actual = append(actual, cellIndexNode{cellID: iter.CellID(), label: iter.Label()})
}

want := copyCellIndexNodes(index.cellTree)
if !cellIndexNodesEqual(actual, want) {
t.Errorf("%s: cellIndexNodes not equal but should be. %v != %v", desc, actual, want)
}
*/
want := copyCellIndexNodes(index.cellTree)
if !cellIndexNodesEqual(actual, want) {
t.Errorf("%s: cellIndexNodes not equal but should be. %v != %v", desc, actual, want)
}
}

func verifyCellIndexRangeIterators(t *testing.T, desc string, index *CellIndex) {
Expand Down Expand Up @@ -338,9 +341,157 @@ func TestCellIndexRandomCellUnions(t *testing.T) {
cellIndexQuadraticValidate(t, "Random Cell Unions", index, nil)
}

// TODO(roberts): Differences from C++
//
// Add remainder of TestCellIndexContentsIteratorSuppressesDuplicates
//
// additional Iterator related parts
// Intersections related
func expectContents(t *testing.T, target CellID, index *CellIndex, contents *CellIndexContentsIterator, expected []cellIndexNode) {
rangeIter := NewCellIndexRangeIterator(index)
rangeIter.Seek(target)

var actual []cellIndexNode
for contents.StartUnion(rangeIter); !contents.Done(); contents.Next() {
actual = append(actual, cellIndexNode{cellID: contents.CellID(), label: contents.Label()})
}
if !cellIndexNodesEqual(expected, actual) {
t.Errorf("comparing contents iterator contents to this range: got %+v, want %+v", actual, expected)
}
}

func TestCellIndexContentsIteratorSuppressesDuplicates(t *testing.T) {
index := &CellIndex{}
index.Add(CellIDFromString("2/1"), 1)
index.Add(CellIDFromString("2/1"), 2)
index.Add(CellIDFromString("2/10"), 3)
index.Add(CellIDFromString("2/100"), 4)
index.Add(CellIDFromString("2/102"), 5)
index.Add(CellIDFromString("2/1023"), 6)
index.Add(CellIDFromString("2/31"), 7)
index.Add(CellIDFromString("2/313"), 8)
index.Add(CellIDFromString("2/3132"), 9)
index.Add(CellIDFromString("3/1"), 10)
index.Add(CellIDFromString("3/12"), 11)
index.Add(CellIDFromString("3/13"), 12)

cellIndexQuadraticValidate(t, "Suppress Duplicates", index, nil)

contents := NewCellIndexContentsIterator(index)
expectContents(t, CellIDFromString("1/123"), index, contents, []cellIndexNode{})
expectContents(t, CellIDFromString("2/100123"), index, contents, []cellIndexNode{{cellID: CellIDFromString("2/1"), label: 1}, {cellID: CellIDFromString("2/1"), label: 2}, {cellID: CellIDFromString("2/10"), label: 3}, {cellID: CellIDFromString("2/100"), label: 4}})
// Check that a second call with the same key yields no additional results.
expectContents(t, CellIDFromString("2/100123"), index, contents, []cellIndexNode{})
// Check that seeking to a different branch yields only the new values.
expectContents(t, CellIDFromString("2/10232"), index, contents, []cellIndexNode{{cellID: CellIDFromString("2/102"), label: 5}, {cellID: CellIDFromString("2/1023"), label: 6}})
// Seek to a node with a different root.
expectContents(t, CellIDFromString("2/313"), index, contents, []cellIndexNode{{cellID: CellIDFromString("2/31"), label: 7}, {cellID: CellIDFromString("2/313"), label: 8}})
// Seek to a descendant of the previous node.
expectContents(t, CellIDFromString("2/3132333"), index, contents, []cellIndexNode{{cellID: CellIDFromString("2/3132"), label: 9}})
// Seek to an ancestor of the previous node.
expectContents(t, CellIDFromString("2/213"), index, contents, []cellIndexNode{})
// A few more tests of incremental reporting.
expectContents(t, CellIDFromString("3/1232"), index, contents, []cellIndexNode{{cellID: CellIDFromString("3/1"), label: 10}, {cellID: CellIDFromString("3/12"), label: 11}})
expectContents(t, CellIDFromString("3/133210"), index, contents, []cellIndexNode{{cellID: CellIDFromString("3/13"), label: 12}})
expectContents(t, CellIDFromString("3/133210"), index, contents, []cellIndexNode{})
expectContents(t, CellIDFromString("5/0"), index, contents, []cellIndexNode{})

// Now try moving backwards, which is expected to yield values that were
// already reported above.
expectContents(t, CellIDFromString("3/13221"), index, contents, []cellIndexNode{{cellID: CellIDFromString("3/1"), label: 10}, {cellID: CellIDFromString("3/13"), label: 12}})
expectContents(t, CellIDFromString("2/31112"), index, contents, []cellIndexNode{{cellID: CellIDFromString("2/31"), label: 7}})
}

func TestCellIndexIntersectionOptimization(t *testing.T) {
type cellIndexTestInput struct {
cellID string
label int32
}
tests := []struct {
label string
have []cellIndexTestInput
}{
{
// Tests various corner cases for the binary search optimization in
// VisitIntersectingCells.
label: "Intersection Optimization",
have: []cellIndexTestInput{
{"1/001", 1},
{"1/333", 2},
{"2/00", 3},
{"2/0232", 4},
},
},
}

for _, test := range tests {
index := &CellIndex{}
for _, v := range test.have {
index.Add(CellIDFromString(v.cellID), v.label)
}
index.Build()
checkIntersection(t, test.label, makeCellUnion("1/010", "1/3"), index)
checkIntersection(t, test.label, makeCellUnion("2/010", "2/011", "2/02"), index)
}
}

func TestCellIndexIntersectionRandomCellUnions(t *testing.T) {
// Construct cell unions from random CellIDs at random levels. Note that
// because the cell level is chosen uniformly, there is a very high
// likelihood that the cell unions will overlap.
index := &CellIndex{}
for i := int32(0); i < 100; i++ {
index.AddCellUnion(randomCellUnion(10), i)
}
index.Build()
for i := 0; i < 200; i++ {
checkIntersection(t, "", randomCellUnion(10), index)
}
}

func TestCellIndexIntersectionSemiRandomCellUnions(t *testing.T) {
for i := 0; i < 200; i++ {
index := &CellIndex{}
id := CellIDFromString("1/0123012301230123")
var target CellUnion
for j := 0; j < 100; j++ {
switch {
case oneIn(10):
index.Add(id, int32(j))
case oneIn(4):
target = append(target, id)
case oneIn(2):
id = id.NextWrap()
case oneIn(6) && !id.isFace():
id = id.immediateParent()
case oneIn(6) && !id.IsLeaf():
id = id.ChildBegin()
}
}
target.Normalize()
Copy link
Collaborator

Choose a reason for hiding this comment

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

The call to Normalize wasn't in the C++. Does removing this change the test outcomes?

Copy link
Author

Choose a reason for hiding this comment

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

Removing target.Normalize() fails the test. I can't remember why anymore (my bad, left this open for too long).

Copy link
Collaborator

Choose a reason for hiding this comment

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

index.Build()
checkIntersection(t, "", target, index)
}
}

func checkIntersection(t *testing.T, desc string, target CellUnion, index *CellIndex) {
var expected, actual []int32
for it := NewCellIndexIterator(index); !it.Done(); it.Next() {
if target.IntersectsCellID(it.CellID()) {
expected = append(expected, it.Label())
}
}

index.VisitIntersectingCells(target, func(cellID CellID, label int32) bool {
actual = append(actual, label)
return true
})

if !labelsEqual(actual, expected) {
t.Errorf("%s: labels not equal but should be. %v != %v", desc, actual, expected)
}
}

func labelsEqual(a, b []int32) bool {
sort.Slice(a, func(i, j int) bool {
return a[i] < a[j]
})
sort.Slice(b, func(i, j int) bool {
return b[i] < b[j]
})
return reflect.DeepEqual(a, b)
}
1 change: 1 addition & 0 deletions s2/s2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func randomCellUnion(n int) CellUnion {
for i := 0; i < n; i++ {
cu = append(cu, randomCellID())
}
cu.Normalize()
return cu
}

Expand Down