diff --git a/assets/assets.go b/assets/assets.go index 54e1c4a..e80edaa 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -28,5 +28,6 @@ import "embed" // This approach ensures the web interface is always available without requiring // external file deployment or complicated asset management. // +// Updated to include database.html for database status page //go:embed static var Static embed.FS diff --git a/assets/static/css/style.css b/assets/static/css/style.css index 5389c2d..3879d96 100644 --- a/assets/static/css/style.css +++ b/assets/static/css/style.css @@ -566,6 +566,95 @@ body { color: #00ff88 !important; } +/* Rich callsign display styles */ +.callsign-display { + display: inline-block; +} + +.callsign-display.enriched { + display: inline-flex; + flex-direction: column; + gap: 0.25rem; +} + +.callsign-code { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.airline-code { + color: #00ff88 !important; + font-weight: 600; + font-family: monospace; + background: rgba(0, 255, 136, 0.1); + padding: 0.1rem 0.3rem; + border-radius: 3px; + border: 1px solid rgba(0, 255, 136, 0.3); +} + +.flight-number { + color: #00a8ff !important; + font-weight: 500; + font-family: monospace; +} + +.callsign-details { + font-size: 0.85rem; + opacity: 0.9; +} + +.airline-name { + color: #ffd700 !important; + font-weight: 500; +} + +.airline-country { + color: #cccccc !important; + font-size: 0.8rem; + opacity: 0.8; +} + +.callsign-display.simple { + color: #00ff88 !important; + font-family: monospace; +} + +.callsign-display.no-data { + color: #888888 !important; + font-style: italic; +} + +/* Compact callsign for table view */ +.callsign-compact { + color: #00ff88 !important; + font-family: monospace; + font-weight: 500; +} + +/* Loading state for callsign enhancement */ +.callsign-loading { + position: relative; +} + +.callsign-loading::after { + content: '⟳'; + margin-left: 0.25rem; + opacity: 0.6; + animation: spin 1s linear infinite; + font-size: 0.8rem; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.callsign-enhanced { + /* Smooth transition when enhanced */ + transition: all 0.3s ease; +} + .popup-details { font-size: 0.9rem; color: #ffffff !important; diff --git a/assets/static/index.html b/assets/static/index.html index 5256e82..2994330 100644 --- a/assets/static/index.html +++ b/assets/static/index.html @@ -28,7 +28,10 @@
-

SkyView v0.0.8

+

SkyView v0.0.8 + + 📊 +

