diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod index ecf3a902..906c0999 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod @@ -62,6 +62,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -89,6 +90,7 @@ require ( github.com/moby/term v0.5.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/segmentio/asm v1.2.1 // indirect diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum index 66b233ad..376d0ed0 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum @@ -100,6 +100,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b h1:JlltDRgni6FuoFwluvoZCrE6cmpojccO4WsqeYlFJLE= +github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b/go.mod h1:msW2QeN9IsnRyvuK8OBAzBwn6DHwXpiAiqBk8dbLfrU= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -168,10 +170,14 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/onsi/ginkgo v1.2.1-0.20160509182050-5437a97bf824/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20160516222431-c73e51675ad2/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= @@ -337,6 +343,7 @@ google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/propagator.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/propagator.go new file mode 100644 index 00000000..73e951a4 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/propagator.go @@ -0,0 +1,49 @@ +// ©AngelaMos | 2026 +// propagator.go + +package iss + +import ( + "fmt" + "math" + "strings" + "time" + + satellite "github.com/joshuaferrara/go-satellite" +) + +type Sat struct { + inner satellite.Satellite +} + +func LoadTLE(line1, line2 string) (Sat, error) { + if !strings.HasPrefix(line1, "1 ") || !strings.HasPrefix(line2, "2 ") { + return Sat{}, fmt.Errorf("invalid TLE: lines must start with '1 ' and '2 '") + } + s := satellite.TLEToSat(line1, line2, satellite.GravityWGS84) + if s.Error != 0 || s.ErrorStr != "" { + return Sat{}, fmt.Errorf("parse TLE: error %d / %s", s.Error, s.ErrorStr) + } + return Sat{inner: s}, nil +} + +func Position(s Sat, t time.Time) (lat, lon, altKm float64) { + t = t.UTC() + pos, _ := satellite.Propagate(s.inner, t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second()) + gmst := satellite.GSTimeFromDate(t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second()) + altitude, _, ll := satellite.ECIToLLA(pos, gmst) + deg := satellite.LatLongDeg(ll) + return deg.Latitude, deg.Longitude, altitude +} + +func LookAngles(s Sat, t time.Time, observerLatDeg, observerLonDeg, observerAltKm float64) (azimuthDeg, elevationDeg, rangeKm float64) { + t = t.UTC() + pos, _ := satellite.Propagate(s.inner, t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second()) + jday := satellite.JDay(t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second()) + obs := satellite.LatLong{ + Latitude: observerLatDeg * math.Pi / 180, + Longitude: observerLonDeg * math.Pi / 180, + } + la := satellite.ECIToLookAngles(pos, obs, observerAltKm, jday) + return la.Az * 180 / math.Pi, la.El * 180 / math.Pi, la.Rg +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/propagator_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/propagator_test.go new file mode 100644 index 00000000..ccb18a6c --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/propagator_test.go @@ -0,0 +1,55 @@ +// ©AngelaMos | 2026 +// propagator_test.go + +package iss_test + +import ( + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/iss" +) + +func loadTLE(t *testing.T) (string, string) { + t.Helper() + body, err := os.ReadFile("testdata/tle.txt") + require.NoError(t, err) + lines := strings.Split(strings.TrimSpace(string(body)), "\n") + require.GreaterOrEqual(t, len(lines), 3, "TLE file must have name + 2 element lines") + return strings.TrimRight(lines[1], " \r"), strings.TrimRight(lines[2], " \r") +} + +func TestPropagator_PositionWithinReasonableBounds(t *testing.T) { + l1, l2 := loadTLE(t) + sat, err := iss.LoadTLE(l1, l2) + require.NoError(t, err) + + when := time.Date(2026, 5, 2, 3, 0, 0, 0, time.UTC) + lat, lon, alt := iss.Position(sat, when) + + require.True(t, lat >= -90 && lat <= 90, "lat=%f", lat) + require.True(t, lon >= -180 && lon <= 180, "lon=%f", lon) + require.True(t, alt >= 350 && alt <= 500, "ISS altitude is roughly 380-420 km, got %f", alt) +} + +func TestPropagator_LookAnglesInValidRange(t *testing.T) { + l1, l2 := loadTLE(t) + sat, err := iss.LoadTLE(l1, l2) + require.NoError(t, err) + + when := time.Date(2026, 5, 2, 3, 0, 0, 0, time.UTC) + az, el, rng := iss.LookAngles(sat, when, 40.7128, -74.0060, 0) + + require.True(t, az >= 0 && az <= 360, "az=%f", az) + require.True(t, el >= -90 && el <= 90, "el=%f", el) + require.Greater(t, rng, 0.0) +} + +func TestPropagator_BadTLEReturnsError(t *testing.T) { + _, err := iss.LoadTLE("not a tle", "really not a tle") + require.Error(t, err) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/position.json b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/position.json new file mode 100644 index 00000000..e451a0c3 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/position.json @@ -0,0 +1 @@ +{"name":"iss","id":25544,"latitude":10.63651293933,"longitude":-10.956952860344,"altitude":423.74642684314,"velocity":27564.677940445,"visibility":"daylight","footprint":4526.4746978082,"timestamp":1777710988,"daynum":2461162.8586574,"solar_lat":15.425197566858,"solar_lon":50.133029500693,"units":"kilometers"} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/tle.json b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/tle.json new file mode 100644 index 00000000..ac67dea9 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/tle.json @@ -0,0 +1 @@ +[{"OBJECT_NAME":"ISS (ZARYA)","OBJECT_ID":"1998-067A","EPOCH":"2026-05-02T03:14:52.956096","MEAN_MOTION":15.49057929,"ECCENTRICITY":0.0007234,"INCLINATION":51.6309,"RA_OF_ASC_NODE":168.049,"ARG_OF_PERICENTER":13.9157,"MEAN_ANOMALY":346.2029,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":25544,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":56461,"BSTAR":0.00014312,"MEAN_MOTION_DOT":7.461e-5,"MEAN_MOTION_DDOT":0}] diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/tle.txt b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/tle.txt new file mode 100644 index 00000000..b3934d64 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/iss/testdata/tle.txt @@ -0,0 +1,3 @@ +ISS (ZARYA) +1 25544U 98067A 26122.13533514 .00007461 00000+0 14312-3 0 9999 +2 25544 51.6309 168.0490 0007234 13.9157 346.2029 15.49057929564615