All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.profile.RepeatedRaptorProfileRouter Maven / Gradle / Ivy

package org.opentripplanner.profile;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterables;
import gnu.trove.map.TIntIntMap;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.TObjectLongMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import gnu.trove.map.hash.TObjectLongHashMap;
import org.joda.time.DateTimeZone;
import org.opentripplanner.analyst.SampleSet;
import org.opentripplanner.analyst.TimeSurface;
import org.opentripplanner.analyst.cluster.ResultEnvelope;
import org.opentripplanner.analyst.cluster.TaskStatistics;
import org.opentripplanner.analyst.scenario.AddTripPattern;
import org.opentripplanner.api.parameter.QualifiedModeSet;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.routing.algorithm.AStar;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.GraphIndex;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.spt.DominanceFunction;
import org.opentripplanner.routing.spt.ShortestPathTree;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.DayOfWeek;
import java.util.Arrays;

/**
 * Perform one-to-many profile routing using repeated RAPTOR searches. In this context, profile routing means finding
 * the optimal itinerary for each departure moment in a given window, without trying to reconstruct the exact paths.
 *
 * In other contexts (Modeify-style point to point routing) we would want to include suboptimal but resonable paths
 * and retain enough information to reconstruct all those paths accounting for common trunk frequencies and stop clusters.
 *
 * This method is conceptually very similar to the work of the Minnesota Accessibility Observatory
 * (http://www.its.umn.edu/Publications/ResearchReports/pdfdownloadl.pl?id=2504)
 * They run repeated searches for each departure time in the window. We take advantage of the fact that the street
 * network is static (for the most part, assuming time-dependent turn restrictions and traffic are consistent across
 * the time window) and only run a fast transit search for each minute in the window.
 */
public class RepeatedRaptorProfileRouter {

    private static final Logger LOG = LoggerFactory.getLogger(RepeatedRaptorProfileRouter.class);

    public static final int MAX_DURATION = 10 * 60 * 2; // seconds

    public ProfileRequest request;

    public Graph graph;

    // The spacing in minutes between RAPTOR calls within the time window
    public int stepMinutes = 1;

    /** Three time surfaces for min, max, and average travel time over the given time window. */
    public TimeSurface.RangeSet timeSurfaceRangeSet;

    /** If not null, completely skip this agency during the calculations. */
    public String banAgency = null;

    /**
     * If this is null we will generate a throw-away raptor data table. If it is set, the provided table will be used
     * for routing. Ideally we'd handle the cacheing of reusable raptor tables inside this class, but this class is
     * a throw-away calculator instance and doesn't have access to the job IDs which would be the cache keys.
     */
    public RaptorWorkerData raptorWorkerData;

    private ShortestPathTree preTransitSpt;

    /** The sum of all earliest-arrival travel times to a given transit stop. Will be divided to create an average. */
    TObjectLongMap accumulator = new TObjectLongHashMap();

    /** The number of travel time observations added into the accumulator. The divisor when calculating an average. */
    TObjectIntMap counts = new TObjectIntHashMap();

    /** Samples to propagate times to */
    private SampleSet sampleSet;

    private PropagatedTimesStore propagatedTimesStore;

    // Set this field to an existing taskStatistics before routing if you want to collect performance information.
    public TaskStatistics ts = new TaskStatistics();

    // Set this field to true before routing if you want the full travel times included in your response.
    public boolean includeTimes = false;

    /**
     * Make a router to use for making time surfaces only.
     *
     * If you're building ResultSets, you should use the below method that uses a SampleSet;
     * otherwise you maximum and average may not be correct.
     *
     * If you want isochrones, you should use this method, or use a sampleset that is very fine. The
     * reason for this is that isochrones are interpolated from these points in Euclidean space, so the
     * points need to be very fine. Consider the case of three roads in an equilateral triangle, the edges
     * of which each take ten minutes to traverse. If you're standing at one vertex and want to go
     * halfway down the opposite edge, it is a fifteen minute trip. However, interpolating between the
     * vertices will not give fifteen minutes, it will give ten. The less granular your representation
     * is, the worse this problem will be.
     * TODO merge this with the 3-arg constructor, and just specify what happens when you have a null pointset.
     * This should allow us to get rid of some conditionals in the calling code.
     */
    @Deprecated // just call the one below with null
    public RepeatedRaptorProfileRouter(Graph graph, ProfileRequest request) {
        this(graph, request, null);
    }

    /**
     * Make a router to use for making ResultSets. This propagates times all the way to the samples, so that
     * average and maximum travel time are correct.
     * 
     * Samples are linked to two vertices at the ends of an edge, and it is possible that the average for the sample
     * (as well as the max) is lower than the average or the max at either of the vertices, because it may be that
     * every time the max at one vertex is occurring, a lower value is occurring at the other. This initially
     * seems improbable, but consider the case when there are two parallel transit services running out of phase.
     * It may be that some of the time it makes sense to go out of your house and turn left, and sometimes it makes
     * sense to turn right, depending on which is coming first.
     */
    public RepeatedRaptorProfileRouter (Graph graph, ProfileRequest request, SampleSet sampleSet) {
        this.request = request;
        this.graph = graph;
        this.sampleSet = sampleSet;
    }

