summaryrefslogtreecommitdiff
path: root/iss/predictions.py
blob: 373776010a2da66df6c8f458290fdd7da843ad19 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
from collections import defaultdict
from skyfield.api import Topos, load

from .utils import chunks, deg_to_cardinal, seconds_to_minutes


ISS = "ISS (ZARYA)"
STATIONS_URL = "http://celestrak.com/NORAD/elements/stations.txt"


class Predictions(object):
    def __init__(
        self,
        lat,
        lng,
        altitude=0,
        tz="UTC",
        satellite=ISS,
        start=None,
        days=10,
        tle_file=None,
    ):
        self.lat = lat
        self.lng = lng
        self.altitude = altitude
        self.tz = tz
        self.start = start
        self.days = days
        self.tle_file = tle_file.resolve().as_posix() if tle_file else STATIONS_URL

        self.satellite = self.init_satellite(satellite)
        self.location = self.init_location()

    def init_stations(self):
        return load.tle_file(self.tle_file)

    def init_satellite(self, satellite):
        satellites = self.init_stations()
        by_name = {sat.name: sat for sat in satellites}
        return by_name[satellite]

    def init_location(self):
        return Topos(latitude_degrees=self.lat, longitude_degrees=self.lng)

    def get_next_days(self):
        ts = load.timescale()
        t0 = ts.now() if not self.start else ts.ut1_jd(self.start)
        t1 = ts.ut1_jd(t0.ut1 + self.days)
        return t0, t1

    def get_position_details(self, t):
        difference = self.satellite - self.location
        topocentric = difference.at(t)
        alt, az, distance = topocentric.altaz()
        azimuth = int(az.degrees)
        return {
            "time": t.utc_iso(),
            "degrees": int(alt.degrees),
            "azimuth": azimuth,
            "direction": deg_to_cardinal(azimuth),
            "distance": int(distance.km),
        }

    def get_prediction_details(self, rise, culminate, zet):
        length = int((zet - rise) * 86400)
        return {
            "length": length,
            "length_mins": seconds_to_minutes(length),
            "rise": self.get_position_details(rise),
            "culminate": self.get_position_details(culminate),
            "set": self.get_position_details(zet),
        }

    def get_prediction_events(self):
        t0, t1 = self.get_next_days()

        ts, _events = self.satellite.find_events(
            self.location, t0, t1, altitude_degrees=self.altitude
        )

        # events are returned as 3-tuples of (rise, culminate, set)
        # where rise/set are relative to given altitude
        # docs mention the possibilibity of several culminations
        # https://rhodesmill.org/skyfield/earth-satellites.html#finding-when-a-satellite-rises-and-sets
        # but this doesn't seem to happen in our case
        res = list(chunks(ts, 3))

        if len(res[-1]) != 3:
            # truncate the last event in case it's a partial one
            res = res[:-1]

        return res

    def get_predictions(self):
        preds = self.get_prediction_events()
        return [self.get_prediction_details(*p) for p in preds]

    def truncate_prediction_dates(self, pred):
        for t in ["rise", "culminate", "set"]:
            pred[t]["time"] = pred[t]["time"][11:][:-1]
        return pred

    def get_grouped_predictions(self):
        preds = self.get_predictions()
        res = defaultdict(list)

        for pred in preds:
            date = pred["rise"]["time"][:10]
            res[date].append(self.truncate_prediction_dates(pred))

        return res