Skip to content

Commit 050ea44

Browse files
rsneddsymonds
authored andcommitted
s2: Fix issues with Polygon.Invert.
The counters were not being properly cleared during the invert, so this led to index out of bounds panics. Also pulled in a few small changes to stay in sync with the C++ code for Polycon.ContainsPoint. Fixes #68. Fixes #69. Signed-off-by: David Symonds <dsymonds@golang.org>
1 parent 90a6f84 commit 050ea44

File tree

3 files changed

+52
-21
lines changed

3 files changed

+52
-21
lines changed

s2/loop.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type Loop struct {
7272
func LoopFromPoints(pts []Point) *Loop {
7373
l := &Loop{
7474
vertices: pts,
75+
index: NewShapeIndex(),
7576
}
7677

7778
l.initOriginAndBound()
@@ -96,6 +97,7 @@ func LoopFromCell(c Cell) *Loop {
9697
c.Vertex(2),
9798
c.Vertex(3),
9899
},
100+
index: NewShapeIndex(),
99101
}
100102

101103
l.initOriginAndBound()
@@ -587,16 +589,24 @@ func (l *Loop) bruteForceContainsPoint(p Point) bool {
587589

588590
// ContainsPoint returns true if the loop contains the point.
589591
func (l *Loop) ContainsPoint(p Point) bool {
590-
// Empty and full loops don't need a special case, but invalid loops with
591-
// zero vertices do, so we might as well handle them all at once.
592-
if len(l.vertices) < 3 {
593-
return l.originInside
592+
if !l.index.IsFresh() && !l.bound.ContainsPoint(p) {
593+
return false
594594
}
595595

596-
// For small loops, and during initial construction, it is faster to just
597-
// check all the crossing.
596+
// For small loops it is faster to just check all the crossings. We also
597+
// use this method during loop initialization because InitOriginAndBound()
598+
// calls Contains() before InitIndex(). Otherwise, we keep track of the
599+
// number of calls to Contains() and only build the index when enough calls
600+
// have been made so that we think it is worth the effort. Note that the
601+
// code below is structured so that if many calls are made in parallel only
602+
// one thread builds the index, while the rest continue using brute force
603+
// until the index is actually available.
604+
598605
const maxBruteForceVertices = 32
599-
if len(l.vertices) < maxBruteForceVertices || l.index == nil {
606+
// TODO(roberts): add unindexed contains calls tracking
607+
608+
if len(l.index.shapes) == 0 || // Index has not been initialized yet.
609+
len(l.vertices) <= maxBruteForceVertices {
600610
return l.bruteForceContainsPoint(p)
601611
}
602612

@@ -802,20 +812,23 @@ func (l *Loop) TurningAngle() float64 {
802812
compensation = (oldSum - sum) + angle
803813
n--
804814
}
805-
return float64(dir) * float64(sum+compensation)
815+
816+
const maxCurvature = 2*math.Pi - 4*dblEpsilon
817+
818+
return math.Max(-maxCurvature, math.Min(maxCurvature, float64(dir)*float64(sum+compensation)))
806819
}
807820

808821
// turningAngleMaxError return the maximum error in TurningAngle. The value is not
809822
// constant; it depends on the loop.
810823
func (l *Loop) turningAngleMaxError() float64 {
811824
// The maximum error can be bounded as follows:
812-
// 2.24 * dblEpsilon for RobustCrossProd(b, a)
813-
// 2.24 * dblEpsilon for RobustCrossProd(c, b)
825+
// 3.00 * dblEpsilon for RobustCrossProd(b, a)
826+
// 3.00 * dblEpsilon for RobustCrossProd(c, b)
814827
// 3.25 * dblEpsilon for Angle()
815828
// 2.00 * dblEpsilon for each addition in the Kahan summation
816829
// ------------------
817-
// 9.73 * dblEpsilon
818-
maxErrorPerVertex := 9.73 * dblEpsilon
830+
// 11.25 * dblEpsilon
831+
maxErrorPerVertex := 11.25 * dblEpsilon
819832
return maxErrorPerVertex * float64(len(l.vertices))
820833
}
821834

@@ -1275,12 +1288,12 @@ func (l *Loop) decode(d *decoder) {
12751288
l.vertices[i].Y = d.readFloat64()
12761289
l.vertices[i].Z = d.readFloat64()
12771290
}
1291+
l.index = NewShapeIndex()
12781292
l.originInside = d.readBool()
12791293
l.depth = int(d.readUint32())
12801294
l.bound.decode(d)
12811295
l.subregionBound = ExpandForSubregions(l.bound)
12821296

1283-
l.index = NewShapeIndex()
12841297
l.index.Add(l)
12851298
}
12861299

@@ -1359,6 +1372,7 @@ func (l *Loop) decodeCompressed(d *decoder, snapLevel int) {
13591372
return
13601373
}
13611374

1375+
l.index = NewShapeIndex()
13621376
l.originInside = (properties & originInside) != 0
13631377

13641378
l.depth = int(d.readUvarint())
@@ -1373,7 +1387,6 @@ func (l *Loop) decodeCompressed(d *decoder, snapLevel int) {
13731387
l.initBound()
13741388
}
13751389

1376-
l.index = NewShapeIndex()
13771390
l.index.Add(l)
13781391
}
13791392

s2/polygon.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,12 @@ func (p *Polygon) Invert() {
194194
// The empty and full polygons are handled specially.
195195
if p.IsEmpty() {
196196
*p = *FullPolygon()
197+
p.initLoopProperties()
197198
return
198199
}
199200
if p.IsFull() {
200201
*p = Polygon{}
202+
p.initLoopProperties()
201203
return
202204
}
203205

@@ -244,6 +246,7 @@ func (p *Polygon) Invert() {
244246
newLoops = append(newLoops, l)
245247
}
246248
}
249+
247250
p.loops = newLoops
248251
p.initLoopProperties()
249252
}
@@ -383,6 +386,7 @@ func (p *Polygon) initOneLoop() {
383386

384387
// initLoopProperties sets the properties for polygons with multiple loops.
385388
func (p *Polygon) initLoopProperties() {
389+
p.numVertices = 0
386390
// the loops depths are set by initNested/initOriented prior to this.
387391
p.bound = EmptyRect()
388392
p.hasHoles = false
@@ -402,6 +406,8 @@ func (p *Polygon) initLoopProperties() {
402406
// initEdgesAndIndex performs the shape related initializations and adds the final
403407
// polygon to the index.
404408
func (p *Polygon) initEdgesAndIndex() {
409+
p.numEdges = 0
410+
p.cumulativeEdges = nil
405411
if p.IsFull() {
406412
return
407413
}
@@ -600,13 +606,8 @@ func (p *Polygon) ContainsPoint(point Point) bool {
600606
return inside
601607
}
602608

603-
// Otherwise, look up the ShapeIndex cell containing this point.
604-
it := p.index.Iterator()
605-
if !it.LocatePoint(point) {
606-
return false
607-
}
608-
609-
return p.iteratorContainsPoint(it, point)
609+
// Otherwise we look up the ShapeIndex cell containing this point.
610+
return NewContainsPointQuery(p.index, VertexModelSemiOpen).Contains(point)
610611
}
611612

612613
// ContainsCell reports whether the polygon contains the given cell.

s2/polygon_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,23 @@ func TestPolygonArea(t *testing.T) {
11071107
}
11081108
}
11091109

1110+
func TestPolygonInvert(t *testing.T) {
1111+
origin := PointFromLatLng(LatLngFromDegrees(0, 0))
1112+
pt := PointFromLatLng(LatLngFromDegrees(30, 30))
1113+
p := PolygonFromLoops([]*Loop{
1114+
RegularLoop(origin, 1000/earthRadiusKm, 100),
1115+
})
1116+
1117+
if p.ContainsPoint(pt) {
1118+
t.Errorf("polygon contains point outside itself")
1119+
}
1120+
1121+
p.Invert()
1122+
if !p.ContainsPoint(pt) {
1123+
t.Errorf("inverted polygon does not contain point that is inside itself")
1124+
}
1125+
}
1126+
11101127
// TODO(roberts): Remaining Tests
11111128
// TestInit
11121129
// TestMultipleInit

0 commit comments

Comments
 (0)