diff --git a/src/de/schildbach/pte/VrsProvider.java b/src/de/schildbach/pte/VrsProvider.java index fee05a08..b764f28d 100644 --- a/src/de/schildbach/pte/VrsProvider.java +++ b/src/de/schildbach/pte/VrsProvider.java @@ -25,14 +25,11 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; import java.util.Currency; import java.util.Date; import java.util.EnumSet; import java.util.GregorianCalendar; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -47,9 +44,6 @@ import javax.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.base.Strings; import de.schildbach.pte.dto.Departure; @@ -90,8 +84,6 @@ public class VrsProvider extends AbstractNetworkProvider { Capability.TRIPS_VIA ); - private static final Logger log = LoggerFactory.getLogger(VrsProvider.class); - @SuppressWarnings("serial") private static class Context implements QueryTripsContext { private boolean canQueryLater = true; @@ -156,8 +148,7 @@ public class VrsProvider extends AbstractNetworkProvider { } // valid host names: www.vrsinfo.de, android.vrsinfo.de, ios.vrsinfo.de, ekap.vrsinfo.de (only SSL - // encrypted with - // client certificate) + // encrypted with client certificate) // performance comparison March 2015 showed www.vrsinfo.de to be fastest for trips protected static final HttpUrl API_BASE = HttpUrl.parse("http://android.vrsinfo.de/index.php"); protected static final String SERVER_PRODUCT = "vrs"; @@ -354,68 +345,72 @@ public class VrsProvider extends AbstractNetworkProvider { return CAPABILITIES.contains(capability); } - // only stations supported @Override - public NearbyLocationsResult queryNearbyLocations(Set types /* only STATION supported */, - Location location, int maxDistance, int maxLocations) throws IOException { - // g=p means group by product; not used here - final HttpUrl.Builder url = API_BASE.newBuilder(); - url.addQueryParameter("eID", "tx_vrsinfo_ass2_timetable"); + public NearbyLocationsResult queryNearbyLocations(Set types, Location location, + int maxDistance, int maxLocations) throws IOException { + final Point queryCoord; if (location.hasCoord()) { - url.addQueryParameter("r", - String.format(Locale.ENGLISH, "%.6f,%.6f", location.getLatAsDouble(), location.getLonAsDouble())); + queryCoord = location.coord; } else if (location.type == LocationType.STATION && location.hasId()) { - url.addQueryParameter("i", location.id); + queryCoord = stationToCoord(location.id); } else { throw new IllegalArgumentException("at least one of stationId or lat/lon must be given"); } - // limits the departures at each stop - we need that to guess available products - url.addQueryParameter("c", "8"); - if (maxLocations > 0) { - // s=number of stops, artificially limited by server - url.addQueryParameter("s", Integer.toString(Math.min(16, maxLocations))); - } + final HttpUrl.Builder url = API_BASE.newBuilder(); + url.addQueryParameter("eID", "tx_ekap_here"); + url.addQueryParameter("ta", "vrs"); + url.addQueryParameter("lat", String.format(Locale.ENGLISH, "%.6f", queryCoord.getLatAsDouble())); + url.addQueryParameter("lon", String.format(Locale.ENGLISH, "%.6f", queryCoord.getLonAsDouble())); final CharSequence page = httpClient.get(url.build()); try { + int num = 0; final List locations = new ArrayList<>(); final JSONObject head = new JSONObject(page.toString()); - final String error = Strings.emptyToNull(head.optString("error", "").trim()); - if (error != null) { - if (error.equals("Leere Koordinate.") || error.equals("Leere ASS-ID und leere Koordinate") || error.equals("Keine Abfahrten gefunden.")) - return new NearbyLocationsResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), locations); - else if (error.equals("ASS2-Server lieferte leere Antwort.")) - return new NearbyLocationsResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), - NearbyLocationsResult.Status.SERVICE_DOWN); - else - throw new IllegalStateException("unknown error: " + error); - } - final JSONArray timetable = head.getJSONArray("timetable"); - long serverTime = 0; - for (int i = 0; i < timetable.length(); i++) { - final JSONObject entry = timetable.getJSONObject(i); - final JSONObject stop = entry.getJSONObject("stop"); - final JSONArray events = entry.getJSONArray("events"); - final Location loc = parseLocationAndPosition(stop, events).location; - int distance = stop.getInt("distance"); - if (maxDistance > 0 && distance > maxDistance) { - break; // we rely on the server side sorting by distance + final JSONArray objects = head.getJSONArray("objects"); + for (int i = 0; i < objects.length(); i++) { + final JSONObject entry = objects.getJSONObject(i); + final LocationType type = parseLocationType(entry.getString("type")); + if (!(types.contains(type) || types.contains(LocationType.ANY))) { + continue; } - if (types.contains(loc.type) || types.contains(LocationType.ANY)) { - locations.add(loc); + final Point coord = Point.fromDouble(entry.getDouble("lat"), entry.getDouble("lon")); + if (maxDistance > 0 && entry.optInt("distance") > maxDistance) { + continue; + } + // TODO "distance" is only given for stops. For other location types, calculate distance from coordinates + String id = entry.optString("id"); + if (id == null || id.isEmpty()) { + id = entry.getString("ifopt"); + } + String place = entry.getString("municipality"); + final String locality = entry.optString("locality"); + if (locality != null && !locality.isEmpty()) { + place += "-" + locality; + } + String name = entry.getString("name"); + if (entry.getString("type").equals("parkandride")) { + name = "P+R " + name; + } + final JSONArray lines = entry.optJSONArray("lines"); + final EnumSet products = EnumSet.noneOf(Product.class); + for (int j = 0; lines != null && j < lines.length(); j++) { + final JSONObject line = lines.getJSONObject(j); + products.add(parseProduct(line.getString("productCode"), line.getString("name"))); + } + locations.add(new Location(type, id, coord, place, name, products)); + if (maxLocations > 0 && ++num >= maxLocations) { + break; } - serverTime = parseDateTime(entry.getString("generated")).getTime(); } - final ResultHeader header = new ResultHeader(NetworkId.VRS, SERVER_PRODUCT, null, null, serverTime, null); + final ResultHeader header = new ResultHeader(NetworkId.VRS, SERVER_PRODUCT, null, null, new Date().getTime(), null); return new NearbyLocationsResult(header, locations); - } catch (final JSONException | ParseException x) { + } catch (final JSONException x) { throw new RuntimeException("cannot parse: '" + page + "' on " + url, x); } } - // VRS does not show LongDistanceTrains departures. Parameter p for product - // filter is supported, but LongDistanceTrains filter seems to be ignored. // TODO equivs not supported; JSON result would support multiple timetables @Override public QueryDeparturesResult queryDepartures(final String stationId, @Nullable Date time, int maxDepartures, @@ -431,6 +426,7 @@ public class VrsProvider extends AbstractNetworkProvider { if (time != null) { url.addQueryParameter("t", formatDate(time)); } + url.addQueryParameter("p", "LongDistanceTrains,RegionalTrains,SuburbanTrains,Underground,LightRail,Bus,CommunityBus,RailReplacementServices,Boat,OnDemandServices"); final CharSequence page = httpClient.get(url.build()); try { @@ -452,10 +448,10 @@ public class VrsProvider extends AbstractNetworkProvider { final JSONArray timetable = head.getJSONArray("timetable"); final ResultHeader header = new ResultHeader(NetworkId.VRS, SERVER_PRODUCT); final QueryDeparturesResult result = new QueryDeparturesResult(header); - // for all stations if (timetable.length() == 0) { return new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION); } + // for all stations for (int iStation = 0; iStation < timetable.length(); iStation++) { final List departures = new ArrayList<>(); final JSONObject station = timetable.getJSONObject(iStation); @@ -478,7 +474,7 @@ public class VrsProvider extends AbstractNetworkProvider { Position position = null; final JSONObject post = event.optJSONObject("post"); if (post != null) { - final String postName = post.getString("name"); + String postName = post.getString("name"); for (Pattern pattern : NAME_WITH_POSITION_PATTERNS) { Matcher matcher = pattern.matcher(postName); if (matcher.matches()) { @@ -486,8 +482,11 @@ public class VrsProvider extends AbstractNetworkProvider { break; } } - if (position == null) - log.info("Could not extract position from '{}'", postName); + if (position == null) { + if (postName.startsWith("(") && postName.endsWith(")")) + postName = postName.substring(1, postName.length() - 1); + position = new Position(postName); + } } final Location destination = new Location(LocationType.STATION, null /* id */, null /* place */, lineObj.getString("direction")); @@ -501,8 +500,6 @@ public class VrsProvider extends AbstractNetworkProvider { departures.add(d); } - queryLinesForStation(location.id, lines); - result.stationDepartures.add(new StationDepartures(location, departures, lines)); } @@ -512,62 +509,6 @@ public class VrsProvider extends AbstractNetworkProvider { } } - private void queryLinesForStation(String stationId, List lineDestinations) throws IOException { - Set lineNumbersAlreadyKnown = new HashSet<>(); - for (LineDestination lineDestionation : lineDestinations) { - lineNumbersAlreadyKnown.add(lineDestionation.line.label); - } - final HttpUrl.Builder url = API_BASE.newBuilder(); - url.addQueryParameter("eID", "tx_vrsinfo_his_info"); - url.addQueryParameter("i", stationId); - - final CharSequence page = httpClient.get(url.build()); - - try { - final JSONObject head = new JSONObject(page.toString()); - final JSONObject his = head.optJSONObject("his"); - if (his != null) { - final JSONArray lines = his.optJSONArray("lines"); - if (lines != null) { - for (int iLine = 0; iLine < lines.length(); iLine++) { - final JSONObject line = lines.getJSONObject(iLine); - final String number = processLineNumber(line.getString("number")); - if (lineNumbersAlreadyKnown.contains(number)) { - continue; - } - final Product product = productFromLineNumber(number); - String direction = null; - final JSONArray postings = line.optJSONArray("postings"); - if (postings != null) { - for (int iPosting = 0; iPosting < postings.length(); iPosting++) { - final JSONObject posting = (JSONObject) postings.get(iPosting); - direction = posting.getString("direction"); - lineDestinations.add(new LineDestination( - new Line(null /* id */, NetworkId.VRS.toString(), product, number, - lineStyle("vrs", product, number)), - new Location(LocationType.STATION, null /* id */, null /* place */, - direction))); - } - } else { - lineDestinations.add(new LineDestination(new Line(null /* id */, NetworkId.VRS.toString(), - product, number, lineStyle("vrs", product, number)), null /* direction */)); - } - } - } - } - } catch (final JSONException x) { - throw new RuntimeException("cannot parse: '" + page + "' on " + url, x); - } - Collections.sort(lineDestinations, new LineDestinationComparator()); - } - - private static class LineDestinationComparator implements Comparator { - @Override - public int compare(LineDestination o1, LineDestination o2) { - return o1.line.compareTo(o2.line); - } - } - @Override public SuggestLocationsResult suggestLocations(final CharSequence constraint, final @Nullable Set types, final int maxLocations) throws IOException { @@ -718,6 +659,9 @@ public class VrsProvider extends AbstractNetworkProvider { else if (error.equals("Keine Verbindungen gefunden.")) return new QueryTripsResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), QueryTripsResult.Status.NO_TRIPS); + else if (error.equals("Es wurden keine gültigen Verbindungen für diese Anfrage gefunden.")) + return new QueryTripsResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), + QueryTripsResult.Status.NO_TRIPS); else if (error.startsWith("Keine Verbindung gefunden.")) return new QueryTripsResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), QueryTripsResult.Status.NO_TRIPS); @@ -969,22 +913,6 @@ public class VrsProvider extends AbstractNetworkProvider { return new Point[] { Point.from1E6(50937531, 6960279) }; } - private static Product productFromLineNumber(String number) { - if (number.startsWith("I") || number.startsWith("E")) { - return Product.HIGH_SPEED_TRAIN; - } else if (number.startsWith("R") || number.startsWith("MRB") || number.startsWith("DPN")) { - return Product.REGIONAL_TRAIN; - } else if (number.startsWith("S") && !number.startsWith("SB") && !number.startsWith("SEV")) { - return Product.SUBURBAN_TRAIN; - } else if (number.startsWith("U")) { - return Product.SUBWAY; - } else if (number.length() <= 2 && !number.startsWith("N")) { - return Product.TRAM; - } else { - return Product.BUS; - } - } - private Line parseLine(JSONObject line) throws JSONException { final String number = processLineNumber(line.getString("number")); final Product productObj = parseProduct(line.getString("product"), number); @@ -1160,4 +1088,36 @@ public class VrsProvider extends AbstractNetworkProvider { return new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ssZ") .parse(dateTimeStr.substring(0, dateTimeStr.lastIndexOf(':')) + "00"); } + + private final Point stationToCoord(String id) throws IOException { + final HttpUrl.Builder url = API_BASE.newBuilder(); + url.addQueryParameter("eID", "tx_vrsinfo_ass2_timetable"); + url.addQueryParameter("i", id); + + final CharSequence page = httpClient.get(url.build()); + + try { + final JSONObject head = new JSONObject(page.toString()); + final String error = Strings.emptyToNull(head.optString("error", "").trim()); + if (error != null) { + throw new IllegalStateException(error); + } + final JSONArray timetable = head.getJSONArray("timetable"); + final JSONObject entry = timetable.getJSONObject(0); + final JSONObject stop = entry.getJSONObject("stop"); + return Point.fromDouble(stop.getDouble("x"), stop.getDouble("y")); + } catch (final JSONException x) { + throw new RuntimeException("cannot parse: '" + page + "' on " + url, x); + } + } + + private final static LocationType parseLocationType(String type) { + if (type.equals("stop")) { + return LocationType.STATION; + } else if (type.equals("poi") || type.equals("parkandride")) { + return LocationType.POI; + } else { + return LocationType.ANY; + } + } } diff --git a/test/de/schildbach/pte/live/VrsProviderLiveTest.java b/test/de/schildbach/pte/live/VrsProviderLiveTest.java index 86c792f0..2751071d 100644 --- a/test/de/schildbach/pte/live/VrsProviderLiveTest.java +++ b/test/de/schildbach/pte/live/VrsProviderLiveTest.java @@ -53,7 +53,6 @@ import de.schildbach.pte.dto.StationDepartures; import de.schildbach.pte.dto.Style; import de.schildbach.pte.dto.SuggestLocationsResult; import de.schildbach.pte.dto.TripOptions; -import de.schildbach.pte.util.Iso8601Format; /** * @author Michael Dyrna @@ -71,7 +70,7 @@ public class VrsProviderLiveTest extends AbstractProviderLiveTest { @Test public void nearbyStationsByCoordinate() throws Exception { - final NearbyLocationsResult result = queryNearbyStations(Location.coord(51218693, 6777785)); + final NearbyLocationsResult result = queryNearbyStations(Location.coord(50942970, 6958570)); print(result); final NearbyLocationsResult result2 = queryNearbyStations(Location.coord(51719648, 8754330)); @@ -121,18 +120,18 @@ public class VrsProviderLiveTest extends AbstractProviderLiveTest { print(result); final NearbyLocationsResult result2 = queryNearbyLocations(EnumSet.of(LocationType.STATION), - Location.coord(50732100, 7096820), 0, 50); + Location.coord(50732100, 7096820), 0, 1); print(result2); final NearbyLocationsResult result3 = queryNearbyLocations(EnumSet.of(LocationType.STATION), - Location.coord(50732100, 7096820), 1000, 50); + Location.coord(50732100, 7096820), 100, 0); print(result3); } @Test public void nearbyLocationsEmpty() throws Exception { final NearbyLocationsResult result = queryNearbyLocations(EnumSet.allOf(LocationType.class), - Location.coord(1, 0), 0, 0); + Location.coord(1, 1), 1000, 0); print(result); assertEquals(0, result.locations.size()); } @@ -165,7 +164,6 @@ public class VrsProviderLiveTest extends AbstractProviderLiveTest { @Test public void queryDeparturesGaussstr() throws Exception { final QueryDeparturesResult result = queryDepartures("8984", false); - // will return {"error": "Keine Abfahrten gefunden."} print(result); printLineDestinations(result); } @@ -319,9 +317,9 @@ public class VrsProviderLiveTest extends AbstractProviderLiveTest { } @Test - public void tripEarlierLaterCologneBerlin() throws Exception { - final QueryTripsResult result = queryTrips(new Location(LocationType.STATION, "1"), null, - new Location(LocationType.STATION, "11458"), new Date(), true, null); + public void tripEarlierLaterCologneBonn() throws Exception { + final QueryTripsResult result = queryTrips(new Location(LocationType.STATION, "8"), null, + new Location(LocationType.STATION, "687"), new Date(), true, null); assertEquals(QueryTripsResult.Status.OK, result.status); assertTrue(result.trips.size() > 0); print(result); @@ -426,8 +424,7 @@ public class VrsProviderLiveTest extends AbstractProviderLiveTest { "Kerpen-Sindorf", "Erftstraße 43"); final Location to = new Location(LocationType.ADDRESS, null /* id */, Point.from1E6(50923000, 6818440), "Frechen", "Zedernweg 1"); - final QueryTripsResult result = queryTrips(from, null, to, Iso8601Format.parseDateTime("2015-03-17 21:11:18"), - true, null); + final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null); print(result); assertEquals(QueryTripsResult.Status.OK, result.status); assertTrue(result.trips.size() > 0); @@ -529,8 +526,7 @@ public class VrsProviderLiveTest extends AbstractProviderLiveTest { } Set lines = new TreeSet<>(); for (Location station : stations) { - QueryDeparturesResult qdr = provider.queryDepartures(station.id, - Iso8601Format.parseDateTime("2015-03-16 02:00:00"), 100, false); + QueryDeparturesResult qdr = provider.queryDepartures(station.id, new Date(), 100, false); if (qdr.status == QueryDeparturesResult.Status.OK) { for (StationDepartures stationDepartures : qdr.stationDepartures) { final List stationDeparturesLines = stationDepartures.lines; @@ -549,11 +545,9 @@ public class VrsProviderLiveTest extends AbstractProviderLiveTest { for (Line line : lines) { final Product product = line.product; if (product != null) { - if (product.equals(Product.BUS)) { - final Style style = line.style; - if (style != null) { - System.out.printf("%s %6x\n", line.label, style.backgroundColor); - } + final Style style = line.style; + if (style != null) { + System.out.printf("%s %s %6x\n", product, line.label, style.backgroundColor); } } }