org.opentripplanner.graph_builder.module.TripPatternNamer Maven / Gradle / Ivy
Show all versions of otp Show documentation
package org.opentripplanner.graph_builder.module;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import org.opentripplanner.graph_builder.model.GraphBuilderModule;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.transit.service.TransitModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TripPatternNamer implements GraphBuilderModule {
private static final Logger LOG = LoggerFactory.getLogger(TripPatternNamer.class);
private final TransitModel transitModel;
@Inject
public TripPatternNamer(TransitModel transitModel) {
this.transitModel = transitModel;
}
@Override
public void buildGraph() {
/* Generate unique human-readable names for all the TableTripPatterns. */
generateUniqueNames(transitModel.getAllTripPatterns());
}
@Override
public void checkInputs() {}
/**
* Static method that creates unique human-readable names for a collection of TableTripPatterns.
* Perhaps this should be in TripPattern, and apply to Frequency patterns as well. TODO: resolve
* this question: can a frequency and table pattern have the same stoppattern? If so should they
* have the same "unique" name?
*
* The names should be dataset unique, not just route-unique?
*
* A TripPattern groups all trips visiting a particular pattern of stops on a particular route.
* GFTS Route names are intended for very general customer information, but sometimes there is a
* need to know where a particular trip actually goes. For example, the New York City N train has
* at least four different variants: express (over the Manhattan bridge) and local (via lower
* Manhattan and the tunnel), in two directions (to Astoria or to Coney Island). During
* construction, a fifth variant sometimes appears: trains use the D line to Coney Island after
* 59th St (or from Coney Island to 59th in the opposite direction).
*
* TripPattern names are machine-generated on a best-effort basis. They are guaranteed to be
* unique (among TripPatterns for a single Route) but not stable across graph builds, especially
* when different versions of GTFS inputs are used. For instance, if a variant is the only variant
* of the N that ends at Coney Island, the name will be "N to Coney Island". But if multiple
* variants end at Coney Island (but have different stops elsewhere), that name would not be
* chosen. OTP also tries start and intermediate stations ("from Coney Island", or "via
* Whitehall", or even combinations ("from Coney Island via Whitehall"). But if there is no way to
* create a unique name from start/end/intermediate stops, then the best we can do is to create a
* "like [trip id]" name, which at least tells you where in the GTFS you can find a related trip.
*/
// TODO: pass in a transit index that contains a Multimap and derive all TableTripPatterns
// TODO: combine from/to and via in a single name. this could be accomplished by grouping the trips by destination,
// then disambiguating in groups of size greater than 1.
/*
* Another possible approach: for each route, determine the necessity of each field (which
* combination will create unique names). from, to, via, express. Then concatenate all necessary
* fields. Express should really be determined from number of stops and/or run time of trips.
*/
public static void generateUniqueNames(Collection tableTripPatterns) {
LOG.info("Generating unique names for stop patterns on each route.");
/* Group TripPatterns by Route */
Multimap patternsByRoute = ArrayListMultimap.create();
for (TripPattern ttp : tableTripPatterns) {
patternsByRoute.put(ttp.getRoute(), ttp);
}
/* Iterate over all routes, giving the patterns within each route unique names. */
for (Route route : patternsByRoute.keySet()) {
Collection routeTripPatterns = patternsByRoute.get(route);
// Only generate name for patterns with at least one missing name
if (routeTripPatterns.stream().allMatch(tripPattern -> tripPattern.getName() != null)) {
continue;
}
String routeName = route.getName();
/* Simplest case: there's only one route variant, so we'll just give it the route's name. */
if (routeTripPatterns.size() == 1) {
routeTripPatterns.iterator().next().initName(routeName);
continue;
}
/* Do the patterns within this Route have a unique start, end, or via Stop? */
Multimap signs = ArrayListMultimap.create(); // prefer headsigns
Multimap starts = ArrayListMultimap.create();
Multimap ends = ArrayListMultimap.create();
Multimap vias = ArrayListMultimap.create();
for (TripPattern pattern : routeTripPatterns) {
StopLocation start = pattern.firstStop();
StopLocation end = pattern.lastStop();
String headsign = pattern.getTripHeadsign() != null
? pattern.getTripHeadsign().toString()
: null;
if (headsign != null) {
signs.put(headsign, pattern);
}
starts.put(start, pattern);
ends.put(end, pattern);
for (StopLocation stop : pattern.getStops()) {
vias.put(stop, pattern);
}
}
PATTERN:for (TripPattern pattern : routeTripPatterns) {
if (pattern.getName() != null) {
continue;
}
StringBuilder sb = new StringBuilder(routeName);
String headsign = pattern.getTripHeadsign() != null
? pattern.getTripHeadsign().toString()
: null;
if (headsign != null && signs.get(headsign).size() == 1) {
pattern.initName(sb.append(" ").append(headsign).toString());
continue;
}
/* First try to name with destination. */
var end = pattern.lastStop();
sb.append(" to ").append(stopNameAndId(end));
if (ends.get(end).size() == 1) {
pattern.initName(sb.toString());
continue; // only pattern with this last stop
}
/* Then try to name with origin. */
var start = pattern.firstStop();
sb.append(" from ").append(stopNameAndId(start));
if (starts.get(start).size() == 1) {
pattern.initName((sb.toString()));
continue; // only pattern with this first stop
}
/* Check whether (end, start) is unique. */
Collection tripPatterns = starts.get(start);
Set remainingPatterns = new HashSet<>(tripPatterns);
remainingPatterns.retainAll(ends.get(end)); // set intersection
if (remainingPatterns.size() == 1) {
pattern.initName((sb.toString()));
continue;
}
/* Still not unique; try (end, start, via) for each via. */
for (var via : pattern.getStops()) {
if (via.equals(start) || via.equals(end)) continue;
Set intersection = new HashSet<>(remainingPatterns);
intersection.retainAll(vias.get(via));
if (intersection.size() == 1) {
sb.append(" via ").append(stopNameAndId(via));
pattern.initName((sb.toString()));
continue PATTERN;
}
}
/* Still not unique; check for express. */
if (remainingPatterns.size() == 2) {
// There are exactly two patterns sharing this start/end.
// The current one must be a subset of the other, because it has no unique via.
// Therefore we call it the express.
sb.append(" express");
} else {
// The final fallback: reference a specific trip ID.
Optional
.ofNullable(pattern.getScheduledTimetable().getRepresentativeTripTimes())
.map(TripTimes::getTrip)
.ifPresent(value -> sb.append(" like trip ").append(value.getId()));
}
pattern.initName((sb.toString()));
} // END foreach PATTERN
} // END foreach ROUTE
if (LOG.isDebugEnabled()) {
LOG.debug("Done generating unique names for stop patterns on each route.");
for (Route route : patternsByRoute.keySet()) {
Collection routeTripPatterns = patternsByRoute.get(route);
LOG.debug("Named {} patterns in route {}", routeTripPatterns.size(), route.getName());
for (TripPattern pattern : routeTripPatterns) {
LOG.debug(" {} ({} stops)", pattern.getName(), pattern.numberOfStops());
}
}
}
}
private static String stopNameAndId(StopLocation stop) {
return stop.getName() + " (" + stop.getId().toString() + ")";
}
}