Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

108 convex hull #110

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions clusters/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ func (c *Cluster) Append(point space.Point) {
c.PointList = append(c.PointList, point)
}

// ConvexHull returns the convex hull of the clusters PointList
func (c *Cluster) ConvexHull() PointList {
return c.PointList.ConvexHull()
}

// Nearest returns the index of the cluster nearest to point
func (c Clusters) Nearest(point space.Point) int {
var ci int
Expand Down
57 changes: 57 additions & 0 deletions clusters/point_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package clusters

import (
"fmt"
"sort"

"github.com/spatial-go/geoos/planar"
"github.com/spatial-go/geoos/space"
Expand Down Expand Up @@ -51,3 +52,59 @@ func AverageDistance(point space.Point, points PointList) float64 {
}
return d / float64(l)
}

// ConvexHull returns the convex hull of a list of points
// Implementation of Andrew's Monotone Chain algorithm as specified on https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
func (points PointList) ConvexHull() PointList {
// empty slice, single point, and two points are already their own convex hull
if len(points) <= 2 {
return points
}

// sort points by x coordinates (ties stay in original order)
sort.SliceStable(points, func(i, j int) bool {
return points[i].X() < points[j].X() || (points[i].X() == points[j].X() && points[i].Y() < points[j].Y())
})

// build lower hull
lowerHull := PointList{}
for _, p := range points {
for len(lowerHull) >= 2 && cross(lowerHull[len(lowerHull)-2], lowerHull[len(lowerHull)-1], p) <= 0 {
// pop last point
lowerHull = lowerHull[:len(lowerHull)-1]
}
// add p
lowerHull = append(lowerHull, p)
}
// build upper hull
upperHull := PointList{}
// iterate in reverse order
for i := len(points) - 1; i >= 0; i-- {
p := points[i]
for len(upperHull) >= 2 && cross(upperHull[len(upperHull)-2], upperHull[len(upperHull)-1], p) <= 0 {
// pop last point
upperHull = upperHull[:len(upperHull)-1]
}
// add p
upperHull = append(upperHull, p)
}

// concatenate lower and upper hull to build convexHull.
// omit the last point of lowerHull as it is the beginning of upperHull
// keep last point of upperHull to get a closed polygon shape (last point = first point)
hull := append(lowerHull[:len(lowerHull)-1], upperHull...)

if len(hull) == 3 {
// for lines, remove last point (so no closed polygon shape)
hull = hull[:len(hull)-1]
}
return hull
}

// cross is a helper function for ConvexHull()
// cross product of OA and OB vectors.
// returns positive value, if OAB makes a counter-clockwise turn,
// negative for clockwise turn, and zero if the points are colinear.
func cross(o, a, b space.Point) float64 {
return (a.X()-o.X())*(b.Y()-o.Y()) - (a.Y()-o.Y())*(b.X()-o.X())
}
60 changes: 60 additions & 0 deletions clusters/point_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package clusters

import (
"math/rand"
"reflect"
"testing"

"github.com/spatial-go/geoos/space"
)

func TestConvexHull(t *testing.T) {
// empty list
points := PointList{}
expected := PointList{}
testExample(t, points, expected)

// single point
points = PointList{{1, 1}}
expected = PointList{{1, 1}}
testExample(t, points, expected)

// two points
points = PointList{{1, 1}, {1, 2}}
expected = PointList{{1, 1}, {1, 2}}
testExample(t, points, expected)

// line
points = PointList{{1, 1}, {2, 2}, {3, 3}}
expected = PointList{{1, 1}, {3, 3}} // intermediate point omitted
testExample(t, points, expected)

// triangle
points = PointList{{2, 2}, {1, 2}, {1, 1}}
expected = PointList{{1, 1}, {2, 2}, {1, 2}, {1, 1}} // closed polygon shape
testExample(t, points, expected)

// square
points = PointList{{1, 1}, {2, 1}, {2, 2}, {1, 2}}
expected = PointList{{1, 1}, {2, 1}, {2, 2}, {1, 2}, {1, 1}} // closed polygon-shape
testExample(t, points, expected)

// generate random point cloud with set outer bound
size := 50
min, max := 0., 90.
points = make(PointList, size)
for i := 0; i < size; i++ {
points[i] = space.Point{min + rand.Float64()*max, min + rand.Float64()*max}
}
points = append(points, PointList{{min - 1, min - 1}, {min - 1, max + 1}, {max + 1, max + 1}, {max + 1, min - 1}}...) // square outer boundary in clockwise order
expected = PointList{{min - 1, min - 1}, {max + 1, min - 1}, {max + 1, max + 1}, {min - 1, max + 1}, {min - 1, min - 1}} // closed polygon-shape in counter-clockwise order
testExample(t, points, expected)
}

func testExample(t *testing.T, points PointList, expected PointList) {
hull := points.ConvexHull()
if !reflect.DeepEqual(expected, hull) {
t.Errorf("Expected %v but got %v", expected, hull)
t.FailNow()
}
}
Loading