From fc9f532880429a13a325c65f302906baa9fee481 Mon Sep 17 00:00:00 2001 From: PatrickSteil Date: Sat, 10 Jan 2026 12:21:47 +0100 Subject: [PATCH 1/5] fix intermediate stops along trip headsign --- gtfstidy.go | 17 ++++--- processors/intermediateheadsign.go | 74 ++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 processors/intermediateheadsign.go diff --git a/gtfstidy.go b/gtfstidy.go index 02228fb..1b951e1 100755 --- a/gtfstidy.go +++ b/gtfstidy.go @@ -9,17 +9,18 @@ package main import ( "errors" "fmt" - "github.com/patrickbr/gtfsparser" - "github.com/patrickbr/gtfsparser/gtfs" - "github.com/patrickbr/gtfstidy/processors" - "github.com/patrickbr/gtfswriter" - "github.com/paulmach/go.geojson" - flag "github.com/spf13/pflag" "io/ioutil" "os" "path" "strconv" "strings" + + "github.com/patrickbr/gtfsparser" + "github.com/patrickbr/gtfsparser/gtfs" + "github.com/patrickbr/gtfstidy/processors" + "github.com/patrickbr/gtfswriter" + geojson "github.com/paulmach/go.geojson" + flag "github.com/spf13/pflag" ) func getGtfsPoly(poly [][][]float64) gtfsparser.Polygon { @@ -170,6 +171,7 @@ func main() { explicitCals := flag.BoolP("explicit-calendar", "", false, "add calendar.txt entry for every service, even irregular ones") ensureTripHeadsigns := flag.BoolP("ensure-trip-headsigns", "", false, "write trip headsigns if missing") ensureParents := flag.BoolP("ensure-stop-parents", "", false, "ensure that every stop (location_type=0) has a parent station") + fixTripHeadsigns := flag.BoolP("fix-trip-headsigns", "", false, "fixes trip headsigns pointing to previous stops") keepColOrder := flag.BoolP("keep-col-order", "", false, "keep the original column ordering of the input feed") keepFields := flag.BoolP("keep-additional-fields", "F", false, "keep all non-GTFS fields from the input") dropTooFast := flag.BoolP("drop-too-fast-trips", "", false, "drop trips that are too fast to realistically occur") @@ -641,6 +643,9 @@ func main() { if *ensureTripHeadsigns { minzers = append(minzers, processors.TripHeadsigner{}) } + if *fixTripHeadsigns { + minzers = append(minzers, processors.FixIntermediateHeadsigns{}) + } if *useRedTripMinimizer { // to convert calendar_dates based services into regular calendar.txt services diff --git a/processors/intermediateheadsign.go b/processors/intermediateheadsign.go new file mode 100644 index 0000000..b73afd1 --- /dev/null +++ b/processors/intermediateheadsign.go @@ -0,0 +1,74 @@ +package processors + +import ( + "fmt" + "os" + + "github.com/patrickbr/gtfsparser" +) + +// FixIntermediateHeadsigns checks if the trip headsign matches an intermediate stop. +// If so, it sets the stop_headsign for previous stops to that intermediate name +// and updates the trip_headsign to the final destination. + +type FixIntermediateHeadsigns struct{} + +func (pro FixIntermediateHeadsigns) Run(feed *gtfsparser.Feed) { + fmt.Fprintf(os.Stdout, "Fixing intermediate headsigns... ") + + count := 0 + + for _, trip := range feed.Trips { + if len(trip.StopTimes) < 2 { + continue + } + + currentHeadsign := trip.Headsign + if *currentHeadsign == "" { + continue + } + + lastStopIdx := len(trip.StopTimes) - 1 + lastStop := trip.StopTimes[lastStopIdx].Stop() + if lastStop == nil { + continue + } + + if *currentHeadsign == lastStop.Name { + continue + } + + matchIndex := -1 + + // 1. Check if headsign is equal to a stop along the trip (except the last one) + for i, st := range trip.StopTimes { + if i == lastStopIdx { + break + } + + if st.Stop() != nil && st.Stop().Name == *currentHeadsign { + matchIndex = i + } + } + + if matchIndex != -1 { + // Logic: + // Sequence: A -> B -> C (match) -> D -> E (last) + // Old Trip Headsign: C + // New Trip Headsign: E + // Stop Headsign for A, B: C + + // Update trip headsign to the actual last stop + trip.Headsign = &lastStop.Name + + // Update stop_headsign for all stops prior to the match + for j := 0; j < matchIndex; j++ { + trip.StopTimes[j].SetHeadsign(currentHeadsign) + } + + count++ + } + } + + fmt.Fprintf(os.Stdout, "done. Fixed headsigns for %d trips.\n", count) +} From 690110b60addaaef719d9f6fa8760779cfb52cb8 Mon Sep 17 00:00:00 2001 From: PatrickSteil Date: Wed, 21 Jan 2026 22:10:52 +0100 Subject: [PATCH 2/5] use for loop --- processors/intermediateheadsign.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/processors/intermediateheadsign.go b/processors/intermediateheadsign.go index b73afd1..912ee1b 100644 --- a/processors/intermediateheadsign.go +++ b/processors/intermediateheadsign.go @@ -41,11 +41,8 @@ func (pro FixIntermediateHeadsigns) Run(feed *gtfsparser.Feed) { matchIndex := -1 // 1. Check if headsign is equal to a stop along the trip (except the last one) - for i, st := range trip.StopTimes { - if i == lastStopIdx { - break - } - + for i := 0; i < len(trip.StopTimes)-1; i++ { + st := trip.StopTimes[i] if st.Stop() != nil && st.Stop().Name == *currentHeadsign { matchIndex = i } From b3efb4b57e02659e7a03487bad4d3be0c3815b1e Mon Sep 17 00:00:00 2001 From: PatrickSteil Date: Wed, 21 Jan 2026 22:13:30 +0100 Subject: [PATCH 3/5] can break asap --- processors/intermediateheadsign.go | 1 + 1 file changed, 1 insertion(+) diff --git a/processors/intermediateheadsign.go b/processors/intermediateheadsign.go index 912ee1b..db4f378 100644 --- a/processors/intermediateheadsign.go +++ b/processors/intermediateheadsign.go @@ -45,6 +45,7 @@ func (pro FixIntermediateHeadsigns) Run(feed *gtfsparser.Feed) { st := trip.StopTimes[i] if st.Stop() != nil && st.Stop().Name == *currentHeadsign { matchIndex = i + break } } From 302d0123506e8af71efa288f6749cb7c3e349fd2 Mon Sep 17 00:00:00 2001 From: PatrickSteil Date: Wed, 21 Jan 2026 22:14:36 +0100 Subject: [PATCH 4/5] no, dont break. we want the last stop (if duplicate) --- processors/intermediateheadsign.go | 1 - 1 file changed, 1 deletion(-) diff --git a/processors/intermediateheadsign.go b/processors/intermediateheadsign.go index db4f378..912ee1b 100644 --- a/processors/intermediateheadsign.go +++ b/processors/intermediateheadsign.go @@ -45,7 +45,6 @@ func (pro FixIntermediateHeadsigns) Run(feed *gtfsparser.Feed) { st := trip.StopTimes[i] if st.Stop() != nil && st.Stop().Name == *currentHeadsign { matchIndex = i - break } } From 3922ba6b1203ef67936983378fedeb4504503adf Mon Sep 17 00:00:00 2001 From: PatrickSteil Date: Wed, 21 Jan 2026 22:15:59 +0100 Subject: [PATCH 5/5] break only when looping from behind --- processors/intermediateheadsign.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/processors/intermediateheadsign.go b/processors/intermediateheadsign.go index 912ee1b..4db755b 100644 --- a/processors/intermediateheadsign.go +++ b/processors/intermediateheadsign.go @@ -41,10 +41,11 @@ func (pro FixIntermediateHeadsigns) Run(feed *gtfsparser.Feed) { matchIndex := -1 // 1. Check if headsign is equal to a stop along the trip (except the last one) - for i := 0; i < len(trip.StopTimes)-1; i++ { + for i := len(trip.StopTimes) - 2; i >= 0; i-- { st := trip.StopTimes[i] if st.Stop() != nil && st.Stop().Name == *currentHeadsign { matchIndex = i + break } }