Add satellite ephemeris calculator app with Streamlit
Browse files- Implement main app logic in and for satellite ephemeris calculation.
- Add utility functions in for computing ephemeris and fetching TLE data.
- Add utility functions in for timezone conversion.
- Include dependencies in .
- app.py +120 -0
- app_copy.py +218 -0
- requirements.txt +9 -0
- utils/satellite_utils.py +96 -0
- utils/timezone_utils.py +13 -0
app.py
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import streamlit as st
|
3 |
+
import pandas as pd
|
4 |
+
from geopy.geocoders import Nominatim
|
5 |
+
from streamlit_folium import st_folium
|
6 |
+
import folium
|
7 |
+
from datetime import datetime
|
8 |
+
import pytz
|
9 |
+
from timezonefinder import TimezoneFinder
|
10 |
+
|
11 |
+
from utils.satellite_utils import compute_ephemeris, fetch_custom_tle
|
12 |
+
from utils.timezone_utils import get_abbreviated_timezone
|
13 |
+
|
14 |
+
def main():
|
15 |
+
st.title("Satellites Ephemeris Calculator")
|
16 |
+
|
17 |
+
# Dropdown for satellite category selection
|
18 |
+
satellite_categories = {
|
19 |
+
'Starlink Generation 1': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=starlink&FORMAT=tle',
|
20 |
+
'Starlink Generation 2': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=starlink&FORMAT=tle&VERSION=2',
|
21 |
+
'OneWeb': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=oneweb&FORMAT=tle',
|
22 |
+
'Kuiper': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=kuiper&FORMAT=tle',
|
23 |
+
'Custom Satellite': None
|
24 |
+
}
|
25 |
+
satellite_type = st.selectbox("Select or Search Satellite", list(satellite_categories.keys()))
|
26 |
+
|
27 |
+
custom_tle = None
|
28 |
+
satellite_url = satellite_categories[satellite_type]
|
29 |
+
|
30 |
+
# If custom satellite is selected, ask for satellite name or NORAD ID
|
31 |
+
if satellite_type == 'Custom Satellite':
|
32 |
+
satellite_name_or_id = st.text_input("Enter Satellite Name or NORAD ID:")
|
33 |
+
if satellite_name_or_id:
|
34 |
+
custom_tle = fetch_custom_tle(satellite_name_or_id)
|
35 |
+
|
36 |
+
# Detect user's local time zone
|
37 |
+
local_timezone = datetime.now().astimezone().tzinfo
|
38 |
+
|
39 |
+
# Coordinates and time input
|
40 |
+
with st.container():
|
41 |
+
col1, col2 = st.columns([2, 1])
|
42 |
+
with col1:
|
43 |
+
use_address = st.checkbox("Use Address to Set Location", value=False)
|
44 |
+
if use_address:
|
45 |
+
address = st.text_input("Enter Your Address:")
|
46 |
+
if address:
|
47 |
+
geolocator = Nominatim(user_agent="satellite-observation-app")
|
48 |
+
try:
|
49 |
+
location = geolocator.geocode(address, timeout=10)
|
50 |
+
if location:
|
51 |
+
latitude = location.latitude
|
52 |
+
longitude = location.longitude
|
53 |
+
else:
|
54 |
+
st.write("Could not find the location. Please enter a valid address.")
|
55 |
+
except Exception as e:
|
56 |
+
st.error(f"Error fetching location data: {str(e)}")
|
57 |
+
else:
|
58 |
+
st.write("Select your location on the map:")
|
59 |
+
default_location = [37.7749, -122.4194]
|
60 |
+
map_display = folium.Map(location=default_location, zoom_start=2)
|
61 |
+
folium.Marker(default_location, tooltip="Default Location").add_to(map_display)
|
62 |
+
map_data = st_folium(map_display, width=350, height=300)
|
63 |
+
if map_data and 'last_clicked' in map_data and map_data['last_clicked']:
|
64 |
+
latitude = map_data['last_clicked']['lat']
|
65 |
+
longitude = map_data['last_clicked']['lng']
|
66 |
+
|
67 |
+
with col2:
|
68 |
+
latitude = st.text_input("Latitude:", value=str(latitude) if 'latitude' in locals() else "")
|
69 |
+
longitude = st.text_input("Longitude:", value=str(longitude) if 'longitude' in locals() else "")
|
70 |
+
|
71 |
+
# Determine time zone from GPS coordinates
|
72 |
+
if latitude and longitude:
|
73 |
+
try:
|
74 |
+
tf = TimezoneFinder()
|
75 |
+
timezone_str = tf.timezone_at(lng=float(longitude), lat=float(latitude))
|
76 |
+
timezone = pytz.timezone(timezone_str)
|
77 |
+
current_time = datetime.now()
|
78 |
+
timezone_abbr = get_abbreviated_timezone(timezone_str, current_time)
|
79 |
+
st.write(f"Detected Time Zone: {timezone_str} ({timezone_abbr})")
|
80 |
+
except Exception as e:
|
81 |
+
st.error(f"Error determining timezone: {str(e)}")
|
82 |
+
timezone = local_timezone
|
83 |
+
else:
|
84 |
+
timezone = local_timezone
|
85 |
+
timezone_abbr = timezone.tzname(datetime.now())
|
86 |
+
st.write(f"Using local time zone: {timezone} ({timezone_abbr})")
|
87 |
+
|
88 |
+
start_date_local = st.date_input("Start Date (Local Time):")
|
89 |
+
start_time_local = st.time_input("Start Time (Local Time):")
|
90 |
+
end_time_local = st.time_input("End Time (Local Time):")
|
91 |
+
|
92 |
+
# Convert local time to UTC
|
93 |
+
start_datetime_local = datetime.combine(start_date_local, start_time_local)
|
94 |
+
end_datetime_local = datetime.combine(start_date_local, end_time_local)
|
95 |
+
start_datetime_utc = start_datetime_local.astimezone(pytz.utc)
|
96 |
+
end_datetime_utc = end_datetime_local.astimezone(pytz.utc)
|
97 |
+
|
98 |
+
st.write(f"Start Time in UTC: {start_datetime_utc.strftime('%Y-%m-%d %H:%M:%S')}")
|
99 |
+
st.write(f"End Time in UTC: {end_datetime_utc.strftime('%Y-%m-%d %H:%M:%S')}")
|
100 |
+
|
101 |
+
compute_button = st.button("Compute Satellite Positions")
|
102 |
+
|
103 |
+
# Align the table with the button
|
104 |
+
if compute_button:
|
105 |
+
if latitude and longitude and start_date_local:
|
106 |
+
ephemeris_df = compute_ephemeris(
|
107 |
+
satellite_url, float(latitude), float(longitude),
|
108 |
+
start_datetime_utc.strftime('%Y-%m-%d'),
|
109 |
+
start_datetime_utc.time(), end_datetime_utc.time(),
|
110 |
+
custom_tle
|
111 |
+
)
|
112 |
+
if not ephemeris_df.empty:
|
113 |
+
st.dataframe(ephemeris_df, use_container_width=True)
|
114 |
+
else:
|
115 |
+
st.write("No visible satellites found for the specified time and location.")
|
116 |
+
else:
|
117 |
+
st.write("Please fill in all fields (latitude, longitude, and observation time).")
|
118 |
+
|
119 |
+
if __name__ == "__main__":
|
120 |
+
main()
|
app_copy.py
ADDED
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import streamlit as st
|
3 |
+
from skyfield.api import Topos, load, EarthSatellite
|
4 |
+
import requests
|
5 |
+
import pandas as pd
|
6 |
+
from geopy.geocoders import Nominatim
|
7 |
+
from streamlit_folium import st_folium
|
8 |
+
import folium
|
9 |
+
import numpy as np
|
10 |
+
from datetime import datetime
|
11 |
+
import pytz
|
12 |
+
from timezonefinder import TimezoneFinder
|
13 |
+
|
14 |
+
# Function to convert azimuth degrees to cardinal direction
|
15 |
+
def get_cardinal_direction(azimuth_degrees):
|
16 |
+
directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']
|
17 |
+
index = int((azimuth_degrees + 22.5) // 45)
|
18 |
+
return directions[index % 8]
|
19 |
+
|
20 |
+
# Function to get timezone in an abbreviated format
|
21 |
+
def get_abbreviated_timezone(timezone_str, dt):
|
22 |
+
try:
|
23 |
+
tz = pytz.timezone(timezone_str)
|
24 |
+
return dt.astimezone(tz).tzname()
|
25 |
+
except Exception as e:
|
26 |
+
st.error(f"Error converting timezone: {str(e)}")
|
27 |
+
return timezone_str # Return the original string if conversion fails
|
28 |
+
|
29 |
+
# Generalized function to compute satellite ephemeris
|
30 |
+
def compute_ephemeris(satellite_url, latitude, longitude, start_date_utc, start_time, end_time, custom_tle=None):
|
31 |
+
try:
|
32 |
+
ts = load.timescale()
|
33 |
+
year, month, day = map(int, start_date_utc.split('-'))
|
34 |
+
observer = Topos(latitude, longitude)
|
35 |
+
start_hour, start_minute = start_time.hour, start_time.minute
|
36 |
+
end_hour, end_minute = end_time.hour, end_time.minute
|
37 |
+
start_datetime = datetime(year, month, day, start_hour, start_minute)
|
38 |
+
end_datetime = datetime(year, month, day, end_hour, end_minute)
|
39 |
+
total_minutes = int((end_datetime - start_datetime).total_seconds() // 60)
|
40 |
+
times = ts.utc(year, month, day, [start_hour] * total_minutes, np.arange(start_minute, start_minute + total_minutes))
|
41 |
+
|
42 |
+
# Fetch TLE data for custom satellite or use predefined URL
|
43 |
+
if custom_tle:
|
44 |
+
st.write("Using custom TLE data for satellite computation.")
|
45 |
+
satellites = [EarthSatellite(custom_tle[1], custom_tle[2], custom_tle[0], ts)]
|
46 |
+
else:
|
47 |
+
r = requests.get(satellite_url)
|
48 |
+
with open('satellite_data.txt', 'wb') as f:
|
49 |
+
f.write(r.content)
|
50 |
+
satellites = load.tle_file('satellite_data.txt')
|
51 |
+
|
52 |
+
# Prepare the ephemeris data
|
53 |
+
ephemeris_data = []
|
54 |
+
for satellite in satellites:
|
55 |
+
difference = satellite - observer
|
56 |
+
for ti in times:
|
57 |
+
topocentric = difference.at(ti)
|
58 |
+
ra, dec, distance = topocentric.radec()
|
59 |
+
alt, az, distance = topocentric.altaz()
|
60 |
+
if alt.degrees > 0: # Check if the satellite is above the horizon
|
61 |
+
# Convert Angle objects to string representations and add cardinal direction
|
62 |
+
azimuth_degrees = az.degrees
|
63 |
+
cardinal_direction = get_cardinal_direction(azimuth_degrees)
|
64 |
+
ephemeris_data.append({
|
65 |
+
"Date (UTC)": ti.utc_strftime('%Y-%m-%d %H:%M:%S'),
|
66 |
+
"R.A.": str(ra),
|
67 |
+
"Dec": str(dec),
|
68 |
+
"Altitude": f"{alt.degrees:.2f}°",
|
69 |
+
"Azimuth": f"{azimuth_degrees:.2f}° ({cardinal_direction})"
|
70 |
+
})
|
71 |
+
|
72 |
+
# Convert ephemeris data to DataFrame for display
|
73 |
+
ephemeris_df = pd.DataFrame(ephemeris_data)
|
74 |
+
if not custom_tle:
|
75 |
+
os.remove('satellite_data.txt')
|
76 |
+
return ephemeris_df
|
77 |
+
except Exception as e:
|
78 |
+
st.error(f"Error computing ephemeris: {str(e)}")
|
79 |
+
return pd.DataFrame() # Return an empty DataFrame on error
|
80 |
+
|
81 |
+
# Fetch TLE data for custom satellite by name or NORAD ID
|
82 |
+
def fetch_custom_tle(satellite_name_or_id):
|
83 |
+
try:
|
84 |
+
# Correct URL formats for NORAD ID and satellite name
|
85 |
+
if satellite_name_or_id.isdigit():
|
86 |
+
# Fetch TLE by NORAD ID
|
87 |
+
celestrak_url = f'https://celestrak.org/NORAD/elements/gp.php?CATNR={satellite_name_or_id}'
|
88 |
+
else:
|
89 |
+
# Fetch TLE by satellite name
|
90 |
+
celestrak_url = f'https://celestrak.org/NORAD/elements/gp.php?NAME={satellite_name_or_id}&FORMAT=TLE'
|
91 |
+
|
92 |
+
with st.spinner("Fetching TLE data..."):
|
93 |
+
response = requests.get(celestrak_url)
|
94 |
+
|
95 |
+
if response.status_code == 200 and len(response.text.splitlines()) >= 2:
|
96 |
+
lines = response.text.splitlines()
|
97 |
+
st.success("TLE data fetched successfully!")
|
98 |
+
st.text(f"Name: {lines[0].strip()}\nTLE Line 1: {lines[1].strip()}\nTLE Line 2: {lines[2].strip()}")
|
99 |
+
return lines[0].strip(), lines[1].strip(), lines[2].strip() # Name, TLE line 1, TLE line 2
|
100 |
+
else:
|
101 |
+
st.error("TLE data could not be fetched. Please check the satellite name or NORAD ID.")
|
102 |
+
return None
|
103 |
+
except Exception as e:
|
104 |
+
st.error(f"Error fetching TLE data: {str(e)}")
|
105 |
+
return None
|
106 |
+
|
107 |
+
# Streamlit app
|
108 |
+
def main():
|
109 |
+
st.title("Satellites Ephemeris Calculator")
|
110 |
+
|
111 |
+
# Dropdown for satellite category selection
|
112 |
+
satellite_categories = {
|
113 |
+
'Starlink Generation 1': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=starlink&FORMAT=tle',
|
114 |
+
'Starlink Generation 2': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=starlink&FORMAT=tle&VERSION=2',
|
115 |
+
'OneWeb': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=oneweb&FORMAT=tle',
|
116 |
+
'Kuiper': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=kuiper&FORMAT=tle',
|
117 |
+
'Custom Satellite': None
|
118 |
+
}
|
119 |
+
satellite_type = st.selectbox("Select or Search Satellite", list(satellite_categories.keys()))
|
120 |
+
|
121 |
+
custom_tle = None
|
122 |
+
satellite_url = satellite_categories[satellite_type]
|
123 |
+
|
124 |
+
# If custom satellite is selected, ask for satellite name or NORAD ID
|
125 |
+
if satellite_type == 'Custom Satellite':
|
126 |
+
satellite_name_or_id = st.text_input("Enter Satellite Name or NORAD ID:")
|
127 |
+
if satellite_name_or_id:
|
128 |
+
custom_tle = fetch_custom_tle(satellite_name_or_id)
|
129 |
+
|
130 |
+
# Detect user's local time zone
|
131 |
+
local_timezone = datetime.now().astimezone().tzinfo
|
132 |
+
|
133 |
+
# Coordinates and time input
|
134 |
+
with st.container():
|
135 |
+
col1, col2 = st.columns([2, 1])
|
136 |
+
with col1:
|
137 |
+
use_address = st.checkbox("Use Address to Set Location", value=False)
|
138 |
+
if use_address:
|
139 |
+
address = st.text_input("Enter Your Address:")
|
140 |
+
if address:
|
141 |
+
# Add User-Agent to Nominatim geocoder
|
142 |
+
geolocator = Nominatim(user_agent="satellite-observation-app")
|
143 |
+
try:
|
144 |
+
location = geolocator.geocode(address, timeout=10)
|
145 |
+
if location:
|
146 |
+
latitude = location.latitude
|
147 |
+
longitude = location.longitude
|
148 |
+
else:
|
149 |
+
st.write("Could not find the location. Please enter a valid address.")
|
150 |
+
except Exception as e:
|
151 |
+
st.error(f"Error fetching location data: {str(e)}")
|
152 |
+
else:
|
153 |
+
st.write("Select your location on the map:")
|
154 |
+
default_location = [37.7749, -122.4194]
|
155 |
+
map_display = folium.Map(location=default_location, zoom_start=2)
|
156 |
+
folium.Marker(default_location, tooltip="Default Location").add_to(map_display)
|
157 |
+
map_data = st_folium(map_display, width=350, height=300)
|
158 |
+
if map_data and 'last_clicked' in map_data and map_data['last_clicked']:
|
159 |
+
latitude = map_data['last_clicked']['lat']
|
160 |
+
longitude = map_data['last_clicked']['lng']
|
161 |
+
|
162 |
+
with col2:
|
163 |
+
latitude = st.text_input("Latitude:", value=str(latitude) if 'latitude' in locals() else "")
|
164 |
+
longitude = st.text_input("Longitude:", value=str(longitude) if 'longitude' in locals() else "")
|
165 |
+
|
166 |
+
# Determine time zone from GPS coordinates
|
167 |
+
if latitude and longitude:
|
168 |
+
try:
|
169 |
+
tf = TimezoneFinder()
|
170 |
+
timezone_str = tf.timezone_at(lng=float(longitude), lat=float(latitude))
|
171 |
+
timezone = pytz.timezone(timezone_str)
|
172 |
+
# Display abbreviated timezone
|
173 |
+
current_time = datetime.now()
|
174 |
+
timezone_abbr = get_abbreviated_timezone(timezone_str, current_time)
|
175 |
+
st.write(f"Detected Time Zone: {timezone_str} ({timezone_abbr})")
|
176 |
+
except Exception as e:
|
177 |
+
st.error(f"Error determining timezone: {str(e)}")
|
178 |
+
timezone = local_timezone
|
179 |
+
else:
|
180 |
+
timezone = local_timezone
|
181 |
+
timezone_abbr = timezone.tzname(datetime.now())
|
182 |
+
st.write(f"Using local time zone: {timezone} ({timezone_abbr})")
|
183 |
+
|
184 |
+
start_date_local = st.date_input("Start Date (Local Time):")
|
185 |
+
start_time_local = st.time_input("Start Time (Local Time):")
|
186 |
+
end_time_local = st.time_input("End Time (Local Time):")
|
187 |
+
|
188 |
+
# Convert local time to UTC
|
189 |
+
start_datetime_local = datetime.combine(start_date_local, start_time_local)
|
190 |
+
end_datetime_local = datetime.combine(start_date_local, end_time_local)
|
191 |
+
start_datetime_utc = start_datetime_local.astimezone(pytz.utc)
|
192 |
+
end_datetime_utc = end_datetime_local.astimezone(pytz.utc)
|
193 |
+
|
194 |
+
st.write(f"Start Time in UTC: {start_datetime_utc.strftime('%Y-%m-%d %H:%M:%S')}")
|
195 |
+
st.write(f"End Time in UTC: {end_datetime_utc.strftime('%Y-%m-%d %H:%M:%S')}")
|
196 |
+
|
197 |
+
# Compute button
|
198 |
+
compute_button = st.button("Compute Satellite Positions")
|
199 |
+
|
200 |
+
# Align the table with the button
|
201 |
+
if compute_button:
|
202 |
+
if latitude and longitude and start_date_local:
|
203 |
+
ephemeris_df = compute_ephemeris(
|
204 |
+
satellite_url, float(latitude), float(longitude),
|
205 |
+
start_datetime_utc.strftime('%Y-%m-%d'),
|
206 |
+
start_datetime_utc.time(), end_datetime_utc.time(),
|
207 |
+
custom_tle
|
208 |
+
)
|
209 |
+
if not ephemeris_df.empty:
|
210 |
+
# Display the dataframe with use_container_width to match the app width
|
211 |
+
st.dataframe(ephemeris_df, use_container_width=True)
|
212 |
+
else:
|
213 |
+
st.write("No visible satellites found for the specified time and location.")
|
214 |
+
else:
|
215 |
+
st.write("Please fill in all fields (latitude, longitude, and observation time).")
|
216 |
+
|
217 |
+
if __name__ == "__main__":
|
218 |
+
main()
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit
|
2 |
+
skyfield
|
3 |
+
geopy
|
4 |
+
pandas
|
5 |
+
numpy
|
6 |
+
pytz
|
7 |
+
timezonefinder
|
8 |
+
requests
|
9 |
+
folium
|
utils/satellite_utils.py
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import streamlit as st
|
3 |
+
from skyfield.api import Topos, load, EarthSatellite
|
4 |
+
import requests
|
5 |
+
import pandas as pd
|
6 |
+
import numpy as np
|
7 |
+
from datetime import datetime
|
8 |
+
|
9 |
+
def get_cardinal_direction(azimuth_degrees):
|
10 |
+
"""
|
11 |
+
Converts azimuth degrees to cardinal direction.
|
12 |
+
"""
|
13 |
+
directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']
|
14 |
+
index = int((azimuth_degrees + 22.5) // 45)
|
15 |
+
return directions[index % 8]
|
16 |
+
|
17 |
+
def compute_ephemeris(satellite_url, latitude, longitude, start_date_utc, start_time, end_time, custom_tle=None):
|
18 |
+
"""
|
19 |
+
Computes the ephemeris for the satellite.
|
20 |
+
"""
|
21 |
+
try:
|
22 |
+
ts = load.timescale()
|
23 |
+
year, month, day = map(int, start_date_utc.split('-'))
|
24 |
+
observer = Topos(latitude, longitude)
|
25 |
+
start_hour, start_minute = start_time.hour, start_time.minute
|
26 |
+
end_hour, end_minute = end_time.hour, end_time.minute
|
27 |
+
start_datetime = datetime(year, month, day, start_hour, start_minute)
|
28 |
+
end_datetime = datetime(year, month, day, end_hour, end_minute)
|
29 |
+
total_minutes = int((end_datetime - start_datetime).total_seconds() // 60)
|
30 |
+
times = ts.utc(year, month, day, [start_hour] * total_minutes, np.arange(start_minute, start_minute + total_minutes))
|
31 |
+
|
32 |
+
# Fetch TLE data for custom satellite or use predefined URL
|
33 |
+
if custom_tle:
|
34 |
+
st.write("Using custom TLE data for satellite computation.")
|
35 |
+
st.write(f"Custom TLE Data:\nName: {custom_tle[0]}\nTLE Line 1: {custom_tle[1]}\nTLE Line 2: {custom_tle[2]}")
|
36 |
+
satellites = [EarthSatellite(custom_tle[1], custom_tle[2], custom_tle[0], ts)]
|
37 |
+
else:
|
38 |
+
r = requests.get(satellite_url)
|
39 |
+
with open('satellite_data.txt', 'wb') as f:
|
40 |
+
f.write(r.content)
|
41 |
+
satellites = load.tle_file('satellite_data.txt')
|
42 |
+
|
43 |
+
# Prepare the ephemeris data
|
44 |
+
ephemeris_data = []
|
45 |
+
for satellite in satellites:
|
46 |
+
difference = satellite - observer
|
47 |
+
for ti in times:
|
48 |
+
topocentric = difference.at(ti)
|
49 |
+
ra, dec, distance = topocentric.radec()
|
50 |
+
alt, az, distance = topocentric.altaz()
|
51 |
+
st.write(f"Checking time {ti.utc_iso()} - Altitude: {alt.degrees:.2f} degrees") # Debugging statement
|
52 |
+
|
53 |
+
if alt.degrees > 0: # Check if the satellite is above the horizon
|
54 |
+
azimuth_degrees = az.degrees
|
55 |
+
cardinal_direction = get_cardinal_direction(azimuth_degrees)
|
56 |
+
ephemeris_data.append({
|
57 |
+
"Date (UTC)": ti.utc_strftime('%Y-%m-%d %H:%M:%S'),
|
58 |
+
"R.A.": str(ra),
|
59 |
+
"Dec": str(dec),
|
60 |
+
"Altitude": f"{alt.degrees:.2f}°",
|
61 |
+
"Azimuth": f"{azimuth_degrees:.2f}° ({cardinal_direction})"
|
62 |
+
})
|
63 |
+
|
64 |
+
# Convert ephemeris data to DataFrame for display
|
65 |
+
ephemeris_df = pd.DataFrame(ephemeris_data)
|
66 |
+
if not custom_tle:
|
67 |
+
os.remove('satellite_data.txt')
|
68 |
+
return ephemeris_df
|
69 |
+
except Exception as e:
|
70 |
+
st.error(f"Error computing ephemeris: {str(e)}")
|
71 |
+
return pd.DataFrame() # Return an empty DataFrame on error
|
72 |
+
|
73 |
+
def fetch_custom_tle(satellite_name_or_id):
|
74 |
+
"""
|
75 |
+
Fetches TLE data for a custom satellite by name or NORAD ID.
|
76 |
+
"""
|
77 |
+
try:
|
78 |
+
if satellite_name_or_id.isdigit():
|
79 |
+
celestrak_url = f'https://celestrak.org/NORAD/elements/gp.php?CATNR={satellite_name_or_id}'
|
80 |
+
else:
|
81 |
+
celestrak_url = f'https://celestrak.org/NORAD/elements/gp.php?NAME={satellite_name_or_id}&FORMAT=TLE'
|
82 |
+
|
83 |
+
with st.spinner("Fetching TLE data..."):
|
84 |
+
response = requests.get(celestrak_url)
|
85 |
+
|
86 |
+
if response.status_code == 200 and len(response.text.splitlines()) >= 2:
|
87 |
+
lines = response.text.splitlines()
|
88 |
+
st.success("TLE data fetched successfully!")
|
89 |
+
st.text(f"Name: {lines[0].strip()}\nTLE Line 1: {lines[1].strip()}\nTLE Line 2: {lines[2].strip()}")
|
90 |
+
return lines[0].strip(), lines[1].strip(), lines[2].strip() # Name, TLE line 1, TLE line 2
|
91 |
+
else:
|
92 |
+
st.error("TLE data could not be fetched. Please check the satellite name or NORAD ID.")
|
93 |
+
return None
|
94 |
+
except Exception as e:
|
95 |
+
st.error(f"Error fetching TLE data: {str(e)}")
|
96 |
+
return None
|
utils/timezone_utils.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pytz
|
3 |
+
|
4 |
+
def get_abbreviated_timezone(timezone_str, dt):
|
5 |
+
"""
|
6 |
+
Converts the timezone string to an abbreviated format.
|
7 |
+
"""
|
8 |
+
try:
|
9 |
+
tz = pytz.timezone(timezone_str)
|
10 |
+
return dt.astimezone(tz).tzname()
|
11 |
+
except Exception as e:
|
12 |
+
st.error(f"Error converting timezone: {str(e)}")
|
13 |
+
return timezone_str # Return the original string if conversion fails
|