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.Squawk = update.Squawk
|
||||||
}
|
}
|
||||||
existing.OnGround = update.OnGround
|
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 {
|
func (c *Dump1090Client) shouldAddTrackPoint(existing, update *parser.Aircraft) bool {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ type Aircraft struct {
|
||||||
LastSeen time.Time `json:"last_seen"`
|
LastSeen time.Time `json:"last_seen"`
|
||||||
Messages int `json:"messages"`
|
Messages int `json:"messages"`
|
||||||
TrackHistory []TrackPoint `json:"track_history,omitempty"`
|
TrackHistory []TrackPoint `json:"track_history,omitempty"`
|
||||||
|
RSSI float64 `json:"rssi,omitempty"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
Registration string `json:"registration,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AircraftData struct {
|
type AircraftData struct {
|
||||||
|
|
@ -44,10 +47,10 @@ func ParseSBS1Line(line string) (*Aircraft, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
messageType := parts[1]
|
// messageType := parts[1]
|
||||||
if messageType != "1" && messageType != "3" && messageType != "4" {
|
// Accept all message types to get complete data
|
||||||
return nil, nil
|
// 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{
|
aircraft := &Aircraft{
|
||||||
Hex: strings.TrimSpace(parts[4]),
|
Hex: strings.TrimSpace(parts[4]),
|
||||||
|
|
@ -55,6 +58,8 @@ func ParseSBS1Line(line string) (*Aircraft, error) {
|
||||||
Messages: 1,
|
Messages: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Different message types contain different fields
|
||||||
|
// Always try to extract what's available
|
||||||
if parts[10] != "" {
|
if parts[10] != "" {
|
||||||
aircraft.Flight = strings.TrimSpace(parts[10])
|
aircraft.Flight = strings.TrimSpace(parts[10])
|
||||||
}
|
}
|
||||||
|
|
@ -72,8 +77,8 @@ func ParseSBS1Line(line string) (*Aircraft, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if parts[13] != "" {
|
if parts[13] != "" {
|
||||||
if track, err := strconv.Atoi(parts[13]); err == nil {
|
if track, err := strconv.ParseFloat(parts[13], 64); err == nil {
|
||||||
aircraft.Track = track
|
aircraft.Track = int(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +105,121 @@ func ParseSBS1Line(line string) (*Aircraft, error) {
|
||||||
aircraft.OnGround = parts[21] == "1"
|
aircraft.OnGround = parts[21] == "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aircraft.Country = getCountryFromICAO(aircraft.Hex)
|
||||||
|
aircraft.Registration = getRegistrationFromICAO(aircraft.Hex)
|
||||||
|
|
||||||
return aircraft, nil
|
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;
|
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 {
|
.header h1 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: #00a8ff;
|
color: #00a8ff;
|
||||||
|
|
@ -227,6 +296,12 @@ body {
|
||||||
.type-badge.ga { background: #ffff00; }
|
.type-badge.ga { background: #ffff00; }
|
||||||
.type-badge.ground { background: #888888; color: #ffffff; }
|
.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 {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
|
@ -288,21 +363,65 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.aircraft-popup {
|
.aircraft-popup {
|
||||||
min-width: 200px;
|
min-width: 300px;
|
||||||
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aircraft-popup .flight {
|
.popup-header {
|
||||||
font-weight: bold;
|
border-bottom: 1px solid #404040;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-info {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
margin-bottom: 0.5rem;
|
font-weight: bold;
|
||||||
color: #00a8ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 0.25rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.9rem;
|
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) {
|
@media (max-width: 768px) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,24 @@
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>SkyView</h1>
|
<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">
|
<div class="stats-summary">
|
||||||
<span id="aircraft-count">0 aircraft</span>
|
<span id="aircraft-count">0 aircraft</span>
|
||||||
<span id="connection-status" class="connected">Connected</span>
|
<span id="connection-status" class="connected">Connected</span>
|
||||||
|
|
@ -60,27 +78,32 @@
|
||||||
|
|
||||||
<div id="table-view" class="view">
|
<div id="table-view" class="view">
|
||||||
<div class="table-controls">
|
<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">
|
<select id="sort-select">
|
||||||
<option value="distance">Distance</option>
|
<option value="distance">Distance</option>
|
||||||
<option value="altitude">Altitude</option>
|
<option value="altitude">Altitude</option>
|
||||||
<option value="speed">Speed</option>
|
<option value="speed">Speed</option>
|
||||||
<option value="flight">Flight</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table id="aircraft-table">
|
<table id="aircraft-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>ICAO</th>
|
||||||
<th>Flight</th>
|
<th>Flight</th>
|
||||||
<th>Hex</th>
|
<th>Squawk</th>
|
||||||
<th>Type</th>
|
|
||||||
<th>Altitude</th>
|
<th>Altitude</th>
|
||||||
<th>Speed</th>
|
<th>Speed</th>
|
||||||
<th>Track</th>
|
|
||||||
<th>Distance</th>
|
<th>Distance</th>
|
||||||
|
<th>Track</th>
|
||||||
<th>Msgs</th>
|
<th>Msgs</th>
|
||||||
<th>Seen</th>
|
<th>Age</th>
|
||||||
|
<th>RSSI</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="aircraft-tbody">
|
<tbody id="aircraft-tbody">
|
||||||
|
|
@ -100,8 +123,8 @@
|
||||||
<div class="stat-value" id="messages-sec">0</div>
|
<div class="stat-value" id="messages-sec">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<h3>Signal Strength</h3>
|
<h3>Avg RSSI</h3>
|
||||||
<div class="stat-value" id="signal-strength">0 dB</div>
|
<div class="stat-value" id="signal-strength">0 dBFS</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<h3>Max Range</h3>
|
<h3>Max Range</h3>
|
||||||
|
|
|
||||||
188
static/js/app.js
188
static/js/app.js
|
|
@ -11,6 +11,7 @@ class SkyView {
|
||||||
this.currentView = 'map';
|
this.currentView = 'map';
|
||||||
this.charts = {};
|
this.charts = {};
|
||||||
this.origin = { latitude: 37.7749, longitude: -122.4194, name: 'Default' };
|
this.origin = { latitude: 37.7749, longitude: -122.4194, name: 'Default' };
|
||||||
|
this.lastUpdateTime = new Date();
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +23,7 @@ class SkyView {
|
||||||
this.initializeWebSocket();
|
this.initializeWebSocket();
|
||||||
this.initializeEventListeners();
|
this.initializeEventListeners();
|
||||||
this.initializeCharts();
|
this.initializeCharts();
|
||||||
|
this.initializeClocks();
|
||||||
|
|
||||||
this.startPeriodicUpdates();
|
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) {
|
updateAircraftData(data) {
|
||||||
this.aircraftData = data.aircraft || [];
|
this.aircraftData = data.aircraft || [];
|
||||||
|
this.lastUpdateTime = new Date();
|
||||||
this.updateMapMarkers();
|
this.updateMapMarkers();
|
||||||
this.updateAircraftTable();
|
this.updateAircraftTable();
|
||||||
this.updateStats();
|
this.updateStats();
|
||||||
|
|
@ -341,18 +371,100 @@ class SkyView {
|
||||||
}
|
}
|
||||||
|
|
||||||
createPopupContent(aircraft) {
|
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 `
|
return `
|
||||||
<div class="aircraft-popup">
|
<div class="aircraft-popup">
|
||||||
<div class="flight">${aircraft.flight || aircraft.hex}</div>
|
<div class="popup-header">
|
||||||
<div class="details">
|
<div class="flight-info">
|
||||||
<div>Altitude:</div><div>${aircraft.alt_baro || 'N/A'} ft</div>
|
<span class="icao-flag">${this.getCountryFlag(aircraft.country)}</span>
|
||||||
<div>Speed:</div><div>${aircraft.gs || 'N/A'} kts</div>
|
<span class="flight-id">${aircraft.hex}</span>
|
||||||
<div>Track:</div><div>${aircraft.track || 'N/A'}°</div>
|
${aircraft.flight ? `→ <span class="callsign">${aircraft.flight}</span>` : ''}
|
||||||
<div>Squawk:</div><div>${aircraft.squawk || 'N/A'}</div>
|
</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>
|
||||||
</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) {
|
updatePopupContent(marker, aircraft) {
|
||||||
marker.setPopupContent(this.createPopupContent(aircraft));
|
marker.setPopupContent(this.createPopupContent(aircraft));
|
||||||
|
|
@ -416,7 +528,8 @@ class SkyView {
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
filteredData = filteredData.filter(aircraft =>
|
filteredData = filteredData.filter(aircraft =>
|
||||||
(aircraft.flight && aircraft.flight.toLowerCase().includes(searchTerm)) ||
|
(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 => {
|
filteredData.forEach(aircraft => {
|
||||||
const type = this.getAircraftType(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');
|
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 = `
|
row.innerHTML = `
|
||||||
|
<td><span class="type-badge ${type}">${aircraft.hex}</span></td>
|
||||||
<td>${aircraft.flight || '-'}</td>
|
<td>${aircraft.flight || '-'}</td>
|
||||||
<td>${aircraft.hex}</td>
|
<td>${aircraft.squawk || '-'}</td>
|
||||||
<td><span class="type-badge ${type}">${typeDisplay}</span></td>
|
<td>${altitudeStr}</td>
|
||||||
<td>${aircraft.alt_baro || '-'}</td>
|
|
||||||
<td>${aircraft.gs || '-'}</td>
|
<td>${aircraft.gs || '-'}</td>
|
||||||
<td>${aircraft.track || '-'}</td>
|
<td>${distanceStr}</td>
|
||||||
<td>${this.calculateDistance(aircraft) || '-'}</td>
|
<td>${aircraft.track || '-'}°</td>
|
||||||
<td>${aircraft.messages || '-'}</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', () => {
|
row.addEventListener('click', () => {
|
||||||
|
|
@ -482,6 +615,14 @@ class SkyView {
|
||||||
return (b.gs || 0) - (a.gs || 0);
|
return (b.gs || 0) - (a.gs || 0);
|
||||||
case 'flight':
|
case 'flight':
|
||||||
return (a.flight || a.hex).localeCompare(b.flight || b.hex);
|
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:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -501,7 +642,7 @@ class SkyView {
|
||||||
Math.cos(centerLat * Math.PI / 180) * Math.cos(aircraft.lat * Math.PI / 180) *
|
Math.cos(centerLat * Math.PI / 180) * Math.cos(aircraft.lat * Math.PI / 180) *
|
||||||
Math.sin(dLon/2) * Math.sin(dLon/2);
|
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
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() {
|
updateStats() {
|
||||||
|
|
@ -512,11 +653,12 @@ class SkyView {
|
||||||
.filter(a => a.alt_baro)
|
.filter(a => a.alt_baro)
|
||||||
.reduce((sum, a) => sum + a.alt_baro, 0) / this.aircraftData.length || 0;
|
.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))
|
.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();
|
this.updateChartData();
|
||||||
}
|
}
|
||||||
|
|
@ -589,9 +731,13 @@ class SkyView {
|
||||||
document.getElementById('messages-sec').textContent = Math.round(messagesPerSec);
|
document.getElementById('messages-sec').textContent = Math.round(messagesPerSec);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stats.total && stats.total.signal_power) {
|
// Calculate average RSSI from aircraft data
|
||||||
document.getElementById('signal-strength').textContent =
|
const aircraftWithRSSI = this.aircraftData.filter(a => a.rssi);
|
||||||
`${stats.total.signal_power.toFixed(1)} dB`;
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch stats:', error);
|
console.error('Failed to fetch stats:', error);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue