package de.tum.in.tumcampusapp.managers; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.util.Pair; import com.google.common.base.Optional; import com.google.common.net.UrlEscapers; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; import de.tum.in.tumcampusapp.auxiliary.Const; import de.tum.in.tumcampusapp.auxiliary.NetUtils; import de.tum.in.tumcampusapp.auxiliary.Utils; import de.tum.in.tumcampusapp.cards.MVVCard; import de.tum.in.tumcampusapp.cards.generic.Card; /** * Transport Manager, handles querying data from mvv and card creation */ public class TransportManager extends AbstractManager implements Card.ProvidesCard { /* Documentation for using efa.mvv-muenchen.de * * use XML_STOPFINDER_REQUEST to find available stops, e.g.: "Gar" * http://efa.mvv-muenchen.de/mobile/XML_STOPFINDER_REQUEST?outputFormat=JSON&language=de&stateless=1&coordOutputFormat=WGS84&locationServerActive=1&type_sf=any&name_sf=Gar&anyObjFilter_sf=126&reducedAnyPostcodeObjFilter_sf=64&reducedAnyTooManyObjFilter_sf=2&useHouseNumberList=true&anyMaxSizeHitList=500 * probably hint the one that gets best="1" * then SORT BY QUALITY, since finding 500+ stops is no fun, probably restrict this to around 10 * * Breakdown: * http://efa.mvv-muenchen.de/mobile/XML_STOPFINDER_REQUEST? // MVV API base * outputFormat=JSON * &language=de&stateless=1&coordOutputFormat=WGS84&locationServerActive=1 // Common parameters * &type_sf=any // Station type. Just keep "any" * &name_sf=Gar // Search string * &anyObjFilter_sf=126&reducedAnyPostcodeObjFilter_sf=64&reducedAnyTooManyObjFilter_sf=2&useHouseNumberList=true * &anyMaxSizeHitList=500 // How many results there are being provided * * use XSLT_DM_REQUEST for departures from stations * * Full request: http://efa.mvv-muenchen.de/mobile/XSLT_DM_REQUEST?outputFormat=JSON&language=de&stateless=1&coordOutputFormat=WGS84&type_dm=stop&name_dm=Freising&itOptionsActive=1&ptOptionsActive=1&mergeDep=1&useAllStops=1&mode=direct * * Breakdown: * http://efa.mvv-muenchen.de/mobile/XSLT_DM_REQUEST? // MVV API base * outputFormat=JSON // One could also specify XML * &language=de // Tests showed this gets ignored by MVV, but set to language nevertheless * &stateless=1&coordOutputFormat=WGS84 // Common parameters. They are reasonable, so don't change * &type_dm=stop // Station type. * &name_dm=Freising // This is the actual query string * &itOptionsActive=1&ptOptionsActive=1&mergeDep=1&useAllStops=1&mode=direct // No idea what these parameters actually do. Feel free to experiment with them, just don't blame me if anything breaks */ private static final String MVV_API_BASE = "http://efa.mvv-muenchen.de/mobile/"; // No HTTPS support :( private static final String OUTPUT_FORMAT = "outputFormat=JSON"; private static final String STATELESS = "stateless=1"; private static final String COORD_OUTPUT_FORMAT = "coordOutputFormat=WGS84"; private static final String LOCATION_SERVER = "locationServerActive=1"; private static final String STATION_SEARCH_TYPE = "type_sf=any"; private static final String STATION_SEARCH_COMMON = "anyObjFilter_sf=126&reducedAnyPostcodeObjFilter_sf=64&reducedAnyTooManyObjFilter_sf=2&useHouseNumberList=true"; private static final String STATION_SEARCH_HITLIST_SIZE = "anyMaxSizeHitList=10"; // 10 <=> what we can display on one page private static final String DEPARTURE_QUERY_TYPE = "type_dm=stop"; private static final String DEPARTURE_QUERY_COMMON = "itOptionsActive=1&ptOptionsActive=1&mergeDep=1&useAllStops=1&mode=direct"; private static final String STATION_SEARCH = "XML_STOPFINDER_REQUEST"; private static final String STATION_SEARCH_CONST; private static final String[] STATION_SEARCH_CONST_PARAMS = { OUTPUT_FORMAT, STATELESS, COORD_OUTPUT_FORMAT, LOCATION_SERVER, STATION_SEARCH_TYPE, STATION_SEARCH_COMMON, STATION_SEARCH_HITLIST_SIZE }; private static final String DEPARTURE_QUERY = "XSLT_DM_REQUEST"; private static final String DEPARTURE_QUERY_CONST; private static final String[] DEPARTURE_QUERY_CONST_PARAMS = { OUTPUT_FORMAT, STATELESS, COORD_OUTPUT_FORMAT, DEPARTURE_QUERY_TYPE, DEPARTURE_QUERY_COMMON }; // Set the following parameters before appending private static final String LANGUAGE = "language="; private static final String STATION_SEARCH_QUERY = "name_sf="; private static final String DEPARTURE_QUERY_STATION = "name_dm="; private static final String POINTS = "points"; private static final String ERROR_INVALID_JSON = "invalid JSON from mvv "; static { StringBuilder stationSearch = new StringBuilder(MVV_API_BASE); stationSearch.append(STATION_SEARCH).append('?'); for (String param : STATION_SEARCH_CONST_PARAMS) { stationSearch.append(param).append('&'); } STATION_SEARCH_CONST = stationSearch.toString(); StringBuilder departureQuery = new StringBuilder(MVV_API_BASE); departureQuery.append(DEPARTURE_QUERY).append('?'); for (String param : DEPARTURE_QUERY_CONST_PARAMS) { departureQuery.append(param).append('&'); } DEPARTURE_QUERY_CONST = departureQuery.toString(); } public TransportManager(Context context) { super(context); // Create table if needed db.execSQL("CREATE TABLE IF NOT EXISTS transport_favorites (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, symbol VARCHAR)"); } /** * Check if the transport symbol is one of the user's favorites. * @param symbol The transport symbol * @return True, if favorite */ public boolean isFavorite(String symbol) { return db.rawQuery("SELECT * FROM transport_favorites WHERE symbol = ?", new String[]{symbol}).getCount() > 0; } /** * Adds a transport symbol to the list of the user's favorites. * @param symbol The transport symbol */ public void addFavorite(String symbol) { db.execSQL("INSERT INTO transport_favorites (symbol) VALUES (?)", new String[]{symbol}); } /** * Delete a user's favorite transport symbol. * @param symbol The transport symbol */ public void deleteFavorite(String symbol) { db.execSQL("DELETE FROM transport_favorites WHERE symbol = ?", new String[]{symbol}); } /** * Get all departures for a station. * Cursor includes target station name, departure in remaining minutes. * * @param stationID Station ID, station name might or might not work * @return List of departures */ public static List<Departure> getDeparturesFromExternal(Context context, String stationID) { List<Departure> result = new ArrayList<>(); try { String language = LANGUAGE + Locale.getDefault().getLanguage(); // ISO-8859-1 is needed for mvv String departureQuery = DEPARTURE_QUERY_STATION + UrlEscapers.urlPathSegmentEscaper().escape(stationID); String query = DEPARTURE_QUERY_CONST + language + '&' + departureQuery; Utils.logv(query); NetUtils net = new NetUtils(context); // Download departures Optional<JSONObject> departures = net.downloadJson(query); if (!departures.isPresent()) { return result; } if (departures.get().get("departureList") == null) { return result; } JSONArray arr = departures.get().getJSONArray("departureList"); for (int i = 0; i < arr.length(); i++) { JSONObject departure = arr.getJSONObject(i); JSONObject servingLine = departure.getJSONObject("servingLine"); result.add(new Departure( servingLine.getString("name"), servingLine.getString("direction"), // Limit symbol length to 3, longer symbols are pointless String.format("%3.3s", servingLine.getString("symbol")).trim(), departure.getInt("countdown") )); } Collections.sort(result, new Comparator<Departure>() { @Override public int compare(Departure lhs, Departure rhs) { return lhs.countDown - rhs.countDown; } }); } catch (JSONException e) { //We got no valid JSON, mvg-live is probably bugged Utils.log(e, ERROR_INVALID_JSON + DEPARTURE_QUERY); } return result; } /** * Find stations by station name prefix * * @param prefix Name prefix * @return Database Cursor (name, _id) */ public static Optional<Cursor> getStationsFromExternal(Context context, String prefix) { try { String language = LANGUAGE + Locale.getDefault().getLanguage(); // ISO-8859-1 is needed for mvv String stationQuery = STATION_SEARCH_QUERY + UrlEscapers.urlPathSegmentEscaper().escape(prefix); String query = STATION_SEARCH_CONST + language + '&' + stationQuery; Utils.log(query); NetUtils net = new NetUtils(context); // Download possible stations Optional<JSONObject> jsonObj = net.downloadJsonObject(query, CacheManager.VALIDITY_DO_NOT_CACHE, true); if (!jsonObj.isPresent()) { return Optional.absent(); } List<StationResult> results = new ArrayList<>(); JSONObject stopfinder = jsonObj.get().getJSONObject("stopFinder"); // Possible values for points: Object, Array or null JSONArray pointsArray = stopfinder.optJSONArray(POINTS); if (pointsArray == null) { JSONObject points = stopfinder.optJSONObject(POINTS); if (points == null) { return Optional.absent(); } JSONObject point = points.getJSONObject("point"); addStationResult(results, point); } else { for (int i = 0; i < pointsArray.length(); i++) { JSONObject point = pointsArray.getJSONObject(i); addStationResult(results, point); } } //Sort by quality Collections.sort(results, new Comparator<StationResult>() { @Override public int compare(StationResult lhs, StationResult rhs) { return rhs.quality - lhs.quality; } }); MatrixCursor mc = new MatrixCursor(new String[]{Const.NAME_COLUMN, Const.ID_COLUMN}); for (StationResult result : results) { mc.addRow(new String[]{result.station, result.id}); } return Optional.of((Cursor) mc); } catch (JSONException e) { Utils.log(e, ERROR_INVALID_JSON + STATION_SEARCH); } return Optional.absent(); } private static void addStationResult(Collection<StationResult> results, JSONObject point) throws JSONException { results.add(new StationResult( point.getString("name"), point.getJSONObject("ref").getString("id"), point.getInt("quality") )); } /** * Inserts a MVV card for the nearest public transport station * * @param context Context */ @Override public void onRequestCard(Context context) { if (!NetUtils.isConnected(context)) { return; } // Get station for current campus LocationManager locMan = new LocationManager(context); String station = locMan.getStation(); if (station == null) { return; } List<Departure> cur = getDeparturesFromExternal(context, station); MVVCard card = new MVVCard(context); card.setStation(new Pair<>(station, station)); card.setDepartures(cur); card.apply(); } public static class Departure { final public String servingLine; final public String direction; final public String symbol; final public int countDown; public Departure(String servingLine, String direction, String symbol, int countDown) { this.servingLine = servingLine; this.direction = direction; this.symbol = symbol; this.countDown = countDown; } } public static class StationResult { final String station; final String id; final int quality; public StationResult(String station, String id, int quality) { this.station = station; this.id = id; this.quality = quality; } } }