Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to specify delay or skipped stops on ADDED or REPLACEMENT trips in GTFS-RT #6028

Draft
wants to merge 32 commits into
base: dev-2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fbcbe5d
implement delay and skipped stops on added / replacement trips
miklcct Aug 14, 2024
d7f506f
strengthen test case
miklcct Aug 31, 2024
7affd6c
Skipped stops should always have PickDrop = CANCELLED
miklcct Aug 31, 2024
4240da2
use "var" instead of specifying types
miklcct Sep 3, 2024
30e70fb
add overloads of TripUpdateBuilder.addSkippedStop to specify skipped …
miklcct Sep 3, 2024
979a656
move checks against StopTimeUpdate to AddedStopTime
miklcct Sep 3, 2024
dddd6fe
store the original StopTimeUpdate in AddedStopTime
miklcct Sep 4, 2024
ef9145e
Merge tag 'v2.6.0' into delay_on_added_trips
miklcct Sep 18, 2024
035fcca
Merge branch 'dev-2.x' into delay_on_added_trips
miklcct Sep 27, 2024
f34b149
Merge remote-tracking branch 'upstream/dev-2.x' into delay_on_added_t…
miklcct Sep 27, 2024
f097243
fix test cases
miklcct Sep 30, 2024
2168858
consistency in getter naming
miklcct Sep 30, 2024
86bc0e5
assume that the delay is 0 when it is missing
miklcct Sep 30, 2024
8c72fba
formatting
miklcct Oct 1, 2024
449f18d
Merge remote-tracking branch 'upstream/dev-2.x' into delay_on_added_t…
miklcct Oct 15, 2024
3dbb6ef
Merge remote-tracking branch 'upstream/dev-2.x' into delay_on_added_t…
miklcct Oct 21, 2024
6e21264
Merge branch 'dev-2.x' into delay_on_added_trips
miklcct Nov 5, 2024
f3792b1
Merge branch 'dev-2.x' into delay_on_added_trips
miklcct Nov 26, 2024
25c9d65
Merge branch 'dev-2.x' into delay_on_added_trips
miklcct Dec 4, 2024
b5c16aa
update proto file
miklcct Dec 4, 2024
fe83e7d
process pickup / drop off in StopTimeProperties
miklcct Dec 4, 2024
cafa318
process stop headsign
miklcct Dec 4, 2024
5236c7e
process trip headsign for added trips
miklcct Dec 4, 2024
18adc11
process scheduled time in added trips
miklcct Dec 4, 2024
513eca4
handle replacement trip headsign
miklcct Dec 4, 2024
f0fc986
move modified test into its own module
miklcct Dec 4, 2024
876539d
integrate headsign test into the modified trip test
miklcct Dec 4, 2024
ee933f0
renamed ReplacementTest to ModifiedTest
miklcct Dec 4, 2024
16c4dc6
use seconds from midnight instead of minutes for test
miklcct Dec 4, 2024
331b0c6
Merge branch 'dev-2.x' into delay_on_added_trips
miklcct Dec 17, 2024
f4860c5
Merge branch 'dev-2.x' into delay_on_added_trips
miklcct Jan 9, 2025
591f909
Merge branch 'dev-2.x' into delay_on_added_trips
miklcct Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.google.transit.realtime.GtfsRealtime;
import de.mfdz.MfdzRealtimeExtensions;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import javax.annotation.Nullable;
import org.opentripplanner.gtfs.mapping.PickDropMapper;
import org.opentripplanner.model.PickDrop;
Expand All @@ -13,49 +15,125 @@
*/
final class AddedStopTime {

@Nullable
private final PickDrop pickup;
private final GtfsRealtime.TripUpdate.StopTimeUpdate stopTimeUpdate;

@Nullable
private final PickDrop dropOff;

public static final PickDrop DEFAULT_PICK_DROP = PickDrop.SCHEDULED;

AddedStopTime(@Nullable PickDrop pickup, @Nullable PickDrop dropOff) {
this.pickup = pickup;
this.dropOff = dropOff;
AddedStopTime(GtfsRealtime.TripUpdate.StopTimeUpdate stopTimeUpdate) {
this.stopTimeUpdate = stopTimeUpdate;
}

PickDrop pickup() {
return Objects.requireNonNullElse(pickup, DEFAULT_PICK_DROP);
return getPickDrop(
getStopTimeProperties()
.map(properties -> properties.hasPickupType() ? properties.getPickupType() : null)
.orElse(null),
getStopTimePropertiesExtension()
.map(properties -> properties.hasPickupType() ? properties.getPickupType() : null)
.orElse(null)
);
}

PickDrop dropOff() {
return Objects.requireNonNullElse(dropOff, DEFAULT_PICK_DROP);
}

static AddedStopTime ofStopTime(GtfsRealtime.TripUpdate.StopTimeUpdate props) {
if (props.getStopTimeProperties().hasExtension(MfdzRealtimeExtensions.stopTimeProperties)) {
var ext = props
.getStopTimeProperties()
.getExtension(MfdzRealtimeExtensions.stopTimeProperties);
var pickup = ext.getPickupType();
var dropOff = ext.getDropoffType();
var dropOffType = PickDropMapper.map(dropOff.getNumber());
var pickupType = PickDropMapper.map(pickup.getNumber());
return new AddedStopTime(pickupType, dropOffType);
} else {
var pickDrop = toPickDrop(props.getScheduleRelationship());
return new AddedStopTime(pickDrop, pickDrop);
}
return getPickDrop(
getStopTimeProperties()
.map(properties -> properties.hasDropOffType() ? properties.getDropOffType() : null)
.orElse(null),
getStopTimePropertiesExtension()
.map(properties -> properties.hasDropoffType() ? properties.getDropoffType() : null)
.orElse(null)
);
}

private static PickDrop toPickDrop(
GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship scheduleRelationship
private PickDrop getPickDrop(
@Nullable GtfsRealtime.TripUpdate.StopTimeUpdate.StopTimeProperties.DropOffPickupType dropOffPickupType,
@Nullable MfdzRealtimeExtensions.StopTimePropertiesExtension.DropOffPickupType extensionDropOffPickup
) {
return switch (scheduleRelationship) {
case SCHEDULED, NO_DATA, UNSCHEDULED -> PickDrop.SCHEDULED;
case SKIPPED -> PickDrop.CANCELLED;
};
if (isSkipped()) {
return PickDrop.CANCELLED;
}

if (dropOffPickupType != null) {
return PickDropMapper.map(dropOffPickupType.getNumber());
}

if (extensionDropOffPickup != null) {
return PickDropMapper.map(extensionDropOffPickup.getNumber());
}

return PickDrop.SCHEDULED;
}

private Optional<GtfsRealtime.TripUpdate.StopTimeUpdate.StopTimeProperties> getStopTimeProperties() {
return stopTimeUpdate.hasStopTimeProperties()
? Optional.of(stopTimeUpdate.getStopTimeProperties())
: Optional.empty();
}

private Optional<MfdzRealtimeExtensions.StopTimePropertiesExtension> getStopTimePropertiesExtension() {
return getStopTimeProperties()
.map(stopTimeProperties ->
stopTimeProperties.hasExtension(MfdzRealtimeExtensions.stopTimeProperties)
? stopTimeProperties.getExtension(MfdzRealtimeExtensions.stopTimeProperties)
: null
);
}

OptionalLong arrivalTime() {
return stopTimeUpdate.hasArrival()
? getTime(stopTimeUpdate.getArrival())
: OptionalLong.empty();
}

OptionalLong departureTime() {
return stopTimeUpdate.hasDeparture()
? getTime(stopTimeUpdate.getDeparture())
: OptionalLong.empty();
}

private OptionalLong getTime(GtfsRealtime.TripUpdate.StopTimeEvent stopTimeEvent) {
return stopTimeEvent.hasTime()
? OptionalLong.of(stopTimeEvent.getTime())
: OptionalLong.empty();
}

int arrivalDelay() {
return stopTimeUpdate.hasArrival() ? getDelay(stopTimeUpdate.getArrival()) : 0;
}

int departureDelay() {
return stopTimeUpdate.hasDeparture() ? getDelay(stopTimeUpdate.getDeparture()) : 0;
}

private int getDelay(GtfsRealtime.TripUpdate.StopTimeEvent stopTimeEvent) {
return stopTimeEvent.hasDelay()
? stopTimeEvent.getDelay()
: stopTimeEvent.hasScheduledTime()
? (int) (stopTimeEvent.getTime() - stopTimeEvent.getScheduledTime())
: 0;
}

boolean isSkipped() {
return (
stopTimeUpdate.getScheduleRelationship() ==
GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SKIPPED
);
}

OptionalInt stopSequence() {
return stopTimeUpdate.hasStopSequence()
? OptionalInt.of(stopTimeUpdate.getStopSequence())
: OptionalInt.empty();
}

Optional<String> stopId() {
return stopTimeUpdate.hasStopId() ? Optional.of(stopTimeUpdate.getStopId()) : Optional.empty();
}

Optional<String> stopHeadsign() {
return (
stopTimeUpdate.hasStopTimeProperties() &&
stopTimeUpdate.getStopTimeProperties().hasStopHeadsign()
)
? Optional.of(stopTimeUpdate.getStopTimeProperties().getStopHeadsign())
: Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -582,11 +582,12 @@ private List<StopLocation> checkNewStopTimeUpdatesAndFindStops(
final List<StopLocation> stops = new ArrayList<>(stopTimeUpdates.size());

for (int index = 0; index < stopTimeUpdates.size(); ++index) {
final StopTimeUpdate stopTimeUpdate = stopTimeUpdates.get(index);
final var addedStopTime = new AddedStopTime(stopTimeUpdates.get(index));

// Check stop sequence
if (stopTimeUpdate.hasStopSequence()) {
final Integer stopSequence = stopTimeUpdate.getStopSequence();
final var optionalStopSequence = addedStopTime.stopSequence();
if (optionalStopSequence.isPresent()) {
final var stopSequence = optionalStopSequence.getAsInt();

// Check non-negative
if (stopSequence < 0) {
Expand All @@ -605,10 +606,12 @@ private List<StopLocation> checkNewStopTimeUpdatesAndFindStops(
}

// Find stops
if (stopTimeUpdate.hasStopId()) {
final var optionalStopId = addedStopTime.stopId();
if (optionalStopId.isPresent()) {
final var stopId = optionalStopId.get();
// Find stop
final var stop = transitEditorService.getRegularStop(
new FeedScopedId(tripId.getFeedId(), stopTimeUpdate.getStopId())
new FeedScopedId(tripId.getFeedId(), stopId)
);
if (stop != null) {
// Remember stop
Expand All @@ -618,7 +621,7 @@ private List<StopLocation> checkNewStopTimeUpdatesAndFindStops(
tripId,
serviceDate,
"Graph doesn't contain stop id '{}' of trip update, skipping.",
stopTimeUpdate.getStopId()
stopId
);
return null;
}
Expand All @@ -633,9 +636,10 @@ private List<StopLocation> checkNewStopTimeUpdatesAndFindStops(
}

// Check arrival time
if (stopTimeUpdate.hasArrival() && stopTimeUpdate.getArrival().hasTime()) {
final var arrival = addedStopTime.arrivalTime();
if (arrival.isPresent()) {
final var time = arrival.getAsLong();
// Check for increasing time
final Long time = stopTimeUpdate.getArrival().getTime();
if (previousTime != null && previousTime > time) {
debug(tripId, serviceDate, "Trip update contains decreasing times, skipping.");
return null;
Expand All @@ -647,9 +651,10 @@ private List<StopLocation> checkNewStopTimeUpdatesAndFindStops(
}

// Check departure time
if (stopTimeUpdate.hasDeparture() && stopTimeUpdate.getDeparture().hasTime()) {
final var departure = addedStopTime.departureTime();
if (departure.isPresent()) {
final var time = departure.getAsLong();
// Check for increasing time
final Long time = stopTimeUpdate.getDeparture().getTime();
if (previousTime != null && previousTime > time) {
debug(tripId, serviceDate, "Trip update contains decreasing times, skipping.");
return null;
Expand Down Expand Up @@ -721,17 +726,35 @@ private Result<UpdateSuccess, UpdateError> handleAddedTrip(
// Just use first service id of set
tripBuilder.withServiceId(serviceIds.iterator().next());
}

var tripHeadsign = getTripHeadsign(tripUpdate);
if (tripHeadsign != null) {
tripBuilder.withHeadsign(new NonLocalizedString(tripHeadsign));
}

return addTripToGraphAndBuffer(
tripBuilder.build(),
tripUpdate.getVehicle(),
stopTimeUpdates,
stops,
serviceDate,
RealTimeState.ADDED,
!routeExists
!routeExists,
tripHeadsign
);
}

@Nullable
private static String getTripHeadsign(TripUpdate tripUpdate) {
if (tripUpdate.hasTripProperties()) {
var tripProperties = tripUpdate.getTripProperties();
if (tripProperties.hasTripHeadsign()) {
return tripProperties.getTripHeadsign();
}
}
return null;
}

private Route createRoute(TripDescriptor tripDescriptor, FeedScopedId tripId) {
// the route in this update doesn't already exist, but the update contains the information so it will be created
if (
Expand Down Expand Up @@ -813,7 +836,8 @@ private Result<UpdateSuccess, UpdateError> addTripToGraphAndBuffer(
final List<StopLocation> stops,
final LocalDate serviceDate,
final RealTimeState realTimeState,
final boolean isAddedRoute
final boolean isAddedRoute,
@Nullable final String tripHeadsign
) {
// Preconditions
Objects.requireNonNull(stops);
Expand All @@ -830,16 +854,18 @@ private Result<UpdateSuccess, UpdateError> addTripToGraphAndBuffer(
// Create StopTimes
final List<StopTime> stopTimes = new ArrayList<>(stopTimeUpdates.size());
for (int index = 0; index < stopTimeUpdates.size(); ++index) {
final StopTimeUpdate stopTimeUpdate = stopTimeUpdates.get(index);
final var added = new AddedStopTime(stopTimeUpdates.get(index));
final var stop = stops.get(index);

// Create stop time
final StopTime stopTime = new StopTime();
stopTime.setTrip(trip);
stopTime.setStop(stop);
// Set arrival time
if (stopTimeUpdate.hasArrival() && stopTimeUpdate.getArrival().hasTime()) {
final long arrivalTime = stopTimeUpdate.getArrival().getTime() - midnightSecondsSinceEpoch;
final var arrival = added.arrivalTime();
if (arrival.isPresent()) {
final var delay = added.arrivalDelay();
final var arrivalTime = arrival.getAsLong() - midnightSecondsSinceEpoch - delay;
if (arrivalTime < 0 || arrivalTime > MAX_ARRIVAL_DEPARTURE_TIME) {
debug(
trip.getId(),
Expand All @@ -852,9 +878,10 @@ private Result<UpdateSuccess, UpdateError> addTripToGraphAndBuffer(
stopTime.setArrivalTime((int) arrivalTime);
}
// Set departure time
if (stopTimeUpdate.hasDeparture() && stopTimeUpdate.getDeparture().hasTime()) {
final long departureTime =
stopTimeUpdate.getDeparture().getTime() - midnightSecondsSinceEpoch;
final var departure = added.departureTime();
if (departure.isPresent()) {
final var delay = added.departureDelay();
final long departureTime = departure.getAsLong() - midnightSecondsSinceEpoch - delay;
if (departureTime < 0 || departureTime > MAX_ARRIVAL_DEPARTURE_TIME) {
debug(
trip.getId(),
Expand All @@ -867,12 +894,19 @@ private Result<UpdateSuccess, UpdateError> addTripToGraphAndBuffer(
stopTime.setDepartureTime((int) departureTime);
}
stopTime.setTimepoint(1); // Exact time
if (stopTimeUpdate.hasStopSequence()) {
stopTime.setStopSequence(stopTimeUpdate.getStopSequence());
}
var added = AddedStopTime.ofStopTime(stopTimeUpdate);
added.stopSequence().ifPresent(stopTime::setStopSequence);
stopTime.setPickupType(added.pickup());
stopTime.setDropOffType(added.dropOff());
added
.stopHeadsign()
.ifPresentOrElse(
headsign -> stopTime.setStopHeadsign(new NonLocalizedString(headsign)),
() -> {
if (tripHeadsign != null) {
stopTime.setStopHeadsign(new NonLocalizedString(tripHeadsign));
}
}
);
// Add stop time to list
stopTimes.add(stopTime);
}
Expand All @@ -898,12 +932,23 @@ private Result<UpdateSuccess, UpdateError> addTripToGraphAndBuffer(
);

// Update all times to mark trip times as realtime
// TODO: should we incorporate the delay field if present?
// TODO: This is based on the proposal at https://github.com/google/transit/issues/490
for (int stopIndex = 0; stopIndex < newTripTimes.getNumStops(); stopIndex++) {
newTripTimes.updateArrivalTime(stopIndex, newTripTimes.getScheduledArrivalTime(stopIndex));
final var addedStopTime = new AddedStopTime(stopTimeUpdates.get(stopIndex));

if (addedStopTime.isSkipped()) {
newTripTimes.setCancelled(stopIndex);
}

final int arrivalDelay = addedStopTime.arrivalDelay();
final int departureDelay = addedStopTime.departureDelay();
newTripTimes.updateArrivalTime(
stopIndex,
newTripTimes.getScheduledArrivalTime(stopIndex) + arrivalDelay
);
newTripTimes.updateDepartureTime(
stopIndex,
newTripTimes.getScheduledDepartureTime(stopIndex)
newTripTimes.getScheduledDepartureTime(stopIndex) + departureDelay
);
}

Expand Down Expand Up @@ -1137,7 +1182,8 @@ private Result<UpdateSuccess, UpdateError> handleModifiedTrip(
stops,
serviceDate,
RealTimeState.MODIFIED,
false
false,
getTripHeadsign(tripUpdate)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public interface RealtimeTestConstants {
String STOP_A1_ID = "A1";
String STOP_B1_ID = "B1";
String STOP_C1_ID = "C1";
String STOP_D1_ID = "D1";
String TRIP_1_ID = "TestTrip1";
String TRIP_2_ID = "TestTrip2";
String OPERATOR_1_ID = "TestOperator1";
Expand Down
Loading
Loading