Enhance aircraft details display to match dump1090 format

- Add country lookup from ICAO hex codes with flag display
- Implement UTC clocks and last update time indicators
- Enhance aircraft table with ICAO, squawk, and RSSI columns
- Add color-coded RSSI signal strength indicators
- Fix calculateDistance returning string instead of number
- Accept all SBS-1 message types for complete data capture
- Improve data merging to preserve country and registration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-23 23:20:31 +02:00
commit c8562a4f0d
5 changed files with 457 additions and 41 deletions

View file

@ -11,6 +11,7 @@ class SkyView {
this.currentView = 'map';
this.charts = {};
this.origin = { latitude: 37.7749, longitude: -122.4194, name: 'Default' };
this.lastUpdateTime = new Date();
this.init();
}
@ -22,6 +23,7 @@ class SkyView {
this.initializeWebSocket();
this.initializeEventListeners();
this.initializeCharts();
this.initializeClocks();
this.startPeriodicUpdates();
});
@ -188,8 +190,36 @@ class SkyView {
});
}
initializeClocks() {
this.updateClocks();
setInterval(() => this.updateClocks(), 1000);
}
updateClocks() {
const now = new Date();
const utcNow = new Date(now.getTime() + (now.getTimezoneOffset() * 60000));
this.updateClock('utc', utcNow);
this.updateClock('update', this.lastUpdateTime);
}
updateClock(prefix, time) {
const hours = time.getUTCHours();
const minutes = time.getUTCMinutes();
const hourAngle = (hours % 12) * 30 + minutes * 0.5;
const minuteAngle = minutes * 6;
const hourHand = document.getElementById(`${prefix}-hour`);
const minuteHand = document.getElementById(`${prefix}-minute`);
if (hourHand) hourHand.style.transform = `rotate(${hourAngle}deg)`;
if (minuteHand) minuteHand.style.transform = `rotate(${minuteAngle}deg)`;
}
updateAircraftData(data) {
this.aircraftData = data.aircraft || [];
this.lastUpdateTime = new Date();
this.updateMapMarkers();
this.updateAircraftTable();
this.updateStats();
@ -341,18 +371,100 @@ class SkyView {
}
createPopupContent(aircraft) {
const type = this.getAircraftType(aircraft);
const distance = this.calculateDistance(aircraft);
const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A';
const altitudeM = aircraft.alt_baro ? Math.round(aircraft.alt_baro * 0.3048) : 'N/A';
const speedKmh = aircraft.gs ? Math.round(aircraft.gs * 1.852) : 'N/A';
const trackText = aircraft.track ? `${aircraft.track}° (${this.getTrackDirection(aircraft.track)})` : 'N/A';
return `
<div class="aircraft-popup">
<div class="flight">${aircraft.flight || aircraft.hex}</div>
<div class="details">
<div>Altitude:</div><div>${aircraft.alt_baro || 'N/A'} ft</div>
<div>Speed:</div><div>${aircraft.gs || 'N/A'} kts</div>
<div>Track:</div><div>${aircraft.track || 'N/A'}°</div>
<div>Squawk:</div><div>${aircraft.squawk || 'N/A'}</div>
<div class="popup-header">
<div class="flight-info">
<span class="icao-flag">${this.getCountryFlag(aircraft.country)}</span>
<span class="flight-id">${aircraft.hex}</span>
${aircraft.flight ? `→ <span class="callsign">${aircraft.flight}</span>` : ''}
</div>
</div>
<div class="popup-details">
<div class="detail-row">
<strong>Country of registration:</strong> ${aircraft.country || 'Unknown'}
</div>
<div class="detail-row">
<strong>Registration:</strong> ${aircraft.registration || aircraft.hex}
</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">${aircraft.alt_baro ? `${aircraft.alt_baro} 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.gs ? `${aircraft.gs} kt | ${speedKmh} km/h` : 'N/A'}</div>
</div>
<div class="detail-item">
<div class="label">RSSI:</div>
<div class="value">${aircraft.rssi ? `${aircraft.rssi.toFixed(1)} dBFS` : 'N/A'}</div>
</div>
<div class="detail-item">
<div class="label">Track:</div>
<div class="value">${trackText}</div>
</div>
<div class="detail-item">
<div class="label">Last seen:</div>
<div class="value">${aircraft.seen ? `${aircraft.seen.toFixed(1)}s ago` : 'now'}</div>
</div>
</div>
<div class="detail-row">
<strong>Position:</strong> ${aircraft.lat && aircraft.lon ?
`${aircraft.lat.toFixed(3)}°, ${aircraft.lon.toFixed(3)}°` : 'N/A'}
</div>
<div class="detail-row">
<strong>Distance from Site:</strong> ${distance ? `${distance} NM | ${distanceKm} km` : 'N/A'}
</div>
</div>
</div>
`;
}
getCountryFlag(country) {
const flags = {
'United States': '🇺🇸',
'United Kingdom': '🇬🇧',
'Germany': '🇩🇪',
'France': '🇫🇷',
'Netherlands': '🇳🇱',
'Sweden': '🇸🇪',
'Spain': '🇪🇸',
'Italy': '🇮🇹',
'Canada': '🇨🇦',
'Japan': '🇯🇵',
'Denmark': '🇩🇰',
'Austria': '🇦🇹',
'Belgium': '🇧🇪',
'Finland': '🇫🇮',
'Greece': '🇬🇷'
};
return flags[country] || '🏳️';
}
getTrackDirection(track) {
const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
const index = Math.round(track / 22.5) % 16;
return directions[index];
}
updatePopupContent(marker, aircraft) {
marker.setPopupContent(this.createPopupContent(aircraft));
@ -416,7 +528,8 @@ class SkyView {
if (searchTerm) {
filteredData = filteredData.filter(aircraft =>
(aircraft.flight && aircraft.flight.toLowerCase().includes(searchTerm)) ||
aircraft.hex.toLowerCase().includes(searchTerm)
aircraft.hex.toLowerCase().includes(searchTerm) ||
(aircraft.squawk && aircraft.squawk.includes(searchTerm))
);
}
@ -425,19 +538,39 @@ class SkyView {
filteredData.forEach(aircraft => {
const type = this.getAircraftType(aircraft);
const typeDisplay = type.charAt(0).toUpperCase() + type.slice(1);
const country = aircraft.country || 'Unknown';
const countryFlag = this.getCountryFlag(country);
const age = aircraft.seen ? aircraft.seen.toFixed(0) : '0';
const distance = this.calculateDistance(aircraft);
const distanceStr = distance ? distance.toFixed(1) : '-';
const altitudeStr = aircraft.alt_baro ?
(aircraft.alt_baro >= 0 ? `${aircraft.alt_baro}` : `${Math.abs(aircraft.alt_baro)}`) :
'-';
const row = document.createElement('tr');
// Color code RSSI values
let rssiStr = '-';
let rssiClass = '';
if (aircraft.rssi) {
const rssi = aircraft.rssi;
rssiStr = rssi.toFixed(1);
if (rssi > -10) rssiClass = 'rssi-strong';
else if (rssi > -20) rssiClass = 'rssi-good';
else if (rssi > -30) rssiClass = 'rssi-weak';
else rssiClass = 'rssi-poor';
}
row.innerHTML = `
<td><span class="type-badge ${type}">${aircraft.hex}</span></td>
<td>${aircraft.flight || '-'}</td>
<td>${aircraft.hex}</td>
<td><span class="type-badge ${type}">${typeDisplay}</span></td>
<td>${aircraft.alt_baro || '-'}</td>
<td>${aircraft.squawk || '-'}</td>
<td>${altitudeStr}</td>
<td>${aircraft.gs || '-'}</td>
<td>${aircraft.track || '-'}</td>
<td>${this.calculateDistance(aircraft) || '-'}</td>
<td>${distanceStr}</td>
<td>${aircraft.track || '-'}°</td>
<td>${aircraft.messages || '-'}</td>
<td>${aircraft.seen ? aircraft.seen.toFixed(1) : '-'}s</td>
<td>${age}</td>
<td><span class="${rssiClass}">${rssiStr}</span></td>
`;
row.addEventListener('click', () => {
@ -482,6 +615,14 @@ class SkyView {
return (b.gs || 0) - (a.gs || 0);
case 'flight':
return (a.flight || a.hex).localeCompare(b.flight || b.hex);
case 'icao':
return a.hex.localeCompare(b.hex);
case 'squawk':
return (a.squawk || '').localeCompare(b.squawk || '');
case 'age':
return (a.seen || 0) - (b.seen || 0);
case 'rssi':
return (b.rssi || -999) - (a.rssi || -999);
default:
return 0;
}
@ -501,7 +642,7 @@ class SkyView {
Math.cos(centerLat * Math.PI / 180) * Math.cos(aircraft.lat * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return (R * c).toFixed(1);
return R * c; // Return a number, not a string
}
updateStats() {
@ -512,11 +653,12 @@ class SkyView {
.filter(a => a.alt_baro)
.reduce((sum, a) => sum + a.alt_baro, 0) / this.aircraftData.length || 0;
const maxDistance = Math.max(...this.aircraftData
const distances = this.aircraftData
.map(a => this.calculateDistance(a))
.filter(d => d !== null)) || 0;
.filter(d => d !== null);
const maxDistance = distances.length > 0 ? Math.max(...distances) : 0;
document.getElementById('max-range').textContent = `${maxDistance} nm`;
document.getElementById('max-range').textContent = `${maxDistance.toFixed(1)} nm`;
this.updateChartData();
}
@ -589,9 +731,13 @@ class SkyView {
document.getElementById('messages-sec').textContent = Math.round(messagesPerSec);
}
if (stats.total && stats.total.signal_power) {
document.getElementById('signal-strength').textContent =
`${stats.total.signal_power.toFixed(1)} dB`;
// Calculate average RSSI from aircraft data
const aircraftWithRSSI = this.aircraftData.filter(a => a.rssi);
if (aircraftWithRSSI.length > 0) {
const avgRSSI = aircraftWithRSSI.reduce((sum, a) => sum + a.rssi, 0) / aircraftWithRSSI.length;
document.getElementById('signal-strength').textContent = `${avgRSSI.toFixed(1)} dBFS`;
} else {
document.getElementById('signal-strength').textContent = '0 dBFS';
}
} catch (error) {
console.error('Failed to fetch stats:', error);