// Aircraft marker and data management module export class AircraftManager { constructor(map) { this.map = map; 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) { this.aircraftData.clear(); for (const [icao, aircraft] of Object.entries(data.aircraft)) { this.aircraftData.set(icao, aircraft); } } } updateMarkers() { if (!this.map) { 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++; } } // Update aircraft markers - only for aircraft with valid geographic coordinates for (const [icao, aircraft] of this.aircraftData) { const hasCoords = aircraft.Latitude && aircraft.Longitude && 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); } } } 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 - let Leaflet handle everything const oldPos = marker.getLatLng(); marker.setLatLng(pos); // 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); const rotationChanged = aircraft.Track !== undefined && Math.abs(currentRotation - aircraft.Track) > 5; const typeChanged = currentType !== newType; const groundStatusChanged = currentOnGround !== aircraft.OnGround; if (rotationChanged || typeChanged || groundStatusChanged) { marker.setIcon(this.createAircraftIcon(aircraft)); marker._currentRotation = aircraft.Track || 0; marker._currentType = newType; marker._currentOnGround = aircraft.OnGround || false; } // Handle popup exactly like Leaflet expects if (marker.isPopupOpen()) { marker.setPopupContent(this.createPopupContent(aircraft)); } 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' }); 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); 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, we still need basic categories // This determines which SVG shape to use if (aircraft.OnGround) return 'ground'; if (aircraft.Category) { const cat = aircraft.Category.toLowerCase(); // Map to basic icon types for visual representation if (cat.includes('helicopter') || cat.includes('rotorcraft')) return 'helicopter'; if (cat.includes('military') || cat.includes('fighter') || cat.includes('bomber')) return 'military'; if (cat.includes('cargo') || cat.includes('heavy') || cat.includes('super')) return 'cargo'; if (cat.includes('light') || cat.includes('glider') || cat.includes('ultralight')) return 'ga'; } // Default commercial icon for everything else return 'commercial'; } getAircraftColor(type) { const colors = { commercial: '#00ff88', cargo: '#ff8c00', military: '#ff4444', ga: '#ffff00', ground: '#888888', helicopter: '#ff00ff' // Magenta for helicopters }; return colors[type] || colors.commercial; } 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; } 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)); } } }