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

View File

@ -10,6 +10,7 @@ import (
"github.com/mmcloughlin/geohash"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geo"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/resp"
"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 {
var matched uint32
iter := func(id string, o geojson.Object, fields []float64, dist *float64) bool {
// Calculate distance if we need to
distance := 0.0
if s.distance {
if dist != nil {
distance = *dist
} else {
distance = o.Distance(s.obj)
}
distance = geo.DistanceFromHaversine(*dist)
}
return sw.writeObject(ScanWriterParams{
id: id,
@ -411,6 +407,7 @@ func (server *Server) nearestNeighbors(
iter func(id string, o geojson.Object, fields []float64, dist *float64,
) bool) {
limit := int(sw.cursor + sw.limit)
maxDist := target.Haversine()
var items []iterItem
sw.col.Nearby(target, func(id string, o geojson.Object, fields []float64) bool {
if server.hasExpired(s.key, id) {
@ -423,8 +420,8 @@ func (server *Server) nearestNeighbors(
if !match {
return true
}
dist := o.Distance(target)
if target.Meters() > 0 && dist > target.Meters() {
dist := target.HaversineTo(o.Center())
if maxDist > 0 && dist > maxDist {
return false
}
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
}
// 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
func (g *Circle) Within(obj Object) bool {
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)
if allowOnEdge {
return h <= g.haversine
}
return h < g.haversine
return h <= g.haversine
}
// Contains returns true if the circle contains other object
func (g *Circle) Contains(obj Object) bool {
switch other := obj.(type) {
case *Point:
return g.contains(other.Center(), false)
return g.containsPoint(other.Center())
case *Circle:
return other.Distance(g) < (other.meters + g.meters)
case *LineString:
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
}
}
@ -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 {
start, end := seg.A, seg.B
// These are faster checks. If they succeed there's no need do complicate things.
if g.contains(start, true) {
return true
}
if g.contains(end, true) {
// These are faster checks.
// If they succeed there's no need do complicate things.
if g.containsPoint(start) || g.containsPoint(end) {
return true
}
@ -152,14 +159,14 @@ func (g *Circle) intersectsSegment(seg geometry.Segment) bool {
}
// 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
func (g *Circle) Intersects(obj Object) bool {
switch other := obj.(type) {
case *Point:
return g.contains(other.Center(), true)
return g.containsPoint(other.Center())
case *Circle:
return other.Distance(g) <= (other.meters + g.meters)
case *LineString:

View File

@ -163,11 +163,11 @@ func TestCircleIntersects(t *testing.T) {
// This snippet tests 100M comparisons.
// 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)
// r := rand.New(rand.NewSource(42))
// 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)
//}

View File

@ -45,11 +45,15 @@ func DistanceToHaversine(meters float64) float64 {
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) {
a := Haversine(latA, lonA, latB, lonB)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadius * c
return DistanceFromHaversine(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) {
start := time.Now()
for time.Since(start) < time.Second/4 {
for time.Since(start) < time.Second {
for i := 0; i < 1000; i++ {
meters1 := rand.Float64() * 100000000
meters1 := rand.Float64() * earthRadius * 3 // wrap three times
meters2 := NormalizeDistance(meters1)
dist1 := math.Floor(DistanceToHaversine(meters2) * 100000000.0)
dist2 := math.Floor(DistanceToHaversine(meters1) * 100000000.0)
dist1 := math.Floor(DistanceToHaversine(meters2) * 1e8)
dist2 := math.Floor(DistanceToHaversine(meters1) * 1e8)
if dist1 != dist2 {
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)
}
}