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

org.opentripplanner.routing.impl.GraphPathFinder Maven / Gradle / Ivy

There is a newer version: 2.6.0
Show newest version
/* 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 . */

package org.opentripplanner.routing.impl;

import com.google.common.collect.Lists;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.opentripplanner.api.resource.DebugOutput;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.routing.algorithm.AStar;
import org.opentripplanner.routing.algorithm.strategies.EuclideanRemainingWeightHeuristic;
import org.opentripplanner.routing.algorithm.strategies.InterleavedBidirectionalHeuristic;
import org.opentripplanner.routing.algorithm.strategies.RemainingWeightHeuristic;
import org.opentripplanner.routing.algorithm.strategies.TrivialRemainingWeightHeuristic;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.edgetype.LegSwitchingEdge;
import org.opentripplanner.routing.edgetype.TransitBoardAlight;
import org.opentripplanner.routing.error.PathNotFoundException;
import org.opentripplanner.routing.error.VertexNotFoundException;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.spt.DominanceFunction;
import org.opentripplanner.routing.spt.GraphPath;
import org.opentripplanner.standalone.Router;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.stream.Collectors;

/**
 * This class contains the logic for repeatedly building shortest path trees and accumulating paths through
 * the graph until the requested number of them have been found.
 * It is used in point-to-point (i.e. not one-to-many / analyst) routing.
 *
 * Its exact behavior will depend on whether the routing request allows transit.
 *
 * When using transit it will incorporate techniques from what we called "long distance" mode, which is designed to
 * provide reasonable response times when routing over large graphs (e.g. the entire Netherlands or New York State).
 * In this case it only uses the street network at the first and last legs of the trip, and all other transfers
 * between transit vehicles will occur via SimpleTransfer edges which are pre-computed by the graph builder.
 * 
 * More information is available on the OTP wiki at:
 * https://github.com/openplans/OpenTripPlanner/wiki/LargeGraphs
 *
 * One instance of this class should be constructed per search (i.e. per RoutingRequest: it is request-scoped).
 * Its behavior is undefined if it is reused for more than one search.
 *
 * It is very close to being an abstract library class with only static functions. However it turns out to be convenient
 * and harmless to have the OTPServer object etc. in fields, to avoid passing context around in function parameters.
 */
public class GraphPathFinder {

    private static final Logger LOG = LoggerFactory.getLogger(GraphPathFinder.class);
    private static final double DEFAULT_MAX_WALK = 2000;
    private static final double CLAMP_MAX_WALK = 15000;

    Router router;

    public GraphPathFinder(Router router) {
        this.router = router;
    }

