1010import matplotlib .pyplot as plt
1111from PIL import Image , ImageTk
1212import io
13-
14- # Generate static world map background
15- def generate_map ():
16- fig = plt .figure (figsize = (8 , 4 ))
17- ax = fig .add_subplot (1 , 1 , 1 , projection = ccrs .PlateCarree ())
18- ax .set_global ()
19- ax .stock_img ()
20- ax .coastlines ()
21- buf = io .BytesIO ()
22- plt .savefig (buf , format = 'png' , dpi = 100 , bbox_inches = 'tight' )
23- buf .seek (0 )
24- plt .close (fig )
25- return Image .open (buf )
26-
27- # Get fresh ISS TLE
28- def fetch_iss_tle ():
29- url = "https://celestrak.org/NORAD/elements/stations.txt"
30- tle_data = requests .get (url ).text
31- lines = tle_data .split ('\n ' )
32- for i , line in enumerate (lines ):
33- if "ISS (ZARYA)" in line :
34- return lines [i + 1 ], lines [i + 2 ]
35- return None , None
36-
37- # Convert ECI to ECEF (lat/lon)
38- def eci_to_latlon (pos , jd ):
39- T = (jd - 2451545.0 ) / 36525.0
40- GMST = 280.46061837 + 360.98564736629 * (jd - 2451545.0 ) + 0.000387933 * T ** 2 - T ** 3 / 38710000.0
41- GMST = GMST % 360.0
42- theta = math .radians (GMST )
43-
44- x = pos [0 ]* math .cos (theta ) + pos [1 ]* math .sin (theta )
45- y = - pos [0 ]* math .sin (theta ) + pos [1 ]* math .cos (theta )
46- z = pos [2 ]
47-
48- r = math .sqrt (x ** 2 + y ** 2 + z ** 2 )
49- lat = math .degrees (math .asin (z / r ))
50- lon = math .degrees (math .atan2 (y , x ))
51- if lon > 180 :
52- lon -= 360
53- return lat , lon
54-
55- # Convert lat/lon to map x/y
56- def latlon_to_xy (lat , lon , width , height ):
57- x = (lon + 180 ) * (width / 360 )
58- y = (90 - lat ) * (height / 180 )
59- return x , y
60-
61- # Handle longitudes wrapping across ±180
62- def smooth_longitude (prev_lon , curr_lon ):
63- delta = curr_lon - prev_lon
64- if delta > 180 :
65- curr_lon -= 360
66- elif delta < - 180 :
67- curr_lon += 360
68- return curr_lon
13+ import threading
6914
7015class ISSTrackerApp :
7116 def __init__ (self , root ):
@@ -74,25 +19,60 @@ def __init__(self, root):
7419 self .canvas = Canvas (root , width = 800 , height = 400 , bg = "black" )
7520 self .canvas .pack ()
7621
77- self .image = generate_map ().resize ((800 , 400 ))
22+ self .image = self . generate_map ().resize ((800 , 400 ))
7823 self .tk_image = ImageTk .PhotoImage (self .image )
79-
80- self .load_tle ()
24+ self .canvas .create_image (0 , 0 , anchor = "nw" , image = self .tk_image )
8125
8226 self .time_offset = timedelta (seconds = 0 )
27+ self .satellite = None
28+ self .last_tle_update = None
29+
30+ self .load_tle ()
31+ self .schedule_tle_refresh ()
8332
8433 self .root .bind ("<Left>" , self .go_back )
8534 self .root .bind ("<Right>" , self .go_forward )
8635 self .root .bind ("<space>" , self .reset_time )
8736
8837 self .update_display ()
8938
39+ def generate_map (self ):
40+ fig = plt .figure (figsize = (8 , 4 ))
41+ ax = fig .add_subplot (1 , 1 , 1 , projection = ccrs .PlateCarree ())
42+ ax .set_global ()
43+ ax .stock_img ()
44+ ax .coastlines ()
45+ buf = io .BytesIO ()
46+ plt .savefig (buf , format = 'png' , dpi = 100 , bbox_inches = 'tight' )
47+ buf .seek (0 )
48+ plt .close (fig )
49+ return Image .open (buf )
50+
9051 def load_tle (self ):
91- tle1 , tle2 = fetch_iss_tle ()
92- if tle1 and tle2 :
93- self .satellite = Satrec .twoline2rv (tle1 , tle2 )
94- else :
95- self .satellite = None
52+ try :
53+ url = "https://celestrak.org/NORAD/elements/stations.txt"
54+ tle_data = requests .get (url , timeout = 10 ).text
55+ lines = tle_data .split ('\n ' )
56+ for i , line in enumerate (lines ):
57+ if "ISS (ZARYA)" in line :
58+ tle1 = lines [i + 1 ].strip ()
59+ tle2 = lines [i + 2 ].strip ()
60+ self .satellite = Satrec .twoline2rv (tle1 , tle2 )
61+ self .last_tle_update = datetime .utcnow ()
62+ print ("TLE updated:" , self .last_tle_update )
63+ return
64+ except Exception as e :
65+ print ("Error updating TLE:" , e )
66+
67+ def schedule_tle_refresh (self ):
68+ # Refresh TLE every 10 minutes
69+ def tle_updater ():
70+ while True :
71+ now = datetime .utcnow ()
72+ if self .last_tle_update is None or (now - self .last_tle_update ).total_seconds () > 600 :
73+ self .load_tle ()
74+ threading .Event ().wait (60 )
75+ threading .Thread (target = tle_updater , daemon = True ).start ()
9676
9777 def update_display (self ):
9878 self .canvas .delete ("all" )
@@ -101,13 +81,13 @@ def update_display(self):
10181 now = datetime .utcnow () + self .time_offset
10282 jd , fr = jday (now .year , now .month , now .day , now .hour , now .minute , now .second + now .microsecond / 1e6 )
10383 e , pos , vel = self .satellite .sgp4 (jd , fr )
104- lat , lon = eci_to_latlon (pos , jd )
105- iss_x , iss_y = latlon_to_xy (lat , lon , 800 , 400 )
84+ lat , lon = self . eci_to_latlon (pos , jd )
85+ iss_x , iss_y = self . latlon_to_xy (lat , lon , 800 , 400 )
10686
10787 # Draw ISS current position
10888 self .canvas .create_oval (iss_x - 4 , iss_y - 4 , iss_x + 4 , iss_y + 4 , fill = "purple" , tags = "iss" )
10989
110- # Draw current orbit path (next 90 minutes only)
90+ # Draw current orbit path
11191 path_coords = []
11292 prev_lon = None
11393 for mins in range (- 10 , 90 , 2 ):
@@ -116,13 +96,13 @@ def update_display(self):
11696 future_time .hour , future_time .minute ,
11797 future_time .second + future_time .microsecond / 1e6 )
11898 e_fut , pos_fut , vel_fut = self .satellite .sgp4 (jd_fut , fr_fut )
119- lat_fut , lon_fut = eci_to_latlon (pos_fut , jd_fut )
99+ lat_fut , lon_fut = self . eci_to_latlon (pos_fut , jd_fut )
120100
121101 if prev_lon is not None :
122- lon_fut = smooth_longitude (prev_lon , lon_fut )
102+ lon_fut = self . smooth_longitude (prev_lon , lon_fut )
123103 prev_lon = lon_fut
124104
125- x , y = latlon_to_xy (lat_fut , lon_fut , 800 , 400 )
105+ x , y = self . latlon_to_xy (lat_fut , lon_fut , 800 , 400 )
126106 path_coords .append ((x , y ))
127107
128108 for i in range (len (path_coords )- 1 ):
@@ -136,6 +116,36 @@ def update_display(self):
136116
137117 self .root .after (1000 , self .update_display )
138118
119+ def eci_to_latlon (self , pos , jd ):
120+ T = (jd - 2451545.0 ) / 36525.0
121+ GMST = 280.46061837 + 360.98564736629 * (jd - 2451545.0 ) + 0.000387933 * T ** 2 - T ** 3 / 38710000.0
122+ GMST = GMST % 360.0
123+ theta = math .radians (GMST )
124+
125+ x = pos [0 ]* math .cos (theta ) + pos [1 ]* math .sin (theta )
126+ y = - pos [0 ]* math .sin (theta ) + pos [1 ]* math .cos (theta )
127+ z = pos [2 ]
128+
129+ r = math .sqrt (x ** 2 + y ** 2 + z ** 2 )
130+ lat = math .degrees (math .asin (z / r ))
131+ lon = math .degrees (math .atan2 (y , x ))
132+ if lon > 180 :
133+ lon -= 360
134+ return lat , lon
135+
136+ def latlon_to_xy (self , lat , lon , width , height ):
137+ x = (lon + 180 ) * (width / 360 )
138+ y = (90 - lat ) * (height / 180 )
139+ return x , y
140+
141+ def smooth_longitude (self , prev_lon , curr_lon ):
142+ delta = curr_lon - prev_lon
143+ if delta > 180 :
144+ curr_lon -= 360
145+ elif delta < - 180 :
146+ curr_lon += 360
147+ return curr_lon
148+
139149 def go_forward (self , event ):
140150 self .time_offset += timedelta (minutes = 2 )
141151
0 commit comments