Berechnung der aktuellen Zugposition basierend auf Abfahrtszeit und Routeninformationen
This commit is contained in:
parent
be7b48ac07
commit
fc32a93522
2 changed files with 222 additions and 71 deletions
113
README.md
113
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).
|
||||
|
|
180
main.go
180
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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue