From fc32a93522a26a25711c9c25bdf3c9053168f58f Mon Sep 17 00:00:00 2001 From: Simon Rieger Date: Sun, 19 Jan 2025 04:33:49 +0100 Subject: [PATCH] Berechnung der aktuellen Zugposition basierend auf Abfahrtszeit und Routeninformationen --- README.md | 113 +++++++++++++++++----------------- main.go | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 222 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index f617921..dba6ed4 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,92 @@ -# DB Departure Tracker +# Train Tracker ## Beschreibung -Dieses Projekt ist ein Go-basierter Service, der Abfahrtsinformationen von verschiedenen Bahnhöfen der Deutschen Bahn abruft und in einer MariaDB-Datenbank speichert. Es verwendet die DB REST API, um Echtzeit-Abfahrtsdaten zu erhalten und aktualisiert die Positionen der Züge in regelmäßigen Abständen. +Train Tracker ist ein in Go geschriebenes Programm, das Echtzeitinformationen über Zugbewegungen verfolgt und speichert. Es nutzt die DB-Vendo-API, um Abfahrtsinformationen von spezifizierten Bahnhöfen abzurufen, berechnet die aktuelle Position der Züge basierend auf ihrer Route und speichert diese Informationen in einer MySQL-Datenbank. ## Funktionen -- Abruf von Abfahrtsinformationen für mehrere Bahnhöfe -- Konfigurierbare Einstellungen für verschiedene Verkehrsmittel (Bus, Fähre, Straßenbahn, Taxi) -- Speicherung und Aktualisierung von Zugpositionen in einer MariaDB-Datenbank -- Verwendung von UUIDs für eindeutige Datenbankeinträge -- Konfiguration über Umgebungsvariablen +- Abrufen von Zugabfahrten für mehrere Bahnhöfe +- Berechnung der aktuellen Zugposition basierend auf Abfahrtszeit und Routeninformationen +- Speichern und Aktualisieren von Zuginformationen in einer MySQL-Datenbank +- Automatisches Löschen veralteter Einträge +- Regelmäßige Protokollierung von Datenbankstatistiken ## Voraussetzungen -- Go 1.17 oder höher -- Docker und Docker Compose -- Zugang zur DB REST API +- Go 1.15 oder höher +- MySQL-Datenbank +- Zugang zur DB-Vendo-API ## Installation 1. Klonen Sie das Repository: ``` - git clone https://github.com/yourusername/db-departure-tracker.git - cd db-departure-tracker + git clone https://code.brothertec.eu/simono41/train-tracker.git ``` -2. Erstellen Sie eine `.env` Datei im Projektverzeichnis und füllen Sie sie mit den erforderlichen Umgebungsvariablen (siehe Konfiguration). - -3. Bauen und starten Sie die Docker-Container: +2. Navigieren Sie in das Projektverzeichnis: ``` - docker-compose up --build + cd train-tracker + ``` + +3. Installieren Sie die Abhängigkeiten: + ``` + go mod tidy ``` ## Konfiguration -Konfigurieren Sie die Anwendung durch Setzen der folgenden Umgebungsvariablen: +Konfigurieren Sie die Anwendung über folgende Umgebungsvariablen: -- `DB_DSN`: Datenbankverbindungsstring (z.B. "root:password@tcp(mariadb:3306)/traindb") -- `API_BASE_URL`: Basis-URL der DB REST API -- `MAX_RESULTS`: Maximale Anzahl der abzurufenden Ergebnisse pro Anfrage -- `DURATION`: Zeitspanne in Minuten für die Abfrage der Abfahrten -- `BUS`: Einbeziehung von Busabfahrten (true/false) -- `FERRY`: Einbeziehung von Fährabfahrten (true/false) -- `TRAM`: Einbeziehung von Straßenbahnabfahrten (true/false) -- `TAXI`: Einbeziehung von Taxiabfahrten (true/false) -- `STATION_IDS`: Komma-separierte Liste der Bahnhofs-IDs +- `DB_DSN`: MySQL-Datenbankverbindungsstring +- `API_BASE_URL`: Basis-URL der DB-Vendo-API +- `DURATION`: Zeitspanne in Minuten für die Abfrage von Abfahrten +- `DELETE_AFTER_MINUTES`: Zeit in Minuten, nach der alte Einträge gelöscht werden +- `STATION_IDS`: Komma-getrennte Liste von Bahnhofs-IDs -Beispiel für eine `.env` Datei: +## Datenbankstruktur -``` -DB_DSN=root:password@tcp(mariadb:3306)/traindb -API_BASE_URL=http://db-rest:3000 -MAX_RESULTS=10 -DURATION=240 -BUS=false -FERRY=false -TRAM=false -TAXI=false -STATION_IDS=8000226,8000234 +Stellen Sie sicher, dass Ihre MySQL-Datenbank eine `trips`-Tabelle mit folgender Struktur hat: + +```sql +CREATE TABLE trips ( + id VARCHAR(36) PRIMARY KEY, + timestamp DATETIME, + train_name VARCHAR(255), + fahrt_nr VARCHAR(255), + trip_id VARCHAR(255), + latitude DOUBLE, + longitude DOUBLE +); ``` ## Verwendung -Nach dem Start läuft der Service kontinuierlich und ruft in regelmäßigen Abständen Abfahrtsinformationen ab. Die Daten werden in der konfigurierten MariaDB-Datenbank gespeichert. +1. Setzen Sie die erforderlichen Umgebungsvariablen. -## Datenbankschema +2. Starten Sie die Anwendung: + ``` + go run main.go + ``` -Die Anwendung verwendet folgendes Datenbankschema: - -```sql -CREATE TABLE IF NOT EXISTS trips ( - id VARCHAR(36) PRIMARY KEY, - latitude DOUBLE, - longitude DOUBLE, - timestamp DATETIME, - train_name VARCHAR(50), - fahrt_nr VARCHAR(20) -); -``` +Die Anwendung wird nun kontinuierlich Abfahrtsinformationen abrufen, die Zugpositionen berechnen und in der Datenbank speichern. ## Entwicklung -Um an diesem Projekt mitzuarbeiten: +### Code-Struktur -1. Forken Sie das Repository -2. Erstellen Sie einen Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Committen Sie Ihre Änderungen (`git commit -m 'Add some AmazingFeature'`) -4. Pushen Sie den Branch (`git push origin feature/AmazingFeature`) -5. Öffnen Sie einen Pull Request +- `main.go`: Hauptanwendungslogik und Einstiegspunkt des Programms +- Funktionen wie `fetchDepartures()`, `fetchTripDetails()`, `savePosition()`, `calculateCurrentPosition()` und `deleteOldEntries()` implementieren die Kernfunktionalität + +### Beitrag + +Beiträge sind willkommen! Bitte erstellen Sie einen Pull Request für Verbesserungen oder Fehlerbehebungen. ## Lizenz -Dieses Projekt ist unter der MIT-Lizenz lizenziert. Siehe `LICENSE` Datei für Details. +Dieses Projekt ist unter der [MIT-Lizenz](https://opensource.org/licenses/MIT) lizenziert. + +## Kontakt + +Bei Fragen oder Problemen öffnen Sie bitte ein Issue im [Git-Repository](https://code.brothertec.eu/simono41/train-tracker). diff --git a/main.go b/main.go index 57cc602..0006945 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,9 @@ import ( "fmt" "io/ioutil" "log" + "math" "net/http" + "net/url" "os" "strconv" "strings" @@ -29,6 +31,36 @@ type APIResponse struct { Departures []Departure `json:"departures"` } +type TripDetails struct { + Origin Station `json:"origin"` + Destination Station `json:"destination"` + Departure time.Time `json:"departure"` + Arrival time.Time `json:"arrival"` + Polyline Polyline `json:"polyline"` +} + +type Station struct { + Name string `json:"name"` + Location Location `json:"location"` +} + +type Location struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type Polyline struct { + Features []Feature `json:"features"` +} + +type Feature struct { + Geometry Geometry `json:"geometry"` +} + +type Geometry struct { + Coordinates []float64 `json:"coordinates"` +} + func main() { log.SetOutput(os.Stdout) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) @@ -52,20 +84,31 @@ func main() { } defer db.Close() + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for { for _, stationID := range stationIDs { departures := fetchDepartures(apiBaseURL, stationID, duration) for _, dep := range departures { - savePosition(db, dep) + savePosition(db, dep, apiBaseURL) } } deleteOldEntries(db, deleteAfter) + + select { + case <-ticker.C: + logDatabaseStats(db) + default: + // Do nothing + } + time.Sleep(1 * time.Minute) } } func fetchDepartures(apiBaseURL, stationID string, duration int) []Departure { - url := fmt.Sprintf("%s/stops/%s/departures?duration=%d&linesOfStops=false&remarks=false&language=de&nationalExpress=true&national=true®ionalExpress=true®ional=true&suburban=true&bus=false&ferry=false&subway=false&tram=false&taxi=false&pretty=true", + url := fmt.Sprintf("%s/stops/%s/departures?duration=%d&linesOfStops=false&remarks=true&language=en&nationalExpress=true&national=true®ionalExpress=true®ional=true&suburban=true&bus=false&ferry=false&subway=false&tram=false&taxi=false&pretty=true", apiBaseURL, stationID, duration) resp, err := http.Get(url) if err != nil { @@ -96,10 +139,51 @@ func fetchDepartures(apiBaseURL, stationID string, duration int) []Departure { return response.Departures } -func savePosition(db *sql.DB, dep Departure) { +func fetchTripDetails(apiBaseURL, tripID string) (*TripDetails, error) { + escapedTripID := url.QueryEscape(tripID) + url := fmt.Sprintf("%s/trips/%s?stopovers=true&remarks=true&polyline=true&language=en", apiBaseURL, escapedTripID) + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("Fehler beim Abrufen der Zugdetails: %v", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Fehler beim Lesen der Antwort: %v", err) + } + + if len(body) == 0 { + return nil, fmt.Errorf("Leere Antwort vom Server erhalten") + } + + var tripResponse struct { + Trip TripDetails `json:"trip"` + } + if err := json.Unmarshal(body, &tripResponse); err != nil { + return nil, fmt.Errorf("Fehler beim Dekodieren der Zugdetails: %v", err) + } + + if tripResponse.Trip.Origin.Name == "" || tripResponse.Trip.Destination.Name == "" { + return nil, fmt.Errorf("Unvollständige Tripdaten erhalten") + } + + return &tripResponse.Trip, nil +} + +func savePosition(db *sql.DB, dep Departure, apiBaseURL string) { + tripDetails, err := fetchTripDetails(apiBaseURL, dep.TripId) + if err != nil { + log.Printf("Fehler beim Abrufen der Zugdetails für TripID %s: %v\n", dep.TripId, err) + return + } + + currentTime := time.Now() + longitude, latitude := calculateCurrentPosition(tripDetails, currentTime) + whenTime, err := time.Parse(time.RFC3339, dep.When) if err != nil { - log.Printf("Fehler beim Parsen der Zeit: %v\n", err) + log.Printf("Fehler beim Parsen der Zeit für TripID %s: %v\n", dep.TripId, err) return } @@ -110,26 +194,86 @@ func savePosition(db *sql.DB, dep Departure) { if err == sql.ErrNoRows { id := uuid.New().String() - _, err = db.Exec("INSERT INTO trips (id, timestamp, train_name, fahrt_nr, trip_id) VALUES (?, ?, ?, ?, ?)", - id, whenTime, dep.Line.Name, dep.Line.FahrtNr, dep.TripId) + _, err = db.Exec("INSERT INTO trips (id, timestamp, train_name, fahrt_nr, trip_id, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?)", + id, whenTime, dep.Line.Name, dep.Line.FahrtNr, dep.TripId, latitude, longitude) if err != nil { - log.Printf("Fehler beim Speichern der neuen Position: %v\n", err) + log.Printf("Fehler beim Speichern der neuen Position für TripID %s: %v\n", dep.TripId, err) } else { - log.Printf("Neue Position gespeichert (ID: %s, Zug: %s, FahrtNr: %s)\n", id, dep.Line.Name, dep.Line.FahrtNr) + log.Printf("Neue Position gespeichert (ID: %s, Zug: %s, FahrtNr: %s, Lat: %f, Lon: %f)\n", id, dep.Line.Name, dep.Line.FahrtNr, latitude, longitude) } } else if err == nil { - _, err = db.Exec("UPDATE trips SET timestamp = ?, train_name = ?, trip_id = ? WHERE id = ?", - whenTime, dep.Line.Name, dep.TripId, existingID) + _, err = db.Exec("UPDATE trips SET timestamp = ?, train_name = ?, trip_id = ?, latitude = ?, longitude = ? WHERE id = ?", + whenTime, dep.Line.Name, dep.TripId, latitude, longitude, existingID) if err != nil { - log.Printf("Fehler beim Aktualisieren der Position: %v\n", err) + log.Printf("Fehler beim Aktualisieren der Position für TripID %s: %v\n", dep.TripId, err) } else { - log.Printf("Position aktualisiert (ID: %s, Zug: %s, FahrtNr: %s)\n", existingID, dep.Line.Name, dep.Line.FahrtNr) + log.Printf("Position aktualisiert (ID: %s, Zug: %s, FahrtNr: %s, Lat: %f, Lon: %f)\n", existingID, dep.Line.Name, dep.Line.FahrtNr, latitude, longitude) } } else { - log.Printf("Fehler bei der Überprüfung des existierenden Eintrags: %v\n", err) + log.Printf("Fehler bei der Überprüfung des existierenden Eintrags für TripID %s: %v\n", dep.TripId, err) } } +func calculateCurrentPosition(trip *TripDetails, currentTime time.Time) (float64, float64) { + totalDuration := trip.Arrival.Sub(trip.Departure) + elapsedDuration := currentTime.Sub(trip.Departure) + progress := elapsedDuration.Seconds() / totalDuration.Seconds() + + if progress < 0 { + return trip.Origin.Location.Longitude, trip.Origin.Location.Latitude + } + if progress > 1 { + return trip.Destination.Location.Longitude, trip.Destination.Location.Latitude + } + + polyline := trip.Polyline.Features + totalDistance := 0.0 + distances := make([]float64, len(polyline)-1) + + for i := 0; i < len(polyline)-1; i++ { + dist := distance( + polyline[i].Geometry.Coordinates[1], polyline[i].Geometry.Coordinates[0], + polyline[i+1].Geometry.Coordinates[1], polyline[i+1].Geometry.Coordinates[0], + ) + distances[i] = dist + totalDistance += dist + } + + targetDistance := totalDistance * progress + coveredDistance := 0.0 + + for i, dist := range distances { + if coveredDistance+dist > targetDistance { + remainingDistance := targetDistance - coveredDistance + ratio := remainingDistance / dist + return interpolate( + polyline[i].Geometry.Coordinates[0], polyline[i].Geometry.Coordinates[1], + polyline[i+1].Geometry.Coordinates[0], polyline[i+1].Geometry.Coordinates[1], + ratio, + ) + } + coveredDistance += dist + } + + return trip.Destination.Location.Longitude, trip.Destination.Location.Latitude +} + +func distance(lat1, lon1, lat2, lon2 float64) float64 { + const r = 6371 // Earth radius in kilometers + + 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)) + return r * c +} + +func interpolate(lon1, lat1, lon2, lat2, ratio float64) (float64, float64) { + return lon1 + (lon2-lon1)*ratio, lat1 + (lat2-lat1)*ratio +} + func deleteOldEntries(db *sql.DB, deleteAfterMinutes int) { deleteTime := time.Now().Add(time.Duration(-deleteAfterMinutes) * time.Minute) result, err := db.Exec("DELETE FROM trips WHERE timestamp < ?", deleteTime) @@ -140,3 +284,13 @@ func deleteOldEntries(db *sql.DB, deleteAfterMinutes int) { rowsAffected, _ := result.RowsAffected() log.Printf("%d alte Einträge gelöscht\n", rowsAffected) } + +func logDatabaseStats(db *sql.DB) { + var count int + err := db.QueryRow("SELECT COUNT(*) FROM trips").Scan(&count) + if err != nil { + log.Printf("Fehler beim Abrufen der Datenbankstatistiken: %v\n", err) + return + } + log.Printf("Aktuelle Anzahl der Einträge in der Datenbank: %d\n", count) +}