package com.schneeloch.bostonbusmap_library.transit; import java.io.IOException; import java.io.InputStream; import java.security.interfaces.RSAPrivateCrtKey; import java.util.Collection; import java.util.List; import java.util.Map; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.schneeloch.bostonbusmap_library.data.BusLocation; import com.schneeloch.bostonbusmap_library.data.Directions; import com.schneeloch.bostonbusmap_library.data.IAlerts; import com.schneeloch.bostonbusmap_library.data.ITransitDrawables; import com.schneeloch.bostonbusmap_library.data.Location; import com.schneeloch.bostonbusmap_library.data.Locations; import com.schneeloch.bostonbusmap_library.data.RouteConfig; import com.schneeloch.bostonbusmap_library.data.RoutePool; import com.schneeloch.bostonbusmap_library.data.RouteStopPair; import com.schneeloch.bostonbusmap_library.data.RouteTitles; import com.schneeloch.bostonbusmap_library.data.Selection; import com.schneeloch.bostonbusmap_library.data.StopLocation; import com.schneeloch.bostonbusmap_library.data.TransitSourceCache; import com.schneeloch.bostonbusmap_library.data.TransitSourceTitles; import com.schneeloch.bostonbusmap_library.data.VehicleLocations; import com.schneeloch.bostonbusmap_library.database.Schema; import com.schneeloch.bostonbusmap_library.parser.BusPredictionsFeedParser; import com.schneeloch.bostonbusmap_library.parser.VehicleLocationsFeedParser; import com.schneeloch.bostonbusmap_library.util.DownloadHelper; import com.schneeloch.bostonbusmap_library.util.SearchHelper; /** * A transit source which accesses a NextBus webservice. Override for a specific agency * @author schneg * */ public abstract class NextBusTransitSource implements TransitSource { private final ITransitSystem transitSystem; private static final String prefix = "webservices"; /** * The XML feed URL */ private final String mbtaLocationsDataUrlOneRoute; private final String mbtaLocationsDataUrlAllRoutes; private final String mbtaRouteConfigDataUrl; private final String mbtaRouteConfigDataUrlAllRoutes; private final String mbtaPredictionsDataUrl; private final ITransitDrawables drawables; private final TransitSourceTitles routeTitles; private final TransitSourceCache cache; private static final Schema.Routes.SourceId[] transitSourceIds = new Schema.Routes.SourceId[] {Schema.Routes.SourceId.Bus}; public NextBusTransitSource(TransitSystem transitSystem, ITransitDrawables drawables, String agency, TransitSourceTitles routeTitles, RouteTitles allRouteTitles) { this.transitSystem = transitSystem; this.drawables = drawables; mbtaLocationsDataUrlOneRoute = "http://" + prefix + ".nextbus.com/service/publicXMLFeed?command=vehicleLocations&a=" + agency + "&t="; mbtaLocationsDataUrlAllRoutes = "http://" + prefix + ".nextbus.com/service/publicXMLFeed?command=vehicleLocations&a=" + agency + "&t="; mbtaRouteConfigDataUrl = "http://" + prefix + ".nextbus.com/service/publicXMLFeed?command=routeConfig&a=" + agency + "&r="; mbtaRouteConfigDataUrlAllRoutes = "http://" + prefix + ".nextbus.com/service/publicXMLFeed?command=routeConfig&a=" + agency; mbtaPredictionsDataUrl = "http://" + prefix + ".nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=" + agency; this.routeTitles = routeTitles; cache = new TransitSourceCache(); } @Override public void refreshData(RouteConfig routeConfig, Selection selection, int maxStops, double centerLatitude, double centerLongitude, VehicleLocations busMapping, RoutePool routePool, Directions directions, Locations locationsObj) throws IOException, ParserConfigurationException, SAXException { //read data from the URL DownloadHelper downloadHelper; Selection.Mode mode = selection.getMode(); switch (mode) { case BUS_PREDICTIONS_ONE: case BUS_PREDICTIONS_STAR: case BUS_PREDICTIONS_ALL: List<Location> locations = Lists.newArrayList(); for (ImmutableList<Location> group : locationsObj.getLocations(maxStops, centerLatitude, centerLongitude, false, selection)) { for (Location location : group) { if (location.getTransitSourceType() == Schema.Routes.SourceId.Bus) { locations.add(location); } } } //ok, do predictions now ImmutableSet<String> routes; if (mode == Selection.Mode.BUS_PREDICTIONS_ONE) { routes = ImmutableSet.of(routeConfig.getRouteName()); } else { routes = ImmutableSet.of(); } String url = getPredictionsUrl(locations, routes); if (url == null) { return; } downloadHelper = new DownloadHelper(url); break; case VEHICLE_LOCATIONS_ONE: { final String urlString = getVehicleLocationsUrl(locationsObj.getLastUpdateTime(), routeConfig.getRouteName()); if (urlString == null) { return; } downloadHelper = new DownloadHelper(urlString); break; } case VEHICLE_LOCATIONS_ALL: { final String urlString = getVehicleLocationsUrl(locationsObj.getLastUpdateTime(), null); if (urlString == null) { return; } downloadHelper = new DownloadHelper(urlString); break; } default: throw new RuntimeException("Unexpected enum"); } try { InputStream data = downloadHelper.getResponseData(); switch (mode) { case BUS_PREDICTIONS_ONE: case BUS_PREDICTIONS_ALL: case BUS_PREDICTIONS_STAR: { //bus prediction BusPredictionsFeedParser parser = new BusPredictionsFeedParser(routePool, directions); parser.runParse(data); // set last update time for downloaded stops ImmutableList<ImmutableList<Location>> groups = locationsObj.getLocations(maxStops, centerLatitude, centerLongitude, false, selection); List<Location> locations = Lists.newArrayList(); for (ImmutableList<Location> group : groups) { locations.addAll(group); } ImmutableSet<String> routes; if (mode == Selection.Mode.BUS_PREDICTIONS_ONE) { routes = ImmutableSet.of(routeConfig.getRouteName()); } else { routes = ImmutableSet.of(); } ImmutableList<RouteStopPair> pairs = getStopPairs(locations, routes); for (RouteStopPair pair : pairs) { cache.updatePredictionForStop(pair); } break; } case VEHICLE_LOCATIONS_ALL: case VEHICLE_LOCATIONS_ONE: { //vehicle locations VehicleLocationsFeedParser parser = new VehicleLocationsFeedParser(directions); parser.runParse(data); //get the time that this information is valid until locationsObj.setLastUpdateTime(parser.getLastUpdateTime()); Map<VehicleLocations.Key, BusLocation> newBuses = parser.getNewBuses(); busMapping.update(Schema.Routes.SourceId.Bus, routeTitles.routeTags(), true, newBuses); // now that we've succeeded, update last download times switch (mode) { case VEHICLE_LOCATIONS_ONE: cache.updateVehiclesForRoute(routeConfig.getRouteName()); break; case VEHICLE_LOCATIONS_ALL: cache.updateAllVehicles(); break; default: throw new RuntimeException("Unexpected mode"); } break; } default: throw new RuntimeException("Unexpected enum"); } } finally { downloadHelper.disconnect(); } } protected ImmutableList<RouteStopPair> getStopPairs(Collection<Location> locations, Collection<String> routes) { ImmutableList.Builder<RouteStopPair> builder = ImmutableList.builder(); for (Location location : locations) { if (location instanceof StopLocation) { StopLocation stopLocation = (StopLocation) location; if (stopLocation.getTransitSourceType() == Schema.Routes.SourceId.Bus) { if (routes.isEmpty() == false) { for (String route : routes) { if (stopLocation.hasRoute(route)) { RouteStopPair pair = new RouteStopPair(route, stopLocation.getStopTag()); if (cache.canUpdatePredictionForStop(pair)) { builder.add(pair); } } } } else { for (String stopRoute : stopLocation.getRoutes()) { RouteStopPair pair = new RouteStopPair(stopRoute, stopLocation.getStopTag()); if (cache.canUpdatePredictionForStop(pair)) { builder.add(pair); } } } } } } return builder.build(); } protected String getPredictionsUrl(Collection<Location> locations, Collection<String> routes) { StringBuilder urlString = new StringBuilder(mbtaPredictionsDataUrl); //TODO: hard limit this to 150 requests ImmutableList<RouteStopPair> pairs = getStopPairs(locations, routes); if (pairs.size() == 0) { return null; } for (RouteStopPair pair : pairs) { urlString.append("&stops=").append(pair.getRoute()).append("%7C"); urlString.append("%7C").append(pair.getStopId()); } return urlString.toString(); } protected String getVehicleLocationsUrl(long time, String route) { String url = null; if (route != null) { if (cache.canUpdateVehiclesForRoute(route)) { url = mbtaLocationsDataUrlOneRoute + time + "&r=" + route; } } else { if (cache.canUpdateAllVehicles()) { url = mbtaLocationsDataUrlAllRoutes + time; } } return url; } @Override public boolean hasPaths() { return true; } @Override public StopLocation createStop(float lat, float lon, String stopTag, String title, String route, Optional<String> parent) { StopLocation stop = new StopLocation.Builder(lat, lon, stopTag, title, parent).build(); stop.addRoute(route); return stop; } @Override public String searchForRoute(String indexingQuery, String lowercaseQuery) { return SearchHelper.naiveSearch(indexingQuery, lowercaseQuery, transitSystem.getRouteKeysToTitles()); } @Override public ITransitDrawables getDrawables() { return drawables; } @Override public int getLoadOrder() { return 1; } @Override public TransitSourceTitles getRouteTitles() { return routeTitles; } @Override public Schema.Routes.SourceId[] getTransitSourceIds() { return transitSourceIds; } @Override public boolean requiresSubwayTable() { return false; } @Override public IAlerts getAlerts() { return transitSystem.getAlerts(); } @Override public String getDescription() { return "Bus"; } @Override public BusLocation createVehicleLocation(float latitude, float longitude, String id, long lastFeedUpdateInMillis, Optional<Integer> heading, String routeName, String headsign) { return new BusLocation(latitude, longitude, id, lastFeedUpdateInMillis, heading, routeName, headsign); } }