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"
|
"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
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