// 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]}]`); // 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 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; const svg = ` `; return L.divIcon({ html: svg, iconSize: [size * 2, size * 2], iconAnchor: [size, size], className: 'aircraft-marker' }); } getAircraftType(aircraft) { if (aircraft.OnGround) return 'ground'; if (aircraft.Category) { const cat = aircraft.Category.toLowerCase(); if (cat.includes('military')) return 'military'; if (cat.includes('cargo') || cat.includes('heavy')) return 'cargo'; if (cat.includes('light') || cat.includes('glider')) return 'ga'; } if (aircraft.Callsign) { const cs = aircraft.Callsign.toLowerCase(); if (cs.includes('mil') || cs.includes('army') || cs.includes('navy')) return 'military'; if (cs.includes('cargo') || cs.includes('fedex') || cs.includes('ups')) return 'cargo'; } return 'commercial'; } getAircraftColor(type) { const colors = { commercial: '#00ff88', cargo: '#ff8c00', military: '#ff4444', ga: '#ffff00', ground: '#888888' }; 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}`); } }