Merge pull request #376 from rshura/knn_haversine

Use haversine instead of distance in knn if distance is not required.
This commit is contained in:
Josh Baker 2018-11-02 05:00:26 -07:00 committed by GitHub
commit 1a5ab9fb78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 33 deletions

5
Gopkg.lock generated
View File

@ -243,7 +243,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:c5ac96e72d3ff6694602f3273dd71ef04a67c9591465aac92dc1aa8c821b8f91" digest = "1:145703130ac1de36086ab350337777161f9c1d791e81a73659ac1f569e15b5e5"
name = "github.com/tidwall/geojson" name = "github.com/tidwall/geojson"
packages = [ packages = [
".", ".",
@ -251,7 +251,7 @@
"geometry", "geometry",
] ]
pruneopts = "" pruneopts = ""
revision = "32782c39ca84f98113436a297f14601e4fee527d" revision = "dbcb73c57c65ff784ce2ccaad3f062c9787d6f81"
[[projects]] [[projects]]
digest = "1:3ddca2bd5496c6922a2a9e636530e178a43c2a534ea6634211acdc7d10222794" digest = "1:3ddca2bd5496c6922a2a9e636530e178a43c2a534ea6634211acdc7d10222794"
@ -467,6 +467,7 @@
"github.com/tidwall/buntdb", "github.com/tidwall/buntdb",
"github.com/tidwall/evio", "github.com/tidwall/evio",
"github.com/tidwall/geojson", "github.com/tidwall/geojson",
"github.com/tidwall/geojson/geo",
"github.com/tidwall/geojson/geometry", "github.com/tidwall/geojson/geometry",
"github.com/tidwall/gjson", "github.com/tidwall/gjson",
"github.com/tidwall/lotsa", "github.com/tidwall/lotsa",

View File

