diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index a4dea33..b9fbf72 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -15,9 +15,9 @@ export default async function Home() { return ( <> -
+
{/* Stats Container */} -
+
@@ -29,7 +29,7 @@ export default async function Home() { {/* Globe Container */}
-
+
diff --git a/frontend/src/app/satellites/[satelliteSlug]/page.tsx b/frontend/src/app/satellites/[satelliteSlug]/page.tsx index 3c58b9c..e8c44ba 100644 --- a/frontend/src/app/satellites/[satelliteSlug]/page.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/page.tsx @@ -1,6 +1,5 @@ export const runtime = "edge"; import BlockRendererClient from "@/components/BlockRendererClient"; -import SatelliteFetcher from "@/components/map/SatelliteFetcher"; import fetchSatelliteInfo from "@/lib/data/fetchSatelliteInfo"; import { BlocksContent } from "@strapi/blocks-react-renderer"; import RelatedProjectsAndSatellites from "@/components/RelatedProjectsAndSatellites"; @@ -31,10 +30,6 @@ export default async function SatelliteInfoPage({ return (
-

{satelliteInfo.name}

diff --git a/frontend/src/components/map/MyCustomMap.tsx b/frontend/src/components/map/MyCustomMap.tsx deleted file mode 100644 index 219c79e..0000000 --- a/frontend/src/components/map/MyCustomMap.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; -import React, { useEffect, useRef } from "react"; -import Map from "ol/Map"; -import View from "ol/View"; -import { GeoJSON } from "ol/format"; -import VectorSource from "ol/source/Vector"; -import VectorLayer from "ol/layer/Vector"; -import "ol/ol.css"; -import { Feature } from "ol"; -import { Geometry } from "ol/geom"; -import globeData from "@components/map/githubglobe/files/globe-data.json"; - -export default function Map2d() { - const mapRef = useRef(null); - - useEffect(() => { - const map = new Map({ - target: mapRef.current!, - view: new View({ - center: [0, 0], - zoom: 2, - projection: "EPSG:4326", - }), - }); - - const vectorSource = new VectorSource({ - features: new GeoJSON().readFeatures(globeData, { - dataProjection: "EPSG:4326", - featureProjection: "EPSG:3857", - }) as Feature[], - }); - - const vectorLayer = new VectorLayer({ - source: vectorSource, - }); - - map.addLayer(vectorLayer); - map.getView().fit(vectorSource.getExtent(), { - padding: [50, 50, 50, 50], - }); - - return () => map.setTarget(undefined); - }, []); - - return ( -
-

2D Map

-
-
- ); -} diff --git a/frontend/src/components/map/MyGlobe.tsx b/frontend/src/components/map/MyGlobe.tsx deleted file mode 100644 index 04a0bdc..0000000 --- a/frontend/src/components/map/MyGlobe.tsx +++ /dev/null @@ -1,148 +0,0 @@ -"use client"; -import Globe, { GlobeInstance } from "globe.gl"; -import * as THREE from "three"; -import React, { useEffect } from "react"; -import * as satellite from "satellite.js"; -import { SatelliteData } from "@/lib/mapHelpers"; - -// made with the following packages: -// https://www.npmjs.com/package/globe.gl -// https://www.npmjs.com/package/satellite.js -// https://www.npmjs.com/package/three - -// could possibly switch to https://www.npmjs.com/package/tle.js for TLE parsing, as it i mostly a wrapper around satellite.js - -const EARTH_RADIUS_KM = 6371; // km -const SAT_SIZE = 500; // km -const TIME_STEP = 1; // per frame -//const SATELLITE_AMOUNT = 100; // amount of satellites to display - -export function mapRawDataToTleData(rawData: string): string[][] { - return ( - rawData - // Remove any carriage returns - .replace(/\r/g, "") - // Split the data into individual TLEs (https://en.wikipedia.org/wiki/Two-line_element_set). - /* It splits the string at newline characters (\n) only if they are followed by a character that is not 1 or 2. The (?=[^12]) is a positive lookahead assertion, - ensuring that the newline is followed by a character that is not 1 or 2 without including that character in the split result.*/ - .split(/\n(?=[^12])/) - //This step filters out any empty lines from the array of substrings obtained in the previous step. The callback function (d) => d checks if the substring d is truthy, effectively removing empty lines. - .filter((d) => d) - /* Finally, this step maps each substring (now representing a line) into an array of lines. - It splits each substring again using the newline character (\n) as the delimiter. - This results in a two-dimensional array where each element is an array of lines from the original string. */ - .map((tle) => tle.split("\n")) - ); -} - -interface MyGlobeProps { - satelliteDatas: SatelliteData[]; // Existing prop: a string of TLE strings - selectedSatellite?: SatelliteData; -} - -export default function MyGlobe({ - satelliteDatas, - selectedSatellite, -}: MyGlobeProps) { - const chart = React.useRef(null); - - // useEffect is used because we want to run the code only once when the component is mounted - useEffect(() => { - if (chart.current) { - let myGlobe: GlobeInstance; - myGlobe = Globe()(chart.current) - .globeImageUrl( - "//unpkg.com/three-globe/example/img/earth-blue-marble.jpg", - ) - .objectLat("lat") - .objectLng("lng") - .objectAltitude("alt") - .objectFacesSurface(false) - .backgroundColor("rgba(0,0,0,0)") - .objectLabel("name"); - - // Set initial camera distance - setTimeout(() => myGlobe.pointOfView({ altitude: 3.5 })); - - const handleResize = () => { - //Making it responsive like this - - if (window.innerWidth <= 768) { - myGlobe.width(window.innerWidth); - myGlobe.height(window.innerHeight / 2); - } else { - myGlobe.width(window.innerWidth / 2); - myGlobe.height(window.innerHeight / 2); - } - }; - handleResize(); - window.addEventListener("resize", handleResize); - - // Disable OrbitControls and enable auto-rotation - // myGlobe.controls().autoRotate = true; - myGlobe.controls().enabled = true; - // Disable zooming - myGlobe.controls().enableZoom = false; - // Invert rotation direction - // myGlobe.controls().autoRotateSpeed *= -1; - - // Make the satellite geometry using a sphere - const satGeometry = new THREE.SphereGeometry( - (SAT_SIZE * myGlobe.getGlobeRadius()) / EARTH_RADIUS_KM / 2, - 16, - 8, - ); - - // Make the satellite material - const satMaterial = new THREE.MeshLambertMaterial({ - color: "palegreen", - transparent: true, - opacity: 0.7, - }); - myGlobe.objectThreeObject( - () => new THREE.Mesh(satGeometry, satMaterial), - ); - const satData = satelliteDatas; - - // time ticker - let time = new Date(); - (function frameTicker() { - requestAnimationFrame(frameTicker); - - time = new Date(+time + TIME_STEP); - - // Update satellite positions - - const gmst = satellite.gstime(time); - satData.forEach((d: any) => { - const eci = satellite.propagate(d.satrec, time); - if (eci.position) { - const gdPos = satellite.eciToGeodetic( - eci.position as satellite.EciVec3, - gmst, - ); - - d.lat = THREE.MathUtils.radToDeg(gdPos.latitude); - d.lng = THREE.MathUtils.radToDeg(gdPos.longitude); - d.alt = gdPos.height / EARTH_RADIUS_KM; - - if (d.name == selectedSatellite?.name) { - myGlobe.pointOfView( - { lat: d.lat, lng: d.lng, altitude: 2 }, - 0, - ); - } - } - }); - - myGlobe.objectsData(satData); - })(); - } - }, [selectedSatellite, satelliteDatas]); - - return ( - <> -
- - ); -} diff --git a/frontend/src/components/map/SatelliteFetcher.tsx b/frontend/src/components/map/SatelliteFetcher.tsx deleted file mode 100644 index d33c297..0000000 --- a/frontend/src/components/map/SatelliteFetcher.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use server"; -import { gql } from "@/__generated__/gql"; -import { getClient } from "@/lib/ApolloClient"; - -// Dynamic import because of leaflet and globe.gl ssr problem with next.js - -// Example Datasources -import { exampleData } from "./exampleSatData"; -// Hypso 1 data -// eslint-disable-next-line no-unused-vars -const HYPSO1_TLE_URL = - "https://celestrak.org/NORAD/elements/gp.php?NAME=HYPSO-1&FORMAT=TLE"; - -const GET_ALL_SATELLITE_DATA = - gql(`query Satellites($filters: SatelliteFiltersInput) { - satellites(filters: $filters) { - data { - attributes { - celestrakURL - catalogNumberNORAD - } - } - } - } - `); - -interface SatelliteFetcherInterface { - useExampleData: boolean; - filterList?: string[]; -} - -export default async function fetchSatelliteData({ - useExampleData, - filterList = [], -}: SatelliteFetcherInterface): Promise { - if (useExampleData) { - return exampleData; - } else { - let graphqlData; - - if (filterList.length > 0) { - const filters = { - name: { - in: filterList, - }, - }; - - graphqlData = await getClient().query({ - query: GET_ALL_SATELLITE_DATA, - variables: { - filters, - }, - }); - } else { - graphqlData = await getClient().query({ - query: GET_ALL_SATELLITE_DATA, - }); - } - - const satelliteUrls = graphqlData?.data?.satellites?.data.map( - (satEntity: any) => { - const celestrakURL = satEntity?.attributes?.celestrakURL; - if (celestrakURL) { - return celestrakURL.replace(/FORMAT=[^&]*/, "FORMAT=TLE"); - } - return ( - "https://celestrak.org/NORAD/elements/gp.php?CATNR=" + - satEntity?.attributes?.catalogNumberNORAD - ); - }, - ) as string[]; - - // Fetch every satellite's data from celestrak - let responses: Promise[] = []; - satelliteUrls.forEach((url) => { - responses.push( - fetch(url, { - next: { - revalidate: 10800, // 3 hours - }, - }).then((r) => { - if (r.ok) { - return r.text(); - } - throw new Error("Failed to fetch data from Celestrak"); - }), - ); - }); - // Wait for all the responses to come back - const data = await Promise.all(responses); - - console.log(data); - - let combinedSatelliteDatas = data.join("\n"); - - return combinedSatelliteDatas; - } -} diff --git a/frontend/src/components/map/newGlobe.tsx b/frontend/src/components/map/newGlobe.tsx index b84b199..99998ff 100644 --- a/frontend/src/components/map/newGlobe.tsx +++ b/frontend/src/components/map/newGlobe.tsx @@ -6,7 +6,7 @@ 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 UPDATE_INTERVAL_MS = 100; // Update interval in milliseconds const EARTH_RADIUS_KM = 6371; // Earth radius in kilometers export default function SatelliteGlobe() { @@ -25,7 +25,8 @@ export default function SatelliteGlobe() { globeRef.current = Globe()(chart.current) .globeImageUrl( "//unpkg.com/three-globe/example/img/earth-blue-marble.jpg", - ) /*.backgroundImageUrl( + ) /* + .backgroundImageUrl( "//unpkg.com/three-globe/example/img/night-sky.png", )*/ .objectLat("lat") @@ -55,22 +56,20 @@ export default function SatelliteGlobe() { 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); - } + const setGlobeSize = () => { + if (globeRef.current && chart.current) { + const { width, height } = + chart.current.getBoundingClientRect(); + globeRef.current.width(width); + globeRef.current.height(height / 1.5); } }; - // Handle the resize event - window.addEventListener("resize", handleResize); - handleResize(); // Call it initially to set the size + // Initially set the globe size to match the container + setGlobeSize(); + + // Resize listener to update the globe size + window.addEventListener("resize", setGlobeSize); // Set initial positions of satellites let currentDate = new Date().toISOString(); @@ -92,7 +91,7 @@ export default function SatelliteGlobe() { globeRef.current.objectsData(initialPositions); return () => { - window.removeEventListener("resize", handleResize); + window.removeEventListener("resize", setGlobeSize); }; } }, []); @@ -146,7 +145,7 @@ export default function SatelliteGlobe() { ); return () => clearInterval(intervalId); - }); + }, [satelliteData, selectedSatellite]); return
; } diff --git a/frontend/src/components/satelliteData/SatelliteDataTableMultiple.tsx b/frontend/src/components/satelliteData/SatelliteDataTableMultiple.tsx deleted file mode 100644 index 48ed71b..0000000 --- a/frontend/src/components/satelliteData/SatelliteDataTableMultiple.tsx +++ /dev/null @@ -1,142 +0,0 @@ -// Ensure all necessary imports are present -"use client"; -import React, { useState, useEffect } from "react"; -import dynamic from "next/dynamic"; -import { Combobox } from "../Combobox"; // Adjust the path as necessary -import { - mapRawDataToTleData, - mapTleToSatData, - SatelliteData, -} from "@/lib/mapHelpers"; -import { convertSatrec, SatelliteInfo } from "@/lib/convertSatrec"; -import Map2d from "../2dmap/Map2d"; - -// Define the MyGlobe component with dynamic import -const MyGlobe = dynamic(() => import("@/components/map/MyGlobe"), { - ssr: false, -}); -const updateInterval = 10; - -interface ClientOnlyComponentProps { - fetchSatelliteData: ({ - // eslint-disable-next-line no-unused-vars - useExampleData, - }: { - useExampleData: boolean; - filterList?: string[]; - }) => Promise; -} - -const SatelliteDataTableMultiple: 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(() => { - 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); - }; - - // Map satellite data for Combobox options - - useEffect(() => { - const intervalId = setInterval(() => { - // Access satellite data by name - if (selectedSatellite) { - const updatedInfo = convertSatrec( - selectedSatellite.satrec, - selectedSatellite.name, - ); - setSatelliteInfo(updatedInfo); - } - }, updateInterval); - - // Clear interval on component unmount - return () => clearInterval(intervalId); - }, [selectedSatellite]); - - return satelliteData.length > 0 && selectedSatellite && satelliteInfo ? ( - <> -
-
- -
{/* Include the dropdown arrow icon here */}
-
- -
-
-

{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 */}
-
- - - - ) : ( -
Loading...
- ); -}; - -export default SatelliteDataTableMultiple; diff --git a/frontend/src/components/satelliteData/SatelliteDataTableOutdated.tsx b/frontend/src/components/satelliteData/SatelliteDataTableOutdated.tsx deleted file mode 100644 index 479e0a8..0000000 --- a/frontend/src/components/satelliteData/SatelliteDataTableOutdated.tsx +++ /dev/null @@ -1,234 +0,0 @@ -"use client"; -import React, { useEffect, useState } from "react"; -import { exampleData } from "../map/exampleSatData"; -import { SatelliteData, mapRawDataToSatData } from "@/lib/mapHelpers"; -import { - Table, - TableBody, - TableCaption, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import * as satellite from "satellite.js"; -import { PolyUtil } from "node-geometry-library"; -import globeData from "@components/map/githubglobe/files/globe-data.json"; - -const satellitesShown = 10; // Maximum number of satellites to display -const timeInterval = 1000; // Time interval for updating satellite positions in milliseconds - -// Extends SatelliteData with calculated position properties -interface SatelliteDataWithPosition extends SatelliteData { - latitudeDeg: string; - longitudeDeg: string; - altitude: string; - velocity: string; -} - -export default function SatelliteDataTableOutdated() { - const [satData, setSatData] = useState([]); - - useEffect(() => { - // Updates satellite positions at specified intervals - const updateSatellitePositions = () => { - const updatedData = mapRawDataToSatData(exampleData) - .slice(0, satellitesShown) - .map((data) => { - const positionAndVelocity = satellite.propagate( - data.satrec, - new Date(), - ); - - if ( - positionAndVelocity.position && - typeof positionAndVelocity.position !== "boolean" - ) { - const gmst = satellite.gstime(new Date()); // Calculates Greenwich Mean Sidereal Time - const positionGd = satellite.eciToGeodetic( - positionAndVelocity.position, - gmst, - ); - - // Extracts velocity from positionAndVelocity if it is not false - var velocityEci = positionAndVelocity.velocity; - - if (typeof velocityEci !== "boolean") { - // Calculate the magnitude of the velocity vector if velocityEci is not false - var velocityMagnitude = Math.sqrt( - velocityEci.x * velocityEci.x + - velocityEci.y * velocityEci.y + - velocityEci.z * velocityEci.z, - ); - - // Convert velocity from kilometers per second (km/s) to kilometers per hour (km/h) - velocityMagnitude = velocityMagnitude * 3600; - } else { - // Set velocityMagnitude to NaN if velocityEci is false - velocityMagnitude = NaN; - } - - // Converts geodetic position to readable format - const latitudeDeg = satellite.degreesLat( - positionGd.latitude, - ); - const longitudeDeg = satellite.degreesLong( - positionGd.longitude, - ); - const altitude = positionGd.height; - - return { - ...data, - latitudeDeg: latitudeDeg.toFixed(2), - longitudeDeg: longitudeDeg.toFixed(2), - altitude: altitude.toFixed(2), - velocity: velocityMagnitude.toFixed(0), - }; - } else { - return { - ...data, - latitudeDeg: "N/A", - longitudeDeg: "N/A", - altitude: "N/A", - velocity: "N/A", - }; - } - }); - - setSatData(updatedData); - }; - - // Performs an initial update and sets the interval for further updates - updateSatellitePositions(); - const intervalId = setInterval(() => { - updateSatellitePositions(); - }, timeInterval); - - // Cleans up the interval when the component unmounts - return () => clearInterval(intervalId); - }, []); - - return ( -
- - Satellite Data - - - Satellite - Latitude - Longitude - Altitude - Velocity - Country - - - - {satData.map((data, index) => { - let country = "Ocean"; // Default to Ocean if no country is found - globeData.features.forEach((countryFeature) => { - // Checks if the satellite is within a country's bounding box to reduce the number of polygons to check - const boundingBoxPoints = [ - { - lat: countryFeature.bbox[1], - lng: countryFeature.bbox[0], - }, - { - lat: countryFeature.bbox[3], - lng: countryFeature.bbox[0], - }, - { - lat: countryFeature.bbox[3], - lng: countryFeature.bbox[2], - }, - { - lat: countryFeature.bbox[1], - lng: countryFeature.bbox[2], - }, - ]; - - if ( - PolyUtil.containsLocation( - { - lat: Number(data.latitudeDeg), - lng: Number(data.longitudeDeg), - }, - boundingBoxPoints, - ) - ) { - // Handles polygons to accurately find the country - if (countryFeature.geometry.type == "Polygon") { - let boundingPolygon = - countryFeature.geometry.coordinates[0].map( - (coordinate) => ({ - lat: Number(coordinate[1]), - lng: Number(coordinate[0]), - }), - ); - - if ( - PolyUtil.containsLocation( - { - lat: Number(data.latitudeDeg), - lng: Number(data.longitudeDeg), - }, - boundingPolygon, - ) - ) { - country = - countryFeature.properties.ADMIN; - } - } else if ( - countryFeature.geometry.type == - "MultiPolygon" - ) { - // Loop through each polygon array in the MultiPolygon - const multiPolygon = countryFeature.geometry - .coordinates as number[][][][]; - multiPolygon.forEach((polygon) => { - let boundingPolygon = polygon[0].map( - (coordinate) => ({ - lat: Number(coordinate[1]), - lng: Number(coordinate[0]), - }), - ); - - if ( - PolyUtil.containsLocation( - { - lat: Number( - data.latitudeDeg, - ), - lng: Number( - data.longitudeDeg, - ), - }, - boundingPolygon, - ) - ) { - country = - countryFeature.properties.ADMIN; - } - }); - } - } - }); - - return ( - - {data.name} - {data.latitudeDeg}° N - {data.longitudeDeg}° E - - {(Number(data.altitude) * 10).toFixed(0)}{" "} - MASL - - {data.velocity} km/h - {country} - - ); - })} - -
-
- ); -} diff --git a/frontend/src/lib/getSatelliteData.ts b/frontend/src/lib/getSatelliteData.ts index 46f51b7..7cea5a7 100644 --- a/frontend/src/lib/getSatelliteData.ts +++ b/frontend/src/lib/getSatelliteData.ts @@ -1,5 +1,6 @@ import { twoline2satrec } from "satellite.js"; import { SatRec } from "satellite.js"; +import { exampleData } from "@/components/map/exampleSatData"; // Satellite data interface interface SatelliteData { @@ -18,9 +19,15 @@ let cachedData: { }; // Fetch satellite data from Celestrak by satellite name +// eslint-disable-next-line no-unused-vars async function fetchSatelliteData(satName: string): Promise { const response = await fetch( `https://celestrak.org/NORAD/elements/gp.php?NAME=${satName}&FORMAT=TLE`, + { + next: { + revalidate: 60 * 60 * 24, // revalidate every 24 hours + }, + }, ); if (!response.ok) { throw new Error( @@ -60,8 +67,10 @@ export async function satLoader(satName: string): Promise { !(satName in cachedData.data) ) { // Fetch the data and update the cache - const newDataArray = await fetchSatelliteData(satName); - const newData = newDataArray[0]; + // const newDataArray = await fetchSatelliteData(satName); + const newDataArray = mapTleToSatData(exampleData); + const satExample = newDataArray.find((sat) => sat.name == satName); + const newData = satExample || newDataArray[0]; cachedData = { data: { ...cachedData.data, [satName]: newData }, diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index e87b6d1..64d3912 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -21,8 +21,13 @@ type SatelliteStore = SatelliteState & SatelliteActions; // Create satellite store. Update selectedSatellite if you want a different default export const useSatelliteStore = create((set, get) => ({ satelliteData: {}, - satelliteNames: ["HYPSO-1", "UME (ISS)", "VANGUARD 1", "STARLINK-1007"], - selectedSatellite: "HYPSO-1", + satelliteNames: [ + "0 VANGUARD 2", + "0 EXPLORER 7", + "0 SOLRAD 3/INJUN 1", + "0 STARLINK-1007", + ], + selectedSatellite: "0 VANGUARD 2", setSatelliteData: (satName, data) => set((state) => ({