Skip to content

Commit 5a8e2ef

Browse files
authored
Merge pull request #15 from ProspektStudio/Ace-Edits
Enhance Globe and SatellitePopup components for improved satellite in…
2 parents 3bba916 + 5ab9371 commit 5a8e2ef

7 files changed

Lines changed: 167 additions & 78 deletions

File tree

components/Globe.tsx

Lines changed: 97 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ interface CelestrakResponse {
6262
MEAN_MOTION_DOT: number;
6363
MEAN_MOTION_DDOT: number;
6464
fetchTime: Date;
65+
group: string;
6566
}
6667

6768
const Globe: React.FC = () => {
@@ -73,6 +74,7 @@ const Globe: React.FC = () => {
7374
const [controls, setControls] = useState<OrbitControls | null>(null);
7475
const [tooltip, setTooltip] = useState<TooltipState>({ visible: false, text: '', x: 0, y: 0 });
7576
const [popup, setPopup] = useState<PopupState>({ visible: false, data: null, x: 0, y: 0 });
77+
const [isPopupVisible, setIsPopupVisible] = useState(false);
7678
const [satellites, setSatellites] = useState<SatelliteData[]>([]);
7779
const [activeGroup, setActiveGroup] = useState<string>('stations');
7880
const [selectedSatellite, setSelectedSatellite] = useState<SatelliteData | null>(null);
@@ -276,6 +278,7 @@ const Globe: React.FC = () => {
276278
orbitLinesRef.current.forEach(line => {
277279
if (line.material instanceof THREE.MeshBasicMaterial) {
278280
line.material.opacity = 0;
281+
line.material.color.setHex(0xFAC515);
279282
}
280283
});
281284

@@ -284,6 +287,7 @@ const Globe: React.FC = () => {
284287
if (clickedIndex !== -1 && orbitLinesRef.current[clickedIndex]) {
285288
const lineMaterial = orbitLinesRef.current[clickedIndex].material as THREE.MeshBasicMaterial;
286289
lineMaterial.opacity = 0.8;
290+
lineMaterial.color.setHex(0xFAC515);
287291
}
288292

289293
// Get the satellite's position for camera movement
@@ -405,15 +409,19 @@ const Globe: React.FC = () => {
405409
y = rect.top + padding;
406410
}
407411

408-
// Add a small delay before showing the popup for smoother animation
412+
// First hide the current popup
413+
setIsPopupVisible(false);
414+
415+
// After a short delay, show the new popup
409416
setTimeout(() => {
410417
setPopup({
411418
visible: true,
412419
data: satelliteData.data,
413420
x,
414421
y
415422
});
416-
}, 100);
423+
setIsPopupVisible(true);
424+
}, 300); // Match this with the transition duration
417425
}
418426
};
419427

@@ -422,11 +430,14 @@ const Globe: React.FC = () => {
422430
}
423431
} else {
424432
// If clicking outside of a satellite, hide the popup and orbit line
425-
setPopup({ visible: false, data: null, x: 0, y: 0 });
426-
if (activeOrbit && scene) {
427-
scene.remove(activeOrbit);
428-
setActiveOrbit(null);
429-
}
433+
setIsPopupVisible(false);
434+
setTimeout(() => {
435+
setPopup({ visible: false, data: null, x: 0, y: 0 });
436+
if (activeOrbit && scene) {
437+
scene.remove(activeOrbit);
438+
setActiveOrbit(null);
439+
}
440+
}, 300); // Match this with the transition duration
430441
}
431442
};
432443

@@ -541,6 +552,12 @@ const Globe: React.FC = () => {
541552

542553
setActiveGroup(group);
543554

555+
// Hide the popup immediately
556+
setIsPopupVisible(false);
557+
setTimeout(() => {
558+
setPopup({ visible: false, data: null, x: 0, y: 0 });
559+
}, 300); // Match the transition duration
560+
544561
// Clear existing orbit lines
545562
if (scene) {
546563
orbitLinesRef.current.forEach(line => {
@@ -763,70 +780,39 @@ const Globe: React.FC = () => {
763780
// Add first point again to close the loop
764781
points.push(points[0].clone());
765782

766-
// Create line geometry
767-
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
768-
769-
// Create a wider line using a mesh
770-
const lineWidth = 0.05; // Base width of the line
771-
const positions = new Float32Array(points.length * 6 * 3); // 6 vertices per segment (2 triangles)
772-
const indices = new Uint16Array((points.length - 1) * 6); // 6 indices per segment (2 triangles)
773-
774-
// Create vertices and indices for the line mesh
775-
for (let i = 0; i < points.length - 1; i++) {
776-
const p1 = points[i];
777-
const p2 = points[i + 1];
778-
779-
// Calculate direction vector
780-
const direction = p2.clone().sub(p1).normalize();
781-
782-
// Calculate perpendicular vector in the plane
783-
const perpendicular = new THREE.Vector3().crossVectors(direction, p1.clone().normalize()).normalize();
784-
785-
// Create vertices for the segment
786-
const v1 = p1.clone().add(perpendicular.clone().multiplyScalar(lineWidth));
787-
const v2 = p1.clone().sub(perpendicular.clone().multiplyScalar(lineWidth));
788-
const v3 = p2.clone().add(perpendicular.clone().multiplyScalar(lineWidth));
789-
const v4 = p2.clone().sub(perpendicular.clone().multiplyScalar(lineWidth));
790-
791-
// Add vertices to positions array
792-
const vertices = [v1, v2, v3, v4];
793-
vertices.forEach((v, j) => {
794-
positions[i * 12 + j * 3] = v.x;
795-
positions[i * 12 + j * 3 + 1] = v.y;
796-
positions[i * 12 + j * 3 + 2] = v.z;
797-
});
798-
799-
// Add indices for two triangles
800-
const baseIndex = i * 4;
801-
indices[i * 6] = baseIndex;
802-
indices[i * 6 + 1] = baseIndex + 1;
803-
indices[i * 6 + 2] = baseIndex + 2;
804-
indices[i * 6 + 3] = baseIndex + 1;
805-
indices[i * 6 + 4] = baseIndex + 3;
806-
indices[i * 6 + 5] = baseIndex + 2;
807-
}
783+
// Create a curve from the points
784+
const curve = new THREE.CatmullRomCurve3(points);
808785

809-
const meshGeometry = new THREE.BufferGeometry();
810-
meshGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
811-
meshGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
786+
// Create tube geometry
787+
const tubeGeometry = new THREE.TubeGeometry(
788+
curve,
789+
segments,
790+
0.02, // tube radius
791+
8, // radial segments
792+
false // closed
793+
);
812794

813-
// Create material that will always face the camera
795+
// Create material for the tube
814796
const material = new THREE.MeshBasicMaterial({
815-
color: 0xffffff,
797+
color: 0xFAC515, // Golden yellow color
816798
transparent: true,
817799
opacity: 0, // Start invisible
818800
side: THREE.DoubleSide,
819801
depthTest: false
820802
});
821803

822-
const lineMesh = new THREE.Mesh(meshGeometry, material);
823-
lineMesh.renderOrder = 1;
804+
const tubeMesh = new THREE.Mesh(tubeGeometry, material);
805+
tubeMesh.renderOrder = 1;
824806

825-
return lineMesh;
807+
return tubeMesh;
826808
};
827809

828810
const handleSatelliteClick = (satellite: SatelliteData) => {
829811
setSelectedSatellite(satellite);
812+
813+
// Hide current popup immediately
814+
setIsPopupVisible(false);
815+
830816
// Find the satellite mesh
831817
const satelliteMesh = satelliteMeshesRef.current.find(
832818
sat => sat.data.noradId === satellite.noradId
@@ -837,6 +823,7 @@ const Globe: React.FC = () => {
837823
orbitLinesRef.current.forEach(line => {
838824
if (line.material instanceof THREE.MeshBasicMaterial) {
839825
line.material.opacity = 0;
826+
line.material.color.setHex(0xFAC515);
840827
}
841828
});
842829

@@ -845,6 +832,7 @@ const Globe: React.FC = () => {
845832
if (clickedIndex !== -1 && orbitLinesRef.current[clickedIndex]) {
846833
const lineMaterial = orbitLinesRef.current[clickedIndex].material as THREE.MeshBasicMaterial;
847834
lineMaterial.opacity = 0.8;
835+
lineMaterial.color.setHex(0xFAC515);
848836
}
849837

850838
// Get the satellite's position
@@ -887,6 +875,57 @@ const Globe: React.FC = () => {
887875
} else {
888876
// Re-enable controls after animation
889877
controls.enabled = true;
878+
879+
// Show popup after camera animation
880+
const screenPosition = satellitePosition.clone().project(camera);
881+
882+
// Check if satellite is behind the globe (z > 1)
883+
if (screenPosition.z > 1) {
884+
return;
885+
}
886+
887+
const rect = renderer?.domElement.getBoundingClientRect();
888+
if (!rect) return;
889+
890+
// Position popup at bottom right of satellite dot with 1px spacing
891+
const dotSize = SATELLITE_SIZE * 100; // Convert to pixels
892+
let x = ((screenPosition.x * 0.5 + 0.5) * rect.width) + rect.left;
893+
let y = (-(screenPosition.y * 0.5 - 0.5) * rect.height) + rect.top;
894+
895+
// Ensure popup stays within viewport
896+
const popupWidth = 305;
897+
const popupHeight = 174;
898+
const padding = 10;
899+
900+
// Adjust x position if popup would go off the right edge
901+
if (x + popupWidth > rect.right - padding) {
902+
x = x - popupWidth - dotSize - 1; // Position to the left of the dot
903+
} else {
904+
x = x + dotSize + 1; // Position to the right of the dot
905+
}
906+
907+
// Adjust x position if popup would go off the left edge
908+
if (x < rect.left + padding) {
909+
x = rect.left + padding;
910+
}
911+
912+
// Adjust y position if popup would go off the bottom edge
913+
if (y + popupHeight > rect.bottom - padding) {
914+
y = rect.bottom - popupHeight - padding;
915+
}
916+
// Adjust y position if popup would go off the top edge
917+
if (y < rect.top + padding) {
918+
y = rect.top + padding;
919+
}
920+
921+
// Update popup position and show it
922+
setPopup({
923+
visible: true,
924+
data: satellite,
925+
x,
926+
y
927+
});
928+
setIsPopupVisible(true);
890929
}
891930
};
892931

@@ -989,6 +1028,7 @@ const Globe: React.FC = () => {
9891028
data={popup.data}
9901029
x={popup.x}
9911030
y={popup.y}
1031+
isVisible={isPopupVisible}
9921032
/>
9931033
)}
9941034
{/* <SidePanel satellites={satellites} /> */}

components/SatellitePopup.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,42 @@
11
import { SatelliteData } from '@/services/types';
2+
import { useEffect, useState } from 'react';
23

34
interface SatellitePopupProps {
45
data: SatelliteData;
56
x: number;
67
y: number;
8+
isVisible: boolean;
79
}
810

9-
const SatellitePopup: React.FC<SatellitePopupProps> = ({ data, x, y }) => {
11+
const SatellitePopup: React.FC<SatellitePopupProps> = ({ data, x, y, isVisible }) => {
12+
const [opacity, setOpacity] = useState(0);
1013
const EARTH_RADIUS = 6371; // Earth radius in km
1114

15+
useEffect(() => {
16+
if (isVisible) {
17+
// Fade in
18+
setOpacity(1);
19+
} else {
20+
// Fade out
21+
setOpacity(0);
22+
}
23+
}, [isVisible]);
24+
25+
// Get the appropriate image based on the satellite's group
26+
const getSatelliteImage = () => {
27+
const group = data.rawData?.group || '';
28+
switch (group.toLowerCase()) {
29+
case 'globalstar':
30+
return '/Globalstar_1.webp';
31+
case 'intelsat':
32+
return '/Intelsat.jpg';
33+
case 'stations':
34+
return '/SpaceStation.avif';
35+
default:
36+
return '/default.jpg';
37+
}
38+
};
39+
1240
return (
1341
<div
1442
style={{
@@ -20,11 +48,14 @@ const SatellitePopup: React.FC<SatellitePopupProps> = ({ data, x, y }) => {
2048
border: '1px solid rgba(255, 255, 255, 0.3)',
2149
background: 'rgba(0, 0, 0, 0.8)',
2250
zIndex: 1000,
51+
opacity: opacity,
52+
transition: 'opacity 0.3s ease-in-out',
53+
pointerEvents: opacity > 0 ? 'auto' : 'none',
2354
}}
2455
>
2556
<div style={{ position: 'relative', width: '305px', height: '174px' }}>
2657
<img
27-
src="/default.jpg"
58+
src={getSatelliteImage()}
2859
alt={data.name}
2960
style={{
3061
width: '100%',

public/Globalstar_1.webp

82.5 KB
Loading

public/Intelsat.jpg

135 KB
Loading

public/SpaceStation.avif

266 KB
Binary file not shown.

services/data.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Dexie, { Table } from 'dexie';
2+
import { CelestrakResponse } from './types';
23

34
interface CelestrakResponse {
45
NORAD_CAT_ID: number;
@@ -119,26 +120,21 @@ const fetchSatellitePositions = async (group: string): Promise<CelestrakResponse
119120
}
120121
};
121122

122-
const getSatelliteData = async (group: string = 'stations'): Promise<CelestrakResponse[]> => {
123-
// First try to get data from Dexie/IndexedDB
124-
const cachedData = await getSatelliteDataFromDB(group);
125-
126-
const oldestFetchTime = cachedData.length > 0
127-
? cachedData.reduce((min, sat) => Math.min(min, sat.fetchTime.getTime()), Infinity)
128-
: null;
129-
const oneHourAgo = Date.now() - 1 * 60 * 60 * 1000;
130-
131-
if (oldestFetchTime && oldestFetchTime > oneHourAgo) {
132-
console.log('Retrieved fresh satellite data from Dexie/IndexedDB');
133-
return cachedData;
134-
} else if (oldestFetchTime) {
135-
console.log('Cached data is too old, fetching new data.');
136-
} else {
137-
console.log('No cached data found, fetching new data.');
123+
export const getSatelliteData = async (group: string): Promise<CelestrakResponse[]> => {
124+
try {
125+
const response = await fetch(`https://celestrak.org/NORAD/elements/gp.php?GROUP=${group}&FORMAT=json`);
126+
const data = await response.json();
127+
128+
// Add group information to each satellite
129+
return data.map((satellite: CelestrakResponse) => ({
130+
...satellite,
131+
group: group,
132+
fetchTime: new Date()
133+
}));
134+
} catch (error) {
135+
console.error('Error fetching satellite data:', error);
136+
return [];
138137
}
139-
140-
// If no fresh cached data, fetch from API
141-
return fetchSatellitePositions(group);
142138
};
143139

144140
export type { CelestrakResponse };

services/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,25 @@ interface SatelliteData {
1717
}
1818

1919
export type { SatelliteData };
20+
21+
export interface CelestrakResponse {
22+
NORAD_CAT_ID: number;
23+
OBJECT_ID: string;
24+
OBJECT_NAME: string;
25+
EPOCH: string;
26+
MEAN_MOTION: number;
27+
ECCENTRICITY: number;
28+
INCLINATION: number;
29+
RA_OF_ASC_NODE: number;
30+
ARG_OF_PERICENTER: number;
31+
MEAN_ANOMALY: number;
32+
EPHEMERIS_TYPE: number;
33+
CLASSIFICATION_TYPE: string;
34+
ELEMENT_SET_NO: number;
35+
REV_AT_EPOCH: number;
36+
BSTAR: number;
37+
MEAN_MOTION_DOT: number;
38+
MEAN_MOTION_DDOT: number;
39+
fetchTime: Date;
40+
group: string;
41+
}

0 commit comments

Comments
 (0)