org.opentripplanner.index.IndexAPI Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otp Show documentation
Show all versions of otp Show documentation
The OpenTripPlanner multimodal journey planning system
package org.opentripplanner.index;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
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 org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.opentripplanner.api.mapping.AgencyMapper;
import org.opentripplanner.api.mapping.AlertMapper;
import org.opentripplanner.api.mapping.FeedInfoMapper;
import org.opentripplanner.api.mapping.FeedScopedIdMapper;
import org.opentripplanner.api.mapping.RouteMapper;
import org.opentripplanner.api.mapping.StopMapper;
import org.opentripplanner.api.mapping.StopTimesInPatternMapper;
import org.opentripplanner.api.mapping.TransferMapper;
import org.opentripplanner.api.mapping.TripMapper;
import org.opentripplanner.api.mapping.TripPatternMapper;
import org.opentripplanner.api.mapping.TripTimeMapper;
import org.opentripplanner.api.model.ApiAgency;
import org.opentripplanner.api.model.ApiAlert;
import org.opentripplanner.api.model.ApiFeedInfo;
import org.opentripplanner.api.model.ApiPatternShort;
import org.opentripplanner.api.model.ApiRoute;
import org.opentripplanner.api.model.ApiRouteShort;
import org.opentripplanner.api.model.ApiStop;
import org.opentripplanner.api.model.ApiStopShort;
import org.opentripplanner.api.model.ApiStopTimesInPattern;
import org.opentripplanner.api.model.ApiTransfer;
import org.opentripplanner.api.model.ApiTrip;
import org.opentripplanner.api.model.ApiTripShort;
import org.opentripplanner.api.model.ApiTripTimeShort;
import org.opentripplanner.api.support.SemanticHash;
import org.opentripplanner.model.StopTimesInPattern;
import org.opentripplanner.model.TripTimeOnDate;
import org.opentripplanner.routing.RoutingService;
import org.opentripplanner.routing.graphfinder.DirectGraphFinder;
import org.opentripplanner.routing.stoptimes.ArrivalDeparture;
import org.opentripplanner.standalone.api.OtpServerRequestContext;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.organization.Agency;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.service.TransitService;
import org.opentripplanner.util.PolylineEncoder;
import org.opentripplanner.util.model.EncodedPolyline;
import org.opentripplanner.util.time.ServiceDateUtils;
// TODO move to org.opentripplanner.api.resource, this is a Jersey resource class
@Path("/routers/{ignoreRouterId}/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 {
private static final double MAX_STOP_SEARCH_RADIUS = 5000;
private final OtpServerRequestContext serverContext;
/* Needed to check whether query parameter map is empty, rather than chaining " && x == null"s */
@Context
UriInfo uriInfo;
public IndexAPI(
@Context OtpServerRequestContext serverContext,
/**
* @deprecated The support for multiple routers are removed from OTP2.
* See https://github.com/opentripplanner/OpenTripPlanner/issues/2760
*/
@PathParam("ignoreRouterId") String ignoreRouterId
) {
this.serverContext = serverContext;
}
@GET
@Path("/feeds")
public Collection getFeeds() {
return transitService().getFeedIds();
}
@GET
@Path("/feeds/{feedId}")
public ApiFeedInfo getFeedInfo(@PathParam("feedId") String feedId) {
var feedInfo = FeedInfoMapper.mapToApi(transitService().getFeedInfo(feedId));
return validateExist("FeedInfo", feedInfo, "feedId", feedId);
}
/** Return a list of all agencies in the graph. */
@GET
@Path("/agencies/{feedId}")
public Collection getAgencies(@PathParam("feedId") String feedId) {
Collection agencies = transitService()
.getAgencies()
.stream()
.filter(agency -> agency.getId().getFeedId().equals(feedId))
.collect(Collectors.toList());
validateExist("Agency", agencies, "feedId", feedId);
return AgencyMapper.mapToApi(agencies);
}
/** Return specific agency in the graph, by ID. */
@GET
@Path("/agencies/{feedId}/{agencyId}")
public ApiAgency httpGgetAgency(
@PathParam("feedId") String feedId,
@PathParam("agencyId") String agencyId
) {
return AgencyMapper.mapToApi(agency(feedId, agencyId));
}
/** Return all routes for the specific agency. */
@GET
@Path("/agencies/{feedId}/{agencyId}/routes")
public Response getAgencyRoutes(
@PathParam("feedId") String feedId,
@PathParam("agencyId") String agencyId,
/** Choose short or long form of results. */
@QueryParam("detail") @DefaultValue("false") Boolean detail
) {
var agency = agency(feedId, agencyId);
Collection routes = transitService()
.getAllRoutes()
.stream()
.filter(r -> r.getAgency() == agency)
.collect(Collectors.toList());
if (detail) {
return Response.status(Status.OK).entity(RouteMapper.mapToApi(routes)).build();
} else {
return Response.status(Status.OK).entity(RouteMapper.mapToApiShort(routes)).build();
}
}
/**
* Return all alerts for an agency
*/
@GET
@Path("/agencies/{feedId}/{agencyId}/alerts")
public Collection getAlertsForTrip(
@PathParam("feedId") String feedId,
@PathParam("agencyId") String agencyId
) {
var alertMapper = new AlertMapper(null); // TODO: Add locale
var id = new FeedScopedId(feedId, agencyId);
return alertMapper.mapToApi(transitService().getTransitAlertService().getAgencyAlerts(id));
}
/** Return specific transit stop in the graph, by ID. */
@GET
@Path("/stops/{stopId}")
public ApiStop getStop(@PathParam("stopId") String stopIdString) {
return StopMapper.mapToApi(stop(stopIdString));
}
/** Return a list of all stops within a circle around the given coordinate. */
@SuppressWarnings("ConstantConditions")
@GET
@Path("/stops")
public List 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()) {
return StopMapper.mapToApiShort(transitService().listStopLocations());
}
/* If any of the circle parameters are specified, expect a circle not a box. */
boolean expectCircle = (lat != null || lon != null || radius != null);
if (expectCircle) {
verifyParams()
.withinBounds("lat", lat, -90.0, 90.0)
.withinBounds("lon", lon, -180, 180)
.positiveOrZero("radius", radius)
.validate();
radius = Math.min(radius, MAX_STOP_SEARCH_RADIUS);
return new DirectGraphFinder(serverContext.transitService()::findRegularStop)
.findClosestStops(new Coordinate(lon, lat), radius)
.stream()
.map(it -> StopMapper.mapToApiShort(it.stop, it.distance))
.collect(Collectors.toList());
} else {
/* We're not circle mode, we must be in box mode. */
verifyParams()
.withinBounds("minLat", minLat, -90.0, 90.0)
.withinBounds("maxLat", maxLat, -90.0, 90.0)
.withinBounds("minLon", minLon, -180.0, 180.0)
.withinBounds("maxLon", maxLon, -180.0, 180.0)
.lessThan("minLat", minLat, "maxLat", maxLat)
.lessThan("minLon", minLon, "maxLon", maxLon)
.validate();
Envelope envelope = new Envelope(
new Coordinate(minLon, minLat),
new Coordinate(maxLon, maxLat)
);
var stops = transitService().findRegularStop(envelope);
return stops
.stream()
.filter(stop -> envelope.contains(stop.getCoordinate().asJtsCoordinate()))
.map(StopMapper::mapToApiShort)
.toList();
}
}
@GET
@Path("/stops/{stopId}/routes")
public List getRoutesForStop(@PathParam("stopId") String stopId) {
var stop = stop(stopId);
return transitService()
.getPatternsForStop(stop)
.stream()
.map(TripPattern::getRoute)
.map(RouteMapper::mapToApiShort)
.collect(Collectors.toList());
}
@GET
@Path("/stops/{stopId}/patterns")
public List getPatternsForStop(@PathParam("stopId") String stopId) {
var stop = stop(stopId);
return transitService()
.getPatternsForStop(stop)
.stream()
.map(TripPatternMapper::mapToApiShort)
.collect(Collectors.toList());
}
/**
* Return upcoming vehicle arrival/departure times at the given stop.
*
* @param stopIdString Stop ID in Agency:Stop ID format
* @param startTimeSeconds 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 Collection getStopTimesForStop(
@PathParam("stopId") String stopIdString,
@QueryParam("startTime") long startTimeSeconds,
@QueryParam("timeRange") @DefaultValue("86400") int timeRange,
@QueryParam("numberOfDepartures") @DefaultValue("2") int numberOfDepartures,
@QueryParam("omitNonPickups") boolean omitNonPickups
) {
Instant startTime = startTimeSeconds == 0
? Instant.now()
: Instant.ofEpochSecond(startTimeSeconds);
return transitService()
.stopTimesForStop(
stop(stopIdString),
startTime,
Duration.ofSeconds(timeRange),
numberOfDepartures,
omitNonPickups ? ArrivalDeparture.DEPARTURES : ArrivalDeparture.BOTH,
false
)
.stream()
.map(StopTimesInPatternMapper::mapToApi)
.collect(Collectors.toList());
}
/**
* Return upcoming vehicle arrival/departure times at the given stop.
*
* @param date in YYYYMMDD or YYYY-MM-DD format
*/
@GET
@Path("/stops/{stopId}/stoptimes/{date}")
public List getStoptimesForStopAndDate(
@PathParam("stopId") String stopId,
@PathParam("date") String date,
@QueryParam("omitNonPickups") boolean omitNonPickups
) {
var stop = stop(stopId);
var serviceDate = parseServiceDate("date", date);
List stopTimes = transitService()
.getStopTimesForStop(
stop,
serviceDate,
omitNonPickups ? ArrivalDeparture.DEPARTURES : ArrivalDeparture.BOTH,
true
);
return StopTimesInPatternMapper.mapToApi(stopTimes);
}
/**
* Return the generated transfers a stop in the graph, by stop ID
*/
@GET
@Path("/stops/{stopId}/transfers")
public Collection getTransfers(@PathParam("stopId") String stopId) {
var stop = stop(stopId);
// get the transfers for the stop
return transitService()
.getTransfersByStop(stop)
.stream()
.map(TransferMapper::mapToApi)
.collect(Collectors.toList());
}
/**
* Return all alerts for a stop
*/
@GET
@Path("/stops/{stopId}/alerts")
public Collection getAlertsForStop(@PathParam("stopId") String stopId) {
var alertMapper = new AlertMapper(null); // TODO: Add locale
var id = createId("stopId", stopId);
return alertMapper.mapToApi(transitService().getTransitAlertService().getStopAlerts(id));
}
/** Return a list of all routes in the graph. */
// with repeated hasStop parameters, replaces old routesBetweenStops
@GET
@Path("/routes")
public List getRoutes(@QueryParam("hasStop") List stopIds) {
Collection routes = transitService().getAllRoutes();
// 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 = new ArrayList<>(routes);
for (String stopId : stopIds) {
var stop = stop(stopId);
Set routesHere = new HashSet<>();
for (TripPattern pattern : transitService().getPatternsForStop(stop)) {
routesHere.add(pattern.getRoute());
}
routes.retainAll(routesHere);
}
}
return RouteMapper.mapToApiShort(routes);
}
/** Return specific route in the graph, for the given ID. */
@GET
@Path("/routes/{routeId}")
public ApiRoute getRoute(@PathParam("routeId") String routeId) {
return RouteMapper.mapToApi(route(routeId));
}
/** Return all stop patterns used by trips on the given route. */
@GET
@Path("/routes/{routeId}/patterns")
public List getPatternsForRoute(@PathParam("routeId") String routeId) {
Collection patterns = transitService().getPatternsForRoute(route(routeId));
return TripPatternMapper.mapToApiShort(patterns);
}
/** Return all stops in any pattern on a given route. */
@GET
@Path("/routes/{routeId}/stops")
public List getStopsForRoute(@PathParam("routeId") String routeId) {
var route = route(routeId);
Set stops = new HashSet<>();
Collection patterns = transitService().getPatternsForRoute(route);
for (TripPattern pattern : patterns) {
stops.addAll(pattern.getStops());
}
return StopMapper.mapToApiShort(stops);
}
/** Return all trips in any pattern on the given route. */
@GET
@Path("/routes/{routeId}/trips")
public List getTripsForRoute(@PathParam("routeId") String routeId) {
var route = route(routeId);
var patterns = transitService().getPatternsForRoute(route);
return patterns
.stream()
.flatMap(TripPattern::scheduledTripsAsStream)
.map(TripMapper::mapToApiShort)
.collect(Collectors.toList());
}
/**
* Return all alerts for a route
*/
@GET
@Path("/routes/{routeId}/alerts")
public Collection getAlertsForRoute(@PathParam("routeId") String routeId) {
var alertMapper = new AlertMapper(null); // TODO: Add locale
var id = createId("routeId", routeId);
return alertMapper.mapToApi(transitService().getTransitAlertService().getRouteAlerts(id));
}
// Not implemented, results would be too voluminous.
// @Path("/trips")
@GET
@Path("/trips/{tripId}")
public ApiTrip getTrip(@PathParam("tripId") String tripId) {
return TripMapper.mapToApi(trip(tripId));
}
@GET
@Path("/trips/{tripId}/stops")
public List getStopsForTrip(@PathParam("tripId") String tripId) {
Collection stops = tripPatternForTripId(tripId).getStops();
return StopMapper.mapToApiShort(stops);
}
@GET
@Path("/trips/{tripId}/semanticHash")
public String getSemanticHashForTrip(@PathParam("tripId") String tripId) {
var trip = trip(tripId);
return SemanticHash.forTripPattern(tripPattern(trip), trip);
}
@GET
@Path("/trips/{tripId}/stoptimes")
public List getStoptimesForTrip(@PathParam("tripId") String tripId) {
var trip = trip(tripId);
var pattern = tripPattern(trip);
// Note, we need the updated timetable not the scheduled one (which contains no real-time updates).
var table = transitService()
.getTimetableForTripPattern(pattern, LocalDate.now(transitService().getTimeZone()));
var tripTimesOnDate = TripTimeOnDate.fromTripTimes(table, trip);
return TripTimeMapper.mapToApi(tripTimesOnDate);
}
/** Return geometry for the trip as a packed coordinate sequence */
@GET
@Path("/trips/{tripId}/geometry")
public EncodedPolyline getGeometryForTrip(@PathParam("tripId") String tripId) {
var pattern = tripPatternForTripId(tripId);
return PolylineEncoder.encodeGeometry(pattern.getGeometry());
}
/**
* Return all alerts for a trip
*/
@GET
@Path("/trips/{tripId}/alerts")
public Collection getAlertsForTrip(@PathParam("tripId") String tripId) {
var alertMapper = new AlertMapper(null); // TODO: Add locale
var id = createId("tripId", tripId);
return alertMapper.mapToApi(transitService().getTransitAlertService().getTripAlerts(id, null));
}
@GET
@Path("/patterns")
public List getPatterns() {
Collection patterns = transitService().getAllTripPatterns();
return TripPatternMapper.mapToApiShort(patterns);
}
@GET
@Path("/patterns/{patternId}")
public ApiPatternShort getPattern(@PathParam("patternId") String patternId) {
var pattern = tripPattern(patternId);
return TripPatternMapper.mapToApiDetailed(pattern);
}
@GET
@Path("/patterns/{patternId}/trips")
public List getTripsForPattern(@PathParam("patternId") String patternId) {
var trips = tripPattern(patternId).scheduledTripsAsStream();
return TripMapper.mapToApiShort(trips);
}
@GET
@Path("/patterns/{patternId}/stops")
public List getStopsForPattern(@PathParam("patternId") String patternId) {
var stops = tripPattern(patternId).getStops();
return StopMapper.mapToApiShort(stops);
}
@GET
@Path("/patterns/{patternId}/semanticHash")
public String getSemanticHashForPattern(@PathParam("patternId") String patternId) {
var tripPattern = tripPattern(patternId);
return SemanticHash.forTripPattern(tripPattern, null);
}
/** Return geometry for the pattern as a packed coordinate sequence */
@GET
@Path("/patterns/{patternId}/geometry")
public EncodedPolyline getGeometryForPattern(@PathParam("patternId") String patternId) {
var line = tripPattern(patternId).getGeometry();
return PolylineEncoder.encodeGeometry(line);
}
/**
* Return all alerts for a pattern
*/
@GET
@Path("/patterns/{patternId}/alerts")
public Collection getAlertsForPattern(@PathParam("patternId") String patternId) {
var alertMapper = new AlertMapper(null); // TODO: Add locale
var pattern = tripPattern(patternId);
return alertMapper.mapToApi(
transitService()
.getTransitAlertService()
.getDirectionAndRouteAlerts(pattern.getDirection(), pattern.getRoute().getId())
);
}
// TODO include pattern ID for each trip in responses
/**
* List basic information about all service IDs. This is a placeholder endpoint and is not
* implemented yet.
*/
@GET
@Path("/services")
public Response getServices() {
// TODO complete: index.serviceForId.values();
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. This is a placeholder endpoint and is not implemented yet.
*/
@GET
@Path("/services/{serviceId}")
public Response getServices(@PathParam("serviceId") String serviceId) {
// TODO complete: index.serviceForId.get(serviceId);
return Response.status(Status.OK).entity("NONE").build();
}
/* PRIVATE METHODS */
private static FeedScopedId createId(String name, String value) {
return FeedScopedIdMapper.mapToDomain(name, value);
}
@SuppressWarnings("SameParameterValue")
private static LocalDate parseServiceDate(String label, String date) {
try {
return ServiceDateUtils.parseString(date);
} catch (ParseException e) {
throw new BadRequestException(
"Unable to parse date, not on format: YYYY-MM-DD. " + label + ": '" + date + "'"
);
}
}
private static ValidateParameters verifyParams() {
return new ValidateParameters();
}
private static T validateExist(String eName, T entity, String keyLabel, Object key) {
if (entity != null) {
return entity;
} else {
throw notFoundException(eName, keyLabel, key);
}
}
private static NotFoundException notFoundException(String eName, String keyLbl, Object key) {
return notFoundException(eName, keyLbl + ": " + key);
}
private static NotFoundException notFoundException(String entity, String details) {
return new NotFoundException(entity + " not found. " + details);
}
private Agency agency(String feedId, String agencyId) {
var agency = transitService().getAgencyForId(new FeedScopedId(feedId, agencyId));
if (agency == null) {
throw notFoundException("Agency", "feedId: " + feedId + ", agencyId: " + agencyId);
}
return agency;
}
private StopLocation stop(String stopId) {
var stop = transitService().getRegularStop(createId("stopId", stopId));
return validateExist("Stop", stop, "stopId", stop);
}
private Route route(String routeId) {
var route = transitService().getRouteForId(createId("routeId", routeId));
return validateExist("Route", route, "routeId", routeId);
}
private Trip trip(String tripId) {
var trip = transitService().getTripForId(createId("tripId", tripId));
return validateExist("Trip", trip, "tripId", tripId);
}
private TripPattern tripPattern(String tripPatternId) {
var id = createId("patternId", tripPatternId);
var pattern = transitService().getTripPatternForId(id);
return validateExist("TripPattern", pattern, "patternId", tripPatternId);
}
private TripPattern tripPatternForTripId(String tripId) {
return tripPattern(trip(tripId));
}
private TripPattern tripPattern(Trip trip) {
var pattern = transitService().getPatternForTrip(trip);
return validateExist("TripPattern", pattern, "trip", trip.getId());
}
private RoutingService routingService() {
return serverContext.routingService();
}
private TransitService transitService() {
return serverContext.transitService();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy