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:
parent
55710614da
commit
c8562a4f0d
5 changed files with 457 additions and 41 deletions
188
static/js/app.js
188
static/js/app.js
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue