diff --git a/windingorder/debug.go b/windingorder/debug.go new file mode 100644 index 00000000..16af6b81 --- /dev/null +++ b/windingorder/debug.go @@ -0,0 +1,3 @@ +package windingorder + +const debug = false diff --git a/windingorder/windingorder.go b/windingorder/windingorder.go index d1e9322a..f00e2113 100644 --- a/windingorder/windingorder.go +++ b/windingorder/windingorder.go @@ -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 @@ -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...) } diff --git a/windingorder/windingorder_test.go b/windingorder/windingorder_test.go index 020194ea..7e61e0b2 100644 --- a/windingorder/windingorder_test.go +++ b/windingorder/windingorder_test.go @@ -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])) } }