diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 777fd94..d70f9e3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,7 +42,8 @@ "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "three": "^0.161.0", - "vaul": "^0.9.0" + "vaul": "^0.9.0", + "zustand": "^4.5.2" }, "devDependencies": { "@cloudflare/next-on-pages": "^1.8.3", @@ -18001,6 +18002,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18890,6 +18899,33 @@ "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==" }, + "node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3edc0bc..71124ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,8 @@ "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "three": "^0.161.0", - "vaul": "^0.9.0" + "vaul": "^0.9.0", + "zustand": "^4.5.2" }, "devDependencies": { "@cloudflare/next-on-pages": "^1.8.3", diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 5f86ee6..937db5c 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -63,7 +63,14 @@ export default async function Home() { return (
- +
+ + + + + + +
diff --git a/frontend/src/components/satelliteData/SatelliteDataTable.tsx b/frontend/src/components/satelliteData/SatelliteDataTable.tsx index aa52cee..d9ffa3a 100644 --- a/frontend/src/components/satelliteData/SatelliteDataTable.tsx +++ b/frontend/src/components/satelliteData/SatelliteDataTable.tsx @@ -1,235 +1,75 @@ -// This component displays a table of satellite data including position and country "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"; +import { useState, useEffect } from "react"; +import { convertSatrec, SatelliteInfo } from "@/lib/convertSatrec"; +import { useSatelliteStore } from "@/lib/store"; -const satellitesShown = 10; // Maximum number of satellites to display -const timeInterval = 1000; // Time interval for updating satellite positions in milliseconds +const updateInterval = 10; -// Extends SatelliteData with calculated position properties -interface SatelliteDataWithPosition extends SatelliteData { - latitudeDeg: string; - longitudeDeg: string; - altitude: string; - velocity: string; -} - -export default function SatelliteDataTable() { - const [satData, setSatData] = useState([]); +export default function SatelliteDataTable({ satName }: { satName: string }) { + const { satelliteData, fetchAndSetSatelliteData } = useSatelliteStore(); + const [satelliteInfo, setSatelliteInfo] = useState( + null, + ); + // Fetch satellite data on component mount 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", - }; - } - }); + fetchAndSetSatelliteData(satName); + }, [fetchAndSetSatelliteData, satName]); - setSatData(updatedData); - }; - - // Performs an initial update and sets the interval for further updates - updateSatellitePositions(); + // Update satellite info every `updateInterval` ms + useEffect(() => { const intervalId = setInterval(() => { - updateSatellitePositions(); - }, timeInterval); - - // Cleans up the interval when the component unmounts + // 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); - }, []); - - 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]), - }), - ); + }, [satelliteData, satName]); - 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]), - }), - ); + // Display loading message if satellite info is not available + if (!satelliteInfo) { + return ( +
+

Loading...

