
org.opentripplanner.transit.model.network.TripPattern Maven / Gradle / Ivy
package org.opentripplanner.transit.model.network;
import static java.util.Objects.requireNonNull;
import static org.opentripplanner.util.lang.ObjectUtils.requireNotInitialized;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.LineString;
import org.opentripplanner.model.PickDrop;
import org.opentripplanner.model.Timetable;
import org.opentripplanner.transit.model.basic.Accessibility;
import org.opentripplanner.transit.model.basic.I18NString;
import org.opentripplanner.transit.model.basic.SubMode;
import org.opentripplanner.transit.model.basic.TransitMode;
import org.opentripplanner.transit.model.framework.AbstractTransitEntity;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.framework.LogInfo;
import org.opentripplanner.transit.model.site.Station;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Direction;
import org.opentripplanner.transit.model.timetable.FrequencyEntry;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.util.geometry.CompactLineStringUtils;
import org.opentripplanner.util.geometry.GeometryUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// TODO OTP2 instances of this class are still mutable after construction with a builder, this will be refactored in a subsequent step
/**
* Represents a group of trips on a route, with the same direction id that all call at the same
* sequence of stops. For each stop, there is a list of departure times, running times, arrival
* times, dwell times, and wheelchair accessibility information (one of each of these per trip per
* stop). Trips are assumed to be non-overtaking, so that an earlier trip never arrives after a
* later trip.
*
* This is called a JOURNEY_PATTERN in the Transmodel vocabulary. However, GTFS calls a Transmodel
* JOURNEY a "trip", thus TripPattern.
*
* The {@code id} is a unique identifier for this trip pattern. For GTFS feeds this is generally
* generated in the format FeedId:Agency:RouteId:DirectionId:PatternNumber. For NeTEx the
* JourneyPattern id is used.
*/
public final class TripPattern
extends AbstractTransitEntity
implements Cloneable, LogInfo {
private static final Logger LOG = LoggerFactory.getLogger(TripPattern.class);
private final Route route;
/**
* The stop-pattern help us reuse the same stops in several trip-patterns; Hence saving memory.
* The field should not be accessible outside the class, and all access is done through method
* delegation, like the {@link #numberOfStops()} and {@link #canBoard(int)} methods.
*/
private final StopPattern stopPattern;
private final Timetable scheduledTimetable;
private String name;
/**
* Geometries of each inter-stop segment of the tripPattern.
*/
private final byte[][] hopGeometries;
/**
* The original TripPattern this replaces at least for one modified trip.
*/
private final TripPattern originalTripPattern;
/**
* Has the TripPattern been created by a real-time update.
*/
private final boolean createdByRealtimeUpdater;
private final RoutingTripPattern routingTripPattern;
public TripPattern(TripPatternBuilder builder) {
super(builder.getId());
this.name = builder.getName();
this.route = builder.getRoute();
this.stopPattern = requireNonNull(builder.getStopPattern());
this.createdByRealtimeUpdater = builder.isCreatedByRealtimeUpdate();
this.scheduledTimetable =
builder.getScheduledTimetable() != null
? builder.getScheduledTimetable()
: new Timetable(this);
this.originalTripPattern = builder.getOriginalTripPattern();
this.hopGeometries = builder.hopGeometries();
this.routingTripPattern = new RoutingTripPattern(this, builder);
}
public static TripPatternBuilder of(@Nonnull FeedScopedId id) {
return new TripPatternBuilder(id);
}
/** The human-readable, unique name for this trip pattern. */
public String getName() {
return name;
}
public void initName(String name) {
this.name = requireNotInitialized(this.name, name);
}
/**
* The GTFS Route of all trips in this pattern.
*/
public Route getRoute() {
return route;
}
/**
* Convenience method to get the route traverse mode, the mode for all trips in this pattern.
*/
public TransitMode getMode() {
return route.getMode();
}
public LineString getHopGeometry(int stopPosInPattern) {
if (hopGeometries != null) {
return CompactLineStringUtils.uncompactLineString(hopGeometries[stopPosInPattern], false);
} else {
return GeometryUtils
.getGeometryFactory()
.createLineString(
new Coordinate[] {
coordinate(stopPattern.getStop(stopPosInPattern)),
coordinate(stopPattern.getStop(stopPosInPattern + 1)),
}
);
}
}
public StopPattern getStopPattern() {
return stopPattern;
}
// TODO OTP2 this method modifies the state, it will be refactored in a subsequent step
public void setHopGeometry(int i, LineString hopGeometry) {
this.hopGeometries[i] = CompactLineStringUtils.compactLineString(hopGeometry, false);
}
public LineString getGeometry() {
if (hopGeometries == null || hopGeometries.length == 0) {
return null;
}
List lineStrings = new ArrayList<>();
for (int i = 0; i < hopGeometries.length; i++) {
lineStrings.add(getHopGeometry(i));
}
return GeometryUtils.concatenateLineStrings(lineStrings);
}
public int numHopGeometries() {
return hopGeometries.length;
}
public int numberOfStops() {
return stopPattern.getSize();
}
public StopLocation getStop(int stopPosInPattern) {
return stopPattern.getStop(stopPosInPattern);
}
public StopLocation firstStop() {
return getStop(0);
}
public StopLocation lastStop() {
return getStop(stopPattern.getSize() - 1);
}
/** Read only list of stops */
public List getStops() {
return stopPattern.getStops();
}
/**
* Find the first stop position in pattern matching the given {@code stop}. The search start at
* position {@code 0}. Return a negative number if not found. Use
* {@link #findAlightStopPositionInPattern(StopLocation)} or
* {@link #findBoardingStopPositionInPattern(StopLocation)} if possible.
*/
public int findStopPosition(StopLocation stop) {
return stopPattern.findStopPosition(stop);
}
/**
* Find the first stop position in pattern matching the given {@code station} where it is allowed
* to board. The search start at position {@code 0}. Return a negative number if not found.
*/
public int findBoardingStopPositionInPattern(Station station) {
return stopPattern.findBoardingPosition(station);
}
/**
* Find the first stop position in pattern matching the given {@code station} where it is allowed
* to alight. The search start at position {@code 1}. Return a negative number if not found.
*/
public int findAlightStopPositionInPattern(Station station) {
return stopPattern.findAlightPosition(station);
}
/**
* Find the first stop position in pattern matching the given {@code stop} where it is allowed to
* board. The search start at position {@code 0}. Return a negative number if not found.
*/
public int findBoardingStopPositionInPattern(StopLocation stop) {
return stopPattern.findBoardingPosition(stop);
}
/**
* Find the first stop position in pattern matching the given {@code stop} where it is allowed to
* alight. The search start at position {@code 1}. Return a negative number if not found.
*/
public int findAlightStopPositionInPattern(StopLocation stop) {
return stopPattern.findAlightPosition(stop);
}
/** Returns whether passengers can alight at a given stop */
public boolean canAlight(int stopIndex) {
return stopPattern.canAlight(stopIndex);
}
/** Returns whether passengers can board at a given stop */
public boolean canBoard(int stopIndex) {
return stopPattern.canBoard(stopIndex);
}
/**
* Returns whether passengers can board at a given stop. This is an inefficient method iterating
* over the stops, do not use it in routing.
*/
public boolean canBoard(StopLocation stop) {
return stopPattern.canBoard(stop);
}
/**
* Returns whether passengers can alight at a given stop. This is an inefficient method iterating
* over the stops, do not use it in routing.
*/
public boolean canAlight(StopLocation stop) {
return stopPattern.canAlight(stop);
}
/** Returns whether a given stop is wheelchair-accessible. */
public boolean wheelchairAccessible(int stopIndex) {
return (stopPattern.getStop(stopIndex).getWheelchairAccessibility() == Accessibility.POSSIBLE);
}
public PickDrop getAlightType(int stopIndex) {
return stopPattern.getDropoff(stopIndex);
}
public PickDrop getBoardType(int stopIndex) {
return stopPattern.getPickup(stopIndex);
}
public boolean isBoardAndAlightAt(int stopIndex, PickDrop value) {
return getBoardType(stopIndex).is(value) && getAlightType(stopIndex).is(value);
}
/* METHODS THAT DELEGATE TO THE SCHEDULED TIMETABLE */
// TODO: These should probably be deprecated. That would require grabbing the scheduled timetable,
// and would avoid mistakes where real-time updates are accidentally not taken into account.
public boolean stopPatternIsEqual(TripPattern other) {
return stopPattern.equals(other.stopPattern);
}
public Trip getTrip(int tripIndex) {
return scheduledTimetable.getTripTimes(tripIndex).getTrip();
}
// TODO OTP2 this method modifies the state, it will be refactored in a subsequent step
/**
* Add the given tripTimes to this pattern's scheduled timetable, recording the corresponding trip
* as one of the scheduled trips on this pattern.
*/
public void add(TripTimes tt) {
// Only scheduled trips (added at graph build time, rather than directly to the timetable
// via updates) are in this list.
scheduledTimetable.addTripTimes(tt);
// Check that all trips added to this pattern are on the initially declared route.
// Identity equality is valid on GTFS entity objects.
if (this.route != tt.getTrip().getRoute()) {
LOG.warn(
"The trip {} is on route {} but its stop pattern is on route {}.",
tt.getTrip(),
tt.getTrip().getRoute(),
route
);
}
}
// TODO OTP2 this method modifies the state, it will be refactored in a subsequent step
/**
* Add the given FrequencyEntry to this pattern's scheduled timetable, recording the corresponding
* trip as one of the scheduled trips on this pattern.
* TODO possible improvements: combine freq entries and TripTimes. Do not keep trips list in TripPattern
* since it is redundant.
*/
public void add(FrequencyEntry freq) {
scheduledTimetable.addFrequencyEntry(freq);
if (this.getRoute() != freq.tripTimes.getTrip().getRoute()) {
LOG.warn(
"The trip {} is on a different route than its stop pattern, which is on {}.",
freq.tripTimes.getTrip(),
route
);
}
}
// TODO OTP2 this method modifies the state, it will be refactored in a subsequent step
/**
* Remove all trips matching the given predicate.
*
* @param removeTrip it the predicate returns true
*/
public void removeTrips(Predicate removeTrip) {
scheduledTimetable.getTripTimes().removeIf(tt -> removeTrip.test(tt.getTrip()));
}
/**
* Checks that this is TripPattern is based of the provided TripPattern and contains same stops
* (but not necessarily with same pickup and dropoff values).
*/
public boolean isModifiedFromTripPatternWithEqualStops(TripPattern other) {
return (
originalTripPattern != null &&
originalTripPattern.equals(other) &&
getStopPattern().stopsEqual(other.getStopPattern())
);
}
/**
* The direction for all the trips in this pattern.
*/
public Direction getDirection() {
return scheduledTimetable.getDirection();
}
/**
* This pattern may have multiple Timetable objects, but they should all contain TripTimes for the
* same trips, in the same order (that of the scheduled Timetable). An exception to this rule may
* arise if unscheduled trips are added to a Timetable. For that case we need to search for
* trips/TripIds in the Timetable rather than the enclosing TripPattern.
*/
public Stream scheduledTripsAsStream() {
var trips = scheduledTimetable.getTripTimes().stream().map(TripTimes::getTrip);
var freqTrips = scheduledTimetable
.getFrequencyEntries()
.stream()
.map(e -> e.tripTimes.getTrip());
return Stream.concat(trips, freqTrips).distinct();
}
/**
* This is the "original" timetable holding the scheduled stop times from GTFS, with no realtime
* updates applied. If realtime stoptime updates are applied, next/previous departure searches
* will be conducted using a different, updated timetable in a snapshot.
*/
public Timetable getScheduledTimetable() {
return scheduledTimetable;
}
/**
* Has the TripPattern been created by a real-time update.
*/
public boolean isCreatedByRealtimeUpdater() {
return createdByRealtimeUpdater;
}
public TripPattern getOriginalTripPattern() {
return originalTripPattern;
}
public I18NString getTripHeadsign() {
var tripTimes = scheduledTimetable.getRepresentativeTripTimes();
if (tripTimes == null) {
return null;
}
return tripTimes.getTrip().getHeadsign();
}
public I18NString getStopHeadsign(int stopIndex) {
var tripTimes = scheduledTimetable.getRepresentativeTripTimes();
if (tripTimes == null) {
return null;
}
return tripTimes.getHeadsign(stopIndex);
}
public boolean matchesModeOrSubMode(TransitMode mode, SubMode transportSubmode) {
return getMode().equals(mode) || route.getNetexSubmode().equals(transportSubmode);
}
public TripPattern clone() {
try {
return (TripPattern) super.clone();
} catch (CloneNotSupportedException e) {
/* cannot happen */
throw new RuntimeException(e);
}
}
/**
* Get the feed id this trip pattern belongs to.
*
* @return feed id for this trip pattern
*/
public String getFeedId() {
// The feed id is the same as the agency id on the route, this allows us to obtain it from there.
return route.getId().getFeedId();
}
public RoutingTripPattern getRoutingTripPattern() {
return routingTripPattern;
}
@Override
public String logName() {
return route.logName();
}
private static Coordinate coordinate(StopLocation s) {
return new Coordinate(s.getLon(), s.getLat());
}
@Override
public boolean sameAs(@Nonnull TripPattern other) {
return (
getId().equals(other.getId()) &&
Objects.equals(this.route, other.route) &&
Objects.equals(this.name, other.name) &&
Objects.equals(this.stopPattern, other.stopPattern) &&
Objects.equals(this.scheduledTimetable, other.scheduledTimetable)
);
}
@Override
public TripPatternBuilder copy() {
return new TripPatternBuilder(this);
}
}