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

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

package org.opentripplanner.profile;

import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import jersey.repackaged.com.google.common.collect.Maps;
import jersey.repackaged.com.google.common.collect.Sets;

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;

import org.opentripplanner.analyst.TimeSurface;
import org.opentripplanner.analyst.TimeSurface.RangeSet;
import org.opentripplanner.api.parameter.QualifiedModeSet;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.common.model.T2;
import org.opentripplanner.profile.ProfileState.Type;
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.edgetype.SimpleTransfer;
import org.opentripplanner.routing.edgetype.TripPattern;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.spt.DominanceFunction;
import org.opentripplanner.routing.spt.ShortestPathTree;
import org.opentripplanner.routing.trippattern.FrequencyEntry;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;

/**
 * Profile routing using a round-based approach, more or less like RAPTOR (http://research.microsoft.com/pubs/156567/raptor_alenex.pdf)
 * @author mattwigway
 *
 */
public class RoundBasedProfileRouter {
    public final Graph graph;
    public final ProfileRequest request;
    public TimeWindow window;
    
    public final int TIMEOUT = 60;
    /** the maximum number of rounds. this is the same as the maximum number of boarding */
    public final int MAX_ROUNDS = 3;
    
    public static final int CUTOFF_SECONDS = 180 * 60;
    
    public static boolean RETAIN_PATTERNS = false;
    
    private static final Logger LOG = LoggerFactory.getLogger(RoundBasedProfileRouter.class);
    
    public Multimap retainedStates = HashMultimap.create();
    
    /** the routing results */
    public RangeSet timeSurfaceRangeSet;  
    
    public RoundBasedProfileRouter (Graph graph, ProfileRequest request) {
        this.graph = graph;
        this.request = request;
    }
    
    public void route () {
        LOG.info("access modes: {}", request.accessModes);
        LOG.info("egress modes: {}", request.egressModes);
        LOG.info("direct modes: {}", request.directModes);
        
        // TimeWindow could constructed in the caller, which does have access to the graph index.
        this.window = new TimeWindow(request.fromTime, request.toTime, graph.index.servicesRunning(request.date));
            
        // Establish search timeouts
        long searchBeginTime = System.currentTimeMillis();
        long abortTime = searchBeginTime + TIMEOUT * 1000;
        
        LOG.info("Finding access/egress paths.");
        // Look for stops that are within a given time threshold of the origin and destination
        // Find the closest stop on each pattern near the origin and destination
        // TODO consider that some stops may be closer by one mode than another
        // and that some stops may be accessible by one mode but not another
        
        ProfileStateStore store = RETAIN_PATTERNS ? new MultiProfileStateStore() : new SingleProfileStateStore();
        
        for (ProfileState ps : findInitialStops(false)) {
            store.put(ps);
        }
        
        LOG.info("Found {} initial stops", store.size());
       
        // note: we do not add the found stops to retainedStates, because, if you are making a zero-transfer trip,
        // we don't want to generate trips that are artificially forced to go past a transit stop.
        ROUNDS: for (int round = 0; round < MAX_ROUNDS; round++) {
            long roundStart = System.currentTimeMillis();
            LOG.info("Begin round {}; {} stops to explore", round, store.size());
            
            ProfileStateStore previousStore = store;
            store = RETAIN_PATTERNS ? new MultiProfileStateStore((MultiProfileStateStore) store) : new SingleProfileStateStore((SingleProfileStateStore) store);
        
            Set patternsToExplore = Sets.newHashSet();
            
            // explore all of the patterns at the stops visited on the previous round
            for (TransitStop tstop : previousStore.keys()) {
                Collection patterns = graph.index.patternsForStop.get(tstop.getStop());
                patternsToExplore.addAll(patterns);
            }
            
            LOG.info("Exploring {} patterns", patternsToExplore.size());
            
            // propagate all of the bounds down each pattern
            PATTERNS: for (final TripPattern pattern : patternsToExplore) {
                STOPS: for (int i = 0; i < pattern.stopVertices.length; i++) {
                    if (!previousStore.containsKey(pattern.stopVertices[i]))
                        continue STOPS;
                    
                    Collection statesToPropagate;                    
                    // only propagate nondominated states
                    statesToPropagate = previousStore.get(pattern.stopVertices[i]);
                    
                    // don't propagate states that use the same pattern
                    statesToPropagate = Collections2.filter(statesToPropagate, new Predicate () {

                        @Override
                        public boolean apply(ProfileState input) {
                            // don't reboard same pattern, and don't board patterns that are better boarded elsewhere
                            return !input.containsPattern(pattern) &&
                                    (input.targetPatterns == null || input.targetPatterns.contains(pattern));
                        }
                        
                    });
                    
                    if (statesToPropagate.isEmpty())
                        continue STOPS;         
                    
                    int minWaitTime = Integer.MAX_VALUE;
                    int maxWaitTime = Integer.MIN_VALUE;
                    
                    // TODO: scheduled transfers. For scheduled transfers, recall that it depends on from whence you have come
                    // (i.e. the transfer time is different for the initial boarding than transfers)
                    for (FrequencyEntry freq : pattern.scheduledTimetable.frequencyEntries) {
                        if (freq.exactTimes) {
                            throw new IllegalStateException("Exact times not yet supported in profile routing.");
                        }
                        int overlap = window.overlap(freq.startTime, freq.endTime, freq.tripTimes.serviceCode);
                        if (overlap > 0) {
                            if (freq.headway > maxWaitTime) maxWaitTime = freq.headway;
                            // if any frequency-based trips are running a wait of 0 is always possible, because it could come
                            // just as you show up at the stop.
                            minWaitTime = 0;
                        }
                    }
                    
                    DESTSTOPS: for (int j = i + 1; j < pattern.stopVertices.length; j++) {
                        // how long does it take to ride this trip from i to j?
                        int minRideTime = Integer.MAX_VALUE;
                        int maxRideTime = Integer.MIN_VALUE;
                        
                        // how long does it take to get to stop j from stop i?
                        for (TripTimes tripTimes : pattern.scheduledTimetable.tripTimes) {
                            int depart = tripTimes.getDepartureTime(i);
                            int arrive = tripTimes.getArrivalTime(j);
                            if (window.includes (depart) && 
                                window.includes (arrive) && 
                                window.servicesRunning.get(tripTimes.serviceCode)) {
                                int t = arrive - depart;
                                if (t < minRideTime) minRideTime = t;
                                if (t > maxRideTime) maxRideTime = t;
                            }
                        }
                        /* Do the same thing for any frequency-based trips. */
                        for (FrequencyEntry freq : pattern.scheduledTimetable.frequencyEntries) {
                            TripTimes tt = freq.tripTimes;
                            int overlap = window.overlap(freq.startTime, freq.endTime, tt.serviceCode);
                            if (overlap == 0) continue;
                            int depart = tt.getDepartureTime(i);
                            int arrive = tt.getArrivalTime(j);
                            int t = arrive - depart;
                            if (t < minRideTime) minRideTime = t;
                            if (t > maxRideTime) maxRideTime = t;
                        }
                        
                        if (minWaitTime == Integer.MAX_VALUE || maxWaitTime == Integer.MIN_VALUE ||
                                minRideTime == Integer.MAX_VALUE || maxRideTime == Integer.MIN_VALUE)
                            // no trips in window that arrive at stop
                            continue DESTSTOPS;
                        
                        if (minRideTime < 0 || maxRideTime < 0) {
                            LOG.error("Pattern {} travels backwards in time between stop {} and {}",
                                    pattern, pattern.stopVertices[i].getStop(), pattern.stopVertices[j].getStop());
                            continue DESTSTOPS;
                        }
                        
                        // note: unnecessary variance in the scheduled case. It is entirely possible that the max wait and the max ride time
                        // cannot occur simultaneously.
                        
                        // propagate every profile state that we picked up at stop i to stop j
                        // we've already checked to ensure we're not reboarding the same pattern
                        for (ProfileState ps : statesToPropagate) {
                            ProfileState ps2 = ps.propagate(minWaitTime + minRideTime, maxWaitTime + maxRideTime);
                            
                            if (ps2.upperBound > CUTOFF_SECONDS)
                                continue;
                            
                            ps2.stop = pattern.stopVertices[j];
                            ps2.accessType = Type.TRANSIT;
                            
                            if (RETAIN_PATTERNS)
                                ps2.patterns = new TripPattern[] { pattern };
                            
                            store.put(ps2);  
                        }
                    }
                }
            }
            
            // merge states that came from the same stop.
            if (RETAIN_PATTERNS) {
                LOG.info("Round completed, merging similar states");
                ((MultiProfileStateStore) store).mergeStates();
            }
            
            for (ProfileState ps : store.getAll()) {
                retainedStates.put(ps.stop, ps);
            }
            
            if (round == MAX_ROUNDS - 1) {
                LOG.info("Finished round {} in {} seconds", round, (System.currentTimeMillis() - roundStart) / 1000);
                break ROUNDS;
            }
            
            // propagate states to nearby stops (transfers)
            LOG.info("Finding transfers . . .");               
            // avoid concurrent modification
            Set touchedStopKeys = new HashSet(store.keys());
            for (TransitStop tstop : touchedStopKeys) {
                List> accessTimes = Lists.newArrayList();
                
                // find transfers for the stop
                for (Edge e : tstop.getOutgoing()) {
                    if (e instanceof SimpleTransfer) {
                        SimpleTransfer t = (SimpleTransfer) e;
                        int time = (int) (t.getDistance() / request.walkSpeed);
                        accessTimes.add(new T2((TransitStop) e.getToVertex(), time));
                    }
                }
                
                // only transfer from nondominated states. only transfer to each pattern once
                Collection statesAtStop = store.get(tstop);
                
                TObjectIntHashMap minBoardTime = new TObjectIntHashMap(1000, .75f, Integer.MAX_VALUE);
                Map optimalBoardState = Maps.newHashMap();
                
                List xferStates = Lists.newArrayList();
                
                
                // make a hashset of the patterns that stop here, because we don't want to transfer to them at another stop
                HashSet patternsAtSource = new HashSet(graph.index.patternsForStop.get(tstop.getStop()));
                
                for (ProfileState ps : statesAtStop) {
                    for (T2 atime : accessTimes) {
                        ProfileState ps2 = ps.propagate(atime.second);
                        ps2.accessType = Type.TRANSFER;
                        ps2.stop = atime.first;
                        // note that we do not reset pattern, as we still don't want to transfer from a pattern to itself.
                        // (TODO: is this true? loop routes?)
                        
                        for (TripPattern patt : graph.index.patternsForStop.get(atime.first.getStop())) {
                            // don't transfer to patterns that we can board at this stop.
                            if (patternsAtSource.contains(patt))
                                continue;
                            
                            if (atime.second < minBoardTime.get(patt)) {
                                minBoardTime.put(patt, atime.second);
                                optimalBoardState.put(patt, ps2);
                            }
                        }

                        xferStates.add(ps2);
                    }
                }
                
                for (Entry e : optimalBoardState.entrySet()) {
                    ProfileState ps = e.getValue();
                    if (ps.targetPatterns == null)
                        ps.targetPatterns = Sets.newHashSet();
                    
                    ps.targetPatterns.add(e.getKey());
                }
                
                for (ProfileState ps : xferStates) {
                    if (ps.targetPatterns != null && !ps.targetPatterns.isEmpty()) {
                        store.put(ps);
                    }
                }
            }
            
            LOG.info("Finished round {} in {} seconds", round, (System.currentTimeMillis() - roundStart) / 1000);
        }
        
        LOG.info("Finished profile routing in {} seconds", (System.currentTimeMillis() - searchBeginTime) / 1000);
        
        makeSurfaces();
        
        LOG.info("Finished analyst request in {} seconds total", (System.currentTimeMillis() - searchBeginTime) / 1000);
    }
    
