Skip to content

Commit

Permalink
Implement SIRI extra calls
Browse files Browse the repository at this point in the history
  • Loading branch information
vpaturet committed Feb 26, 2025
1 parent d1e0b9a commit fd300a7
Show file tree
Hide file tree
Showing 6 changed files with 488 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.organization.Agency;
import org.opentripplanner.transit.model.organization.Operator;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.timetable.RealTimeState;
import org.opentripplanner.transit.model.timetable.RealTimeTripTimes;
import org.opentripplanner.transit.model.timetable.Trip;
Expand All @@ -35,8 +34,6 @@
import org.opentripplanner.transit.service.TransitEditorService;
import org.opentripplanner.updater.spi.DataValidationExceptionMapper;
import org.opentripplanner.updater.spi.UpdateError;
import org.opentripplanner.updater.trip.siri.mapping.PickDropMapper;
import org.opentripplanner.utils.time.ServiceDateUtils;
import org.rutebanken.netex.model.BusSubmodeEnumeration;
import org.rutebanken.netex.model.RailSubmodeEnumeration;
import org.slf4j.Logger;
Expand Down Expand Up @@ -69,6 +66,7 @@ class AddedTripBuilder {
private final String shortName;
private final String headsign;
private final List<TripOnServiceDate> replacedTrips;
private final StopTimesMapper stopTimesMapper;

AddedTripBuilder(
EstimatedVehicleJourney estimatedVehicleJourney,
Expand Down Expand Up @@ -120,6 +118,7 @@ class AddedTripBuilder {
timeZone = transitService.getTimeZone();

replacedTrips = getReplacedVehicleJourneys(estimatedVehicleJourney);
stopTimesMapper = new StopTimesMapper(entityResolver, timeZone);
}

AddedTripBuilder(
Expand Down Expand Up @@ -161,6 +160,7 @@ class AddedTripBuilder {
this.headsign = headsign;
this.replacedTrips = replacedTrips;
this.dataSource = dataSource;
stopTimesMapper = new StopTimesMapper(entityResolver, timeZone);
}

Result<TripUpdate, UpdateError> build() {
Expand Down Expand Up @@ -196,7 +196,7 @@ Result<TripUpdate, UpdateError> build() {
// Create the "scheduled version" of the trip
var aimedStopTimes = new ArrayList<StopTime>();
for (int stopSequence = 0; stopSequence < calls.size(); stopSequence++) {
StopTime stopTime = createStopTime(
StopTime stopTime = stopTimesMapper.createStopTime(
trip,
departureDate,
stopSequence,
Expand Down Expand Up @@ -341,72 +341,6 @@ private Trip createTrip(Route route, FeedScopedId calServiceId) {
return tripBuilder.build();
}

/**
* Map the call to a StopTime or return null if the stop cannot be found in the site repository.
*/
private StopTime createStopTime(
Trip trip,
ZonedDateTime departureDate,
int stopSequence,
CallWrapper call,
boolean isFirstStop,
boolean isLastStop
) {
RegularStop stop = entityResolver.resolveQuay(call.getStopPointRef());
if (stop == null) {
return null;
}

StopTime stopTime = new StopTime();
stopTime.setStopSequence(stopSequence);
stopTime.setTrip(trip);
stopTime.setStop(stop);

// Fallback to other time, if one doesn't exist
var aimedArrivalTime = call.getAimedArrivalTime() != null
? call.getAimedArrivalTime()
: call.getAimedDepartureTime();

var aimedArrivalTimeSeconds = ServiceDateUtils.secondsSinceStartOfService(
departureDate,
aimedArrivalTime,
timeZone
);

var aimedDepartureTime = call.getAimedDepartureTime() != null
? call.getAimedDepartureTime()
: call.getAimedArrivalTime();

var aimedDepartureTimeSeconds = ServiceDateUtils.secondsSinceStartOfService(
departureDate,
aimedDepartureTime,
timeZone
);

// Use departure time for first stop, and arrival time for last stop, to avoid negative dwell times
stopTime.setArrivalTime(isFirstStop ? aimedDepartureTimeSeconds : aimedArrivalTimeSeconds);
stopTime.setDepartureTime(isLastStop ? aimedArrivalTimeSeconds : aimedDepartureTimeSeconds);

// Update destination display
var destinationDisplay = getFirstNameFromList(call.getDestinationDisplaies());
if (!destinationDisplay.isEmpty()) {
stopTime.setStopHeadsign(new NonLocalizedString(destinationDisplay));
} else if (trip.getHeadsign() != null) {
stopTime.setStopHeadsign(trip.getHeadsign());
} else {
// Fallback to empty string
stopTime.setStopHeadsign(new NonLocalizedString(""));
}

// Update pickup / dropoff
PickDropMapper.mapPickUpType(call, stopTime.getPickupType()).ifPresent(stopTime::setPickupType);
PickDropMapper
.mapDropOffType(call, stopTime.getDropOffType())
.ifPresent(stopTime::setDropOffType);

return stopTime;
}

private static String getFirstNameFromList(List<NaturalLanguageStringStructure> names) {
if (names == null) {
return "";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package org.opentripplanner.updater.trip.siri;

import static java.lang.Boolean.TRUE;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_STOP_SEQUENCE;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_START_DATE;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_VALID_STOPS;

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.transit.model.framework.DataValidationException;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.framework.Result;
import org.opentripplanner.transit.model.network.StopPattern;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.timetable.RealTimeState;
import org.opentripplanner.transit.model.timetable.RealTimeTripTimes;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimesFactory;
import org.opentripplanner.transit.service.TransitEditorService;
import org.opentripplanner.updater.spi.DataValidationExceptionMapper;
import org.opentripplanner.updater.spi.UpdateError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.org.siri.siri20.EstimatedVehicleJourney;
import uk.org.siri.siri20.OccupancyEnumeration;

class ExtraCallTripBuilder {

private static final Logger LOG = LoggerFactory.getLogger(ExtraCallTripBuilder.class);
private final TransitEditorService transitService;
private final ZoneId timeZone;
private final Function<Trip, FeedScopedId> getTripPatternId;
private final Trip trip;
private final String dataSource;
private final LocalDate serviceDate;
private final List<CallWrapper> calls;
private final boolean isJourneyPredictionInaccurate;
private final OccupancyEnumeration occupancy;
private final boolean cancellation;
private final StopTimesMapper stopTimesMapper;

ExtraCallTripBuilder(
EstimatedVehicleJourney estimatedVehicleJourney,
TransitEditorService transitService,
EntityResolver entityResolver,
Function<Trip, FeedScopedId> getTripPatternId,
Trip trip
) {
this.trip = Objects.requireNonNull(trip);

// DataSource of added trip
dataSource = estimatedVehicleJourney.getDataSource();

serviceDate = entityResolver.resolveServiceDate(estimatedVehicleJourney);

isJourneyPredictionInaccurate = TRUE.equals(estimatedVehicleJourney.isPredictionInaccurate());
occupancy = estimatedVehicleJourney.getOccupancy();
cancellation = TRUE.equals(estimatedVehicleJourney.isCancellation());

calls = CallWrapper.of(estimatedVehicleJourney);

this.transitService = transitService;
this.getTripPatternId = getTripPatternId;
timeZone = transitService.getTimeZone();

stopTimesMapper = new StopTimesMapper(entityResolver, timeZone);
}

Result<TripUpdate, UpdateError> build() {
if (calls.size() <= transitService.findPattern(trip).numberOfStops()) {
// An extra call trip update is expected to have at least one more stop than the scheduled trip
return UpdateError.result(trip.getId(), INVALID_STOP_SEQUENCE, dataSource);
}

if (serviceDate == null) {
return UpdateError.result(trip.getId(), NO_START_DATE, dataSource);
}

FeedScopedId calServiceId = transitService.getOrCreateServiceIdForDate(serviceDate);
if (calServiceId == null) {
return UpdateError.result(trip.getId(), NO_START_DATE, dataSource);
}

ZonedDateTime departureDate = serviceDate.atStartOfDay(timeZone);

// Create the "scheduled version" of the trip
var aimedStopTimes = new ArrayList<StopTime>();
for (int stopSequence = 0; stopSequence < calls.size(); stopSequence++) {
StopTime stopTime = stopTimesMapper.createStopTime(
trip,
departureDate,
stopSequence,
calls.get(stopSequence),
stopSequence == 0,
stopSequence == (calls.size() - 1)
);

// Drop this update if the call refers to an unknown stop (not present in the site repository).
if (stopTime == null) {
return UpdateError.result(trip.getId(), NO_VALID_STOPS, dataSource);
}

aimedStopTimes.add(stopTime);
}

// TODO: We always create a new TripPattern to be able to modify its scheduled timetable
StopPattern stopPattern = new StopPattern(aimedStopTimes);

RealTimeTripTimes tripTimes = TripTimesFactory.tripTimes(
trip,
aimedStopTimes,
transitService.getDeduplicator()
);
// validate the scheduled trip times
// they are in general superseded by real-time trip times
// but in case of trip cancellation, OTP will fall back to scheduled trip times
// therefore they must be valid
tripTimes.validateNonIncreasingTimes();
tripTimes.setServiceCode(transitService.getServiceCode(trip.getServiceId()));

TripPattern pattern = TripPattern
.of(getTripPatternId.apply(trip))
.withRoute(trip.getRoute())
.withMode(trip.getMode())
.withNetexSubmode(trip.getNetexSubMode())
.withStopPattern(stopPattern)
.withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes))
.withCreatedByRealtimeUpdater(true)
.build();

RealTimeTripTimes updatedTripTimes = tripTimes.copyScheduledTimes();

// Loop through calls again and apply updates
for (int stopSequence = 0; stopSequence < calls.size(); stopSequence++) {
TimetableHelper.applyUpdates(
departureDate,
updatedTripTimes,
stopSequence,
stopSequence == (calls.size() - 1),
isJourneyPredictionInaccurate,
calls.get(stopSequence),
occupancy
);
}

if (cancellation || stopPattern.isAllStopsNonRoutable()) {
updatedTripTimes.cancelTrip();
} else {
updatedTripTimes.setRealTimeState(RealTimeState.MODIFIED);
}

/* Validate */
try {
updatedTripTimes.validateNonIncreasingTimes();
} catch (DataValidationException e) {
return DataValidationExceptionMapper.toResult(e, dataSource);
}

return Result.success(
new TripUpdate(stopPattern, updatedTripTimes, serviceDate, null, pattern, false, dataSource)
);
}
}
Loading

0 comments on commit fd300a7

Please sign in to comment.