cosmo/backend/app/api/celestial_orbit.py

215 lines
7.5 KiB
Python

"""
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")