diff --git a/package-lock.json b/package-lock.json
index 4bdce81..656803f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"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",
@@ -910,6 +912,53 @@
"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",
@@ -2806,7 +2855,6 @@
"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": {
@@ -3159,6 +3207,18 @@
"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",
@@ -3291,6 +3351,15 @@
"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",
@@ -3445,6 +3514,17 @@
"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",
@@ -3476,6 +3556,12 @@
"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",
diff --git a/package.json b/package.json
index 5371653..1b8af89 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
"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",
diff --git a/src/App.jsx b/src/App.jsx
index 0e87781..ec022a4 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,11 +1,11 @@
// src/App.jsx
-import React, { useState, useCallback, useEffect, useMemo } from 'react';
+import { 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' },
- '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': {
title: 'MARKET REVIEW', type: 'stats',
data: [
@@ -66,7 +66,17 @@ 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'
+ 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: '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: '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: '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(() => {
- 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.'] }];
+ // ตรวจสอบและเพิ่ม/อัปเดตจุด 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;
}
- 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 },
@@ -97,6 +120,37 @@ 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) {
@@ -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['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; }
+
+ // สำหรับ '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) => {
- setSelectedPoint(point);
- if (point.panel) {
+ if (point.isClickableForPopUp) {
+ // ถ้าจุดนั้นตั้งค่าให้คลิกแล้วแสดง Pop-up
+ setSelectedPoint(point);
+ } else if (point.panel) {
+ // โหมดปกติ: แสดง 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);
}, []);
@@ -147,7 +258,7 @@ function App() {
}, [isLocalView]);
const handleExitLocalView = useCallback(() => {
- if (isLocalView) { // ตรวจสอบเพื่อไม่ให้เปลี่ยนโหมดซ้ำซ้อน
+ if (isLocalView) {
setIsLocalView(false);
console.log("App.jsx: Exiting Local View");
}
@@ -157,7 +268,7 @@ function App() {
const currentPanel = panelData[activePanel];
const [currentTime, setCurrentTime] = useState('');
- const [population, setPopulation] = useState('7.42 Billion People');
+ const [population] = useState('7.42 Billion People');
useEffect(() => {
const timer = setInterval(() => {
@@ -179,16 +290,17 @@ function App() {
globePoints={globePointsWithRussia}
globeConnections={globeConnections}
onPointClick={handlePointClick}
- onZoomToLocal={handleZoomToLocal} // ส่ง callback ไปยัง GlobeCanvas
- isLocalView={isLocalView} // ส่ง state ไปยัง GlobeCanvas เพื่อควบคุม OrbitControls
+ onZoomToLocal={handleZoomToLocal}
+ isLocalView={isLocalView}
+ satelliteData={satelliteData} // ตรวจสอบว่าส่ง prop นี้ไปแล้ว
/>
) : (
{/* ปุ่มสำหรับออกจาก Local View */}
@@ -246,8 +359,6 @@ function App() {
{/* Top Right - Time Display & Population */}
- {/* เปลี่ยน text-base-content เป็น text-white เพื่อสีที่สว่างขึ้น */}
- {/* เพิ่ม text-shadow เพื่อให้ข้อความมีมิติและอ่านง่ายขึ้นบนพื้นหลังมืด */}
{currentTime}
{population}
diff --git a/src/components/ConnectionLines.jsx b/src/components/ConnectionLines.jsx
index a2e52e4..6a14fd8 100644
--- a/src/components/ConnectionLines.jsx
+++ b/src/components/ConnectionLines.jsx
@@ -1,5 +1,5 @@
// src/components/ConnectionLines.jsx
-import React, { useMemo } from 'react';
+import { useMemo } from 'react';
import { Line } from '@react-three/drei';
import { Vector3, CatmullRomCurve3 } from 'three';
import { latLonToCartesian } from '../utils/threeHelpers';
diff --git a/src/components/EarthBody.jsx b/src/components/EarthBody.jsx
index eb8724a..25f7d99 100644
--- a/src/components/EarthBody.jsx
+++ b/src/components/EarthBody.jsx
@@ -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');
diff --git a/src/components/GlobalNav.jsx b/src/components/GlobalNav.jsx
index 2b17246..fcd399d 100644
--- a/src/components/GlobalNav.jsx
+++ b/src/components/GlobalNav.jsx
@@ -1,28 +1,53 @@
// 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 }) {
- 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' },
+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 },
];
- 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' },
- ];
+ // Filter รายการที่จะแสดงตาม isNewsMode
+ const currentItems = navItems.filter(item =>
+ isNewsMode ? item.type === 'news' : item.type === 'stats'
+ );
- 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 (
@@ -33,11 +58,11 @@ export default function GlobalNav({ activePanel, setActivePanel, isNewsMode, set
- {currentItems.map((item) => (
+ {currentItems.map((item, index) => (
- handlePanelClick(item.id)}
+ onClick={() => handleNavClick(item.id, item.type)}
>
- {item.number}
+ {String(index + 1).padStart(2, '0')}
- {/* แก้ไขตรงนี้: ลบ whitespace-nowrap ออก */}
+
{item.label}
@@ -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'}`}
onClick={() => {
setIsNewsMode(true);
- setActivePanel('world_news');
+ setActivePanel(navItems.find(item => item.type === 'news')?.id || 'world_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'}`}
onClick={() => {
setIsNewsMode(false);
- setActivePanel('market_review');
+ setActivePanel(navItems.find(item => item.type === 'stats')?.id || 'market_review');
}}
>
STATISTICS
diff --git a/src/components/GlobeCanvas.jsx b/src/components/GlobeCanvas.jsx
index 3a294cb..bc07068 100644
--- a/src/components/GlobeCanvas.jsx
+++ b/src/components/GlobeCanvas.jsx
@@ -1,19 +1,21 @@
// 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 { OrbitControls, Html, Sphere } from '@react-three/drei';
import { TextureLoader, LinearSRGBColorSpace, BackSide } from 'three';
-import * as THREE from 'three'; // *** เพิ่มการ import THREE ที่นี่ ***
+import * as THREE from 'three';
import EarthBody from './EarthBody';
import PointsOnGlobe from './PointsOnGlobe';
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
-// 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;
@@ -131,10 +133,14 @@ function CustomOrbitControls({ controlsRef, onZoomChange, onExitLocalView, isLoc
// 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 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]);
@@ -171,6 +177,14 @@ export default function GlobeCanvas({ activePanel, isNewsMode, panelLocations, g
}
}, [onZoomToLocal]);
+ // ลบ callback นี้ออกไป
+ // const handleSatellitePositionUpdate = useCallback((id, position) => {
+ // setSatellitePositions(prevPositions => ({
+ // ...prevPositions,
+ // [id]: position.clone()
+ // }));
+ // }, []);
+
return (
- {/* *** เพิ่มดาวเทียมที่นี่ *** */}
-
-
-
+ {/* Satellite และ SatelliteOrbit - Loop ผ่าน satelliteData */}
+ {satelliteData.map(sat => (
+
+
+ {/* ลบ HTML ส่วนนี้ออก เพราะกล่องข้อความรายละเอียดถูก Render ใน Satellite.jsx แล้ว */}
+ {/* {satellitePositions[sat.id] && (
+
+ {sat.name}
+
+ )} */}
+
+
+ ))}
+
+ {/* SatelliteInfoPanel ยังคง Comment Out ไว้ (เพราะเราจะใช้ Html ที่ลอยตาม) */}
+ {/* {!isNewsMode && (
+
+ )} */}
diff --git a/src/components/LocalMap.jsx b/src/components/LocalMap.jsx
index 418b240..b24e908 100644
--- a/src/components/LocalMap.jsx
+++ b/src/components/LocalMap.jsx
@@ -1,5 +1,5 @@
// src/components/LocalMap.jsx
-import React, { useEffect, useRef } from 'react';
+import { useEffect, useRef } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css'; // ต้อง import CSS ของ Leaflet ด้วย
diff --git a/src/components/NewsPanel.jsx b/src/components/NewsPanel.jsx
index c8722c0..ac56cd0 100644
--- a/src/components/NewsPanel.jsx
+++ b/src/components/NewsPanel.jsx
@@ -1,6 +1,4 @@
// src/components/NewsPanel.jsx
-import React from 'react';
-
// เปลี่ยน videoStreamUrl เป็น youtubeVideoId
export default function NewsPanel({ title, content, youtubeVideoId, isActive }) {
const panelClasses = `
diff --git a/src/components/PointsOnGlobe.jsx b/src/components/PointsOnGlobe.jsx
index 4bbf3cd..89a3b9b 100644
--- a/src/components/PointsOnGlobe.jsx
+++ b/src/components/PointsOnGlobe.jsx
@@ -1,5 +1,4 @@
// src/components/PointsOnGlobe.jsx
-import React from 'react';
import { Sphere } from '@react-three/drei';
import { latLonToCartesian } from '../utils/threeHelpers';
diff --git a/src/components/PopUpDetail.jsx b/src/components/PopUpDetail.jsx
index c4ff3f5..b4b47ce 100644
--- a/src/components/PopUpDetail.jsx
+++ b/src/components/PopUpDetail.jsx
@@ -1,5 +1,5 @@
// src/components/PopUpDetail.jsx
-import React, { useEffect, useRef } from 'react';
+import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
export default function PopUpDetail({ point, onClose }) {
diff --git a/src/components/Satellite.jsx b/src/components/Satellite.jsx
index d742609..f452f56 100644
--- a/src/components/Satellite.jsx
+++ b/src/components/Satellite.jsx
@@ -1,66 +1,118 @@
// 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 { Html } from '@react-three/drei';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import * as THREE from 'three';
-// ฟังก์ชันคำนวณตำแหน่งดาวเทียมบนวงโคจรวงกลมง่ายๆ
-// ปรับเปลี่ยนสูตรการโคจรให้ซับซ้อนขึ้นได้ เช่น วงรี, เอียง
-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 }) {
+/**
+ * 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 satelliteRef = useRef();
- const gltf = useLoader(GLTFLoader, '/models/satellite.glb'); // ตรวจสอบเส้นทางไฟล์โมเดลของคุณ
- const [satelliteScene, setSatelliteScene] = useState(null);
+ const angleRef = useRef(0);
+ const gltf = useLoader(GLTFLoader, '/models/satellite.glb');
- // แปลงมุมเอียงจากองศาเป็นเรเดียน
- const orbitTiltRad = THREE.MathUtils.degToRad(orbitTiltDeg);
+ 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]);
- // Clone scene เพื่อป้องกันการ modify scene เดียวกันซ้ำๆ หากมีหลายดาวเทียม
- useEffect(() => {
- if (gltf.scene) {
- setSatelliteScene(gltf.scene.clone());
- }
- }, [gltf]);
+ const orbitTiltRad = useMemo(() => THREE.MathUtils.degToRad(orbitTiltDeg), [orbitTiltDeg]);
- useFrame(({ clock }) => {
+ useFrame((state, delta) => {
if (satelliteRef.current) {
- const time = clock.getElapsedTime();
- const newPosition = calculateSatellitePosition(time, orbitRadius, orbitSpeed, orbitTiltRad);
- satelliteRef.current.position.copy(newPosition);
+ angleRef.current += orbitSpeed * delta;
- // ให้ดาวเทียมหันหน้าเข้าหาโลกเสมอ (หรือจะให้หันไปทิศทางอื่นก็ได้)
- satelliteRef.current.lookAt(0, 0, 0); // หันเข้าหาจุดศูนย์กลางโลก
- satelliteRef.current.rotation.y += Math.PI; // อาจต้องปรับการหมุนตามทิศทางโมเดลของคุณ
+ 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);
+ }
}
});
- if (!satelliteScene) {
- return null; // ยังไม่โหลดโมเดล
- }
+ const handleClick = useCallback((event) => {
+ event.stopPropagation();
+ console.log(`[Satellite ${id}] CLICKED!`);
+ }, [id]);
+
return (
-
- {/* ปรับ scale ของดาวเทียมให้เหมาะสม */}
+
+
+
+
+
{name}
+ {description || "No description available."}
+ Orbit Radius: {orbitRadius.toFixed(2)} units
+ Orbit Tilt: {orbitTiltDeg.toFixed(1)}°
+
);
}
\ No newline at end of file
diff --git a/src/components/SatelliteOrbit.jsx b/src/components/SatelliteOrbit.jsx
new file mode 100644
index 0000000..460a308
--- /dev/null
+++ b/src/components/SatelliteOrbit.jsx
@@ -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 (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/StatsPanel.jsx b/src/components/StatsPanel.jsx
index 1455e7e..796b582 100644
--- a/src/components/StatsPanel.jsx
+++ b/src/components/StatsPanel.jsx
@@ -1,5 +1,5 @@
// src/components/StatsPanel.jsx
-import React, { useState, useEffect } from 'react';
+import { useState, useEffect } from 'react';
// ถ้าจะใช้ Chart.js สำหรับกราฟ ให้ติดตั้งและ import:
// npm install react-chartjs-2 chart.js
// import { Line } from 'react-chartjs-2';