Berechnung der aktuellen Zugposition basierend auf Abfahrtszeit und Routeninformationen

This commit is contained in:
Simon Rieger 2025-01-19 04:33:49 +01:00
parent be7b48ac07
commit fc32a93522
2 changed files with 222 additions and 71 deletions

113
README.md
View file

@ -1,95 +1,92 @@
# DB Departure Tracker # Train Tracker
## Beschreibung ## 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 ## Funktionen
- Abruf von Abfahrtsinformationen für mehrere Bahnhöfe - Abrufen von Zugabfahrten für mehrere Bahnhöfe
- Konfigurierbare Einstellungen für verschiedene Verkehrsmittel (Bus, Fähre, Straßenbahn, Taxi) - Berechnung der aktuellen Zugposition basierend auf Abfahrtszeit und Routeninformationen
- Speicherung und Aktualisierung von Zugpositionen in einer MariaDB-Datenbank - Speichern und Aktualisieren von Zuginformationen in einer MySQL-Datenbank
- Verwendung von UUIDs für eindeutige Datenbankeinträge - Automatisches Löschen veralteter Einträge
- Konfiguration über Umgebungsvariablen - Regelmäßige Protokollierung von Datenbankstatistiken
## Voraussetzungen ## Voraussetzungen
- Go 1.17 oder höher - Go 1.15 oder höher
- Docker und Docker Compose - MySQL-Datenbank
- Zugang zur DB REST API - Zugang zur DB-Vendo-API
## Installation ## Installation
1. Klonen Sie das Repository: 1. Klonen Sie das Repository:
``` ```
git clone https://github.com/yourusername/db-departure-tracker.git git clone https://code.brothertec.eu/simono41/train-tracker.git
cd db-departure-tracker
``` ```
2. Erstellen Sie eine `.env` Datei im Projektverzeichnis und füllen Sie sie mit den erforderlichen Umgebungsvariablen (siehe Konfiguration). 2. Navigieren Sie in das Projektverzeichnis:
3. Bauen und starten Sie die Docker-Container:
``` ```
docker-compose up --build cd train-tracker
```
3. Installieren Sie die Abhängigkeiten:
```
go mod tidy
``` ```
## Konfiguration ## 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") - `DB_DSN`: MySQL-Datenbankverbindungsstring
- `API_BASE_URL`: Basis-URL der DB REST API - `API_BASE_URL`: Basis-URL der DB-Vendo-API
- `MAX_RESULTS`: Maximale Anzahl der abzurufenden Ergebnisse pro Anfrage - `DURATION`: Zeitspanne in Minuten für die Abfrage von Abfahrten
- `DURATION`: Zeitspanne in Minuten für die Abfrage der Abfahrten - `DELETE_AFTER_MINUTES`: Zeit in Minuten, nach der alte Einträge gelöscht werden
- `BUS`: Einbeziehung von Busabfahrten (true/false) - `STATION_IDS`: Komma-getrennte Liste von Bahnhofs-IDs
- `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
Beispiel für eine `.env` Datei: ## Datenbankstruktur
``` Stellen Sie sicher, dass Ihre MySQL-Datenbank eine `trips`-Tabelle mit folgender Struktur hat:
DB_DSN=root:password@tcp(mariadb:3306)/traindb
API_BASE_URL=http://db-rest:3000 ```sql
MAX_RESULTS=10 CREATE TABLE trips (
DURATION=240 id VARCHAR(36) PRIMARY KEY,
BUS=false timestamp DATETIME,
FERRY=false train_name VARCHAR(255),
TRAM=false fahrt_nr VARCHAR(255),
TAXI=false trip_id VARCHAR(255),
STATION_IDS=8000226,8000234 latitude DOUBLE,
longitude DOUBLE
);
``` ```
## Verwendung ## 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: Die Anwendung wird nun kontinuierlich Abfahrtsinformationen abrufen, die Zugpositionen berechnen und in der Datenbank speichern.
```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)
);
```
## Entwicklung ## Entwicklung
Um an diesem Projekt mitzuarbeiten: ### Code-Struktur
1. Forken Sie das Repository - `main.go`: Hauptanwendungslogik und Einstiegspunkt des Programms
2. Erstellen Sie einen Feature Branch (`git checkout -b feature/AmazingFeature`) - Funktionen wie `fetchDepartures()`, `fetchTripDetails()`, `savePosition()`, `calculateCurrentPosition()` und `deleteOldEntries()` implementieren die Kernfunktionalität
3. Committen Sie Ihre Änderungen (`git commit -m 'Add some AmazingFeature'`)
4. Pushen Sie den Branch (`git push origin feature/AmazingFeature`) ### Beitrag
5. Öffnen Sie einen Pull Request
Beiträge sind willkommen! Bitte erstellen Sie einen Pull Request für Verbesserungen oder Fehlerbehebungen.
## Lizenz ## 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).

