socket added

This commit is contained in:
Roozbeh Karimi 2025-10-01 20:14:07 -04:00
parent 2631ba13d9
commit 85fdbe085a
41 changed files with 9346 additions and 6396 deletions

14
.gitignore vendored
View File

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

File diff suppressed because it is too large Load Diff

27
backend/package.json Normal file
View 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
View 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")}`
);
});

View 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
View 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),
};

View 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,
};

View 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 };

View 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;

View 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;

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

File diff suppressed because it is too large Load Diff

31
frontend/package.json Normal file
View 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"
}
}

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 128 B

View File

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

View File

@ -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);

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

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

View File

@ -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(

View 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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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(),
});
}

View File

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

View 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
View 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
View 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;

View File

@ -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;