File size: 10,536 Bytes
fcec389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import os
import streamlit as st
from skyfield.api import Topos, load, EarthSatellite
import requests
import pandas as pd
from geopy.geocoders import Nominatim
from streamlit_folium import st_folium
import folium
import numpy as np
from datetime import datetime
import pytz
from timezonefinder import TimezoneFinder

# Function to convert azimuth degrees to cardinal direction
def get_cardinal_direction(azimuth_degrees):
    directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']
    index = int((azimuth_degrees + 22.5) // 45)
    return directions[index % 8]

# Function to get timezone in an abbreviated format
def get_abbreviated_timezone(timezone_str, dt):
    try:
        tz = pytz.timezone(timezone_str)
        return dt.astimezone(tz).tzname()
    except Exception as e:
        st.error(f"Error converting timezone: {str(e)}")
        return timezone_str  # Return the original string if conversion fails

# Generalized function to compute satellite ephemeris
def compute_ephemeris(satellite_url, latitude, longitude, start_date_utc, start_time, end_time, custom_tle=None):
    try:
        ts = load.timescale()
        year, month, day = map(int, start_date_utc.split('-'))
        observer = Topos(latitude, longitude)
        start_hour, start_minute = start_time.hour, start_time.minute
        end_hour, end_minute = end_time.hour, end_time.minute
        start_datetime = datetime(year, month, day, start_hour, start_minute)
        end_datetime = datetime(year, month, day, end_hour, end_minute)
        total_minutes = int((end_datetime - start_datetime).total_seconds() // 60)
        times = ts.utc(year, month, day, [start_hour] * total_minutes, np.arange(start_minute, start_minute + total_minutes))

        # Fetch TLE data for custom satellite or use predefined URL
        if custom_tle:
            st.write("Using custom TLE data for satellite computation.")
            satellites = [EarthSatellite(custom_tle[1], custom_tle[2], custom_tle[0], ts)]
        else:
            r = requests.get(satellite_url)
            with open('satellite_data.txt', 'wb') as f:
                f.write(r.content)
            satellites = load.tle_file('satellite_data.txt')

        # Prepare the ephemeris data
        ephemeris_data = []
        for satellite in satellites:
            difference = satellite - observer
            for ti in times:
                topocentric = difference.at(ti)
                ra, dec, distance = topocentric.radec()
                alt, az, distance = topocentric.altaz()
                if alt.degrees > 0:  # Check if the satellite is above the horizon
                    # Convert Angle objects to string representations and add cardinal direction
                    azimuth_degrees = az.degrees
                    cardinal_direction = get_cardinal_direction(azimuth_degrees)
                    ephemeris_data.append({
                        "Date (UTC)": ti.utc_strftime('%Y-%m-%d %H:%M:%S'),
                        "R.A.": str(ra),
                        "Dec": str(dec),
                        "Altitude": f"{alt.degrees:.2f}°",
                        "Azimuth": f"{azimuth_degrees:.2f}° ({cardinal_direction})"
                    })

        # Convert ephemeris data to DataFrame for display
        ephemeris_df = pd.DataFrame(ephemeris_data)
        if not custom_tle:
            os.remove('satellite_data.txt')
        return ephemeris_df
    except Exception as e:
        st.error(f"Error computing ephemeris: {str(e)}")
        return pd.DataFrame()  # Return an empty DataFrame on error

# Fetch TLE data for custom satellite by name or NORAD ID
def fetch_custom_tle(satellite_name_or_id):
    try:
        # Correct URL formats for NORAD ID and satellite name
        if satellite_name_or_id.isdigit():
            # Fetch TLE by NORAD ID
            celestrak_url = f'https://celestrak.org/NORAD/elements/gp.php?CATNR={satellite_name_or_id}'
        else:
            # Fetch TLE by satellite name
            celestrak_url = f'https://celestrak.org/NORAD/elements/gp.php?NAME={satellite_name_or_id}&FORMAT=TLE'

        with st.spinner("Fetching TLE data..."):
            response = requests.get(celestrak_url)

        if response.status_code == 200 and len(response.text.splitlines()) >= 2:
            lines = response.text.splitlines()
            st.success("TLE data fetched successfully!")
            st.text(f"Name: {lines[0].strip()}\nTLE Line 1: {lines[1].strip()}\nTLE Line 2: {lines[2].strip()}")
            return lines[0].strip(), lines[1].strip(), lines[2].strip()  # Name, TLE line 1, TLE line 2
        else:
            st.error("TLE data could not be fetched. Please check the satellite name or NORAD ID.")
            return None
    except Exception as e:
        st.error(f"Error fetching TLE data: {str(e)}")
        return None

# Streamlit app
def main():
    st.title("Satellites Ephemeris Calculator")

    # Dropdown for satellite category selection
    satellite_categories = {
        'Starlink Generation 1': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=starlink&FORMAT=tle',
        'Starlink Generation 2': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=starlink&FORMAT=tle&VERSION=2',
        'OneWeb': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=oneweb&FORMAT=tle',
        'Kuiper': 'https://celestrak.org/NORAD/elements/supplemental/sup-gp.php?FILE=kuiper&FORMAT=tle',
        'Custom Satellite': None
    }
    satellite_type = st.selectbox("Select or Search Satellite", list(satellite_categories.keys()))

    custom_tle = None
    satellite_url = satellite_categories[satellite_type]

    # If custom satellite is selected, ask for satellite name or NORAD ID
    if satellite_type == 'Custom Satellite':
        satellite_name_or_id = st.text_input("Enter Satellite Name or NORAD ID:")
        if satellite_name_or_id:
            custom_tle = fetch_custom_tle(satellite_name_or_id)

    # Detect user's local time zone
    local_timezone = datetime.now().astimezone().tzinfo

    # Coordinates and time input
    with st.container():
        col1, col2 = st.columns([2, 1])
        with col1:
            use_address = st.checkbox("Use Address to Set Location", value=False)
            if use_address:
                address = st.text_input("Enter Your Address:")
                if address:
                    # Add User-Agent to Nominatim geocoder
                    geolocator = Nominatim(user_agent="satellite-observation-app")
                    try:
                        location = geolocator.geocode(address, timeout=10)
                        if location:
                            latitude = location.latitude
                            longitude = location.longitude
                        else:
                            st.write("Could not find the location. Please enter a valid address.")
                    except Exception as e:
                        st.error(f"Error fetching location data: {str(e)}")
            else:
                st.write("Select your location on the map:")
                default_location = [37.7749, -122.4194]
                map_display = folium.Map(location=default_location, zoom_start=2)
                folium.Marker(default_location, tooltip="Default Location").add_to(map_display)
                map_data = st_folium(map_display, width=350, height=300)
                if map_data and 'last_clicked' in map_data and map_data['last_clicked']:
                    latitude = map_data['last_clicked']['lat']
                    longitude = map_data['last_clicked']['lng']

        with col2:
            latitude = st.text_input("Latitude:", value=str(latitude) if 'latitude' in locals() else "")
            longitude = st.text_input("Longitude:", value=str(longitude) if 'longitude' in locals() else "")

            # Determine time zone from GPS coordinates
            if latitude and longitude:
                try:
                    tf = TimezoneFinder()
                    timezone_str = tf.timezone_at(lng=float(longitude), lat=float(latitude))
                    timezone = pytz.timezone(timezone_str)
                    # Display abbreviated timezone
                    current_time = datetime.now()
                    timezone_abbr = get_abbreviated_timezone(timezone_str, current_time)
                    st.write(f"Detected Time Zone: {timezone_str} ({timezone_abbr})")
                except Exception as e:
                    st.error(f"Error determining timezone: {str(e)}")
                    timezone = local_timezone
            else:
                timezone = local_timezone
                timezone_abbr = timezone.tzname(datetime.now())
                st.write(f"Using local time zone: {timezone} ({timezone_abbr})")

            start_date_local = st.date_input("Start Date (Local Time):")
            start_time_local = st.time_input("Start Time (Local Time):")
            end_time_local = st.time_input("End Time (Local Time):")

            # Convert local time to UTC
            start_datetime_local = datetime.combine(start_date_local, start_time_local)
            end_datetime_local = datetime.combine(start_date_local, end_time_local)
            start_datetime_utc = start_datetime_local.astimezone(pytz.utc)
            end_datetime_utc = end_datetime_local.astimezone(pytz.utc)

            st.write(f"Start Time in UTC: {start_datetime_utc.strftime('%Y-%m-%d %H:%M:%S')}")
            st.write(f"End Time in UTC: {end_datetime_utc.strftime('%Y-%m-%d %H:%M:%S')}")

            # Compute button
            compute_button = st.button("Compute Satellite Positions")

    # Align the table with the button
    if compute_button:
        if latitude and longitude and start_date_local:
            ephemeris_df = compute_ephemeris(
                satellite_url, float(latitude), float(longitude),
                start_datetime_utc.strftime('%Y-%m-%d'),
                start_datetime_utc.time(), end_datetime_utc.time(),
                custom_tle
            )
            if not ephemeris_df.empty:
                # Display the dataframe with use_container_width to match the app width
                st.dataframe(ephemeris_df, use_container_width=True)
            else:
                st.write("No visible satellites found for the specified time and location.")
        else:
            st.write("Please fill in all fields (latitude, longitude, and observation time).")

if __name__ == "__main__":
    main()