// 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; // Map event listeners removed - let Leaflet handle positioning naturally } updateAircraftData(data) { if (data.aircraft) { this.aircraftData.clear(); for (const [icao, aircraft] of Object.entries(data.aircraft)) { this.aircraftData.set(icao, aircraft); } console.log(`Aircraft data updated: ${this.aircraftData.size} 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); this.aircraftTrails.delete(icao); 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]; // Debug: Log coordinate format and values console.log(`📍 ${icao}: pos=[${pos[0]}, ${pos[1]}], types=[${typeof pos[0]}, ${typeof pos[1]}]`); console.log(`🔍 Marker check for ${icao}: has=${this.aircraftMarkers.has(icao)}, map_size=${this.aircraftMarkers.size}`); // 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(); console.log(`🔄 Updating ${icao}: [${oldPos.lat}, ${oldPos.lng}] -> [${pos[0]}, ${pos[1]}]`); marker.setLatLng(pos); // Update rotation using Leaflet's options if available, otherwise skip rotation if (aircraft.Track !== undefined) { if (marker.setRotationAngle) { // Use Leaflet rotation plugin method if available marker.setRotationAngle(aircraft.Track); } else if (marker.options) { // Update the marker's options for consistency marker.options.rotationAngle = aircraft.Track; } // Don't manually set CSS transforms - let Leaflet handle it } // Handle popup exactly like Leaflet expects if (marker.isPopupOpen()) { marker.setPopupContent(this.createPopupContent(aircraft)); } this.markerUpdateCount++; } else { // Create new marker console.log(`Creating new marker for ${icao}`); const icon = this.createAircraftIcon(aircraft); try { const marker = L.marker(pos, { icon: icon, rotationAngle: aircraft.Track || 0 }).addTo(this.map); marker.bindPopup(this.createPopupContent(aircraft), { maxWidth: 450, className: 'aircraft-popup' }); this.aircraftMarkers.set(icao, marker); this.markerCreateCount++; console.log(`Created marker for ${icao}, total markers: ${this.aircraftMarkers.size}`); // Force immediate visibility if (marker._icon) { marker._icon.style.display = 'block'; marker._icon.style.opacity = '1'; marker._icon.style.visibility = 'visible'; console.log(`Forced visibility for new marker ${icao}`); } } catch (error) { console.error(`Failed to create marker for ${icao}:`, error); } } // Update trails if (this.showTrails) { this.updateAircraftTrail(icao, pos); } } createAircraftIcon(aircraft) { const type = this.getAircraftType(aircraft); const color = this.getAircraftColor(type); const size = aircraft.OnGround ? 12 : 16; // Create different SVG shapes based on aircraft type let aircraftPath; switch (type) { case 'helicopter': // Helicopter shape with rotor disc aircraftPath = ` `; break; case 'military': // Swept-wing fighter jet shape aircraftPath = ` `; break; case 'cargo': // Wide-body cargo aircraft shape aircraftPath = ` `; break; case 'ga': // Small general aviation aircraft aircraftPath = ` `; break; case 'ground': // Ground vehicle - simplified truck/car shape aircraftPath = ` `; break; default: // Default commercial aircraft shape aircraftPath = ` `; } const svg = ` ${aircraftPath} `; return L.divIcon({ html: svg, iconSize: [size * 2, size * 2], iconAnchor: [size, size], className: 'aircraft-marker' }); } getAircraftType(aircraft) { if (aircraft.OnGround) return 'ground'; // Use ADS-B Category field for proper aircraft classification if (aircraft.Category) { const cat = aircraft.Category.toLowerCase(); // Standard ADS-B aircraft categories - check specific terms first if (cat.includes('helicopter') || cat.includes('rotorcraft') || cat.includes('gyrocopter')) return 'helicopter'; if (cat.includes('military') || cat.includes('fighter') || cat.includes('bomber')) return 'military'; // ADS-B weight-based categories (RTCA DO-260B standard) if (cat.includes('heavy') || cat.includes('super') || cat.includes('136000kg')) return 'cargo'; // Heavy/Super category if (cat.includes('light') || cat.includes('15500kg') || cat.includes('5700kg') || cat.includes('glider') || cat.includes('ultralight') || cat.includes('sport')) return 'ga'; // Light categories if (cat.includes('medium') || cat.includes('34000kg')) return 'commercial'; // Medium category - typically commercial airliners // Specific aircraft type categories if (cat.includes('cargo') || cat.includes('transport') || cat.includes('freighter')) return 'cargo'; if (cat.includes('airliner') || cat.includes('jet') || cat.includes('turboprop')) return 'commercial'; } // Fallback to callsign analysis for classification if (aircraft.Callsign) { const cs = aircraft.Callsign.toLowerCase(); // Helicopter identifiers if (cs.includes('heli') || cs.includes('rescue') || cs.includes('medevac') || cs.includes('lifeguard') || cs.includes('police') || cs.includes('sheriff') || cs.includes('medic')) return 'helicopter'; // Military identifiers if (cs.includes('mil') || cs.includes('army') || cs.includes('navy') || cs.includes('air force') || cs.includes('usaf') || cs.includes('usmc') || cs.includes('uscg')) return 'military'; // Cargo identifiers if (cs.includes('cargo') || cs.includes('fedex') || cs.includes('ups') || cs.includes('dhl') || cs.includes('freight') || cs.includes('express')) return 'cargo'; } // Default to commercial for unclassified aircraft 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, pos) { if (!this.aircraftTrails.has(icao)) { this.aircraftTrails.set(icao, []); } const trail = this.aircraftTrails.get(icao); trail.push(pos); // Keep only last 50 positions if (trail.length > 50) { trail.shift(); } // Draw polyline const trailLine = L.polyline(trail, { color: '#00d4ff', weight: 2, opacity: 0.6 }).addTo(this.map); // Store reference for cleanup if (!this.aircraftTrails.get(icao).polyline) { this.aircraftTrails.get(icao).polyline = trailLine; } else { this.map.removeLayer(this.aircraftTrails.get(icao).polyline); this.aircraftTrails.get(icao).polyline = trailLine; } } createPopupContent(aircraft) { const type = this.getAircraftType(aircraft); const country = this.getCountryFromICAO(aircraft.ICAO24 || ''); const flag = this.getCountryFlag(country); 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; } getCountryFromICAO(icao) { if (!icao || icao.length < 6) return 'Unknown'; const prefix = icao[0]; const countryMap = { '4': 'Europe', 'A': 'United States', 'C': 'Canada', 'D': 'Germany', 'F': 'France', 'G': 'United Kingdom', 'I': 'Italy', 'J': 'Japan' }; return countryMap[prefix] || 'Unknown'; } getCountryFlag(country) { const flags = { 'United States': '🇺🇸', 'Canada': '🇨🇦', 'Germany': '🇩🇪', 'France': '🇫🇷', 'United Kingdom': '🇬🇧', 'Italy': '🇮🇹', 'Japan': '🇯🇵', 'Europe': '🇪🇺' }; return flags[country] || '🏳️'; } 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; } centerMapOnAircraft() { if (this.aircraftData.size === 0) return; const validAircraft = Array.from(this.aircraftData.values()) .filter(a => a.Latitude && a.Longitude); if (validAircraft.length === 0) return; if (validAircraft.length === 1) { // Center on single aircraft const aircraft = validAircraft[0]; this.map.setView([aircraft.Latitude, aircraft.Longitude], 12); } else { // Fit bounds to all aircraft const bounds = L.latLngBounds( validAircraft.map(a => [a.Latitude, a.Longitude]) ); this.map.fitBounds(bounds.pad(0.1)); } } // Simple debug method debugState() { console.log(`Aircraft: ${this.aircraftData.size}, Markers: ${this.aircraftMarkers.size}`); } }