Compare commits
No commits in common. "a4cb7685804fbb0a413a2fe8b3d590c89441a0d7" and "00214c4975b96e0f2512f3c216c9a2c3f78b9dc0" have entirely different histories.
a4cb768580
...
00214c4975
88
package-lock.json
generated
88
package-lock.json
generated
@ -8,8 +8,6 @@
|
||||
"name": "future_website",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@react-three/drei": "^10.4.2",
|
||||
"@react-three/fiber": "^9.1.4",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
@ -912,53 +910,6 @@
|
||||
"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": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -2855,6 +2806,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@ -3207,18 +3159,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@ -3351,15 +3291,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@ -3514,17 +3445,6 @@
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -3556,12 +3476,6 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
|
||||
|
||||
@ -10,8 +10,6 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@react-three/drei": "^10.4.2",
|
||||
"@react-three/fiber": "^9.1.4",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
|
||||
155
src/App.jsx
155
src/App.jsx
@ -1,11 +1,11 @@
|
||||
// src/App.jsx
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import GlobeCanvas from './components/GlobeCanvas';
|
||||
import NewsPanel from './components/NewsPanel';
|
||||
import StatsPanel from './components/StatsPanel';
|
||||
import GlobalNav from './components/GlobalNav';
|
||||
import PopUpDetail from './components/PopUpDetail';
|
||||
import LocalMap from './components/LocalMap';
|
||||
import LocalMap from './components/LocalMap'; // ต้องสร้างไฟล์นี้ด้วย (จากคำตอบก่อนหน้า)
|
||||
|
||||
function App() {
|
||||
const [isNewsMode, setIsNewsMode] = useState(true);
|
||||
@ -18,13 +18,13 @@ function App() {
|
||||
const [localViewCenter, setLocalViewCenter] = useState({ lat: 0, lon: 0 });
|
||||
const [localViewZoom, setLocalViewZoom] = useState(10); // ซูมเริ่มต้นสำหรับ Leaflet
|
||||
|
||||
// Data สำหรับ Panel ต่างๆ
|
||||
// Data สำหรับ Panel ต่างๆ - เหมือนเดิม
|
||||
const panelData = {
|
||||
'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' },
|
||||
'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' },
|
||||
'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 },
|
||||
'asia_news': { title: 'ASIA NEWS', type: 'news', content: 'Emerging tech and market news from Asia, driving innovation forward.', youtubeVideoId: 'k4F9c40tWnQ' },
|
||||
'market_review': {
|
||||
title: 'MARKET REVIEW', type: 'stats',
|
||||
data: [
|
||||
@ -66,17 +66,7 @@ function App() {
|
||||
{ label: 'MICEX Index', value: '3,200', trend: 'up' },
|
||||
{ label: 'Oil Production', value: '10.5 M bbl/d', trend: 'same' },
|
||||
],
|
||||
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' },
|
||||
]
|
||||
}
|
||||
youtubeVideoId: 'your-russia-video-id'
|
||||
},
|
||||
};
|
||||
|
||||
@ -86,33 +76,20 @@ 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: '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.'] },
|
||||
// อัปเดตจุด 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: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'] },
|
||||
{ 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: '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(() => {
|
||||
// ตรวจสอบและเพิ่ม/อัปเดตจุด Russia ตามเดิม
|
||||
const existingRussiaPointIndex = globePoints.findIndex(p => p.id === 'russia');
|
||||
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;
|
||||
const existingRussiaPoint = globePoints.find(p => p.id === 'russia');
|
||||
if (!existingRussiaPoint) {
|
||||
return [...globePoints, { id: 'russia', name: 'Russia', lat: 61.5240, lon: 105.3188, type: 'stats', panel: 'russia_news_stats', news: ['Russia energy exports.', 'Geopolitical developments.'] }];
|
||||
}
|
||||
return globePoints;
|
||||
}, [globePoints]);
|
||||
|
||||
|
||||
const globeConnections = [
|
||||
{ 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 },
|
||||
@ -120,37 +97,6 @@ function App() {
|
||||
{ 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 locations = {};
|
||||
for (const panelId in panelData) {
|
||||
@ -164,85 +110,28 @@ 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; }
|
||||
|
||||
// สำหรับ '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['asia_news'].lat && !locations['asia_news'].lon) { locations['asia_news'].lat = 30; locations['asia_news'].lon = 90; }
|
||||
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; }
|
||||
|
||||
// สำหรับ '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; }
|
||||
|
||||
// สำหรับ '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; }
|
||||
|
||||
// สำหรับ '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; }
|
||||
|
||||
return locations;
|
||||
}, [panelData, globePointsWithRussia]);
|
||||
|
||||
// แก้ไข handlePointClick เพื่อรองรับ Pop-up จากจุดบนโลก
|
||||
const handlePointClick = useCallback((point) => {
|
||||
if (point.isClickableForPopUp) {
|
||||
// ถ้าจุดนั้นตั้งค่าให้คลิกแล้วแสดง Pop-up
|
||||
setSelectedPoint(point);
|
||||
} else if (point.panel) {
|
||||
// โหมดปกติ: แสดง Panel
|
||||
setSelectedPoint(point);
|
||||
if (point.panel) {
|
||||
setActivePanel(point.panel);
|
||||
if (point.type === 'news') setIsNewsMode(true);
|
||||
else if (point.type === 'stats') setIsNewsMode(false);
|
||||
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(() => {
|
||||
setSelectedPoint(null);
|
||||
}, []);
|
||||
@ -258,7 +147,7 @@ function App() {
|
||||
}, [isLocalView]);
|
||||
|
||||
const handleExitLocalView = useCallback(() => {
|
||||
if (isLocalView) {
|
||||
if (isLocalView) { // ตรวจสอบเพื่อไม่ให้เปลี่ยนโหมดซ้ำซ้อน
|
||||
setIsLocalView(false);
|
||||
console.log("App.jsx: Exiting Local View");
|
||||
}
|
||||
@ -268,7 +157,7 @@ function App() {
|
||||
const currentPanel = panelData[activePanel];
|
||||
|
||||
const [currentTime, setCurrentTime] = useState('');
|
||||
const [population] = useState('7.42 Billion People');
|
||||
const [population, setPopulation] = useState('7.42 Billion People');
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
@ -290,17 +179,16 @@ function App() {
|
||||
globePoints={globePointsWithRussia}
|
||||
globeConnections={globeConnections}
|
||||
onPointClick={handlePointClick}
|
||||
onZoomToLocal={handleZoomToLocal}
|
||||
isLocalView={isLocalView}
|
||||
satelliteData={satelliteData} // ตรวจสอบว่าส่ง prop นี้ไปแล้ว
|
||||
onZoomToLocal={handleZoomToLocal} // ส่ง callback ไปยัง GlobeCanvas
|
||||
isLocalView={isLocalView} // ส่ง state ไปยัง GlobeCanvas เพื่อควบคุม OrbitControls
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 z-0">
|
||||
<LocalMap
|
||||
center={localViewCenter}
|
||||
zoom={localViewZoom}
|
||||
points={globePointsWithRussia}
|
||||
onPointClick={handlePointClick}
|
||||
points={globePointsWithRussia} // ส่งจุดที่ต้องการแสดงบนแผนที่
|
||||
onPointClick={handlePointClick} // หากต้องการให้คลิกจุดบน Leaflet แล้วแสดง PopUp
|
||||
/>
|
||||
{/* ปุ่มสำหรับออกจาก Local View */}
|
||||
<button
|
||||
@ -322,7 +210,6 @@ function App() {
|
||||
setActivePanel={setActivePanel}
|
||||
isNewsMode={isNewsMode}
|
||||
setIsNewsMode={setIsNewsMode}
|
||||
onMenuClick={handleMenuClick} // ส่งฟังก์ชันใหม่เข้าไป
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -359,6 +246,8 @@ function App() {
|
||||
|
||||
{/* 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)' }}>
|
||||
{/* เปลี่ยน text-base-content เป็น text-white เพื่อสีที่สว่างขึ้น */}
|
||||
{/* เพิ่ม text-shadow เพื่อให้ข้อความมีมิติและอ่านง่ายขึ้นบนพื้นหลังมืด */}
|
||||
<div className="font-bold text-3xl">{currentTime}</div>
|
||||
<div>{population}</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// src/components/ConnectionLines.jsx
|
||||
import { useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Line } from '@react-three/drei';
|
||||
import { Vector3, CatmullRomCurve3 } from 'three';
|
||||
import { latLonToCartesian } from '../utils/threeHelpers';
|
||||
|
||||
@ -8,7 +8,7 @@ import { gsap } from 'gsap';
|
||||
import atmosphereVertexShader from "../shaders/atmosphereVertex.glsl";
|
||||
import atmosphereFragmentShader from "../shaders/atmosphereFragment.glsl";
|
||||
|
||||
// Clouds และ AtmosphereGlow components
|
||||
// Clouds และ AtmosphereGlow components เหมือนเดิม (ไม่มีการเปลี่ยนแปลง)
|
||||
function Clouds() {
|
||||
const cloudsRef = useRef();
|
||||
const cloudMap = useLoader(TextureLoader, '/textures/05_earthcloudmaptrans.jpg');
|
||||
|
||||
@ -1,53 +1,28 @@
|
||||
// src/components/GlobalNav.jsx
|
||||
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';
|
||||
import React from 'react';
|
||||
|
||||
export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, setIsNewsMode, onMenuClick }) {
|
||||
|
||||
// รวมรายการเมนูทั้งหมดไว้ใน Array เดียว
|
||||
// ใช้ 'panelId' ในการอ้างอิงถึง key ใน panelData ของ App.jsx
|
||||
const navItems = [
|
||||
// --- News Section ---
|
||||
{ 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 },
|
||||
export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, setIsNewsMode }) {
|
||||
const newsItems = [
|
||||
{ id: 'world_news', label: 'WORLD NEWS', number: '01' },
|
||||
{ id: 'euro_news', label: 'EURO NEWS', number: '02' },
|
||||
{ id: 'america_news', label: 'AMERICA NEWS', number: '03' },
|
||||
{ id: 'china_japan_news', label: 'CHINA / JAPAN NEWS', number: '04' },
|
||||
{ id: 'russia_news', label: 'RUSSIA NEWS', number: '05' },
|
||||
];
|
||||
|
||||
// Filter รายการที่จะแสดงตาม isNewsMode
|
||||
const currentItems = navItems.filter(item =>
|
||||
isNewsMode ? item.type === 'news' : item.type === 'stats'
|
||||
);
|
||||
const statsItems = [
|
||||
{ id: 'market_review', label: 'MARKET REVIEW', number: '01' },
|
||||
{ id: 'next_dollar_currencies', label: 'CURRENCIES', number: '02' },
|
||||
{ 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 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 currentItems = isNewsMode ? newsItems : statsItems;
|
||||
|
||||
const handlePanelClick = (id) => {
|
||||
setActivePanel(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start p-4 z-20 pointer-events-auto text-base-content md:w-64">
|
||||
@ -58,11 +33,11 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
|
||||
</div>
|
||||
|
||||
<ul className="menu p-2 w-full bg-base-100 rounded-box shadow-xl">
|
||||
{currentItems.map((item, index) => (
|
||||
{currentItems.map((item) => (
|
||||
<li
|
||||
key={item.id} // key={item.id} จะต้องไม่ซ้ำกัน
|
||||
key={item.id}
|
||||
className={`${activePanel === item.id ? 'active' : ''}`}
|
||||
onClick={() => handleNavClick(item.id, item.type)}
|
||||
onClick={() => handlePanelClick(item.id)}
|
||||
>
|
||||
<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
|
||||
@ -70,9 +45,9 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
|
||||
? 'badge-primary text-white shadow-lg'
|
||||
: 'bg-transparent text-base-content/70 border border-base-content/50 group-hover:bg-base-300'
|
||||
}`}>
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
{item.number}
|
||||
</span>
|
||||
<FontAwesomeIcon icon={item.icon} className="mr-2" />
|
||||
{/* แก้ไขตรงนี้: ลบ whitespace-nowrap ออก */}
|
||||
<span className="text-sm font-medium uppercase tracking-wide break-words">
|
||||
{item.label}
|
||||
</span>
|
||||
@ -87,7 +62,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'}`}
|
||||
onClick={() => {
|
||||
setIsNewsMode(true);
|
||||
setActivePanel(navItems.find(item => item.type === 'news')?.id || 'world_news');
|
||||
setActivePanel('world_news');
|
||||
}}
|
||||
>
|
||||
NEWS
|
||||
@ -97,7 +72,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'}`}
|
||||
onClick={() => {
|
||||
setIsNewsMode(false);
|
||||
setActivePanel(navItems.find(item => item.type === 'stats')?.id || 'market_review');
|
||||
setActivePanel('market_review');
|
||||
}}
|
||||
>
|
||||
STATISTICS
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
// src/components/GlobeCanvas.jsx
|
||||
import React, { useRef, Suspense, useEffect, useCallback } from 'react'; // <<< เพิ่ม useState
|
||||
import React, { useRef, Suspense, useEffect, useCallback } from 'react';
|
||||
import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber';
|
||||
import { OrbitControls, Html, Sphere } from '@react-three/drei';
|
||||
import { TextureLoader, LinearSRGBColorSpace, BackSide } from 'three';
|
||||
import * as THREE from 'three';
|
||||
import * as THREE from 'three'; // *** เพิ่มการ import THREE ที่นี่ ***
|
||||
|
||||
import EarthBody from './EarthBody';
|
||||
import PointsOnGlobe from './PointsOnGlobe';
|
||||
import ConnectionLines from './ConnectionLines';
|
||||
import Satellite from './Satellite';
|
||||
import SatelliteOrbit from './SatelliteOrbit';
|
||||
// import SatelliteInfoPanel from './SatelliteInfoPanel'; // <<< ลบ หรือ Comment out บรรทัดนี้
|
||||
import Satellite from './Satellite'; // *** Import Satellite ที่นี่ ***
|
||||
|
||||
// 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) {
|
||||
const radius = 1; // *** ตรวจสอบค่านี้ให้ตรงกับขนาดจริงของโลก ***
|
||||
const radius = 1; // *** ตรวจสอบค่านี้ให้ตรงกับขนาดจริงของโลกคุณ ***
|
||||
const lat = Math.asin(vector.y / radius) * (180 / Math.PI);
|
||||
let lon = Math.atan2(vector.z / radius, vector.x / radius) * (180 / Math.PI);
|
||||
if (lon > 180) lon -= 360;
|
||||
@ -133,14 +131,10 @@ function CustomOrbitControls({ controlsRef, onZoomChange, onExitLocalView, isLoc
|
||||
|
||||
|
||||
// GlobeCanvas Component
|
||||
export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, globePoints, globeConnections, onPointClick, onZoomToLocal, onExitLocalView, isLocalView, satelliteData }) {
|
||||
export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, globePoints, globeConnections, onPointClick, onZoomToLocal, onExitLocalView, isLocalView }) {
|
||||
const earthBodyRef = useRef();
|
||||
const orbitControlsRef = useRef();
|
||||
|
||||
// State สำหรับเก็บตำแหน่งปัจจุบันของดาวเทียมแต่ละดวง
|
||||
// key: satelliteId, value: THREE.Vector3
|
||||
//const [satellitePositions, setSatellitePositions] = useState({}); // <<< เพิ่ม state นี้
|
||||
|
||||
const getPanelLocation = useCallback((panelId) => {
|
||||
return panelLocations[panelId] || { lat: 0, lon: 0 };
|
||||
}, [panelLocations]);
|
||||
@ -177,14 +171,6 @@ export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, g
|
||||
}
|
||||
}, [onZoomToLocal]);
|
||||
|
||||
// ลบ callback นี้ออกไป
|
||||
// const handleSatellitePositionUpdate = useCallback((id, position) => {
|
||||
// setSatellitePositions(prevPositions => ({
|
||||
// ...prevPositions,
|
||||
// [id]: position.clone()
|
||||
// }));
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
camera={{ position: [0, 0, 2.5], fov: 70 }}
|
||||
@ -217,44 +203,10 @@ export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, g
|
||||
connections={globeConnections}
|
||||
/>
|
||||
|
||||
{/* Satellite และ SatelliteOrbit - Loop ผ่าน satelliteData */}
|
||||
{satelliteData.map(sat => (
|
||||
<React.Fragment key={sat.id}>
|
||||
<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} />
|
||||
)} */}
|
||||
{/* *** เพิ่มดาวเทียมที่นี่ *** */}
|
||||
<Satellite orbitRadius={1.1} orbitSpeed={0.05} orbitTiltDeg={25} />
|
||||
<Satellite orbitRadius={1.2} orbitSpeed={0.03} orbitTiltDeg={60} />
|
||||
<Satellite orbitRadius={1.3} orbitSpeed={0.07} orbitTiltDeg={-10} />
|
||||
|
||||
</Suspense>
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// src/components/LocalMap.jsx
|
||||
import { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
|
||||
import 'leaflet/dist/leaflet.css'; // ต้อง import CSS ของ Leaflet ด้วย
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
// src/components/NewsPanel.jsx
|
||||
import React from 'react';
|
||||
|
||||
// เปลี่ยน videoStreamUrl เป็น youtubeVideoId
|
||||
export default function NewsPanel({ title, content, youtubeVideoId, isActive }) {
|
||||
const panelClasses = `
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// src/components/PointsOnGlobe.jsx
|
||||
import React from 'react';
|
||||
import { Sphere } from '@react-three/drei';
|
||||
import { latLonToCartesian } from '../utils/threeHelpers';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// src/components/PopUpDetail.jsx
|
||||
import { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
export default function PopUpDetail({ point, onClose }) {
|
||||
|
||||
@ -1,118 +1,66 @@
|
||||
// src/components/Satellite.jsx
|
||||
import React, { useRef, useMemo, Suspense, useCallback } from 'react';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { useFrame, useLoader } from '@react-three/fiber';
|
||||
import { Html } from '@react-three/drei';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import * as THREE from 'three';
|
||||
|
||||
/**
|
||||
* Component สำหรับโมเดลดาวเทียมที่เคลื่อนที่รอบโลก
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {string} props.id - ID ของดาวเทียม (สำหรับ key และการระบุตำแหน่ง)
|
||||
* @param {string} props.name - ชื่อของดาวเทียม
|
||||
* @param {string} [props.description] - รายละเอียดของดาวเทียม
|
||||
* @param {number} props.orbitRadius - รัศมีวงโคจรจากจุดศูนย์กลางโลก
|
||||
* @param {number} props.orbitSpeed - ความเร็วในการโคจร (radians per second)
|
||||
* @param {number} props.orbitTiltDeg - มุมเอียงของวงโคจรเทียบกับเส้นศูนย์สูตร (องศา)
|
||||
* @param {string} [props.color='#ffffff'] - สีของดาวเทียม (อาจไม่ใช้ถ้าโมเดลมีสีเอง)
|
||||
* @param {function} [props.onPositionUpdate] - Callback function (position: THREE.Vector3) => void
|
||||
*/
|
||||
export default function Satellite({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
orbitRadius,
|
||||
orbitSpeed,
|
||||
orbitTiltDeg,
|
||||
color = '#ffffff',
|
||||
onPositionUpdate
|
||||
}) {
|
||||
// ฟังก์ชันคำนวณตำแหน่งดาวเทียมบนวงโคจรวงกลมง่ายๆ
|
||||
// ปรับเปลี่ยนสูตรการโคจรให้ซับซ้อนขึ้นได้ เช่น วงรี, เอียง
|
||||
const calculateSatellitePosition = (time, orbitRadius, orbitSpeed, orbitTiltRad) => {
|
||||
// กำหนดแกนโคจรหลัก (เช่น รอบแกน Y)
|
||||
const angle = (time * orbitSpeed) % (Math.PI * 2);
|
||||
|
||||
// ตำแหน่งเริ่มต้นบนระนาบ XY
|
||||
let x = Math.cos(angle) * orbitRadius;
|
||||
let z = Math.sin(angle) * orbitRadius;
|
||||
let y = 0; // ในขั้นต้น y เป็น 0
|
||||
|
||||
const position = new THREE.Vector3(x, y, z);
|
||||
|
||||
// การเอียงวงโคจร (Orbit Tilt)
|
||||
// ใช้ Quaternion เพื่อหมุนตำแหน่งตามมุมเอียง
|
||||
const quaternion = new THREE.Quaternion();
|
||||
quaternion.setFromAxisAngle(new THREE.Vector3(1, 0, 0).normalize(), orbitTiltRad); // หมุนรอบแกน X เพื่อเอียงวงโคจร
|
||||
|
||||
position.applyQuaternion(quaternion);
|
||||
|
||||
return position;
|
||||
};
|
||||
|
||||
export default function Satellite({ orbitRadius = 1.1, orbitSpeed = 0.05, orbitTiltDeg = 30 }) {
|
||||
const satelliteRef = useRef();
|
||||
const angleRef = useRef(0);
|
||||
const gltf = useLoader(GLTFLoader, '/models/satellite.glb');
|
||||
const gltf = useLoader(GLTFLoader, '/models/satellite.glb'); // ตรวจสอบเส้นทางไฟล์โมเดลของคุณ
|
||||
const [satelliteScene, setSatelliteScene] = useState(null);
|
||||
|
||||
const satelliteModel = useMemo(() => {
|
||||
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]);
|
||||
// แปลงมุมเอียงจากองศาเป็นเรเดียน
|
||||
const orbitTiltRad = THREE.MathUtils.degToRad(orbitTiltDeg);
|
||||
|
||||
const orbitTiltRad = useMemo(() => THREE.MathUtils.degToRad(orbitTiltDeg), [orbitTiltDeg]);
|
||||
// Clone scene เพื่อป้องกันการ modify scene เดียวกันซ้ำๆ หากมีหลายดาวเทียม
|
||||
useEffect(() => {
|
||||
if (gltf.scene) {
|
||||
setSatelliteScene(gltf.scene.clone());
|
||||
}
|
||||
}, [gltf]);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
useFrame(({ clock }) => {
|
||||
if (satelliteRef.current) {
|
||||
angleRef.current += orbitSpeed * delta;
|
||||
const time = clock.getElapsedTime();
|
||||
const newPosition = calculateSatellitePosition(time, orbitRadius, orbitSpeed, orbitTiltRad);
|
||||
satelliteRef.current.position.copy(newPosition);
|
||||
|
||||
const x = orbitRadius * Math.cos(angleRef.current);
|
||||
const z = orbitRadius * Math.sin(angleRef.current);
|
||||
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);
|
||||
}
|
||||
// ให้ดาวเทียมหันหน้าเข้าหาโลกเสมอ (หรือจะให้หันไปทิศทางอื่นก็ได้)
|
||||
satelliteRef.current.lookAt(0, 0, 0); // หันเข้าหาจุดศูนย์กลางโลก
|
||||
satelliteRef.current.rotation.y += Math.PI; // อาจต้องปรับการหมุนตามทิศทางโมเดลของคุณ
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
console.log(`[Satellite ${id}] CLICKED!`);
|
||||
}, [id]);
|
||||
|
||||
if (!satelliteScene) {
|
||||
return null; // ยังไม่โหลดโมเดล
|
||||
}
|
||||
|
||||
return (
|
||||
<group
|
||||
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 ref={satelliteRef}>
|
||||
<primitive object={satelliteScene} scale={0.01} /> {/* ปรับ scale ของดาวเทียมให้เหมาะสม */}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
// 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
|
||||
import { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
// ถ้าจะใช้ Chart.js สำหรับกราฟ ให้ติดตั้งและ import:
|
||||
// npm install react-chartjs-2 chart.js
|
||||
// import { Line } from 'react-chartjs-2';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user