add median support

This commit is contained in:
Simon Rieger 2025-03-20 16:33:31 +01:00
parent 0dccca5b03
commit 451063dfc1
4 changed files with 349 additions and 487 deletions

View file

@ -1 +1,2 @@
db/ db/
.git/

View file

@ -30,6 +30,7 @@ CREATE TABLE IF NOT EXISTS delay_stats (
total_trips INT, total_trips INT,
delayed_trips INT, delayed_trips INT,
avg_delay FLOAT, avg_delay FLOAT,
median_delay FLOAT,
last_updated DATETIME, last_updated DATETIME,
INDEX idx_fahrt_nr (fahrt_nr) INDEX idx_fahrt_nr (fahrt_nr)
); );

670
main.go
View file

@ -1,434 +1,458 @@
package main package main
import ( import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math" "math"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv" "sort"
"strings" "strconv"
"time" "strings"
"time"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/google/uuid" "github.com/google/uuid"
) )
type Departure struct { type Departure struct {
TripId string `json:"tripId"` TripId string `json:"tripId"`
When string `json:"when"` When string `json:"when"`
PlannedWhen string `json:"plannedWhen"` PlannedWhen string `json:"plannedWhen"`
Delay int `json:"delay"` Delay int `json:"delay"`
Line struct { Line struct {
Name string `json:"name"` Name string `json:"name"`
FahrtNr string `json:"fahrtNr"` FahrtNr string `json:"fahrtNr"`
} `json:"line"` } `json:"line"`
} }
type APIResponse struct { type APIResponse struct {
Departures []Departure `json:"departures"` Departures []Departure `json:"departures"`
} }
type TripDetails struct { type TripDetails struct {
Origin Station `json:"origin"` Origin Station `json:"origin"`
Destination Station `json:"destination"` Destination Station `json:"destination"`
Departure time.Time `json:"departure"` Departure time.Time `json:"departure"`
Arrival time.Time `json:"arrival"` Arrival time.Time `json:"arrival"`
Polyline Polyline `json:"polyline"` Polyline Polyline `json:"polyline"`
} }
type Station struct { type Station struct {
Name string `json:"name"` Name string `json:"name"`
Location Location `json:"location"` Location Location `json:"location"`
} }
type Location struct { type Location struct {
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
} }
type Polyline struct { type Polyline struct {
Features []Feature `json:"features"` Features []Feature `json:"features"`
} }
type Feature struct { type Feature struct {
Geometry Geometry `json:"geometry"` Geometry Geometry `json:"geometry"`
} }
type Geometry struct { type Geometry struct {
Coordinates []float64 `json:"coordinates"` 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)
log.Println("Anwendung gestartet") log.Println("Anwendung gestartet")
dbDSN := os.Getenv("DB_DSN") dbDSN := os.Getenv("DB_DSN")
apiBaseURL := os.Getenv("API_BASE_URL") apiBaseURL := os.Getenv("API_BASE_URL")
duration, err := strconv.Atoi(os.Getenv("DURATION")) duration, err := strconv.Atoi(os.Getenv("DURATION"))
if err != nil { if err != nil {
log.Fatalf("Ungültiger Wert für DURATION: %v", err) log.Fatalf("Ungültiger Wert für DURATION: %v", err)
} }
deleteAfter, err := strconv.Atoi(os.Getenv("DELETE_AFTER_MINUTES")) deleteAfter, err := strconv.Atoi(os.Getenv("DELETE_AFTER_MINUTES"))
if err != nil { if err != nil {
log.Fatalf("Ungültiger Wert für DELETE_AFTER_MINUTES: %v", err) log.Fatalf("Ungültiger Wert für DELETE_AFTER_MINUTES: %v", err)
} }
stationIDs := strings.Split(os.Getenv("STATION_IDS"), ",") stationIDs := strings.Split(os.Getenv("STATION_IDS"), ",")
updateInterval, err := strconv.Atoi(os.Getenv("UPDATE_INTERVAL_MINUTES")) updateInterval, err := strconv.Atoi(os.Getenv("UPDATE_INTERVAL_MINUTES"))
if err != nil || updateInterval <= 0 { if err != nil || updateInterval <= 0 {
log.Println("Ungültiger oder fehlender Wert für UPDATE_INTERVAL_MINUTES, verwende Standardwert von 1 Minute") log.Println("Ungültiger oder fehlender Wert für UPDATE_INTERVAL_MINUTES, verwende Standardwert von 1 Minute")
updateInterval = 1 updateInterval = 1
} }
transferTimeStr := os.Getenv("TRANSFER_TIME") transferTimeStr := os.Getenv("TRANSFER_TIME")
transferTime, err := time.Parse("15:04", transferTimeStr) transferTime, err := time.Parse("15:04", transferTimeStr)
if err != nil { if err != nil {
log.Printf("Ungültiger Wert für TRANSFER_TIME, verwende Standardwert 23:00") log.Printf("Ungültiger Wert für TRANSFER_TIME, verwende Standardwert 23:00")
transferTime = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 23, 0, 0, 0, time.Local) transferTime = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 23, 0, 0, 0, time.Local)
} else { } else {
now := time.Now() now := time.Now()
transferTime = time.Date(now.Year(), now.Month(), now.Day(), transferTime.Hour(), transferTime.Minute(), 0, 0, time.Local) transferTime = time.Date(now.Year(), now.Month(), now.Day(), transferTime.Hour(), transferTime.Minute(), 0, 0, time.Local)
} }
db, err := sql.Open("mysql", dbDSN) db, err := sql.Open("mysql", dbDSN)
if err != nil { if err != nil {
log.Fatal("Fehler beim Verbinden mit der Datenbank: ", err) log.Fatal("Fehler beim Verbinden mit der Datenbank: ", err)
} }
defer db.Close() defer db.Close()
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(5 * time.Minute)
updateTicker := time.NewTicker(time.Duration(updateInterval) * time.Minute) updateTicker := time.NewTicker(time.Duration(updateInterval) * time.Minute)
defer ticker.Stop() defer ticker.Stop()
defer updateTicker.Stop() defer updateTicker.Stop()
for { for {
select { select {
case <-updateTicker.C: case <-updateTicker.C:
for _, stationID := range stationIDs { for _, stationID := range stationIDs {
departures := fetchDepartures(apiBaseURL, stationID, duration) departures := fetchDepartures(apiBaseURL, stationID, duration)
// Füge einen 1-Sekunden-Sleeper hinzu // Füge einen 1-Sekunden-Sleeper hinzu
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
for _, dep := range departures { for _, dep := range departures {
savePosition(db, dep, apiBaseURL) savePosition(db, dep, apiBaseURL)
} }
} }
deleteOldEntries(db, deleteAfter) deleteOldEntries(db, deleteAfter)
case <-ticker.C: case <-ticker.C:
logDatabaseStats(db) logDatabaseStats(db)
default: default:
now := time.Now() now := time.Now()
if now.After(transferTime) { if now.After(transferTime) {
transferDailyDelayStats(db) transferDailyDelayStats(db)
transferTime = time.Date(now.Year(), now.Month(), now.Day()+1, transferTime.Hour(), transferTime.Minute(), 0, 0, time.Local) transferTime = time.Date(now.Year(), now.Month(), now.Day()+1, transferTime.Hour(), transferTime.Minute(), 0, 0, time.Local)
} }
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=true&language=en&nationalExpress=true&national=true&regionalExpress=true&regional=true&suburban=true&bus=false&ferry=false&subway=false&tram=false&taxi=false&pretty=false", 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=false",
apiBaseURL, stationID, duration) apiBaseURL, stationID, duration)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
log.Printf("Fehler beim Abrufen der Abfahrten für Station %s: %v\n", stationID, err) log.Printf("Fehler beim Abrufen der Abfahrten für Station %s: %v\n", stationID, err)
return nil return nil
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Printf("Fehler beim Lesen der Antwort für Station %s: %v\n", stationID, err) log.Printf("Fehler beim Lesen der Antwort für Station %s: %v\n", stationID, err)
return nil return nil
} }
if len(body) == 0 { if len(body) == 0 {
log.Printf("Leere Antwort vom Server für Station %s erhalten\n", stationID) log.Printf("Leere Antwort vom Server für Station %s erhalten\n", stationID)
return nil return nil
} }
var response APIResponse var response APIResponse
err = json.Unmarshal(body, &response) err = json.Unmarshal(body, &response)
if err != nil { if err != nil {
log.Printf("Fehler beim Dekodieren der Abfahrten für Station %s: %v\nAntwort-Body: %s\n", stationID, err, string(body)) log.Printf("Fehler beim Dekodieren der Abfahrten für Station %s: %v\nAntwort-Body: %s\n", stationID, err, string(body))
return nil return nil
} }
log.Printf("Erfolgreich %d Abfahrten für Station %s abgerufen\n", len(response.Departures), stationID) log.Printf("Erfolgreich %d Abfahrten für Station %s abgerufen\n", len(response.Departures), stationID)
return response.Departures return response.Departures
} }
func fetchTripDetails(apiBaseURL, tripID string) (*TripDetails, error) { func fetchTripDetails(apiBaseURL, tripID string) (*TripDetails, error) {
escapedTripID := url.QueryEscape(tripID) escapedTripID := url.QueryEscape(tripID)
url := fmt.Sprintf("%s/trips/%s?stopovers=true&remarks=true&polyline=true&language=en&pretty=false", apiBaseURL, escapedTripID) url := fmt.Sprintf("%s/trips/%s?stopovers=true&remarks=true&polyline=true&language=en&pretty=false", apiBaseURL, escapedTripID)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return nil, fmt.Errorf("Fehler beim Abrufen der Zugdetails: %v", err) return nil, fmt.Errorf("Fehler beim Abrufen der Zugdetails: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("Fehler beim Lesen der Antwort: %v", err) return nil, fmt.Errorf("Fehler beim Lesen der Antwort: %v", err)
} }
if len(body) == 0 { if len(body) == 0 {
return nil, fmt.Errorf("Leere Antwort vom Server erhalten") return nil, fmt.Errorf("Leere Antwort vom Server erhalten")
} }
var tripResponse struct { var tripResponse struct {
Trip TripDetails `json:"trip"` Trip TripDetails `json:"trip"`
} }
if err := json.Unmarshal(body, &tripResponse); err != nil { if err := json.Unmarshal(body, &tripResponse); err != nil {
return nil, fmt.Errorf("Fehler beim Dekodieren der Zugdetails: %v", err) return nil, fmt.Errorf("Fehler beim Dekodieren der Zugdetails: %v", err)
} }
if tripResponse.Trip.Origin.Name == "" || tripResponse.Trip.Destination.Name == "" { if tripResponse.Trip.Origin.Name == "" || tripResponse.Trip.Destination.Name == "" {
return nil, fmt.Errorf("Unvollständige Tripdaten erhalten") return nil, fmt.Errorf("Unvollständige Tripdaten erhalten")
} }
return &tripResponse.Trip, nil return &tripResponse.Trip, nil
} }
func savePosition(db *sql.DB, dep Departure, apiBaseURL string) { func savePosition(db *sql.DB, dep Departure, apiBaseURL string) {
// Füge einen 1-Sekunden-Sleeper hinzu // Füge einen 1-Sekunden-Sleeper hinzu
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
tripDetails, err := fetchTripDetails(apiBaseURL, dep.TripId) tripDetails, err := fetchTripDetails(apiBaseURL, dep.TripId)
if err != nil { if err != nil {
log.Printf("Fehler beim Abrufen der Zugdetails für TripID %s: %v\n", dep.TripId, err) log.Printf("Fehler beim Abrufen der Zugdetails für TripID %s: %v\n", dep.TripId, err)
return return
} }
currentTime := time.Now() currentTime := time.Now()
longitude, latitude := calculateCurrentPosition(tripDetails, currentTime) longitude, latitude := calculateCurrentPosition(tripDetails, currentTime)
if dep.When == "" { if dep.When == "" {
log.Printf("Warnung: Leerer Zeitstempel für FahrtNr %s, überspringe Eintrag\n", dep.Line.FahrtNr) log.Printf("Warnung: Leerer Zeitstempel für FahrtNr %s, überspringe Eintrag\n", dep.Line.FahrtNr)
return return
} }
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 für TripID %s: %v\n", dep.TripId, err) log.Printf("Fehler beim Parsen der Zeit für TripID %s: %v\n", dep.TripId, err)
return return
} }
plannedWhenTime, err := time.Parse(time.RFC3339, dep.PlannedWhen) plannedWhenTime, err := time.Parse(time.RFC3339, dep.PlannedWhen)
if err != nil { if err != nil {
log.Printf("Fehler beim Parsen der geplanten Zeit für TripID %s: %v\n", dep.TripId, err) log.Printf("Fehler beim Parsen der geplanten Zeit für TripID %s: %v\n", dep.TripId, err)
return return
} }
var existingID string var existingID string
err = db.QueryRow("SELECT id FROM trips WHERE fahrt_nr = ?", dep.Line.FahrtNr).Scan(&existingID) err = db.QueryRow("SELECT id FROM trips WHERE fahrt_nr = ?", dep.Line.FahrtNr).Scan(&existingID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
id := uuid.New().String() id := uuid.New().String()
_, err = db.Exec("INSERT INTO trips (id, timestamp, planned_timestamp, delay, train_name, fahrt_nr, trip_id, latitude, longitude, destination) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", _, err = db.Exec("INSERT INTO trips (id, timestamp, planned_timestamp, delay, train_name, fahrt_nr, trip_id, latitude, longitude, destination) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
id, whenTime, plannedWhenTime, dep.Delay, dep.Line.Name, dep.Line.FahrtNr, dep.TripId, latitude, longitude, tripDetails.Destination.Name) id, whenTime, plannedWhenTime, dep.Delay, dep.Line.Name, dep.Line.FahrtNr, dep.TripId, latitude, longitude, tripDetails.Destination.Name)
if err != nil { if err != nil {
log.Printf("Fehler beim Speichern der neuen Position für TripID %s: %v\n", dep.TripId, 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, Lat: %f, Lon: %f, Verspätung: %d, Ziel: %s)\n", id, dep.Line.Name, dep.Line.FahrtNr, latitude, longitude, dep.Delay, tripDetails.Destination.Name) log.Printf("Neue Position gespeichert (ID: %s, Zug: %s, FahrtNr: %s, Lat: %f, Lon: %f, Verspätung: %d, Ziel: %s)\n", id, dep.Line.Name, dep.Line.FahrtNr, latitude, longitude, dep.Delay, tripDetails.Destination.Name)
} }
} else if err == nil { } else if err == nil {
_, err = db.Exec("UPDATE trips SET timestamp = ?, planned_timestamp = ?, delay = ?, train_name = ?, trip_id = ?, latitude = ?, longitude = ?, destination = ? WHERE id = ?", _, err = db.Exec("UPDATE trips SET timestamp = ?, planned_timestamp = ?, delay = ?, train_name = ?, trip_id = ?, latitude = ?, longitude = ?, destination = ? WHERE id = ?",
whenTime, plannedWhenTime, dep.Delay, dep.Line.Name, dep.TripId, latitude, longitude, tripDetails.Destination.Name, existingID) whenTime, plannedWhenTime, dep.Delay, dep.Line.Name, dep.TripId, latitude, longitude, tripDetails.Destination.Name, existingID)
if err != nil { if err != nil {
log.Printf("Fehler beim Aktualisieren der Position für TripID %s: %v\n", dep.TripId, 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, Lat: %f, Lon: %f, Verspätung: %d, Ziel: %s)\n", existingID, dep.Line.Name, dep.Line.FahrtNr, latitude, longitude, dep.Delay, tripDetails.Destination.Name) log.Printf("Position aktualisiert (ID: %s, Zug: %s, FahrtNr: %s, Lat: %f, Lon: %f, Verspätung: %d, Ziel: %s)\n", existingID, dep.Line.Name, dep.Line.FahrtNr, latitude, longitude, dep.Delay, tripDetails.Destination.Name)
} }
} else { } else {
log.Printf("Fehler bei der Überprüfung des existierenden Eintrags für TripID %s: %v\n", dep.TripId, err) log.Printf("Fehler bei der Überprüfung des existierenden Eintrags für TripID %s: %v\n", dep.TripId, err)
} }
updateTodayDelayStats(db, dep.Line.FahrtNr, dep.Line.Name, dep.Delay, whenTime) updateTodayDelayStats(db, dep.Line.FahrtNr, dep.Line.Name, dep.Delay, whenTime)
} }
func updateTodayDelayStats(db *sql.DB, fahrtNr, trainName string, delay int, timestamp time.Time) { func updateTodayDelayStats(db *sql.DB, fahrtNr, trainName string, delay int, timestamp time.Time) {
var existingID string var existingID string
err := db.QueryRow("SELECT id FROM today_delay_stats WHERE fahrt_nr = ?", fahrtNr).Scan(&existingID) err := db.QueryRow("SELECT id FROM today_delay_stats WHERE fahrt_nr = ?", fahrtNr).Scan(&existingID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
// Kein existierender Eintrag, führe INSERT aus // Kein existierender Eintrag, führe INSERT aus
_, err = db.Exec(` _, err = db.Exec(`
INSERT INTO today_delay_stats (id, fahrt_nr, train_name, delay, timestamp) INSERT INTO today_delay_stats (id, fahrt_nr, train_name, delay, timestamp)
VALUES (UUID(), ?, ?, ?, ?) VALUES (UUID(), ?, ?, ?, ?)
`, fahrtNr, trainName, delay, timestamp) `, fahrtNr, trainName, delay, timestamp)
if err != nil { if err != nil {
log.Printf("Fehler beim Einfügen der heutigen Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err) log.Printf("Fehler beim Einfügen der heutigen Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err)
} }
} else if err == nil { } else if err == nil {
// Existierender Eintrag gefunden, führe UPDATE aus // Existierender Eintrag gefunden, führe UPDATE aus
_, err = db.Exec(` _, err = db.Exec(`
UPDATE today_delay_stats UPDATE today_delay_stats
SET train_name = ?, delay = ?, timestamp = ? SET train_name = ?, delay = ?, timestamp = ?
WHERE id = ? WHERE id = ?
`, trainName, delay, timestamp, existingID) `, trainName, delay, timestamp, existingID)
if err != nil { if err != nil {
log.Printf("Fehler beim Aktualisieren der heutigen Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err) log.Printf("Fehler beim Aktualisieren der heutigen Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err)
} }
} else { } else {
log.Printf("Fehler beim Überprüfen der heutigen Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err) log.Printf("Fehler beim Überprüfen der heutigen Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err)
} }
}
func calculateMedian(delays []int) float64 {
sort.Ints(delays)
length := len(delays)
if length == 0 {
return 0
}
if length%2 == 0 {
return float64(delays[length/2-1]+delays[length/2]) / 2
}
return float64(delays[length/2])
} }
func transferDailyDelayStats(db *sql.DB) { func transferDailyDelayStats(db *sql.DB) {
log.Println("Starte tägliche Übertragung der Verspätungsstatistiken") log.Println("Starte tägliche Übertragung der Verspätungsstatistiken")
rows, err := db.Query("SELECT fahrt_nr, train_name, delay FROM today_delay_stats")
if err != nil {
log.Printf("Fehler beim Abrufen der heutigen Verspätungsstatistiken: %v\n", err)
return
}
defer rows.Close()
var totalTransferred, totalUpdated, totalInserted int rows, err := db.Query("SELECT fahrt_nr, train_name, delay FROM today_delay_stats")
if err != nil {
log.Printf("Fehler beim Abrufen der heutigen Verspätungsstatistiken: %v\n", err)
return
}
defer rows.Close()
for rows.Next() { delayMap := make(map[string][]int)
var fahrtNr, trainName string for rows.Next() {
var delay int var fahrtNr, trainName string
if err := rows.Scan(&fahrtNr, &trainName, &delay); err != nil { var delay int
log.Printf("Fehler beim Scannen der Verspätungsdaten: %v\n", err) if err := rows.Scan(&fahrtNr, &trainName, &delay); err != nil {
continue log.Printf("Fehler beim Scannen der Verspätungsdaten: %v\n", err)
} continue
}
delayMap[fahrtNr] = append(delayMap[fahrtNr], delay)
}
var existingID string for fahrtNr, delays := range delayMap {
err := db.QueryRow("SELECT id FROM delay_stats WHERE fahrt_nr = ?", fahrtNr).Scan(&existingID) avgDelay := calculateAverage(delays)
medianDelay := calculateMedian(delays)
totalTrips := len(delays)
delayedTrips := countDelayedTrips(delays)
if err == sql.ErrNoRows { var existingID string
// Kein existierender Eintrag, führe INSERT aus err := db.QueryRow("SELECT id FROM delay_stats WHERE fahrt_nr = ?", fahrtNr).Scan(&existingID)
_, err = db.Exec(`
INSERT INTO delay_stats (id, fahrt_nr, total_trips, delayed_trips, avg_delay, last_updated) if err == sql.ErrNoRows {
VALUES (UUID(), ?, 1, ?, ?, NOW()) _, err = db.Exec(`
`, fahrtNr, delay > 300, delay) INSERT INTO delay_stats (id, fahrt_nr, total_trips, delayed_trips, avg_delay, median_delay, last_updated)
if err != nil { VALUES (UUID(), ?, ?, ?, ?, ?, NOW())
log.Printf("Fehler beim Einfügen der Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err) `, fahrtNr, totalTrips, delayedTrips, avgDelay, medianDelay)
} else { } else if err == nil {
totalInserted++ _, err = db.Exec(`
log.Printf("Neue Verspätungsstatistik eingefügt für FahrtNr %s (Zug: %s, Verspätung: %d)\n", fahrtNr, trainName, delay)
}
} else if err == nil {
// Existierender Eintrag gefunden, führe UPDATE aus
_, err = db.Exec(`
UPDATE delay_stats UPDATE delay_stats
SET total_trips = total_trips + 1, SET total_trips = total_trips + ?,
delayed_trips = delayed_trips + ?, delayed_trips = delayed_trips + ?,
avg_delay = ((avg_delay * total_trips) + ?) / (total_trips + 1), avg_delay = ((avg_delay * total_trips) + ?) / (total_trips + ?),
median_delay = ?,
last_updated = NOW() last_updated = NOW()
WHERE id = ? WHERE id = ?
`, delay > 300, delay, existingID) `, totalTrips, delayedTrips, avgDelay*float64(totalTrips), totalTrips, medianDelay, existingID)
if err != nil { }
log.Printf("Fehler beim Aktualisieren der Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err)
} else {
totalUpdated++
log.Printf("Verspätungsstatistik aktualisiert für FahrtNr %s (Zug: %s, Verspätung: %d)\n", fahrtNr, trainName, delay)
}
} else {
log.Printf("Fehler beim Überprüfen der Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err)
}
totalTransferred++ if err != nil {
} log.Printf("Fehler beim Aktualisieren der Verspätungsstatistiken für FahrtNr %s: %v\n", fahrtNr, err)
} else {
log.Printf("Verspätungsstatistik aktualisiert für FahrtNr %s (Durchschnitt: %.2f, Median: %.2f)\n", fahrtNr, avgDelay, medianDelay)
}
}
// Löschen Sie die heutigen Statistiken nach der Übertragung // Löschen Sie die heutigen Statistiken nach der Übertragung
result, err := db.Exec("DELETE FROM today_delay_stats") result, err := db.Exec("DELETE FROM today_delay_stats")
if err != nil { if err != nil {
log.Printf("Fehler beim Löschen der heutigen Verspätungsstatistiken: %v\n", err) log.Printf("Fehler beim Löschen der heutigen Verspätungsstatistiken: %v\n", err)
} else { } else {
rowsAffected, _ := result.RowsAffected() rowsAffected, _ := result.RowsAffected()
log.Printf("%d Einträge aus today_delay_stats gelöscht\n", rowsAffected) log.Printf("%d Einträge aus today_delay_stats gelöscht\n", rowsAffected)
} }
}
log.Printf("Tägliche Übertragung abgeschlossen. Gesamt: %d, Eingefügt: %d, Aktualisiert: %d\n", totalTransferred, totalInserted, totalUpdated) func calculateAverage(delays []int) float64 {
sum := 0
for _, delay := range delays {
sum += delay
}
return float64(sum) / float64(len(delays))
}
func countDelayedTrips(delays []int) int {
count := 0
for _, delay := range delays {
if delay > 300 {
count++
}
}
return count
} }
func calculateCurrentPosition(trip *TripDetails, currentTime time.Time) (float64, float64) { func calculateCurrentPosition(trip *TripDetails, currentTime time.Time) (float64, float64) {
totalDuration := trip.Arrival.Sub(trip.Departure) totalDuration := trip.Arrival.Sub(trip.Departure)
elapsedDuration := currentTime.Sub(trip.Departure) elapsedDuration := currentTime.Sub(trip.Departure)
progress := elapsedDuration.Seconds() / totalDuration.Seconds() progress := elapsedDuration.Seconds() / totalDuration.Seconds()
if progress < 0 { if progress < 0 {
return trip.Origin.Location.Longitude, trip.Origin.Location.Latitude return trip.Origin.Location.Longitude, trip.Origin.Location.Latitude
} }
if progress > 1 { if progress > 1 {
return trip.Destination.Location.Longitude, trip.Destination.Location.Latitude return trip.Destination.Location.Longitude, trip.Destination.Location.Latitude
} }
polyline := trip.Polyline.Features polyline := trip.Polyline.Features
totalDistance := 0.0 totalDistance := 0.0
distances := make([]float64, len(polyline)-1) distances := make([]float64, len(polyline)-1)
for i := 0; i < len(polyline)-1; i++ { for i := 0; i < len(polyline)-1; i++ {
dist := distance( dist := distance(
polyline[i].Geometry.Coordinates[1], polyline[i].Geometry.Coordinates[0], polyline[i].Geometry.Coordinates[1], polyline[i].Geometry.Coordinates[0],
polyline[i+1].Geometry.Coordinates[1], polyline[i+1].Geometry.Coordinates[0], polyline[i+1].Geometry.Coordinates[1], polyline[i+1].Geometry.Coordinates[0],
) )
distances[i] = dist distances[i] = dist
totalDistance += dist totalDistance += dist
} }
targetDistance := totalDistance * progress targetDistance := totalDistance * progress
coveredDistance := 0.0 coveredDistance := 0.0
for i, dist := range distances { for i, dist := range distances {
if coveredDistance+dist > targetDistance { if coveredDistance+dist > targetDistance {
remainingDistance := targetDistance - coveredDistance remainingDistance := targetDistance - coveredDistance
ratio := remainingDistance / dist ratio := remainingDistance / dist
return interpolate( return interpolate(
polyline[i].Geometry.Coordinates[0], polyline[i].Geometry.Coordinates[1], polyline[i].Geometry.Coordinates[0], polyline[i].Geometry.Coordinates[1],
polyline[i+1].Geometry.Coordinates[0], polyline[i+1].Geometry.Coordinates[1], polyline[i+1].Geometry.Coordinates[0], polyline[i+1].Geometry.Coordinates[1],
ratio, ratio,
) )
} }
coveredDistance += dist coveredDistance += dist
} }
return trip.Destination.Location.Longitude, trip.Destination.Location.Latitude return trip.Destination.Location.Longitude, trip.Destination.Location.Latitude
} }
func distance(lat1, lon1, lat2, lon2 float64) float64 { func distance(lat1, lon1, lat2, lon2 float64) float64 {
const r = 6371 // Earth radius in kilometers const r = 6371 // Earth radius in kilometers
dLat := (lat2 - lat1) * math.Pi / 180 dLat := (lat2 - lat1) * math.Pi / 180
dLon := (lon2 - lon1) * math.Pi / 180 dLon := (lon2 - lon1) * math.Pi / 180
a := math.Sin(dLat/2)*math.Sin(dLat/2) + a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)* math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*
math.Sin(dLon/2)*math.Sin(dLon/2) math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return r * c return r * c
} }
func interpolate(lon1, lat1, lon2, lat2, ratio float64) (float64, float64) { func interpolate(lon1, lat1, lon2, lat2, ratio float64) (float64, float64) {
return lon1 + (lon2-lon1)*ratio, lat1 + (lat2-lat1)*ratio 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)
if err != nil { if err != nil {
log.Printf("Fehler beim Löschen alter Einträge: %v\n", err) log.Printf("Fehler beim Löschen alter Einträge: %v\n", err)
return return
} }
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) { func logDatabaseStats(db *sql.DB) {
var count int var count int
err := db.QueryRow("SELECT COUNT(*) FROM trips").Scan(&count) err := db.QueryRow("SELECT COUNT(*) FROM trips").Scan(&count)
if err != nil { if err != nil {
log.Printf("Fehler beim Abrufen der Datenbankstatistiken: %v\n", err) log.Printf("Fehler beim Abrufen der Datenbankstatistiken: %v\n", err)
return return
} }
log.Printf("Aktuelle Anzahl der Einträge in der Datenbank: %d\n", count) log.Printf("Aktuelle Anzahl der Einträge in der Datenbank: %d\n", count)
} }

