From 7af396565448faa0649c1b8469153695c9b98e1f Mon Sep 17 00:00:00 2001 From: Daniel James Date: Wed, 3 Nov 2021 13:48:01 -0700 Subject: [PATCH 1/3] Add function to calculate a new point from a given point, bearing, and distance Based on the implementation described at http://www.movable-type.co.uk/scripts/latlong.html#dest-point --- geo/distance.go | 19 +++++++++++++++++++ geo/distance_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/geo/distance.go b/geo/distance.go index 44d81b5..3c582eb 100644 --- a/geo/distance.go +++ b/geo/distance.go @@ -69,3 +69,22 @@ func Midpoint(p, p2 orb.Point) orb.Point { return r } + +// PointAtBearingAndDistance returns the point at the given bearing and distance in meters from the point +func PointAtBearingAndDistance(p orb.Point, bearing, distance float64) orb.Point { + aLat := deg2rad(p[1]) + aLon := deg2rad(p[0]) + + bearingRadians := deg2rad(bearing) + + distanceRatio := distance / orb.EarthRadius + bLat := math.Asin(math.Sin(aLat)*math.Cos(distanceRatio) + math.Cos(aLat)*math.Sin(distanceRatio)*math.Cos(bearingRadians)) + bLon := aLon + math.Atan2(math.Sin(bearingRadians)*math.Sin(distanceRatio)*math.Cos(aLat), math.Cos(distanceRatio)-math.Sin(aLat)*math.Sin(bLat)) + + r := orb.Point{ + rad2deg(bLon), + rad2deg(bLat), + } + + return r +} diff --git a/geo/distance_test.go b/geo/distance_test.go index af82a4b..8657eef 100644 --- a/geo/distance_test.go +++ b/geo/distance_test.go @@ -89,3 +89,28 @@ func TestMidpoint(t *testing.T) { t.Errorf("expected %v, got %v", answer, m) } } + +func TestPointAtBearingAndDistance(t *testing.T) { + answer := orb.Point{-0.841153, 52.68179432} + bearing := 127.373 + distance := 85194.89 + p := PointAtBearingAndDistance(orb.Point{-1.8444, 53.1506}, bearing, distance) + + if d := DistanceHaversine(p, answer); d > 1 { + t.Errorf("expected %v, got %v (%vm away)", answer, p, d) + } +} +func TestMidpointAgainstPointAtBearingAndDistance(t *testing.T) { + a := orb.Point{-1.8444, 53.1506} + b := orb.Point{0.1406, 52.2047} + bearing := Bearing(a, b) + distance := DistanceHaversine(a, b) + acceptableTolerance := 1e-06 // unit is meters + + p1 := PointAtBearingAndDistance(a, bearing, distance/2) + p2 := Midpoint(a, b) + + if d := DistanceHaversine(p1, p2); d > acceptableTolerance { + t.Errorf("expected %v to be within %vm of %v", p1, acceptableTolerance, p2) + } +} From 03854096891d3f842b673261ee4a8ad1aeefaad4 Mon Sep 17 00:00:00 2001 From: Daniel James Date: Wed, 3 Nov 2021 14:52:40 -0700 Subject: [PATCH 2/3] Add function to calculate a point at a given distance along a line --- geo/distance.go | 26 +++++++++++++++++ geo/distance_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/geo/distance.go b/geo/distance.go index 3c582eb..9d22d4d 100644 --- a/geo/distance.go +++ b/geo/distance.go @@ -88,3 +88,29 @@ func PointAtBearingAndDistance(p orb.Point, bearing, distance float64) orb.Point return r } + +func PointAtDistanceAlongLine(ls orb.LineString, distance float64) orb.Point { + if len(ls) == 0 { + panic("empty LineString") + } + + if distance < 0 || len(ls) == 1 { + return ls[0] + } + + travelled := 0.0 + i := 1 + for ; i < len(ls); i++ { + a := ls[i-1] + b := ls[i] + actualSegmentDistance := DistanceHaversine(a, b) + expectedSegmentDistance := distance - travelled + if expectedSegmentDistance < actualSegmentDistance { + bearing := Bearing(a, b) + return PointAtBearingAndDistance(a, bearing, expectedSegmentDistance) + } + travelled += actualSegmentDistance + } + + return ls[i-1] +} diff --git a/geo/distance_test.go b/geo/distance_test.go index 8657eef..8327d6a 100644 --- a/geo/distance_test.go +++ b/geo/distance_test.go @@ -114,3 +114,69 @@ func TestMidpointAgainstPointAtBearingAndDistance(t *testing.T) { t.Errorf("expected %v to be within %vm of %v", p1, acceptableTolerance, p2) } } + +func TestPointAtDistanceAlongLineWithEmptyLineString(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("PointAtDistanceAlongLine did not panic") + } + }() + + line := orb.LineString{} + PointAtDistanceAlongLine(line, 90000) +} + +func TestPointAtDistanceAlongLineWithSinglePoint(t *testing.T) { + expected := orb.Point{-1.8444, 53.1506} + line := orb.LineString{ + expected, + } + actual := PointAtDistanceAlongLine(line, 90000) + + if actual != expected { + t.Errorf("expected %v but got %v", expected, actual) + } +} + +func TestPointAtDistanceAlongLineWithMinimalPoints(t *testing.T) { + expected := orb.Point{-0.841153, 52.68179432} + acceptableTolerance := 1.0 // unit is meters + line := orb.LineString{ + orb.Point{-1.8444, 53.1506}, + orb.Point{0.1406, 52.2047}, + } + actual := PointAtDistanceAlongLine(line, 85194.89) + + if d := DistanceHaversine(expected, actual); d > acceptableTolerance { + t.Errorf("expected %v to be within %vm of %v (%vm away)", expected, acceptableTolerance, actual, d) + } +} + +func TestPointAtDistanceAlongLineWithMultiplePoints(t *testing.T) { + expected := orb.Point{-0.78526, 52.65506} + acceptableTolerance := 1.0 // unit is meters + line := orb.LineString{ + orb.Point{-1.8444, 53.1506}, + orb.Point{-0.8411, 52.6817}, + orb.Point{0.1406, 52.2047}, + } + actual := PointAtDistanceAlongLine(line, 90000) + + if d := DistanceHaversine(expected, actual); d > acceptableTolerance { + t.Errorf("expected %v to be within %vm of %v (%vm away)", expected, acceptableTolerance, actual, d) + } +} + +func TestPointAtDistanceAlongLinePastEndOfLine(t *testing.T) { + expected := orb.Point{0.1406, 52.2047} + line := orb.LineString{ + orb.Point{-1.8444, 53.1506}, + orb.Point{-0.8411, 52.6817}, + expected, + } + actual := PointAtDistanceAlongLine(line, 200000) + + if actual != expected { + t.Errorf("expected %v but got %v", expected, actual) + } +} From 1b88c0eb9a33377347aecfcb1dc50a21b17146a3 Mon Sep 17 00:00:00 2001 From: Daniel James Date: Wed, 3 Nov 2021 16:29:32 -0700 Subject: [PATCH 3/3] Update PointAtDistanceAlongLine to return bearing --- geo/distance.go | 18 +++++++------- geo/distance_test.go | 56 +++++++++++++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/geo/distance.go b/geo/distance.go index 9d22d4d..a04aa9b 100644 --- a/geo/distance.go +++ b/geo/distance.go @@ -89,28 +89,30 @@ func PointAtBearingAndDistance(p orb.Point, bearing, distance float64) orb.Point return r } -func PointAtDistanceAlongLine(ls orb.LineString, distance float64) orb.Point { +func PointAtDistanceAlongLine(ls orb.LineString, distance float64) (orb.Point, float64) { if len(ls) == 0 { panic("empty LineString") } if distance < 0 || len(ls) == 1 { - return ls[0] + return ls[0], 0.0 } travelled := 0.0 i := 1 + var from, to orb.Point for ; i < len(ls); i++ { - a := ls[i-1] - b := ls[i] - actualSegmentDistance := DistanceHaversine(a, b) + from = ls[i-1] + to = ls[i] + actualSegmentDistance := DistanceHaversine(from, to) expectedSegmentDistance := distance - travelled if expectedSegmentDistance < actualSegmentDistance { - bearing := Bearing(a, b) - return PointAtBearingAndDistance(a, bearing, expectedSegmentDistance) + bearing := Bearing(from, to) + return PointAtBearingAndDistance(from, bearing, expectedSegmentDistance), bearing } travelled += actualSegmentDistance } - return ls[i-1] + bearing := Bearing(from, to) + return to, bearing } diff --git a/geo/distance_test.go b/geo/distance_test.go index 8327d6a..f13933e 100644 --- a/geo/distance_test.go +++ b/geo/distance_test.go @@ -91,13 +91,13 @@ func TestMidpoint(t *testing.T) { } func TestPointAtBearingAndDistance(t *testing.T) { - answer := orb.Point{-0.841153, 52.68179432} + expected := orb.Point{-0.841153, 52.68179432} bearing := 127.373 distance := 85194.89 - p := PointAtBearingAndDistance(orb.Point{-1.8444, 53.1506}, bearing, distance) + actual := PointAtBearingAndDistance(orb.Point{-1.8444, 53.1506}, bearing, distance) - if d := DistanceHaversine(p, answer); d > 1 { - t.Errorf("expected %v, got %v (%vm away)", answer, p, d) + if d := DistanceHaversine(actual, expected); d > 1 { + t.Errorf("expected %v, got %v (%vm away)", expected, actual, d) } } func TestMidpointAgainstPointAtBearingAndDistance(t *testing.T) { @@ -127,28 +127,36 @@ func TestPointAtDistanceAlongLineWithEmptyLineString(t *testing.T) { } func TestPointAtDistanceAlongLineWithSinglePoint(t *testing.T) { - expected := orb.Point{-1.8444, 53.1506} + expectedPoint := orb.Point{-1.8444, 53.1506} line := orb.LineString{ - expected, + expectedPoint, } - actual := PointAtDistanceAlongLine(line, 90000) + actualPoint, actualBearing := PointAtDistanceAlongLine(line, 90000) - if actual != expected { - t.Errorf("expected %v but got %v", expected, actual) + if actualPoint != expectedPoint { + t.Errorf("expected %v but got %v", expectedPoint, actualPoint) + } + if actualBearing != 0.0 { + t.Errorf("expected %v but got %v", actualBearing, 0.0) } } func TestPointAtDistanceAlongLineWithMinimalPoints(t *testing.T) { expected := orb.Point{-0.841153, 52.68179432} - acceptableTolerance := 1.0 // unit is meters + acceptableDistanceTolerance := 1.0 // unit is meters line := orb.LineString{ orb.Point{-1.8444, 53.1506}, orb.Point{0.1406, 52.2047}, } - actual := PointAtDistanceAlongLine(line, 85194.89) + acceptableBearingTolerance := 0.01 // unit is degrees + expectedBearing := Bearing(line[0], line[1]) + actual, actualBearing := PointAtDistanceAlongLine(line, 85194.89) - if d := DistanceHaversine(expected, actual); d > acceptableTolerance { - t.Errorf("expected %v to be within %vm of %v (%vm away)", expected, acceptableTolerance, actual, d) + if d := DistanceHaversine(expected, actual); d > acceptableDistanceTolerance { + t.Errorf("expected %v to be within %vm of %v (%vm away)", actual, acceptableDistanceTolerance, expected, d) + } + if b := math.Abs(actualBearing - expectedBearing); b > acceptableBearingTolerance { + t.Errorf("expected bearing %v to be within %v degrees of %v", actualBearing, acceptableBearingTolerance, expectedBearing) } } @@ -160,10 +168,15 @@ func TestPointAtDistanceAlongLineWithMultiplePoints(t *testing.T) { orb.Point{-0.8411, 52.6817}, orb.Point{0.1406, 52.2047}, } - actual := PointAtDistanceAlongLine(line, 90000) + acceptableBearingTolerance := 0.01 // unit is degrees + expectedBearing := Bearing(line[1], line[2]) + actualPoint, actualBearing := PointAtDistanceAlongLine(line, 90000) - if d := DistanceHaversine(expected, actual); d > acceptableTolerance { - t.Errorf("expected %v to be within %vm of %v (%vm away)", expected, acceptableTolerance, actual, d) + if d := DistanceHaversine(expected, actualPoint); d > acceptableTolerance { + t.Errorf("expected %v to be within %vm of %v (%vm away)", expected, acceptableTolerance, actualPoint, d) + } + if b := math.Abs(actualBearing - expectedBearing); b > acceptableBearingTolerance { + t.Errorf("expected bearing %v to be within %v degrees of %v", actualBearing, acceptableBearingTolerance, expectedBearing) } } @@ -174,9 +187,14 @@ func TestPointAtDistanceAlongLinePastEndOfLine(t *testing.T) { orb.Point{-0.8411, 52.6817}, expected, } - actual := PointAtDistanceAlongLine(line, 200000) + acceptableBearingTolerance := 0.01 // unit is degrees + expectedBearing := Bearing(line[1], line[2]) + actualPoint, actualBearing := PointAtDistanceAlongLine(line, 200000) - if actual != expected { - t.Errorf("expected %v but got %v", expected, actual) + if actualPoint != expected { + t.Errorf("expected %v but got %v", expected, actualPoint) + } + if b := math.Abs(actualBearing - expectedBearing); b > acceptableBearingTolerance { + t.Errorf("expected bearing %v to be within %v degrees of %v", actualBearing, acceptableBearingTolerance, expectedBearing) } }