210 lines
5 KiB
Go
210 lines
5 KiB
Go
|
|
package server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"embed"
|
||
|
|
"encoding/json"
|
||
|
|
"log"
|
||
|
|
"net/http"
|
||
|
|
"sync"
|
||
|
|
|
||
|
|
"github.com/gorilla/mux"
|
||
|
|
"github.com/gorilla/websocket"
|
||
|
|
|
||
|
|
"skyview/internal/client"
|
||
|
|
"skyview/internal/config"
|
||
|
|
"skyview/internal/parser"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Server struct {
|
||
|
|
config *config.Config
|
||
|
|
staticFiles embed.FS
|
||
|
|
upgrader websocket.Upgrader
|
||
|
|
wsClients map[*websocket.Conn]bool
|
||
|
|
wsClientsMux sync.RWMutex
|
||
|
|
dump1090 *client.Dump1090Client
|
||
|
|
ctx context.Context
|
||
|
|
}
|
||
|
|
|
||
|
|
type WebSocketMessage struct {
|
||
|
|
Type string `json:"type"`
|
||
|
|
Data interface{} `json:"data"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func New(cfg *config.Config, staticFiles embed.FS, ctx context.Context) http.Handler {
|
||
|
|
s := &Server{
|
||
|
|
config: cfg,
|
||
|
|
staticFiles: staticFiles,
|
||
|
|
upgrader: websocket.Upgrader{
|
||
|
|
CheckOrigin: func(r *http.Request) bool {
|
||
|
|
return true
|
||
|
|
},
|
||
|
|
},
|
||
|
|
wsClients: make(map[*websocket.Conn]bool),
|
||
|
|
dump1090: client.NewDump1090Client(cfg),
|
||
|
|
ctx: ctx,
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := s.dump1090.Start(ctx); err != nil {
|
||
|
|
log.Printf("Failed to start dump1090 client: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
go s.subscribeToAircraftUpdates()
|
||
|
|
|
||
|
|
router := mux.NewRouter()
|
||
|
|
|
||
|
|
router.HandleFunc("/", s.serveIndex).Methods("GET")
|
||
|
|
router.HandleFunc("/ws", s.handleWebSocket).Methods("GET")
|
||
|
|
|
||
|
|
apiRouter := router.PathPrefix("/api").Subrouter()
|
||
|
|
apiRouter.HandleFunc("/aircraft", s.getAircraft).Methods("GET")
|
||
|
|
apiRouter.HandleFunc("/stats", s.getStats).Methods("GET")
|
||
|
|
apiRouter.HandleFunc("/config", s.getConfig).Methods("GET")
|
||
|
|
|
||
|
|
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(s.staticFiles))))
|
||
|
|
|
||
|
|
return s.enableCORS(router)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) serveIndex(w http.ResponseWriter, r *http.Request) {
|
||
|
|
data, err := s.staticFiles.ReadFile("static/index.html")
|
||
|
|
if err != nil {
|
||
|
|
http.Error(w, "Failed to read index.html", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "text/html")
|
||
|
|
w.Write(data)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) getAircraft(w http.ResponseWriter, r *http.Request) {
|
||
|
|
data := s.dump1090.GetAircraftData()
|
||
|
|
|
||
|
|
response := map[string]interface{}{
|
||
|
|
"now": data.Now,
|
||
|
|
"messages": data.Messages,
|
||
|
|
"aircraft": s.aircraftMapToSlice(data.Aircraft),
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
json.NewEncoder(w).Encode(response)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) getStats(w http.ResponseWriter, r *http.Request) {
|
||
|
|
data := s.dump1090.GetAircraftData()
|
||
|
|
|
||
|
|
stats := map[string]interface{}{
|
||
|
|
"total": map[string]interface{}{
|
||
|
|
"aircraft": len(data.Aircraft),
|
||
|
|
"messages": map[string]interface{}{
|
||
|
|
"total": data.Messages,
|
||
|
|
"last1min": data.Messages,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
json.NewEncoder(w).Encode(stats)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) getConfig(w http.ResponseWriter, r *http.Request) {
|
||
|
|
configData := map[string]interface{}{
|
||
|
|
"origin": map[string]interface{}{
|
||
|
|
"latitude": s.config.Origin.Latitude,
|
||
|
|
"longitude": s.config.Origin.Longitude,
|
||
|
|
"name": s.config.Origin.Name,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
json.NewEncoder(w).Encode(configData)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) aircraftMapToSlice(aircraftMap map[string]parser.Aircraft) []parser.Aircraft {
|
||
|
|
aircraft := make([]parser.Aircraft, 0, len(aircraftMap))
|
||
|
|
for _, a := range aircraftMap {
|
||
|
|
aircraft = append(aircraft, a)
|
||
|
|
}
|
||
|
|
return aircraft
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) subscribeToAircraftUpdates() {
|
||
|
|
updates := s.dump1090.Subscribe()
|
||
|
|
|
||
|
|
for data := range updates {
|
||
|
|
message := WebSocketMessage{
|
||
|
|
Type: "aircraft_update",
|
||
|
|
Data: map[string]interface{}{
|
||
|
|
"now": data.Now,
|
||
|
|
"messages": data.Messages,
|
||
|
|
"aircraft": s.aircraftMapToSlice(data.Aircraft),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
s.broadcastToWebSocketClients(message)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||
|
|
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||
|
|
if err != nil {
|
||
|
|
log.Printf("WebSocket upgrade error: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer conn.Close()
|
||
|
|
|
||
|
|
s.wsClientsMux.Lock()
|
||
|
|
s.wsClients[conn] = true
|
||
|
|
s.wsClientsMux.Unlock()
|
||
|
|
|
||
|
|
defer func() {
|
||
|
|
s.wsClientsMux.Lock()
|
||
|
|
delete(s.wsClients, conn)
|
||
|
|
s.wsClientsMux.Unlock()
|
||
|
|
}()
|
||
|
|
|
||
|
|
data := s.dump1090.GetAircraftData()
|
||
|
|
initialMessage := WebSocketMessage{
|
||
|
|
Type: "aircraft_update",
|
||
|
|
Data: map[string]interface{}{
|
||
|
|
"now": data.Now,
|
||
|
|
"messages": data.Messages,
|
||
|
|
"aircraft": s.aircraftMapToSlice(data.Aircraft),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
conn.WriteJSON(initialMessage)
|
||
|
|
|
||
|
|
for {
|
||
|
|
_, _, err := conn.ReadMessage()
|
||
|
|
if err != nil {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) broadcastToWebSocketClients(message WebSocketMessage) {
|
||
|
|
s.wsClientsMux.RLock()
|
||
|
|
defer s.wsClientsMux.RUnlock()
|
||
|
|
|
||
|
|
for client := range s.wsClients {
|
||
|
|
if err := client.WriteJSON(message); err != nil {
|
||
|
|
client.Close()
|
||
|
|
delete(s.wsClients, client)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) enableCORS(handler http.Handler) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||
|
|
|
||
|
|
if r.Method == "OPTIONS" {
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
handler.ServeHTTP(w, r)
|
||
|
|
})
|
||
|
|
}
|