View file

@ -1,164 +0,0 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/google/uuid"
)
type Departure struct {
CurrentTripPosition struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
} `json:"currentTripPosition"`
When time.Time `json:"when"`
Line struct {
Name string `json:"name"`
FahrtNr string `json:"fahrtNr"`
} `json:"line"`
}
type APIResponse struct {
Departures []Departure `json:"departures"`
}
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("Anwendung gestartet")
dbDSN := os.Getenv("DB_DSN")
apiBaseURL := os.Getenv("API_BASE_URL")
maxResults, err := strconv.Atoi(os.Getenv("MAX_RESULTS"))
if err != nil {
log.Fatalf("Ungültiger Wert für MAX_RESULTS: %v", err)
}
duration, err := strconv.Atoi(os.Getenv("DURATION"))
if err != nil {
log.Fatalf("Ungültiger Wert für DURATION: %v", err)
}
includeBus, err := strconv.ParseBool(os.Getenv("BUS"))
if err != nil {
log.Fatalf("Ungültiger Wert für BUS: %v", err)
}
includeFerry, err := strconv.ParseBool(os.Getenv("FERRY"))
if err != nil {
log.Fatalf("Ungültiger Wert für FERRY: %v", err)
}
includeTram, err := strconv.ParseBool(os.Getenv("TRAM"))
if err != nil {
log.Fatalf("Ungültiger Wert für TRAM: %v", err)
}
includeTaxi, err := strconv.ParseBool(os.Getenv("TAXI"))
if err != nil {
log.Fatalf("Ungültiger Wert für TAXI: %v", err)
}
deleteAfter, err := strconv.Atoi(os.Getenv("DELETE_AFTER_MINUTES"))
if err != nil {
log.Fatalf("Ungültiger Wert für DELETE_AFTER_MINUTES: %v", err)
}
stationIDs := strings.Split(os.Getenv("STATION_IDS"), ",")
db, err := sql.Open("mysql", dbDSN)
if err != nil {
log.Fatal("Fehler beim Verbinden mit der Datenbank: ", err)
}
defer db.Close()
for {
for _, stationID := range stationIDs {
departures := fetchDepartures(apiBaseURL, stationID, maxResults, duration, includeBus, includeFerry, includeTram, includeTaxi)
for _, dep := range departures {
savePosition(db, dep)
}
}
deleteOldEntries(db, deleteAfter)
time.Sleep(1 * time.Minute)
}
}
func fetchDepartures(apiBaseURL, stationID string, maxResults, duration int, includeBus, includeFerry, includeTram, includeTaxi bool) []Departure {
url := fmt.Sprintf("%s/stops/%s/departures?results=%d&duration=%d&bus=%t&ferry=%t&tram=%t&taxi=%t",
apiBaseURL, stationID, maxResults, duration, includeBus, includeFerry, includeTram, includeTaxi)
resp, err := http.Get(url)
if err != nil {
log.Printf("Fehler beim Abrufen der Abfahrten für Station %s: %v\n", stationID, err)
return nil
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Fehler beim Lesen der Antwort für Station %s: %v\n", stationID, err)
return nil
}
if len(body) == 0 {
log.Printf("Leere Antwort vom Server für Station %s erhalten\n", stationID)
return nil
}
var response APIResponse
err = json.Unmarshal(body, &response)
if err != nil {
log.Printf("Fehler beim Dekodieren der Abfahrten für Station %s: %v\nAntwort-Body: %s\n", stationID, err, string(body))
return nil
}
log.Printf("Erfolgreich %d Abfahrten für Station %s abgerufen\n", len(response.Departures), stationID)
return response.Departures
}
func savePosition(db *sql.DB, dep Departure) {
if dep.CurrentTripPosition.Latitude == 0 && dep.CurrentTripPosition.Longitude == 0 {
log.Println("Keine gültige Position verfügbar")
return
}
today := time.Now().Format("2006-01-02")
var existingID string
err := db.QueryRow("SELECT id FROM trips WHERE fahrt_nr = ? AND DATE(timestamp) = ?", dep.Line.FahrtNr, today).Scan(&existingID)
if err == sql.ErrNoRows {
id := uuid.New().String()
_, err = db.Exec("INSERT INTO trips (id, latitude, longitude, timestamp, train_name, fahrt_nr) VALUES (?, ?, ?, ?, ?, ?)",
id, dep.CurrentTripPosition.Latitude, dep.CurrentTripPosition.Longitude, dep.When, dep.Line.Name, dep.Line.FahrtNr)
if err != nil {
log.Printf("Fehler beim Speichern der neuen Position: %v\n", err)
} else {
log.Printf("Neue Position gespeichert (ID: %s, Zug: %s, FahrtNr: %s)\n", id, dep.Line.Name, dep.Line.FahrtNr)
}
} else if err == nil {
_, err = db.Exec("UPDATE trips SET latitude = ?, longitude = ?, timestamp = ?, train_name = ? WHERE id = ?",
dep.CurrentTripPosition.Latitude, dep.CurrentTripPosition.Longitude, dep.When, dep.Line.Name, existingID)
if err != nil {
log.Printf("Fehler beim Aktualisieren der Position: %v\n", err)
} else {
log.Printf("Position aktualisiert (ID: %s, Zug: %s, FahrtNr: %s)\n", existingID, dep.Line.Name, dep.Line.FahrtNr)
}
} else {
log.Printf("Fehler bei der Überprüfung des existierenden Eintrags: %v\n", err)
}
}
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)
if err != nil {
log.Printf("Fehler beim Löschen alter Einträge: %v\n", err)
return
}
rowsAffected, _ := result.RowsAffected()
log.Printf("%d alte Einträge gelöscht\n", rowsAffected)
}