diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index c2528a0..09ba632 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -111,6 +111,30 @@ async def search_celestial_body( ) +@router.get("/{body_id}/nasa-data") +async def get_celestial_nasa_data( + body_id: str, + db: AsyncSession = Depends(get_db) +): + """ + Get raw text data from NASA Horizons for a celestial body + (Hacker terminal style output) + """ + # Check if body exists + body = await celestial_body_service.get_body_by_id(body_id, db) + if not body: + raise HTTPException(status_code=404, detail="Celestial body not found") + + try: + # Fetch raw text from Horizons using the body_id + # Note: body.id corresponds to JPL Horizons ID + raw_text = await horizons_service.get_object_data_raw(body.id) + return {"id": body.id, "name": body.name, "raw_data": raw_text} + except Exception as e: + logger.error(f"Failed to fetch raw data for {body_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to fetch NASA data: {str(e)}") + + @router.put("/{body_id}") async def update_celestial_body( body_id: str, diff --git a/backend/app/services/horizons.py b/backend/app/services/horizons.py index 06429b3..c9abca7 100644 --- a/backend/app/services/horizons.py +++ b/backend/app/services/horizons.py @@ -6,6 +6,7 @@ from astroquery.jplhorizons import Horizons from astropy.time import Time import logging import re +import httpx from app.models.celestial import Position, CelestialBody @@ -19,6 +20,42 @@ class HorizonsService: """Initialize the service""" self.location = "@sun" # Heliocentric coordinates + async def get_object_data_raw(self, body_id: str) -> str: + """ + Get raw object data (terminal style text) from Horizons + + Args: + body_id: JPL Horizons ID + + Returns: + Raw text response from NASA + """ + url = "https://ssd.jpl.nasa.gov/api/horizons.api" + # Ensure ID is quoted for COMMAND + cmd_val = f"'{body_id}'" if not body_id.startswith("'") else body_id + + params = { + "format": "text", + "COMMAND": cmd_val, + "OBJ_DATA": "YES", + "MAKE_EPHEM": "NO", + "EPHEM_TYPE": "VECTORS", + "CENTER": "@sun" + } + + try: + async with httpx.AsyncClient() as client: + logger.info(f"Fetching raw data for body {body_id}") + response = await client.get(url, params=params, timeout=30.0) + + if response.status_code != 200: + raise Exception(f"NASA API returned status {response.status_code}") + + return response.text + except Exception as e: + logger.error(f"Error fetching raw data for {body_id}: {str(e)}") + raise + def get_body_positions( self, body_id: str, diff --git a/frontend/src/components/FocusInfo.tsx b/frontend/src/components/FocusInfo.tsx index 5e33c8d..c080b57 100644 --- a/frontend/src/components/FocusInfo.tsx +++ b/frontend/src/components/FocusInfo.tsx @@ -1,4 +1,7 @@ -import { X, Ruler, Activity } from 'lucide-react'; +import { X, Ruler, Activity, Radar } from 'lucide-react'; +import { useState } from 'react'; +import { Modal, message, Spin } from 'antd'; +import { request } from '../utils/request'; import type { CelestialBody } from '../types'; interface FocusInfoProps { @@ -7,6 +10,10 @@ interface FocusInfoProps { } export function FocusInfo({ body, onClose }: FocusInfoProps) { + const [showTerminal, setShowTerminal] = useState(false); + const [terminalData, setTerminalData] = useState(''); + const [loading, setLoading] = useState(false); + if (!body) return null; // Calculate distance if position is available @@ -16,9 +23,58 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) { const isProbe = body.type === 'probe'; const isActive = body.is_active !== false; + const fetchNasaData = async () => { + setShowTerminal(true); + setLoading(true); + try { + const { data } = await request.get(`/celestial/${body.id}/nasa-data`); + setTerminalData(data.raw_data); + } catch (err) { + console.error(err); + message.error('连接 NASA Horizons 失败'); + // If failed, maybe show error in terminal + setTerminalData("CONNECTION FAILED.\n\nError establishing link with JPL Horizons System.\nCheck connection frequencies."); + } finally { + setLoading(false); + } + }; + + const terminalStyles = ` + .terminal-modal .ant-modal-content { + background-color: #0d1117 !important; + border: 1px solid #238636 !important; + box-shadow: 0 0 30px rgba(35, 134, 54, 0.15) !important; + color: #2ea043 !important; + padding: 0 !important; + overflow: hidden !important; + } + .terminal-modal .ant-modal-header { + background-color: #161b22 !important; + border-bottom: 1px solid #238636 !important; + margin-bottom: 0 !important; + } + .terminal-modal .ant-modal-title { + color: #2ea043 !important; + } + .terminal-modal .ant-modal-close { + color: #2ea043 !important; + } + .terminal-modal .ant-modal-close:hover { + background-color: rgba(35, 134, 54, 0.2) !important; + } + @keyframes spin-slow { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + .animate-spin-slow { + animation: spin-slow 3s linear infinite; + } + `; + return ( // Remove fixed positioning, now handled by parent container (Html component in 3D) -
+
+ {/* Main Info Card */}
@@ -55,7 +111,7 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) {
{/* Stats Grid */} -
+
@@ -80,10 +136,71 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) {
)}
+ + {/* Actions Row */} +
+ +
{/* Connecting Line/Triangle pointing down to the body */}
+ + {/* Terminal Modal */} + setShowTerminal(false)} + footer={null} + width={800} + centered + className="terminal-modal" + styles={{ + header: { + backgroundColor: '#161b22', + borderBottom: '1px solid #238636', + padding: '12px 20px', + marginBottom: 0, + display: 'flex', + alignItems: 'center' + }, + mask: { + backgroundColor: 'rgba(0, 0, 0, 0.85)', + backdropFilter: 'blur(4px)' + }, + body: { + padding: '20px', + backgroundColor: '#0d1117' + } + }} + title={ +
+
+ JPL/HORIZONS SYSTEM INTERFACE // {body.name.toUpperCase()} +
+ } + closeIcon={} + > +
+ {loading ? ( +
+ } /> +
ESTABLISHING SECURE UPLINK...
+
Connecting to ssd.jpl.nasa.gov...
+
+ ) : ( +
+ {terminalData} +
+ )} +
+
); }