""" Orbit Management API routes Handles precomputed orbital data for celestial bodies """ import logging from fastapi import APIRouter, HTTPException, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from typing import Optional from app.database import get_db from app.services.horizons import horizons_service from app.services.db_service import celestial_body_service from app.services.orbit_service import orbit_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/celestial", tags=["celestial-orbit"]) @router.get("/orbits") async def get_orbits( body_type: Optional[str] = Query(None, description="Filter by body type (planet, dwarf_planet)"), db: AsyncSession = Depends(get_db) ): """ Get all precomputed orbital data Query parameters: - body_type: Optional filter by celestial body type (planet, dwarf_planet) Returns: - List of orbits with points, colors, and metadata """ logger.info(f"Fetching orbits (type filter: {body_type})") try: orbits = await orbit_service.get_all_orbits(db, body_type=body_type) result = [] for orbit in orbits: # Get body info body = await celestial_body_service.get_body_by_id(orbit.body_id, db) result.append({ "body_id": orbit.body_id, "body_name": body.name if body else "Unknown", "body_name_zh": body.name_zh if body else None, "points": orbit.points, "num_points": orbit.num_points, "period_days": orbit.period_days, "color": orbit.color, "updated_at": orbit.updated_at.isoformat() if orbit.updated_at else None }) logger.info(f"✅ Returning {len(result)} orbits") return {"orbits": result} except Exception as e: logger.error(f"Failed to fetch orbits: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/admin/orbits/generate") async def generate_orbits( body_ids: Optional[str] = Query(None, description="Comma-separated body IDs to generate. If empty, generates for all planets and dwarf planets"), db: AsyncSession = Depends(get_db) ): """ Generate orbital data for celestial bodies This endpoint queries NASA Horizons API to get complete orbital paths and stores them in the orbits table for fast frontend rendering. Query parameters: - body_ids: Optional comma-separated list of body IDs (e.g., "399,999") If not provided, generates orbits for all planets and dwarf planets Returns: - List of generated orbits with success/failure status """ logger.info("🌌 Starting orbit generation...") # Orbital periods in days (from astronomical data) # Note: NASA Horizons data is limited to ~2199 for most bodies # We use single complete orbits that fit within this range ORBITAL_PERIODS = { # Planets - single complete orbit "199": 88.0, # Mercury "299": 224.7, # Venus "399": 365.25, # Earth "499": 687.0, # Mars "599": 4333.0, # Jupiter (11.86 years) "699": 10759.0, # Saturn (29.46 years) "799": 30687.0, # Uranus (84.01 years) "899": 60190.0, # Neptune (164.79 years) # Dwarf Planets - single complete orbit "999": 90560.0, # Pluto (247.94 years - full orbit) "2000001": 1680.0, # Ceres (4.6 years) "136199": 203500.0, # Eris (557 years - full orbit) "136108": 104000.0, # Haumea (285 years - full orbit) "136472": 112897.0, # Makemake (309 years - full orbit) } # Default colors for orbits DEFAULT_COLORS = { "199": "#8C7853", # Mercury - brownish "299": "#FFC649", # Venus - yellowish "399": "#4A90E2", # Earth - blue "499": "#CD5C5C", # Mars - red "599": "#DAA520", # Jupiter - golden "699": "#F4A460", # Saturn - sandy brown "799": "#4FD1C5", # Uranus - cyan "899": "#4169E1", # Neptune - royal blue "999": "#8B7355", # Pluto - brown "2000001": "#9E9E9E", # Ceres - gray "136199": "#E0E0E0", # Eris - light gray "136108": "#D4A574", # Haumea - tan "136472": "#C49A6C", # Makemake - beige } try: # Determine which bodies to generate orbits for if body_ids: # Parse comma-separated list target_body_ids = [bid.strip() for bid in body_ids.split(",")] bodies_to_process = [] for bid in target_body_ids: body = await celestial_body_service.get_body_by_id(bid, db) if body: bodies_to_process.append(body) else: logger.warning(f"Body {bid} not found in database") else: # Get all planets and dwarf planets all_bodies = await celestial_body_service.get_all_bodies(db) bodies_to_process = [ b for b in all_bodies if b.type in ["planet", "dwarf_planet"] and b.id in ORBITAL_PERIODS ] if not bodies_to_process: raise HTTPException(status_code=400, detail="No valid bodies to process") logger.info(f"📋 Generating orbits for {len(bodies_to_process)} bodies") results = [] success_count = 0 failure_count = 0 for body in bodies_to_process: try: period = ORBITAL_PERIODS.get(body.id) if not period: logger.warning(f"No orbital period defined for {body.name}, skipping") continue color = DEFAULT_COLORS.get(body.id, "#CCCCCC") # Generate orbit orbit = await orbit_service.generate_orbit( body_id=body.id, body_name=body.name_zh or body.name, period_days=period, color=color, session=db, horizons_service=horizons_service ) results.append({ "body_id": body.id, "body_name": body.name_zh or body.name, "status": "success", "num_points": orbit.num_points, "period_days": orbit.period_days }) success_count += 1 except Exception as e: logger.error(f"Failed to generate orbit for {body.name}: {e}") results.append({ "body_id": body.id, "body_name": body.name_zh or body.name, "status": "failed", "error": str(e) }) failure_count += 1 logger.info(f"🎉 Orbit generation complete: {success_count} succeeded, {failure_count} failed") return { "message": f"Generated {success_count} orbits ({failure_count} failed)", "results": results } except Exception as e: logger.error(f"Orbit generation failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/admin/orbits/{body_id}") async def delete_orbit( body_id: str, db: AsyncSession = Depends(get_db) ): """Delete orbit data for a specific body""" logger.info(f"Deleting orbit for body {body_id}") deleted = await orbit_service.delete_orbit(body_id, db) if deleted: return {"message": f"Orbit for {body_id} deleted successfully"} else: raise HTTPException(status_code=404, detail="Orbit not found")