socket added
14
.gitignore
vendored
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
# dependencies
|
||||
/node_modules
|
||||
*/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
|
|
@ -31,7 +32,9 @@ yarn-error.log*
|
|||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
@ -39,3 +42,12 @@ yarn-error.log*
|
|||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
|
||||
# Build outputs
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
1486
backend/package-lock.json
generated
Normal file
27
backend/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "nodemon server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"all-the-cities": "^3.1.0",
|
||||
"axios": "^1.12.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^5.1.0",
|
||||
"express-fileupload": "^1.5.2",
|
||||
"helmet": "^8.1.0",
|
||||
"morgan": "^1.10.1",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
}
|
||||
158
backend/server.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
const http = require("http");
|
||||
const morgan = require("morgan");
|
||||
const bodyParser = require("body-parser");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const fileUpload = require("express-fileupload");
|
||||
const helmet = require("helmet");
|
||||
const cors = require("cors");
|
||||
const flightStore = require("./src/stores/flightStore");
|
||||
const flighData = require("./src/modules/data.js");
|
||||
const app = express();
|
||||
const { init } = require("./src/modules/socket.js");
|
||||
const { initFlightSockets } = require("./src/controllers/flights");
|
||||
|
||||
const RELEASE = require("./package.json").version;
|
||||
const NODE_ENV = process.env.NODE_ENV || "production";
|
||||
const NODE_PORT = process.env.NODE_PORT || 3001;
|
||||
|
||||
app.set("port", NODE_PORT);
|
||||
app.set("env", NODE_ENV);
|
||||
app.set("ver", RELEASE);
|
||||
|
||||
function logRequests(req, res, next) {
|
||||
console.log(`Incoming Request: ${req.method} ${req.url}`);
|
||||
console.log(`Origin: ${req.headers.origin}`);
|
||||
next();
|
||||
}
|
||||
app.use(logRequests);
|
||||
|
||||
const allowedOrigins = ["http://localhost:3000"];
|
||||
|
||||
// if (NODE_ENV === "development" || NODE_ENV === "staging") {
|
||||
// allowedOrigins.push("http://localhost:3000");
|
||||
// }
|
||||
|
||||
const corsOptions = {
|
||||
origin: function (origin, callback) {
|
||||
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
}
|
||||
},
|
||||
allowedHeaders: [
|
||||
"Accept-Version",
|
||||
"Authorization",
|
||||
"Credentials",
|
||||
"Content-Type",
|
||||
"baggage",
|
||||
],
|
||||
credentials: true,
|
||||
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
|
||||
optionsSuccessStatus: 204,
|
||||
};
|
||||
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
"default-src": ["'self'"],
|
||||
"connect-src": ["'self'", "ws:", "wss:", ...allowedOrigins],
|
||||
"script-src": ["'self'", "'unsafe-inline'"],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
app.disable("x-powered-by");
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
app.use(bodyParser.json({ limit: "200mb" }));
|
||||
app.use(bodyParser.urlencoded({ extended: true, limit: "200mb" }));
|
||||
|
||||
app.use(
|
||||
fileUpload({
|
||||
limits: { fileSize: 200 * 1024 * 1024 }, // 200MB
|
||||
})
|
||||
);
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
//Socket io
|
||||
const server = http.createServer(app);
|
||||
const io = init(server, {
|
||||
cors: { origin: allowedOrigins },
|
||||
});
|
||||
initFlightSockets(io);
|
||||
|
||||
app.options(/.*/, cors(corsOptions));
|
||||
app.set("trust proxy", true);
|
||||
|
||||
app.use(
|
||||
morgan(
|
||||
":remote-addr - :remote-user - [:date[clf]] ':method :url HTTP/:http-version' :status :res[content-length] ':referrer' ':user-agent'",
|
||||
{
|
||||
stream: {
|
||||
write: (message, encoding) => {
|
||||
console.log(message.substring(0, message.lastIndexOf("\n")));
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
setInterval(flighData.getAllFlights, 2000);
|
||||
|
||||
const routesBasePath = require("./src/routes");
|
||||
app.use("/api/v1", routesBasePath);
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack); // Log the error for debugging
|
||||
const statusCode = err.statusCode || 500;
|
||||
const message = err.message || "Something went wrong!";
|
||||
|
||||
res.status(statusCode).json({
|
||||
status: "error",
|
||||
message: message,
|
||||
path: req.path,
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error("Error Details:", {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const statusCode = err.statusCode || err.status || 500;
|
||||
|
||||
const message =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "Internal Server Error"
|
||||
: err.message;
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: message,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(process.env.NODE_ENV !== "production" && {
|
||||
stack: err.stack,
|
||||
details: err,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Set listening port
|
||||
server.listen(NODE_PORT, async () => {
|
||||
console.log(
|
||||
`Express Server with Socket.io started on Port: ${app.get(
|
||||
"port"
|
||||
)} | Environment: ${app.get("env")} | Release: ${app.get("ver")}`
|
||||
);
|
||||
});
|
||||
61
backend/src/controllers/flights.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
let ioInstance = null;
|
||||
const api = require("../modules/api");
|
||||
|
||||
async function getAllFlights(req, res, next) {
|
||||
try {
|
||||
return res.status(200).json(global.flighsData.ac);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
function initFlightSockets(io) {
|
||||
ioInstance = io;
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log("User connected:", socket.id);
|
||||
|
||||
socket.on("flightsData", (bounds) => {
|
||||
console.log(bounds);
|
||||
socket.bounds = bounds;
|
||||
|
||||
// Send initial filtered flights
|
||||
if (global.flightsData) {
|
||||
const filtered = filterFlightsByBounds(global.flightsData, bounds);
|
||||
socket.emit("flightsData", { ac: filtered });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("User disconnected:", socket.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filterFlightsByBounds(flights, bounds) {
|
||||
if (!bounds) return flights;
|
||||
return flights.ac.filter(
|
||||
(flight) =>
|
||||
flight.lat <= bounds.northEast.lat &&
|
||||
flight.lat >= bounds.southWest.lat &&
|
||||
flight.lon <= bounds.northEast.lng &&
|
||||
flight.lon >= bounds.southWest.lng
|
||||
);
|
||||
}
|
||||
|
||||
function notifyFlightsUpdate(flights) {
|
||||
if (!ioInstance) return;
|
||||
|
||||
ioInstance.sockets.sockets.forEach((socket) => {
|
||||
if (socket.bounds) {
|
||||
const filtered = filterFlightsByBounds(flights, socket.bounds);
|
||||
socket.emit("flightsData", { ac: filtered });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllFlights,
|
||||
initFlightSockets,
|
||||
notifyFlightsUpdate,
|
||||
};
|
||||
134
backend/src/modules/api.js
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
function respond(req, res, code = 200, data = null) {
|
||||
// Complete HTTP status code definitions
|
||||
const STATUS_CODES = {
|
||||
// 2xx Success
|
||||
200: "OK",
|
||||
201: "Created",
|
||||
202: "Accepted",
|
||||
204: "No Content",
|
||||
|
||||
// 3xx Redirection
|
||||
301: "Moved Permanently",
|
||||
302: "Found",
|
||||
304: "Not Modified",
|
||||
|
||||
// 4xx Client Error
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
409: "Conflict",
|
||||
422: "Unprocessable Entity",
|
||||
429: "Too Many Requests",
|
||||
|
||||
// 5xx Server Error
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
};
|
||||
|
||||
// Validate status code
|
||||
if (!Number.isInteger(code) || code < 100 || code > 599) {
|
||||
throw new Error(`Invalid HTTP status code: ${code}`);
|
||||
}
|
||||
|
||||
// Determine status category based on first digit
|
||||
const getStatusCategory = (statusCode) => {
|
||||
const firstDigit = Math.floor(statusCode / 100);
|
||||
switch (firstDigit) {
|
||||
case 2:
|
||||
return "success";
|
||||
case 3:
|
||||
return "redirect";
|
||||
case 4:
|
||||
return "fail";
|
||||
case 5:
|
||||
return "error";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
// Build base response object
|
||||
const baseResponse = {
|
||||
code,
|
||||
status: getStatusCategory(code),
|
||||
message: STATUS_CODES[code] || "Unknown Status",
|
||||
};
|
||||
|
||||
// Handle different data types
|
||||
if (data === null || data === false) {
|
||||
// Simple status response
|
||||
return res.status(code).json(baseResponse);
|
||||
}
|
||||
|
||||
if (typeof data === "string") {
|
||||
// String message override
|
||||
return res.status(code).json({
|
||||
...baseResponse,
|
||||
message: data,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof data === "object" && data !== null) {
|
||||
// Handle pagination links if present
|
||||
if (data.links && req) {
|
||||
const baseUrl = `${req.protocol}://${req.hostname}${
|
||||
req.originalUrl.split("?")[0]
|
||||
}?`;
|
||||
|
||||
const linkFields = ["self", "first", "previous", "next", "last"];
|
||||
linkFields.forEach((field) => {
|
||||
if (data.links[field]) {
|
||||
data.links[field] = baseUrl + data.links[field];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return merged response
|
||||
return res.status(code).json({
|
||||
...baseResponse,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback for unexpected data types
|
||||
return res.status(code).json({
|
||||
...baseResponse,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
function debug(output, detail = null) {
|
||||
// TODO: re-enable if statement below
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const err = new Error();
|
||||
const stack = err.stack.split("\n");
|
||||
const callerLine = stack[2];
|
||||
const callerInfo = callerLine.match(/\((.*):(\d+):\d+\)/);
|
||||
const lineNumber = callerInfo ? callerInfo[2] : "unknown";
|
||||
const functionName = debug.caller ? debug.caller.name : "anonymous";
|
||||
console.log(
|
||||
`====== Function: ${functionName} | Line: ${lineNumber} | Detail: ${detail} ======`
|
||||
);
|
||||
console.log(output);
|
||||
// //old
|
||||
// +console.log(============= function ${debug.caller.name} ${detail ? ': ' + detail : ''} =============== );
|
||||
// console.log(output);
|
||||
}
|
||||
}
|
||||
// Optional: Export with additional helper methods
|
||||
module.exports = {
|
||||
respond,
|
||||
debug,
|
||||
// Convenience methods for common responses
|
||||
success: (req, res, data = null) => respond(req, res, 200, data),
|
||||
created: (req, res, data = null) => respond(req, res, 201, data),
|
||||
badRequest: (req, res, message = null) => respond(req, res, 400, message),
|
||||
unauthorized: (req, res, message = null) => respond(req, res, 401, message),
|
||||
forbidden: (req, res, message = null) => respond(req, res, 403, message),
|
||||
notFound: (req, res, message = null) => respond(req, res, 404, message),
|
||||
serverError: (req, res, message = null) => respond(req, res, 500, message),
|
||||
};
|
||||
41
backend/src/modules/data.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
const axios = require("axios");
|
||||
const api = require("./api");
|
||||
|
||||
const { getIO } = require("./socket");
|
||||
|
||||
const controller = {
|
||||
flightsController: require("../controllers/flights"),
|
||||
};
|
||||
|
||||
async function getAllFlights(req, res) {
|
||||
try {
|
||||
/*
|
||||
/v2/hex/[hex] GET Return all aircraft with an exact match on one or more Mode S hex IDs (max ~1000 per request).
|
||||
/v2/callsign/[callsign] GET Return all aircraft exactly matching one or more callsigns.
|
||||
/v2/reg/[reg] GET Return all aircraft exactly matching one or more registrations (tail numbers).
|
||||
/v2/type/[type] GET Return all aircraft having specified ICAO aircraft type codes (e.g. “A321”, “B738”, etc.).
|
||||
/v2/squawk/[squawk] GET Return all aircraft currently squawking a specified transponder code.
|
||||
/v2/mil/ GET Returns all aircraft tagged as military.
|
||||
/v2/ladd/ GET Returns all aircraft tagged as LADD. (LADD = “Low Altitude Demonstration Data” / some specific tag)
|
||||
/v2/pia/ GET Returns all aircraft tagged as PIA (Privacy-oriented / some masking / private-or-government address)
|
||||
/v2/point/[lat]/[lon]/[radius] GET Returns all aircraft within a given radius of a geographic point, up to 250 nautical miles radius.
|
||||
*/
|
||||
|
||||
const response = await axios.get(
|
||||
// `https://api.adsb.one/v2/mil/`
|
||||
`https://api.adsb.one/v2/point/43.6532/-79.3832/250`
|
||||
);
|
||||
|
||||
// set flights details in global.
|
||||
|
||||
global.flightsData = response.data;
|
||||
controller.flightsController.notifyFlightsUpdate(global.flightsData); // tell controller to send updates
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
api.respond(req, res, 404, "Error: failed to get flights information");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllFlights,
|
||||
};
|
||||
19
backend/src/modules/socket.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
let io;
|
||||
const api = require("./api");
|
||||
|
||||
function init(server, cors) {
|
||||
const { Server } = require("socket.io");
|
||||
io = new Server(server, {
|
||||
cors: cors,
|
||||
});
|
||||
return io;
|
||||
}
|
||||
|
||||
function getIO() {
|
||||
if (!io) {
|
||||
throw new Error("Socket.io not initialized!");
|
||||
}
|
||||
return io;
|
||||
}
|
||||
|
||||
module.exports = { init, getIO };
|
||||
15
backend/src/routes/flights.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const api = require("../modules/api");
|
||||
const controllers = {
|
||||
flightsController: require("../controllers/flights"),
|
||||
};
|
||||
|
||||
router.use(async (req, res, next) => {
|
||||
// run any additional pre operations
|
||||
next();
|
||||
});
|
||||
|
||||
router.get("/", controllers.flightsController.getAllFlights);
|
||||
|
||||
module.exports = router;
|
||||
16
backend/src/routes/index.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const api = require("../modules/api");
|
||||
|
||||
const routes = {
|
||||
flights: require("./flights"),
|
||||
};
|
||||
|
||||
router.use(async (req, res, next) => {
|
||||
// run any additional pre operations
|
||||
next();
|
||||
});
|
||||
|
||||
router.use("/flights", routes.flights);
|
||||
|
||||
module.exports = router;
|
||||
293
backend/src/stores/flightStore.js
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
const axios = require("axios");
|
||||
|
||||
// stores/flightStore.js
|
||||
class FlightStore {
|
||||
constructor() {
|
||||
this.flights = new Map(); // flightId -> flightData
|
||||
this.lastUpdated = new Map(); // flightId -> timestamp
|
||||
this.subscribers = new Set(); // callback functions
|
||||
this.updateInterval = null;
|
||||
this.isUpdating = false;
|
||||
this.lastApiCall = 0; // Track last API call time
|
||||
this.minApiInterval = 1000; // 1 second between API calls
|
||||
}
|
||||
|
||||
getFlight(flightId) {
|
||||
return this.flights.get(flightId) || null;
|
||||
}
|
||||
|
||||
getAllFlights() {
|
||||
return Object.fromEntries(this.flights);
|
||||
}
|
||||
|
||||
setFlight(flightId, flightData) {
|
||||
const oldData = this.flights.get(flightId);
|
||||
this.flights.set(flightId, flightData);
|
||||
this.lastUpdated.set(flightId, new Date());
|
||||
|
||||
this.notifySubscribers("flightUpdated", {
|
||||
flightId,
|
||||
oldData,
|
||||
newData: flightData,
|
||||
});
|
||||
|
||||
if (global.emitFlightUpdate) {
|
||||
global.emitFlightUpdate(flightId, flightData);
|
||||
}
|
||||
return flightData;
|
||||
}
|
||||
|
||||
setFlights(flightsData) {
|
||||
Object.entries(flightsData).forEach(([flightId, flightData]) => {
|
||||
this.setFlight(flightId, flightData);
|
||||
});
|
||||
}
|
||||
|
||||
removeFlight(flightId) {
|
||||
const removed = this.flights.delete(flightId);
|
||||
this.lastUpdated.delete(flightId);
|
||||
|
||||
if (removed) {
|
||||
this.notifySubscribers("flightRemoved", { flightId });
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
isFlightStale(flightId, maxAgeMs = 60000) {
|
||||
const lastUpdate = this.lastUpdated.get(flightId);
|
||||
if (!lastUpdate) return true;
|
||||
return Date.now() - lastUpdate.getTime() > maxAgeMs;
|
||||
}
|
||||
|
||||
getStaleFlights(maxAgeMs = 60000) {
|
||||
return Array.from(this.flights.keys()).filter((flightId) =>
|
||||
this.isFlightStale(flightId, maxAgeMs)
|
||||
);
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.subscribers.add(callback);
|
||||
return () => this.subscribers.delete(callback);
|
||||
}
|
||||
|
||||
notifySubscribers(event, data) {
|
||||
this.subscribers.forEach((cb) => {
|
||||
try {
|
||||
cb(event, data);
|
||||
} catch (err) {
|
||||
console.error("Error in subscriber:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startAutoUpdates(intervalMs = 1000) {
|
||||
if (this.updateInterval) {
|
||||
console.log("Auto updates already running");
|
||||
return;
|
||||
}
|
||||
|
||||
if (intervalMs < this.minApiInterval) {
|
||||
console.warn(
|
||||
`⚠️ Interval ${intervalMs}ms < API rate limit. Adjusting to ${this.minApiInterval}ms`
|
||||
);
|
||||
intervalMs = this.minApiInterval;
|
||||
}
|
||||
|
||||
console.log(`🚀 Starting flight auto-updates every ${intervalMs}ms`);
|
||||
|
||||
// Do first update immediately
|
||||
this.updateAllFlights().catch((err) =>
|
||||
console.error("Initial update failed:", err)
|
||||
);
|
||||
|
||||
// Safe async interval
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.updateAllFlights().catch((err) =>
|
||||
console.error("Auto update failed:", err)
|
||||
);
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
stopAutoUpdates() {
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
this.updateInterval = null;
|
||||
console.log("Stopped auto-updates");
|
||||
}
|
||||
}
|
||||
|
||||
async updateAllFlights() {
|
||||
if (this.isUpdating) {
|
||||
console.log("Update already in progress, skipping...");
|
||||
return;
|
||||
}
|
||||
this.isUpdating = true;
|
||||
|
||||
try {
|
||||
// Rate limiting
|
||||
const now = Date.now();
|
||||
const elapsed = now - this.lastApiCall;
|
||||
if (elapsed < this.minApiInterval) {
|
||||
await new Promise((res) =>
|
||||
setTimeout(res, this.minApiInterval - elapsed)
|
||||
);
|
||||
}
|
||||
|
||||
this.lastApiCall = Date.now();
|
||||
console.log("📡 Fetching flights from API...");
|
||||
const flightsData = await this.fetchMultipleFlightsFromAPI();
|
||||
|
||||
if (flightsData && typeof flightsData === "object") {
|
||||
global.emitFlightUpdate(flightsData);
|
||||
// const changedFlights = [];
|
||||
|
||||
// for (const [flightId, flightData] of Object.entries(flightsData)) {
|
||||
// const oldData = this.flights.get(flightId);
|
||||
// if (!oldData || this.hasFlightDataChanged(oldData, flightData)) {
|
||||
// this.setFlight(flightId, flightData);
|
||||
// changedFlights.push(flightId);
|
||||
// }
|
||||
// }
|
||||
|
||||
// this.notifySubscribers("allFlightsUpdated", {
|
||||
// totalFlights: Object.keys(flightsData).length,
|
||||
// changedFlights: changedFlights.length,
|
||||
// updatedFlightIds: changedFlights,
|
||||
// timestamp: new Date(),
|
||||
// });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error updating flights:", err);
|
||||
this.notifySubscribers("updateError", {
|
||||
error: err.message,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
if (global.io) {
|
||||
global.io.emit("flightUpdateError", {
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
hasFlightDataChanged(oldData, newData) {
|
||||
const fields = [
|
||||
"status",
|
||||
"gate",
|
||||
"departure",
|
||||
"arrival",
|
||||
"delay",
|
||||
"terminal",
|
||||
];
|
||||
return fields.some(
|
||||
(field) =>
|
||||
JSON.stringify(oldData[field]) !== JSON.stringify(newData[field])
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// API calls (mocked here)
|
||||
// ===========================
|
||||
async fetchMultipleFlightsFromAPI(flightIds = []) {
|
||||
try {
|
||||
// If you always fetch all flights near Toronto, you can ignore flightIds
|
||||
const response = await axios.get(
|
||||
"https://api.adsb.one/v2/point/43.6532/-79.3832/10"
|
||||
);
|
||||
|
||||
// The ADSB API likely returns an array of flights, not keyed by ID
|
||||
// console.log("======================");
|
||||
// console.log(flightsArray);
|
||||
// Convert to an object keyed by ICAO or flightId
|
||||
const flightsData = response.data.ac || [];
|
||||
const flightsMap = [];
|
||||
Object.entries(flightsData).forEach(([flightId, flightData]) => {
|
||||
flightsMap.push({
|
||||
flightId: flightData.flightId || flightId,
|
||||
callsign: flightData.callsign?.trim() || null,
|
||||
registration: flightData.registration || null,
|
||||
type: flightData.type || null,
|
||||
desc: flightData.desc || null,
|
||||
altitude: flightData.altitude || null,
|
||||
lat: flightData.lat || null,
|
||||
lon: flightData.lon || null,
|
||||
track: flightData.track || null,
|
||||
lastSeen: flightData.lastSeen || Date.now(),
|
||||
});
|
||||
});
|
||||
console.log("==============================");
|
||||
console.log(flightsMap);
|
||||
console.log("==============================");
|
||||
return flightsMap;
|
||||
} catch (err) {
|
||||
console.error("❌ Error fetching flights from ADSB:", err.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async fetchFlightFromAPI(flightId) {
|
||||
const result = await this.fetchMultipleFlightsFromAPI([flightId]);
|
||||
return result[flightId];
|
||||
}
|
||||
|
||||
async getActiveFlightIds() {
|
||||
return this.flights.size > 0
|
||||
? Array.from(this.flights.keys())
|
||||
: ["FL001", "FL002", "FL003"];
|
||||
}
|
||||
|
||||
async addMultipleFlights(flightIds) {
|
||||
const data = await this.fetchMultipleFlightsFromAPI(flightIds);
|
||||
const results = {};
|
||||
for (const id of flightIds) {
|
||||
if (data[id]) {
|
||||
this.setFlight(id, data[id]);
|
||||
results[id] = data[id];
|
||||
} else {
|
||||
results[id] = null;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async initializeWithFlights(flightIds = null) {
|
||||
if (!flightIds) {
|
||||
flightIds = await this.getActiveFlightIds();
|
||||
}
|
||||
if (flightIds.length === 0) return;
|
||||
|
||||
await this.addMultipleFlights(flightIds);
|
||||
this.notifySubscribers("storeInitialized", {
|
||||
flightCount: flightIds.length,
|
||||
flights: flightIds,
|
||||
});
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Utils
|
||||
// ===========================
|
||||
getStats() {
|
||||
const stale = this.getStaleFlights().length;
|
||||
return {
|
||||
totalFlights: this.flights.size,
|
||||
staleFlights: stale,
|
||||
freshFlights: this.flights.size - stale,
|
||||
isAutoUpdating: !!this.updateInterval,
|
||||
isUpdating: this.isUpdating,
|
||||
subscribers: this.subscribers.size,
|
||||
};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.flights.clear();
|
||||
this.lastUpdated.clear();
|
||||
this.notifySubscribers("storeCleared", {});
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
const flightStore = new FlightStore();
|
||||
module.exports = flightStore;
|
||||
41
frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
6410
frontend/package-lock.json
generated
Normal file
31
frontend/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "aero-echo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"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",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-leaflet-markercluster": "^5.0.0-rc.0",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.3"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 128 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 385 B |
|
|
@ -1,20 +1,13 @@
|
|||
export async function OpenStreetGet(data) {
|
||||
console.log("get data", data);
|
||||
|
||||
export async function getFlightsApi(data) {
|
||||
try {
|
||||
const response = await fetch("/api/opensky", {
|
||||
method: "POST",
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/flights`, {
|
||||
method: "GET",
|
||||
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();
|
||||
|
|
@ -26,14 +19,8 @@ export async function OpenStreetGet(data) {
|
|||
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(),
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch aircraft data:", error);
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
7
frontend/src/app/map/page.jsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import MapView from "@/views/map/page";
|
||||
|
||||
const Map = () => {
|
||||
return <MapView />;
|
||||
};
|
||||
|
||||
export default Map;
|
||||
|
|
@ -5,7 +5,6 @@ import { renderToString } from "react-dom/server";
|
|||
import { divIcon } from "leaflet";
|
||||
|
||||
export function MapBounds({ setBoundries }) {
|
||||
// Destructure props properly
|
||||
const map = useMap();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -22,12 +21,11 @@ export function MapBounds({ setBoundries }) {
|
|||
};
|
||||
|
||||
console.log(data);
|
||||
setBoundries(data); // Actually call the setter function
|
||||
setBoundries(data);
|
||||
};
|
||||
|
||||
map.on("moveend", handleMoveEnd);
|
||||
|
||||
// Cleanup event listener
|
||||
return () => {
|
||||
map.off("moveend", handleMoveEnd);
|
||||
};
|
||||
|
|
@ -37,16 +35,15 @@ export function MapBounds({ setBoundries }) {
|
|||
}
|
||||
|
||||
export const createRotatedFlightIcon = (flight) => {
|
||||
const heading = flight[10] || 0;
|
||||
const velocity = flight[9] || 0;
|
||||
const altitude = flight[7] || 0;
|
||||
const heading = flight.track || 0;
|
||||
const velocity = flight.gs || 0;
|
||||
const altitude = flight.alt_baro || 0;
|
||||
|
||||
// Different yellow shades based on flight characteristics
|
||||
let iconColor = "#FFD700"; // Default gold yellow
|
||||
let iconColor = "#FFD700";
|
||||
if (altitude < 1000) {
|
||||
iconColor = "#FFC107"; // Amber for low altitude
|
||||
iconColor = "#FFC107";
|
||||
} else if (velocity > 250) {
|
||||
iconColor = "#F9A825"; // Darker yellow for high speed
|
||||
iconColor = "#F9A825";
|
||||
}
|
||||
|
||||
const iconHtml = renderToString(
|
||||
231
frontend/src/views/map/page.jsx
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { MapContainer, TileLayer, useMap, Pane } from "react-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { io } from "socket.io-client";
|
||||
import L from "leaflet";
|
||||
import { MapBounds } from "@/app/utils/map";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
delete L.Icon.Default.prototype._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
|
||||
iconUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
|
||||
shadowUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
|
||||
});
|
||||
}
|
||||
|
||||
const FlightsLayer = ({ flightsData, updateInterval = 5000 }) => {
|
||||
const map = useMap();
|
||||
const flightsRef = useRef({});
|
||||
|
||||
const drawMarker = (lat, lon, icon) => {
|
||||
const longitudes = [lon, lon - 360, lon + 360];
|
||||
return longitudes.map((lng) => {
|
||||
const marker = L.marker([lat, lng], { icon }).addTo(map);
|
||||
return marker;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
const now = Date.now();
|
||||
|
||||
flightsData.forEach((f) => {
|
||||
if (!f.lat || !f.lon) return;
|
||||
|
||||
const flightId = f.hex || f.flight || `${f.lat}_${f.lon}`;
|
||||
const direction = f.track || f.nav_heading || f.true_heading || 0;
|
||||
const icon = createPlaneIcon(direction);
|
||||
|
||||
if (!flightsRef.current[flightId]) {
|
||||
const markers = drawMarker(f.lat, f.lon, icon);
|
||||
|
||||
const popupContent = `
|
||||
<strong>Flight:</strong> ${f.flight?.trim() || "Unknown"}<br>
|
||||
<strong>Registration:</strong> ${f.r || "Unknown"}<br>
|
||||
<strong>Aircraft:</strong> ${f.desc || f.t || "Unknown"}<br>
|
||||
<strong>Altitude:</strong> ${f.alt_baro || "Unknown"} ft<br>
|
||||
<strong>Speed:</strong> ${f.gs || "Unknown"} kts<br>
|
||||
<strong>Track:</strong> ${f.track || "Unknown"}°
|
||||
`;
|
||||
markers[0].bindPopup(popupContent);
|
||||
|
||||
flightsRef.current[flightId] = {
|
||||
prev: { lat: f.lat, lon: f.lon },
|
||||
next: { lat: f.lat, lon: f.lon },
|
||||
startTime: now,
|
||||
endTime: now + updateInterval,
|
||||
markers,
|
||||
flightInfo: f,
|
||||
};
|
||||
} else {
|
||||
const flight = flightsRef.current[flightId];
|
||||
flight.prev = flight.next;
|
||||
flight.next = { lat: f.lat, lon: f.lon };
|
||||
flight.startTime = now;
|
||||
flight.endTime = now + updateInterval;
|
||||
flight.flightInfo = f;
|
||||
|
||||
const newIcon = createPlaneIcon(direction);
|
||||
|
||||
const popupContent = `
|
||||
<strong>Flight:</strong> ${f.flight?.trim() || "Unknown"}<br>
|
||||
<strong>Registration:</strong> ${f.r || "Unknown"}<br>
|
||||
<strong>Aircraft:</strong> ${f.desc || f.t || "Unknown"}<br>
|
||||
<strong>Altitude:</strong> ${f.alt_baro || "Unknown"} ft<br>
|
||||
<strong>Speed:</strong> ${f.gs || "Unknown"} kts<br>
|
||||
<strong>Track:</strong> ${f.track || "Unknown"}°
|
||||
`;
|
||||
flight.markers[0].setPopupContent(popupContent);
|
||||
|
||||
flight.markers.forEach((m, i) => {
|
||||
const lng = f.lon + (i === 1 ? -360 : i === 2 ? 360 : 0);
|
||||
m.setLatLng([f.lat, lng]);
|
||||
m.setIcon(newIcon);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const currentFlightIds = new Set(
|
||||
flightsData.map((f) => f.hex || f.flight || `${f.lat}_${f.lon}`)
|
||||
);
|
||||
Object.keys(flightsRef.current).forEach((flightId) => {
|
||||
if (!currentFlightIds.has(flightId)) {
|
||||
flightsRef.current[flightId].markers.forEach((m) => map.removeLayer(m));
|
||||
delete flightsRef.current[flightId];
|
||||
}
|
||||
});
|
||||
}, [flightsData, map, updateInterval]);
|
||||
|
||||
useEffect(() => {
|
||||
let animFrame;
|
||||
const animate = () => {
|
||||
const now = Date.now();
|
||||
Object.values(flightsRef.current).forEach((f) => {
|
||||
const t = Math.min(1, (now - f.startTime) / (f.endTime - f.startTime));
|
||||
const lat = f.prev.lat + (f.next.lat - f.prev.lat) * t;
|
||||
const lon = f.prev.lon + (f.next.lon - f.prev.lon) * t;
|
||||
|
||||
f.markers.forEach((m, i) => {
|
||||
const newLng = lon + (i === 1 ? -360 : i === 2 ? 360 : 0);
|
||||
m.setLatLng([lat, newLng]);
|
||||
});
|
||||
});
|
||||
animFrame = requestAnimationFrame(animate);
|
||||
};
|
||||
animFrame = requestAnimationFrame(animate);
|
||||
return () => cancelAnimationFrame(animFrame);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const createPlaneIcon = (heading = 0) => {
|
||||
const html = `
|
||||
<div style="
|
||||
transform: rotate(${heading}deg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
|
||||
">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"
|
||||
fill="#dc2626"
|
||||
stroke="#000000"
|
||||
stroke-width="1"
|
||||
stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
return L.divIcon({
|
||||
html,
|
||||
className: "",
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 14],
|
||||
});
|
||||
};
|
||||
|
||||
const createSimplePlaneIcon = (heading = 0) => {
|
||||
const html = `
|
||||
<div style="
|
||||
transform: rotate(${heading}deg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #dc2626;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
">
|
||||
✈️
|
||||
</div>
|
||||
`;
|
||||
return L.divIcon({
|
||||
html,
|
||||
className: "",
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12],
|
||||
});
|
||||
};
|
||||
|
||||
const MapView = () => {
|
||||
const [boundaries, setBoundaries] = useState();
|
||||
const [flights, setFlights] = useState([]);
|
||||
const socketRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
socketRef.current = io(process.env.NEXT_PUBLIC_SOCKET_SERVER_URL);
|
||||
|
||||
socketRef.current.on("connect", () =>
|
||||
console.log("Connected:", socketRef.current.id)
|
||||
);
|
||||
|
||||
socketRef.current.on("flightsData", (data) => {
|
||||
console.log(data);
|
||||
setFlights(data.ac || []);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (socketRef.current && boundaries) {
|
||||
socketRef.current.emit("flightsData", boundaries);
|
||||
}
|
||||
}, [boundaries]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{flights && (
|
||||
<MapContainer
|
||||
center={[43.6532, -79.3832]}
|
||||
zoom={9}
|
||||
maxBoundsViscosity={4}
|
||||
style={{ height: "100vh", width: "100%" }}
|
||||
worldCopyJump={true}
|
||||
>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
<FlightsLayer flightsData={flights} updateInterval={2000} />
|
||||
<MapBounds setBoundries={setBoundaries} />
|
||||
</MapContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MapView;
|
||||
6407
package-lock.json
generated
31
package.json
|
|
@ -1,30 +1,15 @@
|
|||
{
|
||||
"name": "aero-echo",
|
||||
"version": "0.1.0",
|
||||
"name": "my-fullstack-app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"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",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-leaflet-markercluster": "^5.0.0-rc.0"
|
||||
"dev": "concurrently \"npm run backend:dev\" \"npm run frontend:dev\"",
|
||||
"backend:dev": "cd backend && npm run dev",
|
||||
"frontend:dev": "cd frontend && npm run dev",
|
||||
"backend:start": "cd backend && npm start",
|
||||
"frontend:start": "cd frontend && npm start",
|
||||
"install:all": "cd backend && npm install && cd ../frontend && npm install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.3"
|
||||
"concurrently": "^8.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
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(),
|
||||
});
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import MapView from '@/views/map/page';
|
||||
|
||||
|
||||
const Map = () => {
|
||||
return <MapView/>
|
||||
};
|
||||
|
||||
export default Map;
|
||||
63
src/controllers/flights.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
const axios = require("axios");
|
||||
|
||||
const CLIENT_ID = process.env.CLIENT_ID;
|
||||
const CLIENT_SECRET = process.env.CLIENT_SECRET;
|
||||
let accessToken = null;
|
||||
let tokenExpiry = null;
|
||||
|
||||
async function getAccessToken() {
|
||||
if (!CLIENT_ID || !CLIENT_SECRET) {
|
||||
throw new Error("Missing OAuth credentials in server environment");
|
||||
}
|
||||
|
||||
if (accessToken && tokenExpiry && Date.now() < tokenExpiry) {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
try {
|
||||
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;
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllFlights(req, res) {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`https://api.adsb.one/v2/point/43.6532/-79.3832/250`
|
||||
);
|
||||
console.log(response);
|
||||
|
||||
return res.status(200).json(response.data.ac);
|
||||
} catch (error) {
|
||||
return res.status(404).message("Error: failed to get flights information");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllFlights,
|
||||
};
|
||||
15
src/routes/flights.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const controllers = {
|
||||
flights: require("../controllers/flights"),
|
||||
};
|
||||
|
||||
router.use(async (req, res, next) => {
|
||||
// run any additional pre operations
|
||||
next();
|
||||
});
|
||||
|
||||
router.use("/", controllers.flights.getAllFlights);
|
||||
|
||||
module.exports = router;
|
||||
14
src/routes/index.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const routes = {
|
||||
flights: require("./flights"),
|
||||
};
|
||||
|
||||
router.use(async (req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
router.use("/flights", routes.flights);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
"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;
|
||||