/* 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.api.common;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.QueryParam;
import javax.xml.datatype.DatatypeConfigurationException;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.opentripplanner.routing.core.OptimizeType;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.TraverseModeSet;
import org.opentripplanner.routing.request.BannedStopSet;
import org.opentripplanner.routing.services.GraphService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jersey.api.core.InjectParam;
/**
* This class defines all the JAX-RS query parameters for a path search as fields, allowing them to
* be inherited by other REST resource classes (the trip planner and the Analyst WMS or tile
* resource). They will be properly included in API docs generated by Enunciate. This implies that
* the concrete REST resource subclasses will be request-scoped rather than singleton-scoped.
*
* @author abyrd
*/
public abstract class RoutingResource {
private static final Logger LOG = LoggerFactory.getLogger(RoutingResource.class);
/* TODO do not specify @DefaultValues here, so all defaults are handled in one place */
/** The start location -- either latitude, longitude pair in degrees or a Vertex
* label. For example, <code>40.714476,-74.005966</code> or
* <code>mtanyctsubway_A27_S</code>. */
@QueryParam("fromPlace") protected List<String> fromPlace;
/** The end location (see fromPlace for format). */
@QueryParam("toPlace") protected List<String> toPlace;
/** An unordered list of intermediate locations to be visited (see the fromPlace for format). */
@QueryParam("intermediatePlaces") protected List<String> intermediatePlaces;
/** Whether or not the order of intermediate locations is to be respected (TSP vs series). */
@DefaultValue("false") @QueryParam("intermediatePlacesOrdered") protected Boolean intermediatePlacesOrdered;
/** The date that the trip should depart (or arrive, for requests where arriveBy is true). */
@QueryParam("date") protected List<String> date;
/** The time that the trip should depart (or arrive, for requests where arriveBy is true). */
@QueryParam("time") protected List<String> time;
/** Router ID used when in multiple graph mode. Unused in singleton graph mode. */
@DefaultValue("") @QueryParam("routerId") protected List<String> routerId;
/** Whether the trip should depart or arrive at the specified date and time. */
@DefaultValue("false") @QueryParam("arriveBy") protected List<Boolean> arriveBy;
/** Whether the trip must be wheelchair accessible. */
@DefaultValue("false") @QueryParam("wheelchair") protected List<Boolean> wheelchair;
/** The maximum distance (in meters) the user is willing to walk. Defaults to unlimited. */
@QueryParam("maxWalkDistance") protected List<Double> maxWalkDistance;
/** How much worse walking is than waiting for an equivalent length of time, as a multiplier.
* Defaults to 2. */
@QueryParam("walkReluctance") protected List<Double> walkReluctance;
/** The user's walking speed in meters/second. Defaults to approximately 3 MPH. */
@QueryParam("walkSpeed") protected List<Double> walkSpeed;
/** The user's biking speed in meters/second. Defaults to approximately 11 MPH, or 9.5 for bikeshare. */
@QueryParam("bikeSpeed") protected List<Double> bikeSpeed;
/** For bike triangle routing, how much safety matters (range 0-1). */
@QueryParam("triangleSafetyFactor") protected List<Double> triangleSafetyFactor;
/** For bike triangle routing, how much slope matters (range 0-1). */
@QueryParam("triangleSlopeFactor") protected List<Double> triangleSlopeFactor;
/** For bike triangle routing, how much time matters (range 0-1). */
@QueryParam("triangleTimeFactor") protected List<Double> triangleTimeFactor;
/** The set of characteristics that the user wants to optimize for. @See OptimizeType */
@DefaultValue("QUICK") @QueryParam("optimize") protected List<OptimizeType> optimize;
/** The set of modes that a user is willing to use. */
@DefaultValue("TRANSIT,WALK") @QueryParam("mode") protected List<TraverseModeSet> modes;
/** The minimum time, in seconds, between successive trips on different vehicles.
* This is designed to allow for imperfect schedule adherence. This is a minimum;
* transfers over longer distances might use a longer time. */
@DefaultValue("-1") @QueryParam("minTransferTime") protected List<Integer> minTransferTime;
/** The maximum number of possible itineraries to return. */
@DefaultValue("-1") @QueryParam("numItineraries") protected List<Integer> numItineraries;
/**
* The list of preferred routes. The format is agency_[routename][_routeid], so TriMet_100 (100 is route short name) or Trimet__42 (two
* underscores, 42 is the route internal ID).
*/
@DefaultValue("") @QueryParam("preferredRoutes") protected List<String> preferredRoutes;
/** The maximum number of possible itineraries to return. */
@DefaultValue("-1") @QueryParam("otherThanPreferredRoutesPenalty") protected List<Integer> otherThanPreferredRoutesPenalty;
/** The comma-separated list of preferred agencies. */
@DefaultValue("") @QueryParam("preferredAgencies") protected List<String> preferredAgencies;
/**
* The list of unpreferred routes. The format is agency_[routename][_routeid], so TriMet_100 (100 is route short name) or Trimet__42 (two
* underscores, 42 is the route internal ID).
*/
@DefaultValue("") @QueryParam("unpreferredRoutes") protected List<String> unpreferredRoutes;
/** The comma-separated list of unpreferred agencies. */
@DefaultValue("") @QueryParam("unpreferredAgencies") protected List<String> unpreferredAgencies;
/** Whether intermediate stops -- those that the itinerary passes in a vehicle, but
* does not board or alight at -- should be returned in the response. For example,
* on a Q train trip from Prospect Park to DeKalb Avenue, whether 7th Avenue and
* Atlantic Avenue should be included. */
@DefaultValue("false") @QueryParam("showIntermediateStops") protected List<Boolean> showIntermediateStops;
/**
* The comma-separated list of banned routes. The format is agency_[routename][_routeid], so TriMet_100 (100 is route short name) or Trimet__42
* (two underscores, 42 is the route internal ID).
*/
@DefaultValue("") @QueryParam("bannedRoutes") protected List<String> bannedRoutes;
/** The comma-separated list of banned agencies. */
@DefaultValue("") @QueryParam("bannedAgencies") protected List<String> bannedAgencies;
/** The comma-separated list of banned trips. The format is agency_trip[:stop*], so:
* TriMet_24601 or TriMet_24601:0:1:2:17:18:19
*/
@DefaultValue("") @QueryParam("bannedTrips") protected List<String> bannedTrips;
/** The comma-separated list of banned stops. A stop is banned by ignoring its
* pre-board and pre-alight edges. This means the stop will be reachable via the
* street network, but can't be used to board or alight transit.
* The format is agencyId_stopId, so: TriMet_2107
*/
@DefaultValue("") @QueryParam("bannedStops") protected List<String> bannedStops;
/**
* An additional penalty added to boardings after the first. The value is in OTP's
* internal weight units, which are roughly equivalent to seconds. Set this to a high
* value to discourage transfers. Of course, transfers that save significant
* time or walking will still be taken.
*/
@DefaultValue("-1") @QueryParam("transferPenalty") protected List<Integer> transferPenalty;
/**
* An additional penalty added to boardings after the first when the transfer is not
* preferred. Preferred transfers also include timed transfers. The value is in OTP's
* internal weight units, which are roughly equivalent to seconds. Set this to a high
* value to discourage transfers that are not preferred. Of course, transfers that save
* significant time or walking will still be taken.
* When no preferred or timed transfer is defined, this value is ignored.
*/
@DefaultValue("-1") @QueryParam("nonpreferredTransferPenalty") protected List<Integer> nonpreferredTransferPenalty;
/** The maximum number of transfers (that is, one plus the maximum number of boardings)
* that a trip will be allowed. Larger values will slow performance, but could give
* better routes. This is limited on the server side by the MAX_TRANSFERS value in
* org.opentripplanner.api.ws.Planner. */
@DefaultValue("-1") @QueryParam("maxTransfers") protected List<Integer> maxTransfers;
/** If true, goal direction is turned off and a full path tree is built (specify only once) */
@DefaultValue("false") @QueryParam("batch") protected List<Boolean> batch;
/** A transit stop required to be the first stop in the search (AgencyId_StopId) */
@DefaultValue("") @QueryParam("startTransitStopId") protected List<String> startTransitStopId;
/** A transit trip acting as a starting "state" for depart-onboard routing (AgencyId_TripId) */
@DefaultValue("") @QueryParam("startTransitTripId") protected List<String> startTransitTripId;
/**
* When subtracting initial wait time, do not subtract more than this value, to prevent overly
* optimistic trips. Reasoning is that it is reasonable to delay a trip start 15 minutes to
* make a better trip, but that it is not reasonable to delay a trip start 15 hours; if that
* is to be done, the time needs to be included in the trip time. This number depends on the
* transit system; for transit systems where trips are planned around the vehicles, this number
* can be much higher. For instance, it's perfectly reasonable to delay one's trip 12 hours if
* one is taking a cross-country Amtrak train from Emeryville to Chicago. Has no effect in
* stock OTP, only in Analyst.
*
* A value of 0 means that initial wait time will not be subtracted out (will be clamped to 0).
* A value of -1 (the default) means that clamping is disabled, so any amount of initial wait
* time will be subtracted out.
*/
@DefaultValue("-1") @QueryParam("clampInitialWait")
protected List<Long> clampInitialWait;
/**
* If true, this trip will be reverse-optimized on the fly. Otherwise, reverse-optimization
* will occur once a trip has been chosen (in Analyst, it will not be done at all).
*/
@QueryParam("reverseOptimizeOnTheFly")
protected List<Boolean> reverseOptimizeOnTheFly;
@DefaultValue("-1") @QueryParam("boardSlack")
private List<Integer> boardSlack;
@DefaultValue("-1") @QueryParam("alightSlack")
private List<Integer> alightSlack;
@DefaultValue("en_US") @QueryParam("locale")
private List<String> locale;
/*
* somewhat ugly bug fix: the graphService is only needed here for fetching per-graph time zones.
* this should ideally be done when setting the routing context, but at present departure/
* arrival time is stored in the request as an epoch time with the TZ already resolved, and other
* code depends on this behavior. (AMB)
* Alternatively, we could eliminate the separate RoutingRequest objects and just resolve
* vertices and timezones here right away, but just ignore them in semantic equality checks.
*/
@InjectParam
protected GraphService graphService;
@InjectParam
protected RoutingRequest prototypeRoutingRequest;
/**
* Build the 0th Request object from the query parameter lists.
* @throws ParameterException when there is a problem interpreting a query parameter
*/
protected RoutingRequest buildRequest() throws ParameterException {
return buildRequest(0);
}
/**
* Range/sanity check the query parameter fields and build a Request object from them.
* @param n allows building several request objects from the same query parameters,
* re-specifying only those parameters that change from one request to the next.
* @throws ParameterException when there is a problem interpreting a query parameter
*/
protected RoutingRequest buildRequest(int n) throws ParameterException {
RoutingRequest request = prototypeRoutingRequest.clone();
request.setRouterId(get(routerId, n, request.getRouterId()));
request.setFromString(get(fromPlace, n, request.getFromPlace().getRepresentation()));
request.setToString(get(toPlace, n, request.getToPlace().getRepresentation()));
{
//FIXME: get defaults for these from request
String d = get(date, n, null);
String t = get(time, n, null);
TimeZone tz;
if (graphService != null) { // in tests it will be null
tz = graphService.getGraph(request.routerId).getTimeZone();
} else {
LOG.warn("no graph service available, using default timezone.");
tz = TimeZone.getDefault();
}
if (d == null && t != null) {
LOG.debug("parsing ISO datetime {}", t);
try { // Full ISO date in time param ?
request.setDateTime(javax.xml.datatype.DatatypeFactory.newInstance()
.newXMLGregorianCalendar(t).toGregorianCalendar().getTime());
} catch (DatatypeConfigurationException e) {
request.setDateTime(d, t, tz);
}
} else {
request.setDateTime(d, t, tz);
}
}
request.setWheelchairAccessible(get(wheelchair, n, request.isWheelchairAccessible()));
request.setNumItineraries(get(numItineraries, n, request.getNumItineraries()));
request.setMaxWalkDistance(get(maxWalkDistance, n, request.getMaxWalkDistance()));
request.setWalkReluctance(get(walkReluctance, n, request.getWalkReluctance()));
request.setWalkSpeed(get(walkSpeed, n, request.getWalkSpeed()));
double bikeSpeedParam = get(bikeSpeed, n, request.getBikeSpeed());
request.setBikeSpeed(bikeSpeedParam);
OptimizeType opt = get(optimize, n, request.getOptimize());
{
Double tsafe = get(triangleSafetyFactor, n, null);
Double tslope = get(triangleSlopeFactor, n, null);
Double ttime = get(triangleTimeFactor, n, null);
if (tsafe != null || tslope != null || ttime != null ) {
if (tsafe == null || tslope == null || ttime == null) {
throw new ParameterException(Message.UNDERSPECIFIED_TRIANGLE);
}
if (opt == null) {
opt = OptimizeType.TRIANGLE;
} else if (opt != OptimizeType.TRIANGLE) {
throw new ParameterException(Message.TRIANGLE_OPTIMIZE_TYPE_NOT_SET);
}
if (Math.abs(tsafe + tslope + ttime - 1) > Math.ulp(1) * 3) {
throw new ParameterException(Message.TRIANGLE_NOT_AFFINE);
}
request.setTriangleSafetyFactor(tsafe);
request.setTriangleSlopeFactor(tslope);
request.setTriangleTimeFactor(ttime);
} else if (opt == OptimizeType.TRIANGLE) {
throw new ParameterException(Message.TRIANGLE_VALUES_NOT_SET);
}
}
request.setArriveBy(get(arriveBy, n, false));
request.setShowIntermediateStops(get(showIntermediateStops, n, request.isShowIntermediateStops()));
/* intermediate places and their ordering are shared because they are themselves a list */
if (intermediatePlaces != null && intermediatePlaces.size() > 0
&& ! intermediatePlaces.get(0).equals("")) {
request.setIntermediatePlacesFromStrings(intermediatePlaces);
}
if (intermediatePlacesOrdered == null)
intermediatePlacesOrdered = request.isIntermediatePlacesOrdered();
request.setIntermediatePlacesOrdered(intermediatePlacesOrdered);
request.setPreferredRoutes(get(preferredRoutes, n, request.getPreferredRouteStr()));
request.setOtherThanPreferredRoutesPenalty(get(otherThanPreferredRoutesPenalty, n, request.getOtherThanPreferredRoutesPenalty()));
request.setPreferredAgencies(get(preferredAgencies, n, request.getPreferredAgenciesStr()));
request.setUnpreferredRoutes(get(unpreferredRoutes, n, request.getUnpreferredRouteStr()));
request.setUnpreferredAgencies(get(unpreferredAgencies, n, request.getUnpreferredAgenciesStr()));
request.setBannedRoutes(get(bannedRoutes, n, request.getBannedRouteStr()));
request.setBannedAgencies(get(bannedAgencies, n, request.getBannedAgenciesStr()));
HashMap<AgencyAndId, BannedStopSet> bannedTripMap = makeBannedTripMap(get(bannedTrips, n, null));
if (bannedTripMap != null) {
request.setBannedTrips(bannedTripMap);
}
request.setBannedStops(get(bannedStops, n, request.getBannedStopsStr()));
// "Least transfers" optimization is accomplished via an increased transfer penalty.
// See comment on RoutingRequest.transferPentalty.
if (opt == OptimizeType.TRANSFERS) {
opt = OptimizeType.QUICK;
request.setTransferPenalty(get(transferPenalty, n, 0) + 1800);
} else {
request.setTransferPenalty(get(transferPenalty, n, request.getTransferPenalty()));
}
request.setBatch(get(batch, n, new Boolean(request.isBatch())));
request.setOptimize(opt);
TraverseModeSet modeSet = get(modes, n, request.getModes());
request.setModes(modeSet);
if (modeSet.getBicycle() && modeSet.getWalk() && bikeSpeedParam == -1) {
//slower bike speed for bike sharing, based on empirical evidence from DC.
request.setBikeSpeed(4.3);
}
request.setBoardSlack(get(boardSlack, n, request.getBoardSlack()));
request.setAlightSlack(get(alightSlack, n, request.getAlightSlack()));
request.setTransferSlack(get(minTransferTime, n, request.getTransferSlack()));
request.setNonpreferredTransferPenalty(get(nonpreferredTransferPenalty, n, request.getNonpreferredTransferPenalty()));
if (request.getBoardSlack() + request.getAlightSlack() > request.getTransferSlack()) {
throw new RuntimeException("Invalid parameters: transfer slack must "
+ "be greater than or equal to board slack plus alight slack");
}
request.setMaxTransfers(get(maxTransfers, n, request.getMaxTransfers()));
final long NOW_THRESHOLD_MILLIS = 15 * 60 * 60 * 1000;
boolean tripPlannedForNow = Math.abs(request.getDateTime().getTime() - new Date().getTime())
< NOW_THRESHOLD_MILLIS;
request.setUseBikeRentalAvailabilityInformation(tripPlannedForNow);
if (request.getIntermediatePlaces() != null
&& (request.getModes().isTransit() ||
(request.getModes().getWalk() &&
request.getModes().getBicycle())))
throw new UnsupportedOperationException("TSP is not supported for transit or bike share trips");
String startTransitStopId = get(this.startTransitStopId, n,
AgencyAndId.convertToString(request.getStartingTransitStopId()));
if (startTransitStopId != null && !"".equals(startTransitStopId)) {
request.setStartingTransitStopId(AgencyAndId.convertFromString(startTransitStopId));
}
String startTransitTripId = get(this.startTransitTripId, n,
AgencyAndId.convertToString(request.getStartingTransitTripId()));
if (startTransitTripId != null && !"".equals(startTransitTripId)) {
request.setStartingTransitTripId(AgencyAndId.convertFromString(startTransitTripId));
}
request.setClampInitialWait(get(clampInitialWait, n, request.getClampInitialWait()));
request.setReverseOptimizeOnTheFly(get(reverseOptimizeOnTheFly, n,
request.isReverseOptimizeOnTheFly()));
String localeSpec = get(locale, n, "en");
String[] localeSpecParts = localeSpec.split("_");
Locale locale;
switch (localeSpecParts.length) {
case 1:
locale = new Locale(localeSpecParts[0]);
break;
case 2:
locale = new Locale(localeSpecParts[0]);
break;
case 3:
locale = new Locale(localeSpecParts[0]);
break;
default:
LOG.debug("Bogus locale " + localeSpec + ", defaulting to en");
locale = new Locale("en");
}
request.setLocale(locale);
return request;
}
private HashMap<AgencyAndId, BannedStopSet> makeBannedTripMap(String banned) {
if (banned == null) {
return null;
}
HashMap<AgencyAndId, BannedStopSet> bannedTripMap = new HashMap<AgencyAndId, BannedStopSet>();
String[] tripStrings = banned.split(",");
for (String tripString : tripStrings) {
String[] parts = tripString.split(":");
String tripIdString = parts[0];
AgencyAndId tripId = AgencyAndId.convertFromString(tripIdString);
BannedStopSet bannedStops;
if (parts.length == 1) {
bannedStops = BannedStopSet.ALL;
} else {
bannedStops = new BannedStopSet();
for (int i = 1; i < parts.length; ++i) {
bannedStops.add(Integer.parseInt(parts[i]));
}
}
bannedTripMap.put(tripId, bannedStops);
}
return bannedTripMap;
}
/**
* Gets the nth item in a list, or the item with the highest index if there are less than n
* elements, or the default value if the list is empty or null.
* Throughout buildRequest() you will see the following idiom:
* request.setParamX(get(paramX, n, request.getParamX));
*
* This checks a query parameter field from Jersey (which is a list, one element for each occurrence
* of the parameter in the query string) for the nth occurrence, or the one with the highest index.
* If a parameter was supplied, it replaces the value in the RoutingRequest under construction
* (which was cloned from the prototypeRoutingRequest). If not, it uses the value already in that
* RoutingRequest as a default (i.e. it uses the value cloned from the PrototypeRoutingRequest).
*
* @param l list of query parameter values
* @param n requested item index
* @return nth item if it exists, closest existing item otherwise, or defaultValue if the list l
* is null or empty.
*/
private <T> T get(List<T> l, int n, T defaultValue) {
if (l == null || l.size() == 0)
return defaultValue;
int maxIndex = l.size() - 1;
if (n > maxIndex)
n = maxIndex;
T value = l.get(n);
if (value instanceof Integer) {
if (value.equals(-1)) {
return defaultValue;
}
} else if (value instanceof Double) {
if (value.equals(-1.0)) {
return defaultValue;
}
}
return value;
}
}