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:
Ole-Morten Duesund 2025-08-24 00:57:49 +02:00
commit 1425f0a018
20 changed files with 263 additions and 2139 deletions

View file

@ -6,7 +6,7 @@ import (
"net"
"sync"
"time"
"skyview/internal/beast"
"skyview/internal/merger"
"skyview/internal/modes"
@ -23,7 +23,7 @@ type BeastClient struct {
errChan chan error
stopChan chan struct{}
wg sync.WaitGroup
reconnectDelay time.Duration
maxReconnect time.Duration
}
@ -60,9 +60,9 @@ func (c *BeastClient) Stop() {
// run is the main client loop
func (c *BeastClient) run(ctx context.Context) {
defer c.wg.Done()
reconnectDelay := c.reconnectDelay
for {
select {
case <-ctx.Done():
@ -71,16 +71,16 @@ func (c *BeastClient) run(ctx context.Context) {
return
default:
}
// Connect to Beast TCP stream
addr := fmt.Sprintf("%s:%d", c.source.Host, c.source.Port)
fmt.Printf("Connecting to Beast stream at %s (%s)...\n", addr, c.source.Name)
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
fmt.Printf("Failed to connect to %s: %v\n", c.source.Name, err)
c.source.Active = false
// Exponential backoff
time.Sleep(reconnectDelay)
if reconnectDelay < c.maxReconnect {
@ -88,21 +88,21 @@ func (c *BeastClient) run(ctx context.Context) {
}
continue
}
c.conn = conn
c.source.Active = true
reconnectDelay = c.reconnectDelay // Reset backoff
fmt.Printf("Connected to %s at %s\n", c.source.Name, addr)
// Create parser for this connection
c.parser = beast.NewParser(conn, c.source.ID)
// Start processing messages
c.wg.Add(2)
go c.readMessages()
go c.processMessages()
// Wait for disconnect
select {
case <-ctx.Done():
@ -116,7 +116,7 @@ func (c *BeastClient) run(ctx context.Context) {
c.conn.Close()
c.source.Active = false
}
// Wait for goroutines to finish
time.Sleep(1 * time.Second)
}
@ -131,7 +131,7 @@ func (c *BeastClient) readMessages() {
// processMessages decodes and merges aircraft data
func (c *BeastClient) processMessages() {
defer c.wg.Done()
for {
select {
case <-c.stopChan:
@ -140,13 +140,13 @@ func (c *BeastClient) processMessages() {
if msg == nil {
return
}
// Decode Mode S message
aircraft, err := c.decoder.Decode(msg.Data)
if err != nil {
continue // Skip invalid messages
}
// Update merger with new data
c.merger.UpdateAircraft(
c.source.ID,
@ -154,7 +154,7 @@ func (c *BeastClient) processMessages() {
msg.GetSignalStrength(),
msg.ReceivedAt,
)
// Update source statistics
c.source.Messages++
}
@ -180,10 +180,10 @@ func NewMultiSourceClient(merger *merger.Merger) *MultiSourceClient {
func (m *MultiSourceClient) AddSource(source *merger.Source) {
m.mu.Lock()
defer m.mu.Unlock()
// Register source with merger
m.merger.AddSource(source)
// Create and start client
client := NewBeastClient(source, m.merger)
m.clients = append(m.clients, client)
@ -193,11 +193,11 @@ func (m *MultiSourceClient) AddSource(source *merger.Source) {
func (m *MultiSourceClient) Start(ctx context.Context) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, client := range m.clients {
client.Start(ctx)
}
// Start cleanup routine
go m.cleanupRoutine(ctx)
}
@ -206,7 +206,7 @@ func (m *MultiSourceClient) Start(ctx context.Context) {
func (m *MultiSourceClient) Stop() {
m.mu.RLock()
defer m.mu.RUnlock()
for _, client := range m.clients {
client.Stop()
}
@ -216,7 +216,7 @@ func (m *MultiSourceClient) Stop() {
func (m *MultiSourceClient) cleanupRoutine(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
@ -231,9 +231,9 @@ func (m *MultiSourceClient) cleanupRoutine(ctx context.Context) {
func (m *MultiSourceClient) GetStatistics() map[string]interface{} {
m.mu.RLock()
defer m.mu.RUnlock()
stats := m.merger.GetStatistics()
// Add client-specific stats
activeClients := 0
for _, client := range m.clients {
@ -241,9 +241,9 @@ func (m *MultiSourceClient) GetStatistics() map[string]interface{} {
activeClients++
}
}
stats["active_clients"] = activeClients
stats["total_clients"] = len(m.clients)
return stats
}
}

View file

@ -52,7 +52,7 @@ func (c *Dump1090Client) startDataStream(ctx context.Context) {
func (c *Dump1090Client) connectAndRead(ctx context.Context) error {
address := fmt.Sprintf("%s:%d", c.config.Dump1090.Host, c.config.Dump1090.DataPort)
conn, err := net.Dial("tcp", address)
if err != nil {
return fmt.Errorf("failed to connect to %s: %w", address, err)
@ -109,7 +109,7 @@ func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraf
if update.Latitude != 0 && update.Longitude != 0 {
existing.Latitude = update.Latitude
existing.Longitude = update.Longitude
// Add to track history if position changed significantly
if c.shouldAddTrackPoint(existing, update) {
trackPoint := parser.TrackPoint{
@ -120,9 +120,9 @@ func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraf
Speed: update.GroundSpeed,
Track: update.Track,
}
existing.TrackHistory = append(existing.TrackHistory, trackPoint)
// Keep only last 200 points (about 3-4 hours at 1 point/minute)
if len(existing.TrackHistory) > 200 {
existing.TrackHistory = existing.TrackHistory[1:]
@ -136,7 +136,7 @@ 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
@ -152,19 +152,19 @@ func (c *Dump1090Client) shouldAddTrackPoint(existing, update *parser.Aircraft)
if len(existing.TrackHistory) == 0 {
return true
}
lastPoint := existing.TrackHistory[len(existing.TrackHistory)-1]
// 2. At least 30 seconds since last point
if time.Since(lastPoint.Timestamp) < 30*time.Second {
return false
}
// 3. Position changed by at least 0.001 degrees (~100m)
latDiff := existing.Latitude - lastPoint.Latitude
lonDiff := existing.Longitude - lastPoint.Longitude
distanceChange := latDiff*latDiff + lonDiff*lonDiff
return distanceChange > 0.000001 // ~0.001 degrees squared
}
@ -249,7 +249,7 @@ func (c *Dump1090Client) cleanupStaleAircraft() {
cutoff := time.Now().Add(-2 * time.Minute)
trackCutoff := time.Now().Add(-24 * time.Hour)
for hex, aircraft := range c.aircraftMap {
if aircraft.LastSeen.Before(cutoff) {
delete(c.aircraftMap, hex)
@ -264,4 +264,4 @@ func (c *Dump1090Client) cleanupStaleAircraft() {
aircraft.TrackHistory = validTracks
}
}
}
}