Skip to content

Commit 3089048

Browse files
committed
First pack of modules: point definition and distance calculation functions.
1 parent 3881894 commit 3089048

6 files changed

+227
-2
lines changed

Makefile

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
all: test
2+
3+
test:
4+
go test -v ./... -gocheck.v
5+
6+
bench:
7+
go test -v -bench=. ./... -gocheck.b

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
# go-point-clustering
2-
(Lat, lon) points fast clustering using DBScan algorithm
1+
# Point Clutering
2+
3+
(Lat, lon) points fast clustering using DBScan algorithm in Golang.
4+
5+

distance.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package cluster
2+
3+
import (
4+
"math"
5+
)
6+
7+
const (
8+
// Translating from degrees to radians
9+
DegreeRad = math.Pi / 180.0
10+
// Earth radius
11+
EarthR = 6371.0
12+
)
13+
14+
// DistanceSheprical is a spherical (optimized) distance between two points
15+
//
16+
// Result is distance in kilometers
17+
func DistanceSpherical(p1, p2 *Point) float64 {
18+
v1 := (p1[1] - p2[1]) * DegreeRad
19+
v1 = v1 * v1
20+
21+
v2 := (p1[0] - p2[0]) * DegreeRad * math.Cos((p1[1]+p2[1])/2.0*DegreeRad)
22+
v2 = v2 * v2
23+
24+
return EarthR * math.Sqrt(v1+v2)
25+
}
26+
27+
// FastSine caclulates sinus approximated to parabola
28+
//
29+
// Taken from: http://forum.devmaster.net/t/fast-and-accurate-sine-cosine/9648
30+
func FastSine(x float64) float64 {
31+
const (
32+
B = 4 / math.Pi
33+
C = -4 / (math.Pi * math.Pi)
34+
P = 0.225
35+
)
36+
37+
if x > math.Pi || x < -math.Pi {
38+
panic("out of range")
39+
}
40+
41+
y := B*x + C*x*math.Abs(x)
42+
return P*(y*math.Abs(y)-y) + y
43+
}
44+
45+
// FastCos calculates cosinus from sinus
46+
func FastCos(x float64) float64 {
47+
x += math.Pi / 2.0
48+
if x > math.Pi {
49+
x -= 2 * math.Pi
50+
}
51+
52+
return FastSine(x)
53+
}
54+
55+
// Spherical distance with fast cosine and w/o sqrt and
56+
// normalization to Earth radius/radians
57+
//
58+
// To get real distance in km, take sqrt and multiply result by EarthR*DegreeRad
59+
//
60+
// In this library eps (distance) is adjusted so that we don't need
61+
// to do sqrt and multiplication
62+
func DistanceSphericalFast(p1, p2 *Point) float64 {
63+
v1 := (p1[1] - p2[1])
64+
v2 := (p1[0] - p2[0]) * FastCos((p1[1]+p2[1])/2.0*DegreeRad)
65+
66+
return v1*v1 + v2*v2
67+
}

distance_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cluster
2+
3+
import (
4+
. "gopkg.in/check.v1"
5+
"math"
6+
)
7+
8+
type DistanceSuite struct {
9+
p1, p2 Point
10+
}
11+
12+
var _ = Suite(&DistanceSuite{})
13+
14+
func (s *DistanceSuite) SetUpTest(c *C) {
15+
s.p1 = Point{30.244759, 59.955982}
16+
s.p2 = Point{30.24472, 59.955975}
17+
}
18+
19+
func (s *DistanceSuite) TestFastCos(c *C) {
20+
c.Check(FastCos(0), Equals, math.Cos(0))
21+
c.Check(math.Abs(FastCos(0.1)-math.Cos(0.1)) < 0.001, Equals, true)
22+
c.Check(math.Abs(FastCos(-0.1)-math.Cos(-0.1)) < 0.001, Equals, true)
23+
c.Check(math.Abs(FastCos(1.0)-math.Cos(1.0)) < 0.001, Equals, true)
24+
}
25+
26+
func (s *DistanceSuite) TestDistanceSpherical(c *C) {
27+
c.Check(DistanceSpherical(&s.p1, &s.p2), Equals, 0.0023064907653812116)
28+
c.Check(DistanceSpherical(&s.p2, &s.p1), Equals, 0.0023064907653812116)
29+
c.Check(DistanceSpherical(&s.p1, &s.p1), Equals, 0.0)
30+
c.Check(DistanceSpherical(&s.p2, &s.p2), Equals, 0.0)
31+
}
32+
33+
func (s *DistanceSuite) TestDistanceSphericalFast(c *C) {
34+
c.Check(DistanceSphericalFast(&s.p1, &s.p2), Equals, 4.3026720164084415e-10)
35+
c.Check(DistanceSphericalFast(&s.p2, &s.p1), Equals, 4.3026720164084415e-10)
36+
c.Check(DistanceSphericalFast(&s.p1, &s.p1), Equals, 0.0)
37+
c.Check(DistanceSphericalFast(&s.p2, &s.p2), Equals, 0.0)
38+
39+
c.Check(math.Abs(math.Sqrt(DistanceSphericalFast(&s.p1, &s.p2))*DegreeRad*EarthR-
40+
DistanceSpherical(&s.p1, &s.p2)) < 0.000001, Equals, true)
41+
}
42+
43+
func (s *DistanceSuite) BenchmarkDistanceSpherical(c *C) {
44+
for i := 0; i < c.N; i++ {
45+
DistanceSpherical(&s.p1, &s.p2)
46+
}
47+
}
48+
49+
func (s *DistanceSuite) BenchmarkDistanceSphericalFast(c *C) {
50+
for i := 0; i < c.N; i++ {
51+
DistanceSphericalFast(&s.p1, &s.p2)
52+
}
53+
}

