Skip to content

Conversation

@tkalvas
Copy link
Contributor

@tkalvas tkalvas commented Dec 9, 2025

Summary

Adds two-directional replacement relation for TripOnServiceDates, and simpler queries for Routes and Trips to find
if there are replacements. Works for both GTFS and NeTEx sources, which have very different capabilities in this
respect.

Issue

Discussion and plan in #6817

Unit tests

Yes.

Testing revealed that when we create a TripOnServiceDate replacing a Trip on some ServiceDate, we do not currently generate a synthetic TripOnServiceDate from that Trip, that the created TOSD could include it in its replacementFor list. For cancellations we create synthetic TOSDs, should we create them for replacements too?

Documentation

Todo.

Changelog

Yes.

Bumping the serialization version id

No.

@tkalvas tkalvas requested a review from a team as a code owner December 9, 2025 14:23
@tkalvas tkalvas marked this pull request as draft December 9, 2025 14:23
@codecov
Copy link

codecov bot commented Dec 9, 2025

Codecov Report

❌ Patch coverage is 66.27219% with 57 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.11%. Comparing base (eb1c726) to head (f32dbb2).
⚠️ Report is 216 commits behind head on dev-2.x.

Files with missing lines Patch % Lines
...tripplanner/transit/service/ReplacementHelper.java 51.21% 13 Missing and 7 partials ⚠️
...ntripplanner/apis/gtfs/datafetchers/RouteImpl.java 25.00% 6 Missing ⚠️
...lanner/apis/transmodel/model/network/LineType.java 70.00% 6 Missing ⚠️
...model/model/timetable/DatedServiceJourneyType.java 70.00% 6 Missing ⚠️
...transmodel/model/timetable/ServiceJourneyType.java 70.00% 6 Missing ⚠️
...ipplanner/transit/service/TimetableRepository.java 37.50% 4 Missing and 1 partial ⚠️
...entripplanner/apis/gtfs/datafetchers/TripImpl.java 33.33% 4 Missing ⚠️
.../apis/gtfs/datafetchers/TripOnServiceDateImpl.java 50.00% 2 Missing ⚠️
...lanner/apis/gtfs/datafetchers/ReplacementImpl.java 75.00% 1 Missing ⚠️
...tripplanner/transit/model/network/Replacement.java 83.33% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #7126      +/-   ##
=============================================
+ Coverage      72.05%   72.11%   +0.06%     
- Complexity     20699    20865     +166     
=============================================
  Files           2242     2272      +30     
  Lines          83853    84506     +653     
  Branches        8366     8426      +60     
=============================================
+ Hits           60418    60943     +525     
- Misses         20499    20581      +82     
- Partials        2936     2982      +46     

☔ 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.

@tkalvas tkalvas marked this pull request as ready for review December 17, 2025 12:15
@tkalvas tkalvas added New GraphQL API Label issues we should consider when making a new GraphQL API for OTP2. !Improvement A functional improvement or micro feature and removed New GraphQL API Label issues we should consider when making a new GraphQL API for OTP2. labels Dec 18, 2025
@optionsome optionsome self-requested a review December 18, 2025 14:03
Copy link
Member

@optionsome optionsome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could try adding/updating some integration test for the GTFS API.

Comment on lines +2020 to +2023
type Replacement {
isReplacement: Boolean!
replacementFor: [TripOnServiceDate!]
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some docs for this.

id: ID!
"""
Is this a replacement route.
Only true for GTFS-sourced data if set by the extended GTFS type, because GTFS does not implement
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Only true for GTFS-sourced data if set by the extended GTFS type, because GTFS does not implement
Only true for GTFS-sourced data if set by the extended GTFS route type, because GTFS does not implement

id: ID!
"""
Is this a replacement trip.
Only true for GTFS-sourced data if set by the extended GTFS type, because GTFS does not implement
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Only true for GTFS-sourced data if set by the extended GTFS type, because GTFS does not implement
Only true for GTFS-sourced data if set by the extended GTFS route type, because GTFS does not implement

"Information related to trip's scheduled arrival to the final stop location. Can contain real-time information."
end: StopCall!
"Replaced by these trips."
replacedBy: [TripOnServiceDate!]!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this replacedBy should be here or in the Replacement type. Also, a bit inconsistent that this is marked as non-null while the replacementFor is not. I guess in both cases we should return an empty list and explain that the empty list doesn't necessarily mean that there is no replacement happening.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm opposed to folding null and [] together when they so obviously mean different things. Based on where our information here is coming from, we might not know what the replacedBy should be (signified by null here), or we do know what it should be, but that can be nothing (signified by [] here) or a non-empty collection (signified by a non-empty list).

But it might be the case that replacementFor is already implemented incorrectly, in that it will be [] even when it should be null. This kind of thing is purposeful degradation of quality we could have.

Comment on lines +2671 to +2672
"Is a replacement trip."
replacement: Replacement!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs here make it sound like this is a boolean field, when the type can potentially contain more information than just that.

Map<Trip, TripPattern> realTimeAddedPatternForTrip,
Multimap<Route, TripPattern> realTimeAddedPatternsForRoute,
Map<FeedScopedId, TripOnServiceDate> realTimeAddedTripOnServiceDateById,
Map<FeedScopedId, List<TripOnServiceDate>> realTimeAddedReplacedByTripOnServiceDateById,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use a multimap here? I wonder what is the cost of having this separate index here vs going through all the realtime added TripOnServiceDate (my intuition is that this separate collection probably makes sense).

import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripOnServiceDate;

public class ReplacementHelper {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add javadoc.

}

public boolean isReplacementRoute(Route route) {
if (route.getGtfsType() != null && route.getGtfsType() == 714) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create some constant for 714 in this file. Also while we are at it, we could probably check for 110 as well since it's also some replacement submode.


private final Map<FeedScopedId, TripPattern> tripPatternForId = new HashMap<>();
private final Map<FeedScopedId, TripOnServiceDate> tripOnServiceDates = new HashMap<>();
private final Map<FeedScopedId, List<TripOnServiceDate>> replacedByTripOnServiceDates =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider multimap again.


var addedTripOnServiceDate2 = createReplacementTrip(ADDED_TRIP_ID, ADDED_TRIP_2_ID, "2");
assertNotNull(addedTripOnServiceDate2);
assertEquals(1, addedTripOnServiceDate2.getReplacementFor().size());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use truth.assertThat for checking collection sizes, although it is a not a requirement.

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

Labels

!Improvement A functional improvement or micro feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants