Move documentation to docs/ directory and improve terminology
- Move ARCHITECTURE.md and CLAUDE.md to docs/ directory - Replace "real-time" terminology with accurate "low-latency" and "high-performance" - Update README to reflect correct performance characteristics - Add comprehensive ICAO country database with SQLite backend - Fix display options positioning and functionality - Add map scale controls and improved range ring visibility - Enhance aircraft marker orientation and trail management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
43e55b2ba0
commit
20bdcf54ec
15 changed files with 746 additions and 67 deletions
119
internal/icao/database.go
Normal file
119
internal/icao/database.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package icao
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed icao.db
|
||||
var icaoFS embed.FS
|
||||
|
||||
// Database handles ICAO address to country lookups
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// CountryInfo represents country information for an aircraft
|
||||
type CountryInfo struct {
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"country_code"`
|
||||
Flag string `json:"flag"`
|
||||
}
|
||||
|
||||
// NewDatabase creates a new ICAO database connection
|
||||
func NewDatabase() (*Database, error) {
|
||||
// Extract embedded database to a temporary file
|
||||
data, err := icaoFS.ReadFile("icao.db")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read embedded ICAO database: %w", err)
|
||||
}
|
||||
|
||||
// Create temporary file for the database
|
||||
tmpFile, err := os.CreateTemp("", "icao-*.db")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temporary file: %w", err)
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
// Write database data to temporary file
|
||||
if _, err := io.WriteString(tmpFile, string(data)); err != nil {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpPath)
|
||||
return nil, fmt.Errorf("failed to write database to temp file: %w", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
// Open SQLite database
|
||||
db, err := sql.Open("sqlite3", tmpPath+"?mode=ro") // Read-only mode
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return nil, fmt.Errorf("failed to open SQLite database: %w", err)
|
||||
}
|
||||
|
||||
// Test the database connection
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close()
|
||||
os.Remove(tmpPath)
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
return &Database{db: db}, nil
|
||||
}
|
||||
|
||||
// LookupCountry returns country information for an ICAO address
|
||||
func (d *Database) LookupCountry(icaoHex string) (*CountryInfo, error) {
|
||||
if len(icaoHex) != 6 {
|
||||
return &CountryInfo{
|
||||
Country: "Unknown",
|
||||
CountryCode: "XX",
|
||||
Flag: "🏳️",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert hex string to integer
|
||||
icaoInt, err := strconv.ParseInt(icaoHex, 16, 64)
|
||||
if err != nil {
|
||||
return &CountryInfo{
|
||||
Country: "Unknown",
|
||||
CountryCode: "XX",
|
||||
Flag: "🏳️",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var country, countryCode, flag string
|
||||
query := `
|
||||
SELECT country, country_code, flag
|
||||
FROM icao_allocations
|
||||
WHERE ? BETWEEN start_addr AND end_addr
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
err = d.db.QueryRow(query, icaoInt).Scan(&country, &countryCode, &flag)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return &CountryInfo{
|
||||
Country: "Unknown",
|
||||
CountryCode: "XX",
|
||||
Flag: "🏳️",
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("database query failed: %w", err)
|
||||
}
|
||||
|
||||
return &CountryInfo{
|
||||
Country: country,
|
||||
CountryCode: countryCode,
|
||||
Flag: flag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (d *Database) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
BIN
internal/icao/icao.db
Normal file
BIN
internal/icao/icao.db
Normal file
Binary file not shown.
|
|
@ -26,6 +26,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"skyview/internal/icao"
|
||||
"skyview/internal/modes"
|
||||
)
|
||||
|
||||
|
|
@ -72,6 +73,9 @@ type AircraftState struct {
|
|||
MLATSources []string `json:"mlat_sources"` // Sources providing MLAT position data
|
||||
PositionSource string `json:"position_source"` // Source providing current position
|
||||
UpdateRate float64 `json:"update_rate"` // Recent updates per second
|
||||
Country string `json:"country"` // Country of registration
|
||||
CountryCode string `json:"country_code"` // ISO country code
|
||||
Flag string `json:"flag"` // Country flag emoji
|
||||
}
|
||||
|
||||
// MarshalJSON provides custom JSON marshaling for AircraftState to format ICAO24 as hex.
|
||||
|
|
@ -221,6 +225,7 @@ type SpeedPoint struct {
|
|||
type Merger struct {
|
||||
aircraft map[uint32]*AircraftState // ICAO24 -> merged aircraft state
|
||||
sources map[string]*Source // Source ID -> source information
|
||||
icaoDB *icao.Database // ICAO country lookup database
|
||||
mu sync.RWMutex // Protects all maps and slices
|
||||
historyLimit int // Maximum history points to retain
|
||||
staleTimeout time.Duration // Time before aircraft considered stale (15 seconds)
|
||||
|
|
@ -242,14 +247,20 @@ type updateMetric struct {
|
|||
// - Update metrics tracking enabled
|
||||
//
|
||||
// The merger is ready for immediate use after creation.
|
||||
func NewMerger() *Merger {
|
||||
func NewMerger() (*Merger, error) {
|
||||
icaoDB, err := icao.NewDatabase()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize ICAO database: %w", err)
|
||||
}
|
||||
|
||||
return &Merger{
|
||||
aircraft: make(map[uint32]*AircraftState),
|
||||
sources: make(map[string]*Source),
|
||||
icaoDB: icaoDB,
|
||||
historyLimit: 500,
|
||||
staleTimeout: 15 * time.Second, // Aircraft timeout - reasonable for ADS-B tracking
|
||||
updateMetrics: make(map[uint32]*updateMetric),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddSource registers a new data source with the merger.
|
||||
|
|
@ -304,6 +315,19 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
|||
AltitudeHistory: make([]AltitudePoint, 0),
|
||||
SpeedHistory: make([]SpeedPoint, 0),
|
||||
}
|
||||
|
||||
// Lookup country information for new aircraft
|
||||
if countryInfo, err := m.icaoDB.LookupCountry(fmt.Sprintf("%06X", aircraft.ICAO24)); err == nil {
|
||||
state.Country = countryInfo.Country
|
||||
state.CountryCode = countryInfo.CountryCode
|
||||
state.Flag = countryInfo.Flag
|
||||
} else {
|
||||
// Fallback to unknown if lookup fails
|
||||
state.Country = "Unknown"
|
||||
state.CountryCode = "XX"
|
||||
state.Flag = "🏳️"
|
||||
}
|
||||
|
||||
m.aircraft[aircraft.ICAO24] = state
|
||||
m.updateMetrics[aircraft.ICAO24] = &updateMetric{
|
||||
updates: make([]time.Time, 0),
|
||||
|
|
@ -777,3 +801,14 @@ func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64)
|
|||
|
||||
return distance, bearing
|
||||
}
|
||||
|
||||
// Close closes the merger and releases resources
|
||||
func (m *Merger) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.icaoDB != nil {
|
||||
return m.icaoDB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue