Skip to content

Commit

Permalink
408 feature request add next pass over trondheim or any location as a…
Browse files Browse the repository at this point in the history
… feature (#437)

* feat: adding the feature to know when a satellite is passing over a chosen location

* fix: fix the correct width of the satellite indicators

* fix: fixing the CI

* fix: trying to fix the CI

* fix: application of prettier to the codebase
  • Loading branch information
Thibault authored and GitHub committed Jun 12, 2025
1 parent b95030c commit 80c7572
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 9 deletions.
5 changes: 3 additions & 2 deletions frontend/src/app/_homeComponents/GlobeWithStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,11 +16,11 @@ export default function GlobeWithStats() {
return (
<>
<div className="flex min-h-[calc(100vh-73px)] flex-col gap-0 bg-black md:flex-row">
<div className="z-10 flex flex-col border-b-2 border-l-2 border-r-2 border-t-2 border-gray-600 bg-black">
<div className="z-10 flex w-full flex-col border-b-2 border-l-2 border-r-2 border-t-2 border-gray-600 bg-black md:w-[400px]">
<SatelliteSelector />
<SatelliteDataHome satelliteNum={null} />
<SatellitePassOver />
</div>

<div className="relative z-0 h-full w-full grow overflow-x-hidden border-b-2 border-l-2 border-r-2 border-t-0 border-gray-600 bg-black md:border-l-0 md:border-t-2 xl:w-2/3">
<div className="flex h-full w-full items-center justify-center bg-black">
<SatelliteGlobeNoSSR />
Expand Down
17 changes: 12 additions & 5 deletions frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -45,9 +51,10 @@ const OrbitDataGraph: React.FC<OrbitDataProps> = ({
const [chartData, setChartData] = useState<ChartData[]>([]);

// 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 (
Expand Down Expand Up @@ -194,7 +201,7 @@ const OrbitDataGraph: React.FC<OrbitDataProps> = ({
updateSize();

return () => window.removeEventListener("resize", updateSize);
}, []);
}, [handleChartScroll, months]);

console.log("orbitalData", orbitalData);

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/satellites/[satelliteSlug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default async function SatelliteInfoPage({
: null}
</p>
</div>
{satAttributes.missionStatus === "ON ORBIT" ? (
{satAttributes.missionStatus === "IN ORBIT" ? (
<div>
<SatelliteDataHome satelliteNum={noradId} />
</div>
Expand Down Expand Up @@ -151,7 +151,7 @@ export default async function SatelliteInfoPage({
) : null}

{/* Container for map */}
{noradId && satAttributes.missionStatus === "ON ORBIT" ? (
{noradId && satAttributes.missionStatus === "IN ORBIT" ? (
<div className="mt-6 w-full">
<Map2d satNum={noradId} />
</div>
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/components/satelliteData/SatellitePassOver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";
import SatellitePassOverLocation from "./SatellitePassOverLocation";
import SatellitePassOverTime from "./SatellitePassOverTime";

export default function SatellitePassOver() {
return (
<div>
<SatellitePassOverLocation />
<SatellitePassOverTime />
</div>
);
}
197 changes: 197 additions & 0 deletions frontend/src/components/satelliteData/SatellitePassOverLocation.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(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<boolean>(isLargeScreen.current);
const [latitude, setLatitude] = useState<string>("");
const [longitude, setLongitude] = useState<string>("");
const [error, setError] = useState<string>("");
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<string>(
selectedLocation?.name || "",
);
const [displayedLocation, setDisplayedLocation] = useState<string>(
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<HTMLInputElement>) => {
if (event.key === "Enter") {
handleAddLocation(latitude, longitude);
}
};
const handleLatitudeChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const value = event.target.value;
// Allow only numeric input
if (/^-?\d*\.?\d*$/.test(value)) {
setLatitude(value);
}
};
const handleLongitudeChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
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 (
<div className="w-full">
<button
className="flex w-full cursor-pointer flex-row justify-between bg-black p-4 text-left"
onClick={toggleDropdown}
>
<div className="flex flex-col">
<div>
{displaeydCity ||
displayedLocation ||
"Select a Location"}
</div>
<p className="text-gray-400">
{displaeydCity
? "Selected city"
: displayedLocation
? "Selected location"
: null}
</p>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="2em"
height="2em"
viewBox="0 0 24 24"
className="self-center justify-self-center"
>
<path fill="currentColor" d="m7 10l5 5l5-5z" />
</svg>
</button>
{isOpen && <hr />}
<motion.div
className={cn("overflow-hidden", isOpen && "overflow-y-scroll")}
initial="collapsed"
animate={isOpen ? "open" : "collapsed"}
variants={variants}
transition={{ duration: 0.5 }}
>
{locations.map((location, idx) => (
<div
key={idx}
className="cursor-pointer p-2 text-white hover:bg-gray-700"
onClick={() => handleSelect(location)}
>
{location.name} ({location.latitude + "° N"},{" "}
{location.longitude + "° E"})
</div>
))}

<div className="mb-2 flex w-full items-center gap-0">
<div className=" items-center rounded bg-black text-white">
<input
type="text"
value={latitude.toString()}
onChange={handleLatitudeChange}
onKeyDown={handleKeyDown}
className="flex-1 bg-black p-2 text-white outline-none"
placeholder="latitude"
/>
<input
type="text"
value={longitude.toString()}
onChange={handleLongitudeChange}
onKeyDown={handleKeyDown}
className="flex-2 bg-black p-2 text-white outline-none"
placeholder="longitude"
/>
</div>
<button
onClick={() => {
if (latitude && longitude) {
handleAddLocation(latitude, longitude);
}
}}
className="flex-3 mr-2 whitespace-nowrap rounded-md border bg-primary p-1 text-white duration-200 ease-in-out hover:opacity-80"
>
Add Location
</button>
</div>

{error && <div className="p-2 pt-0 text-red-500">{error}</div>}
</motion.div>
</div>
);
}
90 changes: 90 additions & 0 deletions frontend/src/components/satelliteData/SatellitePassOverTime.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>([
"Calculating...",
]);
const [nextPassTime, setNextPassTime] = useState<number | undefined>(
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 (
<div className="border border-l-0 border-r-0 border-gray-600 bg-black p-5">
{displayTime.map((part, index) => (
<div key={index} className="mr-2 inline-block">
<p>{part}</p>
</div>
))}

<p className="text-gray-400">
Time before the satellite passes over the selected location,
with precision of {deltaDegree}°.
</p>
</div>
);
}
2 changes: 2 additions & 0 deletions frontend/src/lib/convertSatrec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -162,6 +163,7 @@ export const predictFuturePositions = (
futurePositions.push({
longitudeDeg: longitudeDeg.toFixed(2),
latitudeDeg: latitudeDeg.toFixed(2),
time: futureTime.getTime(), // Store time in milliseconds
});
}
}
Expand Down
Loading

0 comments on commit 80c7572

Please sign in to comment.