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
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).

180
main.go
View file

@ -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&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)
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)
}