Skip to content

Commit

Permalink
live sats
Browse files Browse the repository at this point in the history
  • Loading branch information
MouseAndKeyboard committed Oct 6, 2024
1 parent 346853c commit af3fc99
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 10 deletions.
87 changes: 81 additions & 6 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -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}


1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
fastapi
uvicorn
skyfield
12 changes: 12 additions & 0 deletions frontend/src/app/components/MapComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -28,6 +33,7 @@ interface MapComponentProps {
pins: Pin[];
setPins: React.Dispatch<React.SetStateAction<Pin[]>>;
customIcon: Icon<IconOptions> | DivIcon | undefined;
satelliteIcon: Icon<IconOptions> | DivIcon | undefined;
latInput: string;
lngInput: string;
setLatInput: React.Dispatch<React.SetStateAction<string>>;
Expand All @@ -38,6 +44,7 @@ export default function MapComponent({
pins,
setPins,
customIcon,
satelliteIcon,
latInput,
lngInput,
setLatInput,
Expand Down Expand Up @@ -70,6 +77,11 @@ export default function MapComponent({
onLatChange={setLatInput}
onLngChange={setLngInput}
/>
<SatelliteLayer customIcon={satelliteIcon} />
</MapContainer>




);
}
2 changes: 1 addition & 1 deletion frontend/src/app/components/SRDataModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface SRDataModalProps {

interface SRDataPoint {
date: string;
[key: string]: string | number; // Allow both string and number types
[key: string]: string | number;
}


Expand Down
86 changes: 86 additions & 0 deletions frontend/src/app/components/SatelliteLayer.tsx
Original file line number Diff line number Diff line change
@@ -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<IconOptions> | DivIcon | undefined;
}

export default function SatelliteLayer({ customIcon }: SatelliteLayerProps) {
// State for the Landsat 8 satellite position and trajectory
const [satellitePosition, setSatellitePosition] = useState<LatLngTuple | null>(null);
const [satelliteTrajectory, setSatelliteTrajectory] = useState<LatLngTuple[]>([]);
const [forecastTrajectory, setForecastTrajectory] = useState<LatLngTuple[]>([]); // 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 && <Marker position={satellitePosition} icon={customIcon} />}

{/* Render the Landsat 8 satellite trajectory */}
{satelliteTrajectory.length > 1 && (
<Polyline positions={satelliteTrajectory} color="red" />
)}

{/* Render the forecasted trajectory */}
{forecastTrajectory.length > 1 && (
<Polyline positions={forecastTrajectory} color="blue" />
)}
</>
);
}
5 changes: 3 additions & 2 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pin[]>([]);
const [latInput, setLatInput] = useState("");
Expand All @@ -21,10 +21,11 @@ export default function LandsatMap() {

return (
<div className="relative h-screen w-screen">
{customIcon != null && (<MapComponent
{customIcon != null && satelliteIcon != null && (<MapComponent
pins={pins}
setPins={setPins}
customIcon={customIcon}
satelliteIcon={satelliteIcon}
latInput={latInput}
lngInput={lngInput}
setLatInput={setLatInput}
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/hooks/useLeaflet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type * as Leaflet from "leaflet"; // Import types only
export default function useLeaflet() {
const LRef = useRef<typeof Leaflet>();
const [customIcon, setCustomIcon] = useState<Leaflet.Icon | null>(null);
const [satelliteIcon, setSatelliteIcon] = useState<Leaflet.Icon | null>(null);

useEffect(() => {
(async () => {
Expand Down Expand Up @@ -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 };
}

0 comments on commit af3fc99

Please sign in to comment.