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) => (
+
+ ))}
+
+
+ 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 }),
+}));