Repository: dharmab/skyeye
Pull request #151 (2024-08-08 06:19:04)

Fix aspect from angle function
Fix an issue where aspects were often incorrectly computed.


Patch:
diff --git a/pkg/bearings/bearing.go b/pkg/bearings/bearing.go
index ad8a274..b8248fc 100644
--- a/pkg/bearings/bearing.go
+++ b/pkg/bearings/bearing.go
@@ -19,6 +19,8 @@ type Bearing interface {
 	True(declination unit.Angle) Bearing
 	// Magnetic bearing conversion computed with the given declination.
 	Magnetic(declination unit.Angle) Bearing
+	// Reciprocal bearing.
+	Reciprocal() Bearing
 	// IsTrue returns true if the bearing is a true bearing.
 	IsTrue() bool
 	// IsMagnetic returns true if the bearing is a magnetic bearing.
diff --git a/pkg/bearings/magnetic.go b/pkg/bearings/magnetic.go
index 1e4361d..f682f73 100644
--- a/pkg/bearings/magnetic.go
+++ b/pkg/bearings/magnetic.go
@@ -6,18 +6,18 @@ import (
 	"github.com/martinlindhe/unit"
 )
 
-// Magnetic is a magnetic bearing.
+// Magnetic is a magnetic bearing, pointing at the magnetic north pole.
 type Magnetic struct {
 	θ unit.Angle
 }
 
+var _ Bearing = &Magnetic{}
+
 // NewMagneticBearing creates a new magnetic bearing from the given value.
 func NewMagneticBearing(value unit.Angle) *Magnetic {
 	return &Magnetic{θ: normalize(value)}
 }
 
-var _ Bearing = &Magnetic{}
-
 // Value returns the magnetic bearing value.
 func (b *Magnetic) Value() unit.Angle {
 	return normalize(b.θ)
@@ -33,7 +33,7 @@ func (b *Magnetic) Degrees() float64 {
 	return b.Value().Degrees()
 }
 
-// RoundedDegrees returns the magnetic bearing rounded to the nearest degree.
+// RoundedDegrees returns the magnetic bearing in degrees, rounded to the nearest degree.
 func (b *Magnetic) RoundedDegrees() float64 {
 	return math.Round(b.Degrees())
 }
@@ -48,6 +48,11 @@ func (b *Magnetic) Magnetic(declination unit.Angle) Bearing {
 	return b
 }
 
+// Reciprocal returns a magnetic reciprocal.
+func (b *Magnetic) Reciprocal() Bearing {
+	return NewMagneticBearing(b.Value() + 180*unit.Degree)
+}
+
 // IsTrue returns false for a magnetic bearing.
 func (b *Magnetic) IsTrue() bool {
 	return false
diff --git a/pkg/bearings/true.go b/pkg/bearings/true.go
index 36fb509..21807b9 100644
--- a/pkg/bearings/true.go
+++ b/pkg/bearings/true.go
@@ -6,48 +6,64 @@ import (
 	"github.com/martinlindhe/unit"
 )
 
+// True is a bearing pointing to geographic north.
 type True struct {
 	θ unit.Angle
 }
 
 var _ Bearing = True{}
 
+// NewTrueBearing creates a new bearing from the given value.
 func NewTrueBearing(value unit.Angle) True {
 	return True{θ: normalize(value)}
 }
 
+// Value returns the bearing value.
 func (b True) Value() unit.Angle {
 	return normalize(b.θ)
 }
 
+// Rounded returns the bearing rounded to the nearest degree.
 func (b True) Rounded() unit.Angle {
 	return unit.Angle(b.RoundedDegrees()) * unit.Degree
 }
 
+// Degrees returns the bearing in degrees.
 func (b True) Degrees() float64 {
 	return b.Value().Degrees()
 }
 
+// RoundedDegrees returns the bearing in degrees, rounded to the nearest degree.
 func (b True) RoundedDegrees() float64 {
 	return math.Round(b.Degrees())
 }
 
+// True returns this bearing.
 func (b True) True(declination unit.Angle) Bearing {
 	return b
 }
 
+// Magnetic converts this true bearing to a magnetic bearing by subtracting the given declination.
 func (b True) Magnetic(declination unit.Angle) Bearing {
 	return NewMagneticBearing(b.Value() - declination)
 }
 
+// Reciprocal returns a reciprocal true bearing.
+func (b True) Reciprocal() Bearing {
+	return NewTrueBearing(b.Value() + 180*unit.Degree)
+}
+
+// IsTrue returns true for a true bearing.
 func (b True) IsTrue() bool {
 	return true
 }
 
+// IsMagnetic returns false for a true bearing.
 func (b True) IsMagnetic() bool {
 	return false
 }
 
+// String implements [Bearing.String]
 func (b True) String() string {
 	return toString(b)
 }
diff --git a/pkg/brevity/aspect.go b/pkg/brevity/aspect.go
index 645c1bc..315f425 100644
--- a/pkg/brevity/aspect.go
+++ b/pkg/brevity/aspect.go
@@ -28,23 +28,29 @@ func AspectFromAngle(bearing bearings.Bearing, track bearings.Bearing) Aspect {
 		log.Warn().Any("bearing", bearing).Any("track", track).Msg("bearing and track provided to AspectFromAngle should be magnetic")
 	}
 
-	var targetAspect unit.Angle
-	if track.Value() > bearing.Value() {
-		targetAspect = track.Value() - bearing.Magnetic(0).Value()
+	var θ float64
+	if bearing.Reciprocal().Value() > track.Value() {
+		θ = bearing.Reciprocal().Degrees() - track.Degrees()
 	} else {
-		targetAspect = bearing.Value() - track.Value()
+		θ = track.Reciprocal().Degrees() - bearing.Degrees()
 	}
+	θ = bearings.NewMagneticBearing(unit.Angle(θ) * unit.Degree).Degrees()
 
-	θ := targetAspect.Degrees()
 	switch {
-	case θ <= 30:
+	case 0 <= θ && θ <= 35:
 		return Hot
-	case θ <= 70:
+	case 35 < θ && θ <= 75:
 		return Flank
-	case θ <= 110:
+	case 75 < θ && θ <= 115:
 		return Beam
-	case θ <= 180:
+	case 115 < θ && θ <= 245:
 		return Drag
+	case 245 < θ && θ <= 285:
+		return Beam
+	case 285 < θ && θ <= 325:
+		return Flank
+	case 325 < θ && θ <= 360:
+		return Hot
 	default:
 		return UnknownAspect
 	}


Test patch:
diff --git a/pkg/brevity/aspect_test.go b/pkg/brevity/aspect_test.go
new file mode 100644
index 0000000..e9081eb
--- /dev/null
+++ b/pkg/brevity/aspect_test.go
@@ -0,0 +1,413 @@
+package brevity
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/dharmab/skyeye/pkg/bearings"
+	"github.com/martinlindhe/unit"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAspectFromAngle(t *testing.T) {
+	n := bearings.NewMagneticBearing(0 * unit.Degree)
+	nne := bearings.NewMagneticBearing(22.5 * unit.Degree)
+	ne := bearings.NewMagneticBearing(45 * unit.Degree)
+	ene := bearings.NewMagneticBearing(67.5 * unit.Degree)
+	e := bearings.NewMagneticBearing(90 * unit.Degree)
+	ese := bearings.NewMagneticBearing(112.5 * unit.Degree)
+	se := bearings.NewMagneticBearing(135 * unit.Degree)
+	sse := bearings.NewMagneticBearing(157.5 * unit.Degree)
+	s := bearings.NewMagneticBearing(180 * unit.Degree)
+	ssw := bearings.NewMagneticBearing(202.5 * unit.Degree)
+	sw := bearings.NewMagneticBearing(225 * unit.Degree)
+	wsw := bearings.NewMagneticBearing(247.5 * unit.Degree)
+	w := bearings.NewMagneticBearing(270 * unit.Degree)
+	wnw := bearings.NewMagneticBearing(292.5 * unit.Degree)
+	nw := bearings.NewMagneticBearing(315 * unit.Degree)
+	nnw := bearings.NewMagneticBearing(337.5 * unit.Degree)
+
+	testCases := []struct {
+		bearing  bearings.Bearing
+		track    bearings.Bearing
+		expected Aspect
+	}{
+		// ⇧
+		// ⬆
+		{
+			bearing:  n,
+			track:    n,
+			expected: Drag,
+		},
+		// ⬀
+		// ⬆
+		{
+			bearing:  n,
+			track:    nne,
+			expected: Drag,
+		},
+		{
+			bearing:  n,
+			track:    ne,
+			expected: Drag,
+		},
+		{
+			bearing:  n,
+			track:    ene,
+			expected: Beam,
+		},
+		// ⇨
+		// ⬆
+		{
+			bearing:  n,
+			track:    e,
+			expected: Beam,
+		},
+		// ⬂
+		// ⬆
+		{
+			bearing:  n,
+			track:    ese,
+			expected: Flank,
+		},
+		{
+			bearing:  n,
+			track:    se,
+			expected: Flank,
+		},
+		{
+			bearing:  n,
+			track:    sse,
+			expected: Hot,
+		},
+		// ⇩
+		// ⬆
+		{
+			bearing:  n,
+			track:    s,
+			expected: Hot,
+		},
+		// ⬃
+		// ⬆
+		{
+			bearing:  n,
+			track:    ssw,
+			expected: Hot,
+		},
+		{
+			bearing:  n,
+			track:    sw,
+			expected: Flank,
+		},
+		{
+			bearing:  n,
+			track:    wsw,
+			expected: Flank,
+		},
+		// ⇦
+		// ⬆
+		{
+			bearing:  n,
+			track:    w,
+			expected: Beam,
+		},
+		// ⬁
+		// ⬆
+		{
+			bearing:  n,
+			track:    wnw,
+			expected: Beam,
+		},
+		{
+			bearing:  n,
+			track:    nw,
+			expected: Drag,
+		},
+		{
+			bearing:  n,
+			track:    nnw,
+			expected: Drag,
+		},
+		// ⮕⇧
+		{
+			bearing:  e,
+			track:    n,
+			expected: Beam,
+		},
+		// ⮕⬀
+		{
+			bearing:  e,
+			track:    nne,
+			expected: Beam,
+		},
+		{
+			bearing:  e,
+			track:    ne,
+			expected: Drag,
+		},
+
+		{
+			bearing:  e,
+			track:    ene,
+			expected: Drag,
+		},
+		// ⮕⇨
+		{
+			bearing:  e,
+			track:    e,
+			expected: Drag,
+		},
+		// ⮕⬂
+		{
+			bearing:  e,
+			track:    ese,
+			expected: Drag,
+		},
+		{
+			bearing:  e,
+			track:    se,
+			expected: Drag,
+		},
+		{
+			bearing:  e,
+			track:    sse,
+			expected: Beam,
+		},
+		// ⮕⇩
+		{
+			bearing:  e,
+			track:    s,
+			expected: Beam,
+		},
+		// ⮕⬃
+		{
+			bearing:  e,
+			track:    ssw,
+			expected: Flank,
+		},
+		{
+			bearing:  e,
+			track:    sw,
+			expected: Flank,
+		},
+		{
+			bearing:  e,
+			track:    wsw,
+			expected: Hot,
+		},
+		// ⮕⇦
+		{
+			bearing:  e,
+			track:    w,
+			expected: Hot,
+		},
+		// ⮕⬁
+		{
+			bearing:  e,
+			track:    wnw,
+			expected: Hot,
+		},
+		{
+			bearing:  e,
+			track:    nw,
+			expected: Flank,
+		},
+		{
+			bearing:  e,
+			track:    nnw,
+			expected: Flank,
+		},
+		// ⬇
+		// ⇧
+		{
+			bearing:  s,
+			track:    n,
+			expected: Hot,
+		},
+		// ⬇
+		// ⬀
+		{
+			bearing:  s,
+			track:    nne,
+			expected: Hot,
+		},
+		{
+			bearing:  s,
+			track:    ne,
+			expected: Flank,
+		},
+		{
+			bearing:  s,
+			track:    ene,
+			expected: Flank,
+		},
+		// ⬇
+		// ⇨
+		{
+			bearing:  s,
+			track:    e,
+			expected: Beam,
+		},
+		// ⬇
+		// ⬂
+		{
+			bearing:  s,
+			track:    ese,
+			expected: Beam,
+		},
+		{
+			bearing:  s,
+			track:    se,
+			expected: Drag,
+		},
+		{
+			bearing:  s,
+			track:    sse,
+			expected: Drag,
+		},
+		// ⬇
+		// ⇩
+		{
+			bearing:  s,
+			track:    s,
+			expected: Drag,
+		},
+		// ⬇
+		// ⬃
+		{
+			bearing:  s,
+			track:    ssw,
+			expected: Drag,
+		},
+		{
+			bearing:  s,
+			track:    sw,
+			expected: Drag,
+		},
+		{
+			bearing:  s,
+			track:    wsw,
+			expected: Beam,
+		},
+		// ⬇
+		// ⇦
+		{
+			bearing:  s,
+			track:    w,
+			expected: Beam,
+		},
+		// ⬇
+		// ⬁
+		{
+			bearing:  s,
+			track:    wnw,
+			expected: Flank,
+		},
+		{
+			bearing:  s,
+			track:    nw,
+			expected: Flank,
+		},
+		{
+			bearing:  s,
+			track:    nnw,
+			expected: Hot,
+		},
+		// ⇧⬅
+		{
+			bearing:  w,
+			track:    n,
+			expected: Beam,
+		},
+		// ⬀⬅
+		{
+			bearing:  w,
+			track:    nne,
+			expected: Flank,
+		},
+		{
+			bearing:  w,
+			track:    ne,
+			expected: Flank,
+		},
+		{
+			bearing:  w,
+			track:    ene,
+			expected: Hot,
+		},
+		// ⇨⬅
+		{
+			bearing:  w,
+			track:    e,
+			expected: Hot,
+		},
+		// ⬂⬅
+		{
+			bearing:  w,
+			track:    ese,
+			expected: Hot,
+		},
+		{
+			bearing:  w,
+			track:    se,
+			expected: Flank,
+		},
+		{
+			bearing:  w,
+			track:    sse,
+			expected: Flank,
+		},
+		// ⇩⬅
+
+		{
+			bearing:  w,
+			track:    s,
+			expected: Beam,
+		},
+		// ⬃⬅
+		{
+			bearing:  w,
+			track:    ssw,
+			expected: Beam,
+		},
+		{
+			bearing:  w,
+			track:    sw,
+			expected: Drag,
+		},
+		{
+			bearing:  w,
+			track:    wsw,
+			expected: Drag,
+		},
+		// ⇦⬅
+		{
+			bearing:  w,
+			track:    w,
+			expected: Drag,
+		},
+		// ⬁⬅
+		{
+			bearing:  w,
+			track:    wnw,
+			expected: Drag,
+		},
+		{
+			bearing:  w,
+			track:    nw,
+			expected: Drag,
+		},
+		{
+			bearing:  w,
+			track:    nnw,
+			expected: Beam,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(fmt.Sprintf("bearing %.1f track %.1f", test.bearing.Degrees(), test.track.Degrees()), func(t *testing.T) {
+			actual := AspectFromAngle(test.bearing, test.track)
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+}