cosmo/backend/app/api/celestial_orbit.py

217 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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