Skip to content
Merged
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
3 changes: 3 additions & 0 deletions windingorder/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package windingorder

const debug = false
102 changes: 91 additions & 11 deletions windingorder/windingorder.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
// Package windingorder provides primitives for determining the winding order of a
// set of points
package windingorder

import (
"github.com/go-spatial/geom"
"log"
)

// WindingOrder is the clockwise direction of a set of points.
type WindingOrder bool
type WindingOrder uint8

const (
Clockwise WindingOrder = false // false is the zero value of bool. We want clockwise to be the default.
CounterClockwise WindingOrder = true

// Clockwise indicates that the winding order is in the clockwise direction
Clockwise WindingOrder = 0
// Colinear indicates that the points are colinear to each other
Colinear WindingOrder = 1
// CounterClockwise indicates that the winding order is in the counter clockwise direction
CounterClockwise WindingOrder = 2

// Collinear alternative spelling of Colinear
Collinear = Colinear
)

// String implements the stringer interface
func (w WindingOrder) String() string {
if w {
switch w {
case Clockwise:
return "clockwise"
case Colinear:
return "colinear"
case CounterClockwise:
return "counter clockwise"
default:
return "unknown"
}
return "clockwise"
}

// IsClockwise checks if winding is clockwise
Expand All @@ -21,18 +43,76 @@ func (w WindingOrder) IsClockwise() bool { return w == Clockwise }
// IsCounterClockwise checks if winding is counter clockwise
func (w WindingOrder) IsCounterClockwise() bool { return w == CounterClockwise }

// Not returns the inverse of the winding
func (w WindingOrder) Not() WindingOrder { return !w }
// IsColinear check if the points are colinear
func (w WindingOrder) IsColinear() bool { return w == Colinear }

// OfPoints returns the winding order of the given points
func OfPoints(pts ...[2]float64) WindingOrder {
// Not returns the inverse of the winding, clockwise <-> counter-clockwise, colinear is it's own
// inverse
func (w WindingOrder) Not() WindingOrder {
switch w {
case Clockwise:
return CounterClockwise
case CounterClockwise:
return Clockwise
default:
return w
}
}

// Orient will take the points and calculate the Orientation of the points. by
// summing the normal vectors. It will return 0 of the given points are colinear
// or 1, or -1 for clockwise and counter clockwise depending on the direction of
// the y axis. If the y axis increase as you go up on the graph then clockwise will
// be -1, otherwise it will be 1; vice versa for counter-clockwise.
func Orient(pts ...[2]float64) int8 {
sum := 0.0
li := len(pts) - 1
if len(pts) < 3 {
return 0
}
for i := range pts[:li] {
sum += (pts[i][0] * pts[i+1][1]) - (pts[i+1][0] * pts[i][1])
}
if sum < 0 {
if debug {
log.Printf("sum: %v", sum)
}
switch {
case sum == 0:
return 0
case sum < 0:
return -1
default:
return 1
}
}

// Orientation returns the clockwise orientation of the set of the points given the
// direction of the positive values of the y axis
func Orientation(yPositiveDown bool, pts ...[2]float64) WindingOrder {
mul := int8(1)
if !yPositiveDown {
mul = -1
}
switch mul * Orient(pts...) {
case 0:
return Colinear
case 1:
return Clockwise
default: // -1
return CounterClockwise
}
return Clockwise
}

// OfPoints returns the winding order of the given points
func OfPoints(pts ...[2]float64) WindingOrder {
return Orientation(true, pts...)
}

// OfGeomPoints is the same as OfPoints, just a convenience to unwrap geom.Point
func OfGeomPoints(points ...geom.Point) WindingOrder {
pts := make([][2]float64, len(points))
for i := range points {
pts[i] = [2]float64(points[i])
}
return OfPoints(pts...)
}
239 changes: 200 additions & 39 deletions windingorder/windingorder_test.go
Original file line number Diff line number Diff line change
@@ -1,66 +1,227 @@
package windingorder

import "testing"
import (
"github.com/go-spatial/geom"
"github.com/go-spatial/geom/encoding/wkt"
"testing"
)

func TestAttributeMethods(t *testing.T) {

fn := func(val WindingOrder, isClockwise bool) {
if val.IsClockwise() != isClockwise {
t.Errorf("is clockwise, expected %v got %v", isClockwise, val.IsClockwise())
}
if val.IsCounterClockwise() == isClockwise {
t.Errorf("is counter clockwise, expected %v got %v", !isClockwise, val.IsClockwise())
}
var cw, ncw = Clockwise, CounterClockwise
if !isClockwise {
cw = CounterClockwise
ncw = Clockwise
}
if val.Not() != ncw {
t.Errorf("not, expected %v got %v", ncw, val.Not())
}
if val.Not().Not() != cw {
t.Errorf("not not, expected %v got %v", cw, val.Not().Not())
}
str := "clockwise"
if !isClockwise {
str = "counter clockwise"
}
if val.String() != str {
t.Errorf("string, expected %v got %v", val.String(), str)
fn := func(val WindingOrder) func(*testing.T) {
return func(t *testing.T) {

var (
// variables based on the type
isClockwise = false
isCounterClockwise = false
isColinear = false
notDir = val
str = "unknown"
)

switch val {
case Clockwise:
isClockwise = true
isCounterClockwise = false
isColinear = false
notDir = CounterClockwise
str = "clockwise"

case CounterClockwise:
isClockwise = false
isCounterClockwise = true
isColinear = false
notDir = Clockwise
str = "counter clockwise"

case Colinear:
isClockwise = false
isCounterClockwise = false
isColinear = true
notDir = Colinear
str = "colinear"

}

if val.IsClockwise() != isClockwise {
t.Errorf("is clockwise, expected %v got %v", isClockwise, val.IsClockwise())
}
if val.IsCounterClockwise() != isCounterClockwise {
t.Errorf("is counter clockwise, expected %v got %v", isCounterClockwise, val.IsCounterClockwise())
}
if val.IsColinear() != isColinear {
t.Errorf("is colinear, expected %v got %v", isColinear, val.IsColinear())
}

if val.Not() != notDir {
t.Errorf("not, expected %v got %v", notDir, val.Not())
}
if val.Not().Not() != val {
t.Errorf("not not, expected %v got %v", val, val.Not().Not())
}
if val.String() != str {
t.Errorf("string, expected %v got %v", val.String(), str)
}
}
}
fn(Clockwise, true)
fn(CounterClockwise, false)
tests := []WindingOrder{Clockwise, CounterClockwise, Colinear, 3}
for i := range tests {
t.Run(tests[i].String(), fn(tests[i]))
}
}

func TestOfPoints(t *testing.T) {
type tcase struct {
Desc string
pts [][2]float64
order WindingOrder
}
fn := func(t *testing.T, tc tcase) {
got := OfPoints(tc.pts...)
if got != tc.order {
t.Errorf("OfPoints, expected %v got %v", tc.order, got)
fn := func(tc tcase) func(*testing.T) {
return func(t *testing.T) {

got := OfPoints(tc.pts...)
if got != tc.order {
t.Errorf("OfPoints, expected %v got %v", tc.order, got)
for i := range tc.pts {
t.Logf("%03v:%v", i, wkt.MustEncode(geom.Point(tc.pts[i])))
}
return
}

points := make([]geom.Point, len(tc.pts))
for i := range tc.pts {
points[i] = geom.Point(tc.pts[i])
}

got = OfGeomPoints(points...)
if got != tc.order {
t.Errorf("OfGeomPoints, expected %v got %v", tc.order, got)
}

// Test with yPostiveDown set to false
got = Orientation(false, tc.pts...)
if got != tc.order.Not() {
t.Errorf("Orientation y-false, expected %v got %v", tc.order.Not(), got)
}

}
}
tests := map[string]tcase{
"simple points": {
tests := [...]tcase{
{
Desc: "simple points",
pts: [][2]float64{{0, 0}, {10, 0}, {10, 10}, {0, 10}},
order: Clockwise,
},
{
Desc: "counter simple points",
pts: [][2]float64{{0, 10}, {10, 10}, {10, 0}, {0, 0}},
order: CounterClockwise,
},
{
pts: [][2]float64{{0, 0}, {10, 0}, {0, 10}},
order: Clockwise,
},
{
pts: [][2]float64{{0, 0}, {1, 0}, {0, 1}},
order: Clockwise,
},
{
pts: [][2]float64{{0, 0}, {0, 10}, {10, 0}},
order: CounterClockwise,
},
{
pts: [][2]float64{{0, 0}, {0, 1}, {1, 0}},
order: CounterClockwise,
},
{
pts: [][2]float64{{10, 0}, {10, 10}, {0, 10}},
order: Clockwise,
},
{
pts: [][2]float64{{0, 10}, {10, 10}, {10, 0}},
order: CounterClockwise,
},
{
Desc: "colinear",
pts: [][2]float64{{0, 0}, {0, 1}, {0, 2}},
order: Colinear, // This is really colinear
},
{
Desc: "colinear",
pts: [][2]float64{{0, 0}, {0, 2}, {0, 1}},
order: Colinear, // This is really colinear
},
{
Desc: "empty",
order: Colinear,
},
{
Desc: "one",
pts: [][2]float64{{0, 0}},
order: Colinear,
},
{
Desc: "two",
pts: [][2]float64{{0, 0}, {0, 1}},
order: Colinear,
},
{
Desc: "3-true",
pts: [][2]float64{{0, 0}, {0, 1}, {0, 2}},
order: Colinear,
},
{
Desc: "3-false",
pts: [][2]float64{{0, 0}, {0, 1}, {1, 2}},
order: CounterClockwise,
},
{
pts: [][2]float64{{0, 0}, {1, 0}, {1, 1}},
order: Clockwise,
},
{
pts: [][2]float64{{204, 694}, {-2511, -3640}, {3462, -3660}},
order: Clockwise,
},
{
pts: [][2]float64{{-2511, -3640}, {204, 694}, {3462, -3660}},
order: CounterClockwise,
},
{
pts: [][2]float64{{204, 694}, {3462, -3660}, {-2511, -3640}},
order: CounterClockwise,
},
{
Desc: "from n america",
pts: [][2]float64{
{0, 0}, {10, 0}, {10, 10}, {0, 10},
{854.210, 1424.142},
{853.491, 1424.329},
{852.395, 1424.635},
},
order: Clockwise,
},
"counter simple points": {
{
Desc: "edge_test initial good",
pts: [][2]float64{
{375, 113},
{372, 114},
{368, 117},
{384, 112},
},
order: CounterClockwise,
},
{
Desc: "edge_test initial good",
pts: [][2]float64{
{0, 10}, {10, 10}, {10, 0}, {0, 0},
{365.513, 116.162},
{366.318, 117.961},
{384.939, 111.896},
},
order: CounterClockwise,
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) { fn(t, tc) })
for i := range tests {
t.Run(tests[i].Desc, fn(tests[i]))
}
}