ปรับปรุง Worldview_Dashboard
This commit is contained in:
parent
57797bc5e0
commit
a2c9d2d91d
88
package-lock.json
generated
88
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
153
src/App.jsx
153
src/App.jsx
@ -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) => {
|
||||||
|
if (point.isClickableForPopUp) {
|
||||||
|
// ถ้าจุดนั้นตั้งค่าให้คลิกแล้วแสดง Pop-up
|
||||||
setSelectedPoint(point);
|
setSelectedPoint(point);
|
||||||
if (point.panel) {
|
} 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>
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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 ด้วย
|
||||||
|
|
||||||
|
|||||||
@ -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 = `
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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 }) {
|
||||||
|
|||||||
@ -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 เท่า) ***
|
||||||
// Clone scene เพื่อป้องกันการ modify scene เดียวกันซ้ำๆ หากมีหลายดาวเทียม
|
model.scale.set(0.015, 0.015, 0.015); // <<< ปรับตรงนี้
|
||||||
useEffect(() => {
|
model.traverse((obj) => {
|
||||||
if (gltf.scene) {
|
if (obj.isMesh) {
|
||||||
setSatelliteScene(gltf.scene.clone());
|
if (!obj.material) {
|
||||||
|
obj.material = new THREE.MeshStandardMaterial({ color: new THREE.Color(color) });
|
||||||
|
} else {
|
||||||
|
// obj.material.color.set(new THREE.Color(color));
|
||||||
}
|
}
|
||||||
}, [gltf]);
|
obj.castShadow = true;
|
||||||
|
obj.receiveShadow = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return model;
|
||||||
|
}, [gltf, color]);
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
const orbitTiltRad = useMemo(() => THREE.MathUtils.degToRad(orbitTiltDeg), [orbitTiltDeg]);
|
||||||
|
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
68
src/components/SatelliteOrbit.jsx
Normal file
68
src/components/SatelliteOrbit.jsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user