    /**
     * Repeatedly build shortest path trees, retaining the best path to the destination after each try.
     * For search N, all trips used in itineraries retained from trips 0..(N-1) are "banned" to create variety.
     * The goal direction heuristic is reused between tries, which means the later tries have more information to
     * work with (in the case of the more sophisticated bidirectional heuristic, which improves over time).
     */
    public List getPaths(RoutingRequest options) {

        RoutingRequest originalReq = options.clone();

        if (options == null) {
            LOG.error("PathService was passed a null routing request.");
            return null;
        }

        // Reuse one instance of AStar for all N requests, which are carried out sequentially
        AStar aStar = new AStar();
        if (options.rctx == null) {
            options.setRoutingContext(router.graph);
            // The special long-distance heuristic should be sufficient to constrain the search to the right area.
        }
        // If this Router has a GraphVisualizer attached to it, set it as a callback for the AStar search
        if (router.graphVisualizer != null) {
            aStar.setTraverseVisitor(router.graphVisualizer.traverseVisitor);
            // options.disableRemainingWeightHeuristic = true; // DEBUG
        }

        // Without transit, we'd just just return multiple copies of the same on-street itinerary.
        if (!options.modes.isTransit()) {
            options.numItineraries = 1;
        }
        options.dominanceFunction = new DominanceFunction.MinimumWeight(); // FORCING the dominance function to weight only
        LOG.debug("rreq={}", options);

        // Choose an appropriate heuristic for goal direction.
        RemainingWeightHeuristic heuristic;
        RemainingWeightHeuristic reversedSearchHeuristic;
        if (options.disableRemainingWeightHeuristic) {
            heuristic = new TrivialRemainingWeightHeuristic();
            reversedSearchHeuristic = new TrivialRemainingWeightHeuristic();
        } else if (options.modes.isTransit()) {
            // Only use the BiDi heuristic for transit. It is not very useful for on-street modes.
            // heuristic = new InterleavedBidirectionalHeuristic(options.rctx.graph);
            // Use a simplistic heuristic until BiDi heuristic is improved, see #2153
            heuristic = new InterleavedBidirectionalHeuristic();
            reversedSearchHeuristic = new InterleavedBidirectionalHeuristic();
        } else {
            heuristic = new EuclideanRemainingWeightHeuristic();
            reversedSearchHeuristic = new EuclideanRemainingWeightHeuristic();
        }
        options.rctx.remainingWeightHeuristic = heuristic;


        /* In RoutingRequest, maxTransfers defaults to 2. Over long distances, we may see
         * itineraries with far more transfers. We do not expect transfer limiting to improve
         * search times on the LongDistancePathService, so we set it to the maximum we ever expect
         * to see. Because people may use either the traditional path services or the 
         * LongDistancePathService, we do not change the global default but override it here. */
        options.maxTransfers = 4;
        // Now we always use what used to be called longDistance mode. Non-longDistance mode is no longer supported.
        options.longDistance = true;

        /* In long distance mode, maxWalk has a different meaning than it used to.
         * It's the radius around the origin or destination within which you can walk on the streets.
         * If no value is provided, max walk defaults to the largest double-precision float.
         * This would cause long distance mode to do unbounded street searches and consider the whole graph walkable. */
        if (options.maxWalkDistance == Double.MAX_VALUE) options.maxWalkDistance = DEFAULT_MAX_WALK;
        if (options.maxWalkDistance > CLAMP_MAX_WALK) options.maxWalkDistance = CLAMP_MAX_WALK;
        long searchBeginTime = System.currentTimeMillis();
        LOG.debug("BEGIN SEARCH");
        List paths = Lists.newArrayList();
        while (paths.size() < options.numItineraries) {
            // TODO pull all this timeout logic into a function near org.opentripplanner.util.DateUtils.absoluteTimeout()
            int timeoutIndex = paths.size();
            if (timeoutIndex >= router.timeouts.length) {
                timeoutIndex = router.timeouts.length - 1;
            }
            double timeout = searchBeginTime + (router.timeouts[timeoutIndex] * 1000);
            timeout -= System.currentTimeMillis(); // Convert from absolute to relative time
            timeout /= 1000; // Convert milliseconds to seconds
            if (timeout <= 0) {
                // Catch the case where advancing to the next (lower) timeout value means the search is timed out
                // before it even begins. Passing a negative relative timeout in the SPT call would mean "no timeout".
                options.rctx.aborted = true;
                break;
            }
            // Don't dig through the SPT object, just ask the A star algorithm for the states that reached the target.
            aStar.getShortestPathTree(options, timeout);

            if (options.rctx.aborted) {
                break; // Search timed out or was gracefully aborted for some other reason.
            }
            List newPaths = aStar.getPathsToTarget();
            if (newPaths.isEmpty()) {
                break;
            }

            // Do a full reversed search to compact the legs
            if(options.compactLegsByReversedSearch){
                newPaths = compactLegsByReversedSearch(aStar, originalReq, options, newPaths, timeout, reversedSearchHeuristic);
            }

            // Find all trips used in this path and ban them for the remaining searches
            for (GraphPath path : newPaths) {
                // path.dump();
                List tripIds = path.getTrips();
                for (AgencyAndId tripId : tripIds) {
                    options.banTrip(tripId);
                }
                if (tripIds.isEmpty()) {
                    // This path does not use transit (is entirely on-street). Do not repeatedly find the same one.
                    options.onlyTransitTrips = true;
                }
            }

            paths.addAll(newPaths.stream()
                    .filter(path -> {
                        double duration = options.useRequestedDateTimeInMaxHours
                            ? options.arriveBy
                                ? options.dateTime - path.getStartTime()
                                : path.getEndTime() - options.dateTime
                            : path.getDuration();
                        return duration < options.maxHours * 60 * 60;
                    })
                    .collect(Collectors.toList()));

            LOG.debug("we have {} paths", paths.size());
        }
        LOG.debug("END SEARCH ({} msec)", System.currentTimeMillis() - searchBeginTime);
        Collections.sort(paths, new PathComparator(options.arriveBy));
        return paths;
    }

