/* 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.index;
import com.beust.jcommander.internal.Lists;
import com.beust.jcommander.internal.Sets;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import org.onebusaway.gtfs.model.Agency;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Route;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.gtfs.model.Trip;
import org.onebusaway.gtfs.model.calendar.ServiceDate;
import org.opentripplanner.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.gtfs.GtfsLibrary;
import org.opentripplanner.index.model.PatternDetail;
import org.opentripplanner.index.model.PatternShort;
import org.opentripplanner.index.model.RouteShort;
import org.opentripplanner.index.model.StopClusterDetail;
import org.opentripplanner.index.model.StopShort;
import org.opentripplanner.index.model.StopTimesInPattern;
import org.opentripplanner.index.model.TripShort;
import org.opentripplanner.index.model.TripTimeShort;
import org.opentripplanner.profile.StopCluster;
import org.opentripplanner.routing.edgetype.SimpleTransfer;
import org.opentripplanner.routing.edgetype.Timetable;
import org.opentripplanner.routing.edgetype.TripPattern;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.GraphIndex;
import org.opentripplanner.routing.services.StreetVertexIndexService;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.opentripplanner.standalone.OTPServer;
import org.opentripplanner.standalone.Router;
import org.opentripplanner.util.PolylineEncoder;
import org.opentripplanner.util.model.EncodedPolylineBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
// TODO move to org.opentripplanner.api.resource, this is a Jersey resource class
@Path("/routers/{routerId}/index") // It would be nice to get rid of the final /index.
@Produces(MediaType.APPLICATION_JSON) // One @Produces annotation for all endpoints.
public class IndexAPI {
@SuppressWarnings("unused")
private static final Logger LOG = LoggerFactory.getLogger(IndexAPI.class);
private static final double MAX_STOP_SEARCH_RADIUS = 5000;
private static final String MSG_404 = "FOUR ZERO FOUR";
private static final String MSG_400 = "FOUR HUNDRED";
/** Choose short or long form of results. */
@QueryParam("detail") private boolean detail = false;
/** Include GTFS entities referenced by ID in the result. */
@QueryParam("refs") private boolean refs = false;
private final GraphIndex index;
private final StreetVertexIndexService streetIndex;
private final ObjectMapper deserializer = new ObjectMapper();
public IndexAPI (@Context OTPServer otpServer, @PathParam("routerId") String routerId) {
Router router = otpServer.getRouter(routerId);
index = router.graph.index;
streetIndex = router.graph.streetIndex;
}
/* Needed to check whether query parameter map is empty, rather than chaining " && x == null"s */
@Context UriInfo uriInfo;
@GET
@Path("/feeds")
public Response getFeeds() {
return Response.status(Status.OK).entity(index.agenciesForFeedId.keySet()).build();
}
/** Return a list of all agencies in the graph. */
@GET
@Path("/agencies/{feedId}")
public Response getAgencies (@PathParam("feedId") String feedId) {
return Response.status(Status.OK).entity(
index.agenciesForFeedId.getOrDefault(feedId, new HashMap<>()).values()).build();
}
/** Return specific agency in the graph, by ID. */
@GET
@Path("/agencies/{feedId}/{agencyId}")
public Response getAgency (@PathParam("feedId") String feedId, @PathParam("agencyId") String agencyId) {
for (Agency agency : index.agenciesForFeedId.get(feedId).values()) {
if (agency.getId().equals(agencyId)) {
return Response.status(Status.OK).entity(agency).build();
}
}
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
/** Return all routes for the specific agency. */
@GET
@Path("/agencies/{feedId}/{agencyId}/routes")
public Response getAgencyRoutes (@PathParam("feedId") String feedId, @PathParam("agencyId") String agencyId) {
Collection<Route> routes = index.routeForId.values();
Agency agency = index.agenciesForFeedId.get(feedId).get(agencyId);
if (agency == null) return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
Collection<Route> agencyRoutes = new ArrayList<>();
for (Route route: routes) {
if (route.getAgency() == agency) {
agencyRoutes.add(route);
}
}
routes = agencyRoutes;
if (detail){
return Response.status(Status.OK).entity(routes).build();
}
else {
return Response.status(Status.OK).entity(RouteShort.list(routes)).build();
}
}
/** Return specific transit stop in the graph, by ID. */
@GET
@Path("/stops/{stopId}")
public Response getStop (@PathParam("stopId") String stopIdString) {
AgencyAndId stopId = GtfsLibrary.convertIdFromString(stopIdString);
Stop stop = index.stopForId.get(stopId);
if (stop != null) {
return Response.status(Status.OK).entity(stop).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
/** Return a list of all stops within a circle around the given coordinate. */
@GET
@Path("/stops")
public Response getStopsInRadius (
@QueryParam("minLat") Double minLat,
@QueryParam("minLon") Double minLon,
@QueryParam("maxLat") Double maxLat,
@QueryParam("maxLon") Double maxLon,
@QueryParam("lat") Double lat,
@QueryParam("lon") Double lon,
@QueryParam("radius") Double radius) {
/* When no parameters are supplied, return all stops. */
if (uriInfo.getQueryParameters().isEmpty()) {
Collection<Stop> stops = index.stopForId.values();
return Response.status(Status.OK).entity(StopShort.list(stops)).build();
}
/* If any of the circle parameters are specified, expect a circle not a box. */
boolean expectCircle = (lat != null || lon != null || radius != null);
if (expectCircle) {
if (lat == null || lon == null || radius == null || radius < 0) {
return Response.status(Status.BAD_REQUEST).entity(MSG_400).build();
}
if (radius > MAX_STOP_SEARCH_RADIUS){
radius = MAX_STOP_SEARCH_RADIUS;
}
List<StopShort> stops = Lists.newArrayList();
Coordinate coord = new Coordinate(lon, lat);
for (TransitStop stopVertex : streetIndex.getNearbyTransitStops(
new Coordinate(lon, lat), radius)) {
double distance = SphericalDistanceLibrary.fastDistance(stopVertex.getCoordinate(), coord);
if (distance < radius) {
stops.add(new StopShort(stopVertex.getStop(), (int) distance));
}
}
return Response.status(Status.OK).entity(stops).build();
} else {
/* We're not circle mode, we must be in box mode. */
if (minLat == null || minLon == null || maxLat == null || maxLon == null) {
return Response.status(Status.BAD_REQUEST).entity(MSG_400).build();
}
if (maxLat <= minLat || maxLon <= minLon) {
return Response.status(Status.BAD_REQUEST).entity(MSG_400).build();
}
List<StopShort> stops = Lists.newArrayList();
Envelope envelope = new Envelope(new Coordinate(minLon, minLat), new Coordinate(maxLon, maxLat));
for (TransitStop stopVertex : streetIndex.getTransitStopForEnvelope(envelope)) {
stops.add(new StopShort(stopVertex.getStop()));
}
return Response.status(Status.OK).entity(stops).build();
}
}
@GET
@Path("/stops/{stopId}/routes")
public Response getRoutesForStop (@PathParam("stopId") String stopId) {
Stop stop = index.stopForId.get(GtfsLibrary.convertIdFromString(stopId));
if (stop == null) return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
Set<Route> routes = Sets.newHashSet();
for (TripPattern pattern : index.patternsForStop.get(stop)) {
routes.add(pattern.route);
}
return Response.status(Status.OK).entity(RouteShort.list(routes)).build();
}
@GET
@Path("/stops/{stopId}/patterns")
public Response getPatternsForStop (@PathParam("stopId") String stopIdString) {
AgencyAndId id = GtfsLibrary.convertIdFromString(stopIdString);
Stop stop = index.stopForId.get(id);
if (stop == null) return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
Collection<TripPattern> patterns = index.patternsForStop.get(stop);
return Response.status(Status.OK).entity(PatternShort.list(patterns)).build();
}
/** Return upcoming vehicle arrival/departure times at the given stop.
*
* @param stopIdString Stop ID in Agency:Stop ID format
* @param startTime Start time for the search. Seconds from UNIX epoch
* @param timeRange Searches forward for timeRange seconds from startTime
* @param numberOfDepartures Number of departures to fetch per pattern
*/
@GET
@Path("/stops/{stopId}/stoptimes")
public Response getStoptimesForStop (@PathParam("stopId") String stopIdString,
@QueryParam("startTime") long startTime,
@QueryParam("timeRange") @DefaultValue("86400") int timeRange,
@QueryParam("numberOfDepartures") @DefaultValue("2") int numberOfDepartures) {
Stop stop = index.stopForId.get(GtfsLibrary.convertIdFromString(stopIdString));
if (stop == null) return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
return Response.status(Status.OK).entity(index.stopTimesForStop(stop, startTime, timeRange, numberOfDepartures)).build();
}
/**
* Return upcoming vehicle arrival/departure times at the given stop.
* @param date in YYYYMMDD format
*/
@GET
@Path("/stops/{stopId}/stoptimes/{date}")
public Response getStoptimesForStopAndDate (@PathParam("stopId") String stopIdString,
@PathParam("date") String date) {
Stop stop = index.stopForId.get(GtfsLibrary.convertIdFromString(stopIdString));
if (stop == null) return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
ServiceDate sd;
try {
sd = ServiceDate.parseString(date);
}
catch (ParseException e){
return Response.status(Status.BAD_REQUEST).entity(MSG_400).build();
}
List<StopTimesInPattern> ret = index.getStopTimesForStop(stop, sd);
return Response.status(Status.OK).entity(ret).build();
}
/**
* Return the generated transfers a stop in the graph, by stop ID
*/
@GET
@Path("/stops/{stopId}/transfers")
public Response getTransfers(@PathParam("stopId") String stopIdString) {
Stop stop = index.stopForId.get(GtfsLibrary.convertIdFromString(stopIdString));
if (stop != null) {
// get the transfers for the stop
TransitStop v = index.stopVertexForStop.get(stop);
Collection<Edge> transfers = Collections2.filter(v.getOutgoing(), new Predicate<Edge>() {
@Override
public boolean apply(Edge edge) {
return edge instanceof SimpleTransfer;
}
});
Collection<Transfer> out = Collections2.transform(transfers, new Function<Edge, Transfer> () {
@Override
public Transfer apply(Edge edge) {
// TODO Auto-generated method stub
return new Transfer((SimpleTransfer) edge);
}
});
return Response.status(Status.OK).entity(out).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
/** Return a list of all routes in the graph. */
// with repeated hasStop parameters, replaces old routesBetweenStops
@GET
@Path("/routes")
public Response getRoutes (@QueryParam("hasStop") List<String> stopIds) {
Collection<Route> routes = index.routeForId.values();
// Filter routes to include only those that pass through all given stops
if (stopIds != null) {
// Protective copy, we are going to calculate the intersection destructively
routes = Lists.newArrayList(routes);
for (String stopId : stopIds) {
Stop stop = index.stopForId.get(GtfsLibrary.convertIdFromString(stopId));
if (stop == null) return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
Set<Route> routesHere = Sets.newHashSet();
for (TripPattern pattern : index.patternsForStop.get(stop)) {
routesHere.add(pattern.route);
}
routes.retainAll(routesHere);
}
}
return Response.status(Status.OK).entity(RouteShort.list(routes)).build();
}
/** Return specific route in the graph, for the given ID. */
@GET
@Path("/routes/{routeId}")
public Response getRoute (@PathParam("routeId") String routeIdString) {
AgencyAndId routeId = GtfsLibrary.convertIdFromString(routeIdString);
Route route = index.routeForId.get(routeId);
if (route != null) {
return Response.status(Status.OK).entity(route).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
/** Return all stop patterns used by trips on the given route. */
@GET
@Path("/routes/{routeId}/patterns")
public Response getPatternsForRoute (@PathParam("routeId") String routeIdString) {
AgencyAndId routeId = GtfsLibrary.convertIdFromString(routeIdString);
Route route = index.routeForId.get(routeId);
if (route != null) {
Collection<TripPattern> patterns = index.patternsForRoute.get(route);
return Response.status(Status.OK).entity(PatternShort.list(patterns)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
/** Return all stops in any pattern on a given route. */
@GET
@Path("/routes/{routeId}/stops")
public Response getStopsForRoute (@PathParam("routeId") String routeIdString) {
AgencyAndId routeId = GtfsLibrary.convertIdFromString(routeIdString);
Route route = index.routeForId.get(routeId);
if (route != null) {
Set<Stop> stops = Sets.newHashSet();
Collection<TripPattern> patterns = index.patternsForRoute.get(route);
for (TripPattern pattern : patterns) {
stops.addAll(pattern.getStops());
}
return Response.status(Status.OK).entity(StopShort.list(stops)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
/** Return all trips in any pattern on the given route. */
@GET
@Path("/routes/{routeId}/trips")
public Response getTripsForRoute (@PathParam("routeId") String routeIdString) {
AgencyAndId routeId = GtfsLibrary.convertIdFromString(routeIdString);
Route route = index.routeForId.get(routeId);
if (route != null) {
List<Trip> trips = Lists.newArrayList();
Collection<TripPattern> patterns = index.patternsForRoute.get(route);
for (TripPattern pattern : patterns) {
trips.addAll(pattern.getTrips());
}
return Response.status(Status.OK).entity(TripShort.list(trips)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
// Not implemented, results would be too voluminous.
// @Path("/trips")
@GET
@Path("/trips/{tripId}")
public Response getTrip (@PathParam("tripId") String tripIdString) {
AgencyAndId tripId = GtfsLibrary.convertIdFromString(tripIdString);
Trip trip = index.tripForId.get(tripId);
if (trip != null) {
return Response.status(Status.OK).entity(trip).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
@GET
@Path("/trips/{tripId}/stops")
public Response getStopsForTrip (@PathParam("tripId") String tripIdString) {
AgencyAndId tripId = GtfsLibrary.convertIdFromString(tripIdString);
Trip trip = index.tripForId.get(tripId);
if (trip != null) {
TripPattern pattern = index.patternForTrip.get(trip);
Collection<Stop> stops = pattern.getStops();
return Response.status(Status.OK).entity(StopShort.list(stops)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
@GET
@Path("/trips/{tripId}/semanticHash")
public Response getSemanticHashForTrip (@PathParam("tripId") String tripIdString) {
AgencyAndId tripId = GtfsLibrary.convertIdFromString(tripIdString);
Trip trip = index.tripForId.get(tripId);
if (trip != null) {
TripPattern pattern = index.patternForTrip.get(trip);
String hashString = pattern.semanticHashString(trip);
return Response.status(Status.OK).entity(hashString).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
@GET
@Path("/trips/{tripId}/stoptimes")
public Response getStoptimesForTrip (@PathParam("tripId") String tripIdString) {
AgencyAndId tripId = GtfsLibrary.convertIdFromString(tripIdString);
Trip trip = index.tripForId.get(tripId);
if (trip != null) {
TripPattern pattern = index.patternForTrip.get(trip);
// Note, we need the updated timetable not the scheduled one (which contains no real-time updates).
Timetable table = index.currentUpdatedTimetableForTripPattern(pattern);
return Response.status(Status.OK).entity(TripTimeShort.fromTripTimes(table, trip)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
/** Return geometry for the trip as a packed coordinate sequence */
@GET
@Path("/trips/{tripId}/geometry")
public Response getGeometryForTrip (@PathParam("tripId") String tripIdString) {
AgencyAndId tripId = GtfsLibrary.convertIdFromString(tripIdString);
Trip trip = index.tripForId.get(tripId);
if (trip != null) {
TripPattern tripPattern = index.patternForTrip.get(trip);
return getGeometryForPattern(tripPattern.code);
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
@GET
@Path("/patterns")
public Response getPatterns () {
Collection<TripPattern> patterns = index.patternForId.values();
return Response.status(Status.OK).entity(PatternShort.list(patterns)).build();
}
@GET
@Path("/patterns/{patternId}")
public Response getPattern (@PathParam("patternId") String patternIdString) {
TripPattern pattern = index.patternForId.get(patternIdString);
if (pattern != null) {
return Response.status(Status.OK).entity(new PatternDetail(pattern)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
@GET
@Path("/patterns/{patternId}/trips")
public Response getTripsForPattern (@PathParam("patternId") String patternIdString) {
TripPattern pattern = index.patternForId.get(patternIdString);
if (pattern != null) {
List<Trip> trips = pattern.getTrips();
return Response.status(Status.OK).entity(TripShort.list(trips)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
@GET
@Path("/patterns/{patternId}/stops")
public Response getStopsForPattern (@PathParam("patternId") String patternIdString) {
// Pattern names are graph-unique because we made them that way (did not read them from GTFS).
TripPattern pattern = index.patternForId.get(patternIdString);
if (pattern != null) {
List<Stop> stops = pattern.getStops();
return Response.status(Status.OK).entity(StopShort.list(stops)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
@GET
@Path("/patterns/{patternId}/semanticHash")
public Response getSemanticHashForPattern (@PathParam("patternId") String patternIdString) {
// Pattern names are graph-unique because we made them that way (did not read them from GTFS).
TripPattern pattern = index.patternForId.get(patternIdString);
if (pattern != null) {
String semanticHash = pattern.semanticHashString(null);
return Response.status(Status.OK).entity(semanticHash).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
/** Return geometry for the pattern as a packed coordinate sequence */
@GET
@Path("/patterns/{patternId}/geometry")
public Response getGeometryForPattern (@PathParam("patternId") String patternIdString) {
TripPattern pattern = index.patternForId.get(patternIdString);
if (pattern != null) {
EncodedPolylineBean geometry = PolylineEncoder.createEncodings(pattern.geometry);
return Response.status(Status.OK).entity(geometry).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
// TODO include pattern ID for each trip in responses
/** List basic information about all service IDs. */
@GET
@Path("/services")
public Response getServices() {
index.serviceForId.values(); // TODO complete
return Response.status(Status.OK).entity("NONE").build();
}
/** List details about a specific service ID including which dates it runs on. Replaces the old /calendar. */
@GET
@Path("/services/{serviceId}")
public Response getServices(@PathParam("serviceId") String serviceId) {
index.serviceForId.get(serviceId); // TODO complete
return Response.status(Status.OK).entity("NONE").build();
}
/** Return all clusters of stops. */
@GET
@Path("/clusters")
public Response getAllStopClusters () {
index.clusterStopsAsNeeded();
// use 'detail' field common to all API methods in this class
List<StopClusterDetail> scl = StopClusterDetail.list(index.stopClusterForId.values(), detail);
return Response.status(Status.OK).entity(scl).build();
}
/** Return a cluster of stops by its ID. */
@GET
@Path("/clusters/{clusterId}")
public Response getStopCluster (@PathParam("clusterId") String clusterIdString) {
index.clusterStopsAsNeeded();
StopCluster cluster = index.stopClusterForId.get(clusterIdString);
if (cluster != null) {
return Response.status(Status.OK).entity(new StopClusterDetail(cluster, true)).build();
} else {
return Response.status(Status.NOT_FOUND).entity(MSG_404).build();
}
}
@POST
@Path("/graphql")
@Consumes(MediaType.APPLICATION_JSON)
public Response getGraphQL (HashMap<String, Object> queryParameters) {
String query = (String) queryParameters.get("query");
Object queryVariables = queryParameters.getOrDefault("variables", null);
String operationName = (String) queryParameters.getOrDefault("operationName", null);
Map<String, Object> variables;
if (queryVariables instanceof Map) {
variables = (Map) queryVariables;
} else if (queryVariables instanceof String && !((String) queryVariables).isEmpty()) {
try {
variables = deserializer.readValue((String) queryVariables, Map.class);
} catch (IOException e) {
LOG.error("Variables must be a valid json object");
return Response.status(Status.BAD_REQUEST).entity(MSG_400).build();
}
} else {
variables = new HashMap<>();
}
return index.getGraphQLResponse(query, variables, operationName);
}
@POST
@Path("/graphql")
@Consumes("application/graphql")
public Response getGraphQL (String query) {
return index.getGraphQLResponse(query, new HashMap<>(), null);
}
// @GET
// @Path("/graphql")
// public Response getGraphQL (@QueryParam("query") String query,
// @QueryParam("variables") HashMap<String, Object> variables) {
// return index.getGraphQLResponse(query, variables == null ? new HashMap<>() : variables);
// }
/** Represents a transfer from a stop */
private static class Transfer {
/** The stop we are connecting to */
public String toStopId;
/** the on-street distance of the transfer (meters) */
public double distance;
/** Make a transfer from a simpletransfer edge from the graph. */
public Transfer(SimpleTransfer e) {
toStopId = GtfsLibrary.convertIdToString(((TransitStop) e.getToVertex()).getStopId());
distance = e.getDistance();
}
}
}