+
+ ); + } - 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)}{" "} - moh - - {data.velocity} km/h - {country} - - ); - })} -
-
+ return ( +
+
+

{satelliteInfo.name}

+
{/* 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 */}
); } diff --git a/frontend/src/components/satelliteData/SatelliteDataTableOutdated.tsx b/frontend/src/components/satelliteData/SatelliteDataTableOutdated.tsx new file mode 100644 index 0000000..479e0a8 --- /dev/null +++ b/frontend/src/components/satelliteData/SatelliteDataTableOutdated.tsx @@ -0,0 +1,234 @@ +"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/convertSatrec.ts b/frontend/src/lib/convertSatrec.ts new file mode 100644 index 0000000..555073a --- /dev/null +++ b/frontend/src/lib/convertSatrec.ts @@ -0,0 +1,132 @@ +import { SatRec } from "satellite.js"; +import * as satellite from "satellite.js"; +import { PolyUtil } from "node-geometry-library"; +import globeData from "@components/map/githubglobe/files/globe-data.json"; + +interface SatelliteInfo { + name: string; + latitudeDeg: string; + longitudeDeg: string; + altitude: string; + velocity: string; + country: string; +} + +export type { SatelliteInfo }; + +// Function to find the country of a satellite +const findCountry = (latitudeDeg: number, longitudeDeg: number): string => { + for (const feature of globeData.features) { + const { bbox, geometry } = feature; + const boundingBoxPoints = [ + { lat: bbox[1], lng: bbox[0] }, + { lat: bbox[3], lng: bbox[0] }, + { lat: bbox[3], lng: bbox[2] }, + { lat: bbox[1], lng: bbox[2] }, + ]; + + if ( + PolyUtil.containsLocation( + { lat: latitudeDeg, lng: longitudeDeg }, + boundingBoxPoints, + ) + ) { + if (geometry.type === "Polygon") { + const coordinates = geometry.coordinates as number[][][]; + for (const polygon of coordinates) { + let boundingPolygon = polygon.map((coordinate) => { + return { lat: coordinate[1], lng: coordinate[0] }; + }); + + if ( + PolyUtil.containsLocation( + { lat: latitudeDeg, lng: longitudeDeg }, + boundingPolygon, + ) + ) { + return feature.properties.ADMIN; + } + } + } else if (geometry.type === "MultiPolygon") { + const multiPolygons = geometry.coordinates as number[][][][]; + for (const multiPolygon of multiPolygons) { + for (const polygon of multiPolygon) { + let boundingPolygon = polygon.map((coordinate) => { + return { lat: coordinate[1], lng: coordinate[0] }; + }); + + if ( + PolyUtil.containsLocation( + { lat: latitudeDeg, lng: longitudeDeg }, + boundingPolygon, + ) + ) { + return feature.properties.ADMIN; + } + } + } + } + } + } + + // Default to "Ocean" if no country is found + return "Ocean"; +}; + +// Convert satellite record to satellite info, including latitude, longitude, altitude, velocity, and country +export const convertSatrec = ( + satrec: SatRec, + satName: string, +): SatelliteInfo => { + if (!satrec) { + return { + name: satName, + latitudeDeg: "N/A", + longitudeDeg: "N/A", + altitude: "N/A", + velocity: "N/A", + country: "N/A", + }; + } + + const positionAndVelocity = satellite.propagate(satrec, new Date()); + + const gmst = satellite.gstime(new Date()); + const positionEci = positionAndVelocity.position; + const velocityEci = positionAndVelocity.velocity; + + let positionGd; + if (positionEci && typeof positionEci !== "boolean") { + positionGd = satellite.eciToGeodetic(positionEci, gmst); + } + + let latitudeDeg = 0; + let longitudeDeg = 0; + let altitude = 0; + if (positionGd && typeof positionGd !== "boolean") { + latitudeDeg = satellite.degreesLat(positionGd.latitude); + longitudeDeg = satellite.degreesLong(positionGd.longitude); + altitude = positionGd.height; + } + + let velocity = 0; + if (velocityEci && typeof velocityEci !== "boolean") { + velocity = Math.sqrt( + velocityEci.x * velocityEci.x + + velocityEci.y * velocityEci.y + + velocityEci.z * velocityEci.z, + ); + } + + // Find the country of the satellite + const country = findCountry(latitudeDeg, longitudeDeg); + + return { + name: satName, + latitudeDeg: latitudeDeg.toFixed(2), + longitudeDeg: longitudeDeg.toFixed(2), + altitude: altitude.toFixed(2), + velocity: velocity.toFixed(2), + country: country, + }; +}; diff --git a/frontend/src/lib/getSatelliteData.ts b/frontend/src/lib/getSatelliteData.ts new file mode 100644 index 0000000..46f51b7 --- /dev/null +++ b/frontend/src/lib/getSatelliteData.ts @@ -0,0 +1,75 @@ +import { twoline2satrec } from "satellite.js"; +import { SatRec } from "satellite.js"; + +// Satellite data interface +interface SatelliteData { + satrec: SatRec; + name: string; + timestamp: Date; +} + +// Cache the satellite data +let cachedData: { + data: Record; + timestamp: Date; +} = { + data: {}, + timestamp: new Date(0), +}; + +// Fetch satellite data from Celestrak by satellite name +async function fetchSatelliteData(satName: string): Promise { + const response = await fetch( + `https://celestrak.org/NORAD/elements/gp.php?NAME=${satName}&FORMAT=TLE`, + ); + if (!response.ok) { + throw new Error( + `Failed to fetch satellite data: ${response.statusText}`, + ); + } + const data = await response.text(); + return mapTleToSatData(data); +} + +// Map TLE data to satellite data +function mapTleToSatData(tleString: string): SatelliteData[] { + const lines = tleString.trim().split("\n"); + const satellites: SatelliteData[] = []; + for (let i = 0; i < lines.length; i += 3) { + const name = lines[i].trim(); + const line1 = lines[i + 1].trim(); + const line2 = lines[i + 2].trim(); + const satrec = twoline2satrec(line1, line2); + const timestamp = new Date(); + satellites.push({ satrec, name, timestamp }); + } + return satellites; +} + +// Check if cached data is stale +function isStale(timestamp: Date): boolean { + const now = new Date(); + return now.getTime() - timestamp.getTime() > 24 * 60 * 60 * 1000; +} + +export async function satLoader(satName: string): Promise { + // The logic to check if data is stale and needs to be fetched + if ( + !cachedData || + isStale(cachedData.timestamp) || + !(satName in cachedData.data) + ) { + // Fetch the data and update the cache + const newDataArray = await fetchSatelliteData(satName); + const newData = newDataArray[0]; + + cachedData = { + data: { ...cachedData.data, [satName]: newData }, + timestamp: new Date(), + }; + } + + return cachedData.data[satName]; +} + +export type { SatelliteData }; diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts new file mode 100644 index 0000000..0161b2c --- /dev/null +++ b/frontend/src/lib/store.ts @@ -0,0 +1,25 @@ +/* eslint-disable no-unused-vars */ +import { create } from "zustand"; +import type { SatelliteData } from "@/lib/getSatelliteData"; +import { satLoader } from "@/lib/getSatelliteData"; + +interface SatelliteStore { + satelliteData: Record; + setSatelliteData: (satName: string, data: SatelliteData) => void; + fetchAndSetSatelliteData: (satName: string) => Promise; +} + +export const useSatelliteStore = create((set) => ({ + satelliteData: {}, + 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 }, + })); + }, +}));