    /**
     * Do a full reversed search to compact the legs of the path.
     *
     * By doing a reversed search we are looking for later departures that will still be in time for transfer
     * to the next trip, shortening the transfer wait time. Also considering other routes than the ones found
     * in the original search.
     *
     * For arrive-by searches, we are looking to shorten transfer wait time and rather arrive earlier.
     */
    private List compactLegsByReversedSearch(AStar aStar, RoutingRequest originalReq, RoutingRequest options,
                                                        List newPaths, double timeout,
                                                        RemainingWeightHeuristic remainingWeightHeuristic){
        List reversedPaths = new ArrayList<>();
        for(GraphPath newPath : newPaths){
            State targetAcceptedState = options.arriveBy ? newPath.states.getLast().reverse() : newPath.states.getLast();
            if(targetAcceptedState.stateData.getNumBooardings() < 2) {
                reversedPaths.add(newPath);
                continue;
            }
            final long arrDepTime = targetAcceptedState.getTimeSeconds();
            LOG.debug("Dep time: " + new Date(newPath.getStartTime() * 1000));
            LOG.debug("Arr time: " + new Date(newPath.getEndTime() * 1000));

            // find first/last transit stop
            Vertex transitStop = null;
            long transitStopTime = arrDepTime;
            while (transitStop == null) {
                if(targetAcceptedState.backEdge instanceof TransitBoardAlight){
                    if(options.arriveBy){
                        transitStop = targetAcceptedState.backEdge.getFromVertex();
                    }else{
                        transitStop = targetAcceptedState.backEdge.getToVertex();
                    }
                    transitStopTime = targetAcceptedState.getTimeSeconds();
                }
                targetAcceptedState = targetAcceptedState.getBackState();
            }

            // find the path from transitStop to origin/destination
            Vertex fromVertex = options.arriveBy ? options.rctx.fromVertex : transitStop;
            Vertex toVertex = options.arriveBy ? transitStop : options.rctx.toVertex;
            RoutingRequest reversedTransitRequest = createReversedTransitRequest(originalReq, options, fromVertex, toVertex,
                    arrDepTime, new EuclideanRemainingWeightHeuristic());
            aStar.getShortestPathTree(reversedTransitRequest, timeout);
            List pathsToTarget = aStar.getPathsToTarget();
            if(pathsToTarget.isEmpty()){
                reversedPaths.add(newPath);
                continue;
            }
            GraphPath walkPath = pathsToTarget.get(0);

            // do the reversed search to/from transitStop
            Vertex fromTransVertex = options.arriveBy ? transitStop : options.rctx.fromVertex;
            Vertex toTransVertex = options.arriveBy ? options.rctx.toVertex: transitStop;
            RoutingRequest reversedMainRequest = createReversedMainRequest(originalReq, options, fromTransVertex,
                    toTransVertex, transitStopTime, remainingWeightHeuristic);
            aStar.getShortestPathTree(reversedMainRequest, timeout);

            List newRevPaths = aStar.getPathsToTarget();
            if (newRevPaths.isEmpty()) {
                reversedPaths.add(newPath);
            }else{
                List joinedPaths = new ArrayList<>();
                for(GraphPath newRevPath : newRevPaths){
                    LOG.debug("REV Dep time: " + new Date(newRevPath.getStartTime() * 1000));
                    LOG.debug("REV Arr time: " + new Date(newRevPath.getEndTime() * 1000));
                    List concatenatedPaths = Arrays.asList(newRevPath, walkPath);
                    if(options.arriveBy){
                        Collections.reverse(concatenatedPaths);
                    }
                    GraphPath joinedPath = joinPaths(concatenatedPaths);

                    if((!options.arriveBy && joinedPath.states.getFirst().getTimeInMillis() > options.dateTime * 1000) ||
                            (options.arriveBy && joinedPath.states.getLast().getTimeInMillis() < options.dateTime * 1000)){
                        joinedPaths.add(joinedPath);
                        if(newPaths.size() > 1){
                            for (AgencyAndId tripId : joinedPath.getTrips()) {
                                options.banTrip(tripId);
                            }
                        }
                    }
                }
                reversedPaths.addAll(joinedPaths);
            }
        }
        return reversedPaths.isEmpty() ? newPaths : reversedPaths;
    }



