Root cause: The merger was blocking position updates from the same source after the first position was established, designed for multi-source scenarios but preventing single-source position updates. Changes: - Refactor JavaScript into modular architecture (WebSocketManager, AircraftManager, MapManager, UIManager) - Add CPR coordinate validation to prevent invalid latitude/longitude values - Fix merger to allow position updates from same source for moving aircraft - Add comprehensive coordinate bounds checking in CPR decoder - Update HTML to use new modular JavaScript with cache busting - Add WebSocket debug logging to track data flow Technical details: - CPR decoder now validates coordinates within ±90° latitude, ±180° longitude - Merger allows updates when currentBest == sourceID (same source continuous updates) - JavaScript modules provide better separation of concerns and debugging - WebSocket properly transmits updated aircraft coordinates to frontend 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
376 lines
No EOL
14 KiB
JavaScript
376 lines
No EOL
14 KiB
JavaScript
// 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 = `
|
|
<svg width="${size * 2}" height="${size * 2}" viewBox="0 0 32 32">
|
|
<g transform="translate(16,16)">
|
|
<path d="M0,-12 L-8,8 L-2,8 L0,12 L2,8 L8,8 Z"
|
|
fill="${color}"
|
|
stroke="#ffffff"
|
|
stroke-width="1"
|
|
filter="drop-shadow(0 0 4px rgba(0,212,255,0.8))"/>
|
|
</g>
|
|
</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 `
|
|
<div class="aircraft-popup">
|
|
<div class="popup-header">
|
|
<div class="flight-info">
|
|
<span class="icao-flag">${flag}</span>
|
|
<span class="flight-id">${aircraft.ICAO24 || 'N/A'}</span>
|
|
${aircraft.Callsign ? `→ <span class="callsign">${aircraft.Callsign}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="popup-details">
|
|
<div class="detail-row">
|
|
<strong>Country:</strong> ${country}
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>Type:</strong> ${type.charAt(0).toUpperCase() + type.slice(1)}
|
|
</div>
|
|
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<div class="label">Altitude:</div>
|
|
<div class="value">${altitude ? `${altitude} ft | ${altitudeM} m` : 'N/A'}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="label">Squawk:</div>
|
|
<div class="value">${aircraft.Squawk || 'N/A'}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="label">Speed:</div>
|
|
<div class="value">${aircraft.GroundSpeed ? `${aircraft.GroundSpeed} kt | ${speedKmh} km/h` : 'N/A'}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="label">Track:</div>
|
|
<div class="value">${aircraft.Track ? `${aircraft.Track}°` : 'N/A'}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="label">V/Rate:</div>
|
|
<div class="value">${aircraft.VerticalRate ? `${aircraft.VerticalRate} ft/min` : 'N/A'}</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<div class="label">Distance:</div>
|
|
<div class="value">${distanceKm} km</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-row">
|
|
<strong>Position:</strong> ${aircraft.Latitude?.toFixed(4)}°, ${aircraft.Longitude?.toFixed(4)}°
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>Messages:</strong> ${aircraft.TotalMessages || 0}
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>Age:</strong> ${aircraft.Age ? aircraft.Age.toFixed(1) : '0'}s
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
|
|
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}`);
|
|
}
|
|
} |