    /** from a collection of profile states at a transit stop, return a collection of all the nondominated states */
    public Collection nondominated(Collection original, TransitStop tstop) {
        // find the min upper bound
        int minUpperBound = Integer.MAX_VALUE;
        
        // TODO optimization: retain min upper bound as states are added.
        for (ProfileState ps : original) {
            if (ps.upperBound < minUpperBound)
                minUpperBound = ps.upperBound;
        }
        
        // we also check against states that were found in previous rounds and have already been propagated;
        // no reason to propagate again.
        for (ProfileState ps : retainedStates.get(tstop)) {
            if (ps.upperBound < minUpperBound)
                minUpperBound = ps.upperBound;
        }
        
        for (Iterator it = original.iterator(); it.hasNext();) {
            ProfileState ps = it.next();
            if (ps.lowerBound > minUpperBound || ps.lowerBound > CUTOFF_SECONDS)
                it.remove();
        }
        
        return original;
    }
    
    /** find the boarding stops */
    private Collection findInitialStops(boolean dest) {
        double lat = dest ? request.toLat : request.fromLat;
        double lon = dest ? request.toLon : request.fromLon;
        QualifiedModeSet modes = dest ? request.accessModes : request.egressModes;
        
        List stops = Lists.newArrayList();
        
        RoutingRequest rr = new RoutingRequest(TraverseMode.WALK);
        rr.dominanceFunction = new DominanceFunction.EarliestArrival();
        rr.batch = true;
        rr.from = new GenericLocation(lat, lon);
        rr.walkSpeed = request.walkSpeed;
        rr.to = rr.from;
        rr.setRoutingContext(graph);
        
        // RoutingRequest dateTime defaults to currentTime.
        // If elapsed time is not capped, searches are very slow.
        rr.worstTime = (rr.dateTime + request.maxWalkTime * 60);
        AStar astar = new AStar();
        rr.longDistance = true;
        rr.setNumItineraries(1);
        ShortestPathTree spt = astar.getShortestPathTree(rr, 5); // timeout in seconds
        for (TransitStop tstop : graph.index.stopVertexForStop.values()) {
            State s = spt.getState(tstop);
            if (s != null) {
                ProfileState ps = new ProfileState();
                ps.lowerBound = ps.upperBound = (int) s.getElapsedTimeSeconds();
                ps.stop = tstop;
                ps.accessType = Type.STREET;
                stops.add(ps);
            }
        }
        
        Map optimalBoardingLocation = Maps.newHashMap();
        TObjectIntMap minBoardTime = new TObjectIntHashMap(100, 0.75f, Integer.MAX_VALUE);
        
        // Only board patterns at the closest possible stop
        for (ProfileState ps : stops) {
            for (TripPattern pattern : graph.index.patternsForStop.get(ps.stop.getStop())) {
                if (ps.lowerBound < minBoardTime.get(pattern)) {
                    optimalBoardingLocation.put(pattern, ps);
                    minBoardTime.put(pattern, ps.lowerBound);
                }                    
            }
            
            ps.targetPatterns = Sets.newHashSet();
        }
        
        LOG.info("Found {} reachable stops, filtering to only board at closest stops", stops.size());
        
        for (Entry e : optimalBoardingLocation.entrySet()) {
            e.getValue().targetPatterns.add(e.getKey());
        }
        
        for (Iterator it = stops.iterator(); it.hasNext();) {
            if (it.next().targetPatterns.isEmpty())
                it.remove();
        }
        
        rr.cleanup();
        
        return stops;
    }
    
    /** analyst mode: propagate to street network */
    private void makeSurfaces() {
        LOG.info("Propagating from transit stops to the street network...");
        List lower = Lists.newArrayList();
        List upper = Lists.newArrayList();
        List avg = Lists.newArrayList();
        
        RoutingRequest rr = new RoutingRequest(TraverseMode.WALK);
        rr.batch = (true);
        rr.from = new GenericLocation(request.fromLat, request.fromLon);
        rr.setRoutingContext(graph);
        rr.longDistance = true;
        rr.dominanceFunction = new DominanceFunction.EarliestArrival();
        rr.setNumItineraries(1);
        rr.worstTime = rr.dateTime + CUTOFF_SECONDS;
       
        long startTime = rr.dateTime;
        
        State origin = new State(rr);
        
        // Iterate over all rides at all clusters
        // Note that some may be dominated, but it doesn't matter
        // Multi-origin Dijkstra search; preinitialize the queue with states at each transit stop
        for (Collection pss : retainedStates.asMap().values()) {
            TransitStop tstop = null;
            int lowerBound = Integer.MAX_VALUE;
            int upperBound = Integer.MAX_VALUE;
            
            for (ProfileState ps : pss) {
                if (tstop == null) tstop = ps.stop;
                if (ps.lowerBound < lowerBound) lowerBound = ps.lowerBound;
                if (ps.upperBound < upperBound) upperBound = ps.upperBound;
            }
            
            if (lowerBound == Integer.MAX_VALUE || upperBound == Integer.MAX_VALUE)
                throw new IllegalStateException("Invalid bound!");
               
            lower.add(new State(tstop, null, lowerBound + startTime, startTime, rr));
            upper.add(new State(tstop, null, upperBound + startTime, startTime, rr));
            
            // TODO extremely incorrect hack!
            avg.add(new State(tstop, null, (upperBound + lowerBound) / 2 + startTime, startTime, rr));
        }
        
        // get direct trips as well
        lower.add(origin);
        upper.add(origin);
        avg.add(origin);
        
        // create timesurfaces
        timeSurfaceRangeSet = new TimeSurface.RangeSet();

        AStar astar = new AStar();
        timeSurfaceRangeSet.min = new TimeSurface(astar.getShortestPathTree(rr, 20, null, lower), false);
        astar = new AStar();
        timeSurfaceRangeSet.max = new TimeSurface(astar.getShortestPathTree(rr, 20, null, upper), false);
        astar = new AStar();
        timeSurfaceRangeSet.avg = new TimeSurface(astar.getShortestPathTree(rr, 20, null, avg), false);
        
        rr.cleanup();
        
        LOG.info("Done with propagation.");
        /* Store the results in a field in the router object. */

    }
    
    public void cleanup () {
        // TODO
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy