// Aircraft marker and data management module export class AircraftManager { constructor(map, callsignManager = null) { this.map = map; this.callsignManager = callsignManager; this.aircraftData = new Map(); this.aircraftMarkers = new Map(); this.aircraftTrails = new Map(); this.showTrails = false; // Debug: Track marker lifecycle this.markerCreateCount = 0; this.markerUpdateCount = 0; this.markerRemoveCount = 0; // SVG icon cache this.iconCache = new Map(); this.loadIcons(); // Selected aircraft trail tracking this.selectedAircraftCallback = null; // Map event listeners removed - let Leaflet handle positioning naturally } async loadIcons() { const iconTypes = ['commercial', 'helicopter', 'military', 'cargo', 'ga', 'ground']; for (const type of iconTypes) { try { const response = await fetch(`/static/icons/${type}.svg`); const svgText = await response.text(); this.iconCache.set(type, svgText); } catch (error) { console.warn(`Failed to load icon for ${type}:`, error); // Fallback to inline SVG if needed this.iconCache.set(type, this.createFallbackIcon(type)); } } } createFallbackIcon(type) { // Fallback inline SVG if file loading fails const color = 'currentColor'; let path = ''; switch (type) { case 'helicopter': path = ` `; break; case 'military': path = ``; break; case 'cargo': path = ` `; break; case 'ga': path = ``; break; case 'ground': path = ` `; break; default: path = ``; } return ` ${path} `; } updateAircraftData(data) { if (data.aircraft) { // Track which aircraft are new or have position changes for debugging let newCount = 0; let updatedCount = 0; let positionChanges = 0; // Clear old data and update with new data const previousData = new Map(this.aircraftData); this.aircraftData.clear(); for (const [icao, aircraft] of Object.entries(data.aircraft)) { const previousAircraft = previousData.get(icao); if (!previousAircraft) { newCount++; } else { updatedCount++; // Check for position changes if (previousAircraft.Latitude !== aircraft.Latitude || previousAircraft.Longitude !== aircraft.Longitude || previousAircraft.Track !== aircraft.Track || previousAircraft.Altitude !== aircraft.Altitude) { positionChanges++; } } this.aircraftData.set(icao, aircraft); } // Debug logging for track propagation issues if (newCount > 0 || positionChanges > 0) { console.debug(`Aircraft update: ${newCount} new, ${updatedCount} updated, ${positionChanges} position changes`); } } } updateMarkers() { if (!this.map) { console.debug("Map not available for updateMarkers"); return; } // Clear stale aircraft markers const currentICAOs = new Set(this.aircraftData.keys()); for (const [icao, marker] of this.aircraftMarkers) { if (!currentICAOs.has(icao)) { this.map.removeLayer(marker); this.aircraftMarkers.delete(icao); // Remove trail if it exists if (this.aircraftTrails.has(icao)) { const trail = this.aircraftTrails.get(icao); if (trail.polyline) { this.map.removeLayer(trail.polyline); } this.aircraftTrails.delete(icao); } // Notify if this was the selected aircraft if (this.selectedAircraftCallback && this.selectedAircraftCallback(icao)) { // Aircraft was selected and disappeared - could notify main app // For now, the callback will return false automatically since selectedAircraft will be cleared } this.markerRemoveCount++; console.debug(`Removed stale aircraft marker: ${icao}`); } } // Update aircraft markers - process ALL aircraft, not just those with valid coordinates yet // Let updateAircraftMarker handle coordinate validation for (const [icao, aircraft] of this.aircraftData) { // More comprehensive coordinate check const hasCoords = aircraft.Latitude != null && aircraft.Longitude != null && aircraft.Latitude !== 0 && aircraft.Longitude !== 0; const validLat = aircraft.Latitude >= -90 && aircraft.Latitude <= 90; const validLng = aircraft.Longitude >= -180 && aircraft.Longitude <= 180; if (hasCoords && validLat && validLng) { this.updateAircraftMarker(icao, aircraft); } else if (hasCoords) { // Log invalid coordinates for debugging console.debug(`Invalid coordinates for ${icao}: lat=${aircraft.Latitude}, lng=${aircraft.Longitude}`); } // If no coordinates, we still want to process for other updates (trails, etc.) } console.debug(`Markers update complete: ${this.aircraftMarkers.size} active markers, ${this.aircraftData.size} aircraft`); } updateAircraftMarker(icao, aircraft) { const pos = [aircraft.Latitude, aircraft.Longitude]; // Check for invalid coordinates - proper geographic bounds const isValidLat = pos[0] >= -90 && pos[0] <= 90; const isValidLng = pos[1] >= -180 && pos[1] <= 180; if (!isValidLat || !isValidLng || isNaN(pos[0]) || isNaN(pos[1])) { console.error(`🚨 Invalid coordinates for ${icao}: [${pos[0]}, ${pos[1]}] (lat must be -90 to +90, lng must be -180 to +180)`); return; // Don't create/update marker with invalid coordinates } if (this.aircraftMarkers.has(icao)) { // Update existing marker - KISS approach const marker = this.aircraftMarkers.get(icao); // Always update position - don't try to be too smart about detecting changes const oldPos = marker.getLatLng(); const positionChanged = Math.abs(oldPos.lat - pos[0]) > 0.0001 || Math.abs(oldPos.lng - pos[1]) > 0.0001; // ALWAYS update position regardless of change detection to ensure propagation marker.setLatLng(pos); if (positionChanged) { // Debug significant position updates console.debug(`Position change for ${icao}: [${pos[0].toFixed(4)}, ${pos[1].toFixed(4)}] (was [${oldPos.lat.toFixed(4)}, ${oldPos.lng.toFixed(4)}])`); } // Check if icon needs to be updated (track rotation, aircraft type, or ground status changes) const currentRotation = marker._currentRotation || 0; const currentType = marker._currentType || null; const currentOnGround = marker._currentOnGround || false; const newType = this.getAircraftIconType(aircraft); // Fix rotation change detection - handle undefined/null tracks properly const newTrack = aircraft.Track || 0; const rotationChanged = aircraft.Track !== undefined && aircraft.Track !== null && Math.abs(currentRotation - newTrack) > 10; // Increased threshold const typeChanged = currentType !== newType; const groundStatusChanged = currentOnGround !== aircraft.OnGround; // Update icon if anything changed, OR if this is a new track value and we didn't have one before const firstTrackUpdate = currentRotation === 0 && aircraft.Track !== undefined && aircraft.Track !== null && aircraft.Track !== 0; if (rotationChanged || typeChanged || groundStatusChanged || firstTrackUpdate) { marker.setIcon(this.createAircraftIcon(aircraft)); marker._currentRotation = newTrack; marker._currentType = newType; marker._currentOnGround = aircraft.OnGround || false; if (rotationChanged || firstTrackUpdate) { console.debug(`Updated track for ${icao}: ${aircraft.Track}° (was ${currentRotation}°)`); } } // 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++; } else { // Create new marker const icon = this.createAircraftIcon(aircraft); try { const marker = L.marker(pos, { icon: icon }).addTo(this.map); // Store current properties for future update comparisons marker._currentRotation = aircraft.Track || 0; marker._currentType = this.getAircraftIconType(aircraft); marker._currentOnGround = aircraft.OnGround || false; marker.bindPopup(this.createPopupContent(aircraft), { 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++; // Force immediate visibility if (marker._icon) { marker._icon.style.display = 'block'; marker._icon.style.opacity = '1'; marker._icon.style.visibility = 'visible'; } } catch (error) { console.error(`Failed to create marker for ${icao}:`, error); } } // Update trails - check both global trails and individual selected aircraft if (this.showTrails || this.isSelectedAircraftTrailEnabled(icao)) { this.updateAircraftTrail(icao, aircraft); } } createAircraftIcon(aircraft) { const iconType = this.getAircraftIconType(aircraft); const color = this.getAircraftColor(iconType, aircraft); const size = aircraft.OnGround ? 12 : 16; const rotation = aircraft.Track || 0; // Get SVG template from cache let svgTemplate = this.iconCache.get(iconType) || this.iconCache.get('commercial'); if (!svgTemplate) { // Ultimate fallback - create a simple circle svgTemplate = ` `; } // Apply color and rotation to the SVG let svg = svgTemplate .replace(/currentColor/g, color) .replace(/width="32"/, `width="${size * 2}"`) .replace(/height="32"/, `height="${size * 2}"`); // Add rotation to the transform if (rotation !== 0) { svg = svg.replace(/transform="translate\(16,16\)"/, `transform="translate(16,16) rotate(${rotation})"`); } return L.divIcon({ html: svg, iconSize: [size * 2, size * 2], iconAnchor: [size, size], className: 'aircraft-marker' }); } getAircraftType(aircraft) { // For display purposes, return the actual ADS-B category // This is used in the popup display if (aircraft.OnGround) return 'On Ground'; if (aircraft.Category) return aircraft.Category; return 'Unknown'; } getAircraftIconType(aircraft) { // For icon selection, determine which SVG shape to use based on category if (aircraft.OnGround) return 'ground'; if (aircraft.Category) { const cat = aircraft.Category.toLowerCase(); // Specialized aircraft types if (cat.includes('helicopter') || cat.includes('rotorcraft')) return 'helicopter'; if (cat.includes('military') || cat.includes('fighter') || cat.includes('bomber')) return 'military'; if (cat.includes('glider') || cat.includes('ultralight')) return 'ga'; // Weight-based categories with specific icons if (cat.includes('light') && cat.includes('7000')) return 'light'; if (cat.includes('medium') && cat.includes('7000-34000')) return 'medium'; if (cat.includes('large') && cat.includes('34000-136000')) return 'large'; if (cat.includes('heavy') && cat.includes('136000')) return 'heavy'; if (cat.includes('high vortex')) return 'large'; // Use large icon for high vortex // Fallback category matching if (cat.includes('heavy') || cat.includes('super')) return 'heavy'; if (cat.includes('large')) return 'large'; if (cat.includes('medium')) return 'medium'; if (cat.includes('light')) return 'light'; } // Default to medium icon for unknown aircraft return 'medium'; } getAircraftColor(type, aircraft) { // Color mapping based on aircraft type/size switch (type) { case 'military': return '#ff4444'; // Red-orange for military case 'helicopter': return '#ff00ff'; // Magenta for helicopters case 'ground': return '#888888'; // Gray for ground vehicles case 'ga': return '#ffff00'; // Yellow for general aviation case 'light': return '#00bfff'; // Sky blue for light aircraft case 'medium': return '#00ff88'; // Green for medium aircraft case 'large': return '#ff8c00'; // Orange for large aircraft case 'heavy': return '#ff0000'; // Red for heavy aircraft default: return '#00ff88'; // Default green for unknown } } updateAircraftTrail(icao, aircraft) { // Use server-provided position history if (!aircraft.position_history || aircraft.position_history.length < 2) { // No trail data available or not enough points if (this.aircraftTrails.has(icao)) { const trail = this.aircraftTrails.get(icao); if (trail.polyline) { this.map.removeLayer(trail.polyline); } this.aircraftTrails.delete(icao); } return; } // Convert position history to Leaflet format const trailPoints = aircraft.position_history.map(point => [point.lat, point.lon]); // Get or create trail object if (!this.aircraftTrails.has(icao)) { this.aircraftTrails.set(icao, {}); } const trail = this.aircraftTrails.get(icao); // Remove old polyline if it exists if (trail.polyline) { this.map.removeLayer(trail.polyline); } // Create gradient effect - newer points are brighter const segments = []; for (let i = 1; i < trailPoints.length; i++) { const opacity = 0.2 + (0.6 * (i / trailPoints.length)); // Fade from 0.2 to 0.8 const segment = L.polyline([trailPoints[i-1], trailPoints[i]], { color: '#00d4ff', weight: 2, opacity: opacity }); segments.push(segment); } // Create a feature group for all segments trail.polyline = L.featureGroup(segments).addTo(this.map); } createPopupContent(aircraft) { const type = this.getAircraftType(aircraft); const country = aircraft.country || 'Unknown'; const flag = aircraft.flag || '🏳️'; const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0; const altitudeM = altitude ? Math.round(altitude * 0.3048) : 0; const speedKmh = aircraft.GroundSpeed ? Math.round(aircraft.GroundSpeed * 1.852) : 0; const distance = this.calculateDistance(aircraft); const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A'; return `
`; } calculateDistance(aircraft) { if (!aircraft.Latitude || !aircraft.Longitude) return null; // Use closest source as reference point let minDistance = Infinity; for (const [id, srcData] of Object.entries(aircraft.sources || {})) { if (srcData.distance && srcData.distance < minDistance) { minDistance = srcData.distance; } } 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; if (!this.showTrails) { // Clear all trails this.aircraftTrails.forEach((trail, icao) => { if (trail.polyline) { this.map.removeLayer(trail.polyline); } }); this.aircraftTrails.clear(); } return this.showTrails; } showAircraftTrail(icao) { const aircraft = this.aircraftData.get(icao); if (aircraft && aircraft.position_history && aircraft.position_history.length >= 2) { this.updateAircraftTrail(icao, aircraft); } } hideAircraftTrail(icao) { if (this.aircraftTrails.has(icao)) { const trail = this.aircraftTrails.get(icao); if (trail.polyline) { this.map.removeLayer(trail.polyline); } this.aircraftTrails.delete(icao); } } setSelectedAircraftCallback(callback) { this.selectedAircraftCallback = callback; } isSelectedAircraftTrailEnabled(icao) { return this.selectedAircraftCallback && this.selectedAircraftCallback(icao); } centerMapOnAircraft(includeSourcesCallback = null) { const validAircraft = Array.from(this.aircraftData.values()) .filter(a => a.Latitude && a.Longitude); const allPoints = []; // Add aircraft positions validAircraft.forEach(a => { allPoints.push([a.Latitude, a.Longitude]); }); // Add source positions if callback provided if (includeSourcesCallback && typeof includeSourcesCallback === 'function') { const sourcePositions = includeSourcesCallback(); allPoints.push(...sourcePositions); } if (allPoints.length === 0) return; if (allPoints.length === 1) { // Center on single point this.map.setView(allPoints[0], 12); } else { // Fit bounds to all points (aircraft + sources) const bounds = L.latLngBounds(allPoints); this.map.fitBounds(bounds.pad(0.1)); } } }