    public ResultEnvelope route () {

        boolean isochrone = (sampleSet == null); // When no sample set is provided, we're making isochrones.
        boolean transit = (request.transitModes != null && request.transitModes.isTransit()); // Does the search involve transit at all?

        long computationStartTime = System.currentTimeMillis();
        LOG.info("Begin profile request");

        // Data tables may have been supplied by the caller (if they are cached). Otherwise generate a throw away one.
        // We only create data tables if transit is in use, otherwise they wouldn't serve any purpose.
        if (raptorWorkerData == null && transit) {
            long dataStart = System.currentTimeMillis();
            raptorWorkerData = getRaptorWorkerData(request, graph, sampleSet, ts);
            ts.raptorData = (int) (System.currentTimeMillis() - dataStart);
        }

        // Find the transit stops that are accessible from the origin, leaving behind an SPT behind of access
        // times to all reachable vertices.
        long initialStopStartTime = System.currentTimeMillis();
        // This will return null if we have no transit data, but will leave behind a pre-transit SPT.
        TIntIntMap transitStopAccessTimes = findInitialStops(false, raptorWorkerData);
        // Create an array containing the best travel time in seconds to each vertex in the graph when not using transit.
        int[] nonTransitTimes = new int[Vertex.getMaxIndex()];
        Arrays.fill(nonTransitTimes, Integer.MAX_VALUE);
        for (State state : preTransitSpt.getAllStates()) {
            // Note that we are using the walk distance divided by speed here in order to be consistent with the
            // least-walk optimization in the initial stop search (and the stop tree cache which is used at egress)
            // TODO consider why this matters, I'm using reported travel time from the states
            int time = (int) state.getElapsedTimeSeconds();
            int vidx = state.getVertex().getIndex();
            int otime = nonTransitTimes[vidx];
            // There may be dominated states in the SPT. Make sure we don't include them here.
            if (otime > time) {
                nonTransitTimes[vidx] = time;
            }
        }
        ts.initialStopSearch = (int) (System.currentTimeMillis() - initialStopStartTime);

        long walkSearchStart = System.currentTimeMillis(); // FIXME wasn't the walk search already performed above?

        // At this point we have an array of the travel times in seconds to each vertex in the graph without transit.
        // In the event that a pointset was supplied, our real targets are the points in the pointset, not the vertices
        // in the graph. Therefore we must replace the vertex-indexed array with a new point-indexed array.
        if (sampleSet != null) {
            nonTransitTimes = sampleSet.eval(nonTransitTimes);
        }
        ts.walkSearch = (int) (System.currentTimeMillis() - walkSearchStart);

        if (transit) {
            RaptorWorker worker = new RaptorWorker(raptorWorkerData, request);
            propagatedTimesStore = worker.runRaptor(graph, transitStopAccessTimes, nonTransitTimes, ts);
            ts.initialStopCount = transitStopAccessTimes.size();
        } else {
            // Nontransit case: skip transit routing and make a propagated times store based on only one row.
            propagatedTimesStore = new PropagatedTimesStore(graph, request, nonTransitTimes.length);
            int[][] singleRoundResults = new int[1][];
            singleRoundResults[0] = nonTransitTimes;
            propagatedTimesStore.setFromArray(singleRoundResults, new boolean[] {true},
                    PropagatedTimesStore.ConfidenceCalculationMethod.MIN_MAX);
        }
        for (int min : propagatedTimesStore.mins) {
            if (min != RaptorWorker.UNREACHED) ts.targetsReached++;
        }
        ts.compute = (int) (System.currentTimeMillis() - computationStartTime);
        LOG.info("Profile request finished in {} seconds", (ts.compute) / 1000.0);

        // Turn the results of the search into isochrone geometries or accessibility data as requested.
        long resultSetStart = System.currentTimeMillis();
        ResultEnvelope envelope = new ResultEnvelope();
        if (isochrone) {
            // No destination point set was provided and we're just making isochrones based on travel time to vertices,
            // rather than finding access times to a set of user-specified points.
            envelope = propagatedTimesStore.makeIsochronesForVertices();
        } else {
            // A destination point set was provided. We've found access times to a set of specified points.
            // TODO actually use those boolean params to calculate isochrones on a regular grid pointset
            // TODO maybe there's a better way to pass includeTimes in here from the clusterRequest,
            // maybe we should just provide the whole clusterRequest not just the wrapped profileRequest.
            envelope = propagatedTimesStore.makeResults(sampleSet, includeTimes, true, false);
        }
        ts.resultSets = (int) (System.currentTimeMillis() - resultSetStart);
        return envelope;
    }

