Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.opentripplanner.updater.trip.gtfs;

import java.util.Objects;
import java.util.Optional;
import org.opentripplanner.core.model.i18n.I18NString;
import org.opentripplanner.core.model.i18n.NonLocalizedString;
import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.gtfs.mapping.TransitModeMapper;
import org.opentripplanner.transit.model.basic.TransitMode;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.organization.Agency;
import org.opentripplanner.transit.service.TransitService;
import org.opentripplanner.updater.trip.gtfs.model.AddedRoute;
import org.opentripplanner.updater.trip.gtfs.model.TripUpdate;

/**
* Factory for creating routes based on GTFS real-time trip updates.
*/
class RouteFactory {

record Result(boolean newRouteCreated, Route route) {}

private final TransitService transitService;

public RouteFactory(TransitService transitService) {
this.transitService = transitService;
}

/**
* Extract a new route from the given trip update, creating one if it doesn't exist.
*/
Result getOrCreate(TripUpdate update) {
var optionalRoute = findRoute(update);
var route = optionalRoute.orElseGet(() -> createRoute(update));
return new Result(optionalRoute.isEmpty(), route);
}

private Route createRoute(TripUpdate update) {
var tripId = update.tripId();
// the route in this update doesn't already exist, but the update contains the information so it will be created
var routeId = update.routeId();
return routeId
.map(id -> {
var builder = Route.of(id);

var addedRouteExtension = AddedRoute.ofTripDescriptor(update);

var agency = transitService
.findAgency(new FeedScopedId(tripId.getFeedId(), addedRouteExtension.agencyId()))
.orElseGet(() -> fallbackAgency(tripId.getFeedId()));

builder.withAgency(agency);

builder.withGtfsType(addedRouteExtension.routeType());
var mode = TransitModeMapper.mapMode(addedRouteExtension.routeType());
builder.withMode(mode);

// Create route name
var name = Objects.requireNonNullElse(
addedRouteExtension.routeLongName(),
tripId.toString()
);
builder.withLongName(new NonLocalizedString(name));
builder.withUrl(addedRouteExtension.routeUrl());
return builder.build();
})
.orElseGet(() -> {
I18NString longName = NonLocalizedString.ofNullable(tripId.getId());
return Route.of(tripId)
.withAgency(fallbackAgency(tripId.getFeedId()))
// Guess the route type as it doesn't exist yet in the specifications
// Bus. Used for short- and long-distance bus routes.
.withGtfsType(3)
.withMode(TransitMode.BUS)
// Create route name
.withLongName(longName)
.build();
});
}

private Optional<Route> findRoute(TripUpdate tripUpdate) {
return tripUpdate.routeId().flatMap(id -> Optional.ofNullable(transitService.getRoute(id)));
}

/**
* Create dummy agency for added trips.
*/
private Agency fallbackAgency(String feedId) {
return Agency.of(new FeedScopedId(feedId, "autogenerated-gtfs-rt-added-route"))
.withName("Agency automatically added by GTFS-RT update")
.withTimezone(transitService.getTimeZone().toString())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,18 @@
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_ARRIVAL_TIME;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_DEPARTURE_TIME;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_STOP_SEQUENCE;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TRIP_NOT_FOUND;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TRIP_NOT_FOUND_IN_PATTERN;

import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import javax.annotation.Nullable;
import org.opentripplanner.core.model.i18n.I18NString;
import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.model.PickDrop;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.transit.model.basic.Accessibility;
import org.opentripplanner.transit.model.framework.DataValidationException;
import org.opentripplanner.transit.model.framework.DeduplicatorService;
import org.opentripplanner.transit.model.framework.Result;
Expand All @@ -48,11 +40,19 @@ class TripTimesUpdater {

private static final Logger LOG = LoggerFactory.getLogger(TripTimesUpdater.class);

private final ZoneId timeZone;
private final DeduplicatorService deduplicator;

/**
* Maximum time in seconds since midnight for arrivals and departures
*/
private static final long MAX_ARRIVAL_DEPARTURE_TIME = 48 * 60 * 60;

TripTimesUpdater(ZoneId timeZone, DeduplicatorService deduplicator) {
this.timeZone = timeZone;
this.deduplicator = deduplicator;
}

/**
* Apply the TripUpdate to the appropriate TripTimes from a Timetable. The existing TripTimes
* must not be modified directly because they may be shared with the underlying
Expand All @@ -62,39 +62,25 @@ class TripTimesUpdater {
* all trips in a timetable are from the same feed, which should always be the case.
*
* @param tripUpdate GTFS-RT trip update
* @param timeZone time zone of trip update
* @param updateServiceDate service date of trip update
* @param backwardsDelayPropagationType Defines when delays are propagated to previous stops and
* if these stops are given the NO_DATA flag
* @return {@link Result < TripTimesPatch , UpdateError >} contains either a new copy of updated
* TripTimes after TripUpdate has been applied on TripTimes of trip with the id specified in the
* trip descriptor of the TripUpdate and a list of stop indices that have been skipped with the
* realtime update; or an error if something went wrong
*/
public static Result<TripTimesPatch, UpdateError> createUpdatedTripTimesFromGtfsRt(
public Result<TripTimesPatch, UpdateError> createUpdatedTripTimesFromGtfsRt(
Timetable timetable,
TripUpdate tripUpdate,
ZoneId timeZone,
LocalDate updateServiceDate,
ForwardsDelayPropagationType forwardsDelayPropagationType,
BackwardsDelayPropagationType backwardsDelayPropagationType
) {
var optionalTripId = tripUpdate.tripDescriptor().tripId();
if (optionalTripId.isEmpty()) {
// I don't think it should happen here as an empty trip id was already rejected in the adapter
LOG.debug("TripDescriptor object has no TripId field");
return Result.failure(UpdateError.noTripId(TRIP_NOT_FOUND));
}
var tripId = optionalTripId.get();
var tripId = tripUpdate.tripId();

var feedScopedTripId = new FeedScopedId(timetable.getPattern().getFeedId(), tripId);

var tripTimes = timetable.getTripTimes(feedScopedTripId);
var tripTimes = timetable.getTripTimes(tripId);
if (tripTimes == null) {
LOG.debug("tripId {} not found in pattern.", tripId);
return Result.failure(new UpdateError(feedScopedTripId, TRIP_NOT_FOUND_IN_PATTERN));
} else {
LOG.trace("tripId {} found in timetable.", tripId);
return Result.failure(new UpdateError(tripId, TRIP_NOT_FOUND_IN_PATTERN));
}

RealTimeTripTimesBuilder builder = tripTimes.createRealTimeWithoutScheduledTimes();
Expand All @@ -117,7 +103,7 @@ public static Result<TripTimesPatch, UpdateError> createUpdatedTripTimesFromGtfs
int numStops = tripTimes.getNumStops();

final long today = ServiceDateUtils.asStartOfService(
updateServiceDate,
tripUpdate.serviceDate(),
timeZone
).toEpochSecond();

Expand Down Expand Up @@ -173,15 +159,15 @@ public static Result<TripTimesPatch, UpdateError> createUpdatedTripTimesFromGtfs
i,
tripId
);
return Result.failure(new UpdateError(feedScopedTripId, INVALID_ARRIVAL_TIME, i));
return Result.failure(new UpdateError(tripId, INVALID_ARRIVAL_TIME, i));
}
if (!update.isDepartureValid()) {
LOG.debug(
"Departure time at index {} of trip {} has neither a delay nor a time.",
i,
tripId
);
return Result.failure(new UpdateError(feedScopedTripId, INVALID_DEPARTURE_TIME, i));
return Result.failure(new UpdateError(tripId, INVALID_DEPARTURE_TIME, i));
}
setArrivalAndDeparture(builder, i, update, today);
}
Expand All @@ -198,7 +184,7 @@ public static Result<TripTimesPatch, UpdateError> createUpdatedTripTimesFromGtfs
"Part of a TripUpdate object could not be applied successfully to trip {}.",
tripId
);
return Result.failure(new UpdateError(feedScopedTripId, INVALID_STOP_SEQUENCE));
return Result.failure(new UpdateError(tripId, INVALID_STOP_SEQUENCE));
}

// Interpolate missing times for stops which don't have times associated. Note: Currently for
Expand All @@ -216,18 +202,14 @@ public static Result<TripTimesPatch, UpdateError> createUpdatedTripTimesFromGtfs
LOG.debug("Propagated delay from stop index {} backwards on trip {}.", index, tripId)
);

getWheelchairAccessibility(tripUpdate).ifPresent(builder::withWheelchairAccessibility);
tripUpdate.wheelchairAccessibility().ifPresent(builder::withWheelchairAccessibility);

// Make sure that updated trip times have the correct real time state
builder.withRealTimeState(RealTimeState.UPDATED);

// Validate for non-increasing times. Log error if present.
try {
var result = builder.build();
LOG.trace(
"A valid TripUpdate object was applied to trip {} using the Timetable class update method.",
tripId
);
return success(
new TripTimesPatch(result, updatedPickups, updatedDropoffs, replacedStopIndices)
);
Expand All @@ -240,64 +222,32 @@ public static Result<TripTimesPatch, UpdateError> createUpdatedTripTimesFromGtfs
* Add a new or replacement trip to the snapshot
*
* @param trip trip
* @param wheelchairAccessibility accessibility information of the vehicle
* @param serviceDate service date of trip
* @param tripUpdate information about the trip
* @param realTimeState real-time state of new trip
* @return empty Result if successful or one containing an error
*/
public static Result<TripTimesWithStopPattern, UpdateError> createNewTripTimesFromGtfsRt(
public Result<TripTimesWithStopPattern, UpdateError> createNewTripTimesFromGtfsRt(
Trip trip,
@Nullable Accessibility wheelchairAccessibility,
TripUpdate tripUpdate,
List<StopAndStopTimeUpdate> stopAndStopTimeUpdates,
ZoneId timeZone,
LocalDate serviceDate,
RealTimeState realTimeState,
@Nullable I18NString tripHeadsign,
DeduplicatorService deduplicator,
int serviceCode
) {
// Calculate seconds since epoch on GTFS midnight (noon minus 12h) of service date
final long midnightSecondsSinceEpoch = ServiceDateUtils.asStartOfService(
serviceDate,
tripUpdate.serviceDate(),
timeZone
).toEpochSecond();

// Create StopTimes based on the scheduled times
final List<StopTime> stopTimes = new ArrayList<>(stopAndStopTimeUpdates.size());
var lastStopSequence = -1;
for (final StopAndStopTimeUpdate item : stopAndStopTimeUpdates) {
final var update = item.stopTimeUpdate();
final var stop = item.stop();

// validate stop sequence
OptionalInt stopSequence = update.stopSequence();
if (stopSequence.isPresent()) {
var seq = stopSequence.getAsInt();
if (seq < 0) {
LOG.debug(
"{} trip {} on {} contains negative stop sequence, skipping.",
realTimeState,
trip.getId(),
serviceDate
);
return UpdateError.result(trip.getId(), INVALID_STOP_SEQUENCE);
}
if (seq <= lastStopSequence) {
LOG.debug(
"{} trip {} on {} contains decreasing stop sequence, skipping.",
realTimeState,
trip.getId(),
serviceDate
);
return UpdateError.result(trip.getId(), INVALID_STOP_SEQUENCE);
}
lastStopSequence = seq;
}

// Create stop time
final StopTime stopTime = new StopTime();
stopTime.setTrip(trip);
stopTime.setStop(stop);
stopTime.setStop(item.stop());
// Set arrival time
final var arrival = update.scheduledArrivalTimeWithRealTimeFallback();
if (arrival.isPresent()) {
Expand All @@ -307,7 +257,7 @@ public static Result<TripTimesWithStopPattern, UpdateError> createNewTripTimesFr
"NEW trip {} on {} has invalid arrival time (compared to start date in " +
"TripDescriptor), skipping.",
trip.getId(),
serviceDate
tripUpdate.serviceDate()
);
return UpdateError.result(trip.getId(), INVALID_ARRIVAL_TIME);
}
Expand All @@ -322,15 +272,15 @@ public static Result<TripTimesWithStopPattern, UpdateError> createNewTripTimesFr
"NEW trip {} on {} has invalid departure time (compared to start date in " +
"TripDescriptor), skipping.",
trip.getId(),
serviceDate
tripUpdate.serviceDate()
);
return UpdateError.result(trip.getId(), INVALID_DEPARTURE_TIME);
}
stopTime.setDepartureTime((int) departureTime);
}
// Exact time
stopTime.setTimepoint(1);
stopSequence.ifPresent(stopTime::setStopSequence);
update.stopSequence().ifPresent(stopTime::setStopSequence);
stopTime.setPickupType(update.effectivePickup());
stopTime.setDropOffType(update.effectiveDropoff());
update.stopHeadsign().ifPresent(stopTime::setStopHeadsign);
Expand All @@ -344,9 +294,6 @@ public static Result<TripTimesWithStopPattern, UpdateError> createNewTripTimesFr
stopTimes,
deduplicator
).createRealTimeFromScheduledTimes();
if (tripHeadsign != null) {
builder.withTripHeadsign(tripHeadsign);
}

// Update all times to mark trip times as realtime
for (int stopIndex = 0; stopIndex < builder.numberOfStops(); stopIndex++) {
Expand All @@ -366,14 +313,10 @@ public static Result<TripTimesWithStopPattern, UpdateError> createNewTripTimesFr
}

// Set service code of new trip times
builder.withServiceCode(serviceCode);
builder.withServiceCode(serviceCode).withRealTimeState(realTimeState);

// Make sure that updated trip times have the correct real time state
builder.withRealTimeState(realTimeState);

if (wheelchairAccessibility != null) {
builder.withWheelchairAccessibility(wheelchairAccessibility);
}
tripUpdate.tripHeadsign().ifPresent(builder::withTripHeadsign);
tripUpdate.wheelchairAccessibility().ifPresent(builder::withWheelchairAccessibility);

RealTimeTripTimes tripTimes = builder.build();

Expand Down Expand Up @@ -403,14 +346,4 @@ private static void setArrivalAndDeparture(
departureDelay.ifPresent(delay -> builder.withDepartureDelay(stopPositionInPattern, delay))
);
}

static Optional<Accessibility> getWheelchairAccessibility(TripUpdate tripUpdate) {
return tripUpdate
.vehicle()
.flatMap(vehicleDescriptor ->
vehicleDescriptor.hasWheelchairAccessible()
? GtfsRealtimeMapper.mapWheelchairAccessible(vehicleDescriptor.getWheelchairAccessible())
: Optional.empty()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ public String routeLongName() {
return routeLongName;
}

public static AddedRoute ofTripDescriptor(TripDescriptor tripDescriptor) {
var rawTripDescriptor = tripDescriptor.original();
public static AddedRoute ofTripDescriptor(TripUpdate tripDescriptor) {
var rawTripDescriptor = tripDescriptor.descriptor().original();
if (rawTripDescriptor.hasExtension(MfdzRealtimeExtensions.tripDescriptor)) {
var ext = rawTripDescriptor.getExtension(MfdzRealtimeExtensions.tripDescriptor);
var url = ext.getRouteUrl();
Expand Down
Loading
Loading