180
main.go
View file

@ -6,7 +6,9 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math"
"net/http" "net/http"
"net/url"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -29,6 +31,36 @@ type APIResponse struct {
Departures []Departure `json:"departures"` 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() { func main() {
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
@ -52,20 +84,31 @@ func main() {
} }
defer db.Close() defer db.Close()
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for { for {
for _, stationID := range stationIDs { for _, stationID := range stationIDs {
departures := fetchDepartures(apiBaseURL, stationID, duration) departures := fetchDepartures(apiBaseURL, stationID, duration)
for _, dep := range departures { for _, dep := range departures {
savePosition(db, dep) savePosition(db, dep, apiBaseURL)
} }
} }
deleteOldEntries(db, deleteAfter) deleteOldEntries(db, deleteAfter)
select {
case <-ticker.C:
logDatabaseStats(db)
default:
// Do nothing
}
time.Sleep(1 * time.Minute) time.Sleep(1 * time.Minute)
} }
} }
func fetchDepartures(apiBaseURL, stationID string, duration int) []Departure { 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&regionalExpress=true&regional=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&regionalExpress=true&regional=true&suburban=true&bus=false&ferry=false&subway=false&tram=false&taxi=false&pretty=true",
apiBaseURL, stationID, duration) apiBaseURL, stationID, duration)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
@ -96,10 +139,51 @@ func fetchDepartures(apiBaseURL, stationID string, duration int) []Departure {
return response.Departures 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) whenTime, err := time.Parse(time.RFC3339, dep.When)
if err != nil { 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 return
} }
@ -110,26 +194,86 @@ func savePosition(db *sql.DB, dep Departure) {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
id := uuid.New().String() id := uuid.New().String()
_, err = db.Exec("INSERT INTO trips (id, timestamp, train_name, fahrt_nr, trip_id) VALUES (?, ?, ?, ?, ?)", _, 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) id, whenTime, dep.Line.Name, dep.Line.FahrtNr, dep.TripId, latitude, longitude)
if err != nil { 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 { } 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 { } else if err == nil {
_, err = db.Exec("UPDATE trips SET timestamp = ?, train_name = ?, trip_id = ? WHERE id = ?", _, err = db.Exec("UPDATE trips SET timestamp = ?, train_name = ?, trip_id = ?, latitude = ?, longitude = ? WHERE id = ?",
whenTime, dep.Line.Name, dep.TripId, existingID) whenTime, dep.Line.Name, dep.TripId, latitude, longitude, existingID)
if err != nil { 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 { } 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 { } 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) { func deleteOldEntries(db *sql.DB, deleteAfterMinutes int) {
deleteTime := time.Now().Add(time.Duration(-deleteAfterMinutes) * time.Minute) deleteTime := time.Now().Add(time.Duration(-deleteAfterMinutes) * time.Minute)
result, err := db.Exec("DELETE FROM trips WHERE timestamp < ?", deleteTime) result, err := db.Exec("DELETE FROM trips WHERE timestamp < ?", deleteTime)
@ -140,3 +284,13 @@ func deleteOldEntries(db *sql.DB, deleteAfterMinutes int) {
rowsAffected, _ := result.RowsAffected() rowsAffected, _ := result.RowsAffected()
log.Printf("%d alte Einträge gelöscht\n", 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)
}