turash/bugulma/backend/internal/service/public_transport_schedule_service.go
2025-12-15 10:06:41 +01:00

147 lines
3.9 KiB
Go

package service
import (
"context"
"sort"
"time"
"bugulma/backend/internal/domain"
"bugulma/backend/internal/repository"
)
// Departure describes a single upcoming departure event
type Departure struct {
Time time.Time `json:"time"`
TripID string `json:"trip_id"`
RouteID string `json:"route_id"`
RouteShort string `json:"route_short_name"`
StopID string `json:"stop_id"`
}
// ScheduleService handles GTFS schedule queries
type ScheduleService struct {
gtfsRepo *repository.PublicTransportGTFSRepository
ptRepo domain.PublicTransportRepository
}
func NewScheduleService(gtfsRepo *repository.PublicTransportGTFSRepository, ptRepo domain.PublicTransportRepository) *ScheduleService {
return &ScheduleService{gtfsRepo: gtfsRepo, ptRepo: ptRepo}
}
// GetNextDepartures returns the next departures for a stop starting from fromTime
func (s *ScheduleService) GetNextDepartures(ctx context.Context, stopID string, fromTime time.Time, limit int) ([]Departure, error) {
// Load stop_times for the stop
stopTimes, err := s.gtfsRepo.ListStopTimesByStop(ctx, stopID)
if err != nil {
return nil, err
}
// Build a map of serviceIDs active today
today := fromTime.UTC()
activeServices := map[string]bool{}
// For each stop_time, check trip->service calendar
// To keep this efficient, we'll iterate stopTimes and inspect their trip's service via DB
var departures []Departure
for _, st := range stopTimes {
// quick filter by departure secs
if st.DepartureSecs < secondsSinceMidnight(fromTime) {
continue
}
// get trip
trip, err := s.gtfsRepo.GetTripByID(ctx, st.TripID)
if err != nil || trip == nil {
continue
}
// check service calendar
svcID := trip.ServiceID
if svcID == "" {
continue
}
if !activeServices[svcID] {
// check calendar
cal, err := s.gtfsRepo.GetCalendarByServiceID(ctx, svcID)
active := false
if err == nil && cal != nil {
yy, mm, dd := today.Date()
todayDate := time.Date(yy, mm, dd, 0, 0, 0, 0, time.UTC)
if !todayDate.Before(cal.StartDate) && !todayDate.After(cal.EndDate) {
weekday := today.Weekday()
switch weekday {
case time.Monday:
active = cal.Monday
case time.Tuesday:
active = cal.Tuesday
case time.Wednesday:
active = cal.Wednesday
case time.Thursday:
active = cal.Thursday
case time.Friday:
active = cal.Friday
case time.Saturday:
active = cal.Saturday
case time.Sunday:
active = cal.Sunday
}
}
}
// TODO: consider calendar_dates exceptions (not implemented yet)
if active {
activeServices[svcID] = true
} else {
activeServices[svcID] = false
}
}
if !activeServices[svcID] {
continue
}
// Get route for trip
var route domain.PublicTransportRoute
if rt, err := s.gtfsRepo.GetRouteByTripID(ctx, st.TripID); err == nil && rt != nil {
route = *rt
}
// compute departure time as today's date + departure secs
dep := secondsToTimeUTC(fromTime, st.DepartureSecs)
departures = append(departures, Departure{
Time: dep,
TripID: st.TripID,
RouteID: route.ID,
RouteShort: route.ShortName,
StopID: st.StopID,
})
}
// For frequencies we would need to expand entries - TODO: support frequencies expansion in future
// Sort departures by time
sort.Slice(departures, func(i, j int) bool {
return departures[i].Time.Before(departures[j].Time)
})
if limit > 0 && len(departures) > limit {
departures = departures[:limit]
}
return departures, nil
}
func secondsSinceMidnight(t time.Time) int {
h := t.Hour()
m := t.Minute()
s := t.Second()
return h*3600 + m*60 + s
}
func secondsToTimeUTC(base time.Time, secs int) time.Time {
// compute midnight of base date then add secs (handle > 86400)
y, mo, d := base.Date()
midnight := time.Date(y, mo, d, 0, 0, 0, 0, time.UTC)
return midnight.Add(time.Duration(secs) * time.Second)
}