diff --git a/README.md b/README.md index afa923c9..f1dde08f 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ S2Cap | ✅ S2Cell | ✅ S2CellId | ✅ S2CellIdVector | ❌ -S2CellIndex | 🟡 +S2CellIndex | ✅ S2CellUnion | ✅ S2Coords | ✅ S2DensityTree | ❌ diff --git a/s2/cell_index.go b/s2/cell_index.go index e1bf4bbc..80cf5d5d 100644 --- a/s2/cell_index.go +++ b/s2/cell_index.go @@ -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, } @@ -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 @@ -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++ } } @@ -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}, @@ -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 @@ -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 @@ -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() { + 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 +} diff --git a/s2/cell_index_test.go b/s2/cell_index_test.go index 7389d806..7e6a416f 100644 --- a/s2/cell_index_test.go +++ b/s2/cell_index_test.go @@ -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) + 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 @@ -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) { @@ -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() + 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) +} diff --git a/s2/s2_test.go b/s2/s2_test.go index 1b85952b..ad0374db 100644 --- a/s2/s2_test.go +++ b/s2/s2_test.go @@ -152,6 +152,7 @@ func randomCellUnion(n int) CellUnion { for i := 0; i < n; i++ { cu = append(cu, randomCellID()) } + cu.Normalize() return cu }