diff --git a/app.py b/app.py index 231f6e7..9226c2b 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ import streamlit as st -from datetime import date, datetime +from datetime import date from streamlit_js_eval import streamlit_js_eval # For geolocation from src.data_acquisition import get_solunar_data from src.data_processing import process_solunar_data @@ -33,7 +33,10 @@ st.sidebar.header("Input Parameters") # Checkbox to use current location -use_current_location = st.sidebar.checkbox("Use my current location", value=st.session_state.use_current_location) +use_current_location = st.sidebar.checkbox( + "Use my current location", + value=st.session_state.use_current_location +) st.session_state.use_current_location = use_current_location # Button to fetch data @@ -65,28 +68,53 @@ st.session_state.latitude = loc["latitude"] st.session_state.longitude = loc["longitude"] st.session_state.loc = loc - st.sidebar.success(f"Location acquired: ({st.session_state.latitude:.6f}, {st.session_state.longitude:.6f})") + st.sidebar.success( + f"Location acquired: " + f"({st.session_state.latitude:.6f}, " + f"{st.session_state.longitude:.6f})" + ) else: - st.sidebar.error("Unable to retrieve location. Please allow location access.") + st.sidebar.error( + "Unable to retrieve location. " + "Please allow location access." + ) else: - st.sidebar.warning("Waiting for location... Make sure to allow location access.") + st.sidebar.warning( + "Waiting for location... " + "Make sure to allow location access." + ) else: # Manual input - st.session_state.latitude = st.sidebar.number_input("Latitude", value=st.session_state.latitude, format="%.6f") - st.session_state.longitude = st.sidebar.number_input("Longitude", value=st.session_state.longitude, format="%.6f") + st.session_state.latitude = st.sidebar.number_input( + "Latitude", value=st.session_state.latitude, format="%.6f" + ) + st.session_state.longitude = st.sidebar.number_input( + "Longitude", value=st.session_state.longitude, format="%.6f" + ) selected_date = st.sidebar.date_input("Date", value=date.today()) if fetch_data: with st.spinner('Fetching data...'): - raw_data = get_solunar_data(st.session_state.latitude, st.session_state.longitude, selected_date) + raw_data = get_solunar_data( + st.session_state.latitude, + st.session_state.longitude, + selected_date + ) if raw_data is None: st.error("Failed to retrieve data. Please try again later.") else: - date_str = selected_date.strftime('%Y-%m-%d') # Convert date to string - st.session_state.solunar_data = process_solunar_data(raw_data, date_str) - major_times, minor_times = calculate_major_minor_times(st.session_state.solunar_data) - st.session_state.recommendations = generate_recommendations(major_times, minor_times) + # Convert date to string + date_str = selected_date.strftime('%Y-%m-%d') + st.session_state.solunar_data = process_solunar_data( + raw_data, date_str + ) + major_times, minor_times = calculate_major_minor_times( + st.session_state.solunar_data + ) + st.session_state.recommendations = generate_recommendations( + major_times, minor_times + ) st.session_state.data_fetched = True # Display Results if Data Has Been Fetched @@ -95,22 +123,40 @@ # Display Recommendations st.header("Recommended Fishing Times") - for rec in st.session_state.recommendations: - start_time = rec['start'].strftime('%I:%M %p') - end_time = rec['end'].strftime('%I:%M %p') - st.write(f"**{rec['type']} Period:** {start_time} - {end_time}") + if st.session_state.recommendations: + for rec in st.session_state.recommendations: + start_time = rec['start'].strftime('%I:%M %p') + end_time = rec['end'].strftime('%I:%M %p') + st.write(f"**{rec['type']} Period:** {start_time} - {end_time}") + else: + st.warning( + "No major or minor times could be calculated. " + "This may be due to missing moonrise/moonset data." + ) # Display Additional Information st.header("Additional Information") - st.write(f"**Location:** {st.session_state.latitude:.6f}, {st.session_state.longitude:.6f}") - st.write(f"**Sunrise:** {st.session_state.solunar_data['sunrise'].strftime('%I:%M %p')}") - st.write(f"**Sunset:** {st.session_state.solunar_data['sunset'].strftime('%I:%M %p')}") - st.write(f"**Moon Phase:** {st.session_state.solunar_data['moon_phase']}") + st.write( + f"**Location:** {st.session_state.latitude:.6f}, " + f"{st.session_state.longitude:.6f}" + ) + sunrise_str = st.session_state.solunar_data['sunrise'].strftime('%I:%M %p') + st.write(f"**Sunrise:** {sunrise_str}") + sunset_str = st.session_state.solunar_data['sunset'].strftime('%I:%M %p') + st.write(f"**Sunset:** {sunset_str}") + moon_phase = st.session_state.solunar_data['moon_phase'] + st.write(f"**Moon Phase:** {moon_phase}") # Display Map st.header("Map") - m = folium.Map(location=[st.session_state.latitude, st.session_state.longitude], zoom_start=12) - folium.Marker([st.session_state.latitude, st.session_state.longitude], tooltip="Your Location").add_to(m) + m = folium.Map( + location=[st.session_state.latitude, st.session_state.longitude], + zoom_start=12 + ) + folium.Marker( + [st.session_state.latitude, st.session_state.longitude], + tooltip="Your Location" + ).add_to(m) st_folium(m, width=700, height=500) else: st.info("Please enter parameters and click 'Get Fishing Times'") diff --git a/src/data_acquisition.py b/src/data_acquisition.py index 38fb5dc..99d5ae7 100644 --- a/src/data_acquisition.py +++ b/src/data_acquisition.py @@ -2,6 +2,7 @@ import requests import streamlit as st + def get_api_key(): # First, try to get the API_KEY from st.secrets try: @@ -27,20 +28,28 @@ def get_api_key(): pass # If all else fails, display an error and stop the app - st.error("API_KEY not found. Please set it in Streamlit Secrets, as an environment variable, or in config.toml.") + st.error( + "API_KEY not found. Please set it in Streamlit Secrets, " + "as an environment variable, or in config.toml." + ) st.stop() + API_KEY = get_api_key() + def get_solunar_data(lat, lon, date): - url = 'https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/' - date_str = date.strftime('%Y-%m-%d') + base_url = ( + 'https://weather.visualcrossing.com/' + 'VisualCrossingWebServices/rest/services/timeline/' + ) + url = f"{base_url}{lat},{lon}/{date.strftime('%Y-%m-%d')}" params = { 'key': API_KEY, 'include': 'hours', 'elements': 'datetime,sunrise,sunset,moonphase,moonrise,moonset', } - response = requests.get(f"{url}{lat},{lon}/{date_str}", params=params) + response = requests.get(url, params=params) if response.status_code == 200: try: data = response.json() @@ -50,6 +59,9 @@ def get_solunar_data(lat, lon, date): st.write("Response content:", response.text) return None else: - st.error(f"Error fetching data: {response.status_code} - {response.reason}") + st.error( + f"Error fetching data: {response.status_code} - " + f"{response.reason}" + ) st.write("Response content:", response.text) return None diff --git a/src/data_processing.py b/src/data_processing.py index a9afe61..f76b918 100644 --- a/src/data_processing.py +++ b/src/data_processing.py @@ -1,5 +1,6 @@ from datetime import datetime + def process_solunar_data(data, date_str): # Extract necessary data try: @@ -16,16 +17,22 @@ def process_solunar_data(data, date_str): datetime_format = f'{date_format} {time_format}' # Combine date and time - sunrise_dt = datetime.strptime(f"{date_str} {sunrise}", datetime_format) + sunrise_dt = datetime.strptime( + f"{date_str} {sunrise}", datetime_format + ) sunset_dt = datetime.strptime(f"{date_str} {sunset}", datetime_format) if moonrise: - moonrise_dt = datetime.strptime(f"{date_str} {moonrise}", datetime_format) + moonrise_dt = datetime.strptime( + f"{date_str} {moonrise}", datetime_format + ) else: moonrise_dt = None if moonset: - moonset_dt = datetime.strptime(f"{date_str} {moonset}", datetime_format) + moonset_dt = datetime.strptime( + f"{date_str} {moonset}", datetime_format + ) else: moonset_dt = None diff --git a/src/solunar_calculations.py b/src/solunar_calculations.py index ea15288..f3476e9 100644 --- a/src/solunar_calculations.py +++ b/src/solunar_calculations.py @@ -1,4 +1,6 @@ from datetime import timedelta +import math + def calculate_major_minor_times(solunar_data): major_times = [] @@ -7,28 +9,101 @@ def calculate_major_minor_times(solunar_data): if not solunar_data: return major_times, minor_times - # Major times occur when the moon is overhead and underfoot - # Approximate times using moonrise and moonset - if solunar_data['moonrise']: + # Try to use moonrise and moonset data first + if solunar_data.get('moonrise') and solunar_data.get('moonset'): moonrise = solunar_data['moonrise'] + moonset = solunar_data['moonset'] + + # Major periods: moon overhead (moonrise) & underfoot (moonset) major_times.append({ 'start': moonrise - timedelta(hours=1), 'end': moonrise + timedelta(hours=1) }) - - if solunar_data['moonset']: - moonset = solunar_data['moonset'] major_times.append({ 'start': moonset - timedelta(hours=1), 'end': moonset + timedelta(hours=1) }) - # Minor Periods occur halfway between major periods - if len(major_times) == 2: - transit_time = major_times[0]['end'] + (major_times[1]['start'] - major_times[0]['end']) / 2 + # Calculate minor periods (halfway between major periods) + # First minor: between moonset and next moonrise + if moonset < moonrise: + transit_time = moonset + (moonrise - moonset) / 2 + else: + # If moonrise is before moonset, calculate for next day's cycle + transit_time = moonrise + timedelta(hours=6) + minor_times.append({ 'start': transit_time - timedelta(minutes=30), 'end': transit_time + timedelta(minutes=30) }) + # Second minor: between moonrise and moonset + if moonset > moonrise: + transit_time2 = moonrise + (moonset - moonrise) / 2 + else: + transit_time2 = moonset + timedelta(hours=6) + minor_times.append({ + 'start': transit_time2 - timedelta(minutes=30), + 'end': transit_time2 + timedelta(minutes=30) + }) + + else: + # Fallback calculation when moonrise/moonset data is not available + # Use standard solunar theory approximations based on moon phase + moon_phase = solunar_data.get('moon_phase', 0.5) + sunrise = solunar_data.get('sunrise') + sunset = solunar_data.get('sunset') + + if sunrise and sunset: + # Calculate approximate moon overhead times based on moon phase + # Full moon (0.5) rises at sunset and sets at sunrise + # New moon (0.0 or 1.0) rises at sunrise and sets at sunset + + # Calculate approximate moonrise time based on phase + # This is a simplified calculation + moonrise_offset = 12 * moon_phase # hours after sunrise + approx_moonrise = sunrise + timedelta(hours=moonrise_offset) + + # Moon overhead is approximately 6 hours after moonrise + moon_overhead = approx_moonrise + timedelta(hours=6) + # Moon underfoot is 12 hours after overhead + moon_underfoot = moon_overhead + timedelta(hours=12) + + # Adjust times to be within the day + if moon_overhead.date() == sunrise.date(): + major_times.append({ + 'start': moon_overhead - timedelta(hours=1), + 'end': moon_overhead + timedelta(hours=1) + }) + + if moon_underfoot.date() == sunrise.date(): + major_times.append({ + 'start': moon_underfoot - timedelta(hours=1), + 'end': moon_underfoot + timedelta(hours=1) + }) + + # Calculate minor periods as midpoints + if len(major_times) >= 1: + # First minor is 6 hours before first major + minor1 = major_times[0]['start'] - timedelta(hours=5) + if minor1.date() == sunrise.date() and minor1 > sunrise: + minor_times.append({ + 'start': minor1 - timedelta(minutes=30), + 'end': minor1 + timedelta(minutes=30) + }) + + # Second minor is 6 hours after first major + minor2 = major_times[0]['end'] + timedelta(hours=5) + sunset_plus_2 = sunset + timedelta(hours=2) + if (minor2.date() == sunrise.date() and + minor2 < sunset_plus_2): + minor_times.append({ + 'start': minor2 - timedelta(minutes=30), + 'end': minor2 + timedelta(minutes=30) + }) + + # Sort all times by start time + major_times.sort(key=lambda x: x['start']) + minor_times.sort(key=lambda x: x['start']) + return major_times, minor_times