#include "crow.h"
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <map>
#include <algorithm>
#include <cmath>
#include <mutex>
// --- Constants & Math Helpers ---
const double PI = 3.14159265358979323846;
double haversine(double lat1, double lon1, double lat2, double lon2) {
const double R = 3958.8; // Earth radius in miles
double dLat = (lat2 - lat1) * PI / 180.0;
double dLon = (lon2 - lon1) * PI / 180.0;
lat1 = lat1 * PI / 180.0;
lat2 = lat2 * PI / 180.0;
double a = std::pow(std::sin(dLat / 2), 2) +
std::pow(std::sin(dLon / 2), 2) * std::cos(lat1) * std::cos(lat2);
double c = 2 * std::asin(std::sqrt(a));
return R * c;
}
// --- Data Structures ---
struct Airport {
std::string id;
std::string name;
std::string city;
std::string country;
std::string iata;
std::string icao;
double latitude;
double longitude;
std::string altitude;
std::string timezone;
std::string dst;
std::string tz_database;
std::string type;
std::string source;
};
struct Airline {
std::string id;
std::string name;
std::string alias;
std::string iata;
std::string icao;
std::string callsign;
std::string country;
std::string active;
};
struct Route {
std::string airline_iata;
std::string source_iata;
std::string dest_iata;
};
struct OneHopResult {
std::string iata;
std::string name;
double total_distance;
int total_routes;
double lat;
double lon;
};
// --- In-Memory Data Collections ---
std::unordered_map<std::string, Airport> airportsMap;
std::unordered_map<std::string, Airline> airlinesMap;
std::vector<Route> routesList;
std::unordered_map<std::string, int> airportRouteCounts; // Cache for performance
// --- Global Mutex for Thread Safety ---
std::mutex data_mutex;
// --- Helper Functions ---
std::vector<std::string> parseCSVLine(const std::string& line) {
std::vector<std::string> result;
std::string current = "";
bool in_quotes = false;
for (char c : line) {
if (c == '"') {
in_quotes = !in_quotes;
} else if (c == ',' && !in_quotes) {
result.push_back(current);
current = "";
} else {
current += c;
}
}
result.push_back(current);
return result;
}
std::string escapeHTML(const std::string& data) {
std::string buffer;
buffer.reserve(data.size());
for(size_t pos = 0; pos != data.size(); ++pos) {
switch(data[pos]) {
case '&': buffer.append("&"); break;
case '\"': buffer.append("""); break;
case '\'': buffer.append("'"); break;
case '<': buffer.append("<"); break;
case '>': buffer.append(">"); break;
default: buffer.append(&data[pos], 1); break;
}
}
return buffer;
}
// URL Decoder for robust parsing of application/x-www-form-urlencoded payloads
std::string urlDecode(const std::string& str) {
std::string ret;
char ch;
int ii;
size_t len = str.length();
for (size_t i = 0; i < len; i++) {
if (str[i] == '+') {
ret += ' ';
} else if (str[i] == '%' && i + 2 < len) {
sscanf(str.substr(i + 1, 2).c_str(), "%x", &ii);
ch = static_cast<char>(ii);
ret += ch;
i += 2;
} else {
ret += str[i];
}
}
return ret;
}
std::string getLayout(const std::string& title, const std::string& content) {
std::string html = "<!DOCTYPE html><html lang='en'><head>";
html += "<meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'>";
html += "<title>" + title + "</title>";
// Inter Font
html += "<link rel='preconnect' href='https://fonts.googleapis.com'>";
html += "<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>";
html += "<link href='https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap' rel='stylesheet'>";
// Bootstrap & Leaflet
html += "<link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css' rel='stylesheet'>";
html += "<link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' />";
html += "<script src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'></script>";
// --- 1. GLOBAL CSS UPGRADES: GLASSMORPHISM & ANIMATED BACKGROUND ---
html += "<style>";
html += ":root { --glass-bg: rgba(255, 255, 255, 0.5); --glass-border: rgba(255, 255, 255, 0.4); --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07); }";
// Animated Gradient Integration
html += "body { background: linear-gradient(99deg, #7b92f9 0%, #5da4d0 50%, #ffffff 100%); background-size: 180% 180%; animation: animated-gradient-wave 7.5s ease-in-out infinite; color: #1e293b; font-family: 'Inter', sans-serif; margin: 0; min-height: 100vh; }";
html += "@keyframes animated-gradient-wave { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }";
// Glass Panel Utility
// Added position and base z-index to establish a baseline for the cards
html += ".glass-panel { position: relative; z-index: 1; background: var(--glass-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--glass-border); border-radius: 16px; box-shadow: var(--glass-shadow); transition: transform 0.3s ease, box-shadow 0.3s ease, z-index 0s; }";
// Elevates the entire card's stacking context when a user clicks into an input
html += ".glass-panel:focus-within { z-index: 100; }";
html += "a.glass-panel:hover, .card.glass-panel:hover { transform: translateY(-4px); box-shadow: 0 12px 40px 0 rgba(31, 38, 135, 0.1); }";
// Floating Pill Navbar
html += ".custom-navbar { position: fixed; top: 24px; left: 50%; transform: translateX(-50%); width: 95%; max-width: 1200px; z-index: 1000; display: flex; justify-content: space-between; align-items: center; padding: 0.8rem 2rem; border-radius: 50px; background: rgba(255, 255, 255, 0.65); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.7); box-shadow: 0 10px 30px rgba(0,0,0,0.06); }";
html += ".brand-logo { font-size: 1.25rem; font-weight: 700; color: #0f172a; text-decoration: none; display: flex; align-items: center; gap: 10px; letter-spacing: -0.5px; }";
html += ".brand-logo:hover { color: #3b82f6; }";
// Navbar Links Container & Center-Out Underline Animation
html += ".navbar-links { display: flex; align-items: center; gap: 2rem; height: 100%; }";
html += ".navbar-links > a, .dropbtn { position: relative; color: #475569; text-decoration: none; font-weight: 500; font-size: 0.95rem; cursor: pointer; transition: color 0.2s ease; border: none; background: none; padding: 5px 0; display: flex; align-items: center; gap: 4px; }";
html += ".navbar-links > a:hover, .dropbtn:hover { color: #0f172a; }";
// Added .dropbtn::after to apply the underline element to the button
html += ".navbar-links > a::after, .dropbtn::after { content: ''; position: absolute; bottom: -2px; left: 10%; width: 80%; height: 2px; background-color: #3b82f6; transform: scaleX(0); transform-origin: center; transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); }";
// Triggers the underline on hover, AND keeps it active while the dropdown is open
html += ".navbar-links > a:hover::after, .dropbtn:hover::after, .nav-dropdown.open .dropbtn::after { transform: scaleX(1); }";
// Pure CSS Dropdown Menus
// --- Pure CSS Dropdown Menus (Upgraded: Click + Grid Animation) ---
html += ".nav-dropdown { position: relative; display: inline-block; }";
// Smooth SVG Arrow rotation
html += ".dropdown-arrow { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); }";
html += ".nav-dropdown.open .dropdown-arrow { transform: rotate(180deg); }";
// Outer absolute container handles visibility, opacity, and positioning
html += ".dropdown-menu-container { position: absolute; top: calc(100% + 15px); left: 50%; transform: translateX(-50%); z-index: 1001; min-width: 200px; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.3s ease, visibility 0.3s ease; }";
html += ".nav-dropdown.open .dropdown-menu-container { opacity: 1; visibility: visible; pointer-events: auto; }";
// Triangles/Pointers placed on the outer container
html += ".dropdown-menu-container::after { content: ''; position: absolute; top: -15px; left: 0; width: 100%; height: 15px; background: transparent; }";
html += ".dropdown-menu-container::before { content: ''; position: absolute; top: -6px; left: 50%; transform: translateX(-50%); border-width: 0 6px 6px 6px; border-style: solid; border-color: transparent transparent rgba(255, 255, 255, 0.85) transparent; z-index: 1; }";
// Inner grid handles the 0fr to 1fr height transition
html += ".dropdown-grid { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1); background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); box-shadow: 0 10px 25px rgba(0,0,0,0.1); border-radius: 12px; border: 1px solid var(--glass-border); }";
html += ".nav-dropdown.open .dropdown-grid { grid-template-rows: 1fr; }";
// Content wrapper needs min-height: 0 and overflow: hidden for the grid trick to work
html += ".dropdown-content { overflow: hidden; min-height: 0; }";
html += ".dropdown-inner { display: flex; flex-direction: column; }";
// Link styling
html += ".dropdown-inner a { color: #1e293b; padding: 12px 16px; text-decoration: none; display: block; font-size: 0.9rem; font-weight: 500; border-bottom: 1px solid rgba(0,0,0,0.03); transition: background-color 0.2s; white-space: nowrap; }";
html += ".dropdown-inner a:last-child { border-bottom: none; }";
html += ".dropdown-inner a:hover { background-color: rgba(0,0,0,0.03); color: #3b82f6; }";
// Content offset for floating nav
html += ".main-content { margin-top: 120px; padding-bottom: 60px; }";
// Button Styling
html += ".btn-glass { background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(0, 0, 0, 0.05); color: #0f172a; font-weight: 600; backdrop-filter: blur(4px); box-shadow: 0 2px 4px rgba(0,0,0,0.02); transition: all 0.2s; border-radius: 10px; }";
html += ".btn-glass:hover { background: rgba(255, 255, 255, 0.9); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.05); }";
html += ".btn-glass-primary { background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); color: #2563eb; }";
html += ".btn-glass-primary:hover { background: rgba(59, 130, 246, 0.2); color: #1d4ed8; }";
// Glass Tables
html += ".glass-table { width: 100%; border-collapse: separate; border-spacing: 0; }";
html += ".glass-table th { background: rgba(255,255,255,0.4); color: #475569; font-weight: 600; padding: 16px; text-align: left; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--glass-border); }";
html += ".glass-table td { padding: 16px; border-bottom: 1px solid rgba(255,255,255,0.3); color: #1e293b; background: rgba(255,255,255,0.15); transition: background 0.2s; }";
html += ".glass-table tr:hover td { background: rgba(255,255,255,0.4); }";
html += ".glass-table tr:first-child th:first-child { border-top-left-radius: 16px; }";
html += ".glass-table tr:first-child th:last-child { border-top-right-radius: 16px; }";
html += ".glass-table tr:last-child td:first-child { border-bottom-left-radius: 16px; border-bottom: none; }";
html += ".glass-table tr:last-child td:last-child { border-bottom-right-radius: 16px; border-bottom: none; }";
// Form Inputs
html += ".glass-input { background: rgba(255, 255, 255, 0.5) !important; border: 1px solid var(--glass-border) !important; border-radius: 10px !important; color: #1e293b !important; padding: 0.6rem 1rem !important; backdrop-filter: blur(8px); transition: all 0.3s ease !important; }";
html += ".glass-input:focus { background: rgba(255, 255, 255, 0.8) !important; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15) !important; border-color: rgba(59, 130, 246, 0.4) !important; outline: none !important; }";
html += ".fade-in { animation: fadeIn 0.6s ease-out forwards; }";
html += "@keyframes fadeIn { 0% { opacity: 0; transform: translateY(15px); } 100% { opacity: 1; transform: translateY(0); } }";
// SVG utility
html += ".svg-icon { width: 18px; height: 18px; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; fill: none; display: inline-block; vertical-align: middle; }";
// --- TYPEAHEAD DROPDOWN CSS ---
html += ".typeahead-dropdown { position: absolute; top: 100%; left: 0; right: 0; z-index: 1050; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--glass-border); border-radius: 10px; box-shadow: var(--glass-shadow); max-height: 250px; overflow-y: auto; margin-top: 4px; display: none; scrollbar-width: none; -ms-overflow-style: none; }";
html += ".typeahead-dropdown::-webkit-scrollbar { display: none; }";
html += ".typeahead-item { padding: 10px 15px; cursor: pointer; border-bottom: 1px solid rgba(0,0,0,0.05); color: #1e293b; font-size: 0.9rem; transition: background 0.2s ease, color 0.2s ease; display: flex; align-items: center; }";
html += ".typeahead-item:last-child { border-bottom: none; }";
html += ".typeahead-item:hover { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }";
html += ".typeahead-iata { font-weight: 700; margin-right: 10px; color: #3b82f6; min-width: 45px; }";
html += "</style>";
html += "</head><body>";
// Hidden Audio Element for click sound
html += "<audio id='clickSound' src='/click.mp3' preload='auto'></audio>";
html += "<audio id='click2Sound' src='/click2.mp3' preload='auto'></audio>";
// --- 2. UPDATED FLEXBOX NAVIGATION BAR ---
html += "<nav class='custom-navbar'>";
// Left Side: Brand Logo with SVG
html += " <div class='navbar-brand-container'>";
html += " <a href='/' class='brand-logo'>";
html += " <svg class='svg-icon' style='width: 24px; height: 24px;' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21.5 4c0 0-2 .5-3.5 2L14.5 9.5 6.3 7.7l-1.6 1.6 7.1 3.5-3.5 3.5-3.6-1.1-1.6 1.6 3.6 2.6 2.6 3.6 1.6-1.6-1.1-3.6 3.5-3.5 3.5 7.1 1.6-1.6z'/></svg>";
html += " OpenFlights";
html += " </a>";
html += " </div>";
// Center: Main Links & Dropdowns
html += " <div class='navbar-links'>";
html += " <a href='/'>Home</a>";
// Directories Dropdown
// Directories Dropdown (Click to expand)
html += " <div class='nav-dropdown' id='directoriesDropdown'>";
html += " <button class='dropbtn' id='directoriesBtn'>Directories <svg class='svg-icon dropdown-arrow' style='width: 14px; height: 14px;' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polyline points='6 9 12 15 18 9'></polyline></svg></button>";
html += " <div class='dropdown-menu-container'>";
html += " <div class='dropdown-grid'>";
html += " <div class='dropdown-content'>";
html += " <div class='dropdown-inner'>";
html += " <a href='/list/airports' class='hover-sound-target'>All Airports</a>";
html += " <a href='/list/airlines' class='hover-sound-target'>All Airlines</a>";
html += " </div>";
html += " </div>";
html += " </div>";
html += " </div>";
html += " </div>";
// Flattened Manage Data Link
html += " <a href='/admin' class='hover-sound-target'>Manage Data</a>";
html += " <a href='/about'>About</a>";
html += " <a href='/get_code'>Source Code</a>";
html += " </div>";
// Right Side: Audio Toggle Button (SVG Icons)
html += " <div class='navbar-actions'>";
html += " <button id='audioToggleBtn' class='btn btn-glass btn-sm d-flex align-items-center gap-2'>";
html += " <svg id='volOffIcon' class='svg-icon' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polygon points='11 5 6 9 2 9 2 15 6 15 11 19 11 5'></polygon><line x1='23' y1='9' x2='17' y2='15'></line><line x1='17' y1='9' x2='23' y2='15'></line></svg>";
html += " <svg id='volOnIcon' class='svg-icon d-none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polygon points='11 5 6 9 2 9 2 15 6 15 11 19 11 5'></polygon><path d='M15.54 8.46a5 5 0 0 1 0 7.07'></path><path d='M19.07 4.93a10 10 0 0 1 0 14.14'></path></svg>";
html += " <span id='volText'>Sound</span>";
html += " </button>";
html += " </div>";
html += "</nav>";
// Content container
html += "<div class='container main-content fade-in'>" + content + "</div>";
// --- 3. JAVASCRIPT AUDIO ENGINE ---
html += "<script>";
html += "document.addEventListener('DOMContentLoaded', function() {";
html += " const audioToggleBtn = document.getElementById('audioToggleBtn');";
html += " const clickSound = document.getElementById('clickSound');";
html += " const volOffIcon = document.getElementById('volOffIcon');";
html += " const volOnIcon = document.getElementById('volOnIcon');";
html += " let isMuted = localStorage.getItem('isMuted') === 'false' ? false : true;";
html += " function updateAudioUI() {";
html += " if (isMuted) {";
html += " volOffIcon.classList.remove('d-none');";
html += " volOnIcon.classList.add('d-none');";
html += " audioToggleBtn.classList.remove('btn-glass-primary');";
html += " } else {";
html += " volOffIcon.classList.add('d-none');";
html += " volOnIcon.classList.remove('d-none');";
html += " audioToggleBtn.classList.add('btn-glass-primary');";
html += " }";
html += " }";
html += " updateAudioUI();";
// Dropdown Click & Outside-Click Logic
html += " const directoriesDropdown = document.getElementById('directoriesDropdown');";
html += " const directoriesBtn = document.getElementById('directoriesBtn');";
html += " if (directoriesBtn && directoriesDropdown) {";
html += " directoriesBtn.addEventListener('click', function(e) {";
html += " e.preventDefault();"; // Stop jump to top if needed
html += " e.stopPropagation();"; // Prevent document click from immediately closing it
html += " directoriesDropdown.classList.toggle('open');";
html += " });";
html += " document.addEventListener('click', function(e) {";
html += " if (!directoriesDropdown.contains(e.target)) {";
html += " directoriesDropdown.classList.remove('open');";
html += " }";
html += " });";
html += " }";
html += " audioToggleBtn.addEventListener('click', function() {";
html += " isMuted = !isMuted;";
html += " localStorage.setItem('isMuted', isMuted);";
html += " updateAudioUI();";
html += " });";
html += " const click2Sound = document.getElementById('click2Sound');";
html += " const interactiveElements = document.querySelectorAll('button, a');";
html += " interactiveElements.forEach(el => {";
html += " el.addEventListener('click', function(e) {";
html += " if (!isMuted && el.id !== 'audioToggleBtn') {";
html += " let isSearchBtn = el.classList.contains('search-action-btn');";
html += " let activeSound = isSearchBtn ? click2Sound : clickSound;";
html += " let inactiveSound = isSearchBtn ? clickSound : click2Sound;";
html += " if (inactiveSound) inactiveSound.pause();";
html += " if (activeSound) {";
html += " activeSound.currentTime = 0;";
html += " activeSound.play().catch(err => console.log('Audio prevented', err));";
html += " }";
html += " if (el.tagName === 'A' && el.href && !el.hasAttribute('target')) {";
html += " e.preventDefault();";
html += " setTimeout(function() { window.location.href = el.href; }, 400);";
html += " }";
html += " }";
html += " });";
html += " });";
// --- NEW: Safely delay valid forms so audio can finish ---
html += " document.querySelectorAll('form').forEach(form => {";
html += " form.addEventListener('submit', function(e) {";
html += " if (!isMuted) {";
html += " e.preventDefault();";
html += " setTimeout(function() { form.submit(); }, 400);";
html += " }";
html += " });";
html += " });";
html += "});";
html += "</script>";
html += "</body></html>";
return html;
}
// --- Data Loading Functions ---
void loadAirports(const std::string& filename) {
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
std::vector<std::string> fields = parseCSVLine(line);
if (fields.size() >= 14) {
Airport a;
a.id = fields[0];
a.name = fields[1];
a.city = fields[2];
a.country = fields[3];
a.iata = fields[4];
a.icao = fields[5];
if (a.iata.length() == 3) {
try {
a.latitude = std::stod(fields[6]);
a.longitude = std::stod(fields[7]);
a.altitude = fields[8];
a.timezone = fields[9];
a.dst = fields[10];
a.tz_database = fields[11];
a.type = fields[12];
a.source = fields[13];
airportsMap[a.iata] = a;
} catch (...) {}
}
}
}
}
void loadAirlines(const std::string& filename) {
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
std::vector<std::string> fields = parseCSVLine(line);
if (fields.size() >= 8) {
Airline a;
a.id = fields[0];
a.name = fields[1];
a.alias = fields[2];
a.iata = fields[3];
a.icao = fields[4];
a.callsign = fields[5];
a.country = fields[6];
a.active = fields[7];
if (a.iata.length() == 2) airlinesMap[a.iata] = a;
}
}
}
void loadRoutes(const std::string& filename) {
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
std::vector<std::string> fields = parseCSVLine(line);
if (fields.size() > 5) {
Route r;
r.airline_iata = fields[0];
r.source_iata = fields[2];
r.dest_iata = fields[4];
routesList.push_back(r);
airportRouteCounts[r.source_iata]++;
airportRouteCounts[r.dest_iata]++;
}
}
}
int main() {
std::cout << "Loading data files..." << std::endl;
loadAirports("airports.dat.txt");
loadAirlines("airlines.dat.txt");
loadRoutes("routes.dat.txt");
std::cout << "Data loaded successfully!" << std::endl;
crow::SimpleApp app;
CROW_ROUTE(app, "/about")([](){
std::string content = "";
// --- Custom CSS for the About Cards ---
content += "<style>";
// Bouncy lift effect for the whole card
content += ".profile-card { transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; cursor: default; }";
content += ".profile-card:hover { transform: translateY(-10px) !important; box-shadow: 0 20px 40px rgba(31, 38, 135, 0.12) !important; }";
// Image scale and slight tilt on hover
content += ".profile-img { transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); }";
content += ".profile-card:hover .profile-img { transform: scale(1.08) rotate(3deg); }";
// Sleek Glass Pill Badge for the CWID
content += ".cwid-badge { display: inline-block; background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.25); color: #2563eb; padding: 0.35rem 0.85rem; border-radius: 50px; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.5px; margin-top: 0.5rem; backdrop-filter: blur(4px); transition: all 0.3s ease; }";
content += ".profile-card:hover .cwid-badge { background: rgba(59, 130, 246, 0.15); border-color: rgba(59, 130, 246, 0.4); }";
content += "</style>";
content += "<div class='text-center mb-5'><h1 class='fw-bold' style='color: #0f172a; font-size: 2.5rem; letter-spacing: -1px;'>About the Project</h1><p class='text-muted'>The architecture powering the OpenFlights Explorer.</p></div>";
content += "<div class='row justify-content-center gap-4'>";
// --- Creative Genius Card ---
content += "<div class='col-md-5'>";
// Added .profile-card class here
content += "<div class='glass-panel profile-card p-5 text-center h-100'>";
content += "<h3 class='fw-bold mb-4'>Creative Genius</h3>";
// Added .profile-img class here
content += "<img src='profile.jpg' alt='Henry Stallard' class='img-fluid rounded-circle profile-img mb-4 mx-auto d-block shadow-sm' style='width: 160px; height: 160px; object-fit: cover; border: 4px solid rgba(255,255,255,0.8);'>";
content += "<h4 class='fw-bold mb-1'>Henry Stallard</h4>";
content += "<p style='color: #3b82f6; font-weight: 500; margin-bottom: 0;'>Lead Developer</p>";
// Injected the new CWID Badge
content += "<div class='cwid-badge'>CWID: 20668498</div>";
content += "<p class='text-muted mt-4'>Genius 17 year old dev</p>";
content += "</div></div>";
// --- Mascot Card ---
content += "<div class='col-md-5'>";
// Added .profile-card class here
content += "<div class='glass-panel profile-card p-5 text-center h-100'>";
content += "<h3 class='fw-bold mb-4'>Database Mascot</h3>";
// Added .profile-img class here
content += "<img src='mascot.jpg' alt='Avi the Eagle' class='img-fluid rounded-circle profile-img mb-4 mx-auto d-block shadow-sm' style='width: 160px; height: 160px; object-fit: cover; border: 4px solid rgba(255,255,255,0.8);'>";
content += "<h4 class='fw-bold mb-1'>Avi the Eagle</h4>";
content += "<p style='color: #3b82f6; font-weight: 500;'>Chief of Routes</p>";
// Added a spacer div to align the text vertically with the left card
content += "<div style='height: 32px;'></div>";
content += "<p class='text-muted mt-4'>Scours the skies and makes sure data is accurate. </p>";
content += "</div></div>";
content += "</div>";
return getLayout("About - Flight Explorer", content);
});
CROW_ROUTE(app, "/")([](){
std::string content = "";
// Premium Glass Input Group CSS (Caret Offset Resolved)
content += "<style>";
content += ".glass-input-group { display: flex; align-items: stretch; background: rgba(255, 255, 255, 0.4); border: 1px solid var(--glass-border); border-radius: 12px; transition: all 0.3s; }";
content += ".glass-input-group:focus-within { background: rgba(255, 255, 255, 0.8); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); border-color: rgba(59, 130, 246, 0.4); position: relative; z-index: 50; }";
content += ".glass-input-wrapper { display: flex; align-items: center; flex: 1; padding: 0.5rem 1rem; position: relative; }";
content += ".glass-prefix { font-weight: 600; color: #475569; margin-right: 0.5rem; }";
content += ".input-stack { position: relative; display: flex; flex: 1; align-items: center; }";
// Fixed Padding and Caret Alignment
content += ".borderless-glass-input { border: none !important; box-shadow: none !important; outline: none !important; width: 100%; background: transparent; color: #0f172a; font-weight: 500; font-size: 1rem; padding-left: 0.5rem !important; }";
content += ".glass-divider { width: 1px; background: var(--glass-border); }";
// Fixed Placeholder positioning and dynamic visibility
content += ".faux-placeholder { position: absolute; left: 0.5rem; pointer-events: none; color: #94a3b8; transition: opacity 0.2s ease, visibility 0.2s ease; font-weight: 500; }";
content += ".borderless-glass-input:focus ~ .faux-placeholder, .borderless-glass-input:not(:placeholder-shown) ~ .faux-placeholder { opacity: 0; visibility: hidden; }";
content += ".faux-fade-out { opacity: 0; }";
content += "</style>";
content += "<div class='text-center mb-5'><h1 class='fw-bold' style='color: #0f172a; font-size: 2.5rem; letter-spacing: -1px;'>Flight Database</h1><p class='text-muted'>Query the global network of airports, airlines, and routes.</p></div>";
content += "<div class='row g-4'>";
// Direct Flight Finder Card (Removed required, maintained novalidate & placeholder=' ')
content += "<div class='col-md-6'><div class='glass-panel p-4 h-100'>";
content += "<div class='d-flex align-items-center gap-2 mb-3'><svg class='svg-icon' style='color:#10b981;' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M2 12h20'/><path d='m15 5 7 7-7 7'/></svg><h5 class='fw-bold mb-0' style='color: #0f172a;'>Direct Flight Finder</h5></div>";
content += "<form action='/direct_flight' method='GET' novalidate>";
content += "<div class='glass-input-group mb-3'>";
content += " <div class='glass-input-wrapper'>";
content += " <span class='glass-prefix'>From</span>";
content += " <div class='input-stack'>";
content += " <input type='text' name='source' class='borderless-glass-input anim-input text-uppercase' placeholder=' ' autocomplete='off'>";
content += " <span class='faux-placeholder airport-src'>SFO</span>";
content += " </div>";
content += " </div>";
content += " <div class='glass-divider'></div>";
content += " <div class='glass-input-wrapper'>";
content += " <span class='glass-prefix'>To</span>";
content += " <div class='input-stack'>";
content += " <input type='text' name='dest' class='borderless-glass-input anim-input text-uppercase' placeholder=' ' autocomplete='off'>";
content += " <span class='faux-placeholder airport-dst'>JFK</span>";
content += " </div>";
content += " </div>";
content += "</div>";
content += "<button class='btn btn-glass w-100 py-2 search-action-btn' type='submit'>Find Direct Flight</button>";
content += "</form></div></div>";
// One-Hop Route Finder Card
content += "<div class='col-md-6'><div class='glass-panel p-4 h-100'>";
content += "<div class='d-flex align-items-center gap-2 mb-3'><svg class='svg-icon' style='color:#3b82f6;' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polyline points='9 18 15 12 9 6'/></svg><h5 class='fw-bold mb-0' style='color: #0f172a;'>One-Hop Route Finder</h5></div>";
content += "<form action='/one_hop' method='GET' novalidate>";
content += "<div class='glass-input-group mb-3'>";
content += " <div class='glass-input-wrapper'>";
content += " <span class='glass-prefix'>From</span>";
content += " <div class='input-stack'>";
content += " <input type='text' name='source' class='borderless-glass-input anim-input text-uppercase' placeholder=' ' autocomplete='off'>";
content += " <span class='faux-placeholder airport-src'>SFO</span>";
content += " </div>";
content += " </div>";
content += " <div class='glass-divider'></div>";
content += " <div class='glass-input-wrapper'>";
content += " <span class='glass-prefix'>To</span>";
content += " <div class='input-stack'>";
content += " <input type='text' name='dest' class='borderless-glass-input anim-input text-uppercase' placeholder=' ' autocomplete='off'>";
content += " <span class='faux-placeholder airport-dst'>JFK</span>";
content += " </div>";
content += " </div>";
content += "</div>";
content += "<button class='btn btn-glass w-100 py-2 search-action-btn' type='submit'>Find Route</button>";
content += "</form></div></div>";
// Search Airline Card
content += "<div class='col-md-6'><div class='glass-panel p-4 h-100'>";
content += "<div class='d-flex align-items-center gap-2 mb-3'><svg class='svg-icon' style='color:#6366f1;' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21.5 4c0 0-2 .5-3.5 2L14.5 9.5 6.3 7.7l-1.6 1.6 7.1 3.5-3.5 3.5-3.6-1.1-1.6 1.6 3.6 2.6 2.6 3.6 1.6-1.6-1.1-3.6 3.5-3.5 3.5 7.1 1.6-1.6z'/></svg><h5 class='fw-bold mb-0' style='color: #0f172a;'>Search Airline</h5></div>";
content += "<form action='/search/airline' method='GET' novalidate><div class='glass-input-group mb-3'>";
content += " <div class='glass-input-wrapper'>";
content += " <span class='glass-prefix'>IATA</span>";
content += " <div class='input-stack'>";
content += " <input type='text' name='iata' class='borderless-glass-input anim-input text-uppercase' placeholder=' ' autocomplete='off'>";
content += " <span class='faux-placeholder airline-single'>AA</span>";
content += " </div>";
content += " </div>";
content += "</div>";
content += "<button class='btn btn-glass w-100 py-2 search-action-btn' type='submit'>Search</button>";
content += "</form></div></div>";
// Search Airport Card
content += "<div class='col-md-6'><div class='glass-panel p-4 h-100'>";
content += "<div class='d-flex align-items-center gap-2 mb-3'><svg class='svg-icon' style='color:#ec4899;' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'></path><circle cx='12' cy='10' r='3'></circle></svg><h5 class='fw-bold mb-0' style='color: #0f172a;'>Search Airport</h5></div>";
content += "<form action='/search/airport' method='GET' novalidate><div class='glass-input-group mb-3'>";
content += " <div class='glass-input-wrapper'>";
content += " <span class='glass-prefix'>IATA</span>";
content += " <div class='input-stack'>";
content += " <input type='text' name='iata' class='borderless-glass-input anim-input text-uppercase' placeholder=' ' autocomplete='off'>";
content += " <span class='faux-placeholder airport-single'>SFO</span>";
content += " </div>";
content += " </div>";
content += "</div>";
content += "<button class='btn btn-glass w-100 py-2 search-action-btn' type='submit'>Search</button>";
content += "</form></div></div>";
content += "</div>"; // End row
// JavaScript Engine for Placeholder Animation
content += "<script>";
content += "document.addEventListener('DOMContentLoaded', () => {";
content += " const airports = ['SFO', 'JFK', 'LHR', 'HND', 'DXB'];";
content += " const airlines = ['AA', 'DL', 'UA', 'BA', 'JL'];";
content += " let idx = 0;";
content += " setInterval(() => {";
content += " const placeholders = document.querySelectorAll('.faux-placeholder');";
content += " placeholders.forEach(p => p.classList.add('faux-fade-out'));";
content += " setTimeout(() => {";
content += " idx = (idx + 1) % airports.length;";
content += " const dstIdx = (idx + 1) % airports.length;";
content += " document.querySelectorAll('.airport-src').forEach(el => el.textContent = airports[idx]);";
content += " document.querySelectorAll('.airport-dst').forEach(el => el.textContent = airports[dstIdx]);";
content += " document.querySelectorAll('.airport-single').forEach(el => el.textContent = airports[idx]);";
content += " document.querySelectorAll('.airline-single').forEach(el => el.textContent = airlines[idx]);";
content += " placeholders.forEach(p => p.classList.remove('faux-fade-out'));";
content += " }, 400);";
content += " }, 2000);";
content += "});";
content += "</script>";
// --- TYPEAHEAD JAVASCRIPT ---
content += "<script>";
content += "document.addEventListener('DOMContentLoaded', () => {";
content += " const debounce = (func, delay) => {";
content += " let timeoutId;";
content += " return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), delay); };";
content += " };";
content += " const setupTypeahead = (input, type) => {";
content += " const stack = input.closest('.input-stack');";
content += " let dropdown = document.createElement('div');";
content += " dropdown.className = 'typeahead-dropdown';";
content += " stack.appendChild(dropdown);";
content += " input.addEventListener('input', debounce(async (e) => {";
content += " const query = e.target.value.trim();";
content += " if (query.length < 1) { dropdown.style.display = 'none'; return; }";
content += " const endpoint = type === 'airport' ? '/api/search/airports' : '/api/search/airlines';";
content += " try {";
content += " const res = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`);";
content += " if (!res.ok) throw new Error('Network error');";
content += " const data = await res.json();";
content += " dropdown.innerHTML = '';";
content += " if (data.length > 0) {";
content += " data.forEach(item => {";
content += " const div = document.createElement('div');";
content += " div.className = 'typeahead-item';";
content += " div.innerHTML = `<span class=\"typeahead-iata\">${item.iata}</span><span class=\"text-truncate\">${item.name}</span>`;";
content += " div.addEventListener('mousedown', (ev) => { ev.preventDefault(); input.value = item.iata; dropdown.style.display = 'none'; });";
content += " dropdown.appendChild(div);";
content += " });";
content += " dropdown.style.display = 'block';";
content += " } else { dropdown.style.display = 'none'; }";
content += " } catch (err) { console.error('Typeahead error:', err); }";
content += " }, 300));";
content += " input.addEventListener('blur', () => dropdown.style.display = 'none');";
content += " input.addEventListener('focus', () => { if (input.value.trim().length > 0 && dropdown.children.length > 0) dropdown.style.display = 'block'; });";
content += " };";
content += " document.querySelectorAll('input[name=\"source\"], input[name=\"dest\"], form[action=\"/search/airport\"] input[name=\"iata\"]').forEach(el => setupTypeahead(el, 'airport'));";
content += " document.querySelectorAll('form[action=\"/search/airline\"] input[name=\"iata\"]').forEach(el => setupTypeahead(el, 'airline'));";
content += "});";
content += "</script>";
return getLayout("Home - Flight Explorer", content);
});
CROW_ROUTE(app, "/direct_flight")([](const crow::request& req) -> crow::response {
char* src_param = req.url_params.get("source");
char* dst_param = req.url_params.get("dest");
if (!src_param || !dst_param) return crow::response(204);
std::string src(src_param);
std::string dst(dst_param);
if (src.empty() || dst.empty()) return crow::response(204);
std::transform(src.begin(), src.end(), src.begin(), ::toupper);
std::transform(dst.begin(), dst.end(), dst.begin(), ::toupper);
std::lock_guard<std::mutex> lock(data_mutex);
if (!airportsMap.count(src) || !airportsMap.count(dst)) {
return crow::response(getLayout("Not Found", "<div class='glass-panel p-4 text-warning'>One or both airports were not found in our database.</div>"));
}
Airport s_airport = airportsMap[src];
Airport d_airport = airportsMap[dst];
std::vector<Airline> directAirlines;
std::unordered_set<std::string> seenAirlines;
for (const auto& r : routesList) {
if (r.source_iata == src && r.dest_iata == dst) {
if (seenAirlines.count(r.airline_iata) == 0 && airlinesMap.count(r.airline_iata)) {
seenAirlines.insert(r.airline_iata);
directAirlines.push_back(airlinesMap[r.airline_iata]);
}
}
}
if (directAirlines.empty()) {
std::string noFlightMsg = "<div class='glass-panel p-4 text-info'>No direct flights found between " + src + " and " + dst + ". Try using the One-Hop Route Finder instead!</div>";
return crow::response(getLayout("No Direct Flights", noFlightMsg));
}
std::string content = "<h2 class='fw-bold mb-4'>Direct Flights: " + src + " <svg class='svg-icon' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><line x1='5' y1='12' x2='19' y2='12'></line><polyline points='12 5 19 12 12 19'></polyline></svg> " + dst + "</h2>";
content += "<div class='glass-panel p-2 mb-5'><div id='directMap' style='height: 450px; border-radius: 12px; z-index: 1;'></div></div>";
std::string safeSrcName = s_airport.name;
std::string safeDstName = d_airport.name;
size_t pos = 0;
while ((pos = safeSrcName.find("'", pos)) != std::string::npos) { safeSrcName.replace(pos, 1, "\\'"); pos += 2; }
pos = 0;
while ((pos = safeDstName.find("'", pos)) != std::string::npos) { safeDstName.replace(pos, 1, "\\'"); pos += 2; }
content += "<script>\n";
content += "var srcLat = " + std::to_string(s_airport.latitude) + ";\n";
content += "var srcLon = " + std::to_string(s_airport.longitude) + ";\n";
content += "var dstLat = " + std::to_string(d_airport.latitude) + ";\n";
content += "var dstLon = " + std::to_string(d_airport.longitude) + ";\n";
content += "var map = L.map('directMap').setView([srcLat, srcLon], 4);\n";
content += "L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, attribution: '© OpenStreetMap © CARTO' }).addTo(map);\n\n";
content += "L.marker([srcLat, srcLon]).addTo(map).bindPopup('<b>Source: " + src + "</b><br>" + safeSrcName + "').openPopup();\n";
content += "L.marker([dstLat, dstLon]).addTo(map).bindPopup('<b>Dest: " + dst + "</b><br>" + safeDstName + "');\n\n";
content += "L.polyline([\n";
content += " [srcLat, srcLon],\n";
content += " [dstLat, dstLon]\n";
content += "], {color: '#3b82f6', weight: 4, opacity: 0.8, dashArray: '5, 10'}).addTo(map);\n";
content += "var bounds = new L.LatLngBounds([[srcLat, srcLon], [dstLat, dstLon]]);\n";
content += "map.fitBounds(bounds, {padding: [50, 50]});\n";
content += "</script>\n";
content += "<div class='glass-panel p-0 overflow-hidden'>";
content += "<table class='glass-table'>";
content += "<thead><tr><th>Airline IATA</th><th>Airline Name</th><th>Country</th></tr></thead><tbody>";
for (const auto& al : directAirlines) {
content += "<tr><td>" + al.iata + "</td><td class='fw-bold'>" + escapeHTML(al.name) + "</td><td>" + escapeHTML(al.country) + "</td></tr>";
}
content += "</tbody></table></div>";
return crow::response(getLayout("Direct Flight Results", content));
});
CROW_ROUTE(app, "/one_hop")([](const crow::request& req) -> crow::response {
char* src_param = req.url_params.get("source");
char* dst_param = req.url_params.get("dest");
if (!src_param || !dst_param) return crow::response(204);
std::string src(src_param);
std::string dst(dst_param);
if (src.empty() || dst.empty()) return crow::response(204);
std::transform(src.begin(), src.end(), src.begin(), ::toupper);
std::transform(dst.begin(), dst.end(), dst.begin(), ::toupper);
std::lock_guard<std::mutex> lock(data_mutex);
if (!airportsMap.count(src) || !airportsMap.count(dst)) {
return crow::response(getLayout("Not Found", "<div class='glass-panel p-4 text-warning'>One or both airports not found.</div>"));
}
Airport s_airport = airportsMap[src];
Airport d_airport = airportsMap[dst];
std::unordered_set<std::string> accessible_from_src;
for (const auto& r : routesList) {
if (r.source_iata == src) accessible_from_src.insert(r.dest_iata);
}
std::vector<OneHopResult> results;
std::unordered_set<std::string> valid_intermediates;
for (const auto& r : routesList) {
if (r.dest_iata == dst && accessible_from_src.count(r.source_iata)) {
std::string x_iata = r.source_iata;
if (valid_intermediates.count(x_iata) == 0 && airportsMap.count(x_iata)) {
valid_intermediates.insert(x_iata);
Airport x_airport = airportsMap[x_iata];
double dist1 = haversine(s_airport.latitude, s_airport.longitude, x_airport.latitude, x_airport.longitude);
double dist2 = haversine(x_airport.latitude, x_airport.longitude, d_airport.latitude, d_airport.longitude);
results.push_back({x_iata, x_airport.name, dist1 + dist2, airportRouteCounts[x_iata], x_airport.latitude, x_airport.longitude});
}
}
}
if (results.empty()) {
return crow::response(getLayout("No Routes", "<div class='glass-panel p-4 text-info'>No one-hop routes found between " + src + " and " + dst + ".</div>"));
}
std::sort(results.begin(), results.end(), [](const OneHopResult& a, const OneHopResult& b) {
return a.total_distance < b.total_distance;
});
std::string content = "<h2 class='fw-bold mb-4'>One-Hop Routes: " + src + " <svg class='svg-icon' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><line x1='5' y1='12' x2='19' y2='12'></line><polyline points='12 5 19 12 12 19'></polyline></svg> " + dst + "</h2>";
content += "<div class='glass-panel p-2 mb-5'><div id='map' style='height: 450px; border-radius: 12px; z-index: 1;'></div></div>";
content += "<script>\n";
content += "var map = L.map('map').setView([" + std::to_string(s_airport.latitude) + ", " + std::to_string(s_airport.longitude) + "], 4);\n";
content += "L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, attribution: '© OpenStreetMap © CARTO' }).addTo(map);\n\n";
content += "var routeData = {\n";
content += " source: { lat: " + std::to_string(s_airport.latitude) + ", lon: " + std::to_string(s_airport.longitude) + ", iata: '" + src + "' },\n";
content += " dest: { lat: " + std::to_string(d_airport.latitude) + ", lon: " + std::to_string(d_airport.longitude) + ", iata: '" + dst + "' },\n";
content += " hops: [\n";
for(size_t i = 0; i < results.size(); i++) {
auto x = results[i];
std::string safeName = x.name;
size_t pos = 0;
while ((pos = safeName.find("'", pos)) != std::string::npos) { safeName.replace(pos, 1, "\\'"); pos += 2; }
content += " { lat: " + std::to_string(x.lat) + ", lon: " + std::to_string(x.lon) + ", iata: '" + x.iata + "', name: '" + safeName + "' }";
if (i < results.size() - 1) content += ",\n";
else content += "\n";
}
content += " ]\n";
content += "};\n\n";
// --- Bezier Curve Generator (Fixed Float Gap) ---
content += "function getBezier(lat1, lon1, lat2, lon2, offset) {\n";
content += " var midLat = (lat1 + lat2) / 2;\n";
content += " var midLon = (lon1 + lon2) / 2;\n";
content += " var dLat = lat2 - lat1;\n";
content += " var dLon = lon2 - lon1;\n";
content += " var cpLat = midLat - (dLon * offset);\n";
content += " var cpLon = midLon + (dLat * offset);\n";
content += " var points = [];\n";
// Changed to < 1 to prevent potential duplicate end-points
content += " for (var t = 0; t < 1; t += 0.05) {\n";
content += " var lat = (1-t)*(1-t)*lat1 + 2*(1-t)*t*cpLat + t*t*lat2;\n";
content += " var lon = (1-t)*(1-t)*lon1 + 2*(1-t)*t*cpLon + t*t*lon2;\n";
content += " points.push([lat, lon]);\n";
content += " }\n";
// Force the absolute exact destination coordinate at the end of the line
content += " points.push([lat2, lon2]);\n";
content += " return points;\n";
content += "}\n\n";
content += "L.marker([routeData.source.lat, routeData.source.lon]).addTo(map).bindPopup('<b>Source: ' + routeData.source.iata + '</b>').openPopup();\n";
content += "L.marker([routeData.dest.lat, routeData.dest.lon]).addTo(map).bindPopup('<b>Dest: ' + routeData.dest.iata + '</b>');\n\n";
// --- Physics-based Easing Function ---
content += "function easeOutBounce(x) {\n";
content += " const n1 = 7.5625, d1 = 2.75;\n";
content += " if (x < 1/d1) return n1 * x * x;\n";
content += " if (x < 2/d1) return n1 * (x -= 1.5/d1) * x + 0.75;\n";
content += " if (x < 2.5/d1) return n1 * (x -= 2.25/d1) * x + 0.9375;\n";
content += " return n1 * (x -= 2.625/d1) * x + 0.984375;\n";
content += "}\n\n";
// --- Custom Line Tracing Animation ---
content += "function animateLine(line, curve, duration) {\n";
content += " let start = null;\n";
content += " function step(timestamp) {\n";
content += " if (!start) start = timestamp;\n";
content += " let progress = Math.min((timestamp - start) / duration, 1);\n";
content += " let eased = easeOutBounce(progress);\n";
content += " let targetIdx = eased * (curve.length - 1);\n";
content += " let idx = Math.floor(targetIdx);\n";
content += " let rem = targetIdx - idx;\n";
content += " let points = curve.slice(0, Math.max(0, idx + 1));\n";
content += " if (idx < curve.length - 1 && rem > 0) {\n";
content += " let p1 = curve[idx], p2 = curve[idx + 1];\n";
content += " points.push([p1[0] + (p2[0]-p1[0])*rem, p1[1] + (p2[1]-p1[1])*rem]);\n";
content += " }\n";
content += " line.setLatLngs(points);\n";
content += " if (progress < 1) requestAnimationFrame(step);\n";
content += " }\n";
content += " requestAnimationFrame(step);\n";
content += "}\n\n";
// --- State Management ---
content += "var routeLayers = {};\n";
content += "var selectedHops = new Set();\n";
content += "var bounds = new L.LatLngBounds([[routeData.source.lat, routeData.source.lon], [routeData.dest.lat, routeData.dest.lon]]);\n\n";
content += "routeData.hops.forEach(function(hop) {\n";
content += " var marker = L.circleMarker([hop.lat, hop.lon], {color: '#ec4899', radius: 5, fillOpacity: 0.6}).bindPopup('<b>' + hop.iata + '</b><br>' + hop.name);\n";
content += " var curve1 = getBezier(routeData.source.lat, routeData.source.lon, hop.lat, hop.lon, 0.15);\n";
content += " var curve2 = getBezier(hop.lat, hop.lon, routeData.dest.lat, routeData.dest.lon, 0.15);\n";
content += " var fullCurve = curve1.concat(curve2);\n"; // Combine for trace animation
content += " var line1 = L.polyline(curve1, {color: '#94a3b8', weight: 2, opacity: 0.6});\n";
content += " var line2 = L.polyline(curve2, {color: '#94a3b8', weight: 2, opacity: 0.6});\n";
content += " marker.addTo(map);\n";
content += " line1.addTo(map);\n";
content += " line2.addTo(map);\n";
content += " routeLayers[hop.iata] = { marker: marker, staticLines: [line1, line2], fullCurve: fullCurve, activeLine: null };\n";
content += " bounds.extend([hop.lat, hop.lon]);\n";
content += "});\n";
content += "map.fitBounds(bounds, {padding: [30, 30]});\n\n";
// --- Toggle Logic with Animation Triggers ---
content += "window.toggleRoute = function(iata) {\n";
content += " let isNewAddition = !selectedHops.has(iata);\n";
content += " if (isNewAddition) { selectedHops.add(iata); } else { selectedHops.delete(iata); }\n";
content += " var buttons = document.querySelectorAll('.route-toggle-btn');\n";
content += " if (selectedHops.size === 0) {\n";
content += " Object.keys(routeLayers).forEach(function(key) {\n";
content += " if (routeLayers[key].activeLine) map.removeLayer(routeLayers[key].activeLine);\n";
content += " map.addLayer(routeLayers[key].marker);\n";
content += " routeLayers[key].staticLines.forEach(l => map.addLayer(l));\n";
content += " });\n";
content += " buttons.forEach(b => { b.textContent = 'Show'; b.className = 'btn btn-glass-primary btn-sm route-toggle-btn'; b.style.cssText = ''; });\n";
content += " return;\n";
content += " }\n";
content += " Object.keys(routeLayers).forEach(function(key) {\n";
content += " routeLayers[key].staticLines.forEach(l => map.removeLayer(l));\n";
content += " if (selectedHops.has(key)) {\n";
content += " if (!map.hasLayer(routeLayers[key].marker)) map.addLayer(routeLayers[key].marker);\n";
content += " if (!routeLayers[key].activeLine) {\n";
content += " routeLayers[key].activeLine = L.polyline([], {color: '#3b82f6', weight: 3, opacity: 0.8});\n";
content += " }\n";
content += " if (!map.hasLayer(routeLayers[key].activeLine)) {\n";
content += " routeLayers[key].activeLine.addTo(map);\n";
content += " animateLine(routeLayers[key].activeLine, routeLayers[key].fullCurve, 1200);\n";
content += " }\n";
content += " } else {\n";
content += " map.removeLayer(routeLayers[key].marker);\n";
content += " if (routeLayers[key].activeLine) map.removeLayer(routeLayers[key].activeLine);\n";
content += " }\n";
content += " });\n";
content += " buttons.forEach(b => {\n";
content += " if (selectedHops.has(b.getAttribute('data-iata'))) {\n";
content += " b.textContent = 'Hide'; b.className = 'btn btn-glass btn-sm route-toggle-btn'; b.style.cssText = 'color: #ec4899; border-color: rgba(236, 72, 153, 0.4); background: rgba(236, 72, 153, 0.05);';\n";
content += " } else {\n";
content += " b.textContent = 'Show'; b.className = 'btn btn-glass-primary btn-sm route-toggle-btn'; b.style.cssText = '';\n";
content += " }\n";
content += " });\n";
content += "};\n";
content += "</script>\n";
content += "<div class='glass-panel p-0 overflow-hidden'>";
content += "<table class='glass-table'>";
content += "<thead><tr><th>Intermediate Airport (X)</th><th>Name</th><th>Total Distance (mi)</th><th>Routes at X</th><th class='text-center'>Action</th></tr></thead><tbody>";
for (const auto& r : results) {
content += "<tr><td class='fw-bold text-primary'>" + r.iata + "</td><td>" + r.name + "</td>";
char dist_str[50];
snprintf(dist_str, sizeof(dist_str), "%.2f", r.total_distance);
content += "<td>" + std::string(dist_str) + "</td>";
content += "<td>" + std::to_string(r.total_routes) + "</td>";
// Injected the interactive toggle button
content += "<td class='text-center'><button class='btn btn-glass-primary btn-sm route-toggle-btn' data-iata='" + r.iata + "' onclick='toggleRoute(\"" + r.iata + "\")'>Show</button></td></tr>";
}
content += "</tbody></table></div>";
return crow::response(getLayout("One Hop Results", content));
});
CROW_ROUTE(app, "/search/airline")([](const crow::request& req) -> crow::response {
char* iata_param = req.url_params.get("iata");
if (!iata_param) return crow::response(204);
std::string iata(iata_param);
if (iata.empty()) return crow::response(204);
std::transform(iata.begin(), iata.end(), iata.begin(), ::toupper);
std::lock_guard<std::mutex> lock(data_mutex);
if (airlinesMap.count(iata) == 0) {
return crow::response(getLayout("Not Found", "<div class='glass-panel p-4 text-warning'>Airline " + iata + " not found.</div>"));
}
std::unordered_map<std::string, int> airportCounts;
std::vector<Route> airlineRoutes;
for (const auto& r : routesList) {
if (r.airline_iata == iata) {
if (airportsMap.count(r.source_iata) && airportsMap.count(r.dest_iata)) {
airportCounts[r.source_iata]++;
airportCounts[r.dest_iata]++;
airlineRoutes.push_back(r);
}
}
}
std::vector<std::pair<std::string, int>> sortedAirports(airportCounts.begin(), airportCounts.end());
std::sort(sortedAirports.begin(), sortedAirports.end(), [](const auto& a, const auto& b) { return a.second > b.second; });
std::string content = "<h2 class='fw-bold mb-4'>Airline Profile: <span class='text-primary'>" + airlinesMap[iata].name + " (" + iata + ")</span></h2>";
if (!airlineRoutes.empty()) {
content += "<div class='glass-panel p-2 mb-5'><div id='airlineMap' style='height: 500px; border-radius: 12px; z-index: 1;'></div></div>";
content += "<script>";
content += "var map = L.map('airlineMap');";
content += "L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, attribution: '© OpenStreetMap © CARTO' }).addTo(map);";
content += "var bounds = new L.LatLngBounds();";
// Inject Bezier Function
content += "function getBezier(lat1, lon1, lat2, lon2, offset) { var midLat = (lat1 + lat2) / 2; var midLon = (lon1 + lon2) / 2; var dLat = lat2 - lat1; var dLon = lon2 - lon1; var cpLat = midLat - (dLon * offset); var cpLon = midLon + (dLat * offset); var points = []; for (var t = 0; t < 1; t += 0.05) { var lat = (1-t)*(1-t)*lat1 + 2*(1-t)*t*cpLat + t*t*lat2; var lon = (1-t)*(1-t)*lon1 + 2*(1-t)*t*cpLon + t*t*lon2; points.push([lat, lon]); } points.push([lat2, lon2]); return points; }";
for (const auto& pair : sortedAirports) {
Airport& a = airportsMap[pair.first];
std::string safeName = a.name;
size_t pos = 0;
while ((pos = safeName.find("'", pos)) != std::string::npos) { safeName.replace(pos, 1, "\\'"); pos += 2; }
content += "var m = L.circleMarker([" + std::to_string(a.latitude) + ", " + std::to_string(a.longitude) + "], {color:'#3b82f6', radius: 4, fillOpacity: 0.8, weight: 1}).addTo(map);";
content += "m.bindPopup('<b>" + a.iata + "</b><br>" + safeName + "');";
content += "bounds.extend([" + std::to_string(a.latitude) + ", " + std::to_string(a.longitude) + "]);";
}
for (const auto& r : airlineRoutes) {
Airport& s = airportsMap[r.source_iata];
Airport& d = airportsMap[r.dest_iata];
// Generate and draw the curve
content += "var curve = getBezier(" + std::to_string(s.latitude) + ", " + std::to_string(s.longitude) + ", " + std::to_string(d.latitude) + ", " + std::to_string(d.longitude) + ", 0.15);";
content += "L.polyline(curve, {color: '#94a3b8', weight: 1.5, opacity: 0.4}).addTo(map);";
}
content += "map.fitBounds(bounds, {padding: [30, 30]});";
content += "</script>";
} else {
content += "<div class='glass-panel p-4 text-info mb-4'>No flight routes found for this airline to display on the map.</div>";
}
content += "<div class='glass-panel p-0 overflow-hidden'>";
content += "<table class='glass-table'><thead><tr><th>Airport Name</th><th>IATA Code</th><th>Total Routes</th></tr></thead><tbody>";
for (const auto& pair : sortedAirports) {
std::string airportName = airportsMap.count(pair.first) ? airportsMap[pair.first].name : "Unknown Airport";
content += "<tr><td>" + airportName + "</td><td class='fw-bold'>" + pair.first + "</td><td>" + std::to_string(pair.second) + "</td></tr>";
}
content += "</tbody></table></div>";
return crow::response(getLayout("Airline Report", content));
});
CROW_ROUTE(app, "/search/airport")([](const crow::request& req) -> crow::response {
char* iata_param = req.url_params.get("iata");
if (!iata_param) return crow::response(204);
std::string iata(iata_param);
if (iata.empty()) return crow::response(204);
std::transform(iata.begin(), iata.end(), iata.begin(), ::toupper);
std::lock_guard<std::mutex> lock(data_mutex);
if (airportsMap.count(iata) == 0) {
return crow::response(getLayout("Not Found", "<div class='glass-panel p-4 text-warning'>Airport " + iata + " not found.</div>"));
}
Airport centerAirport = airportsMap[iata];
std::unordered_map<std::string, int> airlineCounts;
std::unordered_set<std::string> connectedAirports;
for (const auto& r : routesList) {
if (r.source_iata == iata || r.dest_iata == iata) {
airlineCounts[r.airline_iata]++;
if (r.source_iata == iata && airportsMap.count(r.dest_iata)) connectedAirports.insert(r.dest_iata);
if (r.dest_iata == iata && airportsMap.count(r.source_iata)) connectedAirports.insert(r.source_iata);
}
}
std::vector<std::pair<std::string, int>> sortedAirlines(airlineCounts.begin(), airlineCounts.end());
std::sort(sortedAirlines.begin(), sortedAirlines.end(), [](const auto& a, const auto& b) { return a.second > b.second; });
std::string content = "<h2 class='fw-bold mb-4'>Airport Profile: <span class='text-primary'>" + centerAirport.name + " (" + iata + ")</span></h2>";
content += "<div class='glass-panel p-2 mb-5'><div id='airportMap' style='height: 500px; border-radius: 12px; z-index: 1;'></div></div>";
content += "<script>\n";
content += "var map = L.map('airportMap').setView([" + std::to_string(centerAirport.latitude) + ", " + std::to_string(centerAirport.longitude) + "], 4);\n";
content += "L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, attribution: '© OpenStreetMap © CARTO' }).addTo(map);\n\n";
content += "var centerData = { lat: " + std::to_string(centerAirport.latitude) + ", lon: " + std::to_string(centerAirport.longitude) + ", iata: '" + iata + "' };\n";
content += "var connectedData = [\n";
int i = 0;
int n = connectedAirports.size();
for (const std::string& conn_iata : connectedAirports) {
Airport& a = airportsMap[conn_iata];
std::string safeName = a.name;
size_t pos = 0;
while ((pos = safeName.find("'", pos)) != std::string::npos) { safeName.replace(pos, 1, "\\'"); pos += 2; }
content += " { lat: " + std::to_string(a.latitude) + ", lon: " + std::to_string(a.longitude) + ", iata: '" + a.iata + "', name: '" + safeName + "' }";
if (i < n - 1) content += ",\n";
else content += "\n";
i++;
}
content += "];\n\n";
// Inject Bezier Function
content += "function getBezier(lat1, lon1, lat2, lon2, offset) { var midLat = (lat1 + lat2) / 2; var midLon = (lon1 + lon2) / 2; var dLat = lat2 - lat1; var dLon = lon2 - lon1; var cpLat = midLat - (dLon * offset); var cpLon = midLon + (dLat * offset); var points = []; for (var t = 0; t < 1; t += 0.05) { var lat = (1-t)*(1-t)*lat1 + 2*(1-t)*t*cpLat + t*t*lat2; var lon = (1-t)*(1-t)*lon1 + 2*(1-t)*t*cpLon + t*t*lon2; points.push([lat, lon]); } points.push([lat2, lon2]); return points; }\n\n";
content += "L.marker([centerData.lat, centerData.lon]).addTo(map).bindPopup('<b>Searched Airport: ' + centerData.iata + '</b>').openPopup();\n\n";
content += "var bounds = new L.LatLngBounds([ [centerData.lat, centerData.lon] ]);\n";
content += "connectedData.forEach(function(conn) {\n";
content += " L.circleMarker([conn.lat, conn.lon], {color: '#ec4899', radius: 4, fillOpacity: 0.8, weight: 1}).addTo(map).bindPopup('<b>' + conn.iata + '</b><br>' + conn.name);\n";
// Generate and draw the curve
content += " var curve = getBezier(centerData.lat, centerData.lon, conn.lat, conn.lon, 0.15);\n";
content += " L.polyline(curve, {color: '#94a3b8', weight: 1.5, opacity: 0.4}).addTo(map);\n";
content += " bounds.extend([conn.lat, conn.lon]);\n";
content += "});\n";
content += "if (connectedData.length > 0) {\n";
content += " map.fitBounds(bounds, {padding: [30, 30]});\n";
content += "}\n";
content += "</script>\n";
content += "<div class='glass-panel p-0 overflow-hidden'>";
content += "<table class='glass-table'><thead><tr><th>Airline Name</th><th>IATA Code</th><th>Total Routes Here</th></tr></thead><tbody>";
for (const auto& pair : sortedAirlines) {
std::string airlineName = airlinesMap.count(pair.first) ? airlinesMap[pair.first].name : "Unknown Airline";
content += "<tr><td class='fw-bold'>" + airlineName + "</td><td>" + pair.first + "</td><td>" + std::to_string(pair.second) + "</td></tr>";
}
content += "</tbody></table></div>";
return crow::response(getLayout("Airport Report", content));
});
CROW_ROUTE(app, "/list/airlines")([](){
std::lock_guard<std::mutex> lock(data_mutex);
std::map<std::string, Airline> sortedAirlines(airlinesMap.begin(), airlinesMap.end());
std::string content = "<h2 class='fw-bold mb-4'>All Airlines Index</h2><div class='glass-panel p-0 overflow-hidden'><table class='glass-table'><thead><tr><th>IATA</th><th>Airline Name</th></tr></thead><tbody>";
for (const auto& pair : sortedAirlines) {
content += "<tr><td class='fw-bold text-primary'>" + pair.first + "</td><td>" + pair.second.name + "</td></tr>";
}
content += "</tbody></table></div>";
return getLayout("All Airlines", content);
});
CROW_ROUTE(app, "/list/airports")([](){
std::lock_guard<std::mutex> lock(data_mutex);
std::map<std::string, Airport> sortedAirports(airportsMap.begin(), airportsMap.end());
std::string content = "<h2 class='fw-bold mb-4'>All Airports Index</h2><div class='glass-panel p-0 overflow-hidden'><table class='glass-table'><thead><tr><th>IATA</th><th>Airport Name</th><th>City</th></tr></thead><tbody>";
for (const auto& pair : sortedAirports) {
content += "<tr><td class='fw-bold text-primary'>" + pair.first + "</td><td>" + pair.second.name + "</td><td>" + pair.second.city + "</td></tr>";
}
content += "</tbody></table></div>";
return getLayout("All Airports", content);
});
// --- EXPANDED ADMIN DASHBOARD ---
CROW_ROUTE(app, "/admin")([](){
std::string content = "<div class='text-center mb-5'><h1 class='fw-bold' style='color: #0f172a; font-size: 2.5rem; letter-spacing: -1px;'>Admin Dashboard</h1><p class='text-muted'>Manage system data sets via direct database insertion.</p></div>";
// Custom inputs CSS for admin area
content += "<style>";
content += ".admin-select { background: rgba(255,255,255,0.5); border: 1px solid var(--glass-border); border-radius: 10px; padding: 0.6rem; color: #1e293b; width: 100%; outline: none; }";
content += "</style>";
// Airline Manager Card
content += "<div class='glass-panel p-4 mb-4'><h4 class='fw-bold mb-4' style='color: #3b82f6;'>Manage Airlines</h4>";
content += "<form action='/admin/airline' method='POST' novalidate>";
content += "<div class='row mb-3 g-3'>";
content += "<div class='col-md-2'><select name='action' class='admin-select'><option value='add'>Add</option><option value='modify'>Modify</option><option value='delete'>Delete</option></select></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='text' name='iata' class='glass-input w-100' placeholder='IATA (Req)' required></div></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='text' name='id' class='glass-input w-100' placeholder='Airline ID'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='name' class='glass-input w-100' placeholder='Airline Name'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='alias' class='glass-input w-100' placeholder='Alias'></div></div>";
content += "</div><div class='row mb-3 g-3'>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='icao' class='glass-input w-100' placeholder='ICAO'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='callsign' class='glass-input w-100' placeholder='Callsign'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='country' class='glass-input w-100' placeholder='Country'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='active' class='glass-input w-100' placeholder='Active (Y/N)'></div></div>";
content += "</div><div class='row mt-4'><div class='col-12'><button type='submit' class='btn btn-glass-primary w-100 py-2'>Submit Airline</button></div>";
content += "</div><small class='text-muted mt-3 d-block text-center'>For Modify/Delete, IATA is required. Other fields only needed for Add/Modify.</small></form></div>";
// Airport Manager Card
content += "<div class='glass-panel p-4 mb-4'><h4 class='fw-bold mb-4' style='color: #10b981;'>Manage Airports</h4>";
content += "<form action='/admin/airport' method='POST' novalidate>";
content += "<div class='row mb-3 g-3'>";
content += "<div class='col-md-2'><select name='action' class='admin-select'><option value='add'>Add</option><option value='modify'>Modify</option><option value='delete'>Delete</option></select></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='text' name='iata' class='glass-input w-100' placeholder='IATA (Req)' required></div></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='text' name='id' class='glass-input w-100' placeholder='ID'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='name' class='glass-input w-100' placeholder='Name'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='city' class='glass-input w-100' placeholder='City'></div></div>";
content += "</div><div class='row mb-3 g-3'>";
content += "<div class='col-md-2'><div class='input-stack'><input type='text' name='country' class='glass-input w-100' placeholder='Country'></div></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='text' name='icao' class='glass-input w-100' placeholder='ICAO'></div></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='number' step='any' name='lat' class='glass-input w-100' placeholder='Latitude'></div></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='number' step='any' name='lon' class='glass-input w-100' placeholder='Longitude'></div></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='text' name='altitude' class='glass-input w-100' placeholder='Altitude'></div></div>";
content += "<div class='col-md-2'><div class='input-stack'><input type='text' name='timezone' class='glass-input w-100' placeholder='Time Offset'></div></div>";
content += "</div><div class='row mb-3 g-3'>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='dst' class='glass-input w-100' placeholder='DST (E/A/S/O/Z/N)'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='tz_database' class='glass-input w-100' placeholder='TZ Database'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='type' class='glass-input w-100' placeholder='Type (airport)'></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='source' class='glass-input w-100' placeholder='Source'></div></div>";
content += "</div><div class='row mt-4'><div class='col-12'><button type='submit' class='btn btn-glass w-100 py-2' style='color: #10b981; border-color: rgba(16, 185, 129, 0.3); background: rgba(16, 185, 129, 0.1);'>Submit Airport</button></div>";
content += "</div></form></div>";
// Route Manager Card
content += "<div class='glass-panel p-4 mb-4'><h4 class='fw-bold mb-4' style='color: #8b5cf6;'>Manage Routes</h4>";
content += "<form action='/admin/route' method='POST' novalidate>";
content += "<div class='row mb-3 g-3'>";
content += "<div class='col-md-3'><select name='action' class='admin-select'><option value='add'>Add</option><option value='delete'>Delete</option></select></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='airline' class='glass-input w-100' placeholder='Airline IATA' required></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='source' class='glass-input w-100' placeholder='Source IATA' required></div></div>";
content += "<div class='col-md-3'><div class='input-stack'><input type='text' name='dest' class='glass-input w-100' placeholder='Dest IATA' required></div></div>";
content += "<div class='col-12 mt-4'><button type='submit' class='btn btn-glass w-100 py-2' style='color: #8b5cf6; border-color: rgba(139, 92, 246, 0.3); background: rgba(139, 92, 246, 0.1);'>Submit Route</button></div>";
content += "</div></form></div>";
return getLayout("Admin Dashboard", content);
});
CROW_ROUTE(app, "/admin/airline").methods(crow::HTTPMethod::POST)
([](const crow::request& req){
crow::query_string qs("?" + req.body);
std::string action = urlDecode(qs.get("action") ? qs.get("action") : "");
std::string iata = urlDecode(qs.get("iata") ? qs.get("iata") : "");
std::string id = urlDecode(qs.get("id") ? qs.get("id") : "");
std::string name = urlDecode(qs.get("name") ? qs.get("name") : "");
std::string alias = urlDecode(qs.get("alias") ? qs.get("alias") : "");
std::string icao = urlDecode(qs.get("icao") ? qs.get("icao") : "");
std::string callsign = urlDecode(qs.get("callsign") ? qs.get("callsign") : "");
std::string country = urlDecode(qs.get("country") ? qs.get("country") : "");
std::string active = urlDecode(qs.get("active") ? qs.get("active") : "");
std::transform(iata.begin(), iata.end(), iata.begin(), ::toupper);
if (iata.empty() || action.empty()) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Missing required IATA or Action field.</div>");
std::lock_guard<std::mutex> lock(data_mutex);
if (action == "add") {
if (id.empty() || name.empty()) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Missing ID or Name for Add operation.</div>");
if (airlinesMap.count(iata)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airline IATA code already exists.</div>");
for (const auto& pair : airlinesMap) if (pair.second.id == id) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airline ID already exists.</div>");
airlinesMap[iata] = {id, name, alias, iata, icao, callsign, country, active};
return getLayout("Success", "<div class='glass-panel p-4 text-success'>Airline added successfully.</div>");
}
else if (action == "modify") {
if (!airlinesMap.count(iata)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airline IATA not found for modification.</div>");
if (!id.empty()) {
for (const auto& pair : airlinesMap) {
if (pair.second.id == id && pair.first != iata) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>New Airline ID already in use.</div>");
}
airlinesMap[iata].id = id;
}
if (!name.empty()) airlinesMap[iata].name = name;
if (!alias.empty()) airlinesMap[iata].alias = alias;
if (!icao.empty()) airlinesMap[iata].icao = icao;
if (!callsign.empty()) airlinesMap[iata].callsign = callsign;
if (!country.empty()) airlinesMap[iata].country = country;
if (!active.empty()) airlinesMap[iata].active = active;
return getLayout("Success", "<div class='glass-panel p-4 text-success'>Airline modified successfully.</div>");
}
else if (action == "delete") {
if (!airlinesMap.count(iata)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airline IATA not found.</div>");
auto it = std::remove_if(routesList.begin(), routesList.end(), [&](const Route& r){
if (r.airline_iata == iata) {
airportRouteCounts[r.source_iata]--;
airportRouteCounts[r.dest_iata]--;
return true;
}
return false;
});
routesList.erase(it, routesList.end());
airlinesMap.erase(iata);
return getLayout("Success", "<div class='glass-panel p-4 text-success'>Airline and associated routes deleted successfully.</div>");
}
return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Invalid action.</div>");
});
CROW_ROUTE(app, "/admin/airport").methods(crow::HTTPMethod::POST)
([](const crow::request& req){
crow::query_string qs("?" + req.body);
std::string action = urlDecode(qs.get("action") ? qs.get("action") : "");
std::string iata = urlDecode(qs.get("iata") ? qs.get("iata") : "");
std::string id = urlDecode(qs.get("id") ? qs.get("id") : "");
std::string name = urlDecode(qs.get("name") ? qs.get("name") : "");
std::string city = urlDecode(qs.get("city") ? qs.get("city") : "");
std::string country = urlDecode(qs.get("country") ? qs.get("country") : "");
std::string lat_s = urlDecode(qs.get("lat") ? qs.get("lat") : "");
std::string lon_s = urlDecode(qs.get("lon") ? qs.get("lon") : "");
std::string icao = urlDecode(qs.get("icao") ? qs.get("icao") : "");
std::string altitude = urlDecode(qs.get("altitude") ? qs.get("altitude") : "");
std::string timezone = urlDecode(qs.get("timezone") ? qs.get("timezone") : "");
std::string dst = urlDecode(qs.get("dst") ? qs.get("dst") : "");
std::string tz_database = urlDecode(qs.get("tz_database") ? qs.get("tz_database") : "");
std::string type = urlDecode(qs.get("type") ? qs.get("type") : "airport");
std::string source = urlDecode(qs.get("source") ? qs.get("source") : "OurAirports");
std::transform(iata.begin(), iata.end(), iata.begin(), ::toupper);
if (iata.empty() || action.empty()) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Missing required IATA or Action field.</div>");
std::lock_guard<std::mutex> lock(data_mutex);
if (action == "add") {
if (id.empty() || name.empty() || lat_s.empty() || lon_s.empty()) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Missing required fields for Add operation.</div>");
if (airportsMap.count(iata)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airport IATA code already exists.</div>");
for (const auto& pair : airportsMap) if (pair.second.id == id) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airport ID already exists.</div>");
double lat = 0.0, lon = 0.0;
try {
lat = std::stod(lat_s);
lon = std::stod(lon_s);
} catch (...) {
return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Invalid numerical format for Latitude/Longitude.</div>");
}
airportsMap[iata] = {id, name, city, country, iata, icao, lat, lon, altitude, timezone, dst, tz_database, type, source};
return getLayout("Success", "<div class='glass-panel p-4 text-success'>Airport added successfully.</div>");
}
else if (action == "modify") {
if (!airportsMap.count(iata)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airport IATA not found.</div>");
if (!id.empty()) {
for (const auto& pair : airportsMap) {
if (pair.second.id == id && pair.first != iata) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>New Airport ID already in use.</div>");
}
airportsMap[iata].id = id;
}
if (!name.empty()) airportsMap[iata].name = name;
if (!city.empty()) airportsMap[iata].city = city;
if (!country.empty()) airportsMap[iata].country = country;
if (!icao.empty()) airportsMap[iata].icao = icao;
if (!altitude.empty()) airportsMap[iata].altitude = altitude;
if (!timezone.empty()) airportsMap[iata].timezone = timezone;
if (!dst.empty()) airportsMap[iata].dst = dst;
if (!tz_database.empty()) airportsMap[iata].tz_database = tz_database;
if (!type.empty()) airportsMap[iata].type = type;
if (!source.empty()) airportsMap[iata].source = source;
try {
if (!lat_s.empty()) airportsMap[iata].latitude = std::stod(lat_s);
if (!lon_s.empty()) airportsMap[iata].longitude = std::stod(lon_s);
} catch (...) {
return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Invalid numerical format for Latitude/Longitude.</div>");
}
return getLayout("Success", "<div class='glass-panel p-4 text-success'>Airport modified successfully.</div>");
}
else if (action == "delete") {
if (!airportsMap.count(iata)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airport IATA not found.</div>");
auto it = std::remove_if(routesList.begin(), routesList.end(), [&](const Route& r){
if (r.source_iata == iata || r.dest_iata == iata) {
if (r.source_iata != iata) airportRouteCounts[r.source_iata]--;
if (r.dest_iata != iata) airportRouteCounts[r.dest_iata]--;
return true;
}
return false;
});
routesList.erase(it, routesList.end());
airportRouteCounts.erase(iata);
airportsMap.erase(iata);
return getLayout("Success", "<div class='glass-panel p-4 text-success'>Airport and associated routes deleted successfully.</div>");
}
return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Invalid action.</div>");
});
CROW_ROUTE(app, "/admin/route").methods(crow::HTTPMethod::POST)
([](const crow::request& req){
crow::query_string qs("?" + req.body);
std::string action = urlDecode(qs.get("action") ? qs.get("action") : "");
std::string airline = urlDecode(qs.get("airline") ? qs.get("airline") : "");
std::string source = urlDecode(qs.get("source") ? qs.get("source") : "");
std::string dest = urlDecode(qs.get("dest") ? qs.get("dest") : "");
std::transform(airline.begin(), airline.end(), airline.begin(), ::toupper);
std::transform(source.begin(), source.end(), source.begin(), ::toupper);
std::transform(dest.begin(), dest.end(), dest.begin(), ::toupper);
if (action.empty() || airline.empty() || source.empty() || dest.empty()) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Missing required fields.</div>");
std::lock_guard<std::mutex> lock(data_mutex);
if (action == "add") {
if (!airlinesMap.count(airline)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Airline IATA does not exist.</div>");
if (!airportsMap.count(source)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Source Airport IATA does not exist.</div>");
if (!airportsMap.count(dest)) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Destination Airport IATA does not exist.</div>");
routesList.push_back({airline, source, dest});
airportRouteCounts[source]++;
airportRouteCounts[dest]++;
return getLayout("Success", "<div class='glass-panel p-4 text-success'>Route added successfully.</div>");
}
else if (action == "delete") {
auto it = std::remove_if(routesList.begin(), routesList.end(), [&](const Route& r){
if (r.airline_iata == airline && r.source_iata == source && r.dest_iata == dest) {
airportRouteCounts[source]--;
airportRouteCounts[dest]--;
return true;
}
return false;
});
if (it != routesList.end()) {
routesList.erase(it, routesList.end());
return getLayout("Success", "<div class='glass-panel p-4 text-success'>Route(s) deleted successfully.</div>");
} else {
return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Route not found.</div>");
}
}
return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Invalid action.</div>");
});
CROW_ROUTE(app, "/student_id")([](){
std::string content = "<div class='glass-panel p-5 text-center w-50 mx-auto mt-5'><h5 style='color: #3b82f6;' class='mb-3'>De Anza College Capstone Project</h5><h3 class='fw-bold mt-2'>Henry Stallard</h3><p class='text-muted fs-5'>Student ID: 20668498</p></div>";
return getLayout("Student Info", content);
});
CROW_ROUTE(app, "/get_code")([](){
std::ifstream file("main.cpp");
if (!file.is_open()) {
file.open("../main.cpp");
if (!file.is_open()) return getLayout("Error", "<div class='glass-panel p-4 text-danger'>Could not locate main.cpp file.</div>");
}
std::string fileContents((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
std::string content = "<h2 class='fw-bold mb-4'>Source Code (main.cpp)</h2><div class='glass-panel p-4' style='background: #1e293b; color: #cbd5e1; max-height: 75vh; overflow-y: auto;'><pre><code style='font-family: monospace;'>" + escapeHTML(fileContents) + "</code></pre></div>";
return getLayout("Source Code", content);
});
// --- TYPEAHEAD API ENDPOINTS ---
CROW_ROUTE(app, "/api/search/airports")([](const crow::request& req) {
char* q_param = req.url_params.get("q");
if (!q_param) return crow::response(400);
std::string q(q_param);
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
std::vector<crow::json::wvalue> results;
std::lock_guard<std::mutex> lock(data_mutex);
for (const auto& pair : airportsMap) {
if (results.size() >= 10) break;
std::string iata_low = pair.second.iata;
std::string name_low = pair.second.name;
std::transform(iata_low.begin(), iata_low.end(), iata_low.begin(), ::tolower);
std::transform(name_low.begin(), name_low.end(), name_low.begin(), ::tolower);
if (iata_low.find(q) != std::string::npos || name_low.find(q) != std::string::npos) {
crow::json::wvalue item;
item["iata"] = pair.second.iata;
item["name"] = pair.second.name;
results.push_back(std::move(item));
}
}
return crow::response(crow::json::wvalue(results));
});
CROW_ROUTE(app, "/api/search/airlines")([](const crow::request& req) {
char* q_param = req.url_params.get("q");
if (!q_param) return crow::response(400);
std::string q(q_param);
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
std::vector<crow::json::wvalue> results;
std::lock_guard<std::mutex> lock(data_mutex);
for (const auto& pair : airlinesMap) {
if (results.size() >= 10) break;
std::string iata_low = pair.second.iata;
std::string name_low = pair.second.name;
std::transform(iata_low.begin(), iata_low.end(), iata_low.begin(), ::tolower);
std::transform(name_low.begin(), name_low.end(), name_low.begin(), ::tolower);
if (iata_low.find(q) != std::string::npos || name_low.find(q) != std::string::npos) {
crow::json::wvalue item;
item["iata"] = pair.second.iata;
item["name"] = pair.second.name;
results.push_back(std::move(item));
}
}
return crow::response(crow::json::wvalue(results));
});
// --- Static File Routes ---
CROW_ROUTE(app, "/click.mp3")([](crow::response& res) {
res.set_static_file_info("click.mp3");
res.end();
});
CROW_ROUTE(app, "/profile.jpg")([](crow::response& res) {
res.set_static_file_info("profile.jpg");
res.end();
});
CROW_ROUTE(app, "/mascot.jpg")([](crow::response& res) {
res.set_static_file_info("mascot.jpg");
res.end();
});
CROW_ROUTE(app, "/click2.mp3")([](crow::response& res) {
res.set_static_file_info("click2.mp3");
res.end();
});
app.port(8080).multithreaded().run();
return 0;
}