Skip to content

Fix RemoveTransitIfStreetOnlyIsBetter filter not applied to flex#7498

Draft
sigtot wants to merge 4 commits intoopentripplanner:dev-2.xfrom
entur:fix/remove-if-street-only-better-filter-not-applied-to-flex
Draft

Fix RemoveTransitIfStreetOnlyIsBetter filter not applied to flex#7498
sigtot wants to merge 4 commits intoopentripplanner:dev-2.xfrom
entur:fix/remove-if-street-only-better-filter-not-applied-to-flex

Conversation

@sigtot
Copy link
Copy Markdown
Contributor

@sigtot sigtot commented Apr 1, 2026

Summary

There is a filter that removes transit itineraries that are worse than a "street only" itinerary (walk, bike, or car). This PR also enables this for direct flex itineraries. The code is organized in 4 commits that can be reviewed (maybe even merged) independently:

  1. Add a failing test case (not relevant to merge independently)
  2. Fix the failing case (could be merged as a patch)
  3. Rename the now imprecise concept "street only" to be "direct".
  4. Breaking change - Rename router-config.json variables to reflect the renaming

Before merging this I would suggest we test out the fix alone (as Entur test) without breaking config, so only commits 1-3 or maybe even just 1-2.

Issue

Closes #7108

Reproduction url

This query:

{
  first: trip(
    from: {place: "NSR:StopPlace:44672"}
    to: {place: "NSR:StopPlace:58950"}
    modes: {accessMode: flexible, directMode: flexible, egressMode: flexible, transportModes: [{transportMode: bus}, {transportMode: rail}]}
    dateTime: "2026-04-09T09:00:00+02:00"
    numTripPatterns: 1
    searchWindow: 240
    itineraryFilters: {debug: limitToSearchWindow}
  ) {
    nextPageCursor
    tripPatterns {
      generalizedCost
      systemNotices {
        tag
      }
      legs {
        aimedStartTime
        mode
        line {
          id
        }
        fromPlace {
          name
        }
        toPlace {
          name
        }
      }
    }
  }
  second: trip(
    pageCursor: "MnxORVhUX1BBR0V8MjAyNi0wNC0wOVQwOTo1Mzo1OVp8fDFofFNUUkVFVF9BTkRfQVJSSVZBTF9USU1FfGZhbHNlfDIwMjYtMDQtMDlUMDc6MjQ6NDVafDIwMjYtMDQtMDlUMDc6NTg6MDNafDB8NDU1MHx8"
    from: {place: "NSR:StopPlace:44672"}
    to: {place: "NSR:StopPlace:58950"}
    modes: {accessMode: flexible, directMode: flexible, egressMode: flexible, transportModes: [{transportMode: bus}, {transportMode: rail}]}
    dateTime: "2026-04-09T09:00:00+02:00"
    numTripPatterns: 1
    searchWindow: 240
    itineraryFilters: {debug: limitToSearchWindow}
  ) {
    nextPageCursor
    tripPatterns {
      generalizedCost
      systemNotices {
        tag
      }
      legs {
        aimedStartTime
        mode
        line {
          id
        }
        fromPlace {
          name
        }
        toPlace {
          name
        }
      }
    }
  }
}

Returns the following. Notice how the second page includes two redundant rail legs.

{
  "data": {
    "first": {
      "nextPageCursor": "MnxORVhUX1BBR0V8MjAyNi0wNC0wOVQwOTo1Mzo1OVp8fDFofFNUUkVFVF9BTkRfQVJSSVZBTF9USU1FfGZhbHNlfDIwMjYtMDQtMDlUMDc6MjQ6NDVafDIwMjYtMDQtMDlUMDc6NTg6MDNafDB8NDU1MHx8",
      "tripPatterns": [
        {
          "generalizedCost": 4550,
          "systemNotices": [],
          "legs": [
            {
              "aimedStartTime": "2026-04-09T09:24:45+02:00",
              "mode": "foot",
              "line": null,
              "fromPlace": {
                "name": "Frosta skole"
              },
              "toPlace": {
                "name": "Frosta sentrum"
              }
            },
            {
              "aimedStartTime": "2026-04-09T09:30:00+02:00",
              "mode": "bus",
              "line": {
                "id": "ATB:Line:2_635"
              },
              "fromPlace": {
                "name": "Frosta sentrum"
              },
              "toPlace": {
                "name": "Åsen E6"
              }
            },
            {
              "aimedStartTime": "2026-04-09T09:55:00+02:00",
              "mode": "foot",
              "line": null,
              "fromPlace": {
                "name": "Åsen E6"
              },
              "toPlace": {
                "name": "Åsen stasjon"
              }
            }
          ]
        },
        {
          "generalizedCost": 3079,
          "systemNotices": [
            {
              "tag": "number-of-itineraries-filter"
            }
          ],
          "legs": [
            {
              "aimedStartTime": "2026-04-09T11:53:59+02:00",
              "mode": "foot",
              "line": null,
              "fromPlace": {
                "name": "Frosta skole"
              },
              "toPlace": {
                "name": "krysset mellom Alstadvegen og gangvei"
              }
            },
            {
              "aimedStartTime": "2026-04-09T11:59:00+02:00",
              "mode": "bus",
              "line": {
                "id": "ATB:FlexibleLine:13d3f9fe-31f3-578d-867f-2f53dcfa6aa9"
              },
              "fromPlace": {
                "name": "krysset mellom Alstadvegen og gangvei (del av Frosta sentrum (busstopp))"
              },
              "toPlace": {
                "name": "krysset mellom gangvei og stikkvei (del av Levanger )"
              }
            },
            {
              "aimedStartTime": "2026-04-09T12:16:47+02:00",
              "mode": "foot",
              "line": null,
              "fromPlace": {
                "name": "krysset mellom gangvei og stikkvei"
              },
              "toPlace": {
                "name": "Åsen stasjon"
              }
            }
          ]
        },
      // ... other filtered itineraries
      ]
    },
    "second": {
      "nextPageCursor": "MnxORVhUX1BBR0V8MjAyNi0wNC0wOVQwOTo1NDo1OVp8fDFofFNUUkVFVF9BTkRfQVJSSVZBTF9USU1FfGZhbHNlfDIwMjYtMDQtMDlUMDk6NTQ6NTlafDIwMjYtMDQtMDlUMTE6MjI6MDBafDJ8MTE1Nzh8fA==",
      "tripPatterns": [
        {
          "generalizedCost": 11578,
          "systemNotices": [],
          "legs": [
            {
              "aimedStartTime": "2026-04-09T11:54:59+02:00",
              "mode": "foot",
              "line": null,
              "fromPlace": {
                "name": "Frosta skole"
              },
              "toPlace": {
                "name": "krysset mellom Alstadvegen og gangvei"
              }
            },
            {
              "aimedStartTime": "2026-04-09T12:00:00+02:00",
              "mode": "bus",
              "line": {
                "id": "ATB:FlexibleLine:13d3f9fe-31f3-578d-867f-2f53dcfa6aa9"
              },
              "fromPlace": {
                "name": "krysset mellom Alstadvegen og gangvei (del av Frosta sentrum (busstopp))"
              },
              "toPlace": {
                "name": "krysset mellom gangvei og stikkvei (del av Levanger )"
              }
            },
            {
              "aimedStartTime": "2026-04-09T12:18:00+02:00",
              "mode": "foot",
              "line": null,
              "fromPlace": {
                "name": "krysset mellom gangvei og stikkvei"
              },
              "toPlace": {
                "name": "Åsen stasjon"
              }
            },
            {
              "aimedStartTime": "2026-04-09T12:37:00+02:00",
              "mode": "rail",
              "line": {
                "id": "SJN:Line:26"
              },
              "fromPlace": {
                "name": "Åsen stasjon"
              },
              "toPlace": {
                "name": "Skatval stasjon"
              }
            },
            {
              "aimedStartTime": "2026-04-09T13:06:00+02:00",
              "mode": "rail",
              "line": {
                "id": "SJN:Line:26"
              },
              "fromPlace": {
                "name": "Skatval stasjon"
              },
              "toPlace": {
                "name": "Åsen stasjon"
              }
            }
          ]
        }
      ]
    }
  }
}

Notice that the decoded cursor has cost 4550, from the first transit itinerary that should actually have been filtered out.

  • Cursor: MnxORVhUX1BBR0V8MjAyNi0wNC0wOVQwOTo1Mzo1OVp8fDFofFNUUkVFVF9BTkRfQVJSSVZBTF9USU1FfGZhbHNlfDIwMjYtMDQtMDlUMDc6MjQ6NDVafDIwMjYtMDQtMDlUMDc6NTg6MDNafDB8NDU1MHx8
  • Decoded: 2|NEXT_PAGE|2026-04-09T09:53:59Z||1h|STREET_AND_ARRIVAL_TIME|false|2026-04-09T07:24:45Z|2026-04-09T07:58:03Z|0|4550||

With the fix applied:

  1. The foot-bus-foot itinerary is filtered out in favor of the second result, the direct flex, since this has a lower cost. (Though this might depend on the specific config parameters)
  2. Then, the direct flex trip is returned instead of the transit trip in the first page.
  3. Then, the second page applies this flex trip's generalized cost as as generalizedCostMaxLimit so that the itinerary with two extra rail legs gets filtered out! Thus fixing the bug.

Unit tests

New test case is added to check the transit itinerary gets removed in the presence of a better direct flex itinerary.

Documentation

Javadocs updated in commit 3 after changing the concept to allow direct flex.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 97.36842% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 71.01%. Comparing base (630a126) to head (62bdddc).
⚠️ Report is 69 commits behind head on dev-2.x.

Files with missing lines Patch % Lines
...s/transit/RemoveTransitIfDirectIsBetterResult.java 50.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #7498      +/-   ##
=============================================
- Coverage      71.39%   71.01%   -0.38%     
+ Complexity     21112    20997     -115     
=============================================
  Files           2344     2348       +4     
  Lines          87167    87250      +83     
  Branches        8633     8639       +6     
=============================================
- Hits           62231    61963     -268     
- Misses         21936    22291     +355     
+ Partials        3000     2996       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sigtot sigtot changed the title Fix/remove if street only better filter not applied to flex Fix RemoveTransitIfStreetOnlyIsBetter filter not applied to flex Apr 1, 2026
@sigtot
Copy link
Copy Markdown
Contributor Author

sigtot commented Apr 7, 2026

After discussion in the core team meeting, we see a problem with applying this within the RemoveTransitIfStreetOnlyIsBetter filter because that's strictly for StreetOnly. It also wouldn't be much better to rename everything to be a more generic "-IfDirectIsBetter" (like I've one in commits 3-4 in this PR), because we wouldn't be able to configure flex separately from street only. (They are separate concerns and the possible future need for separate configuration is evidence for that). In particular, the costLimitFunction (e.g. "60 + 1.3x") would make sense to configure separately.

Instead, we've considered some alternative options:

  1. Solve this with pruning within Raptor itself, which would automatically exclude back-and-forth itineraries
  2. Add a separate filter for direct flex (with separate configuration)
  • This has the benefit of allowing separate configuration
  • But, would require an additional field in the cursor
  1. Change the cursor to have the limit (with costLimitFunction applied) instead of the maxGeneralizedCost
  • The cursor currently has maxGeneralizedCost and computes the limit based on this and the street-only costLimitFunction.
  • Instead, we could apply the costLimitFunction first, and put that value into the cursor, so it can be applied directly. On the second page, we then require no knowledge of whether the limit comes from street-only or direct flex.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Set the generalizedCostMaxLimit in next token for direct FLEX

1 participant