Restructure assets to top-level package and add Reset Map button
- Move assets from internal/assets to top-level assets/ package for clean embed directive - Consolidate all static files in single location (assets/static/) - Remove duplicate static file locations to maintain single source of truth - Add Reset Map button to map controls with full functionality - Implement resetMap() method to return map to calculated origin position - Store origin in this.mapOrigin for reset functionality - Fix go:embed pattern to work without parent directory references 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
af9bf8ecac
commit
1425f0a018
20 changed files with 263 additions and 2139 deletions
|
|
@ -4,7 +4,7 @@ import (
|
|||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
||||
"skyview/internal/modes"
|
||||
)
|
||||
|
||||
|
|
@ -26,31 +26,31 @@ type Source struct {
|
|||
// AircraftState represents merged aircraft state from all sources
|
||||
type AircraftState struct {
|
||||
*modes.Aircraft
|
||||
Sources map[string]*SourceData `json:"sources"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
TotalMessages int64 `json:"total_messages"`
|
||||
PositionHistory []PositionPoint `json:"position_history"`
|
||||
SignalHistory []SignalPoint `json:"signal_history"`
|
||||
AltitudeHistory []AltitudePoint `json:"altitude_history"`
|
||||
SpeedHistory []SpeedPoint `json:"speed_history"`
|
||||
Distance float64 `json:"distance"` // Distance from closest receiver
|
||||
Bearing float64 `json:"bearing"` // Bearing from closest receiver
|
||||
Age float64 `json:"age"` // Seconds since last update
|
||||
MLATSources []string `json:"mlat_sources"` // Sources providing MLAT data
|
||||
PositionSource string `json:"position_source"` // Source providing current position
|
||||
UpdateRate float64 `json:"update_rate"` // Updates per second
|
||||
Sources map[string]*SourceData `json:"sources"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
TotalMessages int64 `json:"total_messages"`
|
||||
PositionHistory []PositionPoint `json:"position_history"`
|
||||
SignalHistory []SignalPoint `json:"signal_history"`
|
||||
AltitudeHistory []AltitudePoint `json:"altitude_history"`
|
||||
SpeedHistory []SpeedPoint `json:"speed_history"`
|
||||
Distance float64 `json:"distance"` // Distance from closest receiver
|
||||
Bearing float64 `json:"bearing"` // Bearing from closest receiver
|
||||
Age float64 `json:"age"` // Seconds since last update
|
||||
MLATSources []string `json:"mlat_sources"` // Sources providing MLAT data
|
||||
PositionSource string `json:"position_source"` // Source providing current position
|
||||
UpdateRate float64 `json:"update_rate"` // Updates per second
|
||||
}
|
||||
|
||||
// SourceData represents data from a specific source
|
||||
type SourceData struct {
|
||||
SourceID string `json:"source_id"`
|
||||
SignalLevel float64 `json:"signal_level"`
|
||||
Messages int64 `json:"messages"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Distance float64 `json:"distance"`
|
||||
Bearing float64 `json:"bearing"`
|
||||
UpdateRate float64 `json:"update_rate"`
|
||||
SourceID string `json:"source_id"`
|
||||
SignalLevel float64 `json:"signal_level"`
|
||||
Messages int64 `json:"messages"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Distance float64 `json:"distance"`
|
||||
Bearing float64 `json:"bearing"`
|
||||
UpdateRate float64 `json:"update_rate"`
|
||||
}
|
||||
|
||||
// Position/Signal/Altitude/Speed history points
|
||||
|
|
@ -116,7 +116,7 @@ func (m *Merger) AddSource(source *Source) {
|
|||
func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signal float64, timestamp time.Time) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
|
||||
// Get or create aircraft state
|
||||
state, exists := m.aircraft[aircraft.ICAO24]
|
||||
if !exists {
|
||||
|
|
@ -134,7 +134,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
|||
updates: make([]time.Time, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update or create source data
|
||||
srcData, srcExists := state.Sources[sourceID]
|
||||
if !srcExists {
|
||||
|
|
@ -143,12 +143,12 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
|||
}
|
||||
state.Sources[sourceID] = srcData
|
||||
}
|
||||
|
||||
|
||||
// Update source data
|
||||
srcData.SignalLevel = signal
|
||||
srcData.Messages++
|
||||
srcData.LastSeen = timestamp
|
||||
|
||||
|
||||
// Calculate distance and bearing from source
|
||||
if source, ok := m.sources[sourceID]; ok && aircraft.Latitude != 0 && aircraft.Longitude != 0 {
|
||||
srcData.Distance, srcData.Bearing = calculateDistanceBearing(
|
||||
|
|
@ -156,23 +156,23 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
|||
aircraft.Latitude, aircraft.Longitude,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Update merged aircraft data (use best/newest data)
|
||||
m.mergeAircraftData(state, aircraft, sourceID, timestamp)
|
||||
|
||||
|
||||
// Update histories
|
||||
m.updateHistories(state, aircraft, sourceID, signal, timestamp)
|
||||
|
||||
|
||||
// Update metrics
|
||||
m.updateUpdateRate(aircraft.ICAO24, timestamp)
|
||||
|
||||
|
||||
// Update source statistics
|
||||
if source, ok := m.sources[sourceID]; ok {
|
||||
source.LastSeen = timestamp
|
||||
source.Messages++
|
||||
source.Active = true
|
||||
}
|
||||
|
||||
|
||||
state.LastUpdate = timestamp
|
||||
state.TotalMessages++
|
||||
}
|
||||
|
|
@ -182,7 +182,7 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
// Position - use source with best signal or most recent
|
||||
if new.Latitude != 0 && new.Longitude != 0 {
|
||||
updatePosition := false
|
||||
|
||||
|
||||
if state.Latitude == 0 {
|
||||
updatePosition = true
|
||||
} else if srcData, ok := state.Sources[sourceID]; ok {
|
||||
|
|
@ -192,14 +192,14 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
updatePosition = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if updatePosition {
|
||||
state.Latitude = new.Latitude
|
||||
state.Longitude = new.Longitude
|
||||
state.PositionSource = sourceID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Altitude - use most recent
|
||||
if new.Altitude != 0 {
|
||||
state.Altitude = new.Altitude
|
||||
|
|
@ -210,7 +210,7 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
if new.GeomAltitude != 0 {
|
||||
state.GeomAltitude = new.GeomAltitude
|
||||
}
|
||||
|
||||
|
||||
// Speed and track - use most recent
|
||||
if new.GroundSpeed != 0 {
|
||||
state.GroundSpeed = new.GroundSpeed
|
||||
|
|
@ -221,12 +221,12 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
if new.Heading != 0 {
|
||||
state.Heading = new.Heading
|
||||
}
|
||||
|
||||
|
||||
// Vertical rate - use most recent
|
||||
if new.VerticalRate != 0 {
|
||||
state.VerticalRate = new.VerticalRate
|
||||
}
|
||||
|
||||
|
||||
// Identity - use most recent non-empty
|
||||
if new.Callsign != "" {
|
||||
state.Callsign = new.Callsign
|
||||
|
|
@ -237,7 +237,7 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
if new.Category != "" {
|
||||
state.Category = new.Category
|
||||
}
|
||||
|
||||
|
||||
// Status - use most recent
|
||||
if new.Emergency != "" {
|
||||
state.Emergency = new.Emergency
|
||||
|
|
@ -245,7 +245,7 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
state.OnGround = new.OnGround
|
||||
state.Alert = new.Alert
|
||||
state.SPI = new.SPI
|
||||
|
||||
|
||||
// Navigation accuracy - use best available
|
||||
if new.NACp > state.NACp {
|
||||
state.NACp = new.NACp
|
||||
|
|
@ -256,7 +256,7 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
if new.SIL > state.SIL {
|
||||
state.SIL = new.SIL
|
||||
}
|
||||
|
||||
|
||||
// Selected values - use most recent
|
||||
if new.SelectedAltitude != 0 {
|
||||
state.SelectedAltitude = new.SelectedAltitude
|
||||
|
|
@ -280,7 +280,7 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft,
|
|||
Source: sourceID,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Signal history
|
||||
if signal != 0 {
|
||||
state.SignalHistory = append(state.SignalHistory, SignalPoint{
|
||||
|
|
@ -289,7 +289,7 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft,
|
|||
Source: sourceID,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Altitude history
|
||||
if aircraft.Altitude != 0 {
|
||||
state.AltitudeHistory = append(state.AltitudeHistory, AltitudePoint{
|
||||
|
|
@ -298,7 +298,7 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft,
|
|||
VRate: aircraft.VerticalRate,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Speed history
|
||||
if aircraft.GroundSpeed != 0 {
|
||||
state.SpeedHistory = append(state.SpeedHistory, SpeedPoint{
|
||||
|
|
@ -307,7 +307,7 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft,
|
|||
Track: aircraft.Track,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Trim histories if they exceed limit
|
||||
if len(state.PositionHistory) > m.historyLimit {
|
||||
state.PositionHistory = state.PositionHistory[len(state.PositionHistory)-m.historyLimit:]
|
||||
|
|
@ -327,13 +327,13 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft,
|
|||
func (m *Merger) updateUpdateRate(icao uint32, timestamp time.Time) {
|
||||
metric := m.updateMetrics[icao]
|
||||
metric.updates = append(metric.updates, timestamp)
|
||||
|
||||
|
||||
// Keep only last 30 seconds of updates
|
||||
cutoff := timestamp.Add(-30 * time.Second)
|
||||
for len(metric.updates) > 0 && metric.updates[0].Before(cutoff) {
|
||||
metric.updates = metric.updates[1:]
|
||||
}
|
||||
|
||||
|
||||
if len(metric.updates) > 1 {
|
||||
duration := metric.updates[len(metric.updates)-1].Sub(metric.updates[0]).Seconds()
|
||||
if duration > 0 {
|
||||
|
|
@ -348,14 +348,14 @@ func (m *Merger) updateUpdateRate(icao uint32, timestamp time.Time) {
|
|||
func (m *Merger) getBestSignalSource(state *AircraftState) string {
|
||||
var bestSource string
|
||||
var bestSignal float64 = -999
|
||||
|
||||
|
||||
for srcID, srcData := range state.Sources {
|
||||
if srcData.SignalLevel > bestSignal {
|
||||
bestSignal = srcData.SignalLevel
|
||||
bestSource = srcID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return bestSource
|
||||
}
|
||||
|
||||
|
|
@ -363,21 +363,21 @@ func (m *Merger) getBestSignalSource(state *AircraftState) string {
|
|||
func (m *Merger) GetAircraft() map[uint32]*AircraftState {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
|
||||
// Create copy and calculate ages
|
||||
result := make(map[uint32]*AircraftState)
|
||||
now := time.Now()
|
||||
|
||||
|
||||
for icao, state := range m.aircraft {
|
||||
// Skip stale aircraft
|
||||
if now.Sub(state.LastUpdate) > m.staleTimeout {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Calculate age
|
||||
stateCopy := *state
|
||||
stateCopy.Age = now.Sub(state.LastUpdate).Seconds()
|
||||
|
||||
|
||||
// Find closest receiver distance
|
||||
minDistance := float64(999999)
|
||||
for _, srcData := range state.Sources {
|
||||
|
|
@ -387,10 +387,10 @@ func (m *Merger) GetAircraft() map[uint32]*AircraftState {
|
|||
stateCopy.Bearing = srcData.Bearing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result[icao] = &stateCopy
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -398,7 +398,7 @@ func (m *Merger) GetAircraft() map[uint32]*AircraftState {
|
|||
func (m *Merger) GetSources() []*Source {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
|
||||
sources := make([]*Source, 0, len(m.sources))
|
||||
for _, src := range m.sources {
|
||||
sources = append(sources, src)
|
||||
|
|
@ -410,23 +410,23 @@ func (m *Merger) GetSources() []*Source {
|
|||
func (m *Merger) GetStatistics() map[string]interface{} {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
|
||||
totalMessages := int64(0)
|
||||
activeSources := 0
|
||||
aircraftBySources := make(map[int]int) // Count by number of sources
|
||||
|
||||
|
||||
for _, state := range m.aircraft {
|
||||
totalMessages += state.TotalMessages
|
||||
numSources := len(state.Sources)
|
||||
aircraftBySources[numSources]++
|
||||
}
|
||||
|
||||
|
||||
for _, src := range m.sources {
|
||||
if src.Active {
|
||||
activeSources++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_aircraft": len(m.aircraft),
|
||||
"total_messages": totalMessages,
|
||||
|
|
@ -439,7 +439,7 @@ func (m *Merger) GetStatistics() map[string]interface{} {
|
|||
func (m *Merger) CleanupStale() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
|
||||
now := time.Now()
|
||||
for icao, state := range m.aircraft {
|
||||
if now.Sub(state.LastUpdate) > m.staleTimeout {
|
||||
|
|
@ -454,26 +454,26 @@ func (m *Merger) CleanupStale() {
|
|||
func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64) {
|
||||
// Haversine formula for distance
|
||||
const R = 6371.0 // Earth radius in km
|
||||
|
||||
|
||||
dLat := (lat2 - lat1) * math.Pi / 180
|
||||
dLon := (lon2 - lon1) * math.Pi / 180
|
||||
|
||||
|
||||
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*
|
||||
math.Sin(dLon/2)*math.Sin(dLon/2)
|
||||
|
||||
|
||||
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
distance := R * c
|
||||
|
||||
|
||||
// Bearing calculation
|
||||
y := math.Sin(dLon) * math.Cos(lat2*math.Pi/180)
|
||||
x := math.Cos(lat1*math.Pi/180)*math.Sin(lat2*math.Pi/180) -
|
||||
math.Sin(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*math.Cos(dLon)
|
||||
|
||||
|
||||
bearing := math.Atan2(y, x) * 180 / math.Pi
|
||||
if bearing < 0 {
|
||||
bearing += 360
|
||||
}
|
||||
|
||||
|
||||
return distance, bearing
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue