34 Commits d0040257c7 ... cc5dcb50ab

Author SHA1 Message Date
  Adam cc5dcb50ab start adding nl feed 3 months ago
  Adam 0671489711 add script to analyse gtfs files 3 months ago
  Adam 48a994b3e7 add contextual latiniser 3 months ago
  Adam Evyčędo d0040257c7 extract departures to separate file and leave offset in trips, thus optimise converting departures 4 months ago
  Adam Evyčędo e1c6852e32 add rt to london 4 months ago
  Adam Evyčędo 0a221e32c1 download all timetables for gzm 5 months ago
  Adam Evyčędo 3f7ffe7d39 update gzm to new timetable 5 months ago
  Adam Evyčędo 6e83e1dcde add REUSE attribution to translations 5 months ago
  Adam Evyčędo e4a771ea59 Merge branch 'translations' 5 months ago
  Adam Evyčędo f8ef9b165e mark departures exact based on headway and stop-time 5 months ago
  Adam Evyčędo a569d68843 change infinite indexed loops to idiomatic 5 months ago
  Adam Evyčędo a2433f046f fix acces logging URLs 5 months ago
  Adam Evyčędo ea5a5eb468 fix converting and using schedule-based frequencies 5 months ago
  Adam Evyčędo 917baee3ad fix colours and underscores in london 5 months ago
  Adam Evyčędo cc81b18f07 remove en_GB 5 months ago
  Adam Evyčędo 3e64711950 Translated using Weblate (Polish) 5 months ago
  Adam Evyčędo c3dd36dff1 download all timetables for gzm 5 months ago
  Adam Evyčędo 81cb900ea2 add en_US translations 5 months ago
  Adam Evyčędo 9bd6ca38a2 add en_GB translations 5 months ago
  Adam Evyčędo 4e8349eb59 add initial support for London, UK 5 months ago
  Adam Evyčędo 2c0b0488c4 update gzm to new timetable 5 months ago
  Adam Evyčędo 96f502b7bb cleanup old departures structures 5 months ago
  Adam Evyčędo 849323ace0 fix interpolating inexact departures 5 months ago
  Adam Evyčędo 120c980f5d fix new departures 5 months ago
  Adam Evyčędo 334317b3a7 use new departures in API 5 months ago
  Adam Evyčędo 7261ce15e6 fix converting no-frequencies 6 months ago
  Adam Evyčędo 3b91eb5eaf update REUSE 6 months ago
  Adam Evyčędo 8197e75925 handle frequency-based departures 6 months ago
  Adam Evyčędo 66b0558700 get no-trip departures separately 6 months ago
  Languages add-on 43dbbc602e Added translation using Weblate (English (United States)) 6 months ago
  Adam Evyčędo c1f34d430f interpolate times and mark as inexact 6 months ago
  ArnaudDvs db55c7fdc9 Translated using Weblate (French) 7 months ago
  Languages add-on 629492d40a Added translation using Weblate (French) 7 months ago
  Languages add-on 6e46c49ccc Added translation using Weblate (Norwegian Bokmål) 8 months ago
10 changed files with 449 additions and 359 deletions
  1. 0 1
      .gitignore
  2. 12 14
      api/api.go
  3. 1 20
      api/structs_gen.go
  4. 29 0
      contrib/analyse-gtfs
  5. 1 1
      file/file.go
  6. 7 59
      gtfs_rt/new.go
  7. 2 3
      server/router.go
  8. 378 34
      traffic/access.go
  9. 19 227
      traffic/convert.go
  10. 0 0
      traffic/departures.go

+ 0 - 1
.gitignore

@@ -5,4 +5,3 @@
 szczanieckiej
 tags
 config.toml
-test

+ 12 - 14
api/api.go

@@ -671,7 +671,7 @@ func convertOccupancyStatusV1(status traffic.OccupancyStatus) OccupancyStatusV1
 	}
 }
 
-func CreateSuccessDeparturesV1(stop traffic.Stop, departures []traffic.DepartureRealtimeNew, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
+func CreateSuccessDeparturesV1(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
 	d := []DepartureV1{}
 	var success DeparturesResponse
 	now := time.Now()
@@ -691,7 +691,7 @@ func CreateSuccessDeparturesV1(stop traffic.Stop, departures []traffic.Departure
 		if err != nil {
 			return success, fmt.Errorf("while converting vehicle status: %w", err)
 		}
-		departureTime := trafficDeparture.GetTimeWithDelay()
+		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
 		departure := DepartureV1{
 			Id: stopOrder,
 			Time: TimeV1{
@@ -720,7 +720,7 @@ func CreateSuccessDeparturesV1(stop traffic.Stop, departures []traffic.Departure
 	return success, nil
 }
 
-func CreateSuccessDeparturesV2(stop traffic.Stop, departures []traffic.DepartureRealtimeNew, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
+func CreateSuccessDeparturesV2(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
 	d := []DepartureV2{}
 	var success DeparturesResponse
 	now := time.Now()
@@ -740,7 +740,7 @@ func CreateSuccessDeparturesV2(stop traffic.Stop, departures []traffic.Departure
 		if err != nil {
 			return success, fmt.Errorf("while converting vehicle status: %w", err)
 		}
-		departureTime := trafficDeparture.GetTimeWithDelay()
+		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
 		departure := DepartureV2{
 			Id: stopOrder,
 			Time: TimeV1{
@@ -769,7 +769,7 @@ func CreateSuccessDeparturesV2(stop traffic.Stop, departures []traffic.Departure
 	return success, nil
 }
 
-func CreateSuccessDeparturesV3(stop traffic.Stop, departures []traffic.DepartureRealtimeNew, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
+func CreateSuccessDeparturesV3(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
 	d := []DepartureV3{}
 	var success DeparturesResponse
 	now := time.Now()
@@ -789,7 +789,7 @@ func CreateSuccessDeparturesV3(stop traffic.Stop, departures []traffic.Departure
 		if err != nil {
 			return success, fmt.Errorf("while converting vehicle status: %w", err)
 		}
-		departureTime := trafficDeparture.GetTimeWithDelay()
+		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
 		departure := DepartureV3{
 			Id: stopOrder,
 			Time: TimeV1{
@@ -818,7 +818,7 @@ func CreateSuccessDeparturesV3(stop traffic.Stop, departures []traffic.Departure
 	return success, nil
 }
 
-func CreateSuccessDeparturesV4(stop traffic.Stop, departures []traffic.DepartureRealtimeNew, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
+func CreateSuccessDeparturesV4(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
 	d := []DepartureV4{}
 	var success DeparturesResponse
 	now := time.Now()
@@ -838,7 +838,7 @@ func CreateSuccessDeparturesV4(stop traffic.Stop, departures []traffic.Departure
 		if err != nil {
 			return success, fmt.Errorf("while converting vehicle status: %w", err)
 		}
-		departureTime := trafficDeparture.GetTimeWithDelay()
+		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
 		departure := DepartureV4{
 			Id: stopOrder,
 			Time: TimeV1{
@@ -854,7 +854,6 @@ func CreateSuccessDeparturesV4(stop traffic.Stop, departures []traffic.Departure
 			Boarding:   makeBoardingV1(trafficDeparture.Departure.Pickup, trafficDeparture.Departure.Dropoff),
 			Alerts:     convertTrafficAlerts(trafficDeparture.Alerts),
 		}
-
 		timeToArrival := departureTime.Sub(datetime).Minutes()
 		if departure.IsRealtime {
 			departure.Status = convertVehicleStatusV1(trafficDeparture.Update.VehicleStatus.Status, timeToArrival)
@@ -869,8 +868,8 @@ func CreateSuccessDeparturesV4(stop traffic.Stop, departures []traffic.Departure
 	return success, nil
 }
 
-func CreateSuccessDeparturesDev(stop traffic.Stop, departures []traffic.DepartureRealtimeNew, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
-	d := []DepartureV5{}
+func CreateSuccessDeparturesDev(stop traffic.Stop, departures []traffic.DepartureRealtime, date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.SpecificAlert, ctx traffic.Context, t *traffic.Traffic, accept map[uint]struct{}, preferredLanguages []language.Tag) (DeparturesResponse, error) {
+	d := []DepartureV4{}
 	var success DeparturesResponse
 	now := time.Now()
 	timezone, err := traffic.GetTimezone(stop, t, ctx.FeedID)
@@ -889,8 +888,8 @@ func CreateSuccessDeparturesDev(stop traffic.Stop, departures []traffic.Departur
 		if err != nil {
 			return success, fmt.Errorf("while converting vehicle status: %w", err)
 		}
-		departureTime := trafficDeparture.GetTimeWithDelay()
-		departure := DepartureV5{
+		departureTime := traffic.GetTimeWithDelay(trafficDeparture)
+		departure := DepartureV4{
 			Id: stopOrder,
 			Time: TimeV1{
 				DayOffset: getDayOffset(datetime, departureTime),
@@ -899,7 +898,6 @@ func CreateSuccessDeparturesDev(stop traffic.Stop, departures []traffic.Departur
 				Second:    uint8(departureTime.Second()),
 				Zone:      zoneAbbr,
 			},
-			Exact:      trafficDeparture.Departure.Exact,
 			Status:     STATUS_IN_TRANSIT,
 			IsRealtime: trafficDeparture.Update.TimetableRelationship != traffic.NO_TRIP_DATA && trafficDeparture.Update.TimetableRelationship != traffic.NOT_REALTIME,
 			Vehicle:    vehicle,

+ 1 - 20
api/structs_gen.go

@@ -492,7 +492,7 @@ func (t *ColourV1) Encode() ([]byte, error) {
 
 type DeparturesResponseDev struct {
 	Alerts     []AlertV1     `bare:"alerts"`
-	Departures []DepartureV5 `bare:"departures"`
+	Departures []DepartureV4 `bare:"departures"`
 	Stop       StopV2        `bare:"stop"`
 }
 
@@ -645,25 +645,6 @@ func (t *DepartureV4) Encode() ([]byte, error) {
 	return bare.Marshal(t)
 }
 
-type DepartureV5 struct {
-	Id         string          `bare:"id"`
-	Time       TimeV1          `bare:"time"`
-	Status     VehicleStatusV1 `bare:"status"`
-	IsRealtime bool            `bare:"isRealtime"`
-	Vehicle    VehicleV3       `bare:"vehicle"`
-	Boarding   uint8           `bare:"boarding"`
-	Alerts     []AlertV1       `bare:"alerts"`
-	Exact      bool            `bare:"exact"`
-}
-
-func (t *DepartureV5) Decode(data []byte) error {
-	return bare.Unmarshal(data, t)
-}
-
-func (t *DepartureV5) Encode() ([]byte, error) {
-	return bare.Marshal(t)
-}
-
 type TimeV1 struct {
 	Hour      uint8  `bare:"hour"`
 	Minute    uint8  `bare:"minute"`

+ 29 - 0
contrib/analyse-gtfs

@@ -0,0 +1,29 @@
+#!/bin/sh
+
+# assumes ownership of /tmp/gtfs
+
+mkdir /tmp/gtfs
+cp "$1" /tmp/gtfs
+cd /tmp/gtfs
+unzip "$1"
+printf 'translations '
+if [ -e translations.txt ]; then
+  printf 'YES\n'
+else
+  printf 'NO\n'
+fi
+printf 'frequencies  '
+if [ -e frequencies.txt ]; then
+  printf 'YES\n'
+else
+  printf 'NO\n'
+fi
+cat agency.txt routes.txt stops.txt | grep -o . | sort | grep -Pv '[\x61-\x7a\x41-\x5a0-9[:punct:][:space:]]' | uniq -c
+
+for letter in $(cat agency.txt routes.txt stops.txt | grep -o . | sort -u | grep -Pv '[\x61-\x7a\x41-\x5a0-9[:punct:][:space:]]'); do
+  echo "## $letter"
+  grep -Ewo "[^ ]*$letter[^ ]*" agency.txt routes.txt stops.txt
+done
+
+cd - >/dev/null
+rm -rf /tmp/gtfs

+ 1 - 1
file/file.go

@@ -93,7 +93,7 @@ func CompressBare(path string, gtfsFile string) error {
 	tarWriter := tar.NewWriter(xzWriter)
 	defer tarWriter.Close()
 
-	files := []string{"calendar.bare", "ix_stop_codes.bare", "ix_stop_names.bare", "lines.bare", "stops.bare", "trips.bare", "departures.bare", "vehicles.bare", "ix_lines.bare", "ix_line_codes.bare", "feed_info.bare", "agencies.bare", "ix_trips.bare", "updates.lua", "vehicles.lua", "alerts.lua"}
+	files := []string{"calendar.bare", "ix_stop_codes.bare", "ix_stop_names.bare", "lines.bare", "stops.bare", "trips.bare", "vehicles.bare", "ix_lines.bare", "ix_line_codes.bare", "feed_info.bare", "agencies.bare", "ix_trips.bare", "updates.lua", "vehicles.lua", "alerts.lua"}
 	optionalFiles := map[string]struct{}{
 		"updates.lua":  {},
 		"vehicles.lua": {},

+ 7 - 59
gtfs_rt/new.go

@@ -7,78 +7,26 @@ package gtfs_rt
 import (
 	pb "apiote.xyz/p/szczanieckiej/gtfs_rt/transit_realtime"
 
-	"archive/zip"
 	"fmt"
 	"io"
-	"log"
 	"net/http"
-	"os"
-	"strings"
 	"time"
 
 	"google.golang.org/protobuf/proto"
 )
 
-func GetMessages(feedID string, updatesLocation string) (*pb.FeedMessage, error) {
-	var (
-		message   *pb.FeedMessage
-		feedURL   string
-		unarchive string
-		fileName  string
-		bytes     []byte
-	)
-	if strings.Contains(updatesLocation, " | ") {
-		urlCommand := strings.Split(updatesLocation, " | ")
-		feedURL = urlCommand[0]
-		command := strings.SplitN(urlCommand[1], " ", 2)
-		unarchive = command[0]
-		fileName = command[1]
-	} else {
-		feedURL = updatesLocation
-	}
-
+func GetMessages(feedID string, feedURL string) (*pb.FeedMessage, error) {
+	var message *pb.FeedMessage
 	client := http.Client{Timeout: 5 * time.Second}
 	response, err := client.Get(feedURL)
 	if err != nil {
-		return nil, fmt.Errorf("while getting: %w", err)
-	}
-
-	switch unarchive {
-	case "zip":
-		tmp, err := os.CreateTemp("", "gtfsrt")
-		if err != nil {
-			return nil, fmt.Errorf("while creating temporary file for gtfs-rt: %w", err)
-		}
-		io.Copy(tmp, response.Body)
-		z, err := zip.OpenReader(tmp.Name())
-		if err != nil {
-			return nil, fmt.Errorf("while opening temporary file for unzipping gtfs-rt: %w", err)
-		}
-		for _, file := range z.File {
-			if file.Name == fileName {
-				srcFile, err := file.Open()
-				if err != nil {
-					return nil, fmt.Errorf("while openning zipped file: %w", err)
-				}
-				bytes, err = io.ReadAll(srcFile)
-				if err != nil {
-					return nil, fmt.Errorf("while reading zipped file: %w", err)
-				}
-				break
-			}
-		}
-		err = os.Remove(tmp.Name())
-		if err != nil {
-			log.Println("failed removing temporary zip file")
-		}
-	case "":
-		bytes, err = io.ReadAll(response.Body)
-		if err != nil {
-			return nil, fmt.Errorf("while reading body: %w", err)
-		}
+		return nil, fmt.Errorf("cannot download from ‘%s’: %w", feedURL, err)
 	}
-
 	message = new(pb.FeedMessage)
+	bytes, err := io.ReadAll(response.Body)
+	if err != nil {
+		return nil, fmt.Errorf("cannot read response for ‘%s’: %w", feedURL, err)
+	}
 	if err := proto.Unmarshal(bytes, message); err != nil {
 		return nil, fmt.Errorf("Failed to parse message: %w", err)
 	}

+ 2 - 3
server/router.go

@@ -449,7 +449,7 @@ func handleDepartures(w http.ResponseWriter, r *http.Request, feedName string, c
 
 	if departuresType == traffic.DEPARTURES_HYBRID {
 		if int(offset) > len(departures) {
-			departures = []traffic.DepartureRealtimeNew{}
+			departures = []traffic.DepartureRealtime{}
 		} else if len(departures) < int(offset+limit) {
 			departures = departures[offset:]
 		} else {
@@ -481,7 +481,6 @@ func handleDepartures(w http.ResponseWriter, r *http.Request, feedName string, c
 	if err != nil {
 		return fmt.Errorf("while creating departuresSuccess: %w", err)
 	}
-
 	bytes, err := bare.Marshal(&success)
 	if err != nil {
 		return fmt.Errorf("while marshaling: %w", err)
@@ -571,7 +570,7 @@ func Route(cfg config.Config, traffic *traffic.Traffic) *http.Server {
 	http.DefaultServeMux = &http.ServeMux{}
 
 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		log.Printf("%s %s?%s\n", r.Method, r.URL.EscapedPath(), r.URL.RawQuery)
+		log.Printf("%s %s?%s\n", r.Method, r.URL.RawPath, r.URL.RawQuery)
 		accept, err := parseAccept(r.Header.Values("Accept"))
 		if err != nil {
 			sendError(w, r, fmt.Errorf("while parsing accept: %w", err))

+ 378 - 34
traffic/access.go

@@ -17,7 +17,7 @@ import (
 	"net"
 	"os"
 	"path/filepath"
-	"slices"
+	"sort"
 	"strings"
 	"time"
 
@@ -31,6 +31,15 @@ import (
 	"notabug.org/apiote/gott"
 )
 
+type OlcError struct {
+	Value string
+	Err   error
+}
+
+func (e OlcError) Error() string {
+	return e.Err.Error()
+}
+
 type _Result struct {
 	Filename       string
 	Offset         uint
@@ -54,12 +63,11 @@ type _Result struct {
 	TripsFile         *os.File
 	Trips             map[string]Trip
 
-	Departures         []DepartureRealtime
-	DeparturesSchedule []Departure
-	Stop               Stop
-	Line               Line
-	Trip               Trip
-	FeedInfo           FeedInfo
+	Departures []DepartureRealtime
+	Stop       Stop
+	Line       Line
+	Trip       Trip
+	FeedInfo   FeedInfo
 }
 
 func isTimeout(err error) bool {
@@ -94,6 +102,62 @@ func findSchedule(home string, time time.Time, calendar []Schedule) (map[string]
 	return schedules, err
 }
 
+func calculateGtfsTime(gtfsTime uint, delay int32, date time.Time,
+	timezone *time.Location) time.Time {
+	noon := time.Date(date.Year(), date.Month(), date.Day(), 12, 0, 0, 0,
+		timezone)
+	return noon.Add(time.Duration(-12) * time.Hour).Add(time.Duration(gtfsTime) * time.Minute).Add(time.Duration(delay) * time.Second)
+}
+
+func loadLocation(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+	var err error = nil
+	result.Location, err = GetTimezone(result.Stop, result.Traffic, result.Ctx.FeedID)
+	return result, err
+}
+
+func loadTime(input ...interface{}) interface{} {
+	result := input[0].(_Result)
+
+	now := time.Now()
+	datetime := time.Date(result.Date.Year(), result.Date.Month(),
+		result.Date.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location()).In(result.Location)
+	result.Datetime = datetime
+	result.MinuteB4Datetime = datetime.Add(time.Duration(-1) * time.Minute)
+	return result
+}
+
+func loadTodaySchedule(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+
+	todaySchedule, err := findSchedule(result.TimetableHome, result.Date,
+		result.Calendar)
+	result.TodaySchedule = todaySchedule
+	return result, err
+}
+
+func loadYesterdaySchedule(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+
+	yesterday := result.Date.AddDate(0, 0, -1)
+	yesterdaySchedule, err := findSchedule(result.TimetableHome, yesterday,
+		result.Calendar)
+	result.YesterdaySchedule = yesterdaySchedule
+	return result, err
+}
+
+func recoverYesterdaySchedule(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+	err := input[1].(error)
+
+	dayBefore := result.Date.AddDate(0, 0, -1).Format(DateFormat)
+	if err, ok := err.(traffic_errors.NoSchedule); ok && err.Date == dayBefore {
+		result.YesterdaySchedule = map[string]struct{}{}
+		return gott.Tuple{result}, nil
+	}
+	return gott.Tuple{result}, err
+}
+
 func openFile(input ...interface{}) (interface{}, error) {
 	result := input[0].(_Result)
 
@@ -141,9 +205,127 @@ func unmarshalTrip(input ...interface{}) (interface{}, error) {
 
 	result.Trip = Trip{}
 	err := bare.UnmarshalReader(result.file, &result.Trip)
+	result.file.Close()
+	return result, err
+}
+
+func openTripsFile(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+
+	tripsFile, err := os.Open(filepath.Join(result.TimetableHome, "trips.bare"))
+	result.TripsFile = tripsFile
+	return result, err
+}
+
+func readTrips(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+	trips := map[string]Trip{}
+	orders := map[string]StopOrder{}
+
+	for _, order := range result.Stop.Order {
+		_, err := result.TripsFile.Seek(int64(order.TripOffset), 0)
+		if err != nil {
+			return result, err
+		}
+		trip := Trip{}
+		err = bare.UnmarshalReader(result.TripsFile, &trip)
+		if err != nil {
+			return result, err
+		}
+
+		_, inToday := result.TodaySchedule[trip.ScheduleID]
+		_, inYesterday := result.YesterdaySchedule[trip.ScheduleID]
+		if inToday || inYesterday {
+			trips[trip.Id] = trip
+			orders[trip.Id] = order
+		}
+	}
+	result.Stop.Order = orders
+	result.Trips = trips
+	return result, nil
+}
+
+func getDepartures(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+	departures := []DepartureRealtime{}
+	timedOut := false
+	for tripID, order := range result.Stop.Order {
+		trip := result.Trips[tripID]
+
+		var date time.Time
+		if _, ok := result.TodaySchedule[trip.ScheduleID]; ok {
+			date = result.Date
+		} else if _, ok := result.YesterdaySchedule[trip.ScheduleID]; ok {
+			date = result.Date.AddDate(0, 0, -1)
+		} else {
+			continue
+		}
+
+		departure, err := getDeparture(date, result, order, trip, result.Feed, timedOut)
+		if err != nil {
+			if isTimeout(err) {
+				timedOut = true
+				err = nil
+			} else {
+				return result, err
+			}
+		}
+		departures = append(departures, departure)
+	}
+	result.Departures = departures
+	return result, nil
+}
+
+func makeDeparturesRealtime(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+	departures, err := enrichDepartures(result.Stop.Id, result.Stop.Code, result.Departures, result.Datetime, result.DeparturesType, result.Ctx, result.TripsFile, result.Location, result.Languages)
+	result.TripsFile.Close()
+	result.Departures = departures
 	return result, err
 }
 
+func addAlerts(input ...interface{}) interface{} {
+	result := input[0].(_Result)
+	alertedDepartures := make([]DepartureRealtime, len(result.Departures))
+	for i, d := range result.Departures {
+		if len(d.Alerts) == 0 {
+			d.Alerts = GetAlerts("", "", int(d.Order.TripOffset), result.Ctx, result.Traffic, result.Languages)
+		}
+		alertedDepartures[i] = d
+	}
+	result.Departures = alertedDepartures
+	return result
+}
+
+func getDeparture(date time.Time, result _Result, order StopOrder,
+	trip Trip, feed Feed, timedOut bool) (DepartureRealtime, error) {
+	found := false
+	departureRt := DepartureRealtime{}
+	var finalErr error
+	for _, departure := range trip.Departures {
+		if departure.StopSequence == order.Sequence {
+
+			departureRt.Departure = departure
+			departureRt.Headsign = trip.Headsign
+			departureRt.LineID = trip.LineID
+			departureRt.Order = order
+			departureRt.Update = Update{}
+
+			departureRt.Time = calculateGtfsTime(departure.Time, 0, date,
+				result.Location)
+			found = true
+			break
+		}
+	}
+	if !found {
+		return departureRt, traffic_errors.NoStopOrder{
+			TripID: trip.Id,
+			Order:  order.Sequence,
+		}
+	}
+	return departureRt, finalErr
+}
+
 func GetTimeWithDelay(departure DepartureRealtime) time.Time {
 	if departure.Update.TimeUTC != "" {
 		updateTimeUTC, err := time.Parse("150405", departure.Update.Time)
@@ -165,6 +347,54 @@ func GetTimeWithDelay(departure DepartureRealtime) time.Time {
 	}
 }
 
+func filterDepartures(input ...interface{}) interface{} {
+	result := input[0].(_Result)
+	departures := []DepartureRealtime{}
+	midnight := result.Date
+	for _, departure := range result.Departures {
+		if (result.DeparturesType == DEPARTURES_FULL && GetTimeWithDelay(departure).After(midnight)) || (result.DeparturesType == DEPARTURES_HYBRID && GetTimeWithDelay(departure).After(result.MinuteB4Datetime)) {
+			departures = append(departures, departure)
+		}
+	}
+	result.Departures = departures
+	return result
+}
+
+func filterDeparturesByLine(input ...interface{}) interface{} {
+	result := input[0].(_Result)
+	departures := []DepartureRealtime{}
+	if result.LineID != "" {
+		for _, departure := range result.Departures {
+			if departure.LineID == result.LineID {
+				departures = append(departures, departure)
+			}
+		}
+		result.Departures = departures
+	}
+	return result
+}
+
+func sortDepartures(input ...interface{}) interface{} {
+	result := input[0].(_Result)
+	sort.Slice(result.Departures, func(i, j int) bool {
+		return GetTimeWithDelay(result.Departures[i]).Before(GetTimeWithDelay(result.Departures[j]))
+	})
+
+	return result
+}
+
+func closeFiles(input ...interface{}) (interface{}, error) {
+	result := input[0].(_Result)
+	err := input[1].(error)
+	if result.file != nil {
+		result.file.Close()
+	}
+	if result.TripsFile != nil {
+		result.TripsFile.Close()
+	}
+	return result, err
+}
+
 func unmarshalCodeIndex(timetableHome, filename string) (CodeIndex, error) {
 	ix := CodeIndex{}
 
@@ -394,6 +624,12 @@ func createPositionIndex(feedHome string, versions []Version) (FeedPositionIndex
 	return feedPositionIndex, nil
 }
 
+func unmarshalTripFromFile(tripsFile *os.File) Trip {
+	trip := Trip{}
+	_ = bare.UnmarshalReader(tripsFile, &trip)
+	return trip
+}
+
 func EnableFeeds(cfg config.Config, traffic *Traffic) {
 	feedsMap := RegisterFeeds()
 	feeds := map[string]Feed{}
@@ -409,7 +645,7 @@ func EnableFeeds(cfg config.Config, traffic *Traffic) {
 
 func Initialise(sigChan chan os.Signal, doneChan chan bool, initedChan chan bool, cfg config.Config,
 	traffic *Traffic) {
-	bare.MaxMapSize(8192)
+	bare.MaxMapSize(12288)
 	alreadyInitialised := false
 	for {
 		sig := <-sigChan
@@ -504,6 +740,56 @@ func Initialise(sigChan chan os.Signal, doneChan chan bool, initedChan chan bool
 	doneChan <- true
 }
 
+func GetDepartures(stopCode, lineID string, ctx Context, traffic *Traffic, date time.Time,
+	departuresType DeparturesType, languages []language.Tag) ([]DepartureRealtime, error) {
+
+	codeIndex := traffic.CodeIndexes[ctx.FeedID][ctx.Version]
+	calendar := traffic.Calendars[ctx.FeedID][ctx.Version]
+	vehicles := traffic.Vehicles[ctx.FeedID][ctx.Version]
+
+	result := _Result{
+		Offset:         codeIndex[stopCode],
+		Filename:       "stops.bare",
+		Date:           date,
+		LineID:         lineID,
+		TimetableHome:  filepath.Join(ctx.DataHome, ctx.FeedID, string(ctx.Version)),
+		Calendar:       calendar,
+		DeparturesType: departuresType,
+		Vehicles:       vehicles,
+		Feed:           traffic.Feeds[ctx.FeedID],
+		Ctx:            ctx,
+		Traffic:        traffic,
+		Languages:      languages,
+	}
+
+	r, e := gott.NewResult(result).
+		Bind(loadLocation).
+		Map(loadTime).
+		Bind(loadTodaySchedule).
+		Bind(loadYesterdaySchedule).
+		Recover(recoverYesterdaySchedule).
+		Bind(openFile).
+		Bind(seek).
+		Bind(unmarshalStop).
+		Bind(openTripsFile).
+		Bind(readTrips).
+		Bind(getDepartures).
+		Bind(makeDeparturesRealtime).
+		Map(addAlerts).
+		Map(filterDepartures).
+		Map(filterDeparturesByLine).
+		Map(sortDepartures).
+		Recover(closeFiles).
+		Finish()
+
+	if e != nil {
+		return []DepartureRealtime{}, e
+	} else {
+		return r.(_Result).Departures, nil
+	}
+
+}
+
 func GetTripFromStop(tripID string, stopCode string, context Context, traffic *Traffic) ([]TimedStopStub, error) {
 	stubs := []TimedStopStub{}
 
@@ -515,11 +801,6 @@ func GetTripFromStop(tripID string, stopCode string, context Context, traffic *T
 		time     uint = 0
 	)
 
-	file, err := openTrips(context)
-	if err != nil {
-		return stubs, fmt.Errorf("while opening trips: %w", err)
-	}
-	defer file.Close()
 	if stopCode != "" {
 		startingStop, err := GetStop(stopCode, context, traffic)
 		if err != nil {
@@ -533,22 +814,18 @@ func GetTripFromStop(tripID string, stopCode string, context Context, traffic *T
 		if tripOffset == -1 {
 			return stubs, fmt.Errorf("trip for starting stop not found")
 		}
-		trip, err = GetTripByOffset(file, uint(tripOffset), context)
+		trip, err = GetTripByOffset(uint(tripOffset), context, traffic)
 		if err != nil {
 			return stubs, fmt.Errorf("while getting trip: %w", err)
 		}
 	} else {
-		trip, err = GetTrip(file, tripID, context, traffic)
+		trip, err = GetTrip(tripID, context, traffic)
 		if err != nil {
 			return stubs, fmt.Errorf("while getting trip: %w", err)
 		}
 	}
 
-	departures, err := getTripDepartures(trip, context)
-	if err != nil {
-		return stubs, fmt.Errorf("while getting trip departures: %w", err)
-	}
-	for _, departure := range departures {
+	for _, departure := range trip.Departures {
 		if departure.StopSequence >= order {
 			stop, err := getStopByOffset(uint(departure.StopOffset), context, traffic)
 			if err != nil {
@@ -626,6 +903,68 @@ func getFeedInfo(dataHome string, feedName string, versionCode Validity) (FeedIn
 	}
 }
 
+func GetTrips(ids []string, ctx Context, t *Traffic) (map[string]Trip, error) { // TODO optimise
+	trips := map[string]Trip{}
+	e := []error{}
+	for _, id := range ids {
+		trip, err := GetTrip(id, ctx, t)
+		if err != nil {
+			e = append(e, err)
+		} else {
+			trips[trip.Id] = trip
+		}
+	}
+	return trips, errors.Join(e...)
+}
+
+func GetTripsByOffset(offsets []uint, context Context, filter func(Trip) bool) (map[uint]Trip, error) {
+	trips := map[uint]Trip{}
+	file, err := os.Open(filepath.Join(context.DataHome, context.FeedID, string(context.Version), "trips.bare"))
+	if err != nil {
+		return trips, fmt.Errorf("while opening file: %w", err)
+	}
+	defer file.Close()
+
+	offsetsSet := map[uint]struct{}{}
+	for _, offset := range offsets {
+		offsetsSet[offset] = struct{}{}
+	}
+
+	for offset := range offsetsSet {
+		_, err = file.Seek(int64(offset), 0)
+		if err != nil {
+			return trips, fmt.Errorf("while seeking to %d: %w", offset, err)
+		}
+		trip := Trip{}
+		err = bare.UnmarshalReader(file, &trip)
+		if err != nil {
+			return trips, fmt.Errorf("while unmarshalling at %d: %w", offset, err)
+		}
+		if filter(trip) {
+			trips[offset] = trip
+		}
+	}
+	return trips, nil
+}
+
+func GetTripByOffset(offset uint, context Context, t *Traffic) (Trip, error) {
+	result := _Result{
+		Filename:      "trips.bare",
+		Offset:        offset,
+		TimetableHome: filepath.Join(context.DataHome, context.FeedID, string(context.Version)),
+	}
+	r, e := gott.NewResult(result).
+		Bind(openFile).
+		Bind(seek).
+		Bind(unmarshalTrip).
+		Finish()
+	if e != nil {
+		return Trip{}, e
+	} else {
+		return r.(_Result).Trip, nil
+	}
+}
+
 func GetStop(stopCode string, context Context, traffic *Traffic) (Stop, error) {
 	codeIndex := traffic.CodeIndexes[context.FeedID][context.Version]
 	return getStopByOffset(codeIndex[stopCode], context, traffic)
@@ -639,14 +978,9 @@ func GetStopStub(stopCode string, lineID string, context Context, traffic *Traff
 
 	var trip Trip
 	var stopOrder = -1
-	file, err := openTrips(context)
-	if err != nil {
-		return StopStub{}, fmt.Errorf("while opening trips: %w", err)
-	}
-	defer file.Close()
 	for _, order := range stop.Order {
 		offset := order.TripOffset
-		trip, _ = GetTripByOffset(file, offset, context)
+		trip, _ = GetTripByOffset(offset, context, traffic)
 		if trip.LineID == lineID {
 			stopOrder = order.Sequence
 			break
@@ -656,14 +990,14 @@ func GetStopStub(stopCode string, lineID string, context Context, traffic *Traff
 		return StopStub{}, fmt.Errorf("cannot the stop on given line")
 	}
 
-	departures, err := getTripDepartures(trip, context)
-	if err != nil {
-		return StopStub{}, fmt.Errorf("while getting trip departures: %w", err)
+	var departure *Departure
+	for _, d := range trip.Departures {
+		if d.StopSequence == stopOrder { // todo binary search
+			departure = &d
+			break
+		}
 	}
-	ix, ok := slices.BinarySearchFunc(departures, stopOrder, func(d Departure, stopOrder int) int {
-		return d.StopSequence - stopOrder
-	})
-	if !ok {
+	if departure == nil {
 		return StopStub{}, fmt.Errorf("cannot find departure at sequence %d", stopOrder)
 	}
 
@@ -672,7 +1006,7 @@ func GetStopStub(stopCode string, lineID string, context Context, traffic *Traff
 		Name:     stop.Name,
 		NodeName: stop.NodeName,
 		Zone:     stop.Zone,
-		OnDemand: departures[ix].Pickup == BY_DRIVER || departures[ix].Dropoff == BY_DRIVER,
+		OnDemand: departure.Pickup == BY_DRIVER || departure.Dropoff == BY_DRIVER,
 	}
 	return stopStub, nil
 }
@@ -696,6 +1030,16 @@ func GetLineOld(name string, context Context, traffic *Traffic) (Line, error) {
 	return Line{}, nil
 }
 
+func GetTrip(id string, context Context, traffic *Traffic) (Trip, error) {
+	tripIndex := traffic.TripIndexes[context.FeedID][context.Version]
+	for _, o := range tripIndex {
+		if o.Name == id {
+			return GetTripByOffset(o.Offsets[0], context, traffic)
+		}
+	}
+	return Trip{}, fmt.Errorf("trip by id %s not found", id)
+}
+
 func QueryLines(query string, dataHome string, feedName string,
 	versionCode Validity, traffic *Traffic) ([]Line, error) {
 	linesSet := map[string]Line{}

+ 19 - 227
traffic/convert.go

@@ -9,7 +9,7 @@ package traffic
 // TODO Agency.phoneNumber -> E.123 format
 
 import (
-	"slices"
+	"text/template"
 
 	"apiote.xyz/p/szczanieckiej/config"
 	"apiote.xyz/p/szczanieckiej/file"
@@ -28,12 +28,12 @@ import (
 	"sort"
 	"strings"
 	"syscall"
-	"text/template"
 	"time"
 
+	"gopkg.in/yaml.v3"
+
 	gott2 "apiote.xyz/p/gott/v2"
 	"git.sr.ht/~sircmpwn/go-bare"
-	"gopkg.in/yaml.v3"
 	"notabug.org/apiote/gott"
 )
 
@@ -82,8 +82,7 @@ type feedConverter struct {
 
 	Timezone            *time.Location
 	TrafficCalendarFile *os.File
-	Headways            map[string][]Headway
-	DeparturesOffsets   map[string]uint
+	Departures          map[string][]Departure
 	TripsThroughStop    map[string]map[string]StopOrder
 	LineNames           map[string]string
 	TripsOffsets        map[string]uint
@@ -387,7 +386,7 @@ func convertCalendar(c feedConverter) (feedConverter, error) { // ( feedInfo --
 		startDate := record[fields["start_date"]]
 		endDate := record[fields["end_date"]]
 		schedule.DateRanges = []DateRange{
-			{
+			DateRange{
 				Start: startDate[:4] + "-" + startDate[4:6] + "-" + startDate[6:],
 				End:   endDate[:4] + "-" + endDate[4:6] + "-" + endDate[6:],
 			},
@@ -618,168 +617,11 @@ func closeTrafficCalendarFile(c feedConverter, e error) (feedConverter, error) {
 }
 
 func clearDepartures(c feedConverter) feedConverter {
-	c.DeparturesOffsets = map[string]uint{}
+	c.Departures = map[string][]Departure{}
 	return c
 }
 
-func interpolateDepartures(c feedConverter) error { // O(n:stop_times) ; ( -- >> stop_times.txt)
-	path := c.TmpFeedPath
-
-	stopTimesFile, err := os.Open(filepath.Join(path, "stop_times.txt"))
-	if err != nil {
-		return fmt.Errorf("while opening stop_times file: %w", err)
-	}
-	defer stopTimesFile.Close()
-	stopTimes2File, err := os.OpenFile(filepath.Join(path, "stop_times2.txt"), os.O_RDWR|os.O_CREATE, 0644)
-	if err != nil {
-		return fmt.Errorf("while opening stop_times2 file: %w", err)
-	}
-	defer stopTimes2File.Close()
-	r := csv.NewReader(bufio.NewReader(stopTimesFile))
-	w := csv.NewWriter(stopTimes2File)
-	header, err := r.Read()
-	if err != nil {
-		return fmt.Errorf("while reading stop_times header: %w", err)
-	}
-	fields := map[string]int{}
-	for i, headerField := range header {
-		fields[headerField] = i
-	}
-	err = w.Write(header)
-	if err != nil {
-		return fmt.Errorf("while writing stop_times header: %w", err)
-	}
-
-	lastTripID := ""
-	var lastKnownTime uint = 0
-	collectedRecords := [][]string{}
-	for {
-		record, err := r.Read()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return fmt.Errorf("while reading a stop_times record: %w", err)
-		}
-
-		tripID := record[fields["trip_id"]]
-		arrivalTime := record[fields["arrival_time"]]
-		if tripID != lastTripID && arrivalTime == "" {
-			return fmt.Errorf("no arrival time at the beginning of the trip ‘%s’", tripID)
-		}
-
-		if ix, ok := fields["timepoint"]; !ok || record[ix] == "1" || arrivalTime != "" {
-			dT, err := parseDepartureTime(arrivalTime)
-			if err != nil {
-				return fmt.Errorf("incorrect end departure time %s: %w", arrivalTime, err)
-			}
-			if len(collectedRecords) > 0 {
-				if tripID != lastTripID && lastTripID != "" {
-					return fmt.Errorf("no arrival time at the end of the trip ‘%s’", lastTripID)
-				}
-				singleDuration := (dT - lastKnownTime) / uint(len(collectedRecords)+1)
-				for i, collectedRecord := range collectedRecords {
-					collectedRecord[fields["arrival_time"]] = formatDepartureTime(lastKnownTime + (uint(i+1) * singleDuration))
-					err = w.Write(collectedRecord)
-					if err != nil {
-						return fmt.Errorf("while writing a stop_times collected record %d: %w", i, err)
-					}
-				}
-			}
-			err = w.Write(record)
-			if err != nil {
-				return fmt.Errorf("while writing a stop_times record: %w", err)
-			}
-			collectedRecords = [][]string{}
-			lastKnownTime = dT
-		} else {
-			collectedRecords = append(collectedRecords, record)
-		}
-		lastTripID = tripID
-	}
-	w.Flush()
-	err = os.Remove(filepath.Join(path, "stop_times.txt"))
-	if err != nil {
-		return fmt.Errorf("while removing stop_times: %w", err)
-	}
-	err = os.Rename(filepath.Join(path, "stop_times2.txt"), filepath.Join(path, "stop_times.txt"))
-	if err != nil {
-		return fmt.Errorf("while renaming stop_times: %w", err)
-	}
-	return nil
-}
-
-func readFrequencies(c feedConverter) (feedConverter, error) { // O(n:frequencies) ; ( -- headways:map[tripID][]headways >> )
-	path := c.TmpFeedPath
-
-	file, err := os.Open(filepath.Join(path, "frequencies.txt"))
-	if err != nil {
-		if errors.Is(err, fs.ErrNotExist) {
-			log.Println("[WARN] no frequencies.txt")
-			return c, nil
-		} else {
-			return c, fmt.Errorf("while opening file: %w", err)
-		}
-	}
-	defer file.Close()
-
-	headways := map[string][]Headway{}
-	r := csv.NewReader(bufio.NewReader(file))
-	header, err := r.Read()
-	if err != nil {
-		return c, fmt.Errorf("while reading header: %w", err)
-	}
-	fields := map[string]int{}
-	for i, headerField := range header {
-		fields[headerField] = i
-	}
-
-	for {
-		headway := Headway{}
-		record, err := r.Read()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return c, fmt.Errorf("while reading a record: %w", err)
-		}
-
-		tripID := record[fields["trip_id"]]
-		startTime, err := parseDepartureTime(record[fields["start_time"]])
-		if err != nil {
-			return c, fmt.Errorf("while parsing start_time: %w", err)
-		}
-		endTime, err := parseDepartureTime(record[fields["end_time"]])
-		if err != nil {
-			return c, fmt.Errorf("while parsing start_time: %w", err)
-		}
-		headway.StartTime = uint(startTime)
-		headway.EndTime = uint(endTime)
-		fmt.Sscanf(record[fields["headway_secs"]], "%d", &headway.Interval)
-		if ix, ok := fields["exact_times"]; ok && record[ix] == "1" {
-			headway.Exact = true
-		}
-		headways[tripID] = append(headways[tripID], headway)
-	}
-
-	sortedHeadways := map[string][]Headway{}
-	for tripID, h := range headways {
-		slices.SortFunc(h, func(a, b Headway) int {
-			if a.StartTime < b.StartTime {
-				return -1
-			} else if a.StartTime > b.StartTime {
-				return 1
-			} else {
-				return 0
-			}
-		})
-		sortedHeadways[tripID] = h
-	}
-	c.Headways = sortedHeadways
-	return c, nil
-}
-
-func convertDepartures(c feedConverter) (feedConverter, error) { // O(n:stop_times) ; ( -- departuresOffsets:map[tripID]uint, tripsThroughStop:map[stopID][]{tripID,order}, tripHeadsigns:map[tripID]stopID >> departures.bare )
+func convertDepartures(c feedConverter) (feedConverter, error) { // O(n:stop_times) ; ( -- departures:map[tripID][]departure, tripsThroughStop:map[stopID][]{tripID,order}, tripHeadsigns:map[tripID]stopID >> )
 	path := c.TmpFeedPath
 
 	file, err := os.Open(filepath.Join(path, "stop_times.txt"))
@@ -788,11 +630,7 @@ func convertDepartures(c feedConverter) (feedConverter, error) { // O(n:stop_tim
 	}
 	defer file.Close()
 
-	result, err := os.Create(filepath.Join(path, "departures.bare"))
-	if err != nil {
-		return c, fmt.Errorf("while creating file: %w", err)
-	}
-	defer result.Close()
+	departures := map[string][]Departure{}
 
 	r := csv.NewReader(bufio.NewReader(file))
 	header, err := r.Read()
@@ -807,12 +645,6 @@ func convertDepartures(c feedConverter) (feedConverter, error) { // O(n:stop_tim
 	tripsThroughStop := map[string]map[string]StopOrder{}
 	tripHeadsigns := map[string]string{}
 
-	var offset uint = 0
-	departureOffsets := map[string]uint{}
-	departures := []Departure{}
-	lastTrip := ""
-	firstDepartureTime := uint(0)
-
 	for {
 		departure := Departure{}
 		record, err := r.Read()
@@ -820,35 +652,15 @@ func convertDepartures(c feedConverter) (feedConverter, error) { // O(n:stop_tim
 			break
 		}
 		if err != nil {
-			// log.Printf("ERR: header: %v, record: %v\n", header, record)
 			return c, fmt.Errorf("while reading a record: %w", err)
 		}
-		// log.Printf("header: %v, record: %v\n", header, record)
-		tripID := record[fields["trip_id"]]
-		if lastTrip != tripID && lastTrip != "" {
-			bytes, err := bare.Marshal(&departures)
-			if err != nil {
-				return c, fmt.Errorf("while marshalling: %w", err)
-			}
-			b, err := result.Write(bytes)
-			if err != nil {
-				return c, fmt.Errorf("while writing: %w", err)
-			}
-			departureOffsets[lastTrip] = offset
-			offset += uint(b)
-			departures = []Departure{}
-		}
 
 		stopID := record[fields["stop_id"]]
 
+		tripID := record[fields["trip_id"]]
 		fmt.Sscanf(record[fields["stop_sequence"]], "%d", &departure.StopSequence)
 		fmt.Sscanf(record[fields["pickup_type"]], "%d", &departure.Pickup)
 		fmt.Sscanf(record[fields["drop_off_type"]], "%d", &departure.Dropoff)
-		departure.Exact = true
-
-		if timepointIndex, ok := fields["timepoint"]; ok && record[timepointIndex] == "0" {
-			departure.Exact = false
-		}
 
 		if _, ok := tripsThroughStop[stopID]; !ok {
 			tripsThroughStop[stopID] = map[string]StopOrder{}
@@ -861,34 +673,19 @@ func convertDepartures(c feedConverter) (feedConverter, error) { // O(n:stop_tim
 			tripHeadsigns[tripID] = stopID
 		}
 
-		departureTime, err := parseDepartureTime(record[fields["arrival_time"]])
-		if err != nil {
-			return c, fmt.Errorf("while parsing arrival time: %w", err)
-		}
-		departure.Time = uint(departureTime)
-
-		if tripID != lastTrip {
-			firstDepartureTime = departure.Time
-		}
-		if _, ok := c.Headways[tripID]; ok {
-			departure.Time -= firstDepartureTime
-		}
+		var hours, minutes uint
+		fmt.Sscanf(record[fields["arrival_time"]], "%d:%d", &hours, &minutes)
+		departure.Time = hours*60 + minutes
 
-		departures = append(departures, departure)
-		lastTrip = tripID
+		departures[tripID] = append(departures[tripID], departure)
 	}
 
 	c.tripHeadsigns = tripHeadsigns
-	c.DeparturesOffsets = departureOffsets
+	c.Departures = departures
 	c.TripsThroughStop = tripsThroughStop
 	return c, nil
 }
 
-func clearHeadways(c feedConverter) feedConverter {
-	c.Headways = map[string][]Headway{}
-	return c
-}
-
 func clearLineNames(c feedConverter) feedConverter {
 	c.LineNames = map[string]string{}
 	return c
@@ -994,8 +791,9 @@ func clearTripsThroughStops(c feedConverter) feedConverter {
 	return c
 }
 
-func convertTrips(c feedConverter) (feedConverter, error) { // O(n:trips) ; (headways, departuresOffsets, lineNames, stopNames -- tripsOffsets:map[tripID]offset, tripsChangeOpts:map[tripID]{lineID,headsign} >> trips)
+func convertTrips(c feedConverter) (feedConverter, error) { // O(n:trips) ; (departures, lineNames, stopNames -- tripsOffsets:map[tripID]offset, tripsChangeOpts:map[tripID]{lineID,headsign} >> trips)
 	path := c.TmpFeedPath
+	departures := c.Departures
 	lineNames := c.LineNames
 
 	file, err := os.Open(filepath.Join(path, "trips.txt"))
@@ -1043,12 +841,7 @@ func convertTrips(c feedConverter) (feedConverter, error) { // O(n:trips) ; (hea
 			trip.Headsign = c.stopNames[c.tripHeadsigns[trip.Id]]
 		}
 
-		trip.DeparturesOffset = c.DeparturesOffsets[trip.Id]
-
-		if h, ok := c.Headways[trip.Id]; ok {
-			trip.Headways = h
-		}
-
+		trip.Departures = departures[trip.Id]
 		trip.ScheduleID = record[fields["service_id"]]
 		trip.LineID = record[fields["route_id"]]
 		fmt.Sscanf(record[fields["direction_id"]], "%d", &trip.Direction)
@@ -1271,6 +1064,7 @@ func getTrips(c feedConverter) (feedConverter, error) {
 	for {
 		var trip Trip
 		err := bare.UnmarshalReader(file, &trip)
+		trip.Departures = []Departure{}
 		trips[trip.Id] = trip
 		if err != nil {
 			if err == io.EOF {
@@ -1876,6 +1670,7 @@ func writeCodeIndex(c feedConverter, i CodeIndex, filename string) error {
 }
 
 func deleteTxtFiles(c feedConverter) error {
+	return nil
 	return file.DeleteTxtFiles(c.TmpFeedPath, c.GtfsFilename)
 }
 
@@ -1928,13 +1723,10 @@ func convert(input ...interface{}) (interface{}, error) {
 			Tee(saveSchedules).
 			Tee(saveFeedInfo).
 			Recover(closeTrafficCalendarFile).
-			Tee(interpolateDepartures).
-			Bind(readFrequencies).
 			Bind(convertDepartures).
 			Bind(getLineNames).
 			Bind(getStopNames).
 			Bind(convertTrips).
-			Map(clearHeadways).
 			Map(clearDepartures).
 			Map(clearStopNames).
 			Map(clearLineNames).

+ 0 - 0
traffic/departures.go


Some files were not shown because too many files changed in this diff