Compare commits
	
		
			3 commits
		
	
	
		
			37e74404d5
			...
			ac16bb9dab
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
							
							
								
									
								
								 | 
						ac16bb9dab | ||
| 
							
							
								
									
								
								 | 
						d4b516d861 | ||
| 
							
							
								
									
								
								 | 
						13bb753a0b | 
							
								
								
									
										29
									
								
								CLAUDE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								CLAUDE.md
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
# 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
 | 
			
		||||
							
								
								
									
										267
									
								
								agenda/meteors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								agenda/meteors.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,267 @@
 | 
			
		|||
"""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,6 +23,7 @@ import agenda.data
 | 
			
		|||
import agenda.error_mail
 | 
			
		||||
import agenda.fx
 | 
			
		||||
import agenda.holidays
 | 
			
		||||
import agenda.meteors
 | 
			
		||||
import agenda.stats
 | 
			
		||||
import agenda.thespacedevs
 | 
			
		||||
import agenda.trip
 | 
			
		||||
| 
						 | 
				
			
			@ -216,78 +217,7 @@ def launch_list() -> str:
 | 
			
		|||
@app.route("/meteors")
 | 
			
		||||
def meteor_list() -> str:
 | 
			
		||||
    """Web page showing meteor shower information."""
 | 
			
		||||
    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.",
 | 
			
		||||
        },
 | 
			
		||||
    ]
 | 
			
		||||
    meteors = agenda.meteors.get_meteor_data()
 | 
			
		||||
 | 
			
		||||
    return flask.render_template(
 | 
			
		||||
        "meteors.html",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue