200 lines
5.5 KiB
Go
200 lines
5.5 KiB
Go
package orbitviewer
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Standard ISS TLE sample
|
|
const (
|
|
testISSName = "ISS (ZARYA)"
|
|
testISSLine1 = "1 25544U 98067A 24080.56944444 .00014524 00000-0 26388-3 0 9993"
|
|
testISSLine2 = "2 25544 51.6416 195.4568 0004928 120.1234 239.9876 15.49876543445566"
|
|
)
|
|
|
|
func TestParseTLE(t *testing.T) {
|
|
tle, err := ParseTLE(testISSName, testISSLine1, testISSLine2)
|
|
if err != nil {
|
|
t.Fatalf("ParseTLE failed: %v", err)
|
|
}
|
|
|
|
if tle.NoradID != 25544 {
|
|
t.Errorf("Expected NoradID 25544, got %d", tle.NoradID)
|
|
}
|
|
if math.Abs(tle.InclinationDeg-51.6416) > 1e-4 {
|
|
t.Errorf("Expected Inclination 51.6416, got %f", tle.InclinationDeg)
|
|
}
|
|
if math.Abs(tle.Eccentricity-0.0004928) > 1e-6 {
|
|
t.Errorf("Expected Eccentricity 0.0004928, got %f", tle.Eccentricity)
|
|
}
|
|
}
|
|
|
|
func TestSGP4Propagation(t *testing.T) {
|
|
tle, err := ParseTLE(testISSName, testISSLine1, testISSLine2)
|
|
if err != nil {
|
|
t.Fatalf("ParseTLE failed: %v", err)
|
|
}
|
|
|
|
model, err := NewSGP4Model(tle)
|
|
if err != nil {
|
|
t.Fatalf("NewSGP4Model failed: %v", err)
|
|
}
|
|
|
|
// Propagate at epoch (tsince = 0)
|
|
pos, vel, err := model.PropagateMinutes(0)
|
|
if err != nil {
|
|
t.Fatalf("PropagateMinutes(0) failed: %v", err)
|
|
}
|
|
|
|
rKm := math.Sqrt(pos.X*pos.X + pos.Y*pos.Y + pos.Z*pos.Z)
|
|
// ISS orbit radius should be around 6700 - 6850 km (altitude 350 - 450 km)
|
|
if rKm < 6650 || rKm > 6900 {
|
|
t.Errorf("Unrealistic orbital radius: %f km", rKm)
|
|
}
|
|
|
|
vKmS := math.Sqrt(vel.X*vel.X + vel.Y*vel.Y + vel.Z*vel.Z)
|
|
// Orbital velocity for LEO should be around 7.5 - 7.8 km/s
|
|
if vKmS < 7.2 || vKmS > 8.0 {
|
|
t.Errorf("Unrealistic orbital velocity: %f km/s", vKmS)
|
|
}
|
|
|
|
// Propagate 90 minutes (about 1 orbit)
|
|
geo, _, err := model.GeodeticPosition(tle.EpochTime.Add(90 * time.Minute))
|
|
if err != nil {
|
|
t.Fatalf("GeodeticPosition failed: %v", err)
|
|
}
|
|
|
|
if geo.Altitude < 350 || geo.Altitude > 460 {
|
|
t.Errorf("Unrealistic altitude after 90 min: %f km", geo.Altitude)
|
|
}
|
|
if geo.Latitude < -55 || geo.Latitude > 55 {
|
|
t.Errorf("Latitude %f exceeds inclination range +/-52 deg by too much", geo.Latitude)
|
|
}
|
|
}
|
|
|
|
func TestLookAnglesAndSun(t *testing.T) {
|
|
// Observer in San Francisco
|
|
obsSF := Geodetic{
|
|
Latitude: 37.7749,
|
|
Longitude: -122.4194,
|
|
Altitude: 0.03, // 30 meters
|
|
}
|
|
|
|
// Target directly above observer at 400km altitude
|
|
targetECEF := GeodeticToECEF(Geodetic{
|
|
Latitude: 37.7749,
|
|
Longitude: -122.4194,
|
|
Altitude: 400.0,
|
|
})
|
|
|
|
angles := CalculateLookAngles(obsSF, targetECEF)
|
|
if math.Abs(angles.Elevation-90.0) > 0.1 {
|
|
t.Errorf("Expected elevation ~90 deg for overhead satellite, got %f", angles.Elevation)
|
|
}
|
|
if math.Abs(angles.Range-400.0) > 1.0 {
|
|
t.Errorf("Expected range ~400 km, got %f", angles.Range)
|
|
}
|
|
|
|
// Test Sun elevation at noon vs midnight
|
|
noonUTC := time.Date(2024, 6, 21, 20, 0, 0, 0, time.UTC) // roughly solar noon in SF (12:00 PDT)
|
|
elNoon := ObserverSunElevation(obsSF, noonUTC)
|
|
if elNoon < 60.0 {
|
|
t.Errorf("Expected high solar elevation at summer solstice noon, got %f", elNoon)
|
|
}
|
|
|
|
midnightUTC := time.Date(2024, 6, 21, 8, 0, 0, 0, time.UTC) // midnight in SF
|
|
elMidnight := ObserverSunElevation(obsSF, midnightUTC)
|
|
if elMidnight > -15.0 {
|
|
t.Errorf("Expected negative solar elevation at midnight, got %f", elMidnight)
|
|
}
|
|
}
|
|
|
|
func TestPredictPasses(t *testing.T) {
|
|
tle, err := ParseTLE(testISSName, testISSLine1, testISSLine2)
|
|
if err != nil {
|
|
t.Fatalf("ParseTLE failed: %v", err)
|
|
}
|
|
|
|
model, err := NewSGP4Model(tle)
|
|
if err != nil {
|
|
t.Fatalf("NewSGP4Model failed: %v", err)
|
|
}
|
|
|
|
obs := Geodetic{
|
|
Latitude: 40.7128, // New York
|
|
Longitude: -74.0060,
|
|
Altitude: 0.01,
|
|
}
|
|
|
|
// Predict passes for 5 days starting at epoch
|
|
passes, err := PredictPasses(model, PredictPassesOptions{
|
|
Observer: obs,
|
|
StartTime: tle.EpochTime,
|
|
Days: 5,
|
|
MinElevationDeg: 10.0,
|
|
VisibleOnly: false,
|
|
StandardMag: -1.8,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PredictPasses failed: %v", err)
|
|
}
|
|
|
|
if len(passes) == 0 {
|
|
t.Errorf("Expected at least a few passes over 5 days for ISS over NYC, got 0")
|
|
}
|
|
|
|
t.Logf("Found %d passes over 5 days", len(passes))
|
|
for i, p := range passes {
|
|
t.Logf("Pass #%d: Start=%s, MaxEl=%.1f deg (%s), End=%s, Dur=%s, Vis=%s, Mag=%.1f",
|
|
i+1, p.Start.TimeFormatted, p.Max.Elevation, p.Max.AzimuthCardinal,
|
|
p.End.TimeFormatted, p.DurationFormatted, p.Visibility, p.EstimatedMag)
|
|
}
|
|
}
|
|
|
|
func TestTiangongNYC(t *testing.T) {
|
|
entry, ok := DefaultCatalog.Get("tiangong")
|
|
if !ok {
|
|
t.Fatalf("Tiangong not in default catalog")
|
|
}
|
|
|
|
model, err := NewSGP4Model(entry.TLE)
|
|
if err != nil {
|
|
t.Fatalf("NewSGP4Model failed: %v", err)
|
|
}
|
|
|
|
obsNYC := Geodetic{
|
|
Latitude: 40.7128,
|
|
Longitude: -74.0060,
|
|
Altitude: 0.01,
|
|
}
|
|
|
|
// 1. All passes
|
|
allPasses, err := PredictPasses(model, PredictPassesOptions{
|
|
Observer: obsNYC,
|
|
StartTime: time.Now().UTC(),
|
|
Days: 10,
|
|
MinElevationDeg: 10.0,
|
|
VisibleOnly: false,
|
|
StandardMag: -1.0,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PredictPasses failed: %v", err)
|
|
}
|
|
t.Logf("Tiangong over NYC - ALL passes: %d", len(allPasses))
|
|
for i, p := range allPasses {
|
|
t.Logf("All pass #%d: Start=%s, MaxEl=%.1f, Vis=%s, Mag=%.1f", i+1, p.Start.Time.Format(time.RFC3339), p.Max.Elevation, p.Visibility, p.EstimatedMag)
|
|
}
|
|
|
|
// 2. Visible passes only
|
|
visPasses, _ := PredictPasses(model, PredictPassesOptions{
|
|
Observer: obsNYC,
|
|
StartTime: time.Now().UTC(),
|
|
Days: 10,
|
|
MinElevationDeg: 10.0,
|
|
VisibleOnly: true,
|
|
StandardMag: -1.0,
|
|
})
|
|
t.Logf("Tiangong over NYC - VISIBLE ONLY passes: %d", len(visPasses))
|
|
}
|