    private RoutingRequest createReversedTransitRequest(RoutingRequest originalReq, RoutingRequest options, Vertex fromVertex,
                                                 Vertex toVertex, long arrDepTime, RemainingWeightHeuristic remainingWeightHeuristic){

        RoutingRequest request = createReversedRequest(originalReq, options, fromVertex, toVertex,
                arrDepTime, new EuclideanRemainingWeightHeuristic());
        if((originalReq.parkAndRide || originalReq.kissAndRide) && !originalReq.arriveBy){
            request.parkAndRide = false;
            request.kissAndRide = false;
            request.modes.setCar(false);
        }
        request.maxWalkDistance = CLAMP_MAX_WALK;
        return request;
    }

    private RoutingRequest createReversedMainRequest(RoutingRequest originalReq, RoutingRequest options, Vertex fromVertex,
                                                        Vertex toVertex, long dateTime, RemainingWeightHeuristic remainingWeightHeuristic){
        RoutingRequest request = createReversedRequest(originalReq, options, fromVertex,
                toVertex, dateTime, remainingWeightHeuristic);
        if((originalReq.parkAndRide || originalReq.kissAndRide) && originalReq.arriveBy){
            request.parkAndRide = false;
            request.kissAndRide = false;
            request.modes.setCar(false);
        }
        return request;

    }

    private RoutingRequest createReversedRequest(RoutingRequest originalReq, RoutingRequest options, Vertex fromVertex,
                                                 Vertex toVertex, long dateTime, RemainingWeightHeuristic remainingWeightHeuristic){
        RoutingRequest reversedOptions = originalReq.clone();
        reversedOptions.dateTime = dateTime;
        reversedOptions.setArriveBy(!originalReq.arriveBy);
        reversedOptions.setRoutingContext(router.graph, fromVertex, toVertex);
        reversedOptions.dominanceFunction = new DominanceFunction.MinimumWeight();
        reversedOptions.rctx.remainingWeightHeuristic = remainingWeightHeuristic;
        reversedOptions.maxTransfers = 4;
        reversedOptions.longDistance = true;
        reversedOptions.bannedTrips = options.bannedTrips;
        return reversedOptions;
    }

    /* Try to find N paths through the Graph */
    public List graphPathFinderEntryPoint (RoutingRequest request) {

        // We used to perform a protective clone of the RoutingRequest here.
        // There is no reason to do this if we don't modify the request.
        // Any code that changes them should be performing the copy!

        List paths = null;
        try {
            paths = getGraphPathsConsideringIntermediates(request);
            if (paths == null && request.wheelchairAccessible) {
                // There are no paths that meet the user's slope restrictions.
                // Try again without slope restrictions, and warn the user in the response.
                RoutingRequest relaxedRequest = request.clone();
                relaxedRequest.maxSlope = Double.MAX_VALUE;
                request.rctx.slopeRestrictionRemoved = true;
                paths = getGraphPathsConsideringIntermediates(relaxedRequest);
            }
            request.rctx.debugOutput.finishedCalculating();
        } catch (VertexNotFoundException e) {
            LOG.info("Vertex not found: " + request.from + " : " + request.to);
            throw e;
        }

        // Detect and report that most obnoxious of bugs: path reversal asymmetry.
        // Removing paths might result in an empty list, so do this check before the empty list check.
        if (paths != null) {
            Iterator gpi = paths.iterator();
            while (gpi.hasNext()) {
                GraphPath graphPath = gpi.next();
                // TODO check, is it possible that arriveBy and time are modifed in-place by the search?
                if (request.arriveBy) {
                    if (graphPath.states.getLast().getTimeSeconds() > request.dateTime) {
                        LOG.error("A graph path arrives after the requested time. This implies a bug.");
                        gpi.remove();
                    }
                } else {
                    if (graphPath.states.getFirst().getTimeSeconds() < request.dateTime) {
                        LOG.error("A graph path leaves before the requested time. This implies a bug.");
                        gpi.remove();
                    }
                }
            }
        }

        if (paths == null || paths.size() == 0) {
            LOG.debug("Path not found: " + request.from + " : " + request.to);
            request.rctx.debugOutput.finishedRendering(); // make sure we still report full search time
            throw new PathNotFoundException();
        }

        return paths;
    }

