213 lines
7.5 KiB
Python
213 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:
|
|
# Use optimized query with JOIN to avoid N+1 problem
|
|
orbits_with_bodies = await orbit_service.get_all_orbits_with_bodies(db, body_type=body_type)
|
|
|
|
result = []
|
|
for orbit, body in orbits_with_bodies:
|
|
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")
|