Skip to content

Commit

Permalink
Create interline transfers for trips that share the same service date…
Browse files Browse the repository at this point in the history
… and block
  • Loading branch information
optionsome committed Jun 2, 2023
1 parent 8b0046c commit 5f530d2
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -14,6 +17,7 @@
import org.opentripplanner.graph_builder.issues.InterliningTeleport;
import org.opentripplanner.gtfs.mapping.StaySeatedNotAllowed;
import org.opentripplanner.model.Timetable;
import org.opentripplanner.model.calendar.CalendarServiceData;
import org.opentripplanner.model.transfer.ConstrainedTransfer;
import org.opentripplanner.model.transfer.DefaultTransferService;
import org.opentripplanner.model.transfer.TransferConstraint;
Expand All @@ -33,17 +37,27 @@ public class InterlineProcessor {
private final int maxInterlineDistance;
private final DataImportIssueStore issueStore;
private final List<StaySeatedNotAllowed> staySeatedNotAllowed;
private final LocalDate transitServiceStart;
private final int daysInTransitService;
private final CalendarServiceData calendarServiceData;
private final Map<FeedScopedId, BitSet> daysOfServices = new HashMap<>();

public InterlineProcessor(
DefaultTransferService transferService,
List<StaySeatedNotAllowed> staySeatedNotAllowed,
int maxInterlineDistance,
DataImportIssueStore issueStore
DataImportIssueStore issueStore,
CalendarServiceData calendarServiceData
) {
this.transferService = transferService;
this.staySeatedNotAllowed = staySeatedNotAllowed;
this.maxInterlineDistance = maxInterlineDistance > 0 ? maxInterlineDistance : 200;
this.issueStore = issueStore;
this.transitServiceStart = calendarServiceData.getFirstDate().get();
this.daysInTransitService =
(int) ChronoUnit.DAYS.between(transitServiceStart, calendarServiceData.getLastDate().get()) +
1;
this.calendarServiceData = calendarServiceData;
}

public List<ConstrainedTransfer> run(Collection<TripPattern> tripPatterns) {
Expand Down Expand Up @@ -105,8 +119,8 @@ private Multimap<TripPatternPair, TripPair> getInterlinedTrips(
/* Record which Pattern each interlined TripTimes belongs to. */
Map<TripTimes, TripPattern> patternForTripTimes = new HashMap<>();

/* TripTimes grouped by the block ID and service ID of their trips. Must be a ListMultimap to allow sorting. */
ListMultimap<BlockIdAndServiceId, TripTimes> tripTimesForBlock = ArrayListMultimap.create();
/* TripTimes grouped by the block ID of their trips. Must be a ListMultimap to allow sorting. */
ListMultimap<String, TripTimes> tripTimesForBlockId = ArrayListMultimap.create();

LOG.info("Finding interlining trips based on block IDs.");
for (TripPattern pattern : tripPatterns) {
Expand All @@ -115,7 +129,7 @@ private Multimap<TripPatternPair, TripPair> getInterlinedTrips(
for (TripTimes tripTimes : timetable.getTripTimes()) {
Trip trip = tripTimes.getTrip();
if (StringUtils.hasValue(trip.getGtfsBlockId())) {
tripTimesForBlock.put(BlockIdAndServiceId.ofTrip(trip), tripTimes);
tripTimesForBlockId.put(trip.getGtfsBlockId(), tripTimes);
// For space efficiency, only record times that are part of a block.
patternForTripTimes.put(tripTimes, pattern);
}
Expand All @@ -125,66 +139,122 @@ private Multimap<TripPatternPair, TripPair> getInterlinedTrips(
// Associate pairs of TripPatterns with lists of trips that continue from one pattern to the other.
Multimap<TripPatternPair, TripPair> interlines = ArrayListMultimap.create();

// Sort trips within each block by first departure time, then iterate over trips in this block and service,
// linking them. Has no effect on single-trip blocks.
SERVICE_BLOCK:for (BlockIdAndServiceId block : tripTimesForBlock.keySet()) {
List<TripTimes> blockTripTimes = tripTimesForBlock.get(block);
// Sort trips within each block by first departure time, then iterate over trips in this block,
// linking them. One from trip can have multiple interline transfers if trip which interlines
// with the from trip doesn't operate on every service date of the from trip.
for (String blockId : tripTimesForBlockId.keySet()) {
List<TripTimes> blockTripTimes = tripTimesForBlockId.get(blockId);
Collections.sort(blockTripTimes);
TripTimes prev = null;
for (TripTimes curr : blockTripTimes) {
if (prev != null) {
if (prev.getDepartureTime(prev.getNumStops() - 1) > curr.getArrivalTime(0)) {
LOG.error(
"Trip times within block {} are not increasing on service {} after trip {}.",
block.blockId(),
block.serviceId(),
prev.getTrip().getId()
);
continue SERVICE_BLOCK;
for (int i = 0; i < blockTripTimes.size(); i++) {
var fromTripTimes = blockTripTimes.get(i);
var fromServiceId = fromTripTimes.getTrip().getServiceId();
BitSet uncoveredDays = getDaysForService(fromServiceId, true);
for (int j = i + 1; j < blockTripTimes.size(); j++) {
var toTripTimes = blockTripTimes.get(j);
var toServiceId = toTripTimes.getTrip().getServiceId();
if (
toServiceId.equals(fromServiceId) &&
createInterline(fromTripTimes, toTripTimes, blockId, patternForTripTimes, interlines)
) {
break;
}
TripPattern prevPattern = patternForTripTimes.get(prev);
TripPattern currPattern = patternForTripTimes.get(curr);
var fromStop = prevPattern.lastStop();
var toStop = currPattern.firstStop();
double teleportationDistance = SphericalDistanceLibrary.fastDistance(
fromStop.getLat(),
fromStop.getLon(),
toStop.getLat(),
toStop.getLon()
BitSet daysForToTripTimes = getDaysForService(
toTripTimes.getTrip().getServiceId(),
false
);
if (teleportationDistance > maxInterlineDistance) {
issueStore.add(
new InterliningTeleport(
prev.getTrip(),
block.blockId(),
(int) teleportationDistance,
fromStop,
toStop
)
);
// Only skip this particular interline edge; there may be other valid ones in the block.
} else {
interlines.put(
new TripPatternPair(prevPattern, currPattern),
new TripPair(prev.getTrip(), curr.getTrip())
);
if (
uncoveredDays.intersects(daysForToTripTimes) &&
createInterline(fromTripTimes, toTripTimes, blockId, patternForTripTimes, interlines)
) {
uncoveredDays.andNot(daysForToTripTimes);
if (uncoveredDays.isEmpty()) {
break;
}
}
}
prev = curr;
}
}

return interlines;
}

/**
* This compound key object is used when grouping interlining trips together by (serviceId,
* blockId).
* Validates that trip times are continuous and that the transfer stop(s) are not too far away
* from each other. Then creates interline between the trips.
*
* @return true if interline has been created or if there is an issue preventing an interline
* creation for certain service dates.
*/
private record BlockIdAndServiceId(String blockId, FeedScopedId serviceId) {
static BlockIdAndServiceId ofTrip(Trip trip) {
return new BlockIdAndServiceId(trip.getGtfsBlockId(), trip.getServiceId());
private boolean createInterline(
TripTimes fromTripTimes,
TripTimes toTripTimes,
String blockId,
Map<TripTimes, TripPattern> patternForTripTimes,
Multimap<TripPatternPair, TripPair> interlines
) {
if (
fromTripTimes.getDepartureTime(fromTripTimes.getNumStops() - 1) >
toTripTimes.getArrivalTime(0)
) {
LOG.error(
"Trip times within block {} are not increasing on after trip {}.",
blockId,
fromTripTimes.getTrip().getId()
);
return true;
}
TripPattern fromPattern = patternForTripTimes.get(fromTripTimes);
TripPattern toPattern = patternForTripTimes.get(toTripTimes);
var fromStop = fromPattern.lastStop();
var toStop = toPattern.firstStop();
double teleportationDistance = SphericalDistanceLibrary.fastDistance(
fromStop.getLat(),
fromStop.getLon(),
toStop.getLat(),
toStop.getLon()
);
if (teleportationDistance > maxInterlineDistance) {
issueStore.add(
new InterliningTeleport(
fromTripTimes.getTrip(),
blockId,
(int) teleportationDistance,
fromStop,
toStop
)
);
// Only skip this particular interline edge; there may be other valid ones in the block for the
// from trip.
return false;
} else {
interlines.put(
new TripPatternPair(fromPattern, toPattern),
new TripPair(fromTripTimes.getTrip(), toTripTimes.getTrip())
);
return true;
}
}

/**
* @param mutable This should be set as true if the {@link BitSet} will be modified as otherwise
* we don't have to create a copy of a cached entity.
* @return {@link BitSet} which index starts at the first overall date of the services and the
* last index is the last date.
*/
private BitSet getDaysForService(FeedScopedId serviceId, boolean mutable) {
BitSet daysForService = this.daysOfServices.get(serviceId);
if (daysForService == null) {
daysForService = new BitSet(daysInTransitService);
var serviceDates = calendarServiceData.getServiceDatesForServiceId(serviceId);
if (serviceDates != null) {
for (LocalDate serviceDate : serviceDates) {
int daysBetween = (int) ChronoUnit.DAYS.between(transitServiceStart, serviceDate);
daysForService.set(daysBetween);
}
}
daysOfServices.put(serviceId, daysForService);
}
return mutable ? (BitSet) daysForService.clone() : daysForService;
}

private record TripPatternPair(TripPattern from, TripPattern to) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ public void buildGraph() {
transitModel.getTransferService(),
builder.getStaySeatedNotAllowed(),
gtfsBundle.maxInterlineDistance(),
issueStore
issueStore,
calendarServiceData
)
.run(otpTransitService.getTripPatterns());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.opentripplanner.framework.time.ServiceDateUtils;
import org.opentripplanner.transit.model.framework.FeedScopedId;
Expand Down Expand Up @@ -72,6 +73,24 @@ public void add(CalendarServiceData other) {
}
}

public Optional<LocalDate> getFirstDate() {
return serviceDatesByServiceId
.values()
.stream()
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.min(LocalDate::compareTo);
}

public Optional<LocalDate> getLastDate() {
return serviceDatesByServiceId
.values()
.stream()
.filter(list -> !list.isEmpty())
.map(list -> list.get(list.size() - 1))
.max(LocalDate::compareTo);
}

/* private methods */

private static <T> List<T> sortedImmutableList(Collection<T> c) {
Expand Down
Loading

0 comments on commit 5f530d2

Please sign in to comment.