/* 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.ws; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.xml.bind.annotation.XmlRootElement; import lombok.Setter; import org.codehaus.jettison.json.JSONException; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Route; import org.onebusaway.gtfs.model.ServiceCalendar; import org.onebusaway.gtfs.model.ServiceCalendarDate; import org.onebusaway.gtfs.model.Stop; import org.onebusaway.gtfs.model.Trip; import org.opentripplanner.api.model.error.TransitError; import org.opentripplanner.api.model.transit.AgencyList; import org.opentripplanner.api.model.transit.CalendarData; import org.opentripplanner.api.model.transit.ModeList; import org.opentripplanner.api.model.transit.RouteData; import org.opentripplanner.api.model.transit.RouteDataList; import org.opentripplanner.api.model.transit.RouteList; import org.opentripplanner.api.model.transit.ServiceCalendarData; import org.opentripplanner.api.model.transit.StopList; import org.opentripplanner.api.model.transit.StopTime; import org.opentripplanner.api.model.transit.StopTimeList; import org.opentripplanner.api.model.transit.TripList; import org.opentripplanner.api.model.transit.TripMatch; import org.opentripplanner.common.geometry.DistanceLibrary; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.common.model.P2; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.ServiceDay; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.StateEditor; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.edgetype.PatternHop; import org.opentripplanner.routing.edgetype.TableTripPattern; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.services.GraphService; import org.opentripplanner.routing.services.StreetVertexIndexService; import org.opentripplanner.routing.services.TransitIndexService; import org.opentripplanner.routing.transit_index.RouteSegment; import org.opentripplanner.routing.transit_index.RouteVariant; import org.opentripplanner.routing.transit_index.adapters.RouteType; import org.opentripplanner.routing.transit_index.adapters.ServiceCalendarDateType; import org.opentripplanner.routing.transit_index.adapters.ServiceCalendarType; import org.opentripplanner.routing.transit_index.adapters.StopType; import org.opentripplanner.routing.transit_index.adapters.TripType; import org.opentripplanner.routing.trippattern.TripTimes; import org.opentripplanner.routing.vertextype.TransitStop; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.sun.jersey.api.core.InjectParam; import com.sun.jersey.api.spring.Autowire; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.LineString; // NOTE - /ws/transit is the full path -- see web.xml @Path("/transit") @XmlRootElement @Autowire public class TransitIndex { @SuppressWarnings("unused") private static final Logger LOG = LoggerFactory.getLogger(TransitIndex.class); private static final double STOP_SEARCH_RADIUS = 200; @Setter @InjectParam private GraphService graphService; private static final long MAX_STOP_TIME_QUERY_INTERVAL = 86400 * 2; /** * Return a list of all agency ids in the graph */ @GET @Path("/agencyIds") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public AgencyList getAgencyIds(@QueryParam("routerId") String routerId) throws JSONException { Graph graph = getGraph(routerId); AgencyList response = new AgencyList(); response.agencies = graph.getAgencies(); return response; } /** Return data about a route, such as its names, color, variants, stops, and directions. A variant represents a particular stop pattern (ordered list of stops) on a particular route. For example, the N train has at least four different variants: express (over the Manhattan bridge), and local (via lower Manhattan and the tunnel) x to Astoria and to Coney Island. Variant names are machine-generated, and are guaranteed to be unique (among variants for a route) but not stable across graph builds. A route's stops include stops made by any variant of the route. */ @GET @Path("/routeData") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getRouteData(@QueryParam("agency") String agency, @QueryParam("id") String id, @QueryParam("references") Boolean references, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId) throws JSONException { TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } RouteDataList respond = new RouteDataList(); for (String agencyId : getAgenciesIds(agency, routerId)) { AgencyAndId routeId = new AgencyAndId(agencyId, id); List<RouteVariant> variants = transitIndexService.getVariantsForRoute(routeId); if (variants.isEmpty()) continue; RouteData response = new RouteData(); response.id = routeId; response.variants = variants; response.directions = new ArrayList<String>( transitIndexService.getDirectionsForRoute(routeId)); response.route = new RouteType(); for (RouteVariant variant : transitIndexService.getVariantsForRoute(routeId)) { Route route = variant.getRoute(); response.route = new RouteType(route, extended); break; } if (references != null && references.equals(true)) { response.stops = new ArrayList<StopType>(); for (org.onebusaway.gtfs.model.Stop stop : transitIndexService .getStopsForRoute(routeId)) response.stops.add(new StopType(stop, extended)); } respond.routeData.add(response); } return respond; } /** * Return a list of route ids */ @GET @Path("/routes") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getRoutes(@QueryParam("agency") String agency, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId) throws JSONException { TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } Collection<AgencyAndId> allRouteIds = transitIndexService.getAllRouteIds(); RouteList response = makeRouteList(allRouteIds, agency, extended, routerId); return response; } private RouteList makeRouteList(Collection<AgencyAndId> routeIds, String agencyFilter, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId) { RouteList response = new RouteList(); TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); for (AgencyAndId routeId : routeIds) { for (RouteVariant variant : transitIndexService.getVariantsForRoute(routeId)) { Route route = variant.getRoute(); if (agencyFilter != null && !agencyFilter.equals(route.getAgency().getId())) continue; RouteType routeType = new RouteType(route, extended); response.routes.add(routeType); break; } } return response; } /** * Return stops near a point. The default search radius is 200m, but this can be changed with the radius parameter (in meters) */ @GET @Path("/stopsNearPoint") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getStopsNearPoint(@QueryParam("agency") String agency, @QueryParam("lat") Double lat, @QueryParam("lon") Double lon, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId, @QueryParam("radius") Double radius) throws JSONException { // default search radius. Double searchRadius = (radius == null) ? STOP_SEARCH_RADIUS : radius; Graph graph = getGraph(routerId); if (Double.isNaN(searchRadius) || searchRadius <= 0) { searchRadius = STOP_SEARCH_RADIUS; } StreetVertexIndexService streetVertexIndexService = graph.streetIndex; List<TransitStop> stops = streetVertexIndexService.getNearbyTransitStops(new Coordinate( lon, lat), searchRadius); TransitIndexService transitIndexService = graph.getService(TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } StopList response = new StopList(); for (TransitStop transitStop : stops) { AgencyAndId stopId = transitStop.getStopId(); if (agency != null && !agency.equals(stopId.getAgencyId())) continue; StopType stop = new StopType(transitStop.getStop(), extended); stop.routes = transitIndexService.getRoutesForStop(stopId); response.stops.add(stop); } return response; } /** * Return routes that a stop is served by */ @GET @Path("/routesForStop") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getRoutesForStop(@QueryParam("agency") String agency, @QueryParam("id") String id, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId) throws JSONException { TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } RouteList result = new RouteList(); for (String string : getAgenciesIds(agency, routerId)) { List<AgencyAndId> routes = transitIndexService.getRoutesForStop(new AgencyAndId(string, id)); result.routes.addAll(makeRouteList(routes, null, extended, routerId).routes); } return result; } /** * Return stop times for a stop, in seconds since the epoch startTime and endTime are in milliseconds since epoch */ @GET @Path("/stopTimesForStop") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getStopTimesForStop(@QueryParam("agency") String stopAgency, @QueryParam("id") String stopId, @QueryParam("startTime") long startTime, @QueryParam("endTime") Long endTime, @QueryParam("extended") Boolean extended, @QueryParam("references") Boolean references, @QueryParam("routeId") String routeId, @QueryParam("routerId") String routerId) throws JSONException { startTime /= 1000; if (endTime == null) { endTime = startTime + 86400; } else { endTime /= 1000; } if (endTime - startTime > MAX_STOP_TIME_QUERY_INTERVAL) { return new TransitError("Max stop time query interval is " + (endTime - startTime) + " > " + MAX_STOP_TIME_QUERY_INTERVAL); } TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } // add all departures HashSet<TripType> trips = new HashSet<TripType>(); StopTimeList result = new StopTimeList(); result.stopTimes = new ArrayList<StopTime>(); if (references != null && references.equals(true)) { result.routes = new HashSet<Route>(); } for (String stopAgencyId : getAgenciesIds(stopAgency, routerId)) { AgencyAndId stop = new AgencyAndId(stopAgencyId, stopId); Edge preBoardEdge = transitIndexService.getPreBoardEdge(stop); if (preBoardEdge == null) continue; Vertex boarding = preBoardEdge.getToVertex(); RoutingRequest options = makeTraverseOptions(startTime, routerId); HashMap<Long, Edge> seen = new HashMap<Long, Edge>(); //OUTER: for (Edge e : boarding.getOutgoing()) { // each of these edges boards a separate set of trips for (StopTime st : getStopTimesForBoardEdge(startTime, endTime, options, e, extended)) { // different parameters st.phase = "departure"; if (extended != null && extended.equals(true)) { if (routeId != null && !routeId.equals("") && !st.trip.getRoute().getId().getId().equals(routeId)) continue; if (references != null && references.equals(true)) result.routes.add(st.trip.getRoute()); result.stopTimes.add(st); } else result.stopTimes.add(st); trips.add(st.trip); if (seen.containsKey(st.time)) { Edge old = seen.get(st.time); System.out.println("DUP: " + old); getStopTimesForBoardEdge(startTime, endTime, options, e, extended); // break OUTER; } seen.put(st.time, e); } } // add the arriving stop times for cases where there are no departures Edge preAlightEdge = transitIndexService.getPreAlightEdge(stop); Vertex alighting = preAlightEdge.getFromVertex(); for (Edge e : alighting.getIncoming()) { for (StopTime st : getStopTimesForAlightEdge(startTime, endTime, options, e, extended)) { if (!trips.contains(st.trip)) { // diffrent parameters st.phase = "arrival"; if (extended != null && extended.equals(true)) { if (references != null && references.equals(true)) result.routes.add(st.trip.getRoute()); if (routeId != null && !routeId.equals("") && !st.trip.getRoute().getId().getId().equals(routeId)) continue; result.stopTimes.add(st); } else result.stopTimes.add(st); } } } } Collections.sort(result.stopTimes, new Comparator<StopTime>() { @Override public int compare(StopTime o1, StopTime o2) { if (o1.phase.equals("arrival") && o2.phase.equals("departure")) return 1; if (o1.phase.equals("departure") && o2.phase.equals("arrival")) return -1; return o1.time - o2.time > 0 ? 1 : -1; } }); return result; } private RoutingRequest makeTraverseOptions(long startTime, String routerId) { RoutingRequest options = new RoutingRequest(); // if (graphService.getCalendarService() != null) { // options.setCalendarService(graphService.getCalendarService()); // options.setServiceDays(startTime, agencies); // } // TODO: verify correctness options.dateTime = startTime; Graph graph = getGraph(routerId); Collection<Vertex> vertices = graph.getVertices(); Iterator<Vertex> it = vertices.iterator(); options.setFromString(it.next().getLabel()); options.setToString(it.next().getLabel()); options.setRoutingContext(graph); return options; } /** * Return variant for a trip */ @GET @Path("/variantForTrip") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getVariantForTrip(@QueryParam("tripAgency") String tripAgency, @QueryParam("tripId") String tripId, @QueryParam("routerId") String routerId) throws JSONException { TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } AgencyAndId trip = new AgencyAndId(tripAgency, tripId); RouteVariant variant = transitIndexService.getVariantForTrip(trip); return variant; } /** * Return information about calendar for given agency */ @GET @Path("/calendar") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getCalendar(@QueryParam("agency") String agency, @QueryParam("routerId") String routerId) throws JSONException { TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } CalendarData response = new CalendarData(); response.calendarList = new ArrayList<ServiceCalendarType>(); response.calendarDatesList = new ArrayList<ServiceCalendarDateType>(); for (String agencyId : getAgenciesIds(agency, routerId)) { List<ServiceCalendar> scList = transitIndexService.getCalendarsByAgency(agencyId); List<ServiceCalendarDate> scdList = transitIndexService .getCalendarDatesByAgency(agencyId); if (scList != null) for (ServiceCalendar sc : scList) response.calendarList.add(new ServiceCalendarType(sc)); if (scdList != null) for (ServiceCalendarDate scd : scdList) response.calendarDatesList.add(new ServiceCalendarDateType(scd)); } return response; } /** * Return subsequent stop times for a trip; time is in milliseconds since epoch */ @GET @Path("/stopTimesForTrip") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getStopTimesForTrip(@QueryParam("stopAgency") String stopAgency, @QueryParam("stopId") String stopId, @QueryParam("tripAgency") String tripAgency, @QueryParam("tripId") String tripId, @QueryParam("time") long time, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId) throws JSONException { time /= 1000; AgencyAndId firstStop = null; if (stopId != null) { firstStop = new AgencyAndId(stopAgency, stopId); } AgencyAndId trip = new AgencyAndId(tripAgency, tripId); TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } RouteVariant variant = transitIndexService.getVariantForTrip(trip); RoutingRequest options = makeTraverseOptions(time, routerId); StopTimeList result = new StopTimeList(); result.stopTimes = new ArrayList<StopTime>(); State state = null; RouteSegment start = null; for (RouteSegment segment : variant.getSegments()) { // this is all segments across all patterns that match this variant if (segment.stop.equals(firstStop)) { // this might be the correct start segment, but we need to try traversing and see if we get this trip // TODO: verify options and state creation correctness (AMB) State s0 = new State(segment.board.getFromVertex(), options); state = segment.board.traverse(s0); if (state == null) continue; if (state.getBackTrip().getId().equals(trip)) { start = segment; StopTime st = new StopTime(); st.time = state.getTimeSeconds(); for (org.onebusaway.gtfs.model.Stop stop : variant.getStops()) if (stop.getId().equals(segment.stop)) { st.stop = new StopType(stop, extended); } result.stopTimes.add(st); break; } } } if (start == null) { return null; } for (RouteSegment segment : variant.segmentsAfter(start)) { // TODO: verify options/state init correctness StateEditor se = state.edit(null); State s0 = se.makeState(); state = segment.hopIn.traverse(s0); StopTime st = new StopTime(); st.time = state.getTimeSeconds(); if (extended) { for (org.onebusaway.gtfs.model.Stop stop : variant.getStops()) { if (stop.getId().equals(segment.stop)) { st.stop = new StopType(stop, extended); } } } result.stopTimes.add(st); } return result; } private List<StopTime> getStopTimesForBoardEdge(long startTime, long endTime, RoutingRequest options, Edge e, Boolean extended) { List<StopTime> out = new ArrayList<StopTime>(); State result; long time = startTime; do { // TODO verify options/state correctness State s0 = new State(e.getFromVertex(), time, options); result = e.traverse(s0); if (result == null) break; time = result.getTimeSeconds(); if (time > endTime) break; StopTime stopTime = new StopTime(); stopTime.time = time; stopTime.trip = new TripType(result.getBackTrip(), extended); out.add(stopTime); time += 1; // move to the next board time } while (true); return out; } private List<StopTime> getStopTimesForAlightEdge(long startTime, long endTime, RoutingRequest options, Edge e, Boolean extended) { List<StopTime> out = new ArrayList<StopTime>(); State result; long time = endTime; options = options.reversedClone(); do { // TODO: verify options/state correctness State s0 = new State(e.getToVertex(), time, options); result = e.traverse(s0); if (result == null) break; time = result.getTimeSeconds(); if (time < startTime) break; StopTime stopTime = new StopTime(); stopTime.time = time; stopTime.trip = new TripType(result.getBackTrip(), extended); out.add(stopTime); time -= 1; // move to the previous alight time } while (true); return out; } /** * Return a list of all available transit modes supported, if any. * * @throws JSONException */ @GET @Path("/modes") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object getModes(@QueryParam("routerId") String routerId) throws JSONException { TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } ModeList modes = new ModeList(); modes.modes = new ArrayList<TraverseMode>(); for (TraverseMode mode : transitIndexService.getAllModes()) { modes.modes.add(mode); } return modes; } private Graph getGraph(String routerId) { return graphService.getGraph(routerId); } public Object getCalendarServiceDataForAgency(@QueryParam("agency") String agency, @QueryParam("routerId") String routerId) { TransitIndexService transitIndexService = getGraph(routerId).getService( TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } ServiceCalendarData data = new ServiceCalendarData(); data.calendars = transitIndexService.getCalendarsByAgency(agency); data.calendarDates = transitIndexService.getCalendarDatesByAgency(agency); return data; } /** * Return a list of all stops that are inside a rectangle given by lat lon positions. */ @GET @Path("/stopsInRectangle") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object stopsInRectangle(@QueryParam("agency") String agency, @QueryParam("leftUpLat") Double leftUpLat, @QueryParam("leftUpLon") Double leftUpLon, @QueryParam("rightDownLat") Double rightDownLat, @QueryParam("rightDownLon") Double rightDownLon, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId) throws JSONException { Graph graph = getGraph(routerId); StopList response = new StopList(); StreetVertexIndexService streetVertexIndexService = graph.streetIndex; if (leftUpLat == null || leftUpLon == null || rightDownLat == null || rightDownLon == null) { double METERS_PER_DEGREE_LAT = 111111; double distance = 2000; for (Vertex gv : graph.getVertices()) { if (gv instanceof TransitStop) { Coordinate c = gv.getCoordinate(); Envelope env = new Envelope(c); double meters_per_degree_lon_here = METERS_PER_DEGREE_LAT * Math.cos(Math.toRadians(c.y)); env.expandBy(distance / meters_per_degree_lon_here, distance / METERS_PER_DEGREE_LAT); StopType stop = new StopType(((TransitStop) gv).getStop(), extended); response.stops.add(stop); } } } else { Coordinate cOne = new Coordinate(leftUpLon, leftUpLat); Coordinate cTwo = new Coordinate(rightDownLon, rightDownLat); List<TransitStop> stops = streetVertexIndexService.getNearbyTransitStops(cOne, cTwo); TransitIndexService transitIndexService = graph.getService(TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } for (TransitStop transitStop : stops) { AgencyAndId stopId = transitStop.getStopId(); if (agency != null && !agency.equals(stopId.getAgencyId())) continue; StopType stop = new StopType(transitStop.getStop(), extended); if (extended != null && extended.equals(true)) stop.routes = transitIndexService.getRoutesForStop(stopId); response.stops.add(stop); } } return response; } /** * Return a list of all routes that operate between start stop and end stop. */ @GET @Path("/routesBetweenStops") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object routesBetweenStops(@QueryParam("startAgency") String startAgency, @QueryParam("endAgency") String endAgency, @QueryParam("startStopId") String startStopId, @QueryParam("endStopId") String endStopId, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId) throws JSONException { RouteList response = new RouteList(); RouteList routeList = (RouteList) this.getRoutesForStop(startAgency, startStopId, extended, routerId); for (RouteType route : routeList.routes) { for (String agency : getAgenciesIds(null, routerId)) { if (ifRouteBetweenStops(route, agency, routerId, startStopId, endStopId, endAgency)) response.routes.add(route); } } return response; } private Boolean ifRouteBetweenStops(RouteType route, String agency, String routerId, String startStopId, String endStopId, String endAgency) throws JSONException { RouteDataList routeDataList = (RouteDataList) this.getRouteData(agency, route.getId() .getId(), false, false, routerId); for (RouteData routeData : routeDataList.routeData) for (RouteVariant variant : routeData.variants) for (String endStopAgency : getAgenciesIds(endAgency, routerId)) { Boolean start = false; for (Stop stop : variant.getStops()) { if (stop.getId().getId().equals(startStopId)) start = true; if (start && stop.getId().equals(new AgencyAndId(endStopAgency, endStopId))) { return true; } } } return false; } private ArrayList<String> getAgenciesIds(String agency, String routerId) { Graph graph = getGraph(routerId); ArrayList<String> agencyList = new ArrayList<String>(); if (agency == null || agency.equals("")) { for (String a : graph.getAgencyIds()) { agencyList.add(a); } } else { agencyList.add(agency); } return agencyList; } /** * Return a list of all trips for a given route nearby a certain point (used for on-board depart * when client does not know trip ID), sorted by a matching (=relevance) factor. */ @GET @Path("/tripsAtPosition") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) public Object tripsAtPosition(@QueryParam("routeId") String routeIdStr, @QueryParam("latitude") Double latitude, @QueryParam("longitude") Double longitude, @QueryParam("maxReturnedValues") Integer maxReturnedValues, @QueryParam("timeSec") Long timeSec, @QueryParam("extended") Boolean extended, @QueryParam("routerId") String routerId) throws JSONException { // Specify sensible default values if not present if (timeSec == null) timeSec = System.currentTimeMillis() / 1000; if (maxReturnedValues == null) maxReturnedValues = 10; if (extended == null) extended = false; Graph graph = getGraph(routerId); TransitIndexService transitIndexService = graph.getService(TransitIndexService.class); if (transitIndexService == null) { return new TransitError( "No transit index found. Add TransitIndexBuilder to your graph builder configuration and rebuild your graph."); } AgencyAndId routeId = AgencyAndId.convertFromString(routeIdStr); List<RouteVariant> variants = transitIndexService.getVariantsForRoute(routeId); // variants can't be null here Coordinate position = new Coordinate(longitude, latitude); List<ServiceDay> serviceDays = getServiceDays(graph, timeSec, routeId.getAgencyId()); // Brute-force approach: number of variants for a route should be rather small TripList response = new TripList(); for (RouteVariant variant : variants) { response.tripMatches.addAll(matchTripsForVariant(variant, position, timeSec, serviceDays, extended)); } Collections.sort(response.tripMatches, new Comparator<TripMatch>() { @Override public int compare(TripMatch o1, TripMatch o2) { return o1.matchFactor - o2.matchFactor < 0 ? -1 : +1; } }); if (response.tripMatches.size() > maxReturnedValues) response.tripMatches = response.tripMatches.subList(0, maxReturnedValues); return response; } private List<TripMatch> matchTripsForVariant(RouteVariant variant, Coordinate position, long timeSec, List<ServiceDay> serviceDays, boolean extended) { List<TripMatch> matches = new ArrayList<TripMatch>(); DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance(); // Compute for each segment (distance, % inside segment, segment length) from the position List<Double> distToShape = new ArrayList<Double>(); List<Double> fracOfShape = new ArrayList<Double>(); List<Double> shapeLength = new ArrayList<Double>(); int nHops = variant.getStops().size() - 1; for (int hop = 0; hop < nHops; hop++) { LineString geometry = variant.getGeometrySegment(hop); double distanceMeter = distanceLibrary.fastDistance(position, geometry); P2<LineString> geomPair = GeometryUtils.splitGeometryAtPoint(geometry, position); double shapeLen = distanceLibrary.fastLength(geometry); double coverLen = distanceLibrary.fastLength(geomPair.getFirst()); double fraction = shapeLen > 0.01 ? coverLen / shapeLen : 0.0; distToShape.add(distanceMeter); fracOfShape.add(fraction); shapeLength.add(shapeLen); } Set<AgencyAndId> processedTrips = new HashSet<AgencyAndId>(); for (RouteSegment segment : variant.getSegments()) { if (segment.hopOut != null && segment.hopOut instanceof PatternHop) { PatternHop patternHop = (PatternHop) segment.hopOut; TableTripPattern tripPattern = patternHop.getPattern(); int serviceId = tripPattern.getServiceId(); for (Trip trip : tripPattern.getTrips()) { if (!processedTrips.contains(trip.getId())) { processedTrips.add(trip.getId()); // Compute trip times TripTimes tripTimes = tripPattern.getTripTimes(tripPattern .getTripIndex(trip.getId())); assert tripTimes.getNumHops() == nHops; TripMatch tripMatch = matchTrip(timeSec, trip, tripTimes, serviceId, distToShape, fracOfShape, shapeLength, serviceDays, extended); if (tripMatch != null) matches.add(tripMatch); } } } } return matches; } /** * Match a trip and compute it's matching factor. * @return The TripMatch if the trip matches or null if not. */ private TripMatch matchTrip(long timeSec, Trip trip, TripTimes tripTimes, int serviceId, List<Double> distToShape, List<Double> fracOfShape, List<Double> shapeLength, List<ServiceDay> serviceDays, boolean extended) { final int ONE_DAY = 24 * 60 * 60; double dXmin = Double.MAX_VALUE; double dTmin = Double.MAX_VALUE; double dLmin = Double.MAX_VALUE; int nHops = tripTimes.getNumHops(); // For each hop (= variant segment), compute // the best one in term of distance AND time delta. for (ServiceDay sd : serviceDays) { if (!sd.serviceIdRunning(serviceId)) continue; for (int hop = 0; hop < nHops - 1; hop++) { double dL = distToShape.get(hop); double fraction = fracOfShape.get(hop); double shapeLen = shapeLength.get(hop); int depTime = tripTimes.getDepartureTime(hop); int hopTime = tripTimes.getArrivalTime(hop) - depTime; // hopTime=0 usually means rounded time to the minute, // so let's assume a minimum value of 30 seconds/hop. double speedMs = hopTime < 30 ? shapeLen / 30 : shapeLen / hopTime; double dT = Math.abs(sd.time(depTime + (int) Math.round(hopTime * fraction)) - timeSec); // Here is magic: dX = dT . S + dL double dX = dT * speedMs + dL; if (dX < dXmin) { dXmin = dX; dTmin = dT; dLmin = dL; } } } if (dXmin < Double.MAX_VALUE && dTmin < ONE_DAY) { TripMatch tripMatch = new TripMatch(); tripMatch.trip = new TripType(trip, extended); tripMatch.matchDistanceMeter = dLmin; tripMatch.matchTimeSeconds = dTmin; tripMatch.matchFactor = dXmin; return tripMatch; } else { return null; } } /** * @returnĀ 3 service days: yesterday, today and tomorrow for an agency. */ private List<ServiceDay> getServiceDays(Graph graph, long epochSec, String agencyId) { final long SEC_IN_DAY = 60 * 60 * 24; List<ServiceDay> serviceDays = new ArrayList<ServiceDay>(3); serviceDays.add(new ServiceDay(graph, epochSec - SEC_IN_DAY, graph.getCalendarService(), agencyId)); serviceDays.add(new ServiceDay(graph, epochSec, graph.getCalendarService(), agencyId)); serviceDays.add(new ServiceDay(graph, epochSec + SEC_IN_DAY, graph.getCalendarService(), agencyId)); return serviceDays; } }