Skip to content

Commit

Permalink
357 add new satellites with norad (#361)
Browse files Browse the repository at this point in the history
* add error boundries, feat to add new satellites by ID

* small fix

* lint and prettier
  • Loading branch information
Lukas Thrane authored and GitHub committed Apr 22, 2024
1 parent b0cdcdf commit 1db461b
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 89 deletions.
12 changes: 10 additions & 2 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,16 @@ export default async function RootLayout({
if (satellites) {
for (const sat of satellites) {
if (sat.id) {
const data = await satLoaderById(sat.id);
satData.push({ name: sat.name, id: sat.id, data });
try {
const data = await satLoaderById(sat.id);
satData.push({ name: sat.name, id: sat.id, data });
} catch (e) {
console.error(
"Either CelesTrak has IP banned the server, or the satellite data is not available for the provided NORAD ID: " +
sat.id +
", or CelesTrak is down.",
);
}
}
}
}
Expand Down
101 changes: 99 additions & 2 deletions frontend/src/components/homeComponents/SatDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,93 @@
"use client";
import React, { useState } from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
import { satLoaderById } from "@/lib/getSatelliteData";

type DropdownProps = {
satelliteNames: string[];
selectedSatellite: string;
// eslint-disable-next-line no-unused-vars
setSelectedSatellite: (satellite: string) => void;
// eslint-disable-next-line no-unused-vars
setSatellites: (satellites: any) => void;
};

export default function SatDropdown({
satelliteNames,
selectedSatellite,
setSelectedSatellite,
setSatellites,
}: DropdownProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [noradID, setNoradID] = useState("");
const [error, setError] = useState("");

const toggleDropdown = () => setIsOpen(!isOpen);
const toggleDropdown = () => {
setError("");
setIsOpen(!isOpen);
};

const handleSelect = (satellite: string) => {
setSelectedSatellite(satellite);
setIsOpen(false);
};

// Animation variants for the dropdown content
const handleAddSatellite = async (noradID: string) => {
if (!noradID) {
setError("Please enter a valid NORAD ID.");
return;
}

try {
const data = await satLoaderById(noradID);
if (data) {
const newSatellite = {
name: data.name,
id: noradID,
data: data,
};
setSatellites([newSatellite]);
setSelectedSatellite(newSatellite.name);
setError("");
} else {
throw new Error("No data returned for the provided NORAD ID.");
}
} catch (e) {
console.error(
"Failed to fetch satellite data for NORAD ID:",
noradID,
"\n",
(e as Error).message,
);
if (
(e as Error).message ===
"403 - Forbidden: Access is denied. You are likely IP banned temporarily for making too many requests."
) {
setError(
"403 - Forbidden: Access is denied. You are likely IP banned temporarily for making too many requests.",
);
return;
} else {
setError(`Satellite with NORAD ID ${noradID} does not exist.`);
}
}
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
handleAddSatellite(noradID);
}
};

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
// Allow only numeric input
if (/^\d*$/.test(value)) {
setNoradID(value);
}
};

const variants = {
open: { opacity: 1, height: "auto", maxHeight: "250px" },
collapsed: { opacity: 0, height: 0 },
Expand Down Expand Up @@ -65,6 +129,39 @@ export default function SatDropdown({
: `${satellite} (Selected)`}
</div>
))}
<div className="mb-2 flex w-full items-center gap-4">
<div className="flex flex-grow items-center rounded bg-black text-white">
<span className="p-2 pr-0">
<svg
className="h-4 w-4 text-gray-400"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</span>
<input
type="text"
value={noradID}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
className="flex-grow bg-black p-2 text-white outline-none"
placeholder="NORAD ID"
/>
</div>
<button
onClick={() => handleAddSatellite(noradID)}
className=" mr-2 whitespace-nowrap rounded border bg-white p-1 text-black transition duration-150 ease-in-out hover:bg-gray-300"
>
Add Satellite
</button>
</div>

{error && <div className="p-2 pt-0 text-red-500">{error}</div>}
</motion.div>
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/homeComponents/SatelliteSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ export default function SatelliteSelector() {
const setSelectedSatellite = useSatelliteStore(
(state) => state.setSelectedSatellite,
);
const setSatellites = useSatelliteStore((state) => state.setSatellites);

return (
<div className="m-0 w-full border-b border-gray-600 p-0">
<SatDropdown
satelliteNames={satelliteNames}
selectedSatellite={selectedSatellite}
setSelectedSatellite={setSelectedSatellite}
setSatellites={setSatellites}
/>
</div>
);
Expand Down
25 changes: 12 additions & 13 deletions frontend/src/components/satelliteData/SatelliteInitialFetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,26 @@ import { useSatelliteStore } from "@/lib/store";
import { useEffect } from "react";

interface SatelliteInitialFetchProps {
satData: { name: string; id: string; data: any }[];
satData: { name: string; id: string; data: any; selected?: boolean }[]; // Adjusted to possibly include 'selected'
}

export default function SatelliteInitialFetch({
satData,
}: SatelliteInitialFetchProps) {
const setSatellites = useSatelliteStore((state) => state.setSatellites);
const setSatelliteData = useSatelliteStore(
(state) => state.setSatelliteData,
);

useEffect(() => {
// Set the satellite data in the store
setSatellites(satData);
satData.forEach((sat) => {
if (!sat.data) return;
setSatelliteData(sat.name, sat.data);
});
// Convert incoming data to the expected format by the store
const satellites = satData.map((sat) => ({
name: sat.name,
id: sat.id,
data: sat.data, // Assuming data is optional and handled correctly by setSatellites
selected: sat.selected, // Optional, handle as boolean; ensure it's true for exactly one sat or none
}));

console.log("Satellite data fetched and set in store", satData);
}, [satData]);
// Set the satellite data in the store with potential initial data and selected satellite
setSatellites(satellites);
}, [satData, setSatellites]);

return <></>;
return <></>; // The component doesn't render anything
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export default function SatelliteStatsTableRow({
// Display loading message if satellite info is not available
if (!satelliteInfo) {
return (
<TableRow>
<TableRow
className="cursor-pointer hover:bg-white hover:text-black"
onClick={() => {
handleRowClick();
}}
>
<TableCell className="w-1/5 px-6">{satName}</TableCell>
<TableCell className="hidden w-1/5 sm:table-cell">
Loading...
Expand Down
50 changes: 6 additions & 44 deletions frontend/src/lib/getSatelliteData.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { twoline2satrec } from "satellite.js";
import { SatRec } from "satellite.js";
import { exampleData } from "@/components/satelliteData/exampleSatData";

// Satellite data interface
interface SatelliteData {
Expand Down Expand Up @@ -33,28 +32,7 @@ function mapTleToSatData(tleString: string): SatelliteData[] {
return satellites;
}

// Fetch satellite data from Celestrak by satellite name
// eslint-disable-next-line no-unused-vars
async function fetchSatelliteData(satName: string): Promise<any> {
const response = await fetch(
`https://celestrak.org/NORAD/elements/gp.php?NAME=${satName}&FORMAT=TLE`,
{
next: {
revalidate: 60 * 60 * 24, // revalidate every 24 hours
},
},
);
if (!response.ok) {
throw new Error(
`Failed to fetch satellite data: ${response.statusText}`,
);
}
const data = await response.text();
return mapTleToSatData(data);
}

// fetch satellite data from celestrak by id
// eslint-disable-next-line no-unused-vars
async function fetchSatelliteDataById(satId: string): Promise<any> {
const response = await fetch(
`https://celestrak.org/NORAD/elements/gp.php?CATNR=${satId}&FORMAT=TLE`,
Expand All @@ -64,6 +42,11 @@ async function fetchSatelliteDataById(satId: string): Promise<any> {
},
},
);
if (response.status === 403) {
throw new Error(
"403 - Forbidden: Access is denied. You are likely IP banned temporarily for making too many requests.",
);
}
if (!response.ok) {
throw new Error(
`Failed to fetch satellite data from celestrak: ${response.statusText}`,
Expand All @@ -79,28 +62,7 @@ function isStale(timestamp: Date): boolean {
return now.getTime() - timestamp.getTime() > 24 * 60 * 60 * 1000;
}

export async function satLoader(satName: string): Promise<SatelliteData> {
// The logic to check if data is stale and needs to be fetched
if (
!cachedData ||
isStale(cachedData.timestamp) ||
!(satName in cachedData.data)
) {
// Fetch the data and update the cache
// const newDataArray = await fetchSatelliteData(satName);
const newDataArray = mapTleToSatData(exampleData);
const satExample = newDataArray.find((sat) => sat.name == satName);
const newData = satExample || newDataArray[0];

cachedData = {
data: { ...cachedData.data, [satName]: newData },
timestamp: new Date(),
};
}

return cachedData.data[satName];
}

// Load satellite data by id
export async function satLoaderById(satId: string): Promise<SatelliteData> {
// The logic to check if data is stale and needs to be fetched
if (
Expand Down
Loading

0 comments on commit 1db461b

Please sign in to comment.