Skip to content

Commit 1177d2c

Browse files
committed
Implement SIRI extra calls
1 parent d1e0b9a commit 1177d2c

File tree

6 files changed

+486
-115
lines changed

6 files changed

+486
-115
lines changed

application/src/main/java/org/opentripplanner/updater/trip/siri/AddedTripBuilder.java

+4-70
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import org.opentripplanner.transit.model.network.TripPattern;
2727
import org.opentripplanner.transit.model.organization.Agency;
2828
import org.opentripplanner.transit.model.organization.Operator;
29-
import org.opentripplanner.transit.model.site.RegularStop;
3029
import org.opentripplanner.transit.model.timetable.RealTimeState;
3130
import org.opentripplanner.transit.model.timetable.RealTimeTripTimes;
3231
import org.opentripplanner.transit.model.timetable.Trip;
@@ -35,8 +34,6 @@
3534
import org.opentripplanner.transit.service.TransitEditorService;
3635
import org.opentripplanner.updater.spi.DataValidationExceptionMapper;
3736
import org.opentripplanner.updater.spi.UpdateError;
38-
import org.opentripplanner.updater.trip.siri.mapping.PickDropMapper;
39-
import org.opentripplanner.utils.time.ServiceDateUtils;
4037
import org.rutebanken.netex.model.BusSubmodeEnumeration;
4138
import org.rutebanken.netex.model.RailSubmodeEnumeration;
4239
import org.slf4j.Logger;
@@ -69,6 +66,7 @@ class AddedTripBuilder {
6966
private final String shortName;
7067
private final String headsign;
7168
private final List<TripOnServiceDate> replacedTrips;
69+
private final StopTimesMapper stopTimesMapper;
7270

7371
AddedTripBuilder(
7472
EstimatedVehicleJourney estimatedVehicleJourney,
@@ -120,6 +118,7 @@ class AddedTripBuilder {
120118
timeZone = transitService.getTimeZone();
121119

122120
replacedTrips = getReplacedVehicleJourneys(estimatedVehicleJourney);
121+
stopTimesMapper = new StopTimesMapper(entityResolver, timeZone);
123122
}
124123

125124
AddedTripBuilder(
@@ -161,6 +160,7 @@ class AddedTripBuilder {
161160
this.headsign = headsign;
162161
this.replacedTrips = replacedTrips;
163162
this.dataSource = dataSource;
163+
stopTimesMapper = new StopTimesMapper(entityResolver, timeZone);
164164
}
165165

166166
Result<TripUpdate, UpdateError> build() {
@@ -196,7 +196,7 @@ Result<TripUpdate, UpdateError> build() {
196196
// Create the "scheduled version" of the trip
197197
var aimedStopTimes = new ArrayList<StopTime>();
198198
for (int stopSequence = 0; stopSequence < calls.size(); stopSequence++) {
199-
StopTime stopTime = createStopTime(
199+
StopTime stopTime = stopTimesMapper.createStopTime(
200200
trip,
201201
departureDate,
202202
stopSequence,
@@ -341,72 +341,6 @@ private Trip createTrip(Route route, FeedScopedId calServiceId) {
341341
return tripBuilder.build();
342342
}
343343

344-
/**
345-
* Map the call to a StopTime or return null if the stop cannot be found in the site repository.
346-
*/
347-
private StopTime createStopTime(
348-
Trip trip,
349-
ZonedDateTime departureDate,
350-
int stopSequence,
351-
CallWrapper call,
352-
boolean isFirstStop,
353-
boolean isLastStop
354-
) {
355-
RegularStop stop = entityResolver.resolveQuay(call.getStopPointRef());
356-
if (stop == null) {
357-
return null;
358-
}
359-
360-
StopTime stopTime = new StopTime();
361-
stopTime.setStopSequence(stopSequence);
362-
stopTime.setTrip(trip);
363-
stopTime.setStop(stop);
364-
365-
// Fallback to other time, if one doesn't exist
366-
var aimedArrivalTime = call.getAimedArrivalTime() != null
367-
? call.getAimedArrivalTime()
368-
: call.getAimedDepartureTime();
369-
370-
var aimedArrivalTimeSeconds = ServiceDateUtils.secondsSinceStartOfService(
371-
departureDate,
372-
aimedArrivalTime,
373-
timeZone
374-
);
375-
376-
var aimedDepartureTime = call.getAimedDepartureTime() != null
377-
? call.getAimedDepartureTime()
378-
: call.getAimedArrivalTime();
379-
380-
var aimedDepartureTimeSeconds = ServiceDateUtils.secondsSinceStartOfService(
381-
departureDate,
382-
aimedDepartureTime,
383-
timeZone
384-
);
385-
386-
// Use departure time for first stop, and arrival time for last stop, to avoid negative dwell times
387-
stopTime.setArrivalTime(isFirstStop ? aimedDepartureTimeSeconds : aimedArrivalTimeSeconds);
388-
stopTime.setDepartureTime(isLastStop ? aimedArrivalTimeSeconds : aimedDepartureTimeSeconds);
389-
390-
// Update destination display
391-
var destinationDisplay = getFirstNameFromList(call.getDestinationDisplaies());
392-
if (!destinationDisplay.isEmpty()) {
393-
stopTime.setStopHeadsign(new NonLocalizedString(destinationDisplay));
394-
} else if (trip.getHeadsign() != null) {
395-
stopTime.setStopHeadsign(trip.getHeadsign());
396-
} else {
397-
// Fallback to empty string
398-
stopTime.setStopHeadsign(new NonLocalizedString(""));
399-
}
400-
401-
// Update pickup / dropoff
402-
PickDropMapper.mapPickUpType(call, stopTime.getPickupType()).ifPresent(stopTime::setPickupType);
403-
PickDropMapper
404-
.mapDropOffType(call, stopTime.getDropOffType())
405-
.ifPresent(stopTime::setDropOffType);
406-
407-
return stopTime;
408-
}
409-
410344
private static String getFirstNameFromList(List<NaturalLanguageStringStructure> names) {
411345
if (names == null) {
412346
return "";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package org.opentripplanner.updater.trip.siri;
2+
3+
import static java.lang.Boolean.TRUE;
4+
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_STOP_SEQUENCE;
5+
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_START_DATE;
6+
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_VALID_STOPS;
7+
8+
import java.time.LocalDate;
9+
import java.time.ZoneId;
10+
import java.time.ZonedDateTime;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.Objects;
14+
import java.util.function.Function;
15+
import org.opentripplanner.model.StopTime;
16+
import org.opentripplanner.transit.model.framework.DataValidationException;
17+
import org.opentripplanner.transit.model.framework.FeedScopedId;
18+
import org.opentripplanner.transit.model.framework.Result;
19+
import org.opentripplanner.transit.model.network.StopPattern;
20+
import org.opentripplanner.transit.model.network.TripPattern;
21+
import org.opentripplanner.transit.model.timetable.RealTimeState;
22+
import org.opentripplanner.transit.model.timetable.RealTimeTripTimes;
23+
import org.opentripplanner.transit.model.timetable.Trip;
24+
import org.opentripplanner.transit.model.timetable.TripTimesFactory;
25+
import org.opentripplanner.transit.service.TransitEditorService;
26+
import org.opentripplanner.updater.spi.DataValidationExceptionMapper;
27+
import org.opentripplanner.updater.spi.UpdateError;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
import uk.org.siri.siri20.EstimatedVehicleJourney;
31+
import uk.org.siri.siri20.OccupancyEnumeration;
32+
33+
class ExtraCallTripBuilder {
34+
35+
private static final Logger LOG = LoggerFactory.getLogger(ExtraCallTripBuilder.class);
36+
private final TransitEditorService transitService;
37+
private final ZoneId timeZone;
38+
private final Function<Trip, FeedScopedId> getTripPatternId;
39+
private final Trip trip;
40+
private final String dataSource;
41+
private final LocalDate serviceDate;
42+
private final List<CallWrapper> calls;
43+
private final boolean isJourneyPredictionInaccurate;
44+
private final OccupancyEnumeration occupancy;
45+
private final boolean cancellation;
46+
private final StopTimesMapper stopTimesMapper;
47+
48+
ExtraCallTripBuilder(
49+
EstimatedVehicleJourney estimatedVehicleJourney,
50+
TransitEditorService transitService,
51+
EntityResolver entityResolver,
52+
Function<Trip, FeedScopedId> getTripPatternId,
53+
Trip trip
54+
) {
55+
this.trip = Objects.requireNonNull(trip);
56+
57+
// DataSource of added trip
58+
dataSource = estimatedVehicleJourney.getDataSource();
59+
60+
serviceDate = entityResolver.resolveServiceDate(estimatedVehicleJourney);
61+
62+
isJourneyPredictionInaccurate = TRUE.equals(estimatedVehicleJourney.isPredictionInaccurate());
63+
occupancy = estimatedVehicleJourney.getOccupancy();
64+
cancellation = TRUE.equals(estimatedVehicleJourney.isCancellation());
65+
66+
calls = CallWrapper.of(estimatedVehicleJourney);
67+
68+
this.transitService = transitService;
69+
this.getTripPatternId = getTripPatternId;
70+
timeZone = transitService.getTimeZone();
71+
72+
stopTimesMapper = new StopTimesMapper(entityResolver, timeZone);
73+
}
74+
75+
Result<TripUpdate, UpdateError> build() {
76+
if (calls.size() <= transitService.findPattern(trip).numberOfStops()) {
77+
// An extra call trip update is expected to have at least one more stop than the scheduled trip
78+
return UpdateError.result(trip.getId(), INVALID_STOP_SEQUENCE, dataSource);
79+
}
80+
81+
if (serviceDate == null) {
82+
return UpdateError.result(trip.getId(), NO_START_DATE, dataSource);
83+
}
84+
85+
FeedScopedId calServiceId = transitService.getOrCreateServiceIdForDate(serviceDate);
86+
if (calServiceId == null) {
87+
return UpdateError.result(trip.getId(), NO_START_DATE, dataSource);
88+
}
89+
90+
ZonedDateTime departureDate = serviceDate.atStartOfDay(timeZone);
91+
92+
// Create the "scheduled version" of the trip
93+
var aimedStopTimes = new ArrayList<StopTime>();
94+
for (int stopSequence = 0; stopSequence < calls.size(); stopSequence++) {
95+
StopTime stopTime = stopTimesMapper.createStopTime(
96+
trip,
97+
departureDate,
98+
stopSequence,
99+
calls.get(stopSequence),
100+
stopSequence == 0,
101+
stopSequence == (calls.size() - 1)
102+
);
103+
104+
// Drop this update if the call refers to an unknown stop (not present in the site repository).
105+
if (stopTime == null) {
106+
return UpdateError.result(trip.getId(), NO_VALID_STOPS, dataSource);
107+
}
108+
109+
aimedStopTimes.add(stopTime);
110+
}
111+
112+
// TODO: We always create a new TripPattern to be able to modify its scheduled timetable
113+
StopPattern stopPattern = new StopPattern(aimedStopTimes);
114+
115+
RealTimeTripTimes tripTimes = TripTimesFactory.tripTimes(
116+
trip,
117+
aimedStopTimes,
118+
transitService.getDeduplicator()
119+
);
120+
// validate the scheduled trip times
121+
// they are in general superseded by real-time trip times
122+
// but in case of trip cancellation, OTP will fall back to scheduled trip times
123+
// therefore they must be valid
124+
tripTimes.validateNonIncreasingTimes();
125+
tripTimes.setServiceCode(transitService.getServiceCode(trip.getServiceId()));
126+
127+
TripPattern pattern = TripPattern
128+
.of(getTripPatternId.apply(trip))
129+
.withRoute(trip.getRoute())
130+
.withMode(trip.getMode())
131+
.withNetexSubmode(trip.getNetexSubMode())
132+
.withStopPattern(stopPattern)
133+
.withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes))
134+
.withCreatedByRealtimeUpdater(true)
135+
.build();
136+
137+
RealTimeTripTimes updatedTripTimes = tripTimes.copyScheduledTimes();
138+
139+
// Loop through calls again and apply updates
140+
for (int stopSequence = 0; stopSequence < calls.size(); stopSequence++) {
141+
TimetableHelper.applyUpdates(
142+
departureDate,
143+
updatedTripTimes,
144+
stopSequence,
145+
stopSequence == (calls.size() - 1),
146+
isJourneyPredictionInaccurate,
147+
calls.get(stopSequence),
148+
occupancy
149+
);
150+
}
151+
152+
if (cancellation || stopPattern.isAllStopsNonRoutable()) {
153+
updatedTripTimes.cancelTrip();
154+
} else {
155+
updatedTripTimes.setRealTimeState(RealTimeState.MODIFIED);
156+
}
157+
158+
/* Validate */
159+
try {
160+
updatedTripTimes.validateNonIncreasingTimes();
161+
} catch (DataValidationException e) {
162+
return DataValidationExceptionMapper.toResult(e, dataSource);
163+
}
164+
165+
return Result.success(
166+
new TripUpdate(stopPattern, updatedTripTimes, serviceDate, null, pattern, false, dataSource)
167+
);
168+
}
169+
}

0 commit comments

Comments
 (0)