217 lines
7.8 KiB
Python
217 lines
7.8 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:
|
||
# 优先从天体的extra_data读取轨道参数
|
||
extra_data = body.extra_data or {}
|
||
period = extra_data.get("orbit_period_days") or ORBITAL_PERIODS.get(body.id)
|
||
|
||
if not period:
|
||
logger.warning(f"No orbital period defined for {body.name}, skipping")
|
||
continue
|
||
|
||
# 优先从extra_data读取颜色,其次从默认颜色字典,最后使用默认灰色
|
||
color = extra_data.get("orbit_color") or 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")
|