diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9479f0c..777fd94 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "luxon": "^3.4.4", "next": "14.1.0", "next-themes": "^0.2.1", + "node-geometry-library": "^1.2.6", "ol": "^8.2.0", "ol-ext": "^4.0.14", "ol-mapbox-style": "^12.2.0", @@ -13967,6 +13968,11 @@ "node": ">= 6.13.0" } }, + "node_modules/node-geometry-library": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/node-geometry-library/-/node-geometry-library-1.2.6.tgz", + "integrity": "sha512-WGI1tbEjfwhI+3iKhXQro1+A3HjUIKgRGXneZ+E5IT+KpXLp6tFH6IPSr5nbXR8t72/NxSCp/Zax/QDroeSFeg==" + }, "node_modules/node-gyp-build": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b9f1aa4..5fad7fb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "luxon": "^3.4.4", "next": "14.1.0", "next-themes": "^0.2.1", + "node-geometry-library": "^1.2.6", "ol": "^8.2.0", "ol-ext": "^4.0.14", "ol-mapbox-style": "^12.2.0", diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 3bb6811..4553402 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -11,6 +11,7 @@ import Link from "next/link"; // Dynamic import because of leaflet and globe.gl ssr problem with next.js import dynamic from "next/dynamic"; import SatelliteFetcher from "@/components/map/SatelliteFetcher"; +import SatelliteDataTable from "@/components/satelliteData/SatelliteDataTable"; const MyCustomMap = dynamic(() => import("@/components/map/MyCustomMap"), { ssr: false, @@ -62,6 +63,8 @@ export default async function Home() { return (
+ + diff --git a/frontend/src/components/satelliteData/SatelliteDataTable.tsx b/frontend/src/components/satelliteData/SatelliteDataTable.tsx new file mode 100644 index 0000000..aa52cee --- /dev/null +++ b/frontend/src/components/satelliteData/SatelliteDataTable.tsx @@ -0,0 +1,235 @@ +// 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"; + +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 SatelliteDataTable() { + 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)}{" "} + moh + + {data.velocity} km/h + {country} + + ); + })} + +
+
+ ); +}