From 5ecf57bed9e5cbb3657f75c9c3cb46a9008809d8 Mon Sep 17 00:00:00 2001 From: Michael Dyrna Date: Thu, 19 Feb 2015 02:13:56 +0100 Subject: [PATCH] Cologne & Bonn. --- enabler/src/de/schildbach/pte/NetworkId.java | 2 +- .../src/de/schildbach/pte/VrsProvider.java | 1184 +++++++++++++++++ .../pte/live/VrsProviderLiveTest.java | 596 +++++++++ 3 files changed, 1781 insertions(+), 1 deletion(-) create mode 100644 enabler/src/de/schildbach/pte/VrsProvider.java create mode 100644 enabler/test/de/schildbach/pte/live/VrsProviderLiveTest.java diff --git a/enabler/src/de/schildbach/pte/NetworkId.java b/enabler/src/de/schildbach/pte/NetworkId.java index 3b884e70..8ffdd754 100644 --- a/enabler/src/de/schildbach/pte/NetworkId.java +++ b/enabler/src/de/schildbach/pte/NetworkId.java @@ -26,7 +26,7 @@ public enum NetworkId RT, // Germany - DB, BVG, VBB, NVV, BAYERN, MVV, INVG, AVV, VGN, VVM, VMV, RSAG, HVV, SH, GVH, VSN, BSVAG, VBN, NASA, VVO, VMS, VGS, VRR, MVG, NPH, VRN, VVS, DING, KVV, VAGFR, NVBW, VVV, + DB, BVG, VBB, NVV, BAYERN, MVV, INVG, AVV, VGN, VVM, VMV, RSAG, HVV, SH, GVH, VSN, BSVAG, VBN, NASA, VVO, VMS, VGS, VRR, VRS, MVG, NPH, VRN, VVS, DING, KVV, VAGFR, NVBW, VVV, // Austria OEBB, VOR, WIEN, LINZ, SVV, VVT, VMOBIL, IVB, STV, diff --git a/enabler/src/de/schildbach/pte/VrsProvider.java b/enabler/src/de/schildbach/pte/VrsProvider.java new file mode 100644 index 00000000..b04d4cb3 --- /dev/null +++ b/enabler/src/de/schildbach/pte/VrsProvider.java @@ -0,0 +1,1184 @@ +/* + * Copyright 2015 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 . + */ + +package de.schildbach.pte; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +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; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.google.common.base.Charsets; +import com.google.common.base.Strings; + +import de.schildbach.pte.dto.Departure; +import de.schildbach.pte.dto.Fare; +import de.schildbach.pte.dto.Line; +import de.schildbach.pte.dto.LineDestination; +import de.schildbach.pte.dto.Location; +import de.schildbach.pte.dto.LocationType; +import de.schildbach.pte.dto.NearbyLocationsResult; +import de.schildbach.pte.dto.Point; +import de.schildbach.pte.dto.Position; +import de.schildbach.pte.dto.Product; +import de.schildbach.pte.dto.QueryDeparturesResult; +import de.schildbach.pte.dto.QueryTripsContext; +import de.schildbach.pte.dto.QueryTripsResult; +import de.schildbach.pte.dto.ResultHeader; +import de.schildbach.pte.dto.StationDepartures; +import de.schildbach.pte.dto.Stop; +import de.schildbach.pte.dto.Style; +import de.schildbach.pte.dto.SuggestLocationsResult; +import de.schildbach.pte.dto.SuggestedLocation; +import de.schildbach.pte.dto.Trip; +import de.schildbach.pte.dto.Trip.Leg; +import de.schildbach.pte.util.ParserUtils; + +/** + * @author Michael Dyrna + */ +public class VrsProvider extends AbstractNetworkProvider +{ + @SuppressWarnings("serial") + private static class Context implements QueryTripsContext + { + private Date lastDeparture = null; + private Date firstArrival = null; + public Location from; + public Location via; + public Location to; + public Set products; + + private Context() + { + } + + public boolean canQueryLater() + { + return true; + } + + public boolean canQueryEarlier() + { + return true; + } + + public void departure(Date departure) + { + if (this.lastDeparture == null || this.lastDeparture.compareTo(departure) < 0) + { + this.lastDeparture = departure; + } + } + + public void arrival(Date arrival) + { + if (this.firstArrival == null || this.firstArrival.compareTo(arrival) > 0) + { + this.firstArrival = arrival; + } + } + + public Date getLastDeparture() + { + return lastDeparture; + } + + public Date getFirstArrival() + { + return firstArrival; + } + } + + private static class LocationWithPosition + { + public LocationWithPosition(Location location, Position position) + { + this.location = location; + this.position = position; + } + + public Location location; + public Position position; + } + + // valid host names: www.vrsinfo.de, android.vrsinfo.de, ios.vrsinfo.de, ekap.vrsinfo.de (only SSL encrypted with + // client certificate) + // performance comparison March 2015 showed www.vrsinfo.de to be fastest for trips + protected static final String API_BASE = "https://www.vrsinfo.de/index.php"; + protected static final String SERVER_PRODUCT = "vrs"; + + @SuppressWarnings("serial") + protected static final List nameWithPositionPatterns = new ArrayList() + { + { + // Bonn Hauptbahnhof (ZOB) - Bussteig F2 + // Beuel Bf - D + add(Pattern.compile("(.*) - (.*)")); + // Breslauer Platz/Hbf (U) Gleis 2 + add(Pattern.compile("(.*) Gleis (.*)")); + // Düren Bf (Bussteig D/E) + add(Pattern.compile("(.*) \\(Bussteig (.*)\\)")); + } + }; + + protected static final Map STYLES = new HashMap(); + + static + { + // Schnellbusse VRR + STYLES.put("BSB", new Style(Style.parseColor("#00919d"), Style.WHITE)); + + // Stadtbahn Köln-Bonn + STYLES.put("T1", new Style(Style.parseColor("#ed1c24"), Style.WHITE)); + STYLES.put("T3", new Style(Style.parseColor("#f680c5"), Style.WHITE)); + STYLES.put("T4", new Style(Style.parseColor("#f24dae"), Style.WHITE)); + STYLES.put("T5", new Style(Style.parseColor("#9c8dce"), Style.WHITE)); + STYLES.put("T7", new Style(Style.parseColor("#f57947"), Style.WHITE)); + STYLES.put("T9", new Style(Style.parseColor("#f5777b"), Style.WHITE)); + STYLES.put("T12", new Style(Style.parseColor("#80cc28"), Style.WHITE)); + STYLES.put("T13", new Style(Style.parseColor("#9e7b65"), Style.WHITE)); + STYLES.put("T15", new Style(Style.parseColor("#4dbd38"), Style.WHITE)); + STYLES.put("T16", new Style(Style.parseColor("#33baab"), Style.WHITE)); + STYLES.put("T18", new Style(Style.parseColor("#05a1e6"), Style.WHITE)); + STYLES.put("T61", new Style(Style.parseColor("#80cc28"), Style.WHITE)); + STYLES.put("T62", new Style(Style.parseColor("#4dbd38"), Style.WHITE)); + STYLES.put("T63", new Style(Style.parseColor("#73d2f6"), Style.WHITE)); + STYLES.put("T65", new Style(Style.parseColor("#b3db18"), Style.WHITE)); + STYLES.put("T66", new Style(Style.parseColor("#ec008c"), Style.WHITE)); + STYLES.put("T67", new Style(Style.parseColor("#f680c5"), Style.WHITE)); + STYLES.put("T68", new Style(Style.parseColor("#ca93d0"), Style.WHITE)); + + // Busse Bonn + STYLES.put("B16", new Style(Style.parseColor("#33baab"), Style.WHITE)); + STYLES.put("B18", new Style(Style.parseColor("#05a1e6"), Style.WHITE)); + STYLES.put("B61", new Style(Style.parseColor("#80cc28"), Style.WHITE)); + STYLES.put("B62", new Style(Style.parseColor("#4dbd38"), Style.WHITE)); + STYLES.put("B63", new Style(Style.parseColor("#73d2f6"), Style.WHITE)); + STYLES.put("B65", new Style(Style.parseColor("#b3db18"), Style.WHITE)); + STYLES.put("B66", new Style(Style.parseColor("#ec008c"), Style.WHITE)); + STYLES.put("B67", new Style(Style.parseColor("#f680c5"), Style.WHITE)); + STYLES.put("B68", new Style(Style.parseColor("#ca93d0"), Style.WHITE)); + STYLES.put("BSB55", new Style(Style.parseColor("#00919e"), Style.WHITE)); + STYLES.put("BSB60", new Style(Style.parseColor("#8f9867"), Style.WHITE)); + STYLES.put("BSB69", new Style(Style.parseColor("#db5f1f"), Style.WHITE)); + STYLES.put("B529", new Style(Style.parseColor("#2e2383"), Style.WHITE)); + STYLES.put("B537", new Style(Style.parseColor("#2e2383"), Style.WHITE)); + STYLES.put("B541", new Style(Style.parseColor("#2e2383"), Style.WHITE)); + STYLES.put("B550", new Style(Style.parseColor("#2e2383"), Style.WHITE)); + STYLES.put("B163", new Style(Style.parseColor("#2e2383"), Style.WHITE)); + STYLES.put("B551", new Style(Style.parseColor("#2e2383"), Style.WHITE)); + STYLES.put("B600", new Style(Style.parseColor("#817db7"), Style.WHITE)); + STYLES.put("B601", new Style(Style.parseColor("#831b82"), Style.WHITE)); + STYLES.put("B602", new Style(Style.parseColor("#dd6ba6"), Style.WHITE)); + STYLES.put("B603", new Style(Style.parseColor("#e6007d"), Style.WHITE)); + STYLES.put("B604", new Style(Style.parseColor("#009f5d"), Style.WHITE)); + STYLES.put("B605", new Style(Style.parseColor("#007b3b"), Style.WHITE)); + STYLES.put("B606", new Style(Style.parseColor("#9cbf11"), Style.WHITE)); + STYLES.put("B607", new Style(Style.parseColor("#60ad2a"), Style.WHITE)); + STYLES.put("B608", new Style(Style.parseColor("#f8a600"), Style.WHITE)); + STYLES.put("B609", new Style(Style.parseColor("#ef7100"), Style.WHITE)); + STYLES.put("B610", new Style(Style.parseColor("#3ec1f1"), Style.WHITE)); + STYLES.put("B611", new Style(Style.parseColor("#0099db"), Style.WHITE)); + STYLES.put("B612", new Style(Style.parseColor("#ce9d53"), Style.WHITE)); + STYLES.put("B613", new Style(Style.parseColor("#7b3600"), Style.WHITE)); + STYLES.put("B614", new Style(Style.parseColor("#806839"), Style.WHITE)); + STYLES.put("B615", new Style(Style.parseColor("#532700"), Style.WHITE)); + STYLES.put("B630", new Style(Style.parseColor("#c41950"), Style.WHITE)); + STYLES.put("B631", new Style(Style.parseColor("#9b1c44"), Style.WHITE)); + STYLES.put("B633", new Style(Style.parseColor("#88cdc7"), Style.WHITE)); + STYLES.put("B635", new Style(Style.parseColor("#cec800"), Style.WHITE)); + STYLES.put("B636", new Style(Style.parseColor("#af0223"), Style.WHITE)); + STYLES.put("B637", new Style(Style.parseColor("#e3572a"), Style.WHITE)); + STYLES.put("B638", new Style(Style.parseColor("#af5836"), Style.WHITE)); + STYLES.put("B640", new Style(Style.parseColor("#004f81"), Style.WHITE)); + STYLES.put("BT650", new Style(Style.parseColor("#54baa2"), Style.WHITE)); + STYLES.put("BT651", new Style(Style.parseColor("#005738"), Style.WHITE)); + STYLES.put("BT680", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + STYLES.put("B800", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + STYLES.put("B812", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + STYLES.put("B843", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + STYLES.put("B845", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + STYLES.put("B852", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + STYLES.put("B855", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + STYLES.put("B856", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + STYLES.put("B857", new Style(Style.parseColor("#4e6578"), Style.WHITE)); + + STYLES.put("S", new Style(Style.parseColor("#f18e00"), Style.WHITE)); + STYLES.put("R", new Style(Style.parseColor("#009d81"), Style.WHITE)); + } + + public VrsProvider() + { + super(NetworkId.VRS); + + setStyles(STYLES); + } + + @Override + protected boolean hasCapability(Capability capability) + { + switch (capability) + { + case DEPARTURES: + return true; + case NEARBY_LOCATIONS: + return true; + case SUGGEST_LOCATIONS: + return true; + case TRIPS: + return true; + default: + return false; + } + } + + // only stations supported + public NearbyLocationsResult queryNearbyLocations(EnumSet types /* only STATION supported */, Location location, int maxDistance, + int maxLocations) throws IOException + { + // g=p means group by product; not used here + final StringBuilder uri = new StringBuilder(API_BASE); + uri.append("?eID=tx_vrsinfo_ass2_timetable"); + if (location.hasLocation()) + { + uri.append("&r=").append(String.format(Locale.ENGLISH, "%.6f,%.6f", location.lon / 1E6, location.lat / 1E6)); + } + else if (location.type == LocationType.STATION && location.hasId()) + { + uri.append("&i=").append(ParserUtils.urlEncode(location.id)); + } + else + { + throw new IllegalArgumentException("at least one of stationId or lat/lon must be given"); + } + // c=1 limits the departures at each stop to 1 - actually we don't need any at this point + uri.append("&c=1"); + if (maxLocations > 0) + { + // s=number of stops + uri.append("&s=").append(Math.min(16, maxLocations)); // artificial server limit + } + + final CharSequence page = ParserUtils.scrape(uri.toString(), null, Charsets.UTF_8); + + // System.out.println(uri); + // System.out.println(page); + + try + { + final List locations = new ArrayList(); + final JSONObject head = new JSONObject(page.toString()); + final String error = head.optString("error", null); + if (error != null) + { + if (error.equals("Leere Koordinate.") || error.equals("Leere ASS-ID und leere Koordinate")) + return new NearbyLocationsResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT, null, 0, null), 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 Location loc = parseLocationAndPosition(stop).location; + int distance = stop.getInt("distance"); + if (maxDistance > 0 && distance > maxDistance) + { + break; // we rely on the server side sorting by distance + } + if (types.contains(loc.type) || types.contains(LocationType.ANY)) + { + locations.add(loc); + } + serverTime = parseDateTime(timetable.getJSONObject(i).getString("generated")).getTime(); + } + final ResultHeader header = new ResultHeader(NetworkId.VRS, SERVER_PRODUCT, null, serverTime, null); + return new NearbyLocationsResult(header, locations); + } + catch (final JSONException x) + { + throw new RuntimeException("cannot parse: '" + page + "' on " + uri, x); + } + catch (final ParseException e) + { + throw new RuntimeException("cannot parse: '" + page + "' on " + uri, e); + } + } + + // VRS does not show LongDistanceTrains departures. Parameter p for product + // filter is supported, but LongDistanceTrains filter seems to be ignored. + // equivs not supported. + public QueryDeparturesResult queryDepartures(String stationId, @Nullable Date time, int maxDepartures, boolean equivs) throws IOException + { + // g=p means group by product; not used here + // d=minutes overwrites c=count and returns departures for the next d minutes + final StringBuilder uri = new StringBuilder(API_BASE); + uri.append("?eID=tx_vrsinfo_ass2_timetable&i=").append(ParserUtils.urlEncode(stationId)); + uri.append("&c=").append(maxDepartures); + if (time != null) + { + uri.append("&t="); + appendDate(uri, time); + } + final CharSequence page = ParserUtils.scrape(uri.toString(), null, Charsets.UTF_8); + + // System.out.println(uri); + // System.out.println(page); + + try + { + final JSONObject head = new JSONObject(page.toString()); + final String error = head.optString("error", null); + if (error != null) + { + if (error.equals("ASS2-Server lieferte leere Antwort.")) + return new QueryDeparturesResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), QueryDeparturesResult.Status.SERVICE_DOWN); + else if (error.equals("Leere ASS-ID und leere Koordinate")) + return new QueryDeparturesResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), QueryDeparturesResult.Status.INVALID_STATION); + else + throw new IllegalStateException("unknown error: " + error); + } + 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 (int i = 0; i < timetable.length(); i++) + { + final List departures = new ArrayList(); + JSONObject station = timetable.getJSONObject(i); + final Location location = parseLocationAndPosition(station.getJSONObject("stop")).location; + final JSONArray events = station.getJSONArray("events"); + final List lines = new ArrayList(); + // for all departures + for (int j = 0; j < events.length(); j++) + { + JSONObject event = events.getJSONObject(j); + Date plannedTime = null; + Date predictedTime = null; + if (event.has("departureScheduled")) + { + plannedTime = parseDateTime(event.getString("departureScheduled")); + predictedTime = parseDateTime(event.getString("departure")); + } + else + { + plannedTime = parseDateTime(event.getString("departure")); + } + final JSONObject lineObj = event.getJSONObject("line"); + final Line line = parseLine(lineObj); + Position position = null; + final JSONObject post = event.optJSONObject("post"); + if (post != null) + { + final String positionStr = post.getString("name"); + // examples for post: + // (U) Gleis 2 + // Bonn Hauptbahnhof (ZOB) - Bussteig C4 + // A + position = new Position(positionStr.substring(positionStr.lastIndexOf(' ') + 1)); + } + final Location destination = new Location(LocationType.STATION, null /* id */, null /* place */, lineObj.getString("direction")); + + final LineDestination lineDestination = new LineDestination(line, destination); + if (!lines.contains(lineDestination)) + { + lines.add(lineDestination); + } + final Departure d = new Departure(plannedTime, predictedTime, line, position, destination, null, null); + departures.add(d); + } + + queryLinesForStation(location.id, lines); + + result.stationDepartures.add(new StationDepartures(location, departures, lines)); + } + + return result; + } + catch (final JSONException x) + { + throw new RuntimeException("cannot parse: '" + page + "' on " + uri, x); + } + catch (final ParseException e) + { + throw new RuntimeException("cannot parse: '" + page + "' on " + uri, e); + } + } + + private void queryLinesForStation(String stationId, List lineDestinations) throws IOException + { + Set lineNumbersAlreadyKnown = new HashSet(); + for (LineDestination lineDestionation : lineDestinations) + { + lineNumbersAlreadyKnown.add(lineDestionation.line.label); + } + final StringBuilder uri = new StringBuilder(API_BASE); + uri.append("?eID=tx_vrsinfo_his_info&i=").append(ParserUtils.urlEncode(stationId)); + + final CharSequence page = ParserUtils.scrape(uri.toString(), null, Charsets.UTF_8); + + // System.out.println(uri); + // System.out.println(page); + + 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 i = 0; i < lines.length(); i++) + { + final JSONObject line = lines.getJSONObject(i); + 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 j = 0; j < postings.length(); j++) + { + JSONObject posting = (JSONObject) postings.get(j); + 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 " + uri, x); + } + Collections.sort(lineDestinations, new LineDestinationComparator()); + } + + private static class LineDestinationComparator implements Comparator + { + public int compare(LineDestination o1, LineDestination o2) + { + return o1.line.compareTo(o2.line); + } + } + + public SuggestLocationsResult suggestLocations(final CharSequence constraint) throws IOException + { + // sc = station count + final int sc = 10; + // ac = address count + final int ac = 5; + // pc = points of interest count + final int pc = 5; + // t = sap (stops and/or addresses and/or pois) + final String uri = API_BASE + "?eID=tx_vrsinfo_ass2_objects&sc=" + sc + "&ac=" + ac + "&pc=" + ac + "&t=sap&q=" + + ParserUtils.urlEncode(new Location(LocationType.ANY, null, null, constraint.toString()).name); + + final CharSequence page = ParserUtils.scrape(uri, null, Charsets.UTF_8); + + // System.out.println(uri); + // System.out.println(page); + + try + { + final List locations = new ArrayList(); + + final JSONObject head = new JSONObject(page.toString()); + final String error = head.optString("error", null); + if (error != null) + { + if (error.equals("ASS2-Server lieferte leere Antwort.")) + return new SuggestLocationsResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), SuggestLocationsResult.Status.SERVICE_DOWN); + else if (error.equals("Leere Suche")) + return new SuggestLocationsResult(new ResultHeader(NetworkId.VRS, SERVER_PRODUCT), locations); + else + throw new IllegalStateException("unknown error: " + error); + } + final JSONArray stops = head.optJSONArray("stops"); + final JSONArray addresses = head.optJSONArray("addresses"); + final JSONArray pois = head.optJSONArray("pois"); + + final int nStops = stops.length(); + for (int i = 0; i < nStops; i++) + { + final JSONObject stop = stops.optJSONObject(i); + final Location location = parseLocationAndPosition(stop).location; + locations.add(new SuggestedLocation(location, sc + ac + pc - i)); + } + + final int nAddresses = addresses.length(); + for (int i = 0; i < nAddresses; i++) + { + final JSONObject address = addresses.optJSONObject(i); + final Location location = parseLocationAndPosition(address).location; + locations.add(new SuggestedLocation(location, ac + pc - i)); + } + + final int nPois = pois.length(); + for (int i = 0; i < nPois; i++) + { + final JSONObject poi = pois.optJSONObject(i); + final Location location = parseLocationAndPosition(poi).location; + locations.add(new SuggestedLocation(location, pc - i)); + } + + final ResultHeader header = new ResultHeader(NetworkId.VRS, SERVER_PRODUCT); + return new SuggestLocationsResult(header, locations); + } + catch (final JSONException x) + { + throw new RuntimeException("cannot parse: '" + page + "' on " + uri, x); + } + } + + // http://www.vrsinfo.de/index.php?eID=tx_vrsinfo_ass2_router&c=1&f=2071&t=1504&d=2015-02-11T11%3A47%3A20%2B01%3A00 + // c: count (default: 5) + // f: from (id or lat,lon as float) + // v: via (id or lat,lon as float) + // t: to (id or lat,lon as float) + // a/d: date (default now) + // vt: via time in minutes - not supported by Öffi + // s: t => allow surcharge + // p: products as comma separated list + // o: options: + // 'v' for showing via stations + // 'd' for showing walking directions + // 'p' for showing exact geographical coordinates along the route + // walkSpeed not supported. + // accessibility not supported. + // options not supported. + public QueryTripsResult queryTrips(final Location from, final @Nullable Location via, final Location to, Date date, boolean dep, + final @Nullable Set products, final @Nullable WalkSpeed walkSpeed, final @Nullable Accessibility accessibility, + @Nullable Set