firs phase done (show flights on map)

This commit is contained in:
Roozbeh Karimi 2025-09-15 19:41:15 -04:00
parent 4a59ab79f4
commit 2631ba13d9
8 changed files with 1366 additions and 34 deletions

1004
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,13 +9,22 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2",
"@types/leaflet": "^1.9.20",
"axios": "^1.12.2",
"leaflet": "^1.9.4",
"next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"next": "15.5.3" "react-leaflet": "^5.0.0",
"react-leaflet-markercluster": "^5.0.0-rc.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.5.3", "eslint-config-next": "15.5.3"
"@eslint/eslintrc": "^3"
} }
} }

42
src/app/api/Api.jsx Normal file
View File

@ -0,0 +1,42 @@
export async function OpenStreetGet(data) {
console.log("get data", data);
try {
const response = await fetch("/api/opensky", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
southWest: data.southWest,
northEast: data.northEast,
}),
});
if (!response.ok) {
// Only consume the response once
let errorMessage;
try {
const errorData = await response.json();
errorMessage =
errorData.error || `HTTP error! status: ${response.status}`;
} catch {
errorMessage = `HTTP error! status: ${response.status}`;
}
throw new Error(errorMessage);
}
// Parse the JSON only once and store it
const result = await response.json();
console.log("Response data:", {
state: result.states,
time: new Date(result.time * 1000).toISOString(),
});
return result;
} catch (error) {
console.error("Failed to fetch aircraft data:", error);
throw error;
}
}

View File

