/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.graph_builder.impl.transit_index; import static org.opentripplanner.common.IterableLibrary.filter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import org.onebusaway.gtfs.model.Agency; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Route; import org.onebusaway.gtfs.model.ServiceCalendar; import org.onebusaway.gtfs.model.ServiceCalendarDate; import org.onebusaway.gtfs.model.Stop; import org.onebusaway.gtfs.model.StopTime; import org.onebusaway.gtfs.model.Trip; import org.onebusaway.gtfs.services.GtfsRelationalDao; import org.opentripplanner.common.IterableLibrary; import org.opentripplanner.common.geometry.DistanceLibrary; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.graph_builder.services.GraphBuilderWithGtfsDao; import org.opentripplanner.gtfs.GtfsLibrary; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.edgetype.PatternDwell; import org.opentripplanner.routing.edgetype.PatternHop; import org.opentripplanner.routing.edgetype.PatternInterlineDwell; import org.opentripplanner.routing.edgetype.PreAlightEdge; import org.opentripplanner.routing.edgetype.PreBoardEdge; import org.opentripplanner.routing.edgetype.TableTripPattern; import org.opentripplanner.routing.edgetype.TransitBoardAlight; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.services.TransitIndexService; import org.opentripplanner.routing.transit_index.RouteSegment; import org.opentripplanner.routing.transit_index.RouteVariant; import org.opentripplanner.routing.transit_index.TransitIndexServiceImpl; import org.opentripplanner.routing.vertextype.TransitStop; import org.opentripplanner.routing.vertextype.TransitStopDepart; import org.opentripplanner.routing.vertextype.TransitVertex; import org.opentripplanner.util.MapUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vividsolutions.jts.geom.Coordinate; /** * Process GTFS to build transit index for use in patching * * @author novalis * */ public class TransitIndexBuilder implements GraphBuilderWithGtfsDao { private static final Logger LOG = LoggerFactory.getLogger(TransitIndexBuilder.class); private GtfsRelationalDao dao; private HashMap<AgencyAndId, RouteVariant> variantsByTrip = new HashMap<AgencyAndId, RouteVariant>(); private HashMap<AgencyAndId, List<RouteVariant>> variantsByRoute = new HashMap<AgencyAndId, List<RouteVariant>>(); private HashMap<String, List<RouteVariant>> variantsByAgency = new HashMap<String, List<RouteVariant>>(); private HashMap<AgencyAndId, PreAlightEdge> preAlightEdges = new HashMap<AgencyAndId, PreAlightEdge>(); private HashMap<AgencyAndId, TableTripPattern> tableTripPatternsByTrip = new HashMap<AgencyAndId, TableTripPattern>(); private HashMap<AgencyAndId, PreBoardEdge> preBoardEdges = new HashMap<AgencyAndId, PreBoardEdge>(); private HashMap<AgencyAndId, HashSet<String>> directionsByRoute = new HashMap<AgencyAndId, HashSet<String>>(); private HashMap<AgencyAndId, HashSet<Stop>> stopsByRoute = new HashMap<AgencyAndId, HashSet<Stop>>(); List<TraverseMode> modes = new ArrayList<TraverseMode>(); private HashSet<Edge> handledEdges = new HashSet<Edge>(); private DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance(); @Override public void setDao(GtfsRelationalDao dao) { this.dao = dao; } @Override public void buildGraph(Graph graph) { LOG.debug("Building transit index"); createRouteVariants(graph); indexTableTripPatternByTrip(graph); nameVariants(variantsByRoute); int totalVariants = 0; int totalTrips = 0; for (List<RouteVariant> variants : variantsByRoute.values()) { totalVariants += variants.size(); for (RouteVariant variant : variants) { variant.cleanup(); totalTrips += variant.getTrips().size(); } } LOG.debug("Built transit index: " + variantsByAgency.size() + " agencies, " + variantsByRoute.size() + " routes, " + totalTrips + " trips, " + totalVariants + " variants "); TransitIndexServiceImpl service = (TransitIndexServiceImpl) graph .getService(TransitIndexService.class); if (service == null) { service = new TransitIndexServiceImpl(variantsByAgency, variantsByRoute, variantsByTrip, preBoardEdges, preAlightEdges, tableTripPatternsByTrip, directionsByRoute, stopsByRoute, modes); } else { service.merge(variantsByAgency, variantsByRoute, variantsByTrip, preBoardEdges, preAlightEdges, tableTripPatternsByTrip, directionsByRoute, stopsByRoute, modes); } insertCalendarData(service); addAgencies(service); Coordinate coord = findTransitCenter(); service.setCenter(coord); service.setOvernightBreak(findOvernightBreak()); graph.putService(TransitIndexService.class, service); } /** * Find the longest consecutive sequence of minutes with no transit stops; this is assumed to be the overnight service break. * * @return */ private int findOvernightBreak() { final int minutesInDay = 24 * 60; boolean[] minutes = new boolean[minutesInDay]; for (StopTime stopTime : dao.getAllStopTimes()) { int time; if (stopTime.isDepartureTimeSet()) { time = stopTime.getDepartureTime(); } else if (stopTime.isArrivalTimeSet()) { time = stopTime.getArrivalTime(); } else { continue; } minutes[(time / 60) % minutesInDay] = true; } int bestLength = 0; int best = -1; int run = 0; for (int i = 0; i < minutesInDay; ++i) { if (minutes[i]) { // end of run if (run > bestLength) { bestLength = run; best = i - run; } run = 0; } else { run += 1; } } if (run > bestLength) { bestLength = run; best = 1440 - run; } if (best < 0) { return -1; } return best * 60 + 1; } /** * Used in k-means computation for transit centers */ class Center implements Comparable<Center> { public Coordinate coord; public int weight = 0; public Center(Coordinate coord) { this.coord = coord; } @Override public int compareTo(Center arg0) { return (int) Math.signum(weight - arg0.weight); } public String toString() { return "Center(" + coord + ", " + weight + ")"; } } /** * Find the "transit center" of the graph using a weighted k-means technique */ private Coordinate findTransitCenter() { LOG.debug("Finding transit center via k-means"); final int N = 30;// number of clusters final int ITERATIONS = 50; Map<Stop, Integer> stopWeight = new HashMap<Stop, Integer>(); // compute weight of all stop locations, which is the number of trips that stop at them. for (StopTime stopTime : dao.getAllStopTimes()) { Stop stop = stopTime.getStop(); String parent = stop.getParentStation(); if (parent != null) { stop = dao.getStopForId(new AgencyAndId(stop.getId().getAgencyId(), parent)); } Integer weight = stopWeight.get(stop); if (weight == null) { stopWeight.put(stop, 1); } else { stopWeight.put(stop, weight + 1); } } Map<Coordinate, Integer> pointWeight = new HashMap<Coordinate, Integer>(); for (Map.Entry<Stop, Integer> entry : stopWeight.entrySet()) { Stop stop = entry.getKey(); int weight = entry.getValue(); Coordinate c = new Coordinate(stop.getLon(), stop.getLat()); Integer oldWeight = pointWeight.get(c); if (oldWeight == null) { pointWeight.put(c, weight); } else { pointWeight.put(c, oldWeight + weight); } } List<Stop> stops = new ArrayList<Stop>(dao.getAllStops()); // choose N stations that are far away from each other and declare them to be the initial // centers Center[] centers = new Center[N]; Stop stop = stops.get(0); Coordinate coord = new Coordinate(stop.getLon(), stop.getLat()); centers[0] = new Center(coord); for (int i = 1; i < N; ++i) { Coordinate best = coord; double bestDistance = 0; for (int j = 0; j < stops.size(); ++j) { stop = stops.get(j); coord = new Coordinate(stop.getLon(), stop.getLat()); double total = 0; for (int k = 0; k < i; ++k) { double distance = distanceLibrary.distance(coord, centers[k].coord); total += distance * distance; } if (total > bestDistance) { bestDistance = total; best = coord; } } centers[i] = new Center(best); } int[] coord_count = new int[N]; // iterate ITERATIONS times and declare it good enough for (int i = 0; i < ITERATIONS; ++i) { double[] coord_sum_x = new double[centers.length]; double[] coord_sum_y = new double[N]; Arrays.fill(coord_count, 0); for (int c = 0; c < centers.length; ++c) { coord_count[c] = 0; } for (Map.Entry<Coordinate, Integer> entry : pointWeight.entrySet()) { coord = entry.getKey(); int weight = entry.getValue(); int best_center = -1; double best_distance = Double.MAX_VALUE; for (int c = 0; c < centers.length; ++c) { Coordinate center = centers[c].coord; double distance = distanceLibrary.distance(coord, center); if (distance < best_distance) { best_center = c; best_distance = distance; } } coord_sum_x[best_center] += coord.x * weight; coord_sum_y[best_center] += coord.y * weight; coord_count[best_center] += weight; } for (int c = 0; c < centers.length; ++c) { if (coord_count[c] == 0) { // this center has no points near it. centers[c].weight = 0; continue; } centers[c].coord = new Coordinate(coord_sum_x[c] / coord_count[c], coord_sum_y[c] / coord_count[c]); centers[c].weight = coord_count[c]; } } LOG.debug("found transit center"); // the highest-weighted cluster return Collections.max(Arrays.asList(centers)).coord; } private void addAgencies(TransitIndexServiceImpl service) { for (Agency agency : dao.getAllAgencies()) { service.addAgency(agency); } } private void createRouteVariants(Graph graph) { TransitBoardAlight tba; for (TransitVertex gv : IterableLibrary.filter(graph.getVertices(), TransitVertex.class)) { boolean start = false; boolean noStart = false; TableTripPattern pattern = null; Trip trip = null; for (Edge e : gv.getIncoming()) { if (handledEdges.contains(e)) { continue; } handledEdges.add(e); if (!(e instanceof Edge)) { continue; } if (e instanceof PatternHop || e instanceof PatternDwell) { noStart = true; } if (e instanceof TransitBoardAlight) { tba = (TransitBoardAlight) e; if (tba.isBoarding()) { pattern = tba.getPattern(); trip = pattern.getExemplar(); start = true; } } if (e instanceof PreBoardEdge) { TransitStop stop = (TransitStop) e.getFromVertex(); preBoardEdges.put(stop.getStopId(), (PreBoardEdge) e); start = false; } if (e instanceof PreAlightEdge) { TransitStop stop = (TransitStop) ((PreAlightEdge) e).getToVertex(); preAlightEdges.put(stop.getStopId(), (PreAlightEdge) e); start = false; } } if (start && !noStart) { RouteVariant variant = variantsByTrip.get(trip.getId()); if (variant == null) { variant = addTripToVariant(trip); if (pattern != null) { for (Trip trip2 : pattern.getTrips()) { addModeFromTrip(trip2); variantsByTrip.put(trip2.getId(), variant); } variant.addTrip(trip, pattern.getTrips().size()); } else { variant.addTrip(trip, 1); } } else { continue; } boolean setExemplar = !variant.isExemplarSet(); Edge prevHop = null; while (gv != null) { RouteSegment segment = new RouteSegment(gv.getStopId()); segment.hopIn = prevHop; for (Edge e : gv.getIncoming()) { if (e instanceof TransitBoardAlight && ((TransitBoardAlight) e).isBoarding()) { segment.board = e; } } Collection<Edge> outgoing = gv.getOutgoing(); gv = null; for (Edge e : outgoing) { if (e instanceof PatternHop) { segment.hopOut = e; gv = (TransitVertex) e.getToVertex(); } if (e instanceof PatternDwell) { segment.dwell = e; for (Edge e2 : e.getToVertex().getIncoming()) { if (e2 instanceof TransitBoardAlight && ((TransitBoardAlight) e2).isBoarding()) { segment.board = e2; } } for (Edge e2 : e.getToVertex().getOutgoing()) { if (e2 instanceof PatternHop) { segment.hopOut = e2; gv = (TransitVertex) e2.getToVertex(); } if (e2 instanceof TransitBoardAlight && !((TransitBoardAlight) e2).isBoarding()) { segment.alight = e2; } } } if (e instanceof PatternInterlineDwell) { variant.addInterline((PatternInterlineDwell) e); } if (e instanceof TransitBoardAlight && !((TransitBoardAlight) e).isBoarding()) { segment.alight = e; } } prevHop = segment.hopOut; if (setExemplar) { variant.addExemplarSegment(segment); } variant.addSegment(segment); } } } } private void indexTableTripPatternByTrip(Graph graph) { for (TransitStopDepart tsd : filter(graph.getVertices(), TransitStopDepart.class)) { for (TransitBoardAlight tba : filter(tsd.getOutgoing(), TransitBoardAlight.class)) { if (!tba.isBoarding()) continue; TableTripPattern pattern = tba.getPattern(); for (Trip trip : pattern.getTrips()) { tableTripPatternsByTrip.put(trip.getId(), pattern); } } } } private void insertCalendarData(TransitIndexService service) { Collection<ServiceCalendar> allCalendars = dao.getAllCalendars(); service.addCalendars(allCalendars); Collection<ServiceCalendarDate> allDates = dao.getAllCalendarDates(); service.addCalendarDates(allDates); } private void addModeFromTrip(Trip trip) { TraverseMode mode = GtfsLibrary.getTraverseMode(trip.getRoute()); if (!modes.contains(mode)) { modes.add(mode); } } private void nameVariants(HashMap<AgencyAndId, List<RouteVariant>> variantsByRoute) { for (List<RouteVariant> variants : variantsByRoute.values()) { Route route = variants.get(0).getRoute(); String routeName = GtfsLibrary.getRouteName(route); /* * simplest case: there's only one route variant, so we'll just give it the route's name */ if (variants.size() == 1) { variants.get(0).setName(routeName); continue; } /* next, do routes have a unique start, end, or via? */ HashMap<String, List<RouteVariant>> starts = new HashMap<String, List<RouteVariant>>(); HashMap<String, List<RouteVariant>> ends = new HashMap<String, List<RouteVariant>>(); HashMap<String, List<RouteVariant>> vias = new HashMap<String, List<RouteVariant>>(); for (RouteVariant variant : variants) { List<Stop> stops = variant.getStops(); MapUtils.addToMapList(starts, getName(stops.get(0)), variant); MapUtils.addToMapList(ends, getName(stops.get(stops.size() - 1)), variant); for (Stop stop : stops) { MapUtils.addToMapList(vias, getName(stop), variant); } } // do simple naming for unique start/end/via for (RouteVariant variant : variants) { List<Stop> stops = variant.getStops(); String firstStop = getName(stops.get(0)); if (starts.get(firstStop).size() == 1) { // this is the only route with this start String name = routeName + " from " + firstStop; variant.setName(name); } else { String lastStop = getName(stops.get(stops.size() - 1)); if (ends.get(lastStop).size() == 1) { String name = routeName + " to " + lastStop; variant.setName(name); } else { for (Stop stop : stops) { String viaStop = getName(stop); if (vias.get(viaStop).size() == 1) { String name = routeName + " via " + viaStop; variant.setName(name); break; } } } } } /** * now we have the case where no route has a unique start, stop, or via. This can happen if you have a single route which serves trips on * an H-shaped alignment, where trips can start at A or B and end at either C or D, visiting the same sets of stops along the shared * segments. * * <pre> * A B * | | * |------| * | | * | | * C D * </pre> * * First, we try unique start + end, then start + via + end, and if that doesn't work, we check for expresses, and finally we use a random * trip's id. * * It can happen if there is an express and a local version of a given line where the local starts and ends at the same place as the * express but makes a strict superset of stops; the local version will get a "via", but the express will be doomed. * * We can first check for the local/express situation by saying that if there are a subset of routes with the same start/end, and there is * exactly one that can't be named with start/end/via, call it "express". * * Consider the following three trips (A, B, C) along a route with four stops. A is the local, and gets "via stop 3"; B is a limited, and * C is (logically) an express: * * A,B,C -- A,B -- A -- A, B, C * * Here, neither B nor C is nameable. If either were removed, the other would be called "express". * * * */ for (RouteVariant variant : variants) { if (variant.getName() != null) continue; List<Stop> stops = variant.getStops(); String firstStop = getName(stops.get(0)); HashSet<RouteVariant> remainingVariants = new HashSet<RouteVariant>( starts.get(firstStop)); String lastStop = getName(stops.get(stops.size() - 1)); // take the intersection remainingVariants.retainAll(ends.get(lastStop)); if (remainingVariants.size() == 1) { String name = routeName + " from " + firstStop + " to " + lastStop; variant.setName(name); continue; } // this did not yield a unique name; try start / via / end for // each via for (Stop stop : stops) { if (getName(stop).equals(firstStop) || getName(stop).equals(lastStop)) { continue; } List<RouteVariant> via = vias.get(getName(stop)); boolean found = false; boolean bad = false; for (RouteVariant viaVariant : via) { if (remainingVariants.contains(viaVariant)) { if (found) { bad = true; break; } else { found = true; } } } if (found && !bad) { String name = routeName + " from " + firstStop + " to " + lastStop + " via " + getName(stop); variant.setName(name); break; } } if (variant.getName() == null) { // check for express if (remainingVariants.size() == 2) { // there are exactly two remaining variants sharing this start/end // we know that this one must be a subset of the other, because it // has no unique via. So, it is the express String name = routeName + " from " + firstStop + " to " + lastStop + " express"; variant.setName(name); } else { // the final fallback variant.setName(routeName + " like " + variant.getTrips().get(0).getId()); } } } } } private String getName(Stop stop) { return stop.getName() + " (" + stop.getId() + ")"; } private RouteVariant addTripToVariant(Trip trip) { // have we seen this trip before? RouteVariant variant = variantsByTrip.get(trip.getId()); if (variant != null) { return variant; } AgencyAndId routeId = trip.getRoute().getId(); String directionId = trip.getDirectionId(); HashSet<String> directions = directionsByRoute.get(routeId); if (directions == null) { directions = new HashSet<String>(); directionsByRoute.put(routeId, directions); } directions.add(directionId); // build the list of stops for this trip List<StopTime> stopTimes = dao.getStopTimesForTrip(trip); ArrayList<Stop> stops = new ArrayList<Stop>(); for (StopTime stopTime : stopTimes) { //nonduplicate stoptimes if (stops.size() == 0 || !stopTime.getStop().equals(stops.get(stops.size() - 1))) stops.add(stopTime.getStop()); } // build the list of stops for this route HashSet<Stop> stopsForRoute = stopsByRoute.get(routeId); if (stopsForRoute == null) { stopsForRoute = new HashSet<Stop>(); stopsByRoute.put(routeId, stopsForRoute); } for (StopTime stopTime : stopTimes) { stopsByRoute.get(routeId).add(stopTime.getStop()); } Route route = trip.getRoute(); // see if we have a variant for this route like this already List<RouteVariant> agencyVariants = variantsByAgency.get(route.getId().getAgencyId()); if (agencyVariants == null) { agencyVariants = new ArrayList<RouteVariant>(); variantsByAgency.put(route.getId().getAgencyId(), agencyVariants); } List<RouteVariant> variants = variantsByRoute.get(route.getId()); if (variants == null) { variants = new ArrayList<RouteVariant>(); variantsByRoute.put(route.getId(), variants); } for (RouteVariant existingVariant : variants) { if (existingVariant.getStops().equals(stops)) { variant = existingVariant; break; } } if (variant == null) { // create a variant for these stops on this route variant = new RouteVariant(route, stops); variants.add(variant); agencyVariants.add(variant); } variantsByTrip.put(trip.getId(), variant); return variant; } @Override public List<String> provides() { return Arrays.asList("transitIndex"); } @Override public List<String> getPrerequisites() { return Collections.emptyList(); } }