From 78e48a6ee3bf6a386df3556af1a06a7f11aacb94 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Thu, 27 Nov 2025 18:14:25 +0800 Subject: [PATCH] 0.0.9 --- .claude/settings.local.json | 3 +- backend/app/config.py | 4 +- backend/app/models/celestial.py | 18 ++ backend/app/services/horizons.py | 6 +- frontend/public/data/constellations.json | 96 +++++++++ frontend/public/data/galaxies.json | 57 ++++++ frontend/public/data/nearby-stars.json | 110 +++++++++++ frontend/public/data/probe-models.json | 9 + frontend/public/data/solar-system.json | 142 +++++++++++++ frontend/src/App.tsx | 51 ++++- frontend/src/components/CelestialBody.tsx | 3 +- frontend/src/components/Constellations.tsx | 121 ++++++++++++ frontend/src/components/Galaxies.tsx | 187 ++++++++++++++++++ frontend/src/components/Probe.tsx | 52 ++--- frontend/src/components/ProbeList.tsx | 4 +- frontend/src/components/Scene.tsx | 18 +- frontend/src/components/Stars.tsx | 123 ++++++++++++ .../src/components/TimelineController.tsx | 147 ++++++++++++++ frontend/src/components/useHistoricalData.ts | 0 frontend/src/hooks/useHistoricalData.ts | 46 +++++ frontend/src/types/index.ts | 1 + frontend/src/utils/api.ts | 49 ++++- frontend/vite.config.ts | 4 + 23 files changed, 1211 insertions(+), 40 deletions(-) create mode 100644 frontend/public/data/constellations.json create mode 100644 frontend/public/data/galaxies.json create mode 100644 frontend/public/data/nearby-stars.json create mode 100644 frontend/public/data/probe-models.json create mode 100644 frontend/public/data/solar-system.json create mode 100644 frontend/src/components/Constellations.tsx create mode 100644 frontend/src/components/Galaxies.tsx create mode 100644 frontend/src/components/Stars.tsx create mode 100644 frontend/src/components/TimelineController.tsx create mode 100644 frontend/src/components/useHistoricalData.ts create mode 100644 frontend/src/hooks/useHistoricalData.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 38b46e4..9a9c24a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -20,7 +20,8 @@ "Bash(yarn build)", "Bash(source:*)", "Bash(python:*)", - "Bash(uvicorn:*)" + "Bash(uvicorn:*)", + "Bash(cat:*)" ], "deny": [], "ask": [] diff --git a/backend/app/config.py b/backend/app/config.py index 841a57d..3cbe862 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -10,8 +10,8 @@ class Settings(BaseSettings): app_name: str = "Cosmo - Deep Space Explorer" api_prefix: str = "/api" - # CORS settings - cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"] + # CORS settings - allow all origins for development (IP access support) + cors_origins: list[str] = ["*"] # Cache settings cache_ttl_days: int = 3 diff --git a/backend/app/models/celestial.py b/backend/app/models/celestial.py index fce5aba..3822696 100644 --- a/backend/app/models/celestial.py +++ b/backend/app/models/celestial.py @@ -20,6 +20,7 @@ class CelestialBody(BaseModel): id: str = Field(..., description="JPL Horizons ID") name: str = Field(..., description="Display name") + name_zh: str | None = Field(None, description="Chinese name") type: Literal["planet", "probe", "star"] = Field(..., description="Body type") positions: list[Position] = Field( default_factory=list, description="Position history" @@ -52,6 +53,7 @@ CELESTIAL_BODIES = { # Probes "-31": { "name": "Voyager 1", + "name_zh": "旅行者1号", "type": "probe", "description": "离地球最远的人造物体,已进入星际空间", "launch_date": "1977-09-05", @@ -59,6 +61,7 @@ CELESTIAL_BODIES = { }, "-32": { "name": "Voyager 2", + "name_zh": "旅行者2号", "type": "probe", "description": "唯一造访过天王星和海王星的探测器", "launch_date": "1977-08-20", @@ -66,6 +69,7 @@ CELESTIAL_BODIES = { }, "-98": { "name": "New Horizons", + "name_zh": "新视野号", "type": "probe", "description": "飞掠冥王星,正处于柯伊伯带", "launch_date": "2006-01-19", @@ -73,6 +77,7 @@ CELESTIAL_BODIES = { }, "-96": { "name": "Parker Solar Probe", + "name_zh": "帕克太阳探测器", "type": "probe", "description": "正在'触摸'太阳,速度最快的人造物体", "launch_date": "2018-08-12", @@ -80,6 +85,7 @@ CELESTIAL_BODIES = { }, "-61": { "name": "Juno", + "name_zh": "朱诺号", "type": "probe", "description": "正在木星轨道运行", "launch_date": "2011-08-05", @@ -87,6 +93,7 @@ CELESTIAL_BODIES = { }, "-82": { "name": "Cassini", + "name_zh": "卡西尼号", "type": "probe", "description": "土星探测器(已于2017年撞击销毁)", "launch_date": "1997-10-15", @@ -94,6 +101,7 @@ CELESTIAL_BODIES = { }, "-168": { "name": "Perseverance", + "name_zh": "毅力号", "type": "probe", "description": "火星探测车", "launch_date": "2020-07-30", @@ -102,51 +110,61 @@ CELESTIAL_BODIES = { # Planets "10": { "name": "Sun", + "name_zh": "太阳", "type": "star", "description": "太阳,太阳系的中心", }, "199": { "name": "Mercury", + "name_zh": "水星", "type": "planet", "description": "水星,距离太阳最近的行星", }, "299": { "name": "Venus", + "name_zh": "金星", "type": "planet", "description": "金星,太阳系中最热的行星", }, "399": { "name": "Earth", + "name_zh": "地球", "type": "planet", "description": "地球,我们的家园", }, "301": { "name": "Moon", + "name_zh": "月球", "type": "planet", "description": "月球,地球的天然卫星", }, "499": { "name": "Mars", + "name_zh": "火星", "type": "planet", "description": "火星,红色星球", }, "599": { "name": "Jupiter", + "name_zh": "木星", "type": "planet", "description": "木星,太阳系中最大的行星", }, "699": { "name": "Saturn", + "name_zh": "土星", "type": "planet", "description": "土星,拥有美丽的光环", }, "799": { "name": "Uranus", + "name_zh": "天王星", "type": "planet", "description": "天王星,侧躺着自转的行星", }, "899": { "name": "Neptune", + "name_zh": "海王星", "type": "planet", "description": "海王星,太阳系最外层的行星", }, diff --git a/backend/app/services/horizons.py b/backend/app/services/horizons.py index e145add..d25376f 100644 --- a/backend/app/services/horizons.py +++ b/backend/app/services/horizons.py @@ -52,8 +52,9 @@ class HorizonsService: if start_jd == end_jd: epochs = start_jd else: - # Create range with step - epochs = {"start": start_time.isoformat(), "stop": end_time.isoformat(), "step": step} + # Create range with step - use JD (Julian Date) format for Horizons + # JD format is more reliable than ISO strings + epochs = {"start": str(start_jd), "stop": str(end_jd), "step": step} logger.info(f"Querying Horizons for body {body_id} from {start_time} to {end_time}") @@ -139,6 +140,7 @@ class HorizonsService: body = CelestialBody( id=body_id, name=info["name"], + name_zh=info.get("name_zh"), type=info["type"], positions=positions, description=info["description"], diff --git a/frontend/public/data/constellations.json b/frontend/public/data/constellations.json new file mode 100644 index 0000000..da7421f --- /dev/null +++ b/frontend/public/data/constellations.json @@ -0,0 +1,96 @@ +[ + { + "name": "Orion", + "name_zh": "猎户座", + "stars": [ + {"name": "Betelgeuse", "ra": 88.79, "dec": 7.41}, + {"name": "Bellatrix", "ra": 81.28, "dec": 6.35}, + {"name": "Alnitak", "ra": 85.19, "dec": -1.94}, + {"name": "Alnilam", "ra": 84.05, "dec": -1.20}, + {"name": "Mintaka", "ra": 83.00, "dec": -0.30}, + {"name": "Saiph", "ra": 86.94, "dec": -9.67}, + {"name": "Rigel", "ra": 78.63, "dec": -8.20} + ], + "lines": [ + [0, 1], + [1, 2], + [2, 3], + [3, 4], + [2, 5], + [5, 6] + ] + }, + { + "name": "Ursa Major", + "name_zh": "大熊座", + "stars": [ + {"name": "Dubhe", "ra": 165.93, "dec": 61.75}, + {"name": "Merak", "ra": 165.46, "dec": 56.38}, + {"name": "Phecda", "ra": 178.46, "dec": 53.69}, + {"name": "Megrez", "ra": 183.86, "dec": 57.03}, + {"name": "Alioth", "ra": 193.51, "dec": 55.96}, + {"name": "Mizar", "ra": 200.98, "dec": 54.93}, + {"name": "Alkaid", "ra": 206.89, "dec": 49.31} + ], + "lines": [ + [0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6] + ] + }, + { + "name": "Cassiopeia", + "name_zh": "仙后座", + "stars": [ + {"name": "Caph", "ra": 2.29, "dec": 59.15}, + {"name": "Schedar", "ra": 10.13, "dec": 56.54}, + {"name": "Navi", "ra": 14.18, "dec": 60.72}, + {"name": "Ruchbah", "ra": 21.45, "dec": 60.24}, + {"name": "Segin", "ra": 25.65, "dec": 63.67} + ], + "lines": [ + [0, 1], + [1, 2], + [2, 3], + [3, 4] + ] + }, + { + "name": "Leo", + "name_zh": "狮子座", + "stars": [ + {"name": "Regulus", "ra": 152.09, "dec": 11.97}, + {"name": "Denebola", "ra": 177.26, "dec": 14.57}, + {"name": "Algieba", "ra": 154.99, "dec": 19.84}, + {"name": "Zosma", "ra": 168.53, "dec": 20.52}, + {"name": "Chertan", "ra": 173.95, "dec": 15.43} + ], + "lines": [ + [0, 2], + [2, 3], + [3, 4], + [4, 1], + [1, 0] + ] + }, + { + "name": "Scorpius", + "name_zh": "天蝎座", + "stars": [ + {"name": "Antares", "ra": 247.35, "dec": -26.43}, + {"name": "Shaula", "ra": 263.40, "dec": -37.10}, + {"name": "Sargas", "ra": 264.33, "dec": -43.00}, + {"name": "Dschubba", "ra": 240.08, "dec": -22.62}, + {"name": "Lesath", "ra": 262.69, "dec": -37.29} + ], + "lines": [ + [3, 0], + [0, 1], + [1, 4], + [1, 2] + ] + } +] diff --git a/frontend/public/data/galaxies.json b/frontend/public/data/galaxies.json new file mode 100644 index 0000000..c6ede3e --- /dev/null +++ b/frontend/public/data/galaxies.json @@ -0,0 +1,57 @@ +[ + { + "name": "Andromeda Galaxy", + "name_zh": "仙女座星系", + "type": "spiral", + "distance_mly": 2.537, + "ra": 10.68, + "dec": 41.27, + "magnitude": 3.44, + "diameter_kly": 220, + "color": "#CCDDFF" + }, + { + "name": "Triangulum Galaxy", + "name_zh": "三角座星系", + "type": "spiral", + "distance_mly": 2.73, + "ra": 23.46, + "dec": 30.66, + "magnitude": 5.72, + "diameter_kly": 60, + "color": "#AACCEE" + }, + { + "name": "Large Magellanic Cloud", + "name_zh": "大麦哲伦云", + "type": "irregular", + "distance_mly": 0.163, + "ra": 80.89, + "dec": -69.76, + "magnitude": 0.9, + "diameter_kly": 14, + "color": "#DDCCFF" + }, + { + "name": "Small Magellanic Cloud", + "name_zh": "小麦哲伦云", + "type": "irregular", + "distance_mly": 0.197, + "ra": 12.80, + "dec": -73.15, + "magnitude": 2.7, + "diameter_kly": 7, + "color": "#CCBBEE" + }, + { + "name": "Milky Way Center", + "name_zh": "银河系中心", + "type": "galactic_center", + "distance_mly": 0.026, + "ra": 266.42, + "dec": -29.01, + "magnitude": -1, + "diameter_kly": 100, + "color": "#FFFFAA" + } +] diff --git a/frontend/public/data/nearby-stars.json b/frontend/public/data/nearby-stars.json new file mode 100644 index 0000000..5f2764b --- /dev/null +++ b/frontend/public/data/nearby-stars.json @@ -0,0 +1,110 @@ +[ + { + "name": "Proxima Centauri", + "name_zh": "比邻星", + "distance_ly": 4.24, + "ra": 217.43, + "dec": -62.68, + "magnitude": 11.05, + "color": "#FF9966" + }, + { + "name": "Alpha Centauri A", + "name_zh": "南门二A", + "distance_ly": 4.37, + "ra": 219.90, + "dec": -60.83, + "magnitude": -0.01, + "color": "#FFFFAA" + }, + { + "name": "Barnard's Star", + "name_zh": "巴纳德星", + "distance_ly": 5.96, + "ra": 269.45, + "dec": 4.69, + "magnitude": 9.54, + "color": "#FF6666" + }, + { + "name": "Sirius", + "name_zh": "天狼星", + "distance_ly": 8.6, + "ra": 101.29, + "dec": -16.72, + "magnitude": -1.46, + "color": "#FFFFFF" + }, + { + "name": "Epsilon Eridani", + "name_zh": "天苑四", + "distance_ly": 10.5, + "ra": 53.23, + "dec": -9.46, + "magnitude": 3.73, + "color": "#FFDDAA" + }, + { + "name": "Procyon", + "name_zh": "南河三", + "distance_ly": 11.4, + "ra": 114.83, + "dec": 5.22, + "magnitude": 0.34, + "color": "#FFFFDD" + }, + { + "name": "Tau Ceti", + "name_zh": "天仓五", + "distance_ly": 11.9, + "ra": 26.02, + "dec": -15.94, + "magnitude": 3.50, + "color": "#FFFFCC" + }, + { + "name": "Vega", + "name_zh": "织女星", + "distance_ly": 25, + "ra": 279.23, + "dec": 38.78, + "magnitude": 0.03, + "color": "#AACCFF" + }, + { + "name": "Arcturus", + "name_zh": "大角星", + "distance_ly": 37, + "ra": 213.92, + "dec": 19.18, + "magnitude": -0.05, + "color": "#FFCC99" + }, + { + "name": "Altair", + "name_zh": "牛郎星", + "distance_ly": 17, + "ra": 297.70, + "dec": 8.87, + "magnitude": 0.77, + "color": "#FFFFFF" + }, + { + "name": "Betelgeuse", + "name_zh": "参宿四", + "distance_ly": 548, + "ra": 88.79, + "dec": 7.41, + "magnitude": 0.42, + "color": "#FF4444" + }, + { + "name": "Rigel", + "name_zh": "参宿七", + "distance_ly": 860, + "ra": 78.63, + "dec": -8.20, + "magnitude": 0.13, + "color": "#AADDFF" + } +] diff --git a/frontend/public/data/probe-models.json b/frontend/public/data/probe-models.json new file mode 100644 index 0000000..391c8e9 --- /dev/null +++ b/frontend/public/data/probe-models.json @@ -0,0 +1,9 @@ +{ + "Voyager 1": "/models/voyager_1.glb", + "Voyager 2": "/models/voyager_2.glb", + "Juno": "/models/juno.glb", + "Cassini": "/models/cassini.glb", + "New Horizons": null, + "Parker Solar Probe": "/models/parker_solar_probe.glb", + "Perseverance": null +} diff --git a/frontend/public/data/solar-system.json b/frontend/public/data/solar-system.json new file mode 100644 index 0000000..25b286b --- /dev/null +++ b/frontend/public/data/solar-system.json @@ -0,0 +1,142 @@ +{ + "bodies": [ + { + "id": "10", + "name": "Sun", + "name_zh": "太阳", + "type": "star", + "description": "太阳,太阳系的中心" + }, + { + "id": "199", + "name": "Mercury", + "name_zh": "水星", + "type": "planet", + "description": "水星,距离太阳最近的行星" + }, + { + "id": "299", + "name": "Venus", + "name_zh": "金星", + "type": "planet", + "description": "金星,太阳系中最热的行星" + }, + { + "id": "399", + "name": "Earth", + "name_zh": "地球", + "type": "planet", + "description": "地球,我们的家园" + }, + { + "id": "301", + "name": "Moon", + "name_zh": "月球", + "type": "planet", + "description": "月球,地球的天然卫星" + }, + { + "id": "499", + "name": "Mars", + "name_zh": "火星", + "type": "planet", + "description": "火星,红色星球" + }, + { + "id": "599", + "name": "Jupiter", + "name_zh": "木星", + "type": "planet", + "description": "木星,太阳系中最大的行星" + }, + { + "id": "699", + "name": "Saturn", + "name_zh": "土星", + "type": "planet", + "description": "土星,拥有美丽的光环" + }, + { + "id": "799", + "name": "Uranus", + "name_zh": "天王星", + "type": "planet", + "description": "天王星,侧躺着自转的行星" + }, + { + "id": "899", + "name": "Neptune", + "name_zh": "海王星", + "type": "planet", + "description": "海王星,太阳系最外层的行星" + }, + { + "id": "-31", + "name": "Voyager 1", + "name_zh": "旅行者1号", + "type": "probe", + "description": "离地球最远的人造物体,已进入星际空间", + "launch_date": "1977-09-05", + "status": "active" + }, + { + "id": "-32", + "name": "Voyager 2", + "name_zh": "旅行者2号", + "type": "probe", + "description": "唯一造访过天王星和海王星的探测器", + "launch_date": "1977-08-20", + "status": "active" + }, + { + "id": "-98", + "name": "New Horizons", + "name_zh": "新视野号", + "type": "probe", + "description": "飞掠冥王星,正处于柯伊伯带", + "launch_date": "2006-01-19", + "status": "active" + }, + { + "id": "-96", + "name": "Parker Solar Probe", + "name_zh": "帕克太阳探测器", + "type": "probe", + "description": "正在'触摸'太阳,速度最快的人造物体", + "launch_date": "2018-08-12", + "status": "active" + }, + { + "id": "-61", + "name": "Juno", + "name_zh": "朱诺号", + "type": "probe", + "description": "正在木星轨道运行", + "launch_date": "2011-08-05", + "status": "active" + }, + { + "id": "-82", + "name": "Cassini", + "name_zh": "卡西尼号", + "type": "probe", + "description": "土星探测器(已于2017年撞击销毁)", + "launch_date": "1997-10-15", + "status": "inactive" + }, + { + "id": "-168", + "name": "Perseverance", + "name_zh": "毅力号", + "type": "probe", + "description": "火星探测车", + "launch_date": "2020-07-30", + "status": "active" + } + ], + "metadata": { + "version": "1.0.0", + "last_updated": "2025-11-27", + "description": "Solar system celestial bodies catalog including planets, moons, and space probes. Future updates may include comets, asteroids, and other objects." + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a066d87..3efdf32 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,19 +2,47 @@ * Cosmo - Deep Space Explorer * Main application component */ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { useSpaceData } from './hooks/useSpaceData'; +import { useHistoricalData } from './hooks/useHistoricalData'; import { useTrajectory } from './hooks/useTrajectory'; import { Scene } from './components/Scene'; import { ProbeList } from './components/ProbeList'; +import { TimelineController } from './components/TimelineController'; import { Loading } from './components/Loading'; import type { CelestialBody } from './types'; function App() { - const { bodies, loading, error } = useSpaceData(); + const [selectedDate, setSelectedDate] = useState(null); + const [isTimelineMode, setIsTimelineMode] = useState(false); + + // Use real-time data or historical data based on mode + const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData(); + const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate); + + const bodies = isTimelineMode ? historicalBodies : realTimeBodies; + const loading = isTimelineMode ? historicalLoading : realTimeLoading; + const error = isTimelineMode ? historicalError : realTimeError; + const [selectedBody, setSelectedBody] = useState(null); const { trajectoryPositions } = useTrajectory(selectedBody); + // Handle time change from timeline controller + const handleTimeChange = useCallback((date: Date) => { + setSelectedDate(date); + }, []); + + // Toggle timeline mode + const toggleTimelineMode = useCallback(() => { + setIsTimelineMode((prev) => !prev); + if (!isTimelineMode) { + // Entering timeline mode, set initial date to Cassini launch (1997) + setSelectedDate(new Date(1997, 0, 1)); + } else { + setSelectedDate(null); + } + }, [isTimelineMode]); + // Filter probes and planets from all bodies const probes = bodies.filter((b) => b.type === 'probe'); const planets = bodies.filter((b) => b.type === 'planet'); @@ -50,6 +78,16 @@ function App() {

{selectedBody ? `聚焦: ${selectedBody.name}` : `${bodies.length} 个天体`}

+ {/* Probe List Sidebar */} @@ -67,6 +105,15 @@ function App() { trajectoryPositions={trajectoryPositions} /> + {/* Timeline Controller */} + {isTimelineMode && ( + + )} + {/* Instructions overlay */}
{selectedBody ? ( diff --git a/frontend/src/components/CelestialBody.tsx b/frontend/src/components/CelestialBody.tsx index 89da62b..d5158fd 100644 --- a/frontend/src/components/CelestialBody.tsx +++ b/frontend/src/components/CelestialBody.tsx @@ -86,7 +86,6 @@ function Planet({ body, size, emissive, emissiveIntensity }: { if (body.name === 'Moon') { const moonScaled = scalePosition(position.x, position.y, position.z); // Add a visual offset to make Moon visible next to Earth (2 units away) - const earthDistance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); // Moon orbits Earth at ~0.00257 AU, we'll give it a 2-unit offset from Earth's scaled position const angle = Math.atan2(position.y, position.x); const offset = 2.0; // Visual offset in scaled units @@ -185,7 +184,7 @@ function Planet({ body, size, emissive, emissiveIntensity }: { whiteSpace: 'nowrap', }} > - {body.name} + {body.name_zh || body.name}
{distance.toFixed(2)} AU diff --git a/frontend/src/components/Constellations.tsx b/frontend/src/components/Constellations.tsx new file mode 100644 index 0000000..3122b84 --- /dev/null +++ b/frontend/src/components/Constellations.tsx @@ -0,0 +1,121 @@ +/** + * Constellations component - renders major constellations with connecting lines + */ +import { useEffect, useState, useMemo } from 'react'; +import { Line, Text, Billboard } from '@react-three/drei'; +import * as THREE from 'three'; + +interface ConstellationStar { + name: string; + ra: number; // Right Ascension in degrees + dec: number; // Declination in degrees +} + +interface Constellation { + name: string; + name_zh: string; + stars: ConstellationStar[]; + lines: [number, number][]; // Indices of stars to connect +} + +/** + * Convert RA/Dec to Cartesian coordinates + * Use fixed distance for constellation stars to create celestial sphere effect + */ +function raDecToCartesian(ra: number, dec: number, distance: number = 100) { + const raRad = (ra * Math.PI) / 180; + const decRad = (dec * Math.PI) / 180; + + const x = distance * Math.cos(decRad) * Math.cos(raRad); + const y = distance * Math.cos(decRad) * Math.sin(raRad); + const z = distance * Math.sin(decRad); + + return new THREE.Vector3(x, y, z); +} + +export function Constellations() { + const [constellations, setConstellations] = useState([]); + + useEffect(() => { + // Load constellation data + fetch('/data/constellations.json') + .then((res) => res.json()) + .then((data) => setConstellations(data)) + .catch((err) => console.error('Failed to load constellations:', err)); + }, []); + + const constellationLines = useMemo(() => { + return constellations.map((constellation) => { + // Convert all stars to 3D positions + const starPositions = constellation.stars.map((star) => + raDecToCartesian(star.ra, star.dec) + ); + + // Create line segments based on connection indices + const lineSegments = constellation.lines.map(([startIdx, endIdx]) => ({ + start: starPositions[startIdx], + end: starPositions[endIdx], + })); + + // Calculate center position for label (average of all stars) + const center = starPositions.reduce( + (acc, pos) => acc.add(pos), + new THREE.Vector3() + ).divideScalar(starPositions.length); + + return { + name: constellation.name, + nameZh: constellation.name_zh, + starPositions, + lineSegments, + center, + }; + }); + }, [constellations]); + + if (constellationLines.length === 0) { + return null; + } + + return ( + + {constellationLines.map((constellation) => ( + + {/* Render constellation stars */} + {constellation.starPositions.map((pos, idx) => ( + + + + + ))} + + {/* Render connecting lines */} + {constellation.lineSegments.map((segment, idx) => ( + + ))} + + {/* Constellation name label */} + + + {constellation.nameZh} + + + + ))} + + ); +} diff --git a/frontend/src/components/Galaxies.tsx b/frontend/src/components/Galaxies.tsx new file mode 100644 index 0000000..47a1121 --- /dev/null +++ b/frontend/src/components/Galaxies.tsx @@ -0,0 +1,187 @@ +/** + * Galaxies component - renders distant galaxies as billboards + */ +import { useEffect, useState, useMemo } from 'react'; +import { Billboard, Text, useTexture } from '@react-three/drei'; +import * as THREE from 'three'; + +interface Galaxy { + name: string; + name_zh: string; + type: string; + distance_mly: number; // Distance in millions of light years + ra: number; // Right Ascension in degrees + dec: number; // Declination in degrees + magnitude: number; + diameter_kly: number; // Diameter in thousands of light years + color: string; +} + +/** + * Create a procedural galaxy texture + */ +function createGalaxyTexture(color: string, type: string): THREE.Texture { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + const ctx = canvas.getContext('2d')!; + + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const radius = canvas.width / 2; + + // Create radial gradient for galaxy glow + const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); + + // Parse color + const tempColor = new THREE.Color(color); + const r = Math.floor(tempColor.r * 255); + const g = Math.floor(tempColor.g * 255); + const b = Math.floor(tempColor.b * 255); + + if (type === 'spiral') { + // Spiral galaxy: bright core with arms + gradient.addColorStop(0, `rgba(255, 255, 255, 1.0)`); + gradient.addColorStop(0.1, `rgba(${r}, ${g}, ${b}, 0.9)`); + gradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, 0.6)`); + gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.3)`); + gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); + } else if (type === 'irregular') { + // Irregular galaxy: more diffuse + gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.8)`); + gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.5)`); + gradient.addColorStop(0.8, `rgba(${r}, ${g}, ${b}, 0.2)`); + gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); + } else { + // Galactic center or elliptical: bright concentrated core + gradient.addColorStop(0, `rgba(255, 255, 220, 1.0)`); + gradient.addColorStop(0.2, `rgba(${r}, ${g}, ${b}, 0.9)`); + gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, 0.5)`); + gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); + } + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Add some star-like points for detail (only for spiral galaxies) + if (type === 'spiral') { + ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; + for (let i = 0; i < 50; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * radius * 0.7; + const x = centerX + Math.cos(angle) * dist; + const y = centerY + Math.sin(angle) * dist; + const size = Math.random() * 1.5; + ctx.fillRect(x, y, size, size); + } + } + + const texture = new THREE.CanvasTexture(canvas); + texture.needsUpdate = true; + return texture; +} + +/** + * Convert RA/Dec to Cartesian coordinates for distant objects + */ +function raDecToCartesian(ra: number, dec: number, distance: number) { + const raRad = (ra * Math.PI) / 180; + const decRad = (dec * Math.PI) / 180; + + const x = distance * Math.cos(decRad) * Math.cos(raRad); + const y = distance * Math.cos(decRad) * Math.sin(raRad); + const z = distance * Math.sin(decRad); + + return new THREE.Vector3(x, y, z); +} + +/** + * Calculate visual size based on actual diameter and distance + */ +function calculateAngularSize(diameterKly: number, distanceMly: number): number { + // Angular diameter in radians + const angularDiameter = diameterKly / (distanceMly * 1000); + // Significantly reduced multiplier for much smaller galaxies + return Math.max(0.5, angularDiameter * 200); +} + +export function Galaxies() { + const [galaxies, setGalaxies] = useState([]); + + useEffect(() => { + // Load galaxy data + fetch('/data/galaxies.json') + .then((res) => res.json()) + .then((data) => setGalaxies(data)) + .catch((err) => console.error('Failed to load galaxies:', err)); + }, []); + + const galaxyData = useMemo(() => { + return galaxies.map((galaxy) => { + // Place galaxies on celestial sphere at fixed distance for visualization + const visualDistance = 200; // Fixed distance for celestial sphere + const position = raDecToCartesian(galaxy.ra, galaxy.dec, visualDistance); + + // Calculate visual size based on actual properties + const size = galaxy.type === 'galactic_center' + ? 2 // Smaller for Milky Way center + : calculateAngularSize(galaxy.diameter_kly, galaxy.distance_mly); + + // Create procedural texture for this galaxy + const texture = createGalaxyTexture(galaxy.color, galaxy.type); + + return { + ...galaxy, + position, + size, + texture, + }; + }); + }, [galaxies]); + + if (galaxyData.length === 0) { + return null; + } + + return ( + + {galaxyData.map((galaxy) => ( + + + {/* Galaxy texture */} + + + + + + + {/* Galaxy name label - positioned slightly outward from galaxy */} + + + {galaxy.name_zh} + + + + ))} + + ); +} diff --git a/frontend/src/components/Probe.tsx b/frontend/src/components/Probe.tsx index 59e657c..01854af 100644 --- a/frontend/src/components/Probe.tsx +++ b/frontend/src/components/Probe.tsx @@ -1,7 +1,7 @@ /** * Probe component - renders space probes with 3D models */ -import { useRef, useMemo } from 'react'; +import { useRef, useMemo, useState, useEffect } from 'react'; import { Group } from 'three'; import { useGLTF, Html } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; @@ -12,6 +12,12 @@ interface ProbeProps { body: CelestialBody; } +// Load probe model mapping from data file +const loadProbeModels = async (): Promise> => { + const response = await fetch('/data/probe-models.json'); + return response.json(); +}; + // Separate component for each probe type to properly use hooks function ProbeModel({ body, modelPath }: { body: CelestialBody; modelPath: string }) { const groupRef = useRef(null); @@ -110,7 +116,7 @@ function ProbeModel({ body, modelPath }: { body: CelestialBody; modelPath: strin whiteSpace: 'nowrap', }} > - 🛰️ {body.name} + 🛰️ {body.name_zh || body.name}
{distance.toFixed(2)} AU @@ -168,7 +174,7 @@ function ProbeFallback({ body }: { body: CelestialBody }) { whiteSpace: 'nowrap', }} > - 🛰️ {body.name} + 🛰️ {body.name_zh || body.name}
{distance.toFixed(2)} AU @@ -180,18 +186,19 @@ function ProbeFallback({ body }: { body: CelestialBody }) { export function Probe({ body }: ProbeProps) { const position = body.positions[0]; - if (!position) return null; + const [modelMap, setModelMap] = useState>({}); + const [isLoading, setIsLoading] = useState(true); - // Model mapping for probes - match actual filenames - const modelMap: Record = { - 'Voyager 1': '/models/voyager_1.glb', - 'Voyager 2': '/models/voyager_2.glb', - 'Juno': '/models/juno.glb', - 'Cassini': '/models/cassini.glb', - 'New Horizons': null, // No model yet - 'Parker Solar Probe': '/models/parker_solar_probe.glb', - 'Perseverance': null, // No model yet - }; + // Load model mapping on mount + useEffect(() => { + loadProbeModels().then((data) => { + setModelMap(data); + setIsLoading(false); + }); + }, []); + + if (!position) return null; + if (isLoading) return null; // Wait for model map to load const modelPath = modelMap[body.name]; @@ -203,15 +210,10 @@ export function Probe({ body }: ProbeProps) { return ; } -// Preload available models -const modelsToPreload = [ - '/models/voyager_1.glb', - '/models/voyager_2.glb', - '/models/juno.glb', - '/models/cassini.glb', - '/models/parker_solar_probe.glb', -]; - -modelsToPreload.forEach((path) => { - useGLTF.preload(path); +// Preload available models from data file +loadProbeModels().then((modelMap) => { + const modelsToPreload = Object.values(modelMap).filter((path): path is string => path !== null); + modelsToPreload.forEach((path) => { + useGLTF.preload(path); + }); }); diff --git a/frontend/src/components/ProbeList.tsx b/frontend/src/components/ProbeList.tsx index 8924336..fd3b4d7 100644 --- a/frontend/src/components/ProbeList.tsx +++ b/frontend/src/components/ProbeList.tsx @@ -41,7 +41,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody }: Probe return ( + + {/* Speed control */} +
+ 速度: + +
+ + {/* Reset button */} + +
+ + ); +} diff --git a/frontend/src/components/useHistoricalData.ts b/frontend/src/components/useHistoricalData.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/hooks/useHistoricalData.ts b/frontend/src/hooks/useHistoricalData.ts new file mode 100644 index 0000000..01ad514 --- /dev/null +++ b/frontend/src/hooks/useHistoricalData.ts @@ -0,0 +1,46 @@ +/** + * Custom hook for fetching historical celestial data + */ +import { useState, useEffect, useCallback } from 'react'; +import { fetchCelestialPositions } from '../utils/api'; +import type { CelestialBody } from '../types'; + +export function useHistoricalData(selectedDate: Date | null) { + const [bodies, setBodies] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadHistoricalData = useCallback(async (date: Date) => { + try { + setLoading(true); + setError(null); + + // For historical data, we just need a single snapshot at the given date + // Set start and end to the same date, or use a small range + const startDate = new Date(date); + const endDate = new Date(date); + endDate.setDate(endDate.getDate() + 1); // Add 1 day to ensure valid range + + const data = await fetchCelestialPositions( + startDate.toISOString(), + endDate.toISOString(), + '1d' + ); + + setBodies(data.bodies); + } catch (err) { + console.error('Failed to fetch historical data:', err); + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + if (selectedDate) { + loadHistoricalData(selectedDate); + } + }, [selectedDate, loadHistoricalData]); + + return { bodies, loading, error }; +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 69def2b..87f48c9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -14,6 +14,7 @@ export interface Position { export interface CelestialBody { id: string; name: string; + name_zh?: string; type: CelestialBodyType; positions: Position[]; description?: string; diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 7db0976..ccd48e1 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -4,13 +4,60 @@ import axios from 'axios'; import type { CelestialDataResponse, BodyInfo } from '../types'; -const API_BASE_URL = 'http://localhost:8000/api'; +// Dynamically determine the API base URL +// If VITE_API_BASE_URL is set, use it; otherwise use the current host with port 8000 +const getApiBaseUrl = () => { + if (import.meta.env.VITE_API_BASE_URL) { + console.log('[API] Using VITE_API_BASE_URL:', import.meta.env.VITE_API_BASE_URL); + return import.meta.env.VITE_API_BASE_URL; + } + + // Use the same host as the frontend, but with port 8000 + const protocol = window.location.protocol; + const hostname = window.location.hostname; + const apiUrl = `${protocol}//${hostname}:8000/api`; + console.log('[API] Constructed API URL:', apiUrl); + console.log('[API] Protocol:', protocol, 'Hostname:', hostname); + return apiUrl; +}; + +const API_BASE_URL = getApiBaseUrl(); +console.log('[API] Final API_BASE_URL:', API_BASE_URL); export const api = axios.create({ baseURL: API_BASE_URL, timeout: 30000, }); +// Add request interceptor for debugging +api.interceptors.request.use( + (config) => { + console.log('[API Request]', config.method?.toUpperCase(), config.url, config.params); + return config; + }, + (error) => { + console.error('[API Request Error]', error); + return Promise.reject(error); + } +); + +// Add response interceptor for debugging +api.interceptors.response.use( + (response) => { + console.log('[API Response]', response.config.url, response.status, 'Data:', response.data); + return response; + }, + (error) => { + console.error('[API Error]', error.config?.url, error.message); + if (error.response) { + console.error('[API Error Response]', error.response.status, error.response.data); + } else if (error.request) { + console.error('[API Error Request]', error.request); + } + return Promise.reject(error); + } +); + /** * Fetch celestial positions */ diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8b0f57b..59270ce 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + host: '0.0.0.0', // Listen on all network interfaces + port: 5173, + }, })