package com.schneeloch.bostonbusmap_library.transit; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.schneeloch.bostonbusmap_library.data.BusLocation; import com.schneeloch.bostonbusmap_library.data.CommuterRailStopLocation; import com.schneeloch.bostonbusmap_library.data.CommuterTrainLocation; 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.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.CommuterRailPredictionsFeedParser; import com.schneeloch.bostonbusmap_library.util.DownloadHelper; import com.schneeloch.bostonbusmap_library.util.FeedException; import com.schneeloch.bostonbusmap_library.util.LogUtil; import com.schneeloch.bostonbusmap_library.util.SearchHelper; import org.xml.sax.SAXException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.List; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; import javax.xml.parsers.ParserConfigurationException; public class CommuterRailTransitSource implements TransitSource { private final ITransitDrawables drawables; private final TransitSourceTitles routeTitles; private final TransitSystem transitSystem; public static final int COLOR = 0x940088; private final TransitSourceCache cache; private final ImmutableMap<String, String> routesToUrls; private static final Schema.Routes.SourceId[] transitSourceIds = new Schema.Routes.SourceId[] {}; public CommuterRailTransitSource(ITransitDrawables drawables, TransitSourceTitles routeTitles, TransitSystem transitSystem) { this.drawables = drawables; this.routeTitles = routeTitles; this.transitSystem = transitSystem; final String predictionsUrlSuffix = ".json"; final String dataUrlPrefix = "http://developer.mbta.com/lib/RTCR/RailLine_"; ImmutableMap.Builder<String, String> urlBuilder = ImmutableMap.builder(); urlBuilder.put("CR-Greenbush", dataUrlPrefix + 1 + predictionsUrlSuffix); urlBuilder.put("CR-Kingston", dataUrlPrefix + 2 + predictionsUrlSuffix); urlBuilder.put("CR-Middleborough", dataUrlPrefix + 3 + predictionsUrlSuffix); urlBuilder.put("CR-Fairmount", dataUrlPrefix + 4 + predictionsUrlSuffix); urlBuilder.put("CR-Providence", dataUrlPrefix + 5 + predictionsUrlSuffix); urlBuilder.put("CR-Franklin", dataUrlPrefix + 6 + predictionsUrlSuffix); urlBuilder.put("CR-Needham", dataUrlPrefix + 7 + predictionsUrlSuffix); urlBuilder.put("CR-Worcester", dataUrlPrefix + 8 + predictionsUrlSuffix); urlBuilder.put("CR-Fitchburg", dataUrlPrefix + 9 + predictionsUrlSuffix); urlBuilder.put("CR-Lowell", dataUrlPrefix + 10 + predictionsUrlSuffix); urlBuilder.put("CR-Haverhill", dataUrlPrefix + 11 + predictionsUrlSuffix); urlBuilder.put("CR-Newburyport", dataUrlPrefix + 12 + predictionsUrlSuffix); routesToUrls = urlBuilder.build(); cache = new TransitSourceCache(); } @Override public void refreshData(RouteConfig routeConfig, Selection selection, int maxStops, double centerLatitude, double centerLongitude, final VehicleLocations busMapping, final RoutePool routePool, final Directions directions, final Locations locationsObj) throws IOException, ParserConfigurationException, SAXException { Selection.Mode selectedBusPredictions = selection.getMode(); List<RefreshData> outputData = Lists.newArrayList(); switch (selectedBusPredictions) { case BUS_PREDICTIONS_ONE: case VEHICLE_LOCATIONS_ONE: { ImmutableList<ImmutableList<Location>> locations = locationsObj.getLocations(maxStops, centerLatitude, centerLongitude, false, selection); //ok, do predictions now getPredictionsUrl(locations, maxStops, routeConfig.getRouteName(), outputData, selectedBusPredictions); break; } case BUS_PREDICTIONS_ALL: case VEHICLE_LOCATIONS_ALL: case BUS_PREDICTIONS_STAR: { ImmutableList<ImmutableList<Location>> locations = locationsObj.getLocations(maxStops, centerLatitude, centerLongitude, false, selection); getPredictionsUrl(locations, maxStops, null, outputData, selectedBusPredictions); break; } default: throw new RuntimeException("Unexpected mode"); } // split into up to 12 threads final ConcurrentHashMap<String, Exception> exceptions = new ConcurrentHashMap<>(); List<Thread> threads = Lists.newArrayList(); for (final RefreshData outputRow : outputData) { Thread thread = new Thread(new Runnable() { @Override public void run() { try { String url = outputRow.url; DownloadHelper downloadHelper = new DownloadHelper(url); try { InputStream stream = downloadHelper.getResponseData(); InputStreamReader data = new InputStreamReader(stream); //StringReader data = new StringReader(hardcodedData); //bus prediction String route = outputRow.route; RouteConfig railRouteConfig = routePool.get(route); CommuterRailPredictionsFeedParser parser = new CommuterRailPredictionsFeedParser(railRouteConfig, directions, busMapping, locationsObj.getRouteTitles()); parser.runParse(data); data.close(); } finally { downloadHelper.disconnect(); } } catch (Exception e) { exceptions.put(outputRow.route, e); } } }); threads.add(thread); thread.start(); } for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { LogUtil.e(e); } } if (exceptions.size() > 0) { // hopefully no more than one. We can't throw more than one exception at a time String key = exceptions.keySet().iterator().next(); Exception exception = exceptions.get(key); throw new FeedException("Error downloading from commuter rail route " + key + " data feed", exception); } for (RefreshData refreshData : outputData) { cache.updateVehiclesForRoute(refreshData.route); cache.updatePredictionForRoute(refreshData.route); } } private static class RefreshData { private final String url; private final String route; public RefreshData(String url, String route) { this.url = url; this.route = route; } } private void getPredictionsUrl(ImmutableList<ImmutableList<Location>> locationGroups, int maxStops, String routeName, List<RefreshData> outputData, Selection.Mode mode) { //http://developer.mbta.com/lib/RTCR/RailLine_1.csv //BUS_PREDICTIONS_ONE or VEHICLE_LOCATIONS_ONE if (routeName != null) { //we know we're updating only one route if (isCommuterRail(routeName)) { String url = routesToUrls.get(routeName); if (cache.canUpdatePredictionForRoute(routeName) && cache.canUpdateVehiclesForRoute(routeName)) { outputData.add(new RefreshData(url, routeName)); } return; } } else { if (mode == Selection.Mode.BUS_PREDICTIONS_STAR) { //ok, let's look at the locations and see what we can get for (ImmutableList<Location> group : locationGroups) { for (Location location : group) { if (location instanceof StopLocation) { StopLocation stopLocation = (StopLocation) location; for (String route : stopLocation.getRoutes()) { if (isCommuterRail(route) && containsRoute(route, outputData) == false) { String url = routesToUrls.get(route); if (cache.canUpdatePredictionForRoute(route) && cache.canUpdateVehiclesForRoute(route)) { outputData.add(new RefreshData(url, route)); } } } } else if (location instanceof BusLocation) { //bus location BusLocation busLocation = (BusLocation) location; String route = busLocation.getRouteId(); if (isCommuterRail(route) && containsRoute(route, outputData) == false) { String url = routesToUrls.get(route); if (cache.canUpdatePredictionForRoute(route) && cache.canUpdateVehiclesForRoute(route)) { outputData.add(new RefreshData(url, route)); } } } } } } else { //add all 12 of them for (String route : routesToUrls.keySet()) { String url = routesToUrls.get(route); if (cache.canUpdatePredictionForRoute(route) && cache.canUpdateVehiclesForRoute(route)) { outputData.add(new RefreshData(url, route)); } } } } } private static boolean containsRoute(String route, List<RefreshData> outputData) { boolean containsRoute = false; for (RefreshData row : outputData) { if (row.route.equals(route)) { containsRoute = true; break; } } return containsRoute; } private boolean isCommuterRail(String routeName) { return routeTitles.hasRoute(routeName); } @Override public boolean hasPaths() { return false; } @Override public ITransitDrawables getDrawables() { return drawables; } @Override public String searchForRoute(String indexingQuery, String lowercaseQuery) { //try splitting up the route keys along the diagonal and see if they match one piece of it for (String route : routeTitles.routeTags()) { String title = routeTitles.getTitle(route); if (title.contains("/")) { String[] pieces = title.split("/"); for (int i = 0; i < pieces.length; i++) { if (lowercaseQuery.equals(pieces[i].toLowerCase(Locale.US))) { return route; } } } } return SearchHelper.naiveSearch(indexingQuery, lowercaseQuery, routeTitles); } @Override public CommuterRailStopLocation createStop(float latitude, float longitude, String stopTag, String stopTitle, String route, Optional<String> parent) { CommuterRailStopLocation stop = new CommuterRailStopLocation.CommuterRailBuilder( latitude, longitude, stopTag, stopTitle, parent).build(); stop.addRoute(route); return stop; } @Override public BusLocation createVehicleLocation(float latitude, float longitude, String id, long lastFeedUpdateInMillis, Optional<Integer> heading, String routeName, String headsign) { return new CommuterTrainLocation(latitude, longitude, id, lastFeedUpdateInMillis, heading, routeName, headsign); } @Override public int getLoadOrder() { return 3; } @Override public Schema.Routes.SourceId[] getTransitSourceIds() { return transitSourceIds; } @Override public TransitSourceTitles getRouteTitles() { return routeTitles; } @Override public boolean requiresSubwayTable() { return true; } @Override public IAlerts getAlerts() { return transitSystem.getAlerts(); } @Override public String getDescription() { return "Commuter Rail"; } }