-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
408 feature request add next pass over trondheim or any location as a…
… 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
Showing
8 changed files
with
363 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
frontend/src/components/satelliteData/SatellitePassOver.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
197
frontend/src/components/satelliteData/SatellitePassOverLocation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
90
frontend/src/components/satelliteData/SatellitePassOverTime.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ); | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.