Add historical flight track functionality
- Store track history with position, altitude, speed, and timestamp
- Automatic track point collection every 30 seconds when position changes
- API endpoint /api/aircraft/{hex}/history for individual aircraft tracks
- Frontend "Show History" button to display historical flight paths
- Click aircraft markers to show their historical track (dashed red line)
- Track cleanup: keep last 200 points per aircraft, 24-hour retention
- Add aircraft type badges in table view with color coding
- Start/end markers for historical tracks with timestamps
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b6a699c24b
commit
55710614da
6 changed files with 215 additions and 16 deletions
|
|
@ -106,11 +106,28 @@ func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraf
|
|||
if update.Track != 0 {
|
||||
existing.Track = update.Track
|
||||
}
|
||||
if update.Latitude != 0 {
|
||||
if update.Latitude != 0 && update.Longitude != 0 {
|
||||
existing.Latitude = update.Latitude
|
||||
}
|
||||
if update.Longitude != 0 {
|
||||
existing.Longitude = update.Longitude
|
||||
|
||||
// Add to track history if position changed significantly
|
||||
if c.shouldAddTrackPoint(existing, update) {
|
||||
trackPoint := parser.TrackPoint{
|
||||
Timestamp: update.LastSeen,
|
||||
Latitude: update.Latitude,
|
||||
Longitude: update.Longitude,
|
||||
Altitude: update.Altitude,
|
||||
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:]
|
||||
}
|
||||
}
|
||||
}
|
||||
if update.VertRate != 0 {
|
||||
existing.VertRate = update.VertRate
|
||||
|
|
@ -121,6 +138,28 @@ func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraf
|
|||
existing.OnGround = update.OnGround
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) shouldAddTrackPoint(existing, update *parser.Aircraft) bool {
|
||||
// Add track point if:
|
||||
// 1. No history yet
|
||||
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
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) GetAircraftData() parser.AircraftData {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
|
@ -201,9 +240,20 @@ func (c *Dump1090Client) cleanupStaleAircraft() {
|
|||
defer c.mutex.Unlock()
|
||||
|
||||
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)
|
||||
} else {
|
||||
// Clean up old track points (keep last 24 hours)
|
||||
validTracks := make([]parser.TrackPoint, 0)
|
||||
for _, point := range aircraft.TrackHistory {
|
||||
if point.Timestamp.After(trackCutoff) {
|
||||
validTracks = append(validTracks, point)
|
||||
}
|
||||
}
|
||||
aircraft.TrackHistory = validTracks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,20 +6,30 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
type TrackPoint struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Latitude float64 `json:"lat"`
|
||||
Longitude float64 `json:"lon"`
|
||||
Altitude int `json:"altitude"`
|
||||
Speed int `json:"speed"`
|
||||
Track int `json:"track"`
|
||||
}
|
||||
|
||||
type Aircraft struct {
|
||||
Hex string `json:"hex"`
|
||||
Flight string `json:"flight,omitempty"`
|
||||
Altitude int `json:"alt_baro,omitempty"`
|
||||
GroundSpeed int `json:"gs,omitempty"`
|
||||
Track int `json:"track,omitempty"`
|
||||
Latitude float64 `json:"lat,omitempty"`
|
||||
Longitude float64 `json:"lon,omitempty"`
|
||||
VertRate int `json:"vert_rate,omitempty"`
|
||||
Squawk string `json:"squawk,omitempty"`
|
||||
Emergency bool `json:"emergency,omitempty"`
|
||||
OnGround bool `json:"on_ground,omitempty"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Messages int `json:"messages"`
|
||||
Hex string `json:"hex"`
|
||||
Flight string `json:"flight,omitempty"`
|
||||
Altitude int `json:"alt_baro,omitempty"`
|
||||
GroundSpeed int `json:"gs,omitempty"`
|
||||
Track int `json:"track,omitempty"`
|
||||
Latitude float64 `json:"lat,omitempty"`
|
||||
Longitude float64 `json:"lon,omitempty"`
|
||||
VertRate int `json:"vert_rate,omitempty"`
|
||||
Squawk string `json:"squawk,omitempty"`
|
||||
Emergency bool `json:"emergency,omitempty"`
|
||||
OnGround bool `json:"on_ground,omitempty"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Messages int `json:"messages"`
|
||||
TrackHistory []TrackPoint `json:"track_history,omitempty"`
|
||||
}
|
||||
|
||||
type AircraftData struct {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ func New(cfg *config.Config, staticFiles embed.FS, ctx context.Context) http.Han
|
|||
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
apiRouter.HandleFunc("/aircraft", s.getAircraft).Methods("GET")
|
||||
apiRouter.HandleFunc("/aircraft/{hex}/history", s.getAircraftHistory).Methods("GET")
|
||||
apiRouter.HandleFunc("/stats", s.getStats).Methods("GET")
|
||||
apiRouter.HandleFunc("/config", s.getConfig).Methods("GET")
|
||||
|
||||
|
|
@ -122,6 +123,27 @@ func (s *Server) getStats(w http.ResponseWriter, r *http.Request) {
|
|||
json.NewEncoder(w).Encode(stats)
|
||||
}
|
||||
|
||||
func (s *Server) getAircraftHistory(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
hex := vars["hex"]
|
||||
|
||||
data := s.dump1090.GetAircraftData()
|
||||
aircraft, exists := data.Aircraft[hex]
|
||||
if !exists {
|
||||
http.Error(w, "Aircraft not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"hex": aircraft.Hex,
|
||||
"flight": aircraft.Flight,
|
||||
"track_history": aircraft.TrackHistory,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (s *Server) getConfig(w http.ResponseWriter, r *http.Request) {
|
||||
configData := map[string]interface{}{
|
||||
"origin": map[string]interface{}{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue