7 Incheckningar b6f08c3689 ... 9743c5933c

Upphovsman SHA1 Meddelande Datum
  Adam 9743c5933c undo debug 2 månader sedan
  Adam Evyčędo 94cf709308 adapt to new URLs in Poznan 2 månader sedan
  Adam aa1d0e9069 convert stops with less RAM 2 månader sedan
  Adam Evyčędo 272b8aa331 convert stops with less RAM usage 2 månader sedan
  Adam 89e31e7d2f convert stops with less RAM 2 månader sedan
  Adam Evyčędo a9e2136f74 convert departures with less RAM usage 2 månader sedan
  Adam Evyčędo df49752293 add input indices for converting 2 månader sedan
7 ändrade filer med 690 tillägg och 434 borttagningar
  1. 2 0
      go.mod
  2. 3 0
      go.sum
  3. 49 428
      traffic/convert.go
  4. 353 0
      traffic/convert_departures.go
  5. 277 0
      traffic/convert_stops.go
  6. 1 1
      traffic/nl_ovapi.go
  7. 5 5
      traffic/poznan_ztm.go

+ 2 - 0
go.mod

@@ -14,6 +14,7 @@ require (
 	github.com/adrg/strutil v0.3.0
 	github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9
 	github.com/dhconnelly/rtreego v1.1.0
+	github.com/lanrat/extsort v1.0.2
 	github.com/sahilm/fuzzy v0.1.0
 	github.com/ulikunitz/xz v0.5.10
 	github.com/yuin/gopher-lua v1.1.1
@@ -31,4 +32,5 @@ require (
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/stretchr/testify v1.8.0 // indirect
 	golang.org/x/net v0.19.0 // indirect
+	golang.org/x/sync v0.1.0 // indirect
 )

+ 3 - 0
go.sum

@@ -30,6 +30,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lanrat/extsort v1.0.2 h1:p3MLVpQEPwEGPzeLBb+1eSErzRl6Bgjgr+qnIs2RxrU=
+github.com/lanrat/extsort v1.0.2/go.mod h1:ivzsdLm8Tv+88qbdpMElV6Z15StlzPUtZSKsGb51hnQ=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -71,6 +73,7 @@ golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
 golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

+ 49 - 428
traffic/convert.go

@@ -82,29 +82,27 @@ type feedConverter struct {
 
 	Timezone            *time.Location
 	TrafficCalendarFile *os.File
-	Departures          map[string][]Departure
-	TripsThroughStop    map[string]map[string]StopOrder
-	LineNames           map[string]string
-	TripsOffsets        map[string]uint
-	TripChangeOpts      map[string]ChangeOption
-	StopsCodeIndex      CodeIndex
-	StopsNameIndex      map[string][]uint
-	Stops               map[string]string
-	LineGraphs          map[string]map[uint]LineGraph
-	lineHeadsigns       map[string]map[uint][]string
-	LineIndex           map[string][]uint
-	LineIdIndex         CodeIndex
-	ValidFrom           time.Time
-	ValidFromError      []error
-	ValidTill           time.Time
-	ValidTillError      []error
-	tripHeadsigns       map[string]string
-	stopNames           map[string]string
-	feedInfo            FeedInfo
-	defaultLanguage     string
-	translations        map[string]map[string]string
-	schedules           map[string]Schedule
-	trips               map[string]Trip
+	tripsInputIndex     map[string]int64
+	routesInputIndex    map[string]int64
+	stopsInputIndex     map[string]int64
+	tripsOffsets        map[string]uint
+
+	StopsCodeIndex  CodeIndex
+	StopsNameIndex  map[string][]uint
+	Stops           map[string]string
+	LineGraphs      map[string]map[uint]LineGraph
+	lineHeadsigns   map[string]map[uint][]string
+	LineIndex       map[string][]uint
+	LineIdIndex     CodeIndex
+	ValidFrom       time.Time
+	ValidFromError  []error
+	ValidTill       time.Time
+	ValidTillError  []error
+	feedInfo        FeedInfo
+	defaultLanguage string
+	translations    map[string]map[string]string
+	schedules       map[string]Schedule
+	trips           map[string]Trip
 }
 
 // helper functions
@@ -616,257 +614,39 @@ func closeTrafficCalendarFile(c feedConverter, e error) (feedConverter, error) {
 	return c, e
 }
 
-func clearDepartures(c feedConverter) feedConverter {
-	c.Departures = map[string][]Departure{}
-	return c
-}
-
-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"))
+func forEachRow(filename string, f func(int64, map[string]int, []string) error) error {
+	file, err := os.Open(filename)
 	if err != nil {
-		return c, fmt.Errorf("while opening file: %w", err)
+		return fmt.Errorf("while opening file: %w", err)
 	}
 	defer file.Close()
 
-	departures := map[string][]Departure{}
-
-	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
-	}
-
-	tripsThroughStop := map[string]map[string]StopOrder{}
-	tripHeadsigns := map[string]string{}
-
-	for {
-		departure := Departure{}
-		record, err := r.Read()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return c, fmt.Errorf("while reading a record: %w", err)
-		}
-
-		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)
-
-		if _, ok := tripsThroughStop[stopID]; !ok {
-			tripsThroughStop[stopID] = map[string]StopOrder{}
-		}
-		tripsThroughStop[stopID][tripID] = StopOrder{
-			Sequence: departure.StopSequence,
-		}
-
-		if c.Feed.Flags().Headsign == HeadsignTripLastStop {
-			tripHeadsigns[tripID] = stopID
-		}
-
-		var hours, minutes uint
-		fmt.Sscanf(record[fields["arrival_time"]], "%d:%d", &hours, &minutes)
-		departure.Time = hours*60 + minutes
-
-		departures[tripID] = append(departures[tripID], departure)
-	}
-
-	c.tripHeadsigns = tripHeadsigns
-	c.Departures = departures
-	c.TripsThroughStop = tripsThroughStop
-	return c, nil
-}
-
-func clearLineNames(c feedConverter) feedConverter {
-	c.LineNames = map[string]string{}
-	return c
-}
-
-func getLineNames(c feedConverter) (feedConverter, error) { // O(n:routes) ; ( -- lineNames:map[routeID]lineName >> )
-	path := c.TmpFeedPath
-
-	file, err := os.Open(filepath.Join(path, "routes.txt"))
-	if err != nil {
-		return c, fmt.Errorf("while opening file: %w", err)
-	}
-	defer file.Close()
-	r := csv.NewReader(bufio.NewReader(file))
+	r := csv.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
-	}
-
-	names := map[string]string{}
-
-	for {
-		record, err := r.Read()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return c, fmt.Errorf("while reading a record: %w", err)
-		}
-
-		routeID := record[fields["route_id"]]
-		lineName := c.Feed.Flags().LineName
-		for _, template := range []string{"route_short_name", "route_long_name"} {
-			lineName = strings.Replace(lineName, "{{"+template+"}}", record[fields[template]], -1)
-		}
-		names[routeID] = lineName
-	}
-
-	c.LineNames = names
-	return c, nil
-}
-
-func clearStopNames(c feedConverter) feedConverter {
-	c.stopNames = map[string]string{}
-	return c
-}
-
-func getStopNames(c feedConverter) (feedConverter, error) { // O(n:stops) ; ( -- stopNames[stopID]stopName >> )
-	if c.Feed.Flags().Headsign != HeadsignTripLastStop {
-		return c, nil
-	}
-
-	stopNames := map[string]string{}
-
-	path := c.TmpFeedPath
-
-	file, err := os.Open(filepath.Join(path, "stops.txt"))
-	if err != nil {
-		return c, fmt.Errorf("while opening file: %w", err)
-	}
-	defer file.Close()
-
-	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 {
-		record, err := r.Read()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return c, fmt.Errorf("while reading a record: %w", err)
-		}
-
-		stopID := record[fields["stop_id"]]
-		stopName := record[fields["stop_name"]]
-		stopNames[stopID] = stopName
-	}
-
-	c.stopNames = stopNames
-
-	return c, nil
-}
-
-func clearTripsChangeOptions(c feedConverter) feedConverter {
-	c.TripChangeOpts = map[string]ChangeOption{}
-	return c
-}
-
-func clearTripsThroughStops(c feedConverter) feedConverter {
-	c.TripsThroughStop = map[string]map[string]StopOrder{}
-	return c
-}
-
-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"))
-	if err != nil {
-		return c, fmt.Errorf("while opening file: %w", err)
-	}
-	defer file.Close()
-
-	result, err := os.Create(filepath.Join(path, "trips.bare"))
-	if err != nil {
-		return c, fmt.Errorf("while creating file: %w", err)
+		return fmt.Errorf("while reading header: %w", err)
 	}
-	defer result.Close()
 
-	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
 	}
 
-	var offset uint = 0
-	tripsOffsets := map[string]uint{}
-
-	tripChangeOpts := map[string]ChangeOption{}
-
 	for {
-		trip := Trip{}
+		offset := r.InputOffset()
 		record, err := r.Read()
 		if err == io.EOF {
 			break
 		}
 		if err != nil {
-			return c, fmt.Errorf("while reading a record: %w", err)
-		}
-
-		trip.Id = record[fields["trip_id"]]
-		switch c.Feed.Flags().Headsign {
-		case HeadsignTripHeadsing:
-			trip.Headsign = record[fields["trip_headsign"]]
-		case HeadsignTripLastStop:
-			trip.Headsign = c.stopNames[c.tripHeadsigns[trip.Id]]
-		}
-
-		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)
-
-		tripChangeOpts[trip.Id] = ChangeOption{
-			LineName:            lineNames[record[fields["route_id"]]],
-			Headsign:            translateFieldDefault(trip.Headsign, c.feedInfo.Language, c.defaultLanguage, c.translations),
-			TranslatedHeadsigns: translateField(trip.Headsign, c.feedInfo.Language, c.defaultLanguage, c.translations),
-		}
-
-		bytes, err := bare.Marshal(&trip)
-		if err != nil {
-			return c, fmt.Errorf("while marshalling: %w", err)
+			return fmt.Errorf("while reading a record: %w", err)
 		}
-		b, err := result.Write(bytes)
+		err = f(offset, fields, record)
 		if err != nil {
-			return c, fmt.Errorf("while writing: %w", err)
+			return fmt.Errorf("while performing function: %w", err)
 		}
-		tripsOffsets[trip.Id] = offset
-		offset += uint(b)
 	}
-
-	c.TripsOffsets = tripsOffsets
-	c.TripChangeOpts = tripChangeOpts
-	return c, nil
+	return nil
 }
 
 func clearStops(c feedConverter) feedConverter {
@@ -874,172 +654,8 @@ func clearStops(c feedConverter) feedConverter {
 	return c
 }
 
-func convertStops(c feedConverter) (feedConverter, error) { // O(n:stops) ; (translations, tripsThroughStop, tripChangeOpts, tripOffsets -- stopsOffsetsByCode:CodeIndex, stopsOffsetsByName:map[name][]offsets >> stops)
-	path := c.TmpFeedPath
-	tripsThroughStop := c.TripsThroughStop
-	tripChangeOpts := c.TripChangeOpts
-	tripsOffsets := c.TripsOffsets
-
-	file, err := os.Open(filepath.Join(path, "stops.txt"))
-	if err != nil {
-		return c, fmt.Errorf("while opening file: %w", err)
-	}
-	defer file.Close()
-
-	result, err := os.Create(filepath.Join(path, "stops.bare"))
-	if err != nil {
-		return c, fmt.Errorf("while creating file: %w", err)
-	}
-	defer result.Close()
-
-	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
-	}
-
-	var offset uint = 0
-	stopsOffsetsByName := map[string][]uint{}
-	stopsOffsetsByCode := CodeIndex{}
-	stops := map[string]string{}
-
-	maxStopTripsLength := 0
-
-	for {
-		stop := Stop{}
-		record, err := r.Read()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return c, fmt.Errorf("while reading a record: %w", err)
-		}
-
-		if f, ok := fields["location_type"]; ok && record[f] != "" && record[f] != "0" {
-			// NOTE for now ignore everything that’s not a stop/platform
-			// TODO use Portals (location_type == 2) to show on map if platform has a parent (location_type == 1) that has a Portal
-			// TODO use location_type in {3,4} for routing inside stations (with pathways, transfers, and levels)
-			continue
-		}
-
-		stopID := record[fields["stop_id"]]
-
-		stopTrips := tripsThroughStop[stopID]
-		stopTripsLength := len(stopTrips)
-		if maxStopTripsLength < stopTripsLength {
-			maxStopTripsLength = stopTripsLength
-		}
-
-		stop.Id = stopID
-
-		templates := []string{"stop_code", "stop_id", "stop_name", "platform_code"}
-		stop.Code = c.Feed.Flags().StopIdFormat
-		for _, template := range templates {
-			stop.Code = strings.Replace(stop.Code, "{{"+template+"}}", record[fields[template]], -1)
-		}
-		stop.Name = c.Feed.Flags().StopName
-		for _, template := range templates {
-			// TODO if '{{template}}' is empty
-			stop.Name = strings.Replace(stop.Name, "{{"+template+"}}", record[fields[template]], -1)
-		}
-		if field, ok := fields["zone_id"]; ok {
-			stop.Zone = record[field]
-		}
-		stop.NodeName = record[fields["stop_name"]]
-
-		stops[record[fields["stop_id"]]] = stop.Code
-
-		if field, ok := fields["stop_timezone"]; ok {
-			stop.Timezone = record[field]
-		}
-
-		if c.feedInfo.Language == "mul" {
-			key := record[fields["stop_name"]]
-			if _, ok := c.translations[stop.NodeName][c.defaultLanguage]; !ok {
-				stop.TranslatedNames = []Translation{{Language: c.defaultLanguage, Value: stop.Name}}
-				stop.TranslatedNodeNames = []Translation{{Language: c.defaultLanguage, Value: stop.NodeName}}
-			} else {
-				stop.TranslatedNames = []Translation{{Language: c.defaultLanguage, Value: strings.ReplaceAll(stop.Name, key, c.translations[key][c.defaultLanguage])}}
-				stop.TranslatedNodeNames = []Translation{{Language: c.defaultLanguage, Value: c.translations[key][c.defaultLanguage]}}
-			}
-			for language, value := range c.translations[key] {
-				if language == c.defaultLanguage {
-					continue
-				}
-				stop.TranslatedNames = append(stop.TranslatedNames, Translation{Language: c.defaultLanguage, Value: strings.ReplaceAll(stop.Name, key, value)})
-				stop.TranslatedNodeNames = append(stop.TranslatedNodeNames, Translation{Language: c.defaultLanguage, Value: c.translations[key][value]})
-			}
-		}
-
-		var lat, lon float64
-		fmt.Sscanf(record[fields["stop_lat"]], "%f", &lat)
-		fmt.Sscanf(record[fields["stop_lon"]], "%f", &lon)
-		stop.Position = Position{lat, lon}
-
-		changeOptionMap := map[string]ChangeOption{}
-		stop.ChangeOptions = []ChangeOption{}
-		stop.Order = map[string]StopOrder{}
-		for tripID, stopTrip := range stopTrips {
-			changeOption := tripChangeOpts[tripID]
-			stopOrder := StopOrder{
-				TripOffset: tripsOffsets[tripID],
-				Sequence:   stopTrip.Sequence,
-			}
-			stop.Order[tripID] = stopOrder
-			changeOptionMap[changeOption.LineName+"->"+changeOption.Headsign] = changeOption
-		}
-		for _, option := range changeOptionMap {
-			stop.ChangeOptions = append(stop.ChangeOptions, option)
-		}
-		sort.Slice(stop.ChangeOptions, func(i, j int) bool {
-			var num1, num2 int
-			_, err1 := fmt.Sscanf(stop.ChangeOptions[i].LineName, "%d", &num1)
-			_, err2 := fmt.Sscanf(stop.ChangeOptions[j].LineName, "%d", &num2)
-			if err1 != nil && err2 != nil {
-				return stop.ChangeOptions[i].LineName < stop.ChangeOptions[j].LineName
-			} else if err1 != nil {
-				return false
-			} else if err2 != nil {
-				return true
-			} else {
-				return num1 < num2
-			}
-		})
-
-		bytes, err := bare.Marshal(&stop)
-		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)
-		}
-		if len(stop.TranslatedNames) == 0 {
-			stopsOffsetsByName[stop.Name] = append(stopsOffsetsByName[stop.Name], offset)
-		}
-		for _, v := range stop.TranslatedNames {
-			stopsOffsetsByName[v.Value] = append(stopsOffsetsByName[v.Value], offset)
-		}
-		stopsOffsetsByCode[stop.Code] = offset
-		offset += uint(b)
-	}
-
-	if maxStopTripsLength > 8192 {
-		log.Printf("maximum length of StopOrder is %d, more than 8192, which may need to be tweaked", maxStopTripsLength)
-	}
-
-	c.StopsCodeIndex = stopsOffsetsByCode
-	c.StopsNameIndex = stopsOffsetsByName
-	c.Stops = stops
-	return c, nil
-}
-
 func clearTripOffsets(c feedConverter) feedConverter {
-	c.TripsOffsets = map[string]uint{}
+	c.tripsOffsets = map[string]uint{}
 	return c
 }
 
@@ -1635,11 +1251,11 @@ func writeLineIdIndex(c feedConverter) error {
 
 func writeTripIndex(c feedConverter) error {
 	tripIndex := map[string][]uint{}
-	for trip, offset := range c.TripsOffsets {
+	for trip, offset := range c.tripsOffsets {
 		tripIndex[trip] = []uint{offset}
 	}
 	err := writeNameIndex(c, tripIndex, "ix_trips.bare", true)
-	c.TripsOffsets = map[string]uint{}
+	c.tripsOffsets = map[string]uint{}
 	return err
 }
 
@@ -1670,7 +1286,6 @@ func writeCodeIndex(c feedConverter, i CodeIndex, filename string) error {
 }
 
 func deleteTxtFiles(c feedConverter) error {
-	return nil
 	return file.DeleteTxtFiles(c.TmpFeedPath, c.GtfsFilename)
 }
 
@@ -1723,21 +1338,27 @@ func convert(input ...interface{}) (interface{}, error) {
 			Tee(saveSchedules).
 			Tee(saveFeedInfo).
 			Recover(closeTrafficCalendarFile).
+			// ---
+			Bind(readInputTripsIndex).
+			Bind(readInputStopsIndex).
+			Bind(readInputRoutesIndex).
 			Bind(convertDepartures).
-			Bind(getLineNames).
-			Bind(getStopNames).
-			Bind(convertTrips).
-			Map(clearDepartures).
-			Map(clearStopNames).
-			Map(clearLineNames).
+			Map(dropInputRoutesIndex).
+			Map(dropInputStopsIndex).
+			Map(dropInputTripsIndex).
+			// ---
+			Tee(sortChangeOptions).
+			Tee(sortTripsThroughStop).
+			Bind(readChangeOptionsIndex).
+			Bind(readTripsThroughStopsIndex).
 			Bind(convertStops).
+			Map(dropInputRoutesIndex).
+			Map(dropInputStopsIndex).
 			Tee(writeTripIndex).
 			Map(clearTripOffsets).
 			Tee(writeStopNameIndex).
 			Tee(writeStopCodeIndex).
-			Map(clearTripsChangeOptions).
-			Map(clearTripsThroughStops).
-			Map(clearLineNames).
+			// ---
 			Bind(getTrips).
 			Bind(convertLineGraphs).
 			Map(clearStops).

+ 353 - 0
traffic/convert_departures.go

@@ -0,0 +1,353 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package traffic
+
+import (
+	"bufio"
+	"context"
+	"encoding/csv"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"git.sr.ht/~sircmpwn/go-bare"
+	"github.com/lanrat/extsort"
+)
+
+func readInputRoutesIndex(c feedConverter) (feedConverter, error) {
+	index := map[string]int64{}
+
+	path := c.TmpFeedPath
+	err := forEachRow(filepath.Join(path, "routes.txt"), func(offset int64, fields map[string]int, record []string) error {
+		routeID := record[fields["route_id"]]
+		index[routeID] = offset
+		return nil
+	})
+
+	c.routesInputIndex = index
+	return c, err
+}
+
+func readInputStopsIndex(c feedConverter) (feedConverter, error) {
+	index := map[string]int64{}
+
+	path := c.TmpFeedPath
+	forEachRow(filepath.Join(path, "stops.txt"), func(offset int64, fields map[string]int, record []string) error {
+		stopID := record[fields["stop_id"]]
+		index[stopID] = offset
+		return nil
+	})
+
+	c.stopsInputIndex = index
+	return c, nil
+}
+
+func readInputTripsIndex(c feedConverter) (feedConverter, error) {
+	index := map[string]int64{}
+
+	path := c.TmpFeedPath
+	err := forEachRow(filepath.Join(path, "trips.txt"), func(offset int64, fields map[string]int, record []string) error {
+		tripID := record[fields["trip_id"]]
+		index[tripID] = offset
+		return nil
+	})
+
+	c.tripsInputIndex = index
+	return c, err
+}
+
+func convertDepartures(c feedConverter) (feedConverter, error) { // O(n:stop_times) ; (TmpFeedPath, tripsInputIndex, stopsInputIndex -- TripsOffsets:map[tripID]offset >> trips)
+	path := c.TmpFeedPath
+
+	tripsOffsets := map[string]uint{}
+	var outputOffset uint = 0
+	previousTrip := ""
+	trip := Trip{}
+
+	result, err := os.Create(filepath.Join(path, "trips.bare"))
+	if err != nil {
+		return c, fmt.Errorf("while creating file: %w", err)
+	}
+	defer result.Close()
+
+	tripsFile, err := os.Open(filepath.Join(path, "trips.txt"))
+	if err != nil {
+		return c, fmt.Errorf("while opening trips file: %w", err)
+	}
+	defer tripsFile.Close()
+
+	// TODO unnecessary overhead, should parse single csv header line
+	trips := csv.NewReader(tripsFile)
+	tripsHeader, err := trips.Read()
+	if err != nil {
+		return c, fmt.Errorf("while reading trips header: %w", err)
+	}
+
+	tripsFields := map[string]int{}
+	for i, headerField := range tripsHeader {
+		tripsFields[headerField] = i
+	}
+
+	stopsFile, err := os.Open(filepath.Join(path, "stops.txt"))
+	if err != nil {
+		return c, fmt.Errorf("while opening stops file: %w", err)
+	}
+	defer stopsFile.Close()
+
+	// TODO unnecessary overhead, should parse single csv header line
+	stops := csv.NewReader(stopsFile)
+	stopsHeader, err := stops.Read()
+	if err != nil {
+		return c, fmt.Errorf("while reading stops header: %w", err)
+	}
+
+	stopsFields := map[string]int{}
+	for i, headerField := range stopsHeader {
+		stopsFields[headerField] = i
+	}
+
+	routesFile, err := os.Open(filepath.Join(path, "routes.txt"))
+	if err != nil {
+		return c, fmt.Errorf("while opening routes file: %w", err)
+	}
+	defer routesFile.Close()
+
+	// TODO unnecessary overhead, should parse single csv header line
+	routes := csv.NewReader(routesFile)
+	routesHeader, err := routes.Read()
+	if err != nil {
+		return c, fmt.Errorf("while reading routes header: %w", err)
+	}
+
+	routesFields := map[string]int{}
+	for i, headerField := range routesHeader {
+		routesFields[headerField] = i
+	}
+
+	changeOptionsFile, err := os.Create(filepath.Join(path, "changeoptions.csv"))
+	if err != nil {
+		return c, fmt.Errorf("while creating changeOptions file: %w", err)
+	}
+	defer changeOptionsFile.Close()
+	defer changeOptionsFile.Sync()
+	changeOptions := csv.NewWriter(changeOptionsFile)
+	defer changeOptions.Flush()
+	headerWritten := false
+
+	tripsThroughStopFile, err := os.Create(filepath.Join(path, "tripsthroughstop.csv"))
+	if err != nil {
+		return c, fmt.Errorf("while creating tripsThroughStop file: %w", err)
+	}
+	defer tripsThroughStopFile.Close()
+	defer tripsThroughStopFile.Sync()
+	tripsThroughStop := csv.NewWriter(tripsThroughStopFile)
+	defer tripsThroughStop.Flush()
+	err = tripsThroughStop.Write([]string{"stop_id", "trip_id", "sequence"})
+	if err != nil {
+		return c, fmt.Errorf("while writing tripsThroughStop header: %w", err)
+	}
+
+	err = forEachRow(filepath.Join(path, "stop_times.txt"), func(offset int64, fields map[string]int, record []string) error {
+		departure := Departure{}
+		tripID := record[fields["trip_id"]]
+		stopID := record[fields["stop_id"]]
+
+		if previousTrip != tripID && previousTrip != "" {
+			tripRecord, err := readCsvLine(tripsFile, c.tripsInputIndex[tripID], len(tripsHeader))
+			if err != nil && err != io.EOF {
+				return fmt.Errorf("while reading a trips record: %w", err)
+			}
+
+			trip.Id = tripID
+			switch c.Feed.Flags().Headsign {
+			case HeadsignTripHeadsing:
+				trip.Headsign = tripRecord[tripsFields["trip_headsign"]]
+			case HeadsignTripLastStop:
+				// TODO test this case
+				stopRecord, err := readCsvLine(stopsFile, c.stopsInputIndex[stopID], len(stopsHeader))
+				if err != nil && err != io.EOF {
+					return fmt.Errorf("while reading a stops record: %w", err)
+				}
+
+				trip.Headsign = stopRecord[stopsFields["stop_name"]]
+			}
+			// TODO translated headsign(-s)
+
+			trip.ScheduleID = tripRecord[tripsFields["service_id"]]
+			trip.LineID = tripRecord[tripsFields["route_id"]]
+			fmt.Sscanf(tripRecord[tripsFields["direction_id"]], "%d", &trip.Direction)
+
+			bytes, err := bare.Marshal(&trip)
+			if err != nil {
+				return fmt.Errorf("while marshalling: %w", err)
+			}
+			b, err := result.Write(bytes)
+			if err != nil {
+				return fmt.Errorf("while writing: %w", err)
+			}
+			tripsOffsets[trip.Id] = outputOffset
+			outputOffset += uint(b)
+
+			routeRecord, err := readCsvLine(routesFile, c.routesInputIndex[trip.LineID], len(routesHeader))
+			if err != nil && err != io.EOF {
+				return fmt.Errorf("while reading a routes record: %w", err)
+			}
+
+			lineName := c.Feed.Flags().LineName
+			for _, template := range []string{"route_short_name", "route_long_name"} {
+				lineName = strings.Replace(lineName, "{{"+template+"}}", routeRecord[routesFields[template]], -1)
+			}
+			/*headsign := translateFieldDefault(trip.Headsign, c.feedInfo.Language, c.defaultLanguage, c.translations)
+			translatedHeadsigns := translateField(trip.Headsign, c.feedInfo.Language, c.defaultLanguage, c.translations)
+			for _, translatedHeadsign := range translatedHeadsigns {
+				changeOptionsRecord = append(changeOptionsRecord, translatedHeadsign.Language)
+				changeOptionsRecord = append(changeOptionsRecord, translatedHeadsign.Value)
+			}*/
+			if !headerWritten {
+				changeOptionsHeaderRecord := []string{"stop_id", "route_id", "line_name", "headsign"}
+				/*for i := range translatedHeadsigns {
+					changeOptionsHeaderRecord = append(changeOptionsRecord, fmt.Sprintf("language_tag_%d", i))
+					changeOptionsHeaderRecord = append(changeOptionsRecord, fmt.Sprintf("headsign_%d", i))
+				}*/
+				err = changeOptions.Write(changeOptionsHeaderRecord)
+				if err != nil {
+					return fmt.Errorf("while writing changeOptions header: %w", err)
+				}
+				headerWritten = true
+			}
+			changeOptionsRecord := []string{stopID, trip.LineID, lineName, trip.Headsign}
+			// FIXME writes garbage
+			err = changeOptions.Write(changeOptionsRecord)
+			if err != nil {
+				return fmt.Errorf("while writing changeOptions record: %w", err)
+			}
+
+			trip = Trip{}
+		}
+
+		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)
+
+		stopSequence := fmt.Sprintf("%d", departure.StopSequence)
+		tripsThroughStopRecord := []string{stopID, tripID, stopSequence}
+		err := tripsThroughStop.Write(tripsThroughStopRecord)
+		if err != nil {
+			return fmt.Errorf("while writing tripsThroughStop record: %w", err)
+		}
+
+		departureTime, err := parseDepartureTime(record[fields["arrival_time"]])
+		if err != nil {
+			return fmt.Errorf("while parsing arrival time: %w", err)
+		}
+		departure.Time = uint(departureTime)
+
+		trip.Departures = append(trip.Departures, departure)
+		previousTrip = tripID
+
+		return nil
+	})
+
+	c.tripsOffsets = tripsOffsets
+	return c, err
+}
+
+func sortOutIndex(fileName string) error {
+	file, err := os.Open(fileName)
+	if err != nil {
+		return fmt.Errorf("while opening file: %w", err)
+	}
+	defer file.Close()
+
+	result, err := os.Create(fileName + "2")
+	if err != nil {
+		return fmt.Errorf("while creating file: %w", err)
+	}
+	defer result.Close()
+
+	scanner := bufio.NewScanner(file)
+	scanner.Scan()
+	headerLine := scanner.Text()
+
+	if err := scanner.Err(); err != nil {
+		return fmt.Errorf("while scanning: %w", err)
+	}
+	inputChan := make(chan string)
+	go func() {
+		for scanner.Scan() {
+			inputChan <- scanner.Text()
+		}
+		close(inputChan)
+	}()
+
+	sorter, outputChan, errChan := extsort.Strings(inputChan, nil)
+	sorter.Sort(context.Background())
+
+	result.WriteString(headerLine + "\n")
+	for data := range outputChan {
+		result.WriteString(data + "\n")
+	}
+
+	if err := <-errChan; err != nil {
+		return fmt.Errorf("while sorting: %w", err)
+	}
+
+	result.Sync()
+	result.Close()
+	os.Rename(fileName, fileName+"_xxx")
+	err = os.Rename(fileName+"2", fileName)
+	if err != nil {
+		return fmt.Errorf("while replacing file: %w", err)
+	}
+	return nil
+
+}
+
+func sortChangeOptions(c feedConverter) error {
+	return sortOutIndex(filepath.Join(c.TmpFeedPath, "changeoptions.csv"))
+}
+
+func sortTripsThroughStop(c feedConverter) error {
+	return sortOutIndex(filepath.Join(c.TmpFeedPath, "tripsthroughstop.csv"))
+}
+
+// TODO out to separate file
+func readCsvLine(r *os.File, offset int64, fields int) ([]string, error) {
+	if offset != -1 {
+		r.Seek(offset, io.SeekStart)
+	}
+	line := []byte{}
+	for {
+		b := make([]byte, 1)
+		_, err := r.Read(b)
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			return []string{}, fmt.Errorf("while reading byte: %w", err)
+		}
+		if b[0] == '\n' {
+			break
+		}
+		line = append(line, b[0])
+	}
+
+	if string(line) == "" {
+		return []string{}, io.EOF
+	}
+	// TODO unnecessary overhead, should parse single csv line, expecting $fields fields
+	csvReader := csv.NewReader(strings.NewReader(string(line)))
+	csvReader.FieldsPerRecord = fields
+	record, err := csvReader.Read()
+	if err != nil {
+		log.Printf("fields: %d\nline: %s\nrecord:%v\n", fields, string(line), record)
+		return record, fmt.Errorf("while reading record: %w", err)
+	}
+
+	return record, nil
+}

+ 277 - 0
traffic/convert_stops.go

@@ -0,0 +1,277 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package traffic
+
+import (
+	"encoding/csv"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"git.sr.ht/~sircmpwn/go-bare"
+)
+
+func dropInputStopsIndex(c feedConverter) feedConverter {
+	c.stopsInputIndex = map[string]int64{}
+	return c
+}
+
+func readTripsThroughStopsIndex(c feedConverter) (feedConverter, error) {
+	index := map[string]int64{}
+
+	path := c.TmpFeedPath
+	forEachRow(filepath.Join(path, "tripsthroughstop.csv"), func(offset int64, fields map[string]int, record []string) error {
+		stopID := record[fields["stop_id"]]
+		if _, ok := index[stopID]; !ok {
+			index[stopID] = offset
+		}
+		return nil
+	})
+
+	c.stopsInputIndex = index
+	return c, nil
+}
+
+func dropInputTripsIndex(c feedConverter) feedConverter {
+	c.tripsInputIndex = map[string]int64{}
+	return c
+}
+
+func dropInputRoutesIndex(c feedConverter) feedConverter {
+	c.routesInputIndex = map[string]int64{}
+	return c
+}
+
+func readChangeOptionsIndex(c feedConverter) (feedConverter, error) {
+	index := map[string]int64{}
+
+	path := c.TmpFeedPath
+	forEachRow(filepath.Join(path, "changeoptions.csv"), func(offset int64, fields map[string]int, record []string) error {
+		stopID := record[fields["stop_id"]]
+		if _, ok := index[stopID]; !ok {
+			index[stopID] = offset
+		}
+		return nil
+	})
+
+	c.routesInputIndex = index
+	return c, nil
+}
+
+func convertStops(c feedConverter) (feedConverter, error) { // O(n:stops) ; (translations, file:tripsThroughStop, file:tripChangeOpts, tripOffsets -- stopsOffsetsByCode:CodeIndex, stopsOffsetsByName:map[name][]offsets >> stops)
+	path := c.TmpFeedPath
+	var outputOffset uint = 0
+	stopsOffsetsByName := map[string][]uint{}
+	stopsOffsetsByCode := CodeIndex{}
+	stops := map[string]string{}
+	maxStopTripsLength := 0
+
+	result, err := os.Create(filepath.Join(path, "stops.bare"))
+	if err != nil {
+		return c, fmt.Errorf("while creating file: %w", err)
+	}
+	defer result.Close()
+
+	tripsThroughStopFile, err := os.Open(filepath.Join(path, "tripsthroughstop.csv"))
+	if err != nil {
+		return c, fmt.Errorf("while opening tripsThroughStop file: %w", err)
+	}
+	defer tripsThroughStopFile.Close()
+
+	tripsThroughStop := csv.NewReader(tripsThroughStopFile)
+	tripsThroughStopHeader, err := tripsThroughStop.Read()
+	if err != nil {
+		return c, fmt.Errorf("while reading tripsThroughStop header: %w", err)
+	}
+
+	tripsThroughStopFields := map[string]int{}
+	for i, headerField := range tripsThroughStopHeader {
+		tripsThroughStopFields[headerField] = i
+	}
+
+	changeOptionsFile, err := os.Open(filepath.Join(path, "changeoptions.csv"))
+	if err != nil {
+		return c, fmt.Errorf("while opening changeOptions file: %w", err)
+	}
+	defer changeOptionsFile.Close()
+
+	changeOptions := csv.NewReader(changeOptionsFile)
+	changeOptionsHeader, err := changeOptions.Read()
+	if err != nil {
+		return c, fmt.Errorf("while reading changeOptions header: %w", err)
+	}
+
+	changeOptionsFields := map[string]int{}
+	for i, headerField := range changeOptionsHeader {
+		changeOptionsFields[headerField] = i
+	}
+
+	err = forEachRow(filepath.Join(path, "stops.txt"), func(offset int64, fields map[string]int, record []string) error {
+		if f, ok := fields["location_type"]; ok && record[f] != "" && record[f] != "0" {
+			// NOTE for now ignore everything that’s not a stop/platform
+			// TODO use Portals (location_type == 2) to show on map if platform has a parent (location_type == 1) that has a Portal
+			// TODO use location_type in {3,4} for routing inside stations (with pathways, transfers, and levels)
+			return nil
+		}
+
+		stop := Stop{}
+
+		stopID := record[fields["stop_id"]]
+
+		stopTrips := map[string]StopOrder{}
+		if position, ok := c.stopsInputIndex[stopID]; ok {
+			tripsThroughStopFile.Seek(position, 0)
+			for {
+				tripsThroughStopRecord, err := readCsvLine(tripsThroughStopFile, -1, len(tripsThroughStopHeader))
+				if err != nil {
+					if err == io.EOF {
+						break
+					} else {
+						return fmt.Errorf("while reading tripsThroughStop record: %w", err)
+					}
+				}
+				recordStopID := tripsThroughStopRecord[tripsThroughStopFields["stop_id"]]
+				if stopID != recordStopID {
+					break
+				}
+				tripID := tripsThroughStopRecord[tripsThroughStopFields["trip_id"]]
+				var sequence int
+				fmt.Sscanf(tripsThroughStopRecord[tripsThroughStopFields["sequence"]], "%d", &sequence)
+				stopTrips[tripID] = StopOrder{
+					Sequence:   sequence,
+					TripOffset: c.tripsOffsets[tripID],
+				}
+			}
+			stopTripsLength := len(stopTrips)
+			if maxStopTripsLength < stopTripsLength {
+				maxStopTripsLength = stopTripsLength
+			}
+		}
+
+		stop.Id = stopID
+
+		templates := []string{"stop_code", "stop_id", "stop_name", "platform_code"}
+		stop.Code = c.Feed.Flags().StopIdFormat
+		for _, template := range templates {
+			stop.Code = strings.Replace(stop.Code, "{{"+template+"}}", record[fields[template]], -1)
+		}
+		stop.Name = c.Feed.Flags().StopName
+		for _, template := range templates {
+			// TODO if '{{template}}' is empty
+			stop.Name = strings.Replace(stop.Name, "{{"+template+"}}", record[fields[template]], -1)
+		}
+		if field, ok := fields["zone_id"]; ok {
+			stop.Zone = record[field]
+		}
+		stop.NodeName = record[fields["stop_name"]]
+
+		stops[record[fields["stop_id"]]] = stop.Code
+
+		if field, ok := fields["stop_timezone"]; ok {
+			stop.Timezone = record[field]
+		}
+
+		if c.feedInfo.Language == "mul" {
+			key := record[fields["stop_name"]]
+			if _, ok := c.translations[stop.NodeName][c.defaultLanguage]; !ok {
+				stop.TranslatedNames = []Translation{{Language: c.defaultLanguage, Value: stop.Name}}
+				stop.TranslatedNodeNames = []Translation{{Language: c.defaultLanguage, Value: stop.NodeName}}
+			} else {
+				stop.TranslatedNames = []Translation{{Language: c.defaultLanguage, Value: strings.ReplaceAll(stop.Name, key, c.translations[key][c.defaultLanguage])}}
+				stop.TranslatedNodeNames = []Translation{{Language: c.defaultLanguage, Value: c.translations[key][c.defaultLanguage]}}
+			}
+			for language, value := range c.translations[key] {
+				if language == c.defaultLanguage {
+					continue
+				}
+				stop.TranslatedNames = append(stop.TranslatedNames, Translation{Language: c.defaultLanguage, Value: strings.ReplaceAll(stop.Name, key, value)})
+				stop.TranslatedNodeNames = append(stop.TranslatedNodeNames, Translation{Language: c.defaultLanguage, Value: c.translations[key][value]})
+			}
+		}
+
+		var lat, lon float64
+		fmt.Sscanf(record[fields["stop_lat"]], "%f", &lat)
+		fmt.Sscanf(record[fields["stop_lon"]], "%f", &lon)
+		stop.Position = Position{lat, lon}
+
+		changeOptionMap := map[string]ChangeOption{}
+		stop.ChangeOptions = []ChangeOption{}
+		stop.Order = stopTrips
+
+		changeOptionsFile.Seek(c.routesInputIndex[stopID], 0)
+		for {
+			changeOptionsRecord, err := readCsvLine(changeOptionsFile, -1, len(changeOptionsHeader))
+			if err != nil {
+				if err == io.EOF {
+					break
+				} else {
+					return fmt.Errorf("while reading changeOptions record: %w", err)
+				}
+			}
+			recordStopID := changeOptionsRecord[changeOptionsFields["stop_id"]]
+			if stopID != recordStopID {
+				break
+			}
+			lineName := changeOptionsRecord[changeOptionsFields["line_name"]]
+			headsign := changeOptionsRecord[changeOptionsFields["headsign"]]
+			changeOptionMap[lineName+"->"+headsign] = ChangeOption{
+				LineName:            lineName,
+				Headsign:            headsign,
+				TranslatedHeadsigns: []Translation{},
+			}
+			// TODO add translations
+		}
+
+		for _, option := range changeOptionMap {
+			stop.ChangeOptions = append(stop.ChangeOptions, option)
+		}
+		sort.Slice(stop.ChangeOptions, func(i, j int) bool {
+			var num1, num2 int
+			_, err1 := fmt.Sscanf(stop.ChangeOptions[i].LineName, "%d", &num1)
+			_, err2 := fmt.Sscanf(stop.ChangeOptions[j].LineName, "%d", &num2)
+			if err1 != nil && err2 != nil {
+				return stop.ChangeOptions[i].LineName < stop.ChangeOptions[j].LineName
+			} else if err1 != nil {
+				return false
+			} else if err2 != nil {
+				return true
+			} else {
+				return num1 < num2
+			}
+		})
+
+		bytes, err := bare.Marshal(&stop)
+		if err != nil {
+			return fmt.Errorf("while marshalling: %w", err)
+		}
+		b, err := result.Write(bytes)
+		if err != nil {
+			return fmt.Errorf("while writing: %w", err)
+		}
+		if len(stop.TranslatedNames) == 0 {
+			stopsOffsetsByName[stop.Name] = append(stopsOffsetsByName[stop.Name], outputOffset)
+		}
+		for _, v := range stop.TranslatedNames {
+			stopsOffsetsByName[v.Value] = append(stopsOffsetsByName[v.Value], outputOffset)
+		}
+		stopsOffsetsByCode[stop.Code] = outputOffset
+		outputOffset += uint(b)
+
+		return nil
+	})
+
+	if maxStopTripsLength > 12288 {
+		log.Printf("maximum length of StopOrder is %d, more than 12288, which may need to be tweaked", maxStopTripsLength)
+	}
+
+	c.StopsCodeIndex = stopsOffsetsByCode
+	c.StopsNameIndex = stopsOffsetsByName
+	c.Stops = stops
+	return c, err
+}

+ 1 - 1
traffic/nl_ovapi.go

@@ -197,7 +197,7 @@ func (NlOvapi) FeedPrepareZip(path string) error {
 			return fmt.Errorf("while reading a route record: %w", err)
 		}
 
-		record[fields["stop_code"]] = strings.ReplaceAll(record[fields["route_long_name"]], "ë", ZWNJ+"ë")
+		record[fields["route_long_name"]] = strings.ReplaceAll(record[fields["route_long_name"]], "ë", ZWNJ+"ë")
 
 		err = w.Write(record)
 		if err != nil {

+ 5 - 5
traffic/poznan_ztm.go

@@ -37,7 +37,7 @@ func (PoznanZtm) getTimezone() *time.Location {
 func (z PoznanZtm) ConvertVehicles() ([]Vehicle, error) {
 	vehicles := []Vehicle{}
 
-	url := "https://ztm.poznan.pl/en/dla-deweloperow/getGtfsRtFile/?file=vehicle_dictionary.csv"
+	url := "https://ztm.poznan.pl/pl/dla-deweloperow/getGtfsRtFile/?file=vehicle_dictionary.csv"
 	response, err := z.client.Get(url)
 	if err != nil {
 		return vehicles, fmt.Errorf("ConvertVehicles: cannot GET ‘%s’: %w", url, err)
@@ -104,7 +104,7 @@ func (z PoznanZtm) ConvertVehicles() ([]Vehicle, error) {
 }
 
 func (z PoznanZtm) GetVersions(date time.Time, timezone *time.Location) ([]Version, error) {
-	url := "https://ztm.poznan.pl/en/dla-deweloperow/gtfsFiles"
+	url := "https://www.ztm.poznan.pl/otwarte-dane/gtfsfiles/"
 	response, err := z.client.Get(url)
 	if err != nil {
 		return []Version{}, fmt.Errorf("GetVersions: cannot GET ‘%s’: %w", url, err)
@@ -128,7 +128,7 @@ func (z PoznanZtm) GetVersions(date time.Time, timezone *time.Location) ([]Versi
 		if err != nil {
 			return nil, err
 		}
-		version.Link = "https://ztm.poznan.pl/en/dla-deweloperow/getGTFSFile/?file=" + v
+		version.Link = "https://ztm.poznan.pl/pl/dla-deweloperow/getGTFSFile/?file=" + v
 		versions = append(versions, version)
 	}
 	return versions, nil
@@ -140,8 +140,8 @@ func (PoznanZtm) String() string {
 
 func (PoznanZtm) RealtimeFeeds() map[RealtimeFeedType]string {
 	return map[RealtimeFeedType]string{
-		TRIP_UPDATES:      "https://ztm.poznan.pl/en/dla-deweloperow/getGtfsRtFile/?file=feeds.pb",
-		VEHICLE_POSITIONS: "https://ztm.poznan.pl/en/dla-deweloperow/getGtfsRtFile/?file=feeds.pb",
+		TRIP_UPDATES:      "https://ztm.poznan.pl/pl/dla-deweloperow/getGtfsRtFile/?file=feeds.pb",
+		VEHICLE_POSITIONS: "https://ztm.poznan.pl/pl/dla-deweloperow/getGtfsRtFile/?file=feeds.pb",
 	}
 }