diff --git a/backend/main.py b/backend/main.py index 089cb20..6edb869 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,13 +1,88 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException +from skyfield.api import load, EarthSatellite +from datetime import datetime, timedelta app = FastAPI(root_path="/api") +# Define TLE data for Landsat 8 (this can be updated periodically) +landsat_8_tle = [ + "1 39084U 13008A 23270.47419877 .00000029 00000-0 27947-4 0 9999", + "2 39084 98.2045 221.3107 0001356 98.0138 262.1118 14.57109887552706" +] + +# Load timescale and define the satellite +satellite_db = { + "landsat_8": EarthSatellite(landsat_8_tle[0], landsat_8_tle[1], "Landsat 8", load.timescale()) +} + + +@app.get("/satellite/") +def get_all_satellites(): + """ + Returns the names of all available satellites. + """ + return {"satellites": list(satellite_db.keys())} + +@app.get("/satellite/{satellite_name}") +def get_satellite_info(satellite_name: str): + """ + Returns the current location and information of the specified satellite. + """ + # Check if the requested satellite exists in our database + satellite = satellite_db.get(satellite_name.lower()) + if not satellite: + raise HTTPException(status_code=404, detail="Satellite not found") + + # Get the current position of the satellite + t = load.timescale().now() + geocentric = satellite.at(t) + subpoint = geocentric.subpoint() + + # Return the satellite information + return { + "name": satellite_name, + "timestamp": datetime.utcnow().isoformat(), + "latitude": subpoint.latitude.degrees, + "longitude": subpoint.longitude.degrees, + "altitude_km": subpoint.elevation.km + } + +@app.get("/satellite/{satellite_name}/forecast") +def get_satellite_forecast(satellite_name: str, hours: int = 1): + """ + Returns the forecasted location of the satellite over the next specified hours. + """ + # Check if the requested satellite exists in our database + satellite = satellite_db.get(satellite_name.lower()) + if not satellite: + raise HTTPException(status_code=404, detail="Satellite not found") + + # Generate times over the next 'hours' hours at 15-minute intervals + ts = load.timescale() + t0 = ts.now() + t1 = ts.utc(t0.utc_datetime() + timedelta(hours=hours)) + num_intervals = hours * 16 + 1 # Every 15 minutes + times = ts.linspace(t0, t1, num_intervals) + + # Compute positions at each time + positions = [] + for t in times: + geocentric = satellite.at(t) + subpoint = geocentric.subpoint() + positions.append({ + "timestamp": t.utc_iso(), + "latitude": subpoint.latitude.degrees, + "longitude": subpoint.longitude.degrees, + "altitude_km": subpoint.elevation.km + }) + + # Return the forecast data + return { + "name": satellite_name, + "forecast": positions + } + @app.get("/") def read_root(): return {"message": "Hello World!"} -@app.get("/items/{item_id}") -def read_item(item_id: int): - return {"item_id": item_id} - - diff --git a/backend/requirements.txt b/backend/requirements.txt index 97dc7cd..ddc1086 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,2 +1,3 @@ fastapi uvicorn +skyfield \ No newline at end of file diff --git a/frontend/src/app/components/MapComponent.tsx b/frontend/src/app/components/MapComponent.tsx index c448859..db4d6c6 100644 --- a/frontend/src/app/components/MapComponent.tsx +++ b/frontend/src/app/components/MapComponent.tsx @@ -14,6 +14,11 @@ const CustomMarker = dynamic(() => import("./CustomMarker"), { ssr: false, }); +// Dynamically import SatelliteLayer with SSR disabled +const SatelliteLayer = dynamic(() => import("./SatelliteLayer"), { + ssr: false, +}); + // Similarly, dynamically import MapContainer and TileLayer const MapContainer = dynamic( () => import("react-leaflet").then((mod) => mod.MapContainer), @@ -28,6 +33,7 @@ interface MapComponentProps { pins: Pin[]; setPins: React.Dispatch>; customIcon: Icon | DivIcon | undefined; + satelliteIcon: Icon | DivIcon | undefined; latInput: string; lngInput: string; setLatInput: React.Dispatch>; @@ -38,6 +44,7 @@ export default function MapComponent({ pins, setPins, customIcon, + satelliteIcon, latInput, lngInput, setLatInput, @@ -70,6 +77,11 @@ export default function MapComponent({ onLatChange={setLatInput} onLngChange={setLngInput} /> + + + + + ); } diff --git a/frontend/src/app/components/SRDataModal.tsx b/frontend/src/app/components/SRDataModal.tsx index feaf074..eb8e7df 100644 --- a/frontend/src/app/components/SRDataModal.tsx +++ b/frontend/src/app/components/SRDataModal.tsx @@ -21,7 +21,7 @@ interface SRDataModalProps { interface SRDataPoint { date: string; - [key: string]: string | number; // Allow both string and number types + [key: string]: string | number; } diff --git a/frontend/src/app/components/SatelliteLayer.tsx b/frontend/src/app/components/SatelliteLayer.tsx new file mode 100644 index 0000000..ba4f936 --- /dev/null +++ b/frontend/src/app/components/SatelliteLayer.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { LatLngTuple } from "leaflet"; +import { Marker, Polyline } from "react-leaflet"; +import { Icon, IconOptions, DivIcon } from "leaflet"; + +interface SatelliteLayerProps { + customIcon: Icon | DivIcon | undefined; +} + +export default function SatelliteLayer({ customIcon }: SatelliteLayerProps) { + // State for the Landsat 8 satellite position and trajectory + const [satellitePosition, setSatellitePosition] = useState(null); + const [satelliteTrajectory, setSatelliteTrajectory] = useState([]); + const [forecastTrajectory, setForecastTrajectory] = useState([]); // New state for forecast + + // Fetch the current position of Landsat 8 periodically + useEffect(() => { + const fetchSatellitePosition = async () => { + try { + const response = await fetch("/api/satellite/landsat_8"); + const data = await response.json(); + if (data.latitude && data.longitude) { + const currentPosition: LatLngTuple = [data.latitude, data.longitude]; + setSatellitePosition(currentPosition); + + // Append current position to trajectory for visualization + setSatelliteTrajectory((prevTrajectory) => { + // Limit the length of the trajectory for performance + const updatedTrajectory = [...prevTrajectory, currentPosition]; + return updatedTrajectory.length > 100 ? updatedTrajectory.slice(1) : updatedTrajectory; + }); + } + } catch (error) { + console.error("Error fetching satellite position:", error); + } + }; + + // Fetch position every 10 seconds + fetchSatellitePosition(); + const interval = setInterval(fetchSatellitePosition, 10000); + + return () => clearInterval(interval); + }, []); + + // Fetch the forecasted trajectory + useEffect(() => { + const fetchForecastTrajectory = async () => { + try { + const response = await fetch("/api/satellite/landsat_8/forecast"); + const data = await response.json(); + if (data.forecast && Array.isArray(data.forecast)) { + const forecastPositions: LatLngTuple[] = data.forecast.map((point: { longitude: number, latitude: number }) => [point.latitude, point.longitude]); + setForecastTrajectory(forecastPositions); + } + } catch (error) { + console.error("Error fetching satellite forecast trajectory:", error); + } + }; + + fetchForecastTrajectory(); + + // Optionally, refresh the forecast periodically + const interval = setInterval(fetchForecastTrajectory, 3600000); // Refresh every hour + + return () => clearInterval(interval); + }, []); + + return ( + <> + {/* Render the Landsat 8 satellite position */} + {satellitePosition && } + + {/* Render the Landsat 8 satellite trajectory */} + {satelliteTrajectory.length > 1 && ( + + )} + + {/* Render the forecasted trajectory */} + {forecastTrajectory.length > 1 && ( + + )} + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 358343c..e2c061a 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -10,7 +10,7 @@ import SubscribeModal from "./components/SubscribeModal"; import { Pin } from "@/types/types"; export default function LandsatMap() { - const { customIcon } = useLeaflet(); + const { customIcon, satelliteIcon } = useLeaflet(); const [pins, setPins] = useState([]); const [latInput, setLatInput] = useState(""); @@ -21,10 +21,11 @@ export default function LandsatMap() { return (
- {customIcon != null && ((); const [customIcon, setCustomIcon] = useState(null); + const [satelliteIcon, setSatelliteIcon] = useState(null); useEffect(() => { (async () => { @@ -37,9 +38,24 @@ export default function useLeaflet() { shadowSize: [41, 41], }); setCustomIcon(customIcon); + + + const satelliteIcon = new L.Icon({ + iconUrl: + "https://cdn.icon-icons.com/icons2/2479/PNG/512/satellite_icon_149781.png", + iconRetinaUrl: + "https://cdn.icon-icons.com/icons2/2479/PNG/512/satellite_icon_149781.png", + iconSize: [30, 30], + iconAnchor: [15, 15], + popupAnchor: [0, -15], + shadowUrl: + "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png", + shadowSize: [41, 41], + }); + setSatelliteIcon(satelliteIcon); } })(); }, []); - return { LRef, customIcon }; + return { LRef, customIcon, satelliteIcon }; }