    /**
     * Break up a RoutingRequest with intermediate places into separate requests, in the given order.
     *
     * If there are no intermediate places, issue a single request. Otherwise process the places
     * list [from, i1, i2, ..., to] either from left to right (if {@code request.arriveBy==false})
     * or from right to left (if {@code request.arriveBy==true}). In the latter case the order of
     * the requested subpaths is (i2, to), (i1, i2), and (from, i1) which has to be reversed at
     * the end.
     */
    private List getGraphPathsConsideringIntermediates (RoutingRequest request) {
        if (request.hasIntermediatePlaces()) {
            List places = Lists.newArrayList(request.from);
            places.addAll(request.intermediatePlaces);
            places.add(request.to);
            long time = request.dateTime;

            List paths = new ArrayList<>();
            DebugOutput debugOutput = null;
            int placeIndex = (request.arriveBy ? places.size() - 1 : 1);

            while (0 < placeIndex && placeIndex < places.size()) {
                RoutingRequest intermediateRequest = request.clone();
                intermediateRequest.setNumItineraries(1);
                intermediateRequest.dateTime = time;
                intermediateRequest.from = places.get(placeIndex - 1);
                intermediateRequest.to = places.get(placeIndex);
                intermediateRequest.rctx = null;
                intermediateRequest.setRoutingContext(router.graph);

                if (debugOutput != null) {// Restore the previous debug info accumulator
                    intermediateRequest.rctx.debugOutput = debugOutput;
                } else {// Store the debug info accumulator
                    debugOutput = intermediateRequest.rctx.debugOutput;
                }

                List partialPaths = getPaths(intermediateRequest);
                if (partialPaths.size() == 0) {
                    return partialPaths;
                }

                GraphPath path = partialPaths.get(0);
                paths.add(path);
                time = (request.arriveBy ? path.getStartTime() : path.getEndTime());
                placeIndex += (request.arriveBy ? -1 : +1);
            }
            request.setRoutingContext(router.graph);
            request.rctx.debugOutput = debugOutput;
            if (request.arriveBy) {
                Collections.reverse(paths);
            }
            return Collections.singletonList(joinPaths(paths));
        } else {
            return getPaths(request);
        }
    }

    private static GraphPath joinPaths(List paths) {
        State lastState = paths.get(0).states.getLast();
        GraphPath newPath = new GraphPath(lastState, false);
        Vertex lastVertex = lastState.getVertex();

        //With more paths we should allow more transfers
        lastState.getOptions().maxTransfers *= paths.size();

        for (GraphPath path : paths.subList(1, paths.size())) {
            lastState = newPath.states.getLast();
            // add a leg-switching state
            LegSwitchingEdge legSwitchingEdge = new LegSwitchingEdge(lastVertex, lastVertex);
            lastState = legSwitchingEdge.traverse(lastState);
            newPath.edges.add(legSwitchingEdge);
            newPath.states.add(lastState);
            // add the next subpath
            for (Edge e : path.edges) {
                lastState = e.traverse(lastState);
                newPath.edges.add(e);
                newPath.states.add(lastState);
            }
            lastVertex = path.getEndVertex();
        }
        return newPath;
    }

/*
    TODO reimplement
    This should probably be done with a special value in the departure/arrival time.

    public static TripPlan generateFirstTrip(RoutingRequest request) {
        request.setArriveBy(false);

        TimeZone tz = graph.getTimeZone();

        GregorianCalendar calendar = new GregorianCalendar(tz);
        calendar.setTimeInMillis(request.dateTime * 1000);
        calendar.set(Calendar.HOUR, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.AM_PM, 0);
        calendar.set(Calendar.SECOND, graph.index.overnightBreak);

        request.dateTime = calendar.getTimeInMillis() / 1000;
        return generate(request);
    }

    public static TripPlan generateLastTrip(RoutingRequest request) {
        request.setArriveBy(true);

        TimeZone tz = graph.getTimeZone();

        GregorianCalendar calendar = new GregorianCalendar(tz);
        calendar.setTimeInMillis(request.dateTime * 1000);
        calendar.set(Calendar.HOUR, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.AM_PM, 0);
        calendar.set(Calendar.SECOND, graph.index.overnightBreak);
        calendar.add(Calendar.DAY_OF_YEAR, 1);

        request.dateTime = calendar.getTimeInMillis() / 1000;

        return generate(request);
    }
*/

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy