
org.opentripplanner.gtfs.interlining.InterlineProcessor Maven / Gradle / Ivy
The newest version!
package org.opentripplanner.gtfs.interlining;
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;
import java.util.List;
import java.util.Map;
import org.opentripplanner.framework.geometry.SphericalDistanceLibrary;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
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;
import org.opentripplanner.model.transfer.TransferPriority;
import org.opentripplanner.model.transfer.TripTransferPoint;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.utils.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InterlineProcessor {
private static final Logger LOG = LoggerFactory.getLogger(InterlineProcessor.class);
private final DefaultTransferService transferService;
private final int maxInterlineDistance;
private final DataImportIssueStore issueStore;
private final List staySeatedNotAllowed;
private final LocalDate transitServiceStart;
private final int daysInTransitService;
private final CalendarServiceData calendarServiceData;
private final Map daysOfServices = new HashMap<>();
public InterlineProcessor(
DefaultTransferService transferService,
List staySeatedNotAllowed,
int maxInterlineDistance,
DataImportIssueStore issueStore,
CalendarServiceData calendarServiceData
) {
this.transferService = transferService;
this.staySeatedNotAllowed = staySeatedNotAllowed;
this.maxInterlineDistance = maxInterlineDistance > 0 ? maxInterlineDistance : 200;
this.issueStore = issueStore;
this.transitServiceStart = calendarServiceData.getFirstDate().orElse(null);
this.daysInTransitService = calendarServiceData
.getLastDate()
.map(lastDate -> (int) ChronoUnit.DAYS.between(transitServiceStart, lastDate) + 1)
.orElse(0);
this.calendarServiceData = calendarServiceData;
}
public List run(Collection tripPatterns) {
if (daysInTransitService == 0) {
return List.of();
}
var interlinedTrips = this.getInterlinedTrips(tripPatterns);
var transfers = interlinedTrips
.entries()
.stream()
.filter(this::staySeatedAllowed)
.map(p -> {
var constraint = TransferConstraint.of();
constraint.staySeated();
constraint.priority(TransferPriority.ALLOWED);
var fromTrip = p.getValue().from();
var toTrip = p.getValue().to();
var from = new TripTransferPoint(fromTrip, p.getKey().from().numberOfStops() - 1);
var to = new TripTransferPoint(toTrip, 0);
LOG.debug(
"Creating stay-seated transfer from trip {} (route {}) to trip {} (route {})",
fromTrip.getId(),
fromTrip.getRoute().getId(),
toTrip.getId(),
toTrip.getRoute().getId()
);
return new ConstrainedTransfer(null, from, to, constraint.build());
})
.toList();
if (!transfers.isEmpty()) {
LOG.info(
"Found {} pairs of trips for which stay-seated (interlined) transfers were created",
transfers.size()
);
transferService.addAll(transfers);
}
return transfers;
}
private boolean staySeatedAllowed(Map.Entry p) {
var fromTrip = p.getValue().from();
var toTrip = p.getValue().to();
return staySeatedNotAllowed
.stream()
.noneMatch(
t ->
t.fromTrip().getId().equals(fromTrip.getId()) && t.toTrip().getId().equals(toTrip.getId())
);
}
/**
* Identify interlined trips (where a physical vehicle continues on to another logical trip).
*/
private Multimap getInterlinedTrips(
Collection tripPatterns
) {
/* Record which Pattern each interlined TripTimes belongs to. */
Map patternForTripTimes = new HashMap<>();
/* TripTimes grouped by the block ID of their trips. Must be a ListMultimap to allow sorting. */
ListMultimap tripTimesForBlockId = ArrayListMultimap.create();
LOG.info("Finding interlining trips based on block IDs.");
for (TripPattern pattern : tripPatterns) {
Timetable timetable = pattern.getScheduledTimetable();
/* TODO: Block semantics seem undefined for frequency trips, so skip them? */
for (TripTimes tripTimes : timetable.getTripTimes()) {
Trip trip = tripTimes.getTrip();
if (StringUtils.hasValue(trip.getGtfsBlockId())) {
tripTimesForBlockId.put(trip.getGtfsBlockId(), tripTimes);
// For space efficiency, only record times that are part of a block.
patternForTripTimes.put(tripTimes, pattern);
}
}
}
// Associate pairs of TripPatterns with lists of trips that continue from one pattern to the other.
Multimap interlines = ArrayListMultimap.create();
// 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 blockTripTimes = tripTimesForBlockId.get(blockId);
Collections.sort(blockTripTimes);
for (int i = 0; i < blockTripTimes.size(); i++) {
var fromTripTimes = blockTripTimes.get(i);
var fromServiceId = fromTripTimes.getTrip().getServiceId();
BitSet uncoveredDays = getAndCopyDaysForService(fromServiceId);
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;
}
BitSet daysForToTripTimes = getDaysForService(toTripTimes.getTrip().getServiceId());
if (
uncoveredDays.intersects(daysForToTripTimes) &&
createInterline(fromTripTimes, toTripTimes, blockId, patternForTripTimes, interlines)
) {
uncoveredDays.andNot(daysForToTripTimes);
if (uncoveredDays.isEmpty()) {
break;
}
}
}
}
}
return interlines;
}
/**
* 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 boolean createInterline(
TripTimes fromTripTimes,
TripTimes toTripTimes,
String blockId,
Map patternForTripTimes,
Multimap 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;
}
var fromPattern = patternForTripTimes.get(fromTripTimes);
var 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;
}
}
/**
* This method should only be used when the returned {@link BitSet} is not altered as the returned
* value is cached for future use. If the BitSet needs to be modified, use
* {@link #getAndCopyDaysForService(FeedScopedId)}
*
* @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) {
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 daysForService;
}
/**
* This {@link BitSet} returned from this method can be modified. If there is no need to modify
* it, {@link #getDaysForService(FeedScopedId)} can be used instead.
*
* @return {@link BitSet} which index starts at the first overall date of the services and the
* last index is the last date.
*/
private BitSet getAndCopyDaysForService(FeedScopedId serviceId) {
return (BitSet) getDaysForService(serviceId).clone();
}
private record TripPatternPair(TripPattern from, TripPattern to) {}
private record TripPair(Trip from, Trip to) {}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy