diff --git a/frontend/src/app/_homeComponents/GlobeWithStats.tsx b/frontend/src/app/_homeComponents/GlobeWithStats.tsx index 2a89279..cdd669e 100644 --- a/frontend/src/app/_homeComponents/GlobeWithStats.tsx +++ b/frontend/src/app/_homeComponents/GlobeWithStats.tsx @@ -2,6 +2,7 @@ import SatelliteSelector from "./SatelliteSelector"; import SatelliteDataHome from "@/components/satelliteData/SatelliteDataHome"; import dynamic from "next/dynamic"; +import SatellitePassOver from "@/components/satelliteData/SatellitePassOver"; const SatelliteGlobeNoSSR = dynamic(() => import("./SatelliteGlobe"), { ssr: false, @@ -15,11 +16,11 @@ export default function GlobeWithStats() { return ( <>
-
+
+
-
diff --git a/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx b/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx index 4c19927..c9db84b 100644 --- a/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx @@ -1,6 +1,12 @@ "use client"; -import React, { useState, useLayoutEffect, useRef, useCallback } from "react"; +import React, { + useState, + useLayoutEffect, + useRef, + useCallback, + useMemo, +} from "react"; import { XAxis, CartesianGrid, @@ -45,9 +51,10 @@ const OrbitDataGraph: React.FC = ({ const [chartData, setChartData] = useState([]); // Handling button for zooming in and out of the graph on a time scale - const launchDate = launchDateString - ? new Date(launchDateString) - : new Date(); + const launchDate = useMemo( + () => (launchDateString ? new Date(launchDateString) : new Date()), + [launchDateString], + ); const calculateMonthsDiff = () => { const currentDate = new Date(); return ( @@ -194,7 +201,7 @@ const OrbitDataGraph: React.FC = ({ updateSize(); return () => window.removeEventListener("resize", updateSize); - }, []); + }, [handleChartScroll, months]); console.log("orbitalData", orbitalData); diff --git a/frontend/src/app/satellites/[satelliteSlug]/page.tsx b/frontend/src/app/satellites/[satelliteSlug]/page.tsx index 164f7f4..a0bd792 100644 --- a/frontend/src/app/satellites/[satelliteSlug]/page.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/page.tsx @@ -114,7 +114,7 @@ export default async function SatelliteInfoPage({ : null}

- {satAttributes.missionStatus === "ON ORBIT" ? ( + {satAttributes.missionStatus === "IN ORBIT" ? (
@@ -151,7 +151,7 @@ export default async function SatelliteInfoPage({ ) : null} {/* Container for map */} - {noradId && satAttributes.missionStatus === "ON ORBIT" ? ( + {noradId && satAttributes.missionStatus === "IN ORBIT" ? (
diff --git a/frontend/src/components/satelliteData/SatellitePassOver.tsx b/frontend/src/components/satelliteData/SatellitePassOver.tsx new file mode 100644 index 0000000..19ac23d --- /dev/null +++ b/frontend/src/components/satelliteData/SatellitePassOver.tsx @@ -0,0 +1,12 @@ +"use client"; +import SatellitePassOverLocation from "./SatellitePassOverLocation"; +import SatellitePassOverTime from "./SatellitePassOverTime"; + +export default function SatellitePassOver() { + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/satelliteData/SatellitePassOverLocation.tsx b/frontend/src/components/satelliteData/SatellitePassOverLocation.tsx new file mode 100644 index 0000000..27c6ed1 --- /dev/null +++ b/frontend/src/components/satelliteData/SatellitePassOverLocation.tsx @@ -0,0 +1,197 @@ +"use client"; +import React, { useEffect, useRef, useState } from "react"; +import { motion } from "framer-motion"; +import { cn } from "@/lib/utils"; +import { Location } from "@/lib/store"; +import { useLocationStore } from "@/lib/store"; + +export default function SatellitePassOverLocation() { + // State to manage whether the dropdown is open or closed + let isLargeScreen = useRef(false); + // Useeffect to check window type + function isValidCoordinate(latitude: number, longitude: number): boolean { + return ( + !isNaN(latitude) && + !isNaN(longitude) && + latitude >= -90 && + latitude <= 90 && + longitude >= -180 && + longitude <= 180 + ); + } + useEffect(() => { + if (typeof window !== "undefined") { + isLargeScreen.current = + window.matchMedia("(min-width: 768px)").matches; + } + }, []); + + const [isOpen, setIsOpen] = useState(isLargeScreen.current); + const [latitude, setLatitude] = useState(""); + const [longitude, setLongitude] = useState(""); + const [error, setError] = useState(""); + const locations = useLocationStore((state) => state.locations); + const addLocation = useLocationStore((state) => state.addLocation); + const setSelectedLocation = useLocationStore( + (state) => state.setSelectedLocation, + ); + const selectedLocation = useLocationStore( + (state) => state.selectedLocation, + ); + const [displaeydCity, setDisplayedCity] = useState( + selectedLocation?.name || "", + ); + const [displayedLocation, setDisplayedLocation] = useState( + selectedLocation?.latitude.toFixed(2) + + "° N, " + + selectedLocation?.longitude.toFixed(2) + + "° E", + ); + + const toggleDropdown = () => { + setError(""); + setIsOpen(!isOpen); + }; + + const handleAddLocation = (latitude: string, longitude: string) => { + //Placeholder for adding a location + if (!isValidCoordinate(parseFloat(latitude), parseFloat(longitude))) { + setError("Please enter valid latitude and longitude."); + return; + } + addLocation({ + latitude: parseFloat(latitude), + longitude: parseFloat(longitude), + name: "", + }); + setLatitude(""); + setLongitude(""); + }; + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + handleAddLocation(latitude, longitude); + } + }; + const handleLatitudeChange = ( + event: React.ChangeEvent, + ) => { + const value = event.target.value; + // Allow only numeric input + if (/^-?\d*\.?\d*$/.test(value)) { + setLatitude(value); + } + }; + const handleLongitudeChange = ( + event: React.ChangeEvent, + ) => { + const value = event.target.value; + // Allow only numeric input + if (/^-?\d*\.?\d*$/.test(value)) { + setLongitude(value); + } + }; + const handleSelect = (location: Location) => { + if (location.name !== "") { + setDisplayedCity(location.name); + setDisplayedLocation(""); + } else if (location.latitude && location.longitude) { + setDisplayedCity(""); + setDisplayedLocation( + location.latitude.toFixed(2) + + "° N, " + + location.longitude.toFixed(2) + + "° E", + ); + } + setSelectedLocation(location); + }; + const variants = { + open: { opacity: 1, height: "auto", maxHeight: "250px" }, + collapsed: { opacity: 0, height: 0 }, + }; + + return ( +
+ + {isOpen &&
} + + {locations.map((location, idx) => ( +
handleSelect(location)} + > + {location.name} ({location.latitude + "° N"},{" "} + {location.longitude + "° E"}) +
+ ))} + +
+
+ + +
+ +
+ + {error &&
{error}
} +
+
+ ); +} diff --git a/frontend/src/components/satelliteData/SatellitePassOverTime.tsx b/frontend/src/components/satelliteData/SatellitePassOverTime.tsx new file mode 100644 index 0000000..81ace09 --- /dev/null +++ b/frontend/src/components/satelliteData/SatellitePassOverTime.tsx @@ -0,0 +1,90 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { useLocationStore } from "@/lib/store"; +import { useSatelliteStore } from "@/lib/store"; +import { predictFuturePositions } from "@/lib/convertSatrec"; +const updateInterval = 50; // in ms +const deltaDegree = 1; // Delta degree to check if the satellite is over the location + +export default function SatellitePassOverTime() { + //Computation of the time before the satellite pass over the selected location + const selectedLocation = useLocationStore( + (state) => state.selectedLocation, + ); + const selectedSatellite = useSatelliteStore( + (state) => state.selectedSatellite, + ); + + // State to manage the display time + const [displayTime, setDisplayTime] = useState([ + "Calculating...", + ]); + const [nextPassTime, setNextPassTime] = useState( + undefined, + ); + const satNumToEntry = useSatelliteStore((state) => state.satNumToEntry); + + useEffect(() => { + if (!selectedLocation || !selectedSatellite) return; + const satData = satNumToEntry[selectedSatellite]; + + if (satData && satData.satrec) { + const futurePoints = predictFuturePositions(satData.satrec, 10000); + const nextPass = futurePoints.find( + (point) => + Math.abs( + selectedLocation.latitude - + parseFloat(point.latitudeDeg), + ) <= deltaDegree && + Math.abs( + selectedLocation.longitude - + parseFloat(point.longitudeDeg), + ) <= deltaDegree, + ); + if (nextPass) { + setNextPassTime(nextPass?.time); + } else { + setNextPassTime(undefined); + } + } + }, [selectedSatellite, selectedLocation, satNumToEntry]); + + useEffect(() => { + if (!nextPassTime) return; + const intervalId = setInterval(() => { + const diff = nextPassTime - Date.now(); + if (diff <= 0) { + setDisplayTime(["Calculating..."]); + } + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff / (1000 * 60 * 60)) % 24); + const minutes = Math.floor((diff / 1000 / 60) % 60); + const seconds = Math.floor((diff / 1000) % 60); + let timeParts = []; + if (days > 0) timeParts.push(`${days} days`); + if (hours > 0 || timeParts.length > 0) + timeParts.push(`${hours} hours`); + if (minutes > 0 || timeParts.length > 0) + timeParts.push(`${minutes} minutes`); + timeParts.push(`${seconds} seconds`); + // Set the display time + setDisplayTime(timeParts); + }, updateInterval); + return () => clearInterval(intervalId); + // Update the display time every `updateInterval` ms + }, [nextPassTime]); + return ( +
+ {displayTime.map((part, index) => ( +
+

{part}

+
+ ))} + +

+ Time before the satellite passes over the selected location, + with precision of {deltaDegree}°. +

+
+ ); +} diff --git a/frontend/src/lib/convertSatrec.ts b/frontend/src/lib/convertSatrec.ts index 30a9e37..e7b7b0f 100644 --- a/frontend/src/lib/convertSatrec.ts +++ b/frontend/src/lib/convertSatrec.ts @@ -30,6 +30,7 @@ interface SatelliteInfo { interface SatelliteFutureInfo { latitudeDeg: string; longitudeDeg: string; + time: number; // Time of the future position in ISO format } export type { SatelliteInfo, SatelliteFutureInfo }; @@ -162,6 +163,7 @@ export const predictFuturePositions = ( futurePositions.push({ longitudeDeg: longitudeDeg.toFixed(2), latitudeDeg: latitudeDeg.toFixed(2), + time: futureTime.getTime(), // Store time in milliseconds }); } } diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index 0a33f20..bd1c365 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -17,6 +17,11 @@ type Nominal = Type & { export type SatelliteName = Nominal; export type SatelliteNumber = Nominal; +export type Location = { + latitude: number; + longitude: number; + name: string; +}; // Satellite entry for setSatellites export interface SatelliteEntry { @@ -80,3 +85,43 @@ export const useSatelliteStore = create()((set) => ({ })); }, })); + +// Define the state for location management, for the pass over feature +export interface LocationState { + locations: Location[]; + selectedLocation: Location | null; +} + +// Define the actions for location management +/* eslint-disable no-unused-vars */ +// Disable unused variables as the store actions defined here are used in other files, +export interface LocationActions { + setLocations: (locations: Location[]) => void; + addLocation: (location: Location) => void; + setSelectedLocation: (location: Location) => void; +} +/* eslint-enable no-unused-vars */ + +export type LocationStore = LocationState & LocationActions; +// Create location store +export const useLocationStore = create()((set) => ({ + locations: [ + { + latitude: 63.446827, + longitude: 10.421906, + name: "Trondheim", + }, + // Add more default locations if needed + ], + + selectedLocation: { + latitude: 63.446827, + longitude: 10.421906, + name: "Trondheim", + }, + + setLocations: (locations) => set({ locations }), + addLocation: (location) => + set((state) => ({ locations: [...state.locations, location] })), + setSelectedLocation: (location) => set({ selectedLocation: location }), +}));