diff --git a/assets/static/js/app.js b/assets/static/js/app.js index 59b7dae..4ba39d3 100644 --- a/assets/static/js/app.js +++ b/assets/static/js/app.js @@ -7,6 +7,7 @@ import { WebSocketManager } from './modules/websocket.js?v=2'; import { AircraftManager } from './modules/aircraft-manager.js?v=2'; import { MapManager } from './modules/map-manager.js?v=2'; import { UIManager } from './modules/ui-manager.js?v=2'; +import { CallsignManager } from './modules/callsign-manager.js'; class SkyView { constructor() { @@ -15,6 +16,7 @@ class SkyView { this.aircraftManager = null; this.mapManager = null; this.uiManager = null; + this.callsignManager = null; // 3D Radar this.radar3d = null; @@ -37,12 +39,15 @@ class SkyView { this.uiManager.initializeViews(); this.uiManager.initializeEventListeners(); + // Initialize callsign manager for enriched callsign display + this.callsignManager = new CallsignManager(); + // Initialize map manager and get the main map this.mapManager = new MapManager(); const map = await this.mapManager.initializeMap(); - // Initialize aircraft manager with the map - this.aircraftManager = new AircraftManager(map); + // Initialize aircraft manager with the map and callsign manager + this.aircraftManager = new AircraftManager(map, this.callsignManager); // Set up selected aircraft trail callback this.aircraftManager.setSelectedAircraftCallback((icao) => { diff --git a/assets/static/js/modules/aircraft-manager.js b/assets/static/js/modules/aircraft-manager.js index 8346bc9..78f5ada 100644 --- a/assets/static/js/modules/aircraft-manager.js +++ b/assets/static/js/modules/aircraft-manager.js @@ -1,7 +1,8 @@ // Aircraft marker and data management module export class AircraftManager { - constructor(map) { + constructor(map, callsignManager = null) { this.map = map; + this.callsignManager = callsignManager; this.aircraftData = new Map(); this.aircraftMarkers = new Map(); this.aircraftTrails = new Map(); @@ -228,6 +229,11 @@ export class AircraftManager { // Handle popup exactly like Leaflet expects if (marker.isPopupOpen()) { marker.setPopupContent(this.createPopupContent(aircraft)); + // Enhance callsign display for updated popup + const popupElement = marker.getPopup().getElement(); + if (popupElement) { + this.enhanceCallsignDisplay(popupElement); + } } this.markerUpdateCount++; @@ -250,6 +256,14 @@ export class AircraftManager { maxWidth: 450, className: 'aircraft-popup' }); + + // Enhance callsign display when popup opens + marker.on('popupopen', (e) => { + const popupElement = e.popup.getElement(); + if (popupElement) { + this.enhanceCallsignDisplay(popupElement); + } + }); this.aircraftMarkers.set(icao, marker); this.markerCreateCount++; @@ -435,7 +449,7 @@ export class AircraftManager {
${flag} ${aircraft.ICAO24 || 'N/A'} - ${aircraft.Callsign ? `→ ${aircraft.Callsign}` : ''} + ${aircraft.Callsign ? `→ ${aircraft.Callsign}` : ''}
@@ -511,6 +525,29 @@ export class AircraftManager { return minDistance === Infinity ? null : minDistance; } + // Enhance callsign display in popup after it's created + async enhanceCallsignDisplay(popupElement) { + if (!this.callsignManager) return; + + const callsignElements = popupElement.querySelectorAll('.callsign-loading'); + + for (const element of callsignElements) { + const callsign = element.dataset.callsign; + if (!callsign) continue; + + try { + const callsignInfo = await this.callsignManager.getCallsignInfo(callsign); + const richDisplay = this.callsignManager.generateCallsignDisplay(callsignInfo, callsign); + element.innerHTML = richDisplay; + element.classList.remove('callsign-loading'); + element.classList.add('callsign-enhanced'); + } catch (error) { + console.warn(`Failed to enhance callsign display for ${callsign}:`, error); + // Keep the simple display on error + element.classList.remove('callsign-loading'); + } + } + } toggleTrails() { this.showTrails = !this.showTrails; diff --git a/assets/static/js/modules/callsign-manager.js b/assets/static/js/modules/callsign-manager.js new file mode 100644 index 0000000..ef9a089 --- /dev/null +++ b/assets/static/js/modules/callsign-manager.js @@ -0,0 +1,163 @@ +// Callsign enrichment and display module +export class CallsignManager { + constructor() { + this.callsignCache = new Map(); + this.pendingRequests = new Map(); + + // Rate limiting to avoid overwhelming the API + this.lastRequestTime = 0; + this.requestInterval = 100; // Minimum 100ms between requests + } + + /** + * Get enriched callsign information, using cache when available + * @param {string} callsign - The raw callsign to lookup + * @returns {Promise} - Enriched callsign data + */ + async getCallsignInfo(callsign) { + if (!callsign || callsign.trim() === '') { + return null; + } + + const cleanCallsign = callsign.trim().toUpperCase(); + + // Check cache first + if (this.callsignCache.has(cleanCallsign)) { + return this.callsignCache.get(cleanCallsign); + } + + // Check if we already have a pending request for this callsign + if (this.pendingRequests.has(cleanCallsign)) { + return this.pendingRequests.get(cleanCallsign); + } + + // Rate limiting + const now = Date.now(); + if (now - this.lastRequestTime < this.requestInterval) { + await new Promise(resolve => setTimeout(resolve, this.requestInterval)); + } + + // Create the API request + const requestPromise = this.fetchCallsignInfo(cleanCallsign); + this.pendingRequests.set(cleanCallsign, requestPromise); + + try { + const result = await requestPromise; + + // Cache the result for future use + if (result && result.callsign) { + this.callsignCache.set(cleanCallsign, result.callsign); + } + + return result ? result.callsign : null; + } catch (error) { + console.warn(`Failed to lookup callsign ${cleanCallsign}:`, error); + return null; + } finally { + // Clean up pending request + this.pendingRequests.delete(cleanCallsign); + this.lastRequestTime = Date.now(); + } + } + + /** + * Fetch callsign information from the API + * @param {string} callsign - The callsign to lookup + * @returns {Promise} - API response + */ + async fetchCallsignInfo(callsign) { + const response = await fetch(`/api/callsign/${encodeURIComponent(callsign)}`); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return await response.json(); + } + + /** + * Generate rich HTML display for a callsign + * @param {Object} callsignInfo - Enriched callsign data from API + * @param {string} originalCallsign - Original callsign if API data is null + * @returns {string} - HTML string for display + */ + generateCallsignDisplay(callsignInfo, originalCallsign = '') { + if (!callsignInfo || !callsignInfo.is_valid) { + // Fallback for invalid or missing callsign data + if (originalCallsign) { + return `${originalCallsign}`; + } + return 'N/A'; + } + + const parts = []; + + // Airline code + if (callsignInfo.airline_code) { + parts.push(`${callsignInfo.airline_code}`); + } + + // Flight number + if (callsignInfo.flight_number) { + parts.push(`${callsignInfo.flight_number}`); + } + + // Airline name (if available) + let airlineInfo = ''; + if (callsignInfo.airline_name) { + airlineInfo = ` + ${callsignInfo.airline_name} + `; + + // Add country if available + if (callsignInfo.airline_country) { + airlineInfo += ` (${callsignInfo.airline_country})`; + } + } + + return ` + + ${parts.join(' ')} + ${airlineInfo ? `${airlineInfo}` : ''} + + `; + } + + /** + * Generate compact callsign display for table view + * @param {Object} callsignInfo - Enriched callsign data + * @param {string} originalCallsign - Original callsign fallback + * @returns {string} - Compact HTML for table display + */ + generateCompactCallsignDisplay(callsignInfo, originalCallsign = '') { + if (!callsignInfo || !callsignInfo.is_valid) { + return originalCallsign || 'N/A'; + } + + // For tables, use the display_name or format airline + flight + if (callsignInfo.display_name) { + return `${callsignInfo.display_name}`; + } + + return `${callsignInfo.airline_code} ${callsignInfo.flight_number}`; + } + + /** + * Clear the callsign cache (useful for memory management) + */ + clearCache() { + this.callsignCache.clear(); + console.debug('Callsign cache cleared'); + } + + /** + * Get cache statistics for debugging + * @returns {Object} - Cache size and pending requests + */ + getCacheStats() { + return { + cacheSize: this.callsignCache.size, + pendingRequests: this.pendingRequests.size + }; + } +} \ No newline at end of file