Compare commits
No commits in common. "ac16bb9dab178325684631ea27728734856e019b" and "37e74404d55fa94e37b1b06d0d2cd4a02fd47855" have entirely different histories.
ac16bb9dab
...
37e74404d5
29
CLAUDE.md
29
CLAUDE.md
|
@ -1,29 +0,0 @@
|
||||||
# Development Guidelines
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
This is a personal agenda web application built with Flask that tracks various events and important dates:
|
|
||||||
- Events: birthdays, holidays, travel itineraries, conferences, waste collection schedules
|
|
||||||
- Space launches, meteor showers, astronomical events
|
|
||||||
- Financial information (FX rates, stock market)
|
|
||||||
- UK-specific features (holidays, waste collection, railway schedules)
|
|
||||||
- Authentication via UniAuth
|
|
||||||
- Frontend uses Bootstrap 5, Leaflet for maps, FullCalendar for calendar views
|
|
||||||
|
|
||||||
## Python Environment
|
|
||||||
- Always use `python3` directly, never `python`
|
|
||||||
- Run `black` code formatter after creating or modifying Python files
|
|
||||||
- Main entry point: `python3 web_view.py` (Flask app on port 5000)
|
|
||||||
- Tests: Use `pytest` (tests in `/tests/` directory)
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
- `agenda/` - Main Python package with modules for different event types
|
|
||||||
- `web_view.py` - Flask web application entry point
|
|
||||||
- `templates/` - Jinja2 HTML templates
|
|
||||||
- `static/` - CSS, JS, and frontend assets
|
|
||||||
- `config/` - Configuration files
|
|
||||||
- `personal-data/` - User's personal data (not in git)
|
|
||||||
|
|
||||||
## Git Workflow
|
|
||||||
- Avoid committing unrelated untracked files (e.g., `node_modules/`, build artifacts)
|
|
||||||
- Only commit relevant project files
|
|
||||||
- Personal data directory (`personal-data/`) is excluded from git
|
|
|
@ -1,267 +0,0 @@
|
||||||
"""Meteor shower data calculations."""
|
|
||||||
|
|
||||||
import typing
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import ephem # type: ignore
|
|
||||||
|
|
||||||
MeteorShower = dict[str, typing.Any]
|
|
||||||
|
|
||||||
|
|
||||||
# Meteor shower definitions with parent comet orbital elements
|
|
||||||
METEOR_SHOWERS = {
|
|
||||||
"Quadrantids": {
|
|
||||||
"name": "Quadrantids",
|
|
||||||
"radiant_ra": "15h20m", # Right ascension at peak
|
|
||||||
"radiant_dec": "+49.5°", # Declination at peak
|
|
||||||
"peak_solar_longitude": 283.16, # Solar longitude at peak
|
|
||||||
"activity_start": 283.16 - 10, # Activity period
|
|
||||||
"activity_end": 283.16 + 10,
|
|
||||||
"rate_max": 120,
|
|
||||||
"rate_min": 50,
|
|
||||||
"parent_body": "2003 EH1",
|
|
||||||
"visibility": "Northern Hemisphere",
|
|
||||||
"description": "The year kicks off with the Quadrantids, known for their brief but intense peak lasting only about 4 hours.",
|
|
||||||
"velocity_kms": 41,
|
|
||||||
},
|
|
||||||
"Lyrids": {
|
|
||||||
"name": "Lyrids",
|
|
||||||
"radiant_ra": "18h04m",
|
|
||||||
"radiant_dec": "+32.32°",
|
|
||||||
"peak_solar_longitude": 32.32,
|
|
||||||
"activity_start": 32.32 - 10,
|
|
||||||
"activity_end": 32.32 + 10,
|
|
||||||
"rate_max": 18,
|
|
||||||
"rate_min": 10,
|
|
||||||
"parent_body": "C/1861 G1 (Thatcher)",
|
|
||||||
"visibility": "Both hemispheres",
|
|
||||||
"description": "The Lyrids are one of the oldest recorded meteor showers, with observations dating back 2,700 years.",
|
|
||||||
"velocity_kms": 49,
|
|
||||||
},
|
|
||||||
"Eta Aquariids": {
|
|
||||||
"name": "Eta Aquariids",
|
|
||||||
"radiant_ra": "22h32m",
|
|
||||||
"radiant_dec": "-1.0°",
|
|
||||||
"peak_solar_longitude": 45.5,
|
|
||||||
"activity_start": 45.5 - 15,
|
|
||||||
"activity_end": 45.5 + 15,
|
|
||||||
"rate_max": 60,
|
|
||||||
"rate_min": 30,
|
|
||||||
"parent_body": "1P/Halley",
|
|
||||||
"visibility": "Southern Hemisphere (best)",
|
|
||||||
"description": "Created by debris from Halley's Comet, these meteors are fast and often leave glowing trails.",
|
|
||||||
"velocity_kms": 66,
|
|
||||||
},
|
|
||||||
"Perseids": {
|
|
||||||
"name": "Perseids",
|
|
||||||
"radiant_ra": "03h04m",
|
|
||||||
"radiant_dec": "+58.0°",
|
|
||||||
"peak_solar_longitude": 140.0,
|
|
||||||
"activity_start": 140.0 - 15,
|
|
||||||
"activity_end": 140.0 + 15,
|
|
||||||
"rate_max": 100,
|
|
||||||
"rate_min": 50,
|
|
||||||
"parent_body": "109P/Swift-Tuttle",
|
|
||||||
"visibility": "Northern Hemisphere",
|
|
||||||
"description": "One of the most popular meteor showers, viewing conditions vary by year based on moon phase.",
|
|
||||||
"velocity_kms": 59,
|
|
||||||
},
|
|
||||||
"Orionids": {
|
|
||||||
"name": "Orionids",
|
|
||||||
"radiant_ra": "06h20m",
|
|
||||||
"radiant_dec": "+16.0°",
|
|
||||||
"peak_solar_longitude": 208.0,
|
|
||||||
"activity_start": 208.0 - 15,
|
|
||||||
"activity_end": 208.0 + 15,
|
|
||||||
"rate_max": 25,
|
|
||||||
"rate_min": 15,
|
|
||||||
"parent_body": "1P/Halley",
|
|
||||||
"visibility": "Both hemispheres",
|
|
||||||
"description": "Another shower created by Halley's Comet debris, known for their speed and brightness.",
|
|
||||||
"velocity_kms": 66,
|
|
||||||
},
|
|
||||||
"Geminids": {
|
|
||||||
"name": "Geminids",
|
|
||||||
"radiant_ra": "07h28m",
|
|
||||||
"radiant_dec": "+32.0°",
|
|
||||||
"peak_solar_longitude": 262.2,
|
|
||||||
"activity_start": 262.2 - 10,
|
|
||||||
"activity_end": 262.2 + 10,
|
|
||||||
"rate_max": 120,
|
|
||||||
"rate_min": 60,
|
|
||||||
"parent_body": "3200 Phaethon",
|
|
||||||
"visibility": "Both hemispheres",
|
|
||||||
"description": "The best shower of most years with the highest rates. Unusual for being caused by an asteroid rather than a comet.",
|
|
||||||
"velocity_kms": 35,
|
|
||||||
},
|
|
||||||
"Ursids": {
|
|
||||||
"name": "Ursids",
|
|
||||||
"radiant_ra": "14h28m",
|
|
||||||
"radiant_dec": "+75.0°",
|
|
||||||
"peak_solar_longitude": 270.7,
|
|
||||||
"activity_start": 270.7 - 5,
|
|
||||||
"activity_end": 270.7 + 5,
|
|
||||||
"rate_max": 10,
|
|
||||||
"rate_min": 5,
|
|
||||||
"parent_body": "8P/Tuttle",
|
|
||||||
"visibility": "Northern Hemisphere",
|
|
||||||
"description": "A minor shower that closes out the year, best viewed from dark locations away from city lights.",
|
|
||||||
"velocity_kms": 33,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_solar_longitude_date(year: int, target_longitude: float) -> datetime:
|
|
||||||
"""Calculate the date when the Sun reaches a specific longitude for a given year."""
|
|
||||||
# Start from beginning of year
|
|
||||||
start_date = datetime(year, 1, 1)
|
|
||||||
|
|
||||||
# Use PyEphem to calculate solar longitude
|
|
||||||
observer = ephem.Observer()
|
|
||||||
observer.lat = "0" # Equator
|
|
||||||
observer.lon = "0" # Greenwich
|
|
||||||
observer.date = start_date
|
|
||||||
|
|
||||||
sun = ephem.Sun(observer)
|
|
||||||
|
|
||||||
# Search for the date when solar longitude matches target
|
|
||||||
# Solar longitude 0° = Spring Equinox (around March 20)
|
|
||||||
# We need to find when sun reaches the target longitude
|
|
||||||
|
|
||||||
# Approximate: start search from reasonable date based on longitude
|
|
||||||
if target_longitude < 90: # Spring (Mar-Jun)
|
|
||||||
search_start = datetime(year, 3, 1)
|
|
||||||
elif target_longitude < 180: # Summer (Jun-Sep)
|
|
||||||
search_start = datetime(year, 6, 1)
|
|
||||||
elif target_longitude < 270: # Fall (Sep-Dec)
|
|
||||||
search_start = datetime(year, 9, 1)
|
|
||||||
else: # Winter (Dec-Mar)
|
|
||||||
search_start = datetime(year, 12, 1)
|
|
||||||
|
|
||||||
observer.date = search_start
|
|
||||||
|
|
||||||
# Search within a reasonable range (±60 days)
|
|
||||||
for day_offset in range(-60, 61):
|
|
||||||
test_date = search_start + timedelta(days=day_offset)
|
|
||||||
observer.date = test_date
|
|
||||||
sun.compute(observer)
|
|
||||||
|
|
||||||
# Convert ecliptic longitude to degrees
|
|
||||||
sun_longitude = float(sun.hlon) * 180 / ephem.pi
|
|
||||||
|
|
||||||
# Check if we're close to target longitude (within 0.5 degrees)
|
|
||||||
if abs(sun_longitude - target_longitude) < 0.5:
|
|
||||||
return test_date
|
|
||||||
|
|
||||||
# Fallback: return approximation based on solar longitude
|
|
||||||
# Rough approximation: solar longitude increases ~1° per day
|
|
||||||
days_from_equinox = target_longitude
|
|
||||||
equinox_date = datetime(year, 3, 20) # Approximate spring equinox
|
|
||||||
return equinox_date + timedelta(days=days_from_equinox)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_moon_phase(date_obj: datetime) -> tuple[float, str]:
|
|
||||||
"""Calculate moon phase for a given date."""
|
|
||||||
observer = ephem.Observer()
|
|
||||||
observer.date = date_obj
|
|
||||||
|
|
||||||
moon = ephem.Moon(observer)
|
|
||||||
moon.compute(observer)
|
|
||||||
|
|
||||||
# Moon phase (0 = new moon, 0.5 = full moon, 1 = new moon again)
|
|
||||||
phase = moon.phase / 100.0
|
|
||||||
|
|
||||||
# Determine moon phase name and viewing quality
|
|
||||||
if phase < 0.1:
|
|
||||||
phase_name = "New Moon"
|
|
||||||
viewing_quality = "excellent"
|
|
||||||
elif phase < 0.3:
|
|
||||||
phase_name = "Waxing Crescent"
|
|
||||||
viewing_quality = "good"
|
|
||||||
elif phase < 0.7:
|
|
||||||
phase_name = "First Quarter" if phase < 0.5 else "Waxing Gibbous"
|
|
||||||
viewing_quality = "moderate"
|
|
||||||
elif phase < 0.9:
|
|
||||||
phase_name = "Full Moon"
|
|
||||||
viewing_quality = "poor"
|
|
||||||
else:
|
|
||||||
phase_name = "Waning Crescent"
|
|
||||||
viewing_quality = "good"
|
|
||||||
|
|
||||||
return phase, f"{phase_name} ({viewing_quality} viewing)"
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_meteor_shower_data(year: int) -> list[MeteorShower]:
|
|
||||||
"""Calculate meteor shower data for a given year using astronomical calculations."""
|
|
||||||
meteor_data = []
|
|
||||||
|
|
||||||
for shower_id, shower_info in METEOR_SHOWERS.items():
|
|
||||||
# Calculate peak date based on solar longitude
|
|
||||||
peak_date = calculate_solar_longitude_date(
|
|
||||||
year, shower_info["peak_solar_longitude"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate activity period
|
|
||||||
activity_start = calculate_solar_longitude_date(
|
|
||||||
year, shower_info["activity_start"]
|
|
||||||
)
|
|
||||||
activity_end = calculate_solar_longitude_date(year, shower_info["activity_end"])
|
|
||||||
|
|
||||||
# Calculate moon phase at peak
|
|
||||||
moon_illumination, moon_phase_desc = calculate_moon_phase(peak_date)
|
|
||||||
|
|
||||||
# Format dates
|
|
||||||
peak_formatted = peak_date.strftime("%B %d")
|
|
||||||
if peak_date.day != (peak_date + timedelta(days=1)).day:
|
|
||||||
peak_formatted += f"-{(peak_date + timedelta(days=1)).strftime('%d')}"
|
|
||||||
|
|
||||||
active_formatted = (
|
|
||||||
f"{activity_start.strftime('%B %d')} - {activity_end.strftime('%B %d')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine viewing quality based on moon phase
|
|
||||||
viewing_quality = (
|
|
||||||
"excellent"
|
|
||||||
if moon_illumination < 0.3
|
|
||||||
else (
|
|
||||||
"good"
|
|
||||||
if moon_illumination < 0.7
|
|
||||||
else "moderate" if moon_illumination < 0.9 else "poor"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
meteor_shower = {
|
|
||||||
"name": shower_info["name"],
|
|
||||||
"peak": peak_formatted,
|
|
||||||
"active": active_formatted,
|
|
||||||
"rate": f"{shower_info['rate_min']}-{shower_info['rate_max']} meteors per hour",
|
|
||||||
"radiant": shower_info["radiant_ra"].split("h")[0]
|
|
||||||
+ "h "
|
|
||||||
+ shower_info["radiant_dec"],
|
|
||||||
"moon_phase": moon_phase_desc,
|
|
||||||
"visibility": shower_info["visibility"],
|
|
||||||
"description": shower_info["description"],
|
|
||||||
"peak_date": peak_date.strftime("%Y-%m-%d"),
|
|
||||||
"start_date": activity_start.strftime("%Y-%m-%d"),
|
|
||||||
"end_date": activity_end.strftime("%Y-%m-%d"),
|
|
||||||
"rate_min": shower_info["rate_min"],
|
|
||||||
"rate_max": shower_info["rate_max"],
|
|
||||||
"moon_illumination": moon_illumination,
|
|
||||||
"viewing_quality": viewing_quality,
|
|
||||||
"parent_body": shower_info["parent_body"],
|
|
||||||
"velocity_kms": shower_info["velocity_kms"],
|
|
||||||
}
|
|
||||||
|
|
||||||
meteor_data.append(meteor_shower)
|
|
||||||
|
|
||||||
# Sort by peak date
|
|
||||||
meteor_data.sort(key=lambda x: datetime.strptime(x["peak_date"], "%Y-%m-%d"))
|
|
||||||
|
|
||||||
return meteor_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_meteor_data(year: int | None = None) -> list[MeteorShower]:
|
|
||||||
"""Get meteor shower data for a specific year using astronomical calculations."""
|
|
||||||
if year is None:
|
|
||||||
year = datetime.now().year
|
|
||||||
return calculate_meteor_shower_data(year)
|
|
74
web_view.py
74
web_view.py
|
@ -23,7 +23,6 @@ import agenda.data
|
||||||
import agenda.error_mail
|
import agenda.error_mail
|
||||||
import agenda.fx
|
import agenda.fx
|
||||||
import agenda.holidays
|
import agenda.holidays
|
||||||
import agenda.meteors
|
|
||||||
import agenda.stats
|
import agenda.stats
|
||||||
import agenda.thespacedevs
|
import agenda.thespacedevs
|
||||||
import agenda.trip
|
import agenda.trip
|
||||||
|
@ -217,7 +216,78 @@ def launch_list() -> str:
|
||||||
@app.route("/meteors")
|
@app.route("/meteors")
|
||||||
def meteor_list() -> str:
|
def meteor_list() -> str:
|
||||||
"""Web page showing meteor shower information."""
|
"""Web page showing meteor shower information."""
|
||||||
meteors = agenda.meteors.get_meteor_data()
|
meteors = [
|
||||||
|
{
|
||||||
|
"name": "Quadrantids",
|
||||||
|
"peak": "January 3-4",
|
||||||
|
"active": "December 28 - January 12",
|
||||||
|
"rate": "50-100 meteors per hour",
|
||||||
|
"radiant": "Boötes",
|
||||||
|
"moon_phase": "New Moon (excellent viewing)",
|
||||||
|
"visibility": "Northern Hemisphere",
|
||||||
|
"description": "The year kicks off with the Quadrantids, known for their brief but intense peak lasting only about 4 hours.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lyrids",
|
||||||
|
"peak": "April 21-22",
|
||||||
|
"active": "April 14 - April 30",
|
||||||
|
"rate": "10-15 meteors per hour",
|
||||||
|
"radiant": "Lyra",
|
||||||
|
"moon_phase": "Waning Crescent (good viewing)",
|
||||||
|
"visibility": "Both hemispheres",
|
||||||
|
"description": "The Lyrids are one of the oldest recorded meteor showers, with observations dating back 2,700 years.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Eta Aquariids",
|
||||||
|
"peak": "May 5-6",
|
||||||
|
"active": "April 15 - May 27",
|
||||||
|
"rate": "30-60 meteors per hour",
|
||||||
|
"radiant": "Aquarius",
|
||||||
|
"moon_phase": "First Quarter (moderate viewing)",
|
||||||
|
"visibility": "Southern Hemisphere (best)",
|
||||||
|
"description": "Created by debris from Halley's Comet, these meteors are fast and often leave glowing trails.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Perseids",
|
||||||
|
"peak": "August 12-13",
|
||||||
|
"active": "July 17 - August 24",
|
||||||
|
"rate": "50-100 meteors per hour",
|
||||||
|
"radiant": "Perseus",
|
||||||
|
"moon_phase": "Full Moon (poor viewing)",
|
||||||
|
"visibility": "Northern Hemisphere",
|
||||||
|
"description": "One of the most popular meteor showers, though 2025 viewing will be hampered by bright moonlight.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Orionids",
|
||||||
|
"peak": "October 21-22",
|
||||||
|
"active": "October 2 - November 7",
|
||||||
|
"rate": "15-25 meteors per hour",
|
||||||
|
"radiant": "Orion",
|
||||||
|
"moon_phase": "Waning Crescent (good viewing)",
|
||||||
|
"visibility": "Both hemispheres",
|
||||||
|
"description": "Another shower created by Halley's Comet debris, known for their speed and brightness.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Geminids",
|
||||||
|
"peak": "December 13-14",
|
||||||
|
"active": "December 4 - December 20",
|
||||||
|
"rate": "60-120 meteors per hour",
|
||||||
|
"radiant": "Gemini",
|
||||||
|
"moon_phase": "Waxing Gibbous (moderate viewing)",
|
||||||
|
"visibility": "Both hemispheres",
|
||||||
|
"description": "The best shower of 2025 with the highest rates. Unusual for being caused by an asteroid (3200 Phaethon) rather than a comet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ursids",
|
||||||
|
"peak": "December 22-23",
|
||||||
|
"active": "December 17 - December 26",
|
||||||
|
"rate": "5-10 meteors per hour",
|
||||||
|
"radiant": "Ursa Minor",
|
||||||
|
"moon_phase": "New Moon (excellent viewing)",
|
||||||
|
"visibility": "Northern Hemisphere",
|
||||||
|
"description": "A minor shower that closes out the year, best viewed from dark locations away from city lights.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"meteors.html",
|
"meteors.html",
|
||||||
|
|
Loading…
Reference in a new issue