/* 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.routing.core; import com.google.common.collect.Iterables; import com.vividsolutions.jts.geom.LineString; import org.onebusaway.gtfs.model.Agency; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Stop; import org.onebusaway.gtfs.model.calendar.ServiceDate; import org.onebusaway.gtfs.services.calendar.CalendarService; import org.opentripplanner.api.resource.DebugOutput; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.routing.algorithm.strategies.EuclideanRemainingWeightHeuristic; import org.opentripplanner.routing.algorithm.strategies.RemainingWeightHeuristic; import org.opentripplanner.routing.algorithm.strategies.TrivialRemainingWeightHeuristic; import org.opentripplanner.routing.edgetype.StreetEdge; import org.opentripplanner.routing.edgetype.TemporaryPartialStreetEdge; import org.opentripplanner.routing.edgetype.TimetableSnapshot; import org.opentripplanner.routing.error.GraphNotFoundException; import org.opentripplanner.routing.error.TransitTimesException; import org.opentripplanner.routing.error.VertexNotFoundException; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.location.StreetLocation; import org.opentripplanner.routing.location.TemporaryStreetLocation; import org.opentripplanner.routing.services.OnBoardDepartService; import org.opentripplanner.routing.vertextype.TemporaryVertex; import org.opentripplanner.routing.vertextype.TransitStop; import org.opentripplanner.traffic.StreetSpeedSnapshot; import org.opentripplanner.updater.stoptime.TimetableSnapshotSource; import org.opentripplanner.util.NonLocalizedString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * A RoutingContext holds information needed to carry out a search for a particular TraverseOptions, on a specific graph. * Includes things like (temporary) endpoint vertices, transfer tables, service day caches, etc. * * In addition, while the RoutingRequest should only carry parameters _in_ to the routing operation, the routing context * should be used to carry information back out, such as debug figures or flags that certain thresholds have been exceeded. */ public class RoutingContext implements Cloneable { private static final Logger LOG = LoggerFactory.getLogger(RoutingContext.class); /* FINAL FIELDS */ public RoutingRequest opt; // not final so we can reverse-clone public final Graph graph; public final Vertex fromVertex; public final Vertex toVertex; // origin means "where the initial state will be located" not "the beginning of the trip from the user's perspective" public final Vertex origin; // target means "where this search will terminate" not "the end of the trip from the user's perspective" public final Vertex target; // The back edge associated with the origin - i.e. continuing a previous search. // NOTE: not final so that it can be modified post-construction for testing. // TODO(flamholz): figure out a better way. public Edge originBackEdge; // public final Calendar calendar; public final CalendarService calendarService; public final Map<AgencyAndId, Set<ServiceDate>> serviceDatesByServiceId = new HashMap<AgencyAndId, Set<ServiceDate>>(); public RemainingWeightHeuristic remainingWeightHeuristic; public final TransferTable transferTable; /** The timetableSnapshot is a {@link TimetableSnapshot} for looking up real-time updates. */ public final TimetableSnapshot timetableSnapshot; /** A snapshot of street speeds for looking up real-time or historical traffic data */ public final StreetSpeedSnapshot streetSpeedSnapshot; /** * Cache lists of which transit services run on which midnight-to-midnight periods. This ties a TraverseOptions to a particular start time for the * duration of a search so the same options cannot be used for multiple searches concurrently. To do so this cache would need to be moved into * StateData, with all that entails. */ public ArrayList<ServiceDay> serviceDays; /** * The search will be aborted if it is still running after this time (in milliseconds since the epoch). A negative or zero value implies no limit. * This provides an absolute timeout, whereas the maxComputationTime is relative to the beginning of an individual search. While the two might * seem equivalent, we trigger search retries in various places where it is difficult to update relative timeout value. The earlier of the two * timeouts is applied. */ public long searchAbortTime = 0; public Vertex startingStop; /** An object that accumulates profiling and debugging info for inclusion in the response. */ public DebugOutput debugOutput = new DebugOutput(); /** Indicates that the search timed out or was otherwise aborted. */ public boolean aborted; /** Indicates that a maximum slope constraint was specified but was removed during routing to produce a result. */ public boolean slopeRestrictionRemoved = false; /* CONSTRUCTORS */ /** * Constructor that automatically computes origin/target from RoutingRequest. */ public RoutingContext(RoutingRequest routingRequest, Graph graph) { this(routingRequest, graph, null, null, true); } /** * Constructor that takes to/from vertices as input. */ public RoutingContext(RoutingRequest routingRequest, Graph graph, Vertex from, Vertex to) { this(routingRequest, graph, from, to, false); } /** * Returns the StreetEdges that overlap between two vertices edge sets. */ private Set<StreetEdge> overlappingStreetEdges(Vertex u, Vertex v) { Set<Integer> vIds = new HashSet<Integer>(); Set<Integer> uIds = new HashSet<Integer>(); for (Edge e : Iterables.concat(v.getIncoming(), v.getOutgoing())) { vIds.add(e.getId()); } for (Edge e : Iterables.concat(u.getIncoming(), u.getOutgoing())) { uIds.add(e.getId()); } // Intesection of edge IDs between u and v. uIds.retainAll(vIds); Set<Integer> overlappingIds = uIds; // Fetch the edges by ID - important so we aren't stuck with temporary edges. Set<StreetEdge> overlap = new HashSet<>(); for (Integer id : overlappingIds) { Edge e = graph.getEdgeById(id); if (e == null || !(e instanceof StreetEdge)) { continue; } overlap.add((StreetEdge) e); } return overlap; } /** * Creates a PartialStreetEdge along the input StreetEdge iff its direction makes this possible. */ private void makePartialEdgeAlong(StreetEdge streetEdge, TemporaryStreetLocation from, TemporaryStreetLocation to) { LineString parent = streetEdge.getGeometry(); LineString head = GeometryUtils.getInteriorSegment(parent, streetEdge.getFromVertex().getCoordinate(), from.getCoordinate()); LineString tail = GeometryUtils.getInteriorSegment(parent, to.getCoordinate(), streetEdge.getToVertex().getCoordinate()); if (parent.getLength() > head.getLength() + tail.getLength()) { LineString partial = GeometryUtils.getInteriorSegment(parent, from.getCoordinate(), to.getCoordinate()); double lengthRatio = partial.getLength() / parent.getLength(); double length = streetEdge.getDistance() * lengthRatio; //TODO: localize this String name = from.getLabel() + " to " + to.getLabel(); new TemporaryPartialStreetEdge(streetEdge, from, to, partial, new NonLocalizedString(name), length); } } /** * Flexible constructor which may compute to/from vertices. * * TODO(flamholz): delete this flexible constructor and move the logic to constructors above appropriately. * * @param findPlaces if true, compute origin and target from RoutingRequest using spatial indices. */ private RoutingContext(RoutingRequest routingRequest, Graph graph, Vertex from, Vertex to, boolean findPlaces) { if (graph == null) { throw new GraphNotFoundException(); } this.opt = routingRequest; this.graph = graph; this.debugOutput.startedCalculating(); // The following block contains potentially resource-intensive things that are only relevant for transit. // In normal searches the impact is low, because the routing context is only constructed once at the beginning // of the search, but when computing transfers or doing large batch jobs, repeatedly re-constructing useless // transit-specific information can have an impact. if (opt.modes.isTransit()) { // the graph's snapshot may be frequently updated. // Grab a reference to ensure a coherent view of the timetables throughout this search. if (routingRequest.ignoreRealtimeUpdates) { timetableSnapshot = null; } else { TimetableSnapshotSource timetableSnapshotSource = graph.timetableSnapshotSource; if (timetableSnapshotSource == null) { timetableSnapshot = null; } else { timetableSnapshot = timetableSnapshotSource.getTimetableSnapshot(); } } calendarService = graph.getCalendarService(); setServiceDays(); } else { timetableSnapshot = null; calendarService = null; } // do the same for traffic if (graph.streetSpeedSource != null) this.streetSpeedSnapshot = graph.streetSpeedSource.getSnapshot(); else this.streetSpeedSnapshot = null; Edge fromBackEdge = null; Edge toBackEdge = null; if (findPlaces) { if (opt.batch) { // batch mode: find an OSM vertex, don't split // We do this so that we are always linking to the same thing in analyst mode // even if the transit network has changed. // TODO offset time by distance to nearest OSM node? if (opt.arriveBy) { // TODO what if there is no coordinate but instead a named place? toVertex = graph.streetIndex.getSampleVertexAt(opt.to.getCoordinate(), true); fromVertex = null; } else { fromVertex = graph.streetIndex.getSampleVertexAt(opt.from.getCoordinate(), false); toVertex = null; } } else { // normal mode, search for vertices based RoutingRequest and split streets toVertex = graph.streetIndex.getVertexForLocation(opt.to, opt, true); if (opt.to.hasEdgeId()) { toBackEdge = graph.getEdgeById(opt.to.edgeId); } if (opt.startingTransitTripId != null && !opt.arriveBy) { // Depart on-board mode: set the from vertex to "on-board" state OnBoardDepartService onBoardDepartService = graph.getService(OnBoardDepartService.class); if (onBoardDepartService == null) throw new UnsupportedOperationException("Missing OnBoardDepartService"); fromVertex = onBoardDepartService.setupDepartOnBoard(this); } else { fromVertex = graph.streetIndex.getVertexForLocation(opt.from, opt, false); if (opt.from.hasEdgeId()) { fromBackEdge = graph.getEdgeById(opt.from.edgeId); } } } } else { // debug mode, force endpoint vertices to those specified rather than searching fromVertex = from; toVertex = to; } // If the from and to vertices are generated and lie on some of the same edges, we need to wire them // up along those edges so that we don't get odd circuitous routes for really short trips. // TODO(flamholz): seems like this might be the wrong place for this code? Can't find a better one. // if (fromVertex instanceof TemporaryStreetLocation && toVertex instanceof TemporaryStreetLocation) { TemporaryStreetLocation fromStreetVertex = (TemporaryStreetLocation) fromVertex; TemporaryStreetLocation toStreetVertex = (TemporaryStreetLocation) toVertex; Set<StreetEdge> overlap = overlappingStreetEdges(fromStreetVertex, toStreetVertex); for (StreetEdge pse : overlap) { makePartialEdgeAlong(pse, fromStreetVertex, toStreetVertex); } } if (opt.startingTransitStopId != null) { Stop stop = graph.index.stopForId.get(opt.startingTransitStopId); TransitStop tstop = graph.index.stopVertexForStop.get(stop); startingStop = tstop.departVertex; } origin = opt.arriveBy ? toVertex : fromVertex; originBackEdge = opt.arriveBy ? toBackEdge : fromBackEdge; target = opt.arriveBy ? fromVertex : toVertex; transferTable = graph.getTransferTable(); if (opt.batch) remainingWeightHeuristic = new TrivialRemainingWeightHeuristic(); else remainingWeightHeuristic = new EuclideanRemainingWeightHeuristic(); if (this.origin != null) { LOG.debug("Origin vertex inbound edges {}", this.origin.getIncoming()); LOG.debug("Origin vertex outbound edges {}", this.origin.getOutgoing()); } // target is where search will terminate, can be origin or destination depending on arriveBy LOG.debug("Target vertex {}", this.target); if (this.target != null) { LOG.debug("Destination vertex inbound edges {}", this.target.getIncoming()); LOG.debug("Destination vertex outbound edges {}", this.target.getOutgoing()); } } /* INSTANCE METHODS */ public void check() { ArrayList<String> notFound = new ArrayList<String>(); // check origin present when not doing an arrive-by batch search if (!(opt.batch && opt.arriveBy)) if (fromVertex == null) notFound.add("from"); // check destination present when not doing a depart-after batch search if (!opt.batch || opt.arriveBy) { if (toVertex == null) { notFound.add("to"); } } if (notFound.size() > 0) { throw new VertexNotFoundException(notFound); } if (opt.modes.isTransit() && !graph.transitFeedCovers(opt.dateTime)) { // user wants a path through the transit network, // but the date provided is outside those covered by the transit feed. throw new TransitTimesException(); } } /** * Cache ServiceDay objects representing which services are running yesterday, today, and tomorrow relative to the search time. This information * is very heavily used (at every transit boarding) and Date operations were identified as a performance bottleneck. Must be called after the * TraverseOptions already has a CalendarService set. */ private void setServiceDays() { Calendar c = Calendar.getInstance(); c.setTime(new Date(opt.getSecondsSinceEpoch() * 1000)); c.setTimeZone(graph.getTimeZone()); final ServiceDate serviceDate = new ServiceDate(c); this.serviceDays = new ArrayList<ServiceDay>(3); if (calendarService == null && graph.getCalendarService() != null && (opt.modes == null || opt.modes.contains(TraverseMode.TRANSIT))) { LOG.warn("RoutingContext has no CalendarService. Transit will never be boarded."); return; } for (String feedId : graph.getFeedIds()) { for (Agency agency : graph.getAgencies(feedId)) { addIfNotExists(this.serviceDays, new ServiceDay(graph, serviceDate.previous(), calendarService, agency.getId())); addIfNotExists(this.serviceDays, new ServiceDay(graph, serviceDate, calendarService, agency.getId())); addIfNotExists(this.serviceDays, new ServiceDay(graph, serviceDate.next(), calendarService, agency.getId())); } } } private static <T> void addIfNotExists(ArrayList<T> list, T item) { if (!list.contains(item)) { list.add(item); } } /** check if the start and end locations are accessible */ public boolean isAccessible() { if (opt.wheelchairAccessible) { return isWheelchairAccessible(fromVertex) && isWheelchairAccessible(toVertex); } return true; } // this could be handled by method overloading on Vertex public boolean isWheelchairAccessible(Vertex v) { if (v instanceof TransitStop) { TransitStop ts = (TransitStop) v; return ts.hasWheelchairEntrance(); } else if (v instanceof StreetLocation) { StreetLocation sl = (StreetLocation) v; return sl.isWheelchairAccessible(); } return true; } /** * Tear down this routing context, removing any temporary edges. */ public void destroy() { if (origin instanceof TemporaryVertex) ((TemporaryVertex) origin).dispose(); if (target instanceof TemporaryVertex) ((TemporaryVertex) target).dispose(); } }