@ -0,0 +1,111 @@
import axios from "axios";
import { NextResponse } from "next/server";
let accessToken = null;
let tokenExpiry = null;
// Server-side - no NEXT_PUBLIC_ prefix needed
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
async function getAccessToken() {
console.log("Server-side env check:", {
hasClientId: !!CLIENT_ID,
hasClientSecret: !!CLIENT_SECRET,
});
if (!CLIENT_ID || !CLIENT_SECRET) {
throw new Error("Missing OAuth credentials in server environment");
}
if (accessToken && tokenExpiry && Date.now() < tokenExpiry) {
return accessToken;
}
try {
console.log("Requesting new OAuth2 token...");
const response = await axios.post(
"https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token",
new URLSearchParams({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
timeout: 15000,
}
);
accessToken = response.data.access_token;
const expiresIn = response.data.expires_in || 3600;
tokenExpiry = Date.now() + expiresIn * 1000 - 60000;
console.log(`✅ OAuth2 token acquired, expires in ${expiresIn} seconds`);
return accessToken;
} catch (error) {
console.error("❌ Failed to get OAuth2 token:", {
status: error.response?.status,
data: error.response?.data,
message: error.message,
});
throw new Error(`OAuth2 authentication failed: ${error.message}`);
}
}
export async function POST(request) {
try {
const body = await request.json();
const { southWest, northEast } = body;
const params = {
lamin: southWest?.lat || 40,
lamax: northEast?.lat || 50,
lonmin: southWest?.lng || -10,
lonmax: northEast?.lng || 10,
};
console.log("API Parameters:", params);
const token = await getAccessToken();
const response = await axios.get(
`https://opensky-network.org/api/states/all?lamin=${southWest?.lat}&lamax=${northEast?.lat}&lomin=${southWest?.lng}&lomax=${northEast?.lng}`,
{
headers: {
Authorization: `Bearer ${token}`,
"User-Agent": "NextJS-App/1.0",
},
timeout: 30000,
}
);
console.log("✅ Authenticated request successful");
console.log("Response data:", {
statesCount: response.data.states || 0,
time: new Date(response.data.time * 1000).toISOString(),
});
return NextResponse.json(response.data);
} catch (error) {
console.error("❌ API Request failed:", error.message);
return NextResponse.json(
{
error: error.message,
details: error.response?.data,
},
{ status: 500 }
);
}
}
// Optional: Add GET method for testing
export async function GET() {
return NextResponse.json({
message: "OpenSky API endpoint is working",
timestamp: new Date().toISOString(),
});
}

8
src/app/map/page.jsx Normal file
View File

@ -0,0 +1,8 @@
import MapView from '@/views/map/page';
const Map = () => {
return <MapView/>
};
export default Map;

81
src/app/utils/map.jsx Normal file
View File

@ -0,0 +1,81 @@
import { useMap } from "react-leaflet";
import { useEffect } from "react";
import FlightIcon from "@mui/icons-material/Flight";
import { renderToString } from "react-dom/server";
import { divIcon } from "leaflet";
export function MapBounds({ setBoundries }) {
// Destructure props properly
const map = useMap();
useEffect(() => {
const handleMoveEnd = () => {
const bounds = map.getBounds();
console.log("Map Bounds:", bounds);
const southWest = bounds.getSouthWest();
const northEast = bounds.getNorthEast();
const data = {
southWest,
northEast,
};
console.log(data);
setBoundries(data); // Actually call the setter function
};
map.on("moveend", handleMoveEnd);
// Cleanup event listener
return () => {
map.off("moveend", handleMoveEnd);
};
}, [map, setBoundries]);
return null;
}
export const createRotatedFlightIcon = (flight) => {
const heading = flight[10] || 0;
const velocity = flight[9] || 0;
const altitude = flight[7] || 0;
// Different yellow shades based on flight characteristics
let iconColor = "#FFD700"; // Default gold yellow
if (altitude < 1000) {
iconColor = "#FFC107"; // Amber for low altitude
} else if (velocity > 250) {
iconColor = "#F9A825"; // Darker yellow for high speed
}
const iconHtml = renderToString(
<div
style={{
padding: "6px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<FlightIcon
style={{
fill: "#754d40ff",
fontSize: 20,
transform: `rotate(${heading}deg)`,
display: "block",
transition: "transform 0.3s ease",
// filter: "drop-shadow(2px 2px 4px rgba(0,0,0,0.5))",
}}
/>
</div>
);
return divIcon({
html: iconHtml,
iconSize: [36, 36],
iconAnchor: [18, 18],
popupAnchor: [0, -18],
className: "rotated-flight-icon",
});
};

71
src/views/map/page.jsx Normal file
View File

@ -0,0 +1,71 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { MapContainer, TileLayer, useMap, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { createRotatedFlightIcon, MapBounds } from "@/app/utils/map";
import { OpenStreetGet } from "@/app/api/Api";
// Example usage component
const MapView = () => {
const [bounderies, setBoundries] = useState();
const [flights, setFlights] = useState();
const sampleMarkers = [
{
lat: 51.505,
lng: -0.09,
popup: "<b>London</b><br>Capital of England",
},
{
lat: 51.51,
lng: -0.1,
popup: "<b>Westminster</b><br>Government district",
},
];
useEffect(() => {
console.log("Boundaries updated:", bounderies);
if (bounderies) {
getFlights();
console.log(bounderies);
}
}, [bounderies]); // Add bounderies as dependency
const getFlights = async () => {
await OpenStreetGet(bounderies).then((resp) => {
console.log(resp);
setFlights(resp.states);
});
console.log("flights:", flights);
};
const position = [43.6792, -79.6178];
return (
<MapContainer
center={[43.6532, -79.3832]} // Toronto
zoom={13}
style={{ height: "100vh", width: "100%" }}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{flights &&
flights.map((flight, index) => {
console.log(flight);
const position = [flight[6], flight[5]];
return (
<Marker
icon={createRotatedFlightIcon(flight)}
key={index}
position={position}
>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
);
})}
<MapBounds setBoundries={setBoundries} />
</MapContainer>
);
};
export default MapView;

68
src/views/map/styles.min.css vendored Normal file
View File

@ -0,0 +1,68 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}
.leaflet-cluster-anim .leaflet-marker-icon,
.leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out,
-webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out,
-moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out,
-o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}