Skip to content

Commit

Permalink
Calculate one-to-many time surfaces in Modeify profile router
Browse files Browse the repository at this point in the history
(uses the cached distances from stops to streets)
Catch missing to-coordinates in ProfileResource
Error message when analyst is turned off
Switch properly between the two profile routers for one-to-many
  • Loading branch information
abyrd committed Jan 28, 2015
1 parent 1bfa385 commit 0bfdfad
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 92 deletions.
39 changes: 39 additions & 0 deletions src/main/java/org/opentripplanner/analyst/TimeSurface.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ public TimeSurface (AnalystProfileRouterPrototype profileRouter) {
cutoffMinutes = profileRouter.MAX_DURATION / 60;
}

/** Make a max or min timesurface from propagated times in a ProfileRouter. */
public TimeSurface (ProfileRouter profileRouter) {
// TODO merge with the version that takes AnalystProfileRouterPrototype, they are exactly the same.
// But those two classes are not in the same inheritance hierarchy.
ProfileRequest req = profileRouter.request;
lon = req.fromLon;
lat = req.fromLat;
id = makeUniqueId();
dateTime = req.fromTime; // FIXME
routerId = profileRouter.graph.routerId;
cutoffMinutes = profileRouter.MAX_DURATION / 60;
}

public static TimeSurface.RangeSet makeSurfaces (AnalystProfileRouterPrototype profileRouter) {
TimeSurface minSurface = new TimeSurface(profileRouter);
TimeSurface avgSurface = new TimeSurface(profileRouter);
Expand All @@ -115,6 +128,32 @@ public static TimeSurface.RangeSet makeSurfaces (AnalystProfileRouterPrototype p
return result;
}

public static RangeSet makeSurfaces(ProfileRouter profileRouter, TObjectIntMap<Vertex> lbs, TObjectIntMap<Vertex> ubs) {
TimeSurface minSurface = new TimeSurface(profileRouter);
TimeSurface avgSurface = new TimeSurface(profileRouter);
TimeSurface maxSurface = new TimeSurface(profileRouter);
for (Vertex v : lbs.keySet()) {
int min = lbs.get(v);
int max = ubs.get(v);
int avg = UNREACHABLE;
if (min != UNREACHABLE && max != UNREACHABLE) {
avg = (int)((long)min + max / 2);
}
minSurface.times.put(v, min);
avgSurface.times.put(v, avg);
maxSurface.times.put(v, max);
}
// TODO merge with the version that takes AnalystProfileRouterPrototype, they are mostly the same.
RangeSet result = new RangeSet();
minSurface.description = "Travel times assuming best luck (never waiting for a transfer).";
avgSurface.description = "Expected travel times (average wait for every transfer).";
maxSurface.description = "Travel times assuming worst luck (maximum wait for every transfer).";
result.min = minSurface;
result.avg = avgSurface;
result.max = maxSurface;
return result;
}

/** Groups together three TimeSurfaces as a single response for profile-analyst. */
public static class RangeSet {
public TimeSurface min;
Expand Down
44 changes: 29 additions & 15 deletions src/main/java/org/opentripplanner/api/resource/ProfileResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import org.opentripplanner.standalone.Router;

import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.java2d.cmm.Profile;

/**
* A Jersey resource class which exposes OTP profile routing functionality as a web service.
Expand All @@ -37,6 +40,7 @@
@Path("routers/{routerId}/profile")
public class ProfileResource {

private static final Logger LOG = LoggerFactory.getLogger(ProfileResource.class);
private Graph graph;
private SurfaceCache surfaceCache;

Expand Down Expand Up @@ -88,8 +92,11 @@ public Response profileRoute (
ProfileRequest req = new ProfileRequest();
req.fromLat = from.lat;
req.fromLon = from.lon;
req.toLat = to.lat;
req.toLon = to.lon;
// In analyst requests the 'to' coordinates may be null.
// We need to provide some value though because lower-level routing requests are intolerant of a missing 'to'.
if (to == null) to = from;
req.toLat = to.lat;
req.toLon = to.lon;
req.fromTime = fromTime.toSeconds();
req.toTime = toTime.toSeconds();
req.walkSpeed = walkSpeed;
Expand All @@ -111,10 +118,26 @@ public Response profileRoute (
req.minCarTime = minCarTime;
req.suboptimalMinutes = suboptimalMinutes;

/* Use the new prototype faster profile-analyst. Really this should be constrained to freq-only cases. */
if (req.analyst) {
AnalystProfileRouterPrototype router = new AnalystProfileRouterPrototype(graph, req);
TimeSurface.RangeSet result = router.route();
if (surfaceCache == null) {
LOG.error ("You must run OTP with the --analyst option to enable spatial analysis features.");
}
TimeSurface.RangeSet result;

if (graph.hasFrequencyService && ! graph.hasScheduledService) {
/* Use the new prototype profile-analyst for frequency-only cases. */
AnalystProfileRouterPrototype router = new AnalystProfileRouterPrototype(graph, req);
result = router.route();
} else {
/* Use the Modeify profile router for the general case. */
ProfileRouter router = new ProfileRouter(graph, req);
try {
router.route();
result = router.timeSurfaceRangeSet;
} finally {
router.cleanup();
}
}
Map<String, Integer> idForSurface = Maps.newHashMap();
idForSurface.put("min", surfaceCache.add(result.min)); // requires analyst mode turned on
idForSurface.put("avg", surfaceCache.add(result.avg));
Expand All @@ -124,16 +147,7 @@ public Response profileRoute (
ProfileRouter router = new ProfileRouter(graph, req);
try {
ProfileResponse response = router.route();
if (req.analyst) {
surfaceCache.add(router.minSurface);
surfaceCache.add(router.maxSurface);
List<TimeSurfaceShort> surfaceShorts = Lists.newArrayList();
surfaceShorts.add(new TimeSurfaceShort(router.minSurface));
surfaceShorts.add(new TimeSurfaceShort(router.maxSurface));
return Response.status(Status.OK).entity(surfaceShorts).build();
} else {
return Response.status(Status.OK).entity(response).build();
}
return Response.status(Status.OK).entity(response).build();
} finally {
router.cleanup(); // destroy routing contexts even when an exception happens
}
Expand Down
128 changes: 51 additions & 77 deletions src/main/java/org/opentripplanner/profile/ProfileRouter.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.opentripplanner.profile;

import com.google.common.collect.*;
import gnu.trove.iterator.TObjectIntIterator;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import org.onebusaway.gtfs.model.Stop;
import org.opentripplanner.analyst.TimeSurface;
import org.opentripplanner.api.resource.SimpleIsochrone;
Expand Down Expand Up @@ -62,9 +65,8 @@ public ProfileRouter(Graph graph, ProfileRequest request) {
Multimap<StopCluster, StopAtDistance> fromStopPaths, toStopPaths; // ways to reach each origin or dest stop cluster
List<RoutingContext> routingContexts = Lists.newArrayList();

/* Analyst: time bounds for each vertex */
public int[] mins, maxs;
public TimeSurface minSurface, maxSurface;
/* Analyst: time bounds for each vertex. This field contains the output after the search is run. */
public TimeSurface.RangeSet timeSurfaceRangeSet = null;

// while finding direct paths:
// if dist < M meters OR we don't yet have N stations: record station
Expand Down Expand Up @@ -101,13 +103,6 @@ public ProfileResponse route () {
}
}
}
// Analyst
if (request.analyst) {
mins = new int[Vertex.getMaxIndex()];
maxs = new int[Vertex.getMaxIndex()];
Arrays.fill(mins, TimeSurface.UNREACHABLE);
Arrays.fill(maxs, TimeSurface.UNREACHABLE);
}
LOG.info("access modes: {}", request.accessModes);
LOG.info("egress modes: {}", request.egressModes);
LOG.info("direct modes: {}", request.directModes);
Expand Down Expand Up @@ -212,20 +207,8 @@ public ProfileResponse route () {
continue;
}
if ( ! addIfNondominated(r1)) continue; // abandon this ride if it is dominated by some existing ride at the same location
// We have a new, nondominated, completed ride. Record its lower and upper bounds at the arrival stop.
if (request.analyst) {
for (Stop s : r1.to.children) {
// TODO This could be done at the end now that we are retaining all rides.
TransitStop tstop = graph.index.stopVertexForStop.get(s);
int tsidx = tstop.getIndex();
int lb = r1.durationLowerBound();
int ub = r1.durationUpperBound();
if (mins[tsidx] == TimeSurface.UNREACHABLE || mins[tsidx] > lb)
mins[tsidx] = lb;
if (maxs[tsidx] == TimeSurface.UNREACHABLE || maxs[tsidx] > ub) // Yes, we want the _minimum_ upper bound.
maxs[tsidx] = ub;
}
}
// We have a new, nondominated, completed ride.

/* Find transfers out of this new ride. */
// Do not transfer too many times. Check after calculating stats since stats are needed in any case.
int nRides = r1.pathLength();
Expand Down Expand Up @@ -515,59 +498,6 @@ private void findStreetOption(TraverseMode mode) {
routingContexts.add(rr.rctx); // save context for later cleanup so temp edges remain available
}

// Major change: This needs to include all stops, not just those where transfers occur or those near the destination.
/** Make two time surfaces, one for the minimum and one for the maximum. */
public P2<TimeSurface> makeSurfaces() {
LOG.info("Propagating profile router result to street vertices.");
// Make a normal OTP routing request so we can traverse edges and use GenericAStar
RoutingRequest rr = new RoutingRequest(TraverseMode.WALK);
rr.setMode(TraverseMode.WALK);
rr.walkSpeed = request.walkSpeed;
// If max trip duration is not limited, searches are of course much slower.
int worstElapsedTime = request.maxWalkTime * 60; // convert from minutes to seconds, assume walking at egress
rr.worstTime = (rr.dateTime + worstElapsedTime);
rr.batch = (true);
GenericAStar astar = new GenericAStar();
rr.setNumItineraries(1);
for (TransitStop tstop : graph.index.stopVertexForStop.values()) {
int index = tstop.getIndex();
// Generate a tree outward from all stops that have been touched in the basic profile search
if (mins[index] == TimeSurface.UNREACHABLE || maxs[index] == TimeSurface.UNREACHABLE) continue;
rr.setRoutingContext(graph, tstop, null); // Set origin vertex directly instead of generating link edges
astar.setTraverseVisitor(new ExtremaPropagationTraverseVisitor(mins[index], maxs[index]));
ShortestPathTree spt = astar.getShortestPathTree(rr, 5);
rr.rctx.destroy();
}
/* DISABLED while working on analyst-frequency-based-profile prototype */
// minSurface = new TimeSurface(this, false);
// maxSurface = new TimeSurface(this, true);
LOG.info("done making timesurfaces.");
return new P2<TimeSurface>(minSurface, maxSurface);
}

/** Given a minimum and maximum at a starting vertex, build an on-street SPT and propagate those values outward. */
class ExtremaPropagationTraverseVisitor implements TraverseVisitor {
final int min0;
final int max0;
ExtremaPropagationTraverseVisitor(int min0, int max0) {
this.min0 = min0;
this.max0 = max0;
}
@Override public void visitEdge(Edge edge, State state) { }
@Override public void visitEnqueue(State state) { }
@Override public void visitVertex(State state) {
int min = min0 + (int) state.getElapsedTimeSeconds();
int max = max0 + (int) state.getElapsedTimeSeconds();
Vertex vertex = state.getVertex();
int index = vertex.getIndex();
if (index >= mins.length) return; // New temp vertices may have been created since the array was dimensioned.
if (mins[index] == TimeSurface.UNREACHABLE || mins[index] > min)
mins[index] = min;
if (maxs[index] == TimeSurface.UNREACHABLE || maxs[index] > max) // Yes we want the minimum upper bound (minimum maximum)
maxs[index] = max;
}
}

/**
* Destroy all routing contexts created during this search. This method must be called manually on any
* ProfileRouter instance before it is released for garbage collection, because RoutingContexts remain linked into
Expand Down Expand Up @@ -596,4 +526,48 @@ public void finalize() {
}
}

private void makeSurfaces() {
LOG.info("Propagating from transit stops to the street network...");
// A map to store the travel time to each vertex
TObjectIntMap<Vertex> lbs = new TObjectIntHashMap<>(1000000, 0.5f, TimeSurface.UNREACHABLE);
TObjectIntMap<Vertex> ubs = new TObjectIntHashMap<>(1000000, 0.5f, TimeSurface.UNREACHABLE);
// Grab a cached map of distances to street intersections from each transit stop
StopTreeCache stopTreeCache = graph.index.getStopTreeCache();
// Iterate over all nondominated rides at all clusters
for (Entry<StopCluster, Ride> entry : retainedRides.entries()) {
StopCluster cluster = entry.getKey();
Ride ride = entry.getValue();
int lb0 = ride.durationLowerBound();
int ub0 = ride.durationUpperBound();
for (Stop stop : cluster.children) {
TransitStop tstop = graph.index.stopVertexForStop.get(stop);
// Iterate over street intersections in the vicinity of this particular transit stop.
// Shift the time range at this transit stop, merging it into that for all reachable street intersections.
TObjectIntMap<Vertex> distanceToVertex = stopTreeCache.getDistancesForStop(tstop);
for (TObjectIntIterator<Vertex> iter = distanceToVertex.iterator(); iter.hasNext(); ) {
iter.advance();
Vertex vertex = iter.key();
// distance in meters over walkspeed in meters per second --> seconds
int egressWalkTimeSeconds = (int) (iter.value() / request.walkSpeed);
if (egressWalkTimeSeconds > request.maxWalkTime * 60) {
continue;
}
int propagated_lb = lb0 + egressWalkTimeSeconds;
int propagated_ub = ub0 + egressWalkTimeSeconds;
int existing_lb = lbs.get(vertex);
int existing_ub = ubs.get(vertex);
if (existing_lb == TimeSurface.UNREACHABLE || existing_lb > propagated_lb) {
lbs.put(vertex, propagated_lb);
}
if (existing_ub == TimeSurface.UNREACHABLE || existing_ub > propagated_ub) {
ubs.put(vertex, propagated_ub);
}
}
}
}
LOG.info("Done with propagation.");
/* Store the result in a field in the router object. */
timeSurfaceRangeSet = TimeSurface.makeSurfaces(this, lbs, ubs);
}

}

0 comments on commit 0bfdfad

Please sign in to comment.