@ -10,6 +10,7 @@ import (
"github.com/mmcloughlin/geohash" "github.com/mmcloughlin/geohash"
"github.com/tidwall/geojson" "github.com/tidwall/geojson"
"github.com/tidwall/geojson/geo"
"github.com/tidwall/geojson/geometry" "github.com/tidwall/geojson/geometry"
"github.com/tidwall/resp" "github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/bing" "github.com/tidwall/tile38/internal/bing"
@ -371,14 +372,9 @@ func (server *Server) cmdNearby(msg *Message) (res resp.Value, err error) {
if sw.col != nil { if sw.col != nil {
var matched uint32 var matched uint32
iter := func(id string, o geojson.Object, fields []float64, dist *float64) bool { iter := func(id string, o geojson.Object, fields []float64, dist *float64) bool {
// Calculate distance if we need to
distance := 0.0 distance := 0.0
if s.distance { if s.distance {
if dist != nil { distance = geo.DistanceFromHaversine(*dist)
distance = *dist
} else {
distance = o.Distance(s.obj)
}
} }
return sw.writeObject(ScanWriterParams{ return sw.writeObject(ScanWriterParams{
id: id, id: id,
@ -411,6 +407,7 @@ func (server *Server) nearestNeighbors(
iter func(id string, o geojson.Object, fields []float64, dist *float64, iter func(id string, o geojson.Object, fields []float64, dist *float64,
) bool) { ) bool) {
limit := int(sw.cursor + sw.limit) limit := int(sw.cursor + sw.limit)
maxDist := target.Haversine()
var items []iterItem var items []iterItem
sw.col.Nearby(target, func(id string, o geojson.Object, fields []float64) bool { sw.col.Nearby(target, func(id string, o geojson.Object, fields []float64) bool {
if server.hasExpired(s.key, id) { if server.hasExpired(s.key, id) {
@ -423,8 +420,8 @@ func (server *Server) nearestNeighbors(
if !match { if !match {
return true return true
} }
dist := o.Distance(target) dist := target.HaversineTo(o.Center())
if target.Meters() > 0 && dist > target.Meters() { if maxDist > 0 && dist > maxDist {
return false return false
} }
items = append(items, iterItem{id: id, o: o, fields: fields, dist: dist}) items = append(items, iterItem{id: id, o: o, fields: fields, dist: dist})

View File

@ -84,29 +84,37 @@ func (g *Circle) Center() geometry.Point {
return g.center return g.center
} }
// Haversine returns the haversine corresponding to circle's radius
func (g *Circle) Haversine() float64 {
return g.haversine
}
// HaversineTo returns the haversine from a given point to circle's center
func (g *Circle) HaversineTo(p geometry.Point) float64 {
return geo.Haversine(p.Y, p.X, g.center.Y, g.center.X)
}
// Within returns true if circle is contained inside object // Within returns true if circle is contained inside object
func (g *Circle) Within(obj Object) bool { func (g *Circle) Within(obj Object) bool {
return obj.Contains(g) return obj.Contains(g)
} }
func (g *Circle) contains(p geometry.Point, allowOnEdge bool) bool { // containsPoint returns true if circle contains a given point
func (g *Circle) containsPoint(p geometry.Point) bool {
h := geo.Haversine(p.Y, p.X, g.center.Y, g.center.X) h := geo.Haversine(p.Y, p.X, g.center.Y, g.center.X)
if allowOnEdge {
return h <= g.haversine return h <= g.haversine
} }
return h < g.haversine
}
// Contains returns true if the circle contains other object // Contains returns true if the circle contains other object
func (g *Circle) Contains(obj Object) bool { func (g *Circle) Contains(obj Object) bool {
switch other := obj.(type) { switch other := obj.(type) {
case *Point: case *Point:
return g.contains(other.Center(), false) return g.containsPoint(other.Center())
case *Circle: case *Circle:
return other.Distance(g) < (other.meters + g.meters) return other.Distance(g) < (other.meters + g.meters)
case *LineString: case *LineString:
for i := 0; i < other.base.NumPoints(); i++ { for i := 0; i < other.base.NumPoints(); i++ {
if geoDistancePoints(other.base.PointAt(i), g.center) > g.meters { if !g.containsPoint(other.base.PointAt(i)) {
return false return false
} }
} }
@ -124,14 +132,13 @@ func (g *Circle) Contains(obj Object) bool {
} }
} }
// intersectsSegment returns true if the circle intersects a given segment
func (g *Circle) intersectsSegment(seg geometry.Segment) bool { func (g *Circle) intersectsSegment(seg geometry.Segment) bool {
start, end := seg.A, seg.B start, end := seg.A, seg.B
// These are faster checks. If they succeed there's no need do complicate things. // These are faster checks.
if g.contains(start, true) { // If they succeed there's no need do complicate things.
return true if g.containsPoint(start) || g.containsPoint(end) {
}
if g.contains(end, true) {
return true return true
} }
@ -152,14 +159,14 @@ func (g *Circle) intersectsSegment(seg geometry.Segment) bool {
} }
// Distance from the closest point to the center // Distance from the closest point to the center
return g.contains(geometry.Point{X: px, Y: py}, true) return g.containsPoint(geometry.Point{X: px, Y: py})
} }
// Intersects returns true the circle intersects other object // Intersects returns true the circle intersects other object
func (g *Circle) Intersects(obj Object) bool { func (g *Circle) Intersects(obj Object) bool {
switch other := obj.(type) { switch other := obj.(type) {
case *Point: case *Point:
return g.contains(other.Center(), true) return g.containsPoint(other.Center())
case *Circle: case *Circle:
return other.Distance(g) <= (other.meters + g.meters) return other.Distance(g) <= (other.meters + g.meters)
case *LineString: case *LineString:

View File

@ -163,11 +163,11 @@ func TestCircleIntersects(t *testing.T) {
// This snippet tests 100M comparisons. // This snippet tests 100M comparisons.
// On my box this takes 24.5s without haversine trick, and 13.7s with the trick. // On my box this takes 24.5s without haversine trick, and 13.7s with the trick.
// //
//func TestCircle_Performance(t *testing.T) { //func TestCirclePerformance(t *testing.T) {
// g := NewCircle(P(-122.4412, 37.7335), 1000, 64) // g := NewCircle(P(-122.4412, 37.7335), 1000, 64)
// r := rand.New(rand.NewSource(42)) // r := rand.New(rand.NewSource(42))
// for i:= 0; i < 100000000; i++ { // for i:= 0; i < 100000000; i++ {
// g.Contains(PO((r.Float64() - 0.5) * 180, r.Float64() * 90)) // g.Contains(PO(r.Float64()*360 - 180, r.Float64()*180 - 90))
// } // }
// expect(t, true) // expect(t, true)
//} //}

View File

@ -45,11 +45,15 @@ func DistanceToHaversine(meters float64) float64 {
return sin * sin return sin * sin
} }
// DistanceTo return the distance in meteres between two point. // DistanceFromHaversine...
func DistanceFromHaversine(haversine float64) float64 {
return earthRadius * 2 * math.Asin(math.Sqrt(haversine))
}
// DistanceTo return the distance in meters between two point.
func DistanceTo(latA, lonA, latB, lonB float64) (meters float64) { func DistanceTo(latA, lonA, latB, lonB float64) (meters float64) {
a := Haversine(latA, lonA, latB, lonB) a := Haversine(latA, lonA, latB, lonB)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) return DistanceFromHaversine(a)
return earthRadius * c
} }
// DestinationPoint return the destination from a point based on a // DestinationPoint return the destination from a point based on a

View File

@ -77,15 +77,51 @@ func TestHaversine(t *testing.T) {
func TestNormalizeDistance(t *testing.T) { func TestNormalizeDistance(t *testing.T) {
start := time.Now() start := time.Now()
for time.Since(start) < time.Second/4 { for time.Since(start) < time.Second {
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
meters1 := rand.Float64() * 100000000 meters1 := rand.Float64() * earthRadius * 3 // wrap three times
meters2 := NormalizeDistance(meters1) meters2 := NormalizeDistance(meters1)
dist1 := math.Floor(DistanceToHaversine(meters2) * 100000000.0) dist1 := math.Floor(DistanceToHaversine(meters2) * 1e8)
dist2 := math.Floor(DistanceToHaversine(meters1) * 100000000.0) dist2 := math.Floor(DistanceToHaversine(meters1) * 1e8)
if dist1 != dist2 { if dist1 != dist2 {
t.Fatalf("expected %f, got %f", dist2, dist1) t.Fatalf("expected %f, got %f", dist2, dist1)
} }
} }
} }
} }
type point struct {
lat, lon float64
}
func BenchmarkHaversine(b *testing.B) {
pointA := point{
lat: rand.Float64()*180 - 90,
lon: rand.Float64()*360 - 180,
}
points := make([]point, b.N)
for i := 0; i < b.N; i++ {
points[i].lat = rand.Float64()*180 - 90
points[i].lon = rand.Float64()*360 - 180
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Haversine(pointA.lat, pointA.lon, points[i].lat, points[i].lon)
}
}
func BenchmarkDistanceTo(b *testing.B) {
pointA := point{
lat: rand.Float64()*180 - 90,
lon: rand.Float64()*360 - 180,
}
points := make([]point, b.N)
for i := 0; i < b.N; i++ {
points[i].lat = rand.Float64()*180 - 90
points[i].lon = rand.Float64()*360 - 180
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
DistanceTo(pointA.lat, pointA.lon, points[i].lat, points[i].lon)
}
}