    /**
     * Find all transit stops accessible by streets around the origin, leaving behind a shortest path tree of the
     * reachable area in the field preTransitSpt.
     *
     * @param data the raptor data table to use. If this is null (i.e. there is no transit) range is extended,
     *             and we don't care if we actually find any stops, we just want the tree of on-street distances.
     */
    @VisibleForTesting
    public TIntIntMap findInitialStops(boolean dest, RaptorWorkerData data) {
        LOG.info("Finding initial stops");
        double lat = dest ? request.toLat : request.fromLat;
        double lon = dest ? request.toLon : request.fromLon;
        QualifiedModeSet modes = dest ? request.egressModes : request.accessModes;

        RoutingRequest rr = new RoutingRequest(modes);
        rr.batch = true;
        rr.from = new GenericLocation(lat, lon);
        //rr.walkSpeed = request.walkSpeed;
        rr.to = rr.from;
        rr.setRoutingContext(graph);
        rr.dateTime = request.date.toDateMidnight(DateTimeZone.forTimeZone(graph.getTimeZone())).getMillis() / 1000 +
                request.fromTime;
        rr.walkSpeed = request.walkSpeed;
        rr.bikeSpeed = request.bikeSpeed;

        //rr.modes.setWalk(true);

        if (data == null) {
            // Non-transit mode. Search out to the full 120 minutes.
            // Should really use directModes.
            rr.worstTime = rr.dateTime + RaptorWorker.MAX_DURATION;
            rr.dominanceFunction = new DominanceFunction.EarliestArrival();
        } else {
            // Transit mode, limit pre-transit travel.
            if (rr.modes.contains(TraverseMode.BICYCLE)) {
                rr.dominanceFunction = new DominanceFunction.EarliestArrival();
                rr.worstTime = rr.dateTime + request.maxBikeTime * 60;
            } else {
                // We use walk-distance limiting and a least-walk dominance function in order to be consistent with egress walking
                // which is implemented this way because walk times can change when walk speed changes. Also, walk times are floating
                // point and can change slightly when streets are split. Street lengths are internally fixed-point ints, which do not
                // suffer from roundoff. Great care is taken when splitting to preserve sums.
                // When cycling, this is not an issue; we already have an explicitly asymmetrical search (cycling at the origin, walking at the destination),
                // so we need not preserve symmetry.
                // We use the max walk time for the search at the origin, but we clamp it to MAX_WALK_METERS so that we don;t
                // have every transit stop in the state as an initial transit stop if someone sets max walk time to four days,
                // and so that symmetry is preserved.
                rr.maxWalkDistance = Math.min(request.maxWalkTime * 60 * request.walkSpeed, GraphIndex.MAX_WALK_METERS); // FIXME kind of arbitrary
                rr.softWalkLimiting = false;
                rr.dominanceFunction = new DominanceFunction.LeastWalk();
            }
        }

        rr.numItineraries = 1;
        rr.longDistance = true;

        AStar aStar = new AStar();
        preTransitSpt = aStar.getShortestPathTree(rr, 5);

        // Return nearest stops if we're using transit,
        // otherwise return null and leave preTransitSpt around for later use.
        if (data != null) {
            TIntIntMap accessTimes = data.findStopsNear(preTransitSpt, graph, rr.modes.contains(TraverseMode.BICYCLE), request.walkSpeed);
            LOG.info("Found {} transit stops", accessTimes.size());
            return accessTimes;
        } else {
            return null;
        }
    }

    /** Create RAPTOR worker data from a graph, profile request and sample set (the last of which may be null */
    public static RaptorWorkerData getRaptorWorkerData (ProfileRequest request, Graph graph, SampleSet sampleSet, TaskStatistics ts) {
        LOG.info("Make data...");
        long startData = System.currentTimeMillis();

        // assign indices for added transit stops
        // note that they only need be unique in the context of this search.
        // note also that there may be, before this search is over, vertices with higher indices (temp vertices)
        // but they will not be transit stops.
        if (request.scenario != null && request.scenario.modifications != null) {
            for (AddTripPattern atp : Iterables
                    .filter(request.scenario.modifications, AddTripPattern.class)) {
                atp.materialize(graph);
            }
        }

        // convert from joda to java - ISO day of week with monday == 1
        DayOfWeek dayOfWeek = DayOfWeek.of(request.date.getDayOfWeek());

        TimeWindow window = new TimeWindow(request.fromTime, request.toTime + RaptorWorker.MAX_DURATION,
                graph.index.servicesRunning(request.date), dayOfWeek);

        RaptorWorkerData raptorWorkerData;
        if (sampleSet == null)
            raptorWorkerData = new RaptorWorkerData(graph, window, request, ts);
        else
            raptorWorkerData = new RaptorWorkerData(graph, window, request, sampleSet,
                    ts);

        ts.raptorData = (int) (System.currentTimeMillis() - startData);

        LOG.info("done");

        return raptorWorkerData;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy