firs phase done (show flights on map)
This commit is contained in:
parent
4a59ab79f4
commit
2631ba13d9
1004
package-lock.json
generated
1004
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
|
|
@ -9,13 +9,22 @@
|
|||
"lint": "eslint"
|
||||
},
|
||||
"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-dom": "19.1.0",
|
||||
"next": "15.5.3"
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-leaflet-markercluster": "^5.0.0-rc.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.3",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"eslint-config-next": "15.5.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
src/app/api/Api.jsx
Normal file
42
src/app/api/Api.jsx
Normal 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;
|
||||
}
|
||||
}
|
||||
111
src/app/api/opensky/route.js
Normal file
111
src/app/api/opensky/route.js
Normal 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
8
src/app/map/page.jsx
Normal 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
81
src/app/utils/map.jsx
Normal 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
71
src/views/map/page.jsx
Normal 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
68
src/views/map/styles.min.css
vendored
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user