Skip to content

Commit

Permalink
Merge #53673
Browse files Browse the repository at this point in the history
53673: geo: account for duplicate points in IsLinearRingCCW r=sumeerbhola a=otan

Rings can have duplicate adjacent points, so account for this by
scanning potentially more to determing CCW-ness.

Release justification: low risk changes to new functionality

Release note: None

Co-authored-by: Oliver Tan <[email protected]>
  • Loading branch information
craig[bot] and otan committed Sep 1, 2020
2 parents 481772b + 9db90dd commit 9491bf1
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 9 deletions.
41 changes: 32 additions & 9 deletions pkg/geo/geo.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,17 +621,36 @@ func IsLinearRingCCW(linearRing *geom.LinearRing) bool {
}
}

// prevIdx is the previous point. If we are at the 0th point, the last coordinate
// is also the 0th point, so take the second last point.
// Note we don't have to apply this for "nextIdx" as we cap the search above at the
// second last vertex.
// Find the previous point in the ring that is not the same as smallest.
prevIdx := smallestIdx - 1
if smallestIdx == 0 {
prevIdx = linearRing.NumCoords() - 2
if prevIdx < 0 {
prevIdx = linearRing.NumCoords() - 1
}
for prevIdx != smallestIdx {
a := linearRing.Coord(prevIdx)
if a.X() != smallest.X() || a.Y() != smallest.Y() {
break
}
prevIdx--
if prevIdx < 0 {
prevIdx = linearRing.NumCoords() - 1
}
}
// Find the next point in the ring that is not the same as smallest.
nextIdx := smallestIdx + 1
if nextIdx >= linearRing.NumCoords() {
nextIdx = 0
}
for nextIdx != smallestIdx {
c := linearRing.Coord(nextIdx)
if c.X() != smallest.X() || c.Y() != smallest.Y() {
break
}
nextIdx++
if nextIdx >= linearRing.NumCoords() {
nextIdx = 0
}
}
a := linearRing.Coord(prevIdx)
b := smallest
c := linearRing.Coord(smallestIdx + 1)

// We could do the cross product, but we are only interested in the sign.
// To find the sign, reorganize into the orientation matrix:
Expand All @@ -640,6 +659,10 @@ func IsLinearRingCCW(linearRing *geom.LinearRing) bool {
// 1 x_c y_c
// and find the determinant.
// https://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon
a := linearRing.Coord(prevIdx)
b := smallest
c := linearRing.Coord(nextIdx)

areaSign := a.X()*b.Y() - a.Y()*b.X() +
a.Y()*c.X() - a.X()*c.Y() +
b.X()*c.Y() - c.X()*b.Y()
Expand Down
120 changes: 120 additions & 0 deletions pkg/geo/geo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -859,3 +859,123 @@ func TestValidateGeomT(t *testing.T) {
})
}
}

func TestIsLinearRingCCW(t *testing.T) {
testCases := []struct {
desc string
ring *geom.LinearRing
expected bool
}{
{
desc: "flat linear ring",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
0, 0,
0, 0,
0, 0,
0, 0,
}),
expected: false,
},
{
desc: "invalid linear ring with duplicate points at end deemed CW",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
0, 0,
0, 0,
0, 0,
0, 0,
1, 0,
1, 0,
1, 0,
}),
expected: false,
},
{
desc: "invalid linear ring with duplicate points at start deemed CW",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
0, 0,
0, 0,
0, 0,
0, 0,
-1, 1,
-1, 1,
-1, 1,
0, 0,
}),
expected: false,
},

{
desc: "CW linear ring",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
0, 0,
1, 1,
1, 0,
0, 0,
}),
expected: false,
},
{
desc: "CW linear ring, first point is bottom right",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
1, 0,
0, 0,
1, 1,
1, 0,
}),
expected: false,
},
{
desc: "CW linear ring, duplicate points for bottom right",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
0, 0,
1, 1,
1, 0,
1, 0,
1, 0,
0, 0,
}),
expected: false,
},
{
desc: "CCW linear ring",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
0, 0,
1, 0,
1, 1,
0, 0,
}),
expected: true,
},
{
desc: "CCW linear ring, duplicate points for bottom right",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
0, 0,
1, 0,
1, 0,
1, 0,
1, 0,
1, 0,
1, 1,
0, 0,
}),
expected: true,
},
{
desc: "CCW linear ring, first point is bottom right",
ring: geom.NewLinearRingFlat(geom.XY, []float64{
1, 0,
1, 0,
1, 1,
0, 0,
1, 0,
}),
expected: true,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
require.Equal(t, tc.expected, IsLinearRingCCW(tc.ring))
})
}
}

0 comments on commit 9491bf1

Please sign in to comment.