/
RoutingContext.java
436 lines (370 loc) · 19.1 KB
/
RoutingContext.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
/* 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.TemporaryEdge;
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.*;
/**
* 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;
public Collection<TemporaryEdge> temporaryEdges = new ArrayList<>();
public Collection<Vertex> temporaryVertices = new ArrayList<>();
/* 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()));
if (opt.tripDiscoveryMode) {
ServiceDate sd = serviceDate;
for (int i = 0; i < 15; i++) {
sd = opt.arriveBy ? sd.previous() : sd.next();
addIfNotExists(this.serviceDays, new ServiceDay(graph, sd, 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();
LOG.debug("ms Cleaning up {} temporary edges", temporaryEdges.size());
temporaryEdges.forEach(TemporaryEdge::dispose);
temporaryEdges.clear();
temporaryVertices.forEach(graph::remove);
temporaryVertices.clear();
}
}