diff --git a/src/main/java/org/opentripplanner/graph_builder/DataImportIssueStore.java b/src/main/java/org/opentripplanner/graph_builder/DataImportIssueStore.java index 7cab3ec9db5..4789cd09d75 100644 --- a/src/main/java/org/opentripplanner/graph_builder/DataImportIssueStore.java +++ b/src/main/java/org/opentripplanner/graph_builder/DataImportIssueStore.java @@ -20,6 +20,10 @@ public DataImportIssueStore(boolean storeIssues) { this.storeIssues = storeIssues; } + public static DataImportIssueStore noop() { + return new DataImportIssueStore(false); + } + public void add(DataImportIssue issue) { ISSUE_LOG.debug("{} - {}", issue.getType(), issue.getMessage()); if (storeIssues) { diff --git a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java index cd77f207dbf..dc98d77206a 100644 --- a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java @@ -42,6 +42,7 @@ import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.util.OTPFeature; import org.opentripplanner.util.OtpAppException; +import org.opentripplanner.util.time.DurationUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -123,7 +124,6 @@ public static GraphBuilder create( } gtfsBundle.parentStationTransfers = config.stationTransfers; gtfsBundle.subwayAccessTime = config.getSubwayAccessTimeSeconds(); - gtfsBundle.maxInterlineDistance = config.maxInterlineDistance; gtfsBundle.setMaxStopToShapeSnapDistance(config.maxStopToShapeSnapDistance); gtfsBundles.add(gtfsBundle); } @@ -131,7 +131,8 @@ public static GraphBuilder create( gtfsBundles, config.getTransitServicePeriod(), config.fareServiceFactory, - config.discardMinTransferTimes + config.discardMinTransferTimes, + config.maxInterlineDistance ); graphBuilder.addModule(gtfsModule); } @@ -279,7 +280,8 @@ public void run() { long endTime = System.currentTimeMillis(); LOG.info( - String.format("Graph building took %.1f minutes.", (endTime - startTime) / 1000 / 60.0) + "Graph building took {}.", + DurationUtils.durationToStr(Duration.ofMillis(endTime - startTime)) ); LOG.info("Main graph size: |V|={} |E|={}", graph.countVertices(), graph.countEdges()); } diff --git a/src/main/java/org/opentripplanner/graph_builder/model/GtfsBundle.java b/src/main/java/org/opentripplanner/graph_builder/model/GtfsBundle.java index 57544e2e338..6f679afcbfe 100644 --- a/src/main/java/org/opentripplanner/graph_builder/model/GtfsBundle.java +++ b/src/main/java/org/opentripplanner/graph_builder/model/GtfsBundle.java @@ -37,8 +37,6 @@ public class GtfsBundle { private double maxStopToShapeSnapDistance = 150; - public int maxInterlineDistance; - /** Used by unit tests */ public GtfsBundle(File gtfsFile) { this(DataStoreFactory.compositeSource(gtfsFile, FileType.GTFS)); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/GtfsModule.java b/src/main/java/org/opentripplanner/graph_builder/module/GtfsModule.java index 3348662932b..463410060e5 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/GtfsModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/GtfsModule.java @@ -4,7 +4,6 @@ import java.awt.Color; import java.io.IOException; import java.io.Serializable; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -29,7 +28,8 @@ import org.opentripplanner.ext.flex.FlexTripsMapper; import org.opentripplanner.graph_builder.DataImportIssueStore; import org.opentripplanner.graph_builder.model.GtfsBundle; -import org.opentripplanner.graph_builder.module.geometry.GeometryAndBlockProcessor; +import org.opentripplanner.graph_builder.module.geometry.GeometryProcessor; +import org.opentripplanner.graph_builder.module.interlining.InterlineProcessor; import org.opentripplanner.graph_builder.services.GraphBuilderModule; import org.opentripplanner.gtfs.GenerateTripPatternsOperation; import org.opentripplanner.gtfs.RepairStopTimesForEachTripOperation; @@ -62,29 +62,25 @@ public class GtfsModule implements GraphBuilderModule { private final List gtfsBundles; private final FareServiceFactory fareServiceFactory; private final boolean discardMinTransferTimes; - private DataImportIssueStore issueStore; + private final int maxInterlineDistance; private int nextAgencyId = 1; // used for generating agency IDs to resolve ID conflicts public GtfsModule( List bundles, ServiceDateInterval transitPeriodLimit, FareServiceFactory fareServiceFactory, - boolean discardMinTransferTimes + boolean discardMinTransferTimes, + int maxInterlineDistance ) { this.gtfsBundles = bundles; this.transitPeriodLimit = transitPeriodLimit; this.fareServiceFactory = fareServiceFactory; this.discardMinTransferTimes = discardMinTransferTimes; + this.maxInterlineDistance = maxInterlineDistance; } public GtfsModule(List bundles, ServiceDateInterval transitPeriodLimit) { - this(bundles, transitPeriodLimit, new DefaultFareServiceFactory(), false); - } - - public List provides() { - List result = new ArrayList<>(); - result.add("transit"); - return result; + this(bundles, transitPeriodLimit, new DefaultFareServiceFactory(), false, 100); } @Override @@ -94,8 +90,6 @@ public void buildGraph( HashMap, Object> extra, DataImportIssueStore issueStore ) { - this.issueStore = issueStore; - // we're about to add another agency to the graph, so clear the cached timezone // in case it should change // OTP doesn't currently support multiple time zones in a single graph; @@ -128,10 +122,16 @@ public void buildGraph( builder.getFlexTripsById().addAll(FlexTripsMapper.createFlexTrips(builder, issueStore)); } - repairStopTimesForEachTrip(builder.getStopTimesSortedByTrip()); + repairStopTimesForEachTrip(builder.getStopTimesSortedByTrip(), issueStore); // NB! The calls below have side effects - the builder state is updated! - createTripPatterns(graph, transitModel, builder, calendarServiceData.getServiceIds()); + createTripPatterns( + graph, + transitModel, + builder, + calendarServiceData.getServiceIds(), + issueStore + ); OtpTransitService otpTransitService = builder.build(); @@ -140,8 +140,20 @@ public void buildGraph( addTransitModelToGraph(graph, transitModel, gtfsBundle, otpTransitService); - createGeometryAndBlockProcessor(gtfsBundle, otpTransitService) - .run(graph, transitModel, issueStore); + new GeometryProcessor( + otpTransitService, + gtfsBundle.getMaxStopToShapeSnapDistance(), + issueStore + ) + .run(transitModel); + + new InterlineProcessor( + transitModel.getTransferService(), + builder.getStaySeatedNotAllowed(), + maxInterlineDistance, + issueStore + ) + .run(transitModel.getAllTripPatterns()); fareServiceFactory.processGtfs(otpTransitService); graph.putService(FareService.class, fareServiceFactory.makeFareService()); @@ -179,24 +191,28 @@ public void checkInputs() { /* Private Methods */ /** - * This method have side-effects, the {@code stopTimesByTrip} is updated. + * This method has side effects, the {@code stopTimesByTrip} is updated. */ - private void repairStopTimesForEachTrip(TripStopTimes stopTimesByTrip) { + private void repairStopTimesForEachTrip( + TripStopTimes stopTimesByTrip, + DataImportIssueStore issueStore + ) { new RepairStopTimesForEachTripOperation(stopTimesByTrip, issueStore).run(); } /** - * This method have side-effects, the {@code builder} is updated with new TripPatterns. + * This method has side effects, the {@code builder} is updated with new TripPatterns. */ private void createTripPatterns( Graph graph, TransitModel transitModel, OtpTransitServiceBuilder builder, - Set calServiceIds + Set calServiceIds, + DataImportIssueStore issueStore ) { GenerateTripPatternsOperation buildTPOp = new GenerateTripPatternsOperation( builder, - this.issueStore, + issueStore, graph.deduplicator, calServiceIds ); @@ -224,17 +240,6 @@ private void addTransitModelToGraph( ); } - private GeometryAndBlockProcessor createGeometryAndBlockProcessor( - GtfsBundle gtfsBundle, - OtpTransitService transitService - ) { - return new GeometryAndBlockProcessor( - transitService, - gtfsBundle.getMaxStopToShapeSnapDistance(), - gtfsBundle.maxInterlineDistance - ); - } - private GtfsMutableRelationalDao loadBundle(GtfsBundle gtfsBundle) throws IOException { StoreImpl store = new StoreImpl(new GtfsRelationalDaoImpl()); store.open(); @@ -319,7 +324,7 @@ private GtfsMutableRelationalDao loadBundle(GtfsBundle gtfsBundle) throws IOExce /** * Generates routeText colors for routes with routeColor and without routeTextColor *

- * If route doesn't have color or already has routeColor and routeTextColor nothing is done. + * If a route doesn't have color or already has routeColor and routeTextColor nothing is done. *

* textColor can be black or white. White for dark colors and black for light colors of * routeColor. If color is light or dark is calculated based on luminance formula: sqrt( diff --git a/src/main/java/org/opentripplanner/graph_builder/module/geometry/BlockIdAndServiceId.java b/src/main/java/org/opentripplanner/graph_builder/module/geometry/BlockIdAndServiceId.java deleted file mode 100644 index a920a68a7dd..00000000000 --- a/src/main/java/org/opentripplanner/graph_builder/module/geometry/BlockIdAndServiceId.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.opentripplanner.graph_builder.module.geometry; - -import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.timetable.Trip; - -/** - * This compound key object is used when grouping interlining trips together by (serviceId, - * blockId). - */ -class BlockIdAndServiceId { - - String blockId; - FeedScopedId serviceId; - - BlockIdAndServiceId(Trip trip) { - this.blockId = trip.getGtfsBlockId(); - this.serviceId = trip.getServiceId(); - } - - @Override - public int hashCode() { - return blockId.hashCode() * 31 + serviceId.hashCode(); - } - - public boolean equals(Object o) { - if (o instanceof BlockIdAndServiceId) { - BlockIdAndServiceId other = ((BlockIdAndServiceId) o); - return other.blockId.equals(blockId) && other.serviceId.equals(serviceId); - } - return false; - } -} diff --git a/src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryAndBlockProcessor.java b/src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessor.java similarity index 82% rename from src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryAndBlockProcessor.java rename to src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessor.java index ba81f7a40cf..661cd3a70af 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryAndBlockProcessor.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessor.java @@ -1,14 +1,8 @@ package org.opentripplanner.graph_builder.module.geometry; -import com.google.common.base.Strings; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -24,26 +18,20 @@ import org.locationtech.jts.linearref.LocationIndexedLine; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; -import org.opentripplanner.common.model.P2; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.graph_builder.DataImportIssueStore; import org.opentripplanner.graph_builder.issues.BogusShapeDistanceTraveled; import org.opentripplanner.graph_builder.issues.BogusShapeGeometry; import org.opentripplanner.graph_builder.issues.BogusShapeGeometryCaught; -import org.opentripplanner.graph_builder.issues.InterliningTeleport; import org.opentripplanner.graph_builder.issues.MissingShapeGeometry; import org.opentripplanner.graph_builder.issues.ShapeGeometryTooFar; import org.opentripplanner.gtfs.GtfsContext; import org.opentripplanner.model.OtpTransitService; import org.opentripplanner.model.ShapePoint; import org.opentripplanner.model.StopTime; -import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TripPattern; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.trippattern.TripTimes; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.StopLocation; -import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.util.logging.ProgressTracker; import org.slf4j.Logger; @@ -51,50 +39,45 @@ /** * Once transit model entities have been loaded into the graph, this post-processes them to extract - * and prepare geometries. It also does some other postprocessing involving interlined blocks. + * and prepare geometries. * *

- * THREAD SAFETY The computation runs in parallel so be careful about threadsafety when modifying + * THREAD SAFETY The computation runs in parallel so be careful about thread safety when modifying * the logic here. */ -public class GeometryAndBlockProcessor { +public class GeometryProcessor { - private static final Logger LOG = LoggerFactory.getLogger(GeometryAndBlockProcessor.class); + private static final Logger LOG = LoggerFactory.getLogger(GeometryProcessor.class); private static final GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory(); private final OtpTransitService transitService; - // this is threadsafe implementation + // this is a thread-safe implementation private final Map geometriesByShapeSegmentKey = new ConcurrentHashMap<>(); - // this is threadsafe implementation + // this is a thread-safe implementation private final Map geometriesByShapeId = new ConcurrentHashMap<>(); - // this is threadsafe implementation + // this is a thread-safe implementation private final Map distancesByShapeId = new ConcurrentHashMap<>(); private final double maxStopToShapeSnapDistance; - private final int maxInterlineDistance; - private DataImportIssueStore issueStore; + private final DataImportIssueStore issueStore; - public GeometryAndBlockProcessor(GtfsContext context) { - this(context.getTransitService(), -1, -1); + public GeometryProcessor(GtfsContext context) { + this(context.getTransitService(), -1, DataImportIssueStore.noop()); } - public GeometryAndBlockProcessor( - // TODO OTP2 - Operate on the builder, not the transit service and move the executon of + public GeometryProcessor( + // TODO OTP2 - Operate on the builder, not the transit service and move the execution of // - this to where the builder is in context. OtpTransitService transitService, double maxStopToShapeSnapDistance, - int maxInterlineDistance + DataImportIssueStore issueStore ) { this.transitService = transitService; this.maxStopToShapeSnapDistance = maxStopToShapeSnapDistance > 0 ? maxStopToShapeSnapDistance : 150; - this.maxInterlineDistance = maxInterlineDistance > 0 ? maxInterlineDistance : 200; + this.issueStore = issueStore; } // TODO OTP2 - Instead of exposing the graph (the entire world) to this class, this class should // - Create a datastructure and return it, then that should be injected into the graph. - public void run(Graph graph, TransitModel transitModel) { - run(graph, transitModel, new DataImportIssueStore(false)); - } - /** * Generate the edges. Assumes that there are already vertices in the graph for the stops. *

@@ -103,9 +86,7 @@ public void run(Graph graph, TransitModel transitModel) { * the graph, the OtpTransitService and others are not. */ @SuppressWarnings("Convert2MethodRef") - public void run(Graph graph, TransitModel transitModel, DataImportIssueStore issueStore) { - this.issueStore = issueStore; - + public void run(TransitModel transitModel) { LOG.info("Processing geometries and blocks on graph..."); // Wwe have to build the hop geometries before we throw away the modified stopTimes, saving @@ -159,9 +140,7 @@ public void run(Graph graph, TransitModel transitModel, DataImportIssueStore iss tripPattern.setHopGeometries(hopGeometries); } } - /* Identify interlined trips and create the necessary edges. */ - interline(tripPatterns); } private static boolean equals(LinearLocation startIndex, LinearLocation endIndex) { @@ -172,80 +151,6 @@ private static boolean equals(LinearLocation startIndex, LinearLocation endIndex ); } - /** - * Identify interlined trips (where a physical vehicle continues on to another logical trip) and - * update the TripPatterns accordingly. - */ - private void interline(Collection tripPatterns) { - /* Record which Pattern each interlined TripTimes belongs to. */ - Map patternForTripTimes = new HashMap<>(); - - /* TripTimes grouped by the block ID and service ID of their trips. Must be a ListMultimap to allow sorting. */ - ListMultimap tripTimesForBlock = 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 (!Strings.isNullOrEmpty(trip.getGtfsBlockId())) { - tripTimesForBlock.put(new BlockIdAndServiceId(trip), 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, P2> 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 blockTripTimes = tripTimesForBlock.get(block); - 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; - } - 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() - ); - if (teleportationDistance > maxInterlineDistance) { - issueStore.add( - new InterliningTeleport(prev.getTrip(), block.blockId, (int) teleportationDistance) - ); - // Only skip this particular interline edge; there may be other valid ones in the block. - } else { - interlines.put( - new P2<>(prevPattern, currPattern), - new P2<>(prev.getTrip(), curr.getTrip()) - ); - } - } - prev = curr; - } - } - - LOG.info("Done finding interlining trips."); - } - /** * Creates a set of geometries for a single trip, considering the GTFS shapes.txt, The geometry is * broken down into one geometry per inter-stop segment ("hop"). We also need a shape for the diff --git a/src/main/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessor.java b/src/main/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessor.java new file mode 100644 index 00000000000..6cf5fa6dc8d --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessor.java @@ -0,0 +1,184 @@ +package org.opentripplanner.graph_builder.module.interlining; + +import com.google.common.base.Strings; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.opentripplanner.common.geometry.SphericalDistanceLibrary; +import org.opentripplanner.common.model.P2; +import org.opentripplanner.graph_builder.DataImportIssueStore; +import org.opentripplanner.graph_builder.issues.InterliningTeleport; +import org.opentripplanner.gtfs.mapping.StaySeatedNotAllowed; +import org.opentripplanner.model.Timetable; +import org.opentripplanner.model.TripPattern; +import org.opentripplanner.model.transfer.ConstrainedTransfer; +import org.opentripplanner.model.transfer.TransferConstraint; +import org.opentripplanner.model.transfer.TransferPriority; +import org.opentripplanner.model.transfer.TransferService; +import org.opentripplanner.model.transfer.TripTransferPoint; +import org.opentripplanner.routing.trippattern.TripTimes; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.Trip; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InterlineProcessor { + + private static final Logger LOG = LoggerFactory.getLogger(InterlineProcessor.class); + private final TransferService transferService; + private final int maxInterlineDistance; + private final DataImportIssueStore issueStore; + private final List staySeatedNotAllowed; + + public InterlineProcessor( + TransferService transferService, + List staySeatedNotAllowed, + int maxInterlineDistance, + DataImportIssueStore issueStore + ) { + this.transferService = transferService; + this.staySeatedNotAllowed = staySeatedNotAllowed; + this.maxInterlineDistance = maxInterlineDistance > 0 ? maxInterlineDistance : 200; + this.issueStore = issueStore; + } + + public List run(Collection tripPatterns) { + var interlinedTrips = this.getInterlinedTrips(tripPatterns); + var transfers = interlinedTrips + .entries() + .stream() + .filter(this::staySeatedAllowed) + .map(p -> { + var constraint = TransferConstraint.create(); + constraint.staySeated(); + constraint.priority(TransferPriority.ALLOWED); + + var fromTrip = p.getValue().first; + var toTrip = p.getValue().second; + + var from = new TripTransferPoint(fromTrip, p.getKey().first.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", + interlinedTrips.keySet().size() + ); + + transferService.addAll(transfers); + } + return transfers; + } + + private boolean staySeatedAllowed(Map.Entry, P2> p) { + var fromTrip = p.getValue().first; + var toTrip = p.getValue().second; + 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, P2> getInterlinedTrips( + Collection tripPatterns + ) { + /* Record which Pattern each interlined TripTimes belongs to. */ + Map patternForTripTimes = new HashMap<>(); + + /* TripTimes grouped by the block ID and service ID of their trips. Must be a ListMultimap to allow sorting. */ + ListMultimap tripTimesForBlock = 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 (!Strings.isNullOrEmpty(trip.getGtfsBlockId())) { + tripTimesForBlock.put(BlockIdAndServiceId.ofTrip(trip), 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, P2> 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 blockTripTimes = tripTimesForBlock.get(block); + 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; + } + 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() + ); + if (teleportationDistance > maxInterlineDistance) { + issueStore.add( + new InterliningTeleport(prev.getTrip(), block.blockId(), (int) teleportationDistance) + ); + // Only skip this particular interline edge; there may be other valid ones in the block. + } else { + interlines.put( + new P2<>(prevPattern, currPattern), + new P2<>(prev.getTrip(), curr.getTrip()) + ); + } + } + prev = curr; + } + } + + return interlines; + } + + /** + * This compound key object is used when grouping interlining trips together by (serviceId, + * blockId). + */ + private record BlockIdAndServiceId(String blockId, FeedScopedId serviceId) { + static BlockIdAndServiceId ofTrip(Trip trip) { + return new BlockIdAndServiceId(trip.getGtfsBlockId(), trip.getServiceId()); + } + } +} diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java index d82080b3eec..e8adfb37aec 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java @@ -182,6 +182,8 @@ private void mapAndAddTransfersToBuilder() { discardMinTransferTimes, issueStore ); - builder.getTransfers().addAll(transferMapper.map(data.getAllTransfers())); + var result = transferMapper.map(data.getAllTransfers()); + builder.getTransfers().addAll(result.constrainedTransfers()); + builder.getStaySeatedNotAllowed().addAll(result.staySeatedNotAllowed()); } } diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/StaySeatedNotAllowed.java b/src/main/java/org/opentripplanner/gtfs/mapping/StaySeatedNotAllowed.java new file mode 100644 index 00000000000..532a472c724 --- /dev/null +++ b/src/main/java/org/opentripplanner/gtfs/mapping/StaySeatedNotAllowed.java @@ -0,0 +1,5 @@ +package org.opentripplanner.gtfs.mapping; + +import org.opentripplanner.transit.model.timetable.Trip; + +public record StaySeatedNotAllowed(Trip fromTrip, Trip toTrip) {} diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/TransferMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/TransferMapper.java index 949c74e066d..f4c23c5359f 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/TransferMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/TransferMapper.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Objects; import java.util.function.Predicate; -import java.util.stream.Collectors; import org.onebusaway.gtfs.model.Transfer; import org.opentripplanner.graph_builder.DataImportIssueStore; import org.opentripplanner.graph_builder.issues.IgnoredGtfsTransfer; @@ -27,9 +26,6 @@ import org.opentripplanner.transit.model.site.Stop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.util.logging.ThrottleLogger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Responsible for mapping GTFS Transfer into the OTP model. @@ -39,9 +35,6 @@ */ class TransferMapper { - private static final Logger LOG = LoggerFactory.getLogger(TransferMapper.class); - private static final Logger FIXED_ROUTE_ERROR = ThrottleLogger.throttle(LOG); - /** * This transfer is recommended over other transfers. The routing algorithm should prefer this * transfer compared to other transfers, for example by assigning a lower weight to it. @@ -129,21 +122,37 @@ static TransferPriority mapTypeToPriority(int type) { throw new IllegalArgumentException("Mapping missing for type: " + type); } - Collection map(Collection allTransfers) { + TransferMappingResult map(Collection allTransfers) { setup(!allTransfers.isEmpty()); - return allTransfers + List constrainedTransfers = allTransfers .stream() .map(this::map) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .toList(); + + List staySeatedNotAllowed = allTransfers + .stream() + .map(this::toStaySeatedNotAllowed) + .filter(Objects::nonNull) + .toList(); + + return new TransferMappingResult(constrainedTransfers, staySeatedNotAllowed); + } + + private StaySeatedNotAllowed toStaySeatedNotAllowed(Transfer t) { + Trip fromTrip = tripMapper.map(t.getFromTrip()); + Trip toTrip = tripMapper.map(t.getToTrip()); + if (t.getTransferType() == STAY_SEATED_NOT_ALLOWED) { + return new StaySeatedNotAllowed(fromTrip, toTrip); + } else return null; } ConstrainedTransfer map(org.onebusaway.gtfs.model.Transfer rhs) { Trip fromTrip = tripMapper.map(rhs.getFromTrip()); Trip toTrip = tripMapper.map(rhs.getToTrip()); - TransferConstraint constraint = mapConstraint(rhs, fromTrip, toTrip); + TransferConstraint constraint = mapConstraint(rhs); // If this transfer do not give any advantages in the routing, then drop it if (constraint.isRegularTransfer()) { @@ -184,17 +193,14 @@ private void setup(boolean run) { } } - private TransferConstraint mapConstraint(Transfer rhs, Trip fromTrip, Trip toTrip) { + private TransferConstraint mapConstraint(Transfer rhs) { var builder = TransferConstraint.create(); builder.guaranteed(rhs.getTransferType() == GUARANTEED); // A transfer is stay seated, if it is either explicitly mapped as such, or in the same block // and not explicitly disallowed. - builder.staySeated( - rhs.getTransferType() == STAY_SEATED || - (rhs.getTransferType() != STAY_SEATED_NOT_ALLOWED && sameBlockId(fromTrip, toTrip)) - ); + builder.staySeated(rhs.getTransferType() == STAY_SEATED); builder.priority(mapTypeToPriority(rhs.getTransferType())); diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/TransferMappingResult.java b/src/main/java/org/opentripplanner/gtfs/mapping/TransferMappingResult.java new file mode 100644 index 00000000000..0e3753fe9e3 --- /dev/null +++ b/src/main/java/org/opentripplanner/gtfs/mapping/TransferMappingResult.java @@ -0,0 +1,9 @@ +package org.opentripplanner.gtfs.mapping; + +import java.util.Collection; +import org.opentripplanner.model.transfer.ConstrainedTransfer; + +public record TransferMappingResult( + Collection constrainedTransfers, + Collection staySeatedNotAllowed +) {} diff --git a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java index f4fd744ba79..da90c63ec95 100644 --- a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java +++ b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.Set; import org.opentripplanner.ext.flex.trip.FlexTrip; +import org.opentripplanner.gtfs.mapping.StaySeatedNotAllowed; import org.opentripplanner.model.FareAttribute; import org.opentripplanner.model.FareRule; import org.opentripplanner.model.FeedInfo; @@ -105,6 +106,8 @@ public class OtpTransitServiceBuilder { private final List transfers = new ArrayList<>(); + private final List staySeatedNotAllowed = new ArrayList<>(); + private final EntityById tripsById = new EntityById<>(); private final Multimap tripPatterns = ArrayListMultimap.create(); @@ -223,6 +226,10 @@ public List getTransfers() { return transfers; } + public List getStaySeatedNotAllowed() { + return staySeatedNotAllowed; + } + public EntityById getTripsById() { return tripsById; } diff --git a/src/main/java/org/opentripplanner/model/plan/ItinerariesCalculateLegTotals.java b/src/main/java/org/opentripplanner/model/plan/ItinerariesCalculateLegTotals.java index 2c3b0441d17..72728a3fde5 100644 --- a/src/main/java/org/opentripplanner/model/plan/ItinerariesCalculateLegTotals.java +++ b/src/main/java/org/opentripplanner/model/plan/ItinerariesCalculateLegTotals.java @@ -41,7 +41,9 @@ private void calculate(List legs) { if (leg.isTransitLeg()) { transitTimeSeconds += dt; - ++nTransitLegs; + if (!leg.isInterlinedWithPreviousLeg()) { + ++nTransitLegs; + } } else if (leg.isOnStreetNonTransit()) { nonTransitTimeSeconds += dt; nonTransitDistanceMeters += leg.getDistanceMeters(); diff --git a/src/test/java/org/opentripplanner/ConstantsForTests.java b/src/test/java/org/opentripplanner/ConstantsForTests.java index 290f0c3f6f5..0025e70dc93 100644 --- a/src/test/java/org/opentripplanner/ConstantsForTests.java +++ b/src/test/java/org/opentripplanner/ConstantsForTests.java @@ -290,7 +290,8 @@ private static void addGtfsToGraph( List.of(bundle), ServiceDateInterval.unbounded(), fareServiceFactory, - false + false, + 300 ); module.buildGraph(graph, transitModel, new HashMap<>()); diff --git a/src/test/java/org/opentripplanner/GtfsTest.java b/src/test/java/org/opentripplanner/GtfsTest.java index 43b312b8662..ac914d5f94b 100644 --- a/src/test/java/org/opentripplanner/GtfsTest.java +++ b/src/test/java/org/opentripplanner/GtfsTest.java @@ -158,9 +158,6 @@ protected void setUp() { transitModel = new TransitModel(stopModel, deduplicator); gtfsGraphBuilderImpl.buildGraph(graph, transitModel, null); - // Set the agency ID to be used for tests to the first one in the feed. - String agencyId = transitModel.getAgencies().iterator().next().getId().getId(); - System.out.printf("Set the agency ID for this test to %s\n", agencyId); transitModel.index(); graph.index(); router = new Router(graph, transitModel, RouterConfig.DEFAULT, Metrics.globalRegistry); diff --git a/src/test/java/org/opentripplanner/graph_builder/module/geometry/GeometryAndBlockProcessorTest.java b/src/test/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessorTest.java similarity index 89% rename from src/test/java/org/opentripplanner/graph_builder/module/geometry/GeometryAndBlockProcessorTest.java rename to src/test/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessorTest.java index 8a10bda53c6..d1a5d05445b 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/geometry/GeometryAndBlockProcessorTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessorTest.java @@ -11,7 +11,7 @@ import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; -public class GeometryAndBlockProcessorTest { +public class GeometryProcessorTest { @Test public void testBikesAllowed() throws IOException { @@ -38,8 +38,8 @@ public void testBikesAllowed() throws IOException { .withIssueStoreAndDeduplicator(graph) .build(); - GeometryAndBlockProcessor factory = new GeometryAndBlockProcessor(context); + GeometryProcessor processor = new GeometryProcessor(context); - factory.run(graph, transitModel); + processor.run(transitModel); } } diff --git a/src/test/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessorTest.java b/src/test/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessorTest.java new file mode 100644 index 00000000000..cbd42c50db4 --- /dev/null +++ b/src/test/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessorTest.java @@ -0,0 +1,83 @@ +package org.opentripplanner.graph_builder.module.interlining; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.transit.model._data.TransitModelForTest.stopTime; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.graph_builder.DataImportIssueStore; +import org.opentripplanner.gtfs.mapping.StaySeatedNotAllowed; +import org.opentripplanner.model.StopPattern; +import org.opentripplanner.model.TripPattern; +import org.opentripplanner.model.plan.PlanTestConstants; +import org.opentripplanner.model.transfer.TransferService; +import org.opentripplanner.routing.trippattern.Deduplicator; +import org.opentripplanner.routing.trippattern.TripTimes; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +class InterlineProcessorTest implements PlanTestConstants { + + List patterns = List.of( + tripPattern("trip-1", "block-1"), + tripPattern("trip-2", "block-1"), + tripPattern("trip-2", "block-3") + ); + + @Test + void run() { + var transferService = new TransferService(); + var processor = new InterlineProcessor( + transferService, + List.of(), + 100, + DataImportIssueStore.noop() + ); + + var createdTransfers = processor.run(patterns); + assertEquals(1, createdTransfers.size()); + + assertEquals(transferService.listAll(), createdTransfers); + + createdTransfers.forEach(t -> assertTrue(t.getTransferConstraint().isStaySeated())); + } + + @Test + void staySeatedNotAllowed() { + var transferService = new TransferService(); + + var fromTrip = patterns.get(0).getTrip(0); + var toTrip = patterns.get(1).getTrip(0); + + var notAllowed = new StaySeatedNotAllowed(fromTrip, toTrip); + + var processor = new InterlineProcessor( + transferService, + List.of(notAllowed), + 100, + DataImportIssueStore.noop() + ); + + var createdTransfers = processor.run(patterns); + assertEquals(0, createdTransfers.size()); + + assertEquals(transferService.listAll(), createdTransfers); + } + + private static TripPattern tripPattern(String tripId, String blockId) { + var trip = TransitModelForTest + .trip(tripId) + .withGtfsBlockId(blockId) + .withServiceId(new FeedScopedId("1", "1")) + .build(); + + var stopTimes = List.of(stopTime(trip, 0), stopTime(trip, 1), stopTime(trip, 2)); + var stopPattern = new StopPattern(stopTimes); + + var tp = new TripPattern(TransitModelForTest.id(tripId), trip.getRoute(), stopPattern); + var tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + tp.add(tripTimes); + return tp; + } +} diff --git a/src/test/java/org/opentripplanner/gtfs/integration/InterliningTest.java b/src/test/java/org/opentripplanner/gtfs/integration/InterliningTest.java new file mode 100644 index 00000000000..3562a60bbb3 --- /dev/null +++ b/src/test/java/org/opentripplanner/gtfs/integration/InterliningTest.java @@ -0,0 +1,58 @@ +package org.opentripplanner.gtfs.integration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.ZonedDateTime; +import org.junit.jupiter.api.Test; +import org.opentripplanner.GtfsTest; +import org.opentripplanner.model.plan.Itinerary; + +public class InterliningTest extends GtfsTest { + + long time = ZonedDateTime.parse("2014-01-01T00:05:00-05:00[America/New_York]").toEpochSecond(); + + @Override + public String getFeedName() { + return "gtfs/interlining"; + } + + @Test + public void interlineOnSameRoute() { + // We should arrive at the destination using two legs, both of which are on + // the same route and with zero transfers. + Itinerary itinerary = plan(time, "stop0", "stop3", null, false, false, null, null, null, 2); + + assertEquals(itinerary.getLegs().get(0).getRoute().getId().getId(), "route1"); + + var secondLeg = itinerary.getLegs().get(1); + assertEquals(secondLeg.getRoute().getId().getId(), "route1"); + assertTrue(secondLeg.isInterlinedWithPreviousLeg()); + assertEquals(0, itinerary.getNumberOfTransfers()); + } + + @Test + public void interlineOnDifferentRoute() { + var itinerary = plan(time, "stop0", "stop6", null, false, false, null, null, null, 2); + + assertEquals(itinerary.getLegs().get(0).getRoute().getId().getId(), "route0"); + + var secondLeg = itinerary.getLegs().get(1); + assertEquals(secondLeg.getRoute().getId().getId(), "route3"); + assertTrue(secondLeg.isInterlinedWithPreviousLeg()); + assertEquals(0, itinerary.getNumberOfTransfers()); + } + + @Test + public void staySeatedNotAllowed() { + var itinerary = plan(time, "stop0", "stop5", null, false, false, null, null, null, 2); + + assertEquals(itinerary.getLegs().get(0).getRoute().getId().getId(), "route2"); + + var secondLeg = itinerary.getLegs().get(1); + assertEquals(secondLeg.getRoute().getId().getId(), "route2"); + assertFalse(secondLeg.isInterlinedWithPreviousLeg()); + assertEquals(1, itinerary.getNumberOfTransfers()); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/GraphPathTest.java b/src/test/java/org/opentripplanner/routing/algorithm/GraphPathTest.java index 8c739ade4d8..53a99a94456 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/GraphPathTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/GraphPathTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.opentripplanner.ConstantsForTests; -import org.opentripplanner.graph_builder.module.geometry.GeometryAndBlockProcessor; +import org.opentripplanner.graph_builder.module.geometry.GeometryProcessor; import org.opentripplanner.gtfs.GtfsContext; import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.routing.algorithm.astar.AStarBuilder; @@ -46,8 +46,8 @@ public void setUp() throws Exception { graph = new Graph(stopModel, deduplicator); transitModel = new TransitModel(stopModel, deduplicator); - GeometryAndBlockProcessor hl = new GeometryAndBlockProcessor(context); - hl.run(graph, transitModel); + GeometryProcessor hl = new GeometryProcessor(context); + hl.run(transitModel); transitModel.putService(CalendarServiceData.class, context.getCalendarServiceData()); } diff --git a/src/test/java/org/opentripplanner/routing/edgetype/PatternInterlineDwellTest.java b/src/test/java/org/opentripplanner/routing/edgetype/PatternInterlineDwellTest.java deleted file mode 100644 index 20bea5ba1ce..00000000000 --- a/src/test/java/org/opentripplanner/routing/edgetype/PatternInterlineDwellTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.opentripplanner.routing.edgetype; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.opentripplanner.GtfsTest; -import org.opentripplanner.model.plan.Itinerary; - -@Disabled -public class PatternInterlineDwellTest extends GtfsTest { - - @Override - public String getFeedName() { - return "gtfs/interlining"; - } - - // TODO Allow using Calendar or ISOdate for testing, interpret it in the given graph's timezone. - - @Test - public void testInterlining() { - LocalDateTime ldt = LocalDateTime.of(2014, 1, 1, 0, 5, 0); - ZonedDateTime zdt = ZonedDateTime.of(ldt, ZoneId.of("America/New_York")); - long time = zdt.toEpochSecond(); - // We should arrive at the destination using two legs, both of which are on - // the same route and with zero transfers. - Itinerary itinerary = plan(time, "stop0", "stop3", null, false, false, null, null, null, 2); - - assertEquals(itinerary.getLegs().get(0).getRoute().getId().getId(), "route1"); - assertEquals(itinerary.getLegs().get(1).getRoute().getId().getId(), "route1"); - assertEquals(0, itinerary.getNumberOfTransfers()); - } - // TODO test for trips on the same block with no transfer allowed (Trimet special case) - -} diff --git a/src/test/java/org/opentripplanner/routing/edgetype/loader/GeometryAndBlockProcessorTest.java b/src/test/java/org/opentripplanner/routing/edgetype/loader/GeometryProcessorTest.java similarity index 66% rename from src/test/java/org/opentripplanner/routing/edgetype/loader/GeometryAndBlockProcessorTest.java rename to src/test/java/org/opentripplanner/routing/edgetype/loader/GeometryProcessorTest.java index 744d28cf3f9..9a5a99704af 100644 --- a/src/test/java/org/opentripplanner/routing/edgetype/loader/GeometryAndBlockProcessorTest.java +++ b/src/test/java/org/opentripplanner/routing/edgetype/loader/GeometryProcessorTest.java @@ -7,13 +7,11 @@ import static org.opentripplanner.gtfs.GtfsContextBuilder.contextBuilder; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.HashMap; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -23,15 +21,13 @@ import org.opentripplanner.graph_builder.DataImportIssueStore; import org.opentripplanner.graph_builder.issues.NegativeHopTime; import org.opentripplanner.graph_builder.module.StreetLinkerModule; -import org.opentripplanner.graph_builder.module.geometry.GeometryAndBlockProcessor; +import org.opentripplanner.graph_builder.module.geometry.GeometryProcessor; import org.opentripplanner.gtfs.GtfsContext; import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.routing.algorithm.astar.AStarBuilder; import org.opentripplanner.routing.api.request.RoutingRequest; import org.opentripplanner.routing.api.request.WheelchairAccessibilityRequest; -import org.opentripplanner.routing.core.BicycleOptimizeType; import org.opentripplanner.routing.core.RoutingContext; -import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.core.TraverseModeSet; import org.opentripplanner.routing.edgetype.StreetEdge; @@ -52,7 +48,7 @@ * TODO OTP2 - Test is too close to the implementation and will need to be reimplemented. */ @Disabled -public class GeometryAndBlockProcessorTest { +public class GeometryProcessorTest { private Graph graph; private TransitModel transitModel; @@ -73,8 +69,8 @@ public void setUp() throws Exception { contextBuilder(ConstantsForTests.FAKE_GTFS).withIssueStoreAndDeduplicator(graph).build(); feedId = context.getFeedId().getId(); - GeometryAndBlockProcessor factory = new GeometryAndBlockProcessor(context); - factory.run(graph, transitModel, issueStore); + GeometryProcessor factory = new GeometryProcessor(context); + factory.run(transitModel); transitModel.putService(CalendarServiceData.class, context.getCalendarServiceData()); String[] stops = { @@ -153,79 +149,7 @@ public void testIssue() { } @Test - public void testRouting() throws Exception { - Vertex stop_a = graph.getVertex(feedId + ":A"); - Vertex stop_b = graph.getVertex(feedId + ":B"); - Vertex stop_c = graph.getVertex(feedId + ":C"); - Vertex stop_d = graph.getVertex(feedId + ":D"); - Vertex stop_e = graph.getVertex(feedId + ":E"); - - RoutingRequest options = new RoutingRequest(); - // test feed is designed for instantaneous transfers - options.transferSlack = 0; - - long startTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 0, 0, 0); - options.setDateTime(Instant.ofEpochSecond(startTime)); - - ShortestPathTree spt; - GraphPath path; - - // A to B - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_a, stop_b)) - .getShortestPathTree(); - - path = spt.getPath(stop_b); - assertNotNull(path); - assertEquals(6, path.states.size()); - - // A to C - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_a, stop_c)) - .getShortestPathTree(); - - path = spt.getPath(stop_c); - assertNotNull(path); - assertEquals(8, path.states.size()); - - // A to D (change at C) - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_a, stop_d)) - .getShortestPathTree(); - - path = spt.getPath(stop_d); - assertNotNull(path); - // there are two paths of different lengths - // both arrive at 40 minutes after midnight - List stops = extractStopVertices(path); - assertEquals(stops.size(), 3); - assertEquals(stops.get(1), stop_c); - long endTime = startTime + 40 * 60; - assertEquals(endTime, path.getEndTime()); - - //A to E (change at C) - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_a, stop_e)) - .getShortestPathTree(); - path = spt.getPath(stop_e); - assertNotNull(path); - stops = extractStopVertices(path); - assertEquals(stops.size(), 3); - assertEquals(stops.get(1), stop_c); - endTime = startTime + 70 * 60; - assertEquals(endTime, path.getEndTime()); - } - - @Test - public void testRoutingOverMidnight() throws Exception { + public void testRoutingOverMidnight() { // this route only runs on weekdays Vertex stop_g = graph.getVertex(feedId + ":G_depart"); Vertex stop_h = graph.getVertex(feedId + ":H_arrive"); @@ -264,7 +188,7 @@ public void testRoutingOverMidnight() throws Exception { } @Test - public void testPickupDropoff() throws Exception { + public void testPickupDropoff() { Vertex stop_o = graph.getVertex(feedId + ":O_depart"); Vertex stop_p = graph.getVertex(feedId + ":P"); assertEquals(2, stop_o.getOutgoing().size()); @@ -293,54 +217,7 @@ public void testPickupDropoff() throws Exception { } @Test - public void testTraverseMode() throws Exception { - Vertex stop_a = graph.getVertex(feedId + ":A_depart"); - Vertex stop_b = graph.getVertex(feedId + ":B_arrive"); - - ShortestPathTree spt; - - RoutingRequest options = new RoutingRequest(); - options.setStreetSubRequestModes( - new TraverseModeSet( - TraverseMode.TRAM, - TraverseMode.RAIL, - TraverseMode.SUBWAY, - TraverseMode.FUNICULAR, - TraverseMode.GONDOLA - ) - ); - options.setDateTime(TestUtils.dateInstant("America/New_York", 2009, 8, 0, 0, 0, 0)); - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_a, stop_b)) - .getShortestPathTree(); - - //a to b is bus only - assertNull(spt.getPath(stop_b)); - - options.setStreetSubRequestModes( - new TraverseModeSet( - TraverseMode.TRAM, - TraverseMode.RAIL, - TraverseMode.SUBWAY, - TraverseMode.FUNICULAR, - TraverseMode.GONDOLA, - TraverseMode.CABLE_CAR, - TraverseMode.BUS - ) - ); - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_a, stop_b)) - .getShortestPathTree(); - - assertNotNull(spt.getPath(stop_b)); - } - - @Test - public void testTimelessStops() throws Exception { + public void testTimelessStops() { Vertex stop_d = graph.getVertex(feedId + ":D"); Vertex stop_c = graph.getVertex(feedId + ":C"); RoutingRequest options = new RoutingRequest(); @@ -359,54 +236,7 @@ public void testTimelessStops() throws Exception { } @Test - public void testTripBikesAllowed() throws Exception { - Vertex stop_a = graph.getVertex(feedId + ":A"); - Vertex stop_b = graph.getVertex(feedId + ":B"); - Vertex stop_c = graph.getVertex(feedId + ":C"); - Vertex stop_d = graph.getVertex(feedId + ":D"); - - RoutingRequest options = new RoutingRequest(); - options.streetSubRequestModes.setWalk(false); - options.streetSubRequestModes.setBicycle(true); - options.streetSubRequestModes.setTransit(true); - options.setDateTime(TestUtils.dateInstant("America/New_York", 2009, 8, 18, 0, 0, 0)); - - ShortestPathTree spt; - GraphPath path; - - // route: bikes allowed, trip: no value - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_a, stop_b)) - .getShortestPathTree(); - - path = spt.getPath(stop_b); - assertNotNull(path); - - // route: bikes allowed, trip: bikes not allowed - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_d, stop_c)) - .getShortestPathTree(); - - path = spt.getPath(stop_c); - assertNull(path); - - // route: bikes not allowed, trip: bikes allowed - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_c, stop_d)) - .getShortestPathTree(); - - path = spt.getPath(stop_d); - assertNotNull(path); - } - - @Test - public void testWheelchairAccessible() throws Exception { + public void testWheelchairAccessible() { Vertex near_a = graph.getVertex("near_1_" + feedId + "_entrance_a"); Vertex near_b = graph.getVertex("near_1_" + feedId + "_entrance_b"); Vertex near_c = graph.getVertex("near_1_" + feedId + "_C"); @@ -555,44 +385,7 @@ public void testFrequencies() { } @Test - public void testFewestTransfers() { - Vertex stop_c = graph.getVertex(feedId + ":C"); - Vertex stop_d = graph.getVertex(feedId + ":D"); - RoutingRequest options = new RoutingRequest(); - options.bicycleOptimizeType = BicycleOptimizeType.QUICK; - options.setDateTime(TestUtils.dateInstant("America/New_York", 2009, 8, 1, 16, 0, 0)); - - ShortestPathTree spt = AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_c, stop_d)) - .getShortestPathTree(); - - //when optimizing for speed, take the fast two-bus path - GraphPath path = spt.getPath(stop_d); - assertNotNull(path); - assertEquals( - TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 16, 20, 0), - path.getEndTime() - ); - - //when optimizing for fewest transfers, take the slow one-bus path - options.transferCost = 1800; - spt = - AStarBuilder - .oneToOne() - .setContext(new RoutingContext(options, graph, stop_c, stop_d)) - .getShortestPathTree(); - - path = spt.getPath(stop_d); - assertNotNull(path); - assertEquals( - TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 16, 50, 0), - path.getEndTime() - ); - } - - @Test - public void testPathways() throws Exception { + public void testPathways() { Vertex entrance = graph.getVertex(feedId + ":entrance_a"); assertNotNull(entrance); Vertex stop = graph.getVertex(feedId + ":A"); @@ -612,14 +405,4 @@ public void testPathways() throws Exception { path.getEndTime() ); } - - private List extractStopVertices(GraphPath path) { - List ret = Lists.newArrayList(); - for (State state : path.states) { - if (state.getVertex() instanceof TransitStopVertex) { - ret.add(((TransitStopVertex) state.getVertex())); - } - } - return ret; - } } diff --git a/src/test/java/org/opentripplanner/routing/edgetype/loader/HopFactoryTest.java b/src/test/java/org/opentripplanner/routing/edgetype/loader/HopFactoryTest.java index 10bf90b4ae3..7f247e7b4b4 100644 --- a/src/test/java/org/opentripplanner/routing/edgetype/loader/HopFactoryTest.java +++ b/src/test/java/org/opentripplanner/routing/edgetype/loader/HopFactoryTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.opentripplanner.ConstantsForTests; -import org.opentripplanner.graph_builder.module.geometry.GeometryAndBlockProcessor; +import org.opentripplanner.graph_builder.module.geometry.GeometryProcessor; import org.opentripplanner.gtfs.GtfsContext; import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.routing.algorithm.astar.AStarBuilder; @@ -50,8 +50,8 @@ public void setUp() throws Exception { var stopModel = new StopModel(); graph = new Graph(stopModel, deduplicator); transitModel = new TransitModel(stopModel, deduplicator); - GeometryAndBlockProcessor factory = new GeometryAndBlockProcessor(context); - factory.run(graph, transitModel); + GeometryProcessor factory = new GeometryProcessor(context); + factory.run(transitModel); transitModel.putService(CalendarServiceData.class, context.getCalendarServiceData()); feedId = context.getFeedId().getId(); diff --git a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java index 17029b57cce..8b7d17f7ade 100644 --- a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java +++ b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java @@ -2,6 +2,7 @@ import static org.opentripplanner.transit.model.basic.WheelchairAccessibility.NO_INFORMATION; +import org.opentripplanner.model.StopTime; import org.opentripplanner.transit.model.basic.WgsCoordinate; import org.opentripplanner.transit.model.basic.WheelchairAccessibility; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -119,4 +120,22 @@ public static StationBuilder station(String idAndName) { .withDescription(new NonLocalizedString("Station " + idAndName)) .withPriority(StopTransferPriority.ALLOWED); } + + public static StopTime stopTime(Trip trip, int seq) { + var stopTime = new StopTime(); + stopTime.setTrip(trip); + stopTime.setStopSequence(seq); + + var stop = TransitModelForTest.stopForTest("stop-" + seq, 0, 0); + stopTime.setStop(stop); + + return stopTime; + } + + public static StopTime stopTime(Trip trip, int seq, int time) { + var stopTime = TransitModelForTest.stopTime(trip, seq); + stopTime.setArrivalTime(time); + stopTime.setDepartureTime(time); + return stopTime; + } } diff --git a/src/test/java/org/opentripplanner/updater/vehicle_positions/VehiclePositionsMatcherTest.java b/src/test/java/org/opentripplanner/updater/vehicle_positions/VehiclePositionsMatcherTest.java index 56b53efdf1f..b302c7a9953 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_positions/VehiclePositionsMatcherTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_positions/VehiclePositionsMatcherTest.java @@ -1,6 +1,7 @@ package org.opentripplanner.updater.vehicle_positions; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.transit.model._data.TransitModelForTest.stopTime; import com.google.transit.realtime.GtfsRealtime.TripDescriptor; import com.google.transit.realtime.GtfsRealtime.VehiclePosition; @@ -202,22 +203,4 @@ private static VehiclePosition vehiclePosition(String tripId1) { .setStopId("stop-1") .build(); } - - private static StopTime stopTime(Trip trip, int seq, int time) { - var stopTime = stopTime(trip, seq); - stopTime.setArrivalTime(time); - stopTime.setDepartureTime(time); - return stopTime; - } - - private static StopTime stopTime(Trip trip, int seq) { - var stopTime = new StopTime(); - stopTime.setTrip(trip); - stopTime.setStopSequence(seq); - - var stop = TransitModelForTest.stopForTest("stop-" + seq, 0, 0); - stopTime.setStop(stop); - - return stopTime; - } } diff --git a/src/test/resources/gtfs/interlining/routes.txt b/src/test/resources/gtfs/interlining/routes.txt index 93a4252e25c..9547998d29e 100644 --- a/src/test/resources/gtfs/interlining/routes.txt +++ b/src/test/resources/gtfs/interlining/routes.txt @@ -1,3 +1,5 @@ agency_id,route_id,route_short_name,route_long_name,route_type TEST,route0,,NoInterlining,2 TEST,route1,,HasInterlining,2 +TEST,route2,,HasStaySeatedNotAllowed,2 +TEST,route3,,HasInterlining,2 diff --git a/src/test/resources/gtfs/interlining/stop_times.txt b/src/test/resources/gtfs/interlining/stop_times.txt index 6b6532788f7..51048386211 100644 --- a/src/test/resources/gtfs/interlining/stop_times.txt +++ b/src/test/resources/gtfs/interlining/stop_times.txt @@ -7,3 +7,9 @@ route1-trip1,00:10:00,00:10:00,stop0,1 route1-trip1,00:20:00,00:20:00,stop2,2 route1-trip2,00:30:00,00:30:00,stop2,1 route1-trip2,00:40:00,00:40:00,stop3,2 +route2-trip1,00:30:00,00:30:00,stop0,1 +route2-trip1,00:40:00,00:40:00,stop4,2 +route2-trip2,00:45:00,00:45:00,stop4,1 +route2-trip2,00:55:00,00:55:00,stop5,2 +route3-trip1,00:25:00,00:25:00,stop1,1 +route3-trip1,00:35:00,00:35:00,stop6,2 diff --git a/src/test/resources/gtfs/interlining/stops.txt b/src/test/resources/gtfs/interlining/stops.txt index 7e4d4ef6416..572db2057f2 100644 --- a/src/test/resources/gtfs/interlining/stops.txt +++ b/src/test/resources/gtfs/interlining/stops.txt @@ -3,3 +3,6 @@ stop0,Stop Zero,3.601,3.602 stop1,Stop One,3.602,3.601 stop2,Stop Two,3.603,3.602 stop3,Stop Three,3.602,3.603 +stop4,Stop Four,3.604,3.603 +stop5,Stop Five,3.605,3.605 +stop6,Stop Six,3.605,3.605 diff --git a/src/test/resources/gtfs/interlining/transfers.txt b/src/test/resources/gtfs/interlining/transfers.txt new file mode 100644 index 00000000000..cf752033db9 --- /dev/null +++ b/src/test/resources/gtfs/interlining/transfers.txt @@ -0,0 +1,2 @@ +from_stop_id,to_stop_id,transfer_type,from_trip_id,to_trip_id +stop4,stop4,5,route2-trip1,route2-trip2 diff --git a/src/test/resources/gtfs/interlining/trips.txt b/src/test/resources/gtfs/interlining/trips.txt index d7a91c0043f..411a9a18f12 100644 --- a/src/test/resources/gtfs/interlining/trips.txt +++ b/src/test/resources/gtfs/interlining/trips.txt @@ -3,4 +3,6 @@ route0,serv0,route0-trip1,block0 route0,serv0,route0-trip2,block1 route1,serv0,route1-trip1,block2 route1,serv0,route1-trip2,block2 - +route2,serv0,route2-trip1,block3 +route2,serv0,route2-trip2,block3 +route3,serv0,route3-trip1,block0