diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2d6a206..d714e15 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12389,9 +12389,9 @@ } }, "node_modules/framer-motion": { - "version": "11.0.24", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.24.tgz", - "integrity": "sha512-l2iM8NR53qtcujgAqYvGPJJGModPNWEVUaATRDLfnaLvUoFpImovBm0AHalSSsY8tW6knP8mfJTW4WYGbnAe4w==", + "version": "11.0.25", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.25.tgz", + "integrity": "sha512-mRt7vQGzA7++wTgb+PW1TrlXXgndqR6hCiJ48fXr2X9alte2hPQiAq556HRwDCt0Q5X98MNvcSe4KUa27Gm5Lg==", "dependencies": { "tslib": "^2.4.0" }, diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 3ac745f..a4dea33 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -3,21 +3,35 @@ import ColoredSection from "@/components/ui/coloredSection"; import Image from "next/image"; import Link from "next/link"; -import SatelliteDataTableMultiple from "@/components/satelliteData/SatelliteDataTableMultiple"; -import fetchSatelliteData from "@components/map/SatelliteFetcher"; import fetchMostRecentImage from "@/lib/data/fetchMostRecentImage"; +import SatelliteDataHome from "@/components/satelliteData/SatelliteDataHome"; +import SatelliteSelector from "@/components/SatelliteSelector"; +import SatelliteGlobe from "@/components/map/newGlobe"; + export default async function Home() { const mostRecentImageURL = await fetchMostRecentImage(); return ( <> -
-
- +
+ {/* Stats Container */} +
+
+ +
+
+ +
+
+
+ + {/* Globe Container */} +
+
+ +
diff --git a/frontend/src/components/SatelliteSelector.tsx b/frontend/src/components/SatelliteSelector.tsx new file mode 100644 index 0000000..a07feed --- /dev/null +++ b/frontend/src/components/SatelliteSelector.tsx @@ -0,0 +1,46 @@ +"use client"; +import React from "react"; +import { useSatelliteStore } from "@/lib/store"; + +export default function SatelliteSelector() { + const satelliteNames = useSatelliteStore((state) => state.satelliteNames); + const selectedSatellite = useSatelliteStore( + (state) => state.selectedSatellite, + ); + const setSelectedSatellite = useSatelliteStore( + (state) => state.setSelectedSatellite, + ); + const fetchAndSetSatelliteData = useSatelliteStore( + (state) => state.fetchAndSetSatelliteData, + ); + + const handleChange = (event: React.ChangeEvent) => { + setSelectedSatellite(event.target.value); + }; + + for (const satellite of satelliteNames) { + fetchAndSetSatelliteData(satellite); + } + + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/map/MyGlobe.tsx b/frontend/src/components/map/MyGlobe.tsx index 9859cb0..04a0bdc 100644 --- a/frontend/src/components/map/MyGlobe.tsx +++ b/frontend/src/components/map/MyGlobe.tsx @@ -14,7 +14,7 @@ import { SatelliteData } from "@/lib/mapHelpers"; const EARTH_RADIUS_KM = 6371; // km const SAT_SIZE = 500; // km -const TIME_STEP = 1 * 1000; // per frame +const TIME_STEP = 1; // per frame //const SATELLITE_AMOUNT = 100; // amount of satellites to display export function mapRawDataToTleData(rawData: string): string[][] { diff --git a/frontend/src/components/map/newGlobe.tsx b/frontend/src/components/map/newGlobe.tsx new file mode 100644 index 0000000..304ac06 --- /dev/null +++ b/frontend/src/components/map/newGlobe.tsx @@ -0,0 +1,152 @@ +"use client"; +import React, { useEffect, useRef } from "react"; +import Globe, { GlobeInstance } from "globe.gl"; +import * as THREE from "three"; +import { useSatelliteStore } from "@/lib/store"; +import { convertSatrec } from "@/lib/convertSatrec"; + +const SAT_RADIUS = 5; // Relative size of the satellite for visualization +const UPDATE_INTERVAL_MS = 10; // Update interval in milliseconds +const EARTH_RADIUS_KM = 6371; // Earth radius in kilometers + +export default function SatelliteGlobe() { + const chart = useRef(null); + const globeRef = useRef(); + const { satelliteData, selectedSatellite, setSelectedSatellite } = + useSatelliteStore((state) => ({ + satelliteData: state.satelliteData, + selectedSatellite: state.selectedSatellite, + setSelectedSatellite: state.setSelectedSatellite, + })); + + // Initialize the globe + useEffect(() => { + if (chart.current && !globeRef.current) { + globeRef.current = Globe()(chart.current) + .globeImageUrl( + "//unpkg.com/three-globe/example/img/earth-blue-marble.jpg", + )/*.backgroundImageUrl( + "//unpkg.com/three-globe/example/img/night-sky.png", + )*/ + .objectLat("lat") + .objectLng("lng") + .objectAltitude("alt") + .objectFacesSurface(false) + .backgroundColor("rgba(0,0,0,0)") + .objectLabel("name") + .objectsData([]) + .objectThreeObject((sat: any) => { + return new THREE.Mesh( + new THREE.SphereGeometry(SAT_RADIUS, 16, 8), + new THREE.MeshBasicMaterial({ color: sat.color }), + ); + }) + .onObjectClick((obj: any) => { + setSelectedSatellite(obj.name); + }); + + // Set initial POV after globe instantiation + setTimeout(() => { + if (globeRef.current) { + globeRef.current.pointOfView({ altitude: 3.5 }); + } + }); + + globeRef.current.controls().enabled = true; + globeRef.current.controls().enableZoom = false; + + // Define the handleResize function + const handleResize = () => { + if (globeRef.current) { + if (window.innerWidth <= 768) { + globeRef.current.width(window.innerWidth); + globeRef.current.height(window.innerHeight); + } else { + globeRef.current.width(window.innerWidth); + globeRef.current.height(window.innerHeight); + } + } + }; + + // Handle the resize event + window.addEventListener("resize", handleResize); + handleResize(); // Call it initially to set the size + + // Set initial positions of satellites + let currentDate = new Date().toISOString(); + const initialPositions = Object.values(satelliteData).map( + (sat) => ({ + lat: parseFloat( + convertSatrec(sat.satrec, currentDate).latitudeDeg, + ), + lng: parseFloat( + convertSatrec(sat.satrec, currentDate).longitudeDeg, + ), + alt: + parseFloat( + convertSatrec(sat.satrec, currentDate).altitude, + ) / EARTH_RADIUS_KM, + name: sat.name, + }), + ); + globeRef.current.objectsData(initialPositions); + + return () => { + window.removeEventListener("resize", handleResize); + }; + } + }, []); + + // Update satellite positions periodically, or when satelliteData changes + useEffect(() => { + const intervalId = setInterval(() => { + const currentDate = new Date().toISOString(); + + if (globeRef.current) { + const newPositions = Object.values(satelliteData).map((sat) => { + return { + lat: parseFloat( + convertSatrec(sat.satrec, currentDate).latitudeDeg, + ), + lng: parseFloat( + convertSatrec(sat.satrec, currentDate).longitudeDeg, + ), + alt: + parseFloat( + convertSatrec(sat.satrec, currentDate).altitude, + ) / EARTH_RADIUS_KM, + name: sat.name, + color: + selectedSatellite === sat.name + ? "red" + : "palegreen", + }; + }); + + globeRef.current.objectsData(newPositions); + } + }, UPDATE_INTERVAL_MS); + + if (satelliteData[selectedSatellite] === undefined) { + return; + } + + const targetPosition = convertSatrec( + satelliteData[selectedSatellite].satrec, + new Date().toISOString(), + ); + + globeRef?.current?.pointOfView( + { + lat: Number(targetPosition.latitudeDeg), + lng: Number(targetPosition.longitudeDeg), + altitude: 2.5, + }, + 1700, + ); + + return () => clearInterval(intervalId); + }); + + return
; +} diff --git a/frontend/src/components/satelliteData/SatelliteDataHome.tsx b/frontend/src/components/satelliteData/SatelliteDataHome.tsx new file mode 100644 index 0000000..9daa27d --- /dev/null +++ b/frontend/src/components/satelliteData/SatelliteDataHome.tsx @@ -0,0 +1,93 @@ +"use client"; +import { useState, useEffect } from "react"; +import { convertSatrec, SatelliteInfo } from "@/lib/convertSatrec"; +import { useSatelliteStore } from "@/lib/store"; + +const updateInterval = 10; + +export default function SatelliteDataHome() { + const { satelliteData, fetchAndSetSatelliteData, selectedSatellite } = + useSatelliteStore(); + const [satelliteInfo, setSatelliteInfo] = useState( + null, + ); + + // Fetch satellite data on component mount or when selectedSatellite changes + useEffect(() => { + if (selectedSatellite) { + fetchAndSetSatelliteData(selectedSatellite); + } + }, [fetchAndSetSatelliteData, selectedSatellite]); + + // Update satellite info every `updateInterval` ms + useEffect(() => { + const intervalId = setInterval(() => { + if (selectedSatellite) { + // Access satellite data by name + const satData = satelliteData[selectedSatellite]; + if (satData) { + const updatedInfo = convertSatrec( + satData.satrec, + satData.name, + ); + setSatelliteInfo(updatedInfo); + } + } + }, updateInterval); + + // Clear interval on component unmount + return () => clearInterval(intervalId); + }, [satelliteData, selectedSatellite]); + + return ( +
+
+
+

+ {satelliteInfo + ? satelliteInfo.velocity + " km/h" + : "Loading..."} +

+

Velocity

+
+
+

+ {satelliteInfo + ? satelliteInfo.altitude + " km" + : "Loading..."} +

+

Altitude

+
+
+

+ {satelliteInfo + ? satelliteInfo.latitudeDeg + "° N" + : "Loading..."} +

+

Latitude

+
+
+

+ {satelliteInfo + ? satelliteInfo.longitudeDeg + "° E" + : "Loading..."} +

+

Longitude

+
+
+ +
+
+

+ {satelliteInfo + ? "Above " + satelliteInfo.country + : "Loading..."} +

+
+
+

Flag Icon

+
+
+
+ ); +} diff --git a/frontend/src/components/satelliteData/SatelliteDataTable.tsx b/frontend/src/components/satelliteData/SatelliteDataTable.tsx index d9ffa3a..0b286d8 100644 --- a/frontend/src/components/satelliteData/SatelliteDataTable.tsx +++ b/frontend/src/components/satelliteData/SatelliteDataTable.tsx @@ -1,75 +1,129 @@ +// Ensure all necessary imports are present "use client"; -import { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; +import { Combobox } from "../Combobox"; // Adjust the path as necessary +import { + mapRawDataToTleData, + mapTleToSatData, + SatelliteData, +} from "@/lib/mapHelpers"; import { convertSatrec, SatelliteInfo } from "@/lib/convertSatrec"; -import { useSatelliteStore } from "@/lib/store"; const updateInterval = 10; -export default function SatelliteDataTable({ satName }: { satName: string }) { - const { satelliteData, fetchAndSetSatelliteData } = useSatelliteStore(); +interface ClientOnlyComponentProps { + fetchSatelliteData: ({ + // eslint-disable-next-line no-unused-vars + useExampleData, + }: { + useExampleData: boolean; + filterList?: string[]; + }) => Promise; +} + +const SatelliteDataTable: React.FC = ({ + fetchSatelliteData, +}) => { + const [satelliteData, setSatelliteData] = useState([]); + const [selectedSatellite, setSelectedSatellite] = useState< + SatelliteData | undefined + >(); const [satelliteInfo, setSatelliteInfo] = useState( null, ); // Fetch satellite data on component mount useEffect(() => { - fetchAndSetSatelliteData(satName); - }, [fetchAndSetSatelliteData, satName]); + const fetchData = async () => { + const rawData = await fetchSatelliteData({ useExampleData: true }); + + const tleData = mapRawDataToTleData(rawData); + + const mappedData = mapTleToSatData(tleData).slice(0, 10); + setSatelliteData(mappedData); + if (mappedData.length > 0) { + updateSatelliteInfo(mappedData[0]); + setSelectedSatellite(mappedData[0]); // Ensure the first satellite is selected by default + } + }; + + fetchData(); + }, [fetchSatelliteData]); + + // Function to update satellite info based on selected satellite + const updateSatelliteInfo = (satellite: SatelliteData) => { + const sat = satelliteData.find((s) => s.name === satellite.name); + if (sat) { + const info = convertSatrec(sat.satrec, sat.name); + setSatelliteInfo(info); + } + }; + + // Handle satellite selection from Combobox + const handleSelectSatellite = (value: SatelliteData) => { + setSelectedSatellite(value); + updateSatelliteInfo(value); + }; - // Update satellite info every `updateInterval` ms useEffect(() => { const intervalId = setInterval(() => { // Access satellite data by name - const satData = satelliteData[satName]; - if (satData) { - const updatedInfo = convertSatrec(satData.satrec, satData.name); + if (selectedSatellite) { + const updatedInfo = convertSatrec( + selectedSatellite.satrec, + selectedSatellite.name, + ); setSatelliteInfo(updatedInfo); } }, updateInterval); // Clear interval on component unmount return () => clearInterval(intervalId); - }, [satelliteData, satName]); - - // Display loading message if satellite info is not available - if (!satelliteInfo) { - return ( -
-

Loading...

-
- ); - } + }, [selectedSatellite]); - return ( -
-
-

{satelliteInfo.name}

-
{/* Include the dropdown arrow icon here */}
-
- -
-
-

{satelliteInfo.velocity} km/s

-

Velocity

-
-
-

{satelliteInfo.altitude} km

-

Altitude

+ return satelliteData.length > 0 && selectedSatellite && satelliteInfo ? ( + <> +
+
+ +
{/* Include the dropdown arrow icon here */}
-
-

{satelliteInfo.latitudeDeg}° N

-

Latitude

-
-
-

{satelliteInfo.longitudeDeg}° E

-

Longitude

+ +
+
+

{satelliteInfo.velocity} km/s

+

Velocity

+
+
+

{satelliteInfo.altitude} km

+

Altitude

+
+
+

+ {satelliteInfo.latitudeDeg}° N +

+

Latitude

+
+
+

+ {satelliteInfo.longitudeDeg}° E +

+

Longitude

+
-
-
-

Above {satelliteInfo.country}

+
+

Above {satelliteInfo.country}

+
+
{/* Include the flag icon here */}
-
{/* Include the flag icon here */}
-
+ + ) : ( +
Loading...
); -} +}; + +export default SatelliteDataTable; diff --git a/frontend/src/components/satelliteData/SatelliteDataTableSingle.tsx b/frontend/src/components/satelliteData/SatelliteDataTableSingle.tsx new file mode 100644 index 0000000..8d5fc4e --- /dev/null +++ b/frontend/src/components/satelliteData/SatelliteDataTableSingle.tsx @@ -0,0 +1,81 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { convertSatrec, SatelliteInfo } from "@/lib/convertSatrec"; +import { useSatelliteStore } from "@/lib/store"; + +const updateInterval = 10; + +export default function SatelliteDataTable({ + satName, + combobox, +}: { + satName: string; + combobox: React.ReactNode; +}) { + const { satelliteData, fetchAndSetSatelliteData } = useSatelliteStore(); + const [satelliteInfo, setSatelliteInfo] = useState( + null, + ); + + // Fetch satellite data on component mount + useEffect(() => { + fetchAndSetSatelliteData(satName); + }, [fetchAndSetSatelliteData, satName]); + + // Update satellite info every `updateInterval` ms + useEffect(() => { + const intervalId = setInterval(() => { + // Access satellite data by name + const satData = satelliteData[satName]; + if (satData) { + const updatedInfo = convertSatrec(satData.satrec, satData.name); + setSatelliteInfo(updatedInfo); + } + }, updateInterval); + + // Clear interval on component unmount + return () => clearInterval(intervalId); + }, [satelliteData, satName]); + + // Display loading message if satellite info is not available + if (!satelliteInfo) { + return ( +
+

Loading...

+
+ ); + } + + return ( +
+
+
{combobox}
+

{satelliteInfo.name}

+
+ +
+
+

{satelliteInfo.velocity} km/s

+

Velocity

+
+
+

{satelliteInfo.altitude} km

+

Altitude

+
+
+

{satelliteInfo.latitudeDeg}° N

+

Latitude

+
+
+

{satelliteInfo.longitudeDeg}° E

+

Longitude

+
+
+ +
+

Above {satelliteInfo.country}

+
+
{/* Include the flag icon here */}
+
+ ); +} diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index 130e774..e87b6d1 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -3,30 +3,41 @@ import { create } from "zustand"; import type { SatelliteData } from "@/lib/getSatelliteData"; import { satLoader } from "@/lib/getSatelliteData"; -interface SatelliteStore { +// Define the state and actions separately +interface SatelliteState { satelliteData: Record; satelliteNames: string[]; + selectedSatellite: string; +} +interface SatelliteActions { setSatelliteData: (satName: string, data: SatelliteData) => void; fetchAndSetSatelliteData: (satName: string) => Promise; - - getSatelliteNames: () => void; + setSelectedSatellite: (satName: string) => void; } -export const useSatelliteStore = create((set) => ({ +type SatelliteStore = SatelliteState & SatelliteActions; + +// Create satellite store. Update selectedSatellite if you want a different default +export const useSatelliteStore = create((set, get) => ({ satelliteData: {}, - satelliteNames: [], + satelliteNames: ["HYPSO-1", "UME (ISS)", "VANGUARD 1", "STARLINK-1007"], + selectedSatellite: "HYPSO-1", + setSatelliteData: (satName, data) => set((state) => ({ satelliteData: { ...state.satelliteData, [satName]: data }, })), fetchAndSetSatelliteData: async (satName) => { - // Fetch data with the loader, which should handle caching internally const newData = await satLoader(satName); set((state) => ({ satelliteData: { ...state.satelliteData, [satName]: newData }, })); }, - getSatelliteNames: () => {}, + setSelectedSatellite: (satName) => { + set(() => ({ + selectedSatellite: satName, + })); + }, }));