point.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Package cluster implements DBScan clustering on (lat, lon) using K-D Tree
2+
package cluster
3+
4+
// Point is longitue, latittude
5+
type Point [2]float64
6+
7+
// PointList is a slice of Points
8+
type PointList []Point
9+
10+
// Cluster is a result of DBScan work
11+
type Cluster struct {
12+
C int
13+
Points []int
14+
}
15+
16+
// sqDist returns squared (w/o sqrt & normalization) distance between two points
17+
func (a *Point) sqDist(b *Point) float64 {
18+
return DistanceSphericalFast(a, b)
19+
}
20+
21+
// LessEq - a <= b
22+
func (a *Point) LessEq(b *Point) bool {
23+
return a[0] <= b[0] && a[1] <= b[1]
24+
}
25+
26+
// GreaterEq - a >= b
27+
func (a *Point) GreaterEq(b *Point) bool {
28+
return a[0] >= b[0] && a[1] >= b[1]
29+
}
30+
31+
// CentroidAndBounds calculates center and cluster bounds
32+
func (c *Cluster) CentroidAndBounds(points PointList) (center, min, max Point) {
33+
if len(c.Points) == 0 {
34+
panic("empty cluster")
35+
}
36+
37+
min = Point{180.0, 90.0}
38+
max = Point{-180.0, -90.0}
39+
40+
for _, i := range c.Points {
41+
pt := points[i]
42+
43+
for j := range pt {
44+
center[j] += pt[j]
45+
46+
if pt[j] < min[j] {
47+
min[j] = pt[j]
48+
}
49+
if pt[j] > max[j] {
50+
max[j] = pt[j]
51+
}
52+
}
53+
}
54+
55+
for j := range center {
56+
center[j] /= float64(len(c.Points))
57+
}
58+
59+
return
60+
}
61+
62+
// Inside checks if (innerMin, innerMax) rectangle is inside (outerMin, outMax) rectangle
63+
func Inside(innerMin, innerMax, outerMin, outerMax *Point) bool {
64+
return innerMin.GreaterEq(outerMin) && innerMax.LessEq(outerMax)
65+
}

point_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package cluster
2+
3+
import (
4+
. "gopkg.in/check.v1"
5+
"testing"
6+
)
7+
8+
// Launch gocheck tests
9+
func Test(t *testing.T) {
10+
TestingT(t)
11+
}
12+
13+
type PointSuite struct {
14+
points PointList
15+
}
16+
17+
var _ = Suite(&PointSuite{})
18+
19+
func (s *PointSuite) SetUpTest(c *C) {
20+
s.points = PointList{Point{30.244759, 59.955982}, Point{30.24472, 59.955975}, Point{30.244358, 59.96698}}
21+
}
22+
23+
func (s *PointSuite) TestCentroidAndBounds(c *C) {
24+
c1 := Cluster{C: 0, Points: []int{0, 1, 2}}
25+
26+
center, min, max := c1.CentroidAndBounds(s.points)
27+
c.Check(center, DeepEquals, Point{30.244612333333333, 59.95964566666667})
28+
c.Check(min, DeepEquals, Point{30.244358, 59.955975})
29+
c.Check(max, DeepEquals, Point{30.244759, 59.96698})
30+
}

0 commit comments

Comments
 (0)