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
|
|
@ -136,6 +136,14 @@ func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraf
|
|||
existing.Squawk = update.Squawk
|
||||
}
|
||||
existing.OnGround = update.OnGround
|
||||
|
||||
// Preserve country and registration
|
||||
if update.Country != "" && update.Country != "Unknown" {
|
||||
existing.Country = update.Country
|
||||
}
|
||||
if update.Registration != "" {
|
||||
existing.Registration = update.Registration
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) shouldAddTrackPoint(existing, update *parser.Aircraft) bool {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ type Aircraft struct {
|
|||
LastSeen time.Time `json:"last_seen"`
|
||||
Messages int `json:"messages"`
|
||||
TrackHistory []TrackPoint `json:"track_history,omitempty"`
|
||||
RSSI float64 `json:"rssi,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Registration string `json:"registration,omitempty"`
|
||||
}
|
||||
|
||||
type AircraftData struct {
|
||||
|
|
@ -44,10 +47,10 @@ func ParseSBS1Line(line string) (*Aircraft, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
messageType := parts[1]
|
||||
if messageType != "1" && messageType != "3" && messageType != "4" {
|
||||
return nil, nil
|
||||
}
|
||||
// messageType := parts[1]
|
||||
// Accept all message types to get complete data
|
||||
// MSG types: 1=ES_IDENT_AND_CATEGORY, 2=ES_SURFACE_POS, 3=ES_AIRBORNE_POS
|
||||
// 4=ES_AIRBORNE_VEL, 5=SURVEILLANCE_ALT, 6=SURVEILLANCE_ID, 7=AIR_TO_AIR, 8=ALL_CALL_REPLY
|
||||
|
||||
aircraft := &Aircraft{
|
||||
Hex: strings.TrimSpace(parts[4]),
|
||||
|
|
@ -55,6 +58,8 @@ func ParseSBS1Line(line string) (*Aircraft, error) {
|
|||
Messages: 1,
|
||||
}
|
||||
|
||||
// Different message types contain different fields
|
||||
// Always try to extract what's available
|
||||
if parts[10] != "" {
|
||||
aircraft.Flight = strings.TrimSpace(parts[10])
|
||||
}
|
||||
|
|
@ -72,8 +77,8 @@ func ParseSBS1Line(line string) (*Aircraft, error) {
|
|||
}
|
||||
|
||||
if parts[13] != "" {
|
||||
if track, err := strconv.Atoi(parts[13]); err == nil {
|
||||
aircraft.Track = track
|
||||
if track, err := strconv.ParseFloat(parts[13], 64); err == nil {
|
||||
aircraft.Track = int(track)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +105,121 @@ func ParseSBS1Line(line string) (*Aircraft, error) {
|
|||
aircraft.OnGround = parts[21] == "1"
|
||||
}
|
||||
|
||||
aircraft.Country = getCountryFromICAO(aircraft.Hex)
|
||||
aircraft.Registration = getRegistrationFromICAO(aircraft.Hex)
|
||||
|
||||
return aircraft, nil
|
||||
}
|
||||
|
||||
func getCountryFromICAO(icao string) string {
|
||||
if len(icao) < 6 {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
prefix := icao[:1]
|
||||
|
||||
switch prefix {
|
||||
case "4":
|
||||
return getCountryFrom4xxxx(icao)
|
||||
case "A":
|
||||
return "United States"
|
||||
case "C":
|
||||
return "Canada"
|
||||
case "D":
|
||||
return "Germany"
|
||||
case "F":
|
||||
return "France"
|
||||
case "G":
|
||||
return "United Kingdom"
|
||||
case "I":
|
||||
return "Italy"
|
||||
case "J":
|
||||
return "Japan"
|
||||
case "P":
|
||||
return getPCountry(icao)
|
||||
case "S":
|
||||
return getSCountry(icao)
|
||||
case "O":
|
||||
return getOCountry(icao)
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func getCountryFrom4xxxx(icao string) string {
|
||||
if len(icao) >= 2 {
|
||||
switch icao[:2] {
|
||||
case "40":
|
||||
return "United Kingdom"
|
||||
case "44":
|
||||
return "Austria"
|
||||
case "45":
|
||||
return "Denmark"
|
||||
case "46":
|
||||
return "Germany"
|
||||
case "47":
|
||||
return "Germany"
|
||||
case "48":
|
||||
return "Netherlands"
|
||||
case "49":
|
||||
return "Netherlands"
|
||||
}
|
||||
}
|
||||
return "Europe"
|
||||
}
|
||||
|
||||
func getPCountry(icao string) string {
|
||||
if len(icao) >= 2 {
|
||||
switch icao[:2] {
|
||||
case "PH":
|
||||
return "Netherlands"
|
||||
case "PJ":
|
||||
return "Netherlands Antilles"
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func getSCountry(icao string) string {
|
||||
if len(icao) >= 2 {
|
||||
switch icao[:2] {
|
||||
case "SE":
|
||||
return "Sweden"
|
||||
case "SX":
|
||||
return "Greece"
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func getOCountry(icao string) string {
|
||||
if len(icao) >= 2 {
|
||||
switch icao[:2] {
|
||||
case "OO":
|
||||
return "Belgium"
|
||||
case "OH":
|
||||
return "Finland"
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func getRegistrationFromICAO(icao string) string {
|
||||
// This is a simplified conversion - real registration lookup would need a database
|
||||
country := getCountryFromICAO(icao)
|
||||
switch country {
|
||||
case "Germany":
|
||||
return "D-" + icao[2:]
|
||||
case "United Kingdom":
|
||||
return "G-" + icao[2:]
|
||||
case "France":
|
||||
return "F-" + icao[2:]
|
||||
case "Netherlands":
|
||||
return "PH-" + icao[2:]
|
||||
case "Sweden":
|
||||
return "SE-" + icao[2:]
|
||||
default:
|
||||
return icao
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,75 @@ body {
|
|||
border-bottom: 1px solid #404040;
|
||||
}
|
||||
|
||||
.clock-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clock-display {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.clock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.clock-face {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 2px solid #00a8ff;
|
||||
border-radius: 50%;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.clock-face::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: #00a8ff;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.clock-hand {
|
||||
position: absolute;
|
||||
background: #00a8ff;
|
||||
transform-origin: bottom center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.hour-hand {
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
margin-left: -1.5px;
|
||||
}
|
||||
|
||||
.minute-hand {
|
||||
width: 2px;
|
||||
height: 25px;
|
||||
top: 5px;
|
||||
left: 50%;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.clock-label {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
color: #00a8ff;
|
||||
|
|
@ -227,6 +296,12 @@ body {
|
|||
.type-badge.ga { background: #ffff00; }
|
||||
.type-badge.ground { background: #888888; color: #ffffff; }
|
||||
|
||||
/* RSSI signal strength colors */
|
||||
.rssi-strong { color: #00ff88; }
|
||||
.rssi-good { color: #ffff00; }
|
||||
.rssi-weak { color: #ff8c00; }
|
||||
.rssi-poor { color: #ff4444; }
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
|
|
@ -288,21 +363,65 @@ body {
|
|||
}
|
||||
|
||||
.aircraft-popup {
|
||||
min-width: 200px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.aircraft-popup .flight {
|
||||
font-weight: bold;
|
||||
.popup-header {
|
||||
border-bottom: 1px solid #404040;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.flight-info {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #00a8ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.aircraft-popup .details {
|
||||
.icao-flag {
|
||||
font-size: 1.2rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.flight-id {
|
||||
color: #00a8ff;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.callsign {
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.popup-details {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
gap: 0.5rem;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,24 @@
|
|||
<div id="app">
|
||||
<header class="header">
|
||||
<h1>SkyView</h1>
|
||||
<div class="clock-section">
|
||||
<div class="clock-display">
|
||||
<div class="clock" id="utc-clock">
|
||||
<div class="clock-face">
|
||||
<div class="clock-hand hour-hand" id="utc-hour"></div>
|
||||
<div class="clock-hand minute-hand" id="utc-minute"></div>
|
||||
</div>
|
||||
<div class="clock-label">UTC</div>
|
||||
</div>
|
||||
<div class="clock" id="update-clock">
|
||||
<div class="clock-face">
|
||||
<div class="clock-hand hour-hand" id="update-hour"></div>
|
||||
<div class="clock-hand minute-hand" id="update-minute"></div>
|
||||
</div>
|
||||
<div class="clock-label">Last Update</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-summary">
|
||||
<span id="aircraft-count">0 aircraft</span>
|
||||
<span id="connection-status" class="connected">Connected</span>
|
||||
|
|
@ -60,27 +78,32 @@
|
|||
|
||||
<div id="table-view" class="view">
|
||||
<div class="table-controls">
|
||||
<input type="text" id="search-input" placeholder="Search by flight, callsign, or hex...">
|
||||
<input type="text" id="search-input" placeholder="Search by flight, ICAO, or squawk...">
|
||||
<select id="sort-select">
|
||||
<option value="distance">Distance</option>
|
||||
<option value="altitude">Altitude</option>
|
||||
<option value="speed">Speed</option>
|
||||
<option value="flight">Flight</option>
|
||||
<option value="icao">ICAO</option>
|
||||
<option value="squawk">Squawk</option>
|
||||
<option value="age">Age</option>
|
||||
<option value="rssi">RSSI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table id="aircraft-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ICAO</th>
|
||||
<th>Flight</th>
|
||||
<th>Hex</th>
|
||||
<th>Type</th>
|
||||
<th>Squawk</th>
|
||||
<th>Altitude</th>
|
||||
<th>Speed</th>
|
||||
<th>Track</th>
|
||||
<th>Distance</th>
|
||||
<th>Track</th>
|
||||
<th>Msgs</th>
|
||||
<th>Seen</th>
|
||||
<th>Age</th>
|
||||
<th>RSSI</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="aircraft-tbody">
|
||||
|
|
@ -100,8 +123,8 @@
|
|||
<div class="stat-value" id="messages-sec">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Signal Strength</h3>
|
||||
<div class="stat-value" id="signal-strength">0 dB</div>
|
||||
<h3>Avg RSSI</h3>
|
||||
<div class="stat-value" id="signal-strength">0 dBFS</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Max Range</h3>
|
||||
|
|
|
|||
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,19 +371,101 @@ 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