ปรับปรุง Worldview_Dashboard

This commit is contained in:
Flook 2025-07-05 05:32:12 +07:00
parent 57797bc5e0
commit a2c9d2d91d
14 changed files with 503 additions and 114 deletions

88
package-lock.json generated
View File

@ -8,6 +8,8 @@
"name": "future_website", "name": "future_website",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@react-three/drei": "^10.4.2", "@react-three/drei": "^10.4.2",
"@react-three/fiber": "^9.1.4", "@react-three/fiber": "^9.1.4",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
@ -910,6 +912,53 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"react": ">=16.3"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -2806,7 +2855,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@ -3159,6 +3207,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -3291,6 +3351,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -3445,6 +3514,17 @@
"lie": "^3.0.2" "lie": "^3.0.2"
} }
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -3476,6 +3556,12 @@
"react": "^19.1.0" "react": "^19.1.0"
} }
}, },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-leaflet": { "node_modules/react-leaflet": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",

View File

@ -10,6 +10,8 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@react-three/drei": "^10.4.2", "@react-three/drei": "^10.4.2",
"@react-three/fiber": "^9.1.4", "@react-three/fiber": "^9.1.4",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",

View File

@ -1,11 +1,11 @@
// src/App.jsx // src/App.jsx
import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { useState, useCallback, useEffect, useMemo } from 'react';
import GlobeCanvas from './components/GlobeCanvas'; import GlobeCanvas from './components/GlobeCanvas';
import NewsPanel from './components/NewsPanel'; import NewsPanel from './components/NewsPanel';
import StatsPanel from './components/StatsPanel'; import StatsPanel from './components/StatsPanel';
import GlobalNav from './components/GlobalNav'; import GlobalNav from './components/GlobalNav';
import PopUpDetail from './components/PopUpDetail'; import PopUpDetail from './components/PopUpDetail';
import LocalMap from './components/LocalMap'; // () import LocalMap from './components/LocalMap';
function App() { function App() {
const [isNewsMode, setIsNewsMode] = useState(true); const [isNewsMode, setIsNewsMode] = useState(true);
@ -18,13 +18,13 @@ function App() {
const [localViewCenter, setLocalViewCenter] = useState({ lat: 0, lon: 0 }); const [localViewCenter, setLocalViewCenter] = useState({ lat: 0, lon: 0 });
const [localViewZoom, setLocalViewZoom] = useState(10); // Leaflet const [localViewZoom, setLocalViewZoom] = useState(10); // Leaflet
// Data Panel - // Data Panel
const panelData = { const panelData = {
'world_news': { title: 'WORLD NEWS', type: 'news', content: 'Global events unfold across continents, shaping economies and societies.', youtubeVideoId: 'dQw4w9WgXcQ' }, 'world_news': { title: 'WORLD NEWS', type: 'news', content: 'Global events unfold across continents, shaping economies and societies.', youtubeVideoId: 'dQw4w9WgXcQ' },
'euro_news': { title: 'EURO NEWS', type: 'news', content: 'Key developments in European politics and markets are impacting global trends.', youtubeVideoId: '1' }, 'euro_news': { title: 'EURO NEWS', type: 'news', content: 'Key developments in European politics and markets are impacting global trends.', youtubeVideoId: '1' },
'america_news': { title: 'AMERICA NEWS', type: 'news', content: 'Breaking news from North and South America, focusing on economic shifts.', youtubeVideoId: '6' }, 'america_news': { title: 'AMERICA NEWS', type: 'news', content: 'Breaking news from North and South America, focusing on economic shifts.', youtubeVideoId: '6' },
'china_japan_news': { title: 'CHINA / JAPAN NEWS', type: 'news', content: 'Latest economic and cultural updates from China and Japan, shaping regional dynamics.', youtubeVideoId: '11' }, 'china_japan_news': { title: 'CHINA / JAPAN NEWS', type: 'news', content: 'Latest economic and cultural updates from China and Japan, shaping regional dynamics.', youtubeVideoId: '11' },
'asia_news': { title: 'ASIA NEWS', type: 'news', content: 'Emerging tech and market news from Asia, driving innovation forward.', youtubeVideoId: 'k4F9c40tWnQ' }, 'asean_news': { title: 'ASEAN NEWS', type: 'news', content: 'Emerging tech and market news from Asean, driving innovation forward.', youtubeVideoId: 'k4F9c40tWnQ', lat: 13.7563, lon: 105.00 },
'market_review': { 'market_review': {
title: 'MARKET REVIEW', type: 'stats', title: 'MARKET REVIEW', type: 'stats',
data: [ data: [
@ -66,7 +66,17 @@ function App() {
{ label: 'MICEX Index', value: '3,200', trend: 'up' }, { label: 'MICEX Index', value: '3,200', trend: 'up' },
{ label: 'Oil Production', value: '10.5 M bbl/d', trend: 'same' }, { label: 'Oil Production', value: '10.5 M bbl/d', trend: 'same' },
], ],
youtubeVideoId: 'your-russia-video-id' youtubeVideoId: 'your-russia-video-id',
// news_content Pop-up
news_content: {
title: 'Key Russian Economic Updates',
text: 'Recent reports indicate a steady increase in the BIX and MICEX indices, reflecting a resilient domestic market. Oil production maintains consistent levels, impacting global energy prices.',
image: 'https://via.placeholder.com/400x200?text=Russia+News+Image', //
links: [
{ label: 'Read more on BIX Index', url: 'https://example.com/bix' },
{ label: 'MICEX Market Analysis', url: 'https://example.com/micex' },
]
}
}, },
}; };
@ -76,20 +86,33 @@ function App() {
{ id: 'germany', name: 'Germany', lat: 51.1657, lon: 10.4515, type: 'news', panel: 'euro_news', news: ['German economy outlook.', 'Renewable energy growth.'] }, { id: 'germany', name: 'Germany', lat: 51.1657, lon: 10.4515, type: 'news', panel: 'euro_news', news: ['German economy outlook.', 'Renewable energy growth.'] },
{ id: 'china', name: 'China', lat: 35.8617, lon: 104.1954, type: 'news', panel: 'china_japan_news', news: ['China trade balance.', 'Digital currency trials.'] }, { id: 'china', name: 'China', lat: 35.8617, lon: 104.1954, type: 'news', panel: 'china_japan_news', news: ['China trade balance.', 'Digital currency trials.'] },
{ id: 'japan', name: 'Japan', lat: 36.2048, lon: 138.2529, type: 'news', panel: 'china_japan_news', news: ['Japan tech advancements.', 'Olympic preparations.'] }, { id: 'japan', name: 'Japan', lat: 36.2048, lon: 138.2529, type: 'news', panel: 'china_japan_news', news: ['Japan tech advancements.', 'Olympic preparations.'] },
{ id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'] }, // Russia Pop-up
{ id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'], isClickableForPopUp: true },
{ id: 'brazil', name: 'Brazil', lat: -14.2350, lon: -51.9253, type: 'stats', panel: 'next_dollar_currencies', news: ['Brazilian Real fluctuation.', 'Agricultural exports outlook.'] }, { id: 'brazil', name: 'Brazil', lat: -14.2350, lon: -51.9253, type: 'stats', panel: 'next_dollar_currencies', news: ['Brazilian Real fluctuation.', 'Agricultural exports outlook.'] },
{ id: 'india', name: 'India', lat: 20.5937, lon: 78.9629, type: 'stats', panel: 'market_review', news: ['Indian market growth.', 'Startup ecosystem booming.'] }, { id: 'india', name: 'India', lat: 20.5937, lon: 78.9629, type: 'stats', panel: 'market_review', news: ['Indian market growth.', 'Startup ecosystem booming.'] },
{ id: 'australia', name: 'Australia', lat: -25.2744, lon: 133.7751, type: 'news', panel: 'world_news', news: ['Australia bushfire recovery.', 'Mining sector update.'] }, { id: 'australia', name: 'Australia', lat: -25.2744, lon: 133.7751, type: 'news', panel: 'world_news', news: ['Australia bushfire recovery.', 'Mining sector update.'] },
// ASEAN News generic
{ id: 'thailand', name: 'Thailand', lat: 13.7563, lon: 100.5018, type: 'news', panel: 'asean_news', news: ['Thai tourism booming.', 'Digital economy initiatives.'] },
{ id: 'singapore', name: 'Singapore', lat: 1.3521, lon: 103.8198, type: 'news', panel: 'asean_news', news: ['Singapore tech hub.', 'Fintech innovations.'] },
]; ];
const globePointsWithRussia = useMemo(() => { const globePointsWithRussia = useMemo(() => {
const existingRussiaPoint = globePoints.find(p => p.id === 'russia'); // / Russia
if (!existingRussiaPoint) { const existingRussiaPointIndex = globePoints.findIndex(p => p.id === 'russia');
return [...globePoints, { id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'] }]; if (existingRussiaPointIndex === -1) {
return [...globePoints, { id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'], isClickableForPopUp: true }];
} else {
// Russia isClickableForPopUp
const updatedGlobePoints = [...globePoints];
updatedGlobePoints[existingRussiaPointIndex] = {
...updatedGlobePoints[existingRussiaPointIndex],
isClickableForPopUp: true
};
return updatedGlobePoints;
} }
return globePoints;
}, [globePoints]); }, [globePoints]);
const globeConnections = [ const globeConnections = [
{ id: 'us-uk', startLat: 39.8283, startLon: -98.5795, endLat: 51.5074, endLon: -0.1278 }, { id: 'us-uk', startLat: 39.8283, startLon: -98.5795, endLat: 51.5074, endLon: -0.1278 },
{ id: 'uk-germany', startLat: 51.5074, startLon: -0.1278, endLat: 51.1657, endLon: 10.4515 }, { id: 'uk-germany', startLat: 51.5074, startLon: -0.1278, endLat: 51.1657, endLon: 10.4515 },
@ -97,6 +120,37 @@ function App() {
{ id: 'us-china', startLat: 39.8283, startLon: -98.5795, endLat: 35.8617, endLon: 104.1954 }, { id: 'us-china', startLat: 39.8283, startLon: -98.5795, endLat: 35.8617, endLon: 104.1954 },
]; ];
// Mock data for satellites
const satelliteData = [
{
id: 'napa1',
name: 'NAPA-1', // <<<
description: 'Thai Earth Observation Satellite 1',
orbitRadius: 1.05,
orbitSpeed: 0.15, // <<< : 0.000145 2 24 . () delta
orbitTiltDeg: 45, // <<< Polar Orbit
color: '#ff00ff'
},
{
id: 'napa2',
name: 'NAPA-2', // <<<
description: 'Thai Earth Observation Satellite 2',
orbitRadius: 1.07,
orbitSpeed: 0.05, // <<<
orbitTiltDeg: 180, // <<< Polar Orbit
color: '#00ffff'
},
{
id: 'napa3',
name: 'NAPA-3', // <<<
description: 'Thai Earth Observation Satellite 3',
orbitRadius: 1.10,
orbitSpeed: 0.08, // <<<
orbitTiltDeg: -45, // <<< Polar Orbit
color: '#00ff00'
}
];
const panelLocations = useMemo(() => { const panelLocations = useMemo(() => {
const locations = {}; const locations = {};
for (const panelId in panelData) { for (const panelId in panelData) {
@ -110,28 +164,85 @@ function App() {
} }
}); });
// .lat .lon
// 'world_news'
if (!locations['world_news']) locations['world_news'] = {}; // object
if (!locations['world_news'].lat && !locations['world_news'].lon) { locations['world_news'].lat = -25.2744; locations['world_news'].lon = 133.7751; } if (!locations['world_news'].lat && !locations['world_news'].lon) { locations['world_news'].lat = -25.2744; locations['world_news'].lon = 133.7751; }
if (!locations['asia_news'].lat && !locations['asia_news'].lon) { locations['asia_news'].lat = 30; locations['asia_news'].lon = 90; }
// 'asean_news'
if (!locations['asean_news']) locations['asean_news'] = {};
if (!locations['asean_news'].lat && !locations['asean_news'].lon) { locations['asean_news'].lat = 13.7563; locations['asean_news'].lon = 105.1954; }
// 'euro_news'
if (!locations['euro_news']) locations['euro_news'] = {};
if (!locations['euro_news'].lat && !locations['euro_news'].lon) { locations['euro_news'].lat = 50; locations['euro_news'].lon = 10; } if (!locations['euro_news'].lat && !locations['euro_news'].lon) { locations['euro_news'].lat = 50; locations['euro_news'].lon = 10; }
// 'america_news'
if (!locations['america_news']) locations['america_news'] = {};
if (!locations['america_news'].lat && !locations['america_news'].lon) { locations['america_news'].lat = 35; locations['america_news'].lon = -100; } if (!locations['america_news'].lat && !locations['america_news'].lon) { locations['america_news'].lat = 35; locations['america_news'].lon = -100; }
// 'global_population'
if (!locations['global_population']) locations['global_population'] = {};
if (!locations['global_population'].lat && !locations['global_population'].lon) { locations['global_population'].lat = 0; locations['global_population'].lon = 0; } if (!locations['global_population'].lat && !locations['global_population'].lon) { locations['global_population'].lat = 0; locations['global_population'].lon = 0; }
// 'energy_consumption'
if (!locations['energy_consumption']) locations['energy_consumption'] = {};
if (!locations['energy_consumption'].lat && !locations['energy_consumption'].lon) { locations['energy_consumption'].lat = 0; locations['energy_consumption'].lon = 0; } if (!locations['energy_consumption'].lat && !locations['energy_consumption'].lon) { locations['energy_consumption'].lat = 0; locations['energy_consumption'].lon = 0; }
// 'russia_news_stats'
if (!locations['russia_news_stats']) locations['russia_news_stats'] = {};
if (!locations['russia_news_stats'].lat && !locations['russia_news_stats'].lon) { locations['russia_news_stats'].lat = 61.5240; locations['russia_news_stats'].lon = 105.3188; } if (!locations['russia_news_stats'].lat && !locations['russia_news_stats'].lon) { locations['russia_news_stats'].lat = 61.5240; locations['russia_news_stats'].lon = 105.3188; }
return locations; return locations;
}, [panelData, globePointsWithRussia]); }, [panelData, globePointsWithRussia]);
// handlePointClick Pop-up
const handlePointClick = useCallback((point) => { const handlePointClick = useCallback((point) => {
setSelectedPoint(point); if (point.isClickableForPopUp) {
if (point.panel) { // Pop-up
setSelectedPoint(point);
} else if (point.panel) {
// : Panel
setActivePanel(point.panel); setActivePanel(point.panel);
if (point.type === 'news') setIsNewsMode(true); if (point.type === 'news') setIsNewsMode(true);
else if (point.type === 'stats') setIsNewsMode(false); else if (point.type === 'stats') setIsNewsMode(false);
setIsPanelVisible(true); setIsPanelVisible(true);
// Local View GlobeCanvas
} }
}, []); }, []);
// GlobalNav
const handleMenuClick = useCallback((panelId) => {
setActivePanel(panelId); // panel
setIsPanelVisible(true); // Panel
const panelInfo = panelData[panelId];
if (panelInfo) {
if (panelInfo.type === 'news') {
setIsNewsMode(true);
} else if (panelInfo.type === 'stats') {
setIsNewsMode(false);
}
// news_content Pop-up
if (panelInfo.news_content) {
// object point PopUpDetail
const dummyPoint = {
id: panelId,
name: panelInfo.title, // panel
// globePoints
lat: panelLocations[panelId]?.lat || 0,
lon: panelLocations[panelId]?.lon || 0,
// news_content
news_content: panelInfo.news_content
};
setSelectedPoint(dummyPoint);
} else {
setSelectedPoint(null); // Pop-up Pop-up
}
}
}, [panelData, panelLocations]);
const closePopUp = useCallback(() => { const closePopUp = useCallback(() => {
setSelectedPoint(null); setSelectedPoint(null);
}, []); }, []);
@ -147,7 +258,7 @@ function App() {
}, [isLocalView]); }, [isLocalView]);
const handleExitLocalView = useCallback(() => { const handleExitLocalView = useCallback(() => {
if (isLocalView) { // if (isLocalView) {
setIsLocalView(false); setIsLocalView(false);
console.log("App.jsx: Exiting Local View"); console.log("App.jsx: Exiting Local View");
} }
@ -157,7 +268,7 @@ function App() {
const currentPanel = panelData[activePanel]; const currentPanel = panelData[activePanel];
const [currentTime, setCurrentTime] = useState(''); const [currentTime, setCurrentTime] = useState('');
const [population, setPopulation] = useState('7.42 Billion People'); const [population] = useState('7.42 Billion People');
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { const timer = setInterval(() => {
@ -179,16 +290,17 @@ function App() {
globePoints={globePointsWithRussia} globePoints={globePointsWithRussia}
globeConnections={globeConnections} globeConnections={globeConnections}
onPointClick={handlePointClick} onPointClick={handlePointClick}
onZoomToLocal={handleZoomToLocal} // callback GlobeCanvas onZoomToLocal={handleZoomToLocal}
isLocalView={isLocalView} // state GlobeCanvas OrbitControls isLocalView={isLocalView}
satelliteData={satelliteData} // prop
/> />
) : ( ) : (
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<LocalMap <LocalMap
center={localViewCenter} center={localViewCenter}
zoom={localViewZoom} zoom={localViewZoom}
points={globePointsWithRussia} // points={globePointsWithRussia}
onPointClick={handlePointClick} // Leaflet PopUp onPointClick={handlePointClick}
/> />
{/* ปุ่มสำหรับออกจาก Local View */} {/* ปุ่มสำหรับออกจาก Local View */}
<button <button
@ -210,6 +322,7 @@ function App() {
setActivePanel={setActivePanel} setActivePanel={setActivePanel}
isNewsMode={isNewsMode} isNewsMode={isNewsMode}
setIsNewsMode={setIsNewsMode} setIsNewsMode={setIsNewsMode}
onMenuClick={handleMenuClick} //
/> />
</div> </div>
@ -246,8 +359,6 @@ function App() {
{/* Top Right - Time Display & Population */} {/* Top Right - Time Display & Population */}
<div className="absolute top-8 right-8 text-right text-lg z-10 pointer-events-auto text-white" style={{ textShadow: '0px 0px 5px rgba(0, 0, 0, 0.5)' }}> <div className="absolute top-8 right-8 text-right text-lg z-10 pointer-events-auto text-white" style={{ textShadow: '0px 0px 5px rgba(0, 0, 0, 0.5)' }}>
{/* เปลี่ยน text-base-content เป็น text-white เพื่อสีที่สว่างขึ้น */}
{/* เพิ่ม text-shadow เพื่อให้ข้อความมีมิติและอ่านง่ายขึ้นบนพื้นหลังมืด */}
<div className="font-bold text-3xl">{currentTime}</div> <div className="font-bold text-3xl">{currentTime}</div>
<div>{population}</div> <div>{population}</div>
</div> </div>

View File

@ -1,5 +1,5 @@
// src/components/ConnectionLines.jsx // src/components/ConnectionLines.jsx
import React, { useMemo } from 'react'; import { useMemo } from 'react';
import { Line } from '@react-three/drei'; import { Line } from '@react-three/drei';
import { Vector3, CatmullRomCurve3 } from 'three'; import { Vector3, CatmullRomCurve3 } from 'three';
import { latLonToCartesian } from '../utils/threeHelpers'; import { latLonToCartesian } from '../utils/threeHelpers';

View File

@ -8,7 +8,7 @@ import { gsap } from 'gsap';
import atmosphereVertexShader from "../shaders/atmosphereVertex.glsl"; import atmosphereVertexShader from "../shaders/atmosphereVertex.glsl";
import atmosphereFragmentShader from "../shaders/atmosphereFragment.glsl"; import atmosphereFragmentShader from "../shaders/atmosphereFragment.glsl";
// Clouds AtmosphereGlow components () // Clouds AtmosphereGlow components
function Clouds() { function Clouds() {
const cloudsRef = useRef(); const cloudsRef = useRef();
const cloudMap = useLoader(TextureLoader, '/textures/05_earthcloudmaptrans.jpg'); const cloudMap = useLoader(TextureLoader, '/textures/05_earthcloudmaptrans.jpg');

View File

@ -1,28 +1,53 @@
// src/components/GlobalNav.jsx // src/components/GlobalNav.jsx
import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// faChartLine import
import { faNewspaper, faChartBar, faGlobeAsia, faDollarSign, faUsers, faFire, faChartLine } from '@fortawesome/free-solid-svg-icons';
import { useCallback } from 'react';
export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, setIsNewsMode }) { export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, setIsNewsMode, onMenuClick }) {
const newsItems = [
{ id: 'world_news', label: 'WORLD NEWS', number: '01' }, // Array
{ id: 'euro_news', label: 'EURO NEWS', number: '02' }, // 'panelId' key panelData App.jsx
{ id: 'america_news', label: 'AMERICA NEWS', number: '03' }, const navItems = [
{ id: 'china_japan_news', label: 'CHINA / JAPAN NEWS', number: '04' }, // --- News Section ---
{ id: 'russia_news', label: 'RUSSIA NEWS', number: '05' }, { id: 'world_news', label: 'WORLD NEWS', type: 'news', icon: faNewspaper },
{ id: 'euro_news', label: 'EURO NEWS', type: 'news', icon: faNewspaper },
{ id: 'america_news', label: 'AMERICA NEWS', type: 'news', icon: faNewspaper },
{ id: 'china_japan_news', label: 'CHINA / JAPAN NEWS', type: 'news', icon: faNewspaper },
{ id: 'asean_news', label: 'ASEAN NEWS', type: 'news', icon: faGlobeAsia },
// --- Stats Section ---
{ id: 'market_review', label: 'MARKET REVIEW', type: 'stats', icon: faChartBar },
{ id: 'next_dollar_currencies', label: 'CURRENCIES', type: 'stats', icon: faDollarSign },
{ id: 'global_population', label: 'POPULATION', type: 'stats', icon: faUsers },
{ id: 'energy_consumption', label: 'ENERGY', type: 'stats', icon: faFire },
// **:**
// 'RUSSIA NEWS'
// { id: 'russia_news_stats', label: 'RUSSIA NEWS', type: 'stats', icon: faNewspaper }, // <-- !
// : 'RUSSIAN ECONOMY'
{ id: 'russia_news_stats', label: 'RUSSIAN ECONOMY', type: 'stats', icon: faChartLine },
]; ];
const statsItems = [ // Filter isNewsMode
{ id: 'market_review', label: 'MARKET REVIEW', number: '01' }, const currentItems = navItems.filter(item =>
{ id: 'next_dollar_currencies', label: 'CURRENCIES', number: '02' }, isNewsMode ? item.type === 'news' : item.type === 'stats'
{ id: 'global_population', label: 'POPULATION', number: '03' }, );
{ id: 'energy_consumption', label: 'ENERGY', number: '04' },
{ id: 'bix_micex_index', label: 'BIX INDEX / MICEX INDEX', number: '05' },
];
const currentItems = isNewsMode ? newsItems : statsItems; const handleNavClick = useCallback((panelId, type) => {
// activePanel isNewsMode
setActivePanel(panelId);
if (type === 'news') {
setIsNewsMode(true);
} else {
setIsNewsMode(false);
}
// onMenuClick App.jsx
// App.jsx NewsPanel/StatsPanel PopUpDetail
if (onMenuClick) {
onMenuClick(panelId);
}
}, [setActivePanel, setIsNewsMode, onMenuClick]);
const handlePanelClick = (id) => {
setActivePanel(id);
};
return ( return (
<div className="flex flex-col items-start p-4 z-20 pointer-events-auto text-base-content md:w-64"> <div className="flex flex-col items-start p-4 z-20 pointer-events-auto text-base-content md:w-64">
@ -33,11 +58,11 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
</div> </div>
<ul className="menu p-2 w-full bg-base-100 rounded-box shadow-xl"> <ul className="menu p-2 w-full bg-base-100 rounded-box shadow-xl">
{currentItems.map((item) => ( {currentItems.map((item, index) => (
<li <li
key={item.id} key={item.id} // key={item.id}
className={`${activePanel === item.id ? 'active' : ''}`} className={`${activePanel === item.id ? 'active' : ''}`}
onClick={() => handlePanelClick(item.id)} onClick={() => handleNavClick(item.id, item.type)}
> >
<a className="flex items-center gap-3"> <a className="flex items-center gap-3">
<span className={`badge badge-ghost font-bold text-xs w-6 h-6 flex items-center justify-center rounded-full <span className={`badge badge-ghost font-bold text-xs w-6 h-6 flex items-center justify-center rounded-full
@ -45,9 +70,9 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
? 'badge-primary text-white shadow-lg' ? 'badge-primary text-white shadow-lg'
: 'bg-transparent text-base-content/70 border border-base-content/50 group-hover:bg-base-300' : 'bg-transparent text-base-content/70 border border-base-content/50 group-hover:bg-base-300'
}`}> }`}>
{item.number} {String(index + 1).padStart(2, '0')}
</span> </span>
{/* แก้ไขตรงนี้: ลบ whitespace-nowrap ออก */} <FontAwesomeIcon icon={item.icon} className="mr-2" />
<span className="text-sm font-medium uppercase tracking-wide break-words"> <span className="text-sm font-medium uppercase tracking-wide break-words">
{item.label} {item.label}
</span> </span>
@ -62,7 +87,7 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
className={`tab transition-all duration-300 ${isNewsMode ? 'tab-active bg-primary text-primary-content' : 'text-base-content'}`} className={`tab transition-all duration-300 ${isNewsMode ? 'tab-active bg-primary text-primary-content' : 'text-base-content'}`}
onClick={() => { onClick={() => {
setIsNewsMode(true); setIsNewsMode(true);
setActivePanel('world_news'); setActivePanel(navItems.find(item => item.type === 'news')?.id || 'world_news');
}} }}
> >
NEWS NEWS
@ -72,7 +97,7 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
className={`tab transition-all duration-300 ${!isNewsMode ? 'tab-active bg-primary text-primary-content' : 'text-base-content'}`} className={`tab transition-all duration-300 ${!isNewsMode ? 'tab-active bg-primary text-primary-content' : 'text-base-content'}`}
onClick={() => { onClick={() => {
setIsNewsMode(false); setIsNewsMode(false);
setActivePanel('market_review'); setActivePanel(navItems.find(item => item.type === 'stats')?.id || 'market_review');
}} }}
> >
STATISTICS STATISTICS

View File

@ -1,19 +1,21 @@
// src/components/GlobeCanvas.jsx // src/components/GlobeCanvas.jsx
import React, { useRef, Suspense, useEffect, useCallback } from 'react'; import React, { useRef, Suspense, useEffect, useCallback } from 'react'; // <<< useState
import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber';
import { OrbitControls, Html, Sphere } from '@react-three/drei'; import { OrbitControls, Html, Sphere } from '@react-three/drei';
import { TextureLoader, LinearSRGBColorSpace, BackSide } from 'three'; import { TextureLoader, LinearSRGBColorSpace, BackSide } from 'three';
import * as THREE from 'three'; // *** import THREE *** import * as THREE from 'three';
import EarthBody from './EarthBody'; import EarthBody from './EarthBody';
import PointsOnGlobe from './PointsOnGlobe'; import PointsOnGlobe from './PointsOnGlobe';
import ConnectionLines from './ConnectionLines'; import ConnectionLines from './ConnectionLines';
import Satellite from './Satellite'; // *** Import Satellite *** import Satellite from './Satellite';
import SatelliteOrbit from './SatelliteOrbit';
// import SatelliteInfoPanel from './SatelliteInfoPanel'; // <<< Comment out
// Helper function to convert 3D Cartesian coordinates to Lat/Lon // Helper function to convert 3D Cartesian coordinates to Lat/Lon
// Assumes globe radius of 1 ( EarthBody ) // Assumes globe radius of 1 ( EarthBody )
function cartesianToLatLon(vector) { function cartesianToLatLon(vector) {
const radius = 1; // *** *** const radius = 1; // *** ***
const lat = Math.asin(vector.y / radius) * (180 / Math.PI); const lat = Math.asin(vector.y / radius) * (180 / Math.PI);
let lon = Math.atan2(vector.z / radius, vector.x / radius) * (180 / Math.PI); let lon = Math.atan2(vector.z / radius, vector.x / radius) * (180 / Math.PI);
if (lon > 180) lon -= 360; if (lon > 180) lon -= 360;
@ -131,10 +133,14 @@ function CustomOrbitControls({ controlsRef, onZoomChange, onExitLocalView, isLoc
// GlobeCanvas Component // GlobeCanvas Component
export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, globePoints, globeConnections, onPointClick, onZoomToLocal, onExitLocalView, isLocalView }) { export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, globePoints, globeConnections, onPointClick, onZoomToLocal, onExitLocalView, isLocalView, satelliteData }) {
const earthBodyRef = useRef(); const earthBodyRef = useRef();
const orbitControlsRef = useRef(); const orbitControlsRef = useRef();
// State
// key: satelliteId, value: THREE.Vector3
//const [satellitePositions, setSatellitePositions] = useState({}); // <<< state
const getPanelLocation = useCallback((panelId) => { const getPanelLocation = useCallback((panelId) => {
return panelLocations[panelId] || { lat: 0, lon: 0 }; return panelLocations[panelId] || { lat: 0, lon: 0 };
}, [panelLocations]); }, [panelLocations]);
@ -171,6 +177,14 @@ export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, g
} }
}, [onZoomToLocal]); }, [onZoomToLocal]);
// callback
// const handleSatellitePositionUpdate = useCallback((id, position) => {
// setSatellitePositions(prevPositions => ({
// ...prevPositions,
// [id]: position.clone()
// }));
// }, []);
return ( return (
<Canvas <Canvas
camera={{ position: [0, 0, 2.5], fov: 70 }} camera={{ position: [0, 0, 2.5], fov: 70 }}
@ -203,10 +217,44 @@ export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, g
connections={globeConnections} connections={globeConnections}
/> />
{/* *** เพิ่มดาวเทียมที่นี่ *** */} {/* Satellite และ SatelliteOrbit - Loop ผ่าน satelliteData */}
<Satellite orbitRadius={1.1} orbitSpeed={0.05} orbitTiltDeg={25} /> {satelliteData.map(sat => (
<Satellite orbitRadius={1.2} orbitSpeed={0.03} orbitTiltDeg={60} /> <React.Fragment key={sat.id}>
<Satellite orbitRadius={1.3} orbitSpeed={0.07} orbitTiltDeg={-10} /> <Satellite
id={sat.id}
name={sat.name} // <<< name
description={sat.description} // <<< description
orbitRadius={sat.orbitRadius}
orbitSpeed={sat.orbitSpeed}
orbitTiltDeg={sat.orbitTiltDeg}
color={sat.color}
// onPositionUpdate={handleSatellitePositionUpdate} // <<<
/>
{/* ลบ HTML ส่วนนี้ออก เพราะกล่องข้อความรายละเอียดถูก Render ใน Satellite.jsx แล้ว */}
{/* {satellitePositions[sat.id] && (
<Html
position={satellitePositions[sat.id].clone().multiplyScalar(1.02)}
transform
occlude
className="text-white text-3xl whitespace-nowrap p-1 rounded-sm bg-black/50 backdrop-blur-sm"
style={{ pointerEvents: 'none' }}
>
{sat.name}
</Html>
)} */}
<SatelliteOrbit
altitude={sat.orbitRadius}
inclination={sat.orbitTiltDeg}
startLongitude={0}
color={sat.color}
/>
</React.Fragment>
))}
{/* SatelliteInfoPanel ยังคง Comment Out ไว้ (เพราะเราจะใช้ Html ที่ลอยตาม) */}
{/* {!isNewsMode && (
<SatelliteInfoPanel satellites={satelliteData} />
)} */}
</Suspense> </Suspense>

View File

@ -1,5 +1,5 @@
// src/components/LocalMap.jsx // src/components/LocalMap.jsx
import React, { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'; import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css'; // import CSS Leaflet import 'leaflet/dist/leaflet.css'; // import CSS Leaflet

View File

@ -1,6 +1,4 @@
// src/components/NewsPanel.jsx // src/components/NewsPanel.jsx
import React from 'react';
// videoStreamUrl youtubeVideoId // videoStreamUrl youtubeVideoId
export default function NewsPanel({ title, content, youtubeVideoId, isActive }) { export default function NewsPanel({ title, content, youtubeVideoId, isActive }) {
const panelClasses = ` const panelClasses = `

View File

@ -1,5 +1,4 @@
// src/components/PointsOnGlobe.jsx // src/components/PointsOnGlobe.jsx
import React from 'react';
import { Sphere } from '@react-three/drei'; import { Sphere } from '@react-three/drei';
import { latLonToCartesian } from '../utils/threeHelpers'; import { latLonToCartesian } from '../utils/threeHelpers';

View File

@ -1,5 +1,5 @@
// src/components/PopUpDetail.jsx // src/components/PopUpDetail.jsx
import React, { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { gsap } from 'gsap'; import { gsap } from 'gsap';
export default function PopUpDetail({ point, onClose }) { export default function PopUpDetail({ point, onClose }) {

View File

@ -1,66 +1,118 @@
// src/components/Satellite.jsx // src/components/Satellite.jsx
import React, { useRef, useState, useEffect } from 'react'; import React, { useRef, useMemo, Suspense, useCallback } from 'react';
import { useFrame, useLoader } from '@react-three/fiber'; import { useFrame, useLoader } from '@react-three/fiber';
import { Html } from '@react-three/drei';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import * as THREE from 'three'; import * as THREE from 'three';
// /**
// , * Component สำหรบโมเดลดาวเทยมทเคลอนทรอบโลก
const calculateSatellitePosition = (time, orbitRadius, orbitSpeed, orbitTiltRad) => { *
// ( Y) * @param {object} props
const angle = (time * orbitSpeed) % (Math.PI * 2); * @param {string} props.id - ID ของดาวเทยม (สำหร key และการระบตำแหน)
* @param {string} props.name - อของดาวเทยม
// XY * @param {string} [props.description] - รายละเอยดของดาวเทยม
let x = Math.cos(angle) * orbitRadius; * @param {number} props.orbitRadius - ศมวงโคจรจากจดศนยกลางโลก
let z = Math.sin(angle) * orbitRadius; * @param {number} props.orbitSpeed - ความเรวในการโคจร (radians per second)
let y = 0; // y 0 * @param {number} props.orbitTiltDeg - มเอยงของวงโคจรเทยบกบเสนศนยตร (องศา)
* @param {string} [props.color='#ffffff'] - ของดาวเทยม (อาจไมใชาโมเดลมเอง)
const position = new THREE.Vector3(x, y, z); * @param {function} [props.onPositionUpdate] - Callback function (position: THREE.Vector3) => void
*/
// (Orbit Tilt) export default function Satellite({
// Quaternion id,
const quaternion = new THREE.Quaternion(); name,
quaternion.setFromAxisAngle(new THREE.Vector3(1, 0, 0).normalize(), orbitTiltRad); // X description,
orbitRadius,
position.applyQuaternion(quaternion); orbitSpeed,
orbitTiltDeg,
return position; color = '#ffffff',
}; onPositionUpdate
}) {
export default function Satellite({ orbitRadius = 1.1, orbitSpeed = 0.05, orbitTiltDeg = 30 }) {
const satelliteRef = useRef(); const satelliteRef = useRef();
const gltf = useLoader(GLTFLoader, '/models/satellite.glb'); // const angleRef = useRef(0);
const [satelliteScene, setSatelliteScene] = useState(null); const gltf = useLoader(GLTFLoader, '/models/satellite.glb');
// const satelliteModel = useMemo(() => {
const orbitTiltRad = THREE.MathUtils.degToRad(orbitTiltDeg); const model = gltf.scene.clone();
// *** : 0.005 0.015 ( 3 ) ***
model.scale.set(0.015, 0.015, 0.015); // <<<
model.traverse((obj) => {
if (obj.isMesh) {
if (!obj.material) {
obj.material = new THREE.MeshStandardMaterial({ color: new THREE.Color(color) });
} else {
// obj.material.color.set(new THREE.Color(color));
}
obj.castShadow = true;
obj.receiveShadow = true;
}
});
return model;
}, [gltf, color]);
// Clone scene modify scene const orbitTiltRad = useMemo(() => THREE.MathUtils.degToRad(orbitTiltDeg), [orbitTiltDeg]);
useEffect(() => {
if (gltf.scene) {
setSatelliteScene(gltf.scene.clone());
}
}, [gltf]);
useFrame(({ clock }) => { useFrame((state, delta) => {
if (satelliteRef.current) { if (satelliteRef.current) {
const time = clock.getElapsedTime(); angleRef.current += orbitSpeed * delta;
const newPosition = calculateSatellitePosition(time, orbitRadius, orbitSpeed, orbitTiltRad);
satelliteRef.current.position.copy(newPosition);
// () const x = orbitRadius * Math.cos(angleRef.current);
satelliteRef.current.lookAt(0, 0, 0); // const z = orbitRadius * Math.sin(angleRef.current);
satelliteRef.current.rotation.y += Math.PI; // const y = 0; // Y 0 XY ( applyAxisAngle)
const currentPosition = new THREE.Vector3(x, y, z);
// : X
currentPosition.applyAxisAngle(new THREE.Vector3(1, 0, 0), orbitTiltRad);
satelliteRef.current.position.copy(currentPosition);
if (onPositionUpdate) {
onPositionUpdate(id, currentPosition);
}
} }
}); });
if (!satelliteScene) { const handleClick = useCallback((event) => {
return null; // event.stopPropagation();
} console.log(`[Satellite ${id}] CLICKED!`);
}, [id]);
return ( return (
<group ref={satelliteRef}> <group
<primitive object={satelliteScene} scale={0.01} /> {/* ปรับ scale ของดาวเทียมให้เหมาะสม */} ref={satelliteRef}
onClick={handleClick}
>
<primitive object={satelliteModel} />
<Html
// *** ***
// X: ()
// Y: ()
// Z:
position={[0.05, 0.05, 0]}
occlude={false} // false
// distanceFactor
distanceFactor={4}
className="
bg-black/70 backdrop-blur-sm p-1 rounded-md shadow-lg
text-white text-xs font-sans whitespace-normal
text-left pointer-events-none select-none
"
style={{
minWidth: '80px',
maxWidth: '150px',
fontSize: '10px',
lineHeight: '1.2',
visibility: 'visible !important',
opacity: 1,
}}
>
<div className="font-bold text-xs mb-0.5">{name}</div>
<div className="text-gray-300 text-xs leading-tight">{description || "No description available."}</div>
<div className="text-gray-400 mt-1 text-xxs">Orbit Radius: {orbitRadius.toFixed(2)} units</div>
<div className="text-gray-400 text-xxs">Orbit Tilt: {orbitTiltDeg.toFixed(1)}°</div>
</Html>
</group> </group>
); );
} }

View File

@ -0,0 +1,68 @@
// src/components/SatelliteOrbit.jsx
import { useMemo } from 'react';
import { Line } from '@react-three/drei';
import { Vector3, EllipseCurve, Path, BufferGeometry, LineBasicMaterial } from 'three';
import * as THREE from 'three';
//import { latLonToCartesian } from '../utils/threeHelpers';
/**
* Component สำหรบแสดงเสนทางวงโคจรของดาวเทยมรอบโลก
*
* @param {object} props
* @param {number} props.altitude - ความสงของวงโคจรจากผวโลก (หนวยเดยวกบรศมโลกใน Three.js, เช 1.001 สำหรบวงโคจรใกล)
* @param {number} props.inclination - มเอยงของวงโคจรเทยบกบระนาบศนยตร (หนวยเปนองศา)
* @param {number} props.startLongitude - ลองจดเรมตนของจดทวงโคจรตดกบเสนศนยตร (หรอจดอางอ)
* @param {string} [props.color='#00ffff'] - ของเสนวงโคจร (Cyan)
* @param {number} [props.lineWidth=1] - ความหนาของเส
* @param {number} [props.points=100] - จำนวนจดทใชสรางเสนวงโคจร (งมากยงเรยบ)
*/
export default function SatelliteOrbit({
altitude = 1.05, // default ( 1)
inclination = 45, // : 45
startLongitude = 0, // : 0
color = '#00ffff', // Cyan
lineWidth = 1,
points = 100
}) {
const orbitPoints = useMemo(() => {
const radius = altitude; // ( + )
const radInclination = THREE.MathUtils.degToRad(inclination); //
const radStartLongitude = THREE.MathUtils.degToRad(startLongitude); //
const orbit = [];
for (let i = 0; i <= points; i++) {
const angle = (i / points) * Math.PI * 2; // 0 2PI ()
//
// https://math.stackexchange.com/questions/292850/how-to-calculate-coordinates-on-an-inclined-orbit
const x = radius * (Math.cos(angle) * Math.cos(radStartLongitude) - Math.sin(angle) * Math.sin(radStartLongitude) * Math.cos(radInclination));
const y = radius * (Math.sin(angle) * Math.cos(radInclination)); // Y-axis is usually up in Three.js
const z = radius * (Math.cos(angle) * Math.sin(radStartLongitude) + Math.sin(angle) * Math.cos(radStartLongitude) * Math.cos(radInclination));
// Note: Three.js (Y-up) (Z-up)
// XY Z Normal
// Three.js (Y-up)
// XY
const orbitVector = new Vector3(radius * Math.cos(angle), 0, radius * Math.sin(angle));
// Z (yaw) startLongitude
orbitVector.applyAxisAngle(new Vector3(0, 1, 0), radStartLongitude); // Y (longitude)
// X (pitch) inclination
orbitVector.applyAxisAngle(new Vector3(1, 0, 0), radInclination); // X (inclination)
orbit.push(orbitVector);
}
return orbit;
}, [altitude, inclination, startLongitude, points]);
return (
<Line
points={orbitPoints}
color={color}
lineWidth={lineWidth}
dashed={false}
/>
);
}

View File

@ -1,5 +1,5 @@
// src/components/StatsPanel.jsx // src/components/StatsPanel.jsx
import React, { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
// Chart.js import: // Chart.js import:
// npm install react-chartjs-2 chart.js // npm install react-chartjs-2 chart.js
// import { Line } from 'react-chartjs-2'; // import { Line } from 'react-chartjs-2';