diff --git a/enabler/src/de/schildbach/pte/AbstractHafasLegacyProvider.java b/enabler/src/de/schildbach/pte/AbstractHafasLegacyProvider.java
new file mode 100644
index 00000000..33724673
--- /dev/null
+++ b/enabler/src/de/schildbach/pte/AbstractHafasLegacyProvider.java
@@ -0,0 +1,3044 @@
+/*
+ * Copyright 2010-2017 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 static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPInputStream;
+
+import javax.annotation.Nullable;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Strings;
+
+import de.schildbach.pte.dto.Departure;
+import de.schildbach.pte.dto.Line;
+import de.schildbach.pte.dto.Line.Attr;
+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.SuggestLocationsResult;
+import de.schildbach.pte.dto.SuggestedLocation;
+import de.schildbach.pte.dto.Trip;
+import de.schildbach.pte.exception.ParserException;
+import de.schildbach.pte.exception.SessionExpiredException;
+import de.schildbach.pte.util.HttpClient;
+import de.schildbach.pte.util.LittleEndianDataInputStream;
+import de.schildbach.pte.util.ParserUtils;
+import de.schildbach.pte.util.StringReplaceReader;
+import de.schildbach.pte.util.XmlPullUtil;
+
+import okhttp3.HttpUrl;
+import okhttp3.ResponseBody;
+
+/**
+ * @author Andreas Schildbach
+ */
+public abstract class AbstractHafasLegacyProvider extends AbstractHafasProvider {
+ private static final String REQC_PROD = "hafas";
+
+ protected HttpUrl stationBoardEndpoint;
+ protected HttpUrl getStopEndpoint;
+ protected HttpUrl queryEndpoint;
+ private @Nullable HttpUrl extXmlEndpoint = null;
+ protected final String apiLanguage;
+ private @Nullable String accessId = null;
+ private @Nullable String clientType = "ANDROID";
+ private boolean jsonGetStopsUseWeight = true;
+ private Charset jsonNearbyLocationsEncoding = Charsets.ISO_8859_1;
+ private boolean dominantPlanStopTime = false;
+ private boolean useIso8601 = false;
+ private boolean stationBoardHasStationTable = true;
+ private boolean stationBoardHasLocation = false;
+ private boolean stationBoardCanDoEquivs = true;
+
+ @SuppressWarnings("serial")
+ private static class Context implements QueryTripsContext {
+ public final String laterContext;
+ public final String earlierContext;
+ public final int sequence;
+
+ public Context(final String laterContext, final String earlierContext, final int sequence) {
+ this.laterContext = laterContext;
+ this.earlierContext = earlierContext;
+ this.sequence = sequence;
+ }
+
+ @Override
+ public boolean canQueryLater() {
+ return laterContext != null;
+ }
+
+ @Override
+ public boolean canQueryEarlier() {
+ return earlierContext != null;
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class QueryTripsBinaryContext implements QueryTripsContext {
+ public final String ident;
+ public final int seqNr;
+ public final String ld;
+ public final int usedBufferSize;
+ private final boolean canQueryMore;
+
+ public QueryTripsBinaryContext(final String ident, final int seqNr, final String ld, final int usedBufferSize,
+ final boolean canQueryMore) {
+ this.ident = ident;
+ this.seqNr = seqNr;
+ this.ld = ld;
+ this.usedBufferSize = usedBufferSize;
+ this.canQueryMore = canQueryMore;
+ }
+
+ @Override
+ public boolean canQueryLater() {
+ return canQueryMore;
+ }
+
+ @Override
+ public boolean canQueryEarlier() {
+ return canQueryMore;
+ }
+ }
+
+ public AbstractHafasLegacyProvider(final NetworkId network, final HttpUrl apiBase, final String apiLanguage,
+ final Product[] productsMap) {
+ super(network, productsMap);
+ this.stationBoardEndpoint = apiBase.newBuilder().addPathSegment("stboard.exe").build();
+ this.getStopEndpoint = apiBase.newBuilder().addPathSegment("ajax-getstop.exe").build();
+ this.queryEndpoint = apiBase.newBuilder().addPathSegment("query.exe").build();
+ this.apiLanguage = apiLanguage;
+ }
+
+ protected AbstractHafasProvider setStationBoardEndpoint(final HttpUrl stationBoardEndpoint) {
+ this.stationBoardEndpoint = stationBoardEndpoint;
+ return this;
+ }
+
+ protected AbstractHafasProvider setGetStopEndpoint(final HttpUrl getStopEndpoint) {
+ this.getStopEndpoint = getStopEndpoint;
+ return this;
+ }
+
+ protected AbstractHafasProvider setQueryEndpoint(final HttpUrl queryEndpoint) {
+ this.queryEndpoint = queryEndpoint;
+ return this;
+ }
+
+ protected AbstractHafasProvider setExtXmlEndpoint(final HttpUrl extXmlEndpoint) {
+ this.extXmlEndpoint = extXmlEndpoint;
+ return this;
+ }
+
+ protected AbstractHafasProvider setAccessId(final String accessId) {
+ this.accessId = accessId;
+ return this;
+ }
+
+ protected AbstractHafasProvider setClientType(final String clientType) {
+ this.clientType = clientType;
+ return this;
+ }
+
+ protected AbstractHafasProvider setDominantPlanStopTime(final boolean dominantPlanStopTime) {
+ this.dominantPlanStopTime = dominantPlanStopTime;
+ return this;
+ }
+
+ protected AbstractHafasProvider setJsonGetStopsUseWeight(final boolean jsonGetStopsUseWeight) {
+ this.jsonGetStopsUseWeight = jsonGetStopsUseWeight;
+ return this;
+ }
+
+ protected AbstractHafasProvider setJsonNearbyLocationsEncoding(final Charset jsonNearbyLocationsEncoding) {
+ this.jsonNearbyLocationsEncoding = jsonNearbyLocationsEncoding;
+ return this;
+ }
+
+ protected AbstractHafasProvider setUseIso8601(final boolean useIso8601) {
+ this.useIso8601 = useIso8601;
+ return this;
+ }
+
+ protected AbstractHafasProvider setStationBoardHasStationTable(final boolean stationBoardHasStationTable) {
+ this.stationBoardHasStationTable = stationBoardHasStationTable;
+ return this;
+ }
+
+ protected AbstractHafasProvider setStationBoardHasLocation(final boolean stationBoardHasLocation) {
+ this.stationBoardHasLocation = stationBoardHasLocation;
+ return this;
+ }
+
+ protected AbstractHafasProvider setStationBoardCanDoEquivs(final boolean canDoEquivs) {
+ this.stationBoardCanDoEquivs = canDoEquivs;
+ return this;
+ }
+
+ private final String wrapReqC(final CharSequence request, final Charset encoding) {
+ return "" //
+ + "" //
+ + request //
+ + "";
+ }
+
+ private final Location parseStation(final XmlPullParser pp) {
+ final String type = pp.getName();
+ if ("Station".equals(type)) {
+ final String name = XmlPullUtil.attr(pp, "name");
+ final String id = XmlPullUtil.attr(pp, "externalStationNr");
+ final int x = XmlPullUtil.intAttr(pp, "x");
+ final int y = XmlPullUtil.intAttr(pp, "y");
+
+ final String[] placeAndName = splitStationName(name);
+ return new Location(LocationType.STATION, id, y, x, placeAndName[0], placeAndName[1]);
+ }
+ throw new IllegalStateException("cannot handle: " + type);
+ }
+
+ private static final Location parsePoi(final XmlPullParser pp) {
+ final String type = pp.getName();
+ if ("Poi".equals(type)) {
+ String name = XmlPullUtil.attr(pp, "name");
+ if (name.equals("unknown"))
+ name = null;
+ final int x = XmlPullUtil.intAttr(pp, "x");
+ final int y = XmlPullUtil.intAttr(pp, "y");
+ return new Location(LocationType.POI, null, y, x, null, name);
+ }
+ throw new IllegalStateException("cannot handle: " + type);
+ }
+
+ private final Location parseAddress(final XmlPullParser pp) {
+ final String type = pp.getName();
+ if ("Address".equals(type)) {
+ String name = XmlPullUtil.attr(pp, "name");
+ if (name.equals("unknown"))
+ name = null;
+ final int x = XmlPullUtil.intAttr(pp, "x");
+ final int y = XmlPullUtil.intAttr(pp, "y");
+
+ final String[] placeAndName = splitAddress(name);
+ return new Location(LocationType.ADDRESS, null, y, x, placeAndName[0], placeAndName[1]);
+ }
+ throw new IllegalStateException("cannot handle: " + type);
+ }
+
+ private final Position parsePlatform(final XmlPullParser pp) throws XmlPullParserException, IOException {
+ XmlPullUtil.enter(pp, "Platform");
+ final String platformText = XmlPullUtil.valueTag(pp, "Text");
+ XmlPullUtil.skipExit(pp, "Platform");
+
+ if (platformText == null || platformText.length() == 0)
+ return null;
+ else
+ return parsePosition(platformText);
+ }
+
+ @Override
+ public SuggestLocationsResult suggestLocations(final CharSequence constraint) throws IOException {
+ final HttpUrl.Builder url = getStopEndpoint.newBuilder().addPathSegment(apiLanguage);
+ appendJsonGetStopsParameters(url, checkNotNull(constraint), 0);
+ return jsonGetStops(url.build());
+ }
+
+ protected void appendJsonGetStopsParameters(final HttpUrl.Builder url, final CharSequence constraint,
+ final int maxStops) {
+ url.addQueryParameter("getstop", "1");
+ url.addQueryParameter("REQ0JourneyStopsS0A", "255");
+ url.addEncodedQueryParameter("REQ0JourneyStopsS0G",
+ ParserUtils.urlEncode(constraint.toString() + "?", requestUrlEncoding));
+ if (maxStops > 0)
+ url.addQueryParameter("REQ0JourneyStopsB", Integer.toString(maxStops));
+ url.addQueryParameter("js", "true");
+ }
+
+ private static final Pattern P_AJAX_GET_STOPS_JSON = Pattern
+ .compile("SLs\\.sls\\s*=\\s*(.*?);\\s*SLs\\.showSuggestion\\(\\);", Pattern.DOTALL);
+ private static final Pattern P_AJAX_GET_STOPS_ID = Pattern.compile(".*?@L=0*(\\d+)@.*?");
+
+ protected final SuggestLocationsResult jsonGetStops(final HttpUrl url) throws IOException {
+ final CharSequence page = httpClient.get(url);
+
+ final Matcher mJson = P_AJAX_GET_STOPS_JSON.matcher(page);
+ if (mJson.matches()) {
+ final String json = mJson.group(1);
+ final List locations = new ArrayList();
+
+ try {
+ final JSONObject head = new JSONObject(json);
+ final JSONArray aSuggestions = head.getJSONArray("suggestions");
+
+ for (int i = 0; i < aSuggestions.length(); i++) {
+ final JSONObject suggestion = aSuggestions.optJSONObject(i);
+ if (suggestion != null) {
+ final int type = suggestion.getInt("type");
+ final String value = suggestion.getString("value");
+ final int lat = suggestion.optInt("ycoord");
+ final int lon = suggestion.optInt("xcoord");
+ final int weight = jsonGetStopsUseWeight ? suggestion.getInt("weight") : -i;
+ String localId = null;
+ final Matcher m = P_AJAX_GET_STOPS_ID.matcher(suggestion.getString("id"));
+ if (m.matches())
+ localId = m.group(1);
+
+ final Location location;
+
+ if (type == 1) // station
+ {
+ final String[] placeAndName = splitStationName(value);
+ location = new Location(LocationType.STATION, localId, lat, lon, placeAndName[0],
+ placeAndName[1]);
+ } else if (type == 2) // address
+ {
+ final String[] placeAndName = splitAddress(value);
+ location = new Location(LocationType.ADDRESS, null, lat, lon, placeAndName[0],
+ placeAndName[1]);
+ } else if (type == 4) // poi
+ {
+ final String[] placeAndName = splitPOI(value);
+ location = new Location(LocationType.POI, localId, lat, lon, placeAndName[0],
+ placeAndName[1]);
+ } else if (type == 128) // crossing
+ {
+ final String[] placeAndName = splitAddress(value);
+ location = new Location(LocationType.ADDRESS, localId, lat, lon, placeAndName[0],
+ placeAndName[1]);
+ } else if (type == 87) {
+ location = null;
+ // don't know what to do
+ } else {
+ throw new IllegalStateException("unknown type " + type + " on " + url);
+ }
+
+ if (location != null) {
+ final SuggestedLocation suggestedLocation = new SuggestedLocation(location, weight);
+ locations.add(suggestedLocation);
+ }
+ }
+ }
+
+ return new SuggestLocationsResult(new ResultHeader(network, SERVER_PRODUCT), locations);
+ } catch (final JSONException x) {
+ throw new RuntimeException("cannot parse: '" + json + "' on " + url, x);
+ }
+ } else {
+ throw new RuntimeException("cannot parse: '" + page + "' on " + url);
+ }
+ }
+
+ @Override
+ public QueryDeparturesResult queryDepartures(final String stationId, final @Nullable Date time,
+ final int maxDepartures, final boolean equivs) throws IOException {
+ checkNotNull(Strings.emptyToNull(stationId));
+
+ final HttpUrl.Builder url = stationBoardEndpoint.newBuilder().addPathSegment(apiLanguage);
+ appendXmlStationBoardParameters(url, time, stationId, maxDepartures, equivs, "vs_java3");
+ return xmlStationBoard(url.build(), stationId);
+ }
+
+ protected void appendXmlStationBoardParameters(final HttpUrl.Builder url, final @Nullable Date time,
+ final String stationId, final int maxDepartures, final boolean equivs, final @Nullable String styleSheet) {
+ url.addQueryParameter("productsFilter", allProductsString().toString());
+ url.addQueryParameter("boardType", "dep");
+ if (stationBoardCanDoEquivs)
+ url.addQueryParameter("disableEquivs", equivs ? "0" : "1");
+ url.addQueryParameter("maxJourneys",
+ Integer.toString(maxDepartures > 0 ? maxDepartures : DEFAULT_MAX_DEPARTURES));
+ url.addEncodedQueryParameter("input", ParserUtils.urlEncode(normalizeStationId(stationId), requestUrlEncoding));
+ appendDateTimeParameters(url, time, "date", "time");
+ if (clientType != null)
+ url.addEncodedQueryParameter("clientType", ParserUtils.urlEncode(clientType, requestUrlEncoding));
+ if (styleSheet != null)
+ url.addQueryParameter("L", styleSheet);
+ url.addQueryParameter("hcount", "0"); // prevents showing old departures
+ url.addQueryParameter("start", "yes");
+ }
+
+ protected void appendDateTimeParameters(final HttpUrl.Builder url, final Date time, final String dateParamName,
+ final String timeParamName) {
+ final Calendar c = new GregorianCalendar(timeZone);
+ c.setTime(time);
+ final int year = c.get(Calendar.YEAR);
+ final int month = c.get(Calendar.MONTH) + 1;
+ final int day = c.get(Calendar.DAY_OF_MONTH);
+ final int hour = c.get(Calendar.HOUR_OF_DAY);
+ final int minute = c.get(Calendar.MINUTE);
+ url.addEncodedQueryParameter(dateParamName,
+ ParserUtils.urlEncode(
+ useIso8601 ? String.format(Locale.ENGLISH, "%04d-%02d-%02d", year, month, day)
+ : String.format(Locale.ENGLISH, "%02d.%02d.%02d", day, month, year - 2000),
+ requestUrlEncoding));
+ url.addEncodedQueryParameter(timeParamName,
+ ParserUtils.urlEncode(String.format(Locale.ENGLISH, "%02d:%02d", hour, minute), requestUrlEncoding));
+ }
+
+ private static final Pattern P_XML_STATION_BOARD_DELAY = Pattern.compile("(?:-|k\\.A\\.?|cancel|([+-]?\\s*\\d+))");
+
+ protected final QueryDeparturesResult xmlStationBoard(final HttpUrl url, final String stationId)
+ throws IOException {
+ final String normalizedStationId = normalizeStationId(stationId);
+ final AtomicReference result = new AtomicReference();
+
+ httpClient.getInputStream(new HttpClient.Callback() {
+
+ @Override
+ public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
+ StringReplaceReader reader = null;
+ String firstChars = null;
+
+ // work around unparsable XML
+ reader = new StringReplaceReader(body.charStream(), " & ", " & ");
+ reader.replace("", " ");
+ reader.replace("", " ");
+ reader.replace("", " ");
+ reader.replace("", " ");
+ reader.replace("", " ");
+ reader.replace("", " ");
+ reader.replace("
", " ");
+ reader.replace(" ->", " →"); // right arrow
+ reader.replace(" <-", " ←"); // left arrow
+ reader.replace(" <> ", " ↔ "); // left-right arrow
+ addCustomReplaces(reader);
+
+ try {
+ final XmlPullParserFactory factory = XmlPullParserFactory
+ .newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
+ final XmlPullParser pp = factory.newPullParser();
+ pp.setInput(reader);
+
+ pp.nextTag();
+
+ final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT);
+ final QueryDeparturesResult r = new QueryDeparturesResult(header);
+
+ if (XmlPullUtil.test(pp, "Err")) {
+ final String code = XmlPullUtil.attr(pp, "code");
+ final String text = XmlPullUtil.attr(pp, "text");
+
+ if (code.equals("H730")) {
+ result.set(new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION));
+ return;
+ }
+ if (code.equals("H890")) {
+ r.stationDepartures
+ .add(new StationDepartures(new Location(LocationType.STATION, normalizedStationId),
+ Collections. emptyList(), null));
+ result.set(r);
+ return;
+ }
+ throw new IllegalArgumentException("unknown error " + code + ", " + text);
+ }
+
+ String[] stationPlaceAndName = null;
+
+ if (stationBoardHasStationTable)
+ XmlPullUtil.enter(pp, "StationTable");
+ else
+ checkState(!XmlPullUtil.test(pp, "StationTable"));
+
+ if (stationBoardHasLocation) {
+ XmlPullUtil.require(pp, "St");
+
+ final String evaId = XmlPullUtil.attr(pp, "evaId");
+ if (evaId != null) {
+ if (!evaId.equals(normalizedStationId))
+ throw new IllegalStateException(
+ "stationId: " + normalizedStationId + ", evaId: " + evaId);
+
+ final String name = XmlPullUtil.attr(pp, "name");
+ if (name != null)
+ stationPlaceAndName = splitStationName(name.trim());
+ }
+ XmlPullUtil.requireSkip(pp, "St");
+ } else {
+ checkState(!XmlPullUtil.test(pp, "St"));
+ }
+
+ while (XmlPullUtil.test(pp, "Journey")) {
+ final String fpTime = XmlPullUtil.attr(pp, "fpTime");
+ final String fpDate = XmlPullUtil.attr(pp, "fpDate");
+ final String delay = XmlPullUtil.attr(pp, "delay");
+ final String eDelay = XmlPullUtil.optAttr(pp, "e_delay", null);
+ final String platform = XmlPullUtil.optAttr(pp, "platform", null);
+ // TODO newpl
+ final String targetLoc = XmlPullUtil.optAttr(pp, "targetLoc", null);
+ // TODO hafasname
+ final String dirnr = XmlPullUtil.optAttr(pp, "dirnr", null);
+ final String prod = XmlPullUtil.attr(pp, "prod");
+ final String classStr = XmlPullUtil.optAttr(pp, "class", null);
+ final String dir = XmlPullUtil.optAttr(pp, "dir", null);
+ final String capacityStr = XmlPullUtil.optAttr(pp, "capacity", null);
+ final String depStation = XmlPullUtil.optAttr(pp, "depStation", null);
+ final String delayReason = XmlPullUtil.optAttr(pp, "delayReason", null);
+ // TODO is_reachable
+ // TODO disableTrainInfo
+ // TODO lineFG/lineBG (ZVV)
+ final String administration = normalizeLineAdministration(
+ XmlPullUtil.optAttr(pp, "administration", null));
+
+ if (!"cancel".equals(delay) && !"cancel".equals(eDelay)) {
+ final Calendar plannedTime = new GregorianCalendar(timeZone);
+ plannedTime.clear();
+ ParserUtils.parseEuropeanTime(plannedTime, fpTime);
+ if (fpDate.length() == 8)
+ ParserUtils.parseGermanDate(plannedTime, fpDate);
+ else if (fpDate.length() == 10)
+ ParserUtils.parseIsoDate(plannedTime, fpDate);
+ else
+ throw new IllegalStateException("cannot parse: '" + fpDate + "'");
+
+ final Calendar predictedTime;
+ if (eDelay != null) {
+ predictedTime = new GregorianCalendar(timeZone);
+ predictedTime.setTimeInMillis(plannedTime.getTimeInMillis());
+ predictedTime.add(Calendar.MINUTE, Integer.parseInt(eDelay));
+ } else if (delay != null) {
+ final Matcher m = P_XML_STATION_BOARD_DELAY.matcher(delay);
+ if (m.matches()) {
+ if (m.group(1) != null) {
+ predictedTime = new GregorianCalendar(timeZone);
+ predictedTime.setTimeInMillis(plannedTime.getTimeInMillis());
+ predictedTime.add(Calendar.MINUTE, Integer.parseInt(m.group(1)));
+ } else {
+ predictedTime = null;
+ }
+ } else {
+ throw new RuntimeException("cannot parse delay: '" + delay + "'");
+ }
+ } else {
+ predictedTime = null;
+ }
+
+ final Position position = parsePosition(ParserUtils.resolveEntities(platform));
+
+ final String destinationName;
+ if (dir != null)
+ destinationName = dir.trim();
+ else if (targetLoc != null)
+ destinationName = targetLoc.trim();
+ else
+ destinationName = null;
+
+ final Location destination;
+ if (dirnr != null) {
+ final String[] destinationPlaceAndName = splitStationName(destinationName);
+ destination = new Location(LocationType.STATION, dirnr, destinationPlaceAndName[0],
+ destinationPlaceAndName[1]);
+ } else {
+ destination = new Location(LocationType.ANY, null, null, destinationName);
+ }
+
+ final Line prodLine = parseLineAndType(prod);
+ final Line line;
+ if (classStr != null) {
+ final Product product = intToProduct(Integer.parseInt(classStr));
+ if (product == null)
+ throw new IllegalArgumentException();
+ // could check for type consistency here
+ final Set attrs = prodLine.attrs;
+ if (attrs != null)
+ line = newLine(administration, product, prodLine.label, null,
+ attrs.toArray(new Line.Attr[0]));
+ else
+ line = newLine(administration, product, prodLine.label, null);
+ } else {
+ final Set attrs = prodLine.attrs;
+ if (attrs != null)
+ line = newLine(administration, prodLine.product, prodLine.label, null,
+ attrs.toArray(new Line.Attr[0]));
+ else
+ line = newLine(administration, prodLine.product, prodLine.label, null);
+ }
+
+ final int[] capacity;
+ if (capacityStr != null && !"0|0".equals(capacityStr)) {
+ final String[] capacityParts = capacityStr.split("\\|");
+ capacity = new int[] { Integer.parseInt(capacityParts[0]),
+ Integer.parseInt(capacityParts[1]) };
+ } else {
+ capacity = null;
+ }
+
+ final String message;
+ if (delayReason != null) {
+ final String msg = delayReason.trim();
+ message = msg.length() > 0 ? msg : null;
+ } else {
+ message = null;
+ }
+
+ final Departure departure = new Departure(plannedTime.getTime(),
+ predictedTime != null ? predictedTime.getTime() : null, line, position, destination,
+ capacity, message);
+
+ final Location location;
+ if (!stationBoardCanDoEquivs || depStation == null) {
+ location = new Location(LocationType.STATION, normalizedStationId,
+ stationPlaceAndName != null ? stationPlaceAndName[0] : null,
+ stationPlaceAndName != null ? stationPlaceAndName[1] : null);
+ } else {
+ final String[] depPlaceAndName = splitStationName(depStation);
+ location = new Location(LocationType.STATION, null, depPlaceAndName[0],
+ depPlaceAndName[1]);
+ }
+
+ StationDepartures stationDepartures = findStationDepartures(r.stationDepartures, location);
+ if (stationDepartures == null) {
+ stationDepartures = new StationDepartures(location, new ArrayList(8), null);
+ r.stationDepartures.add(stationDepartures);
+ }
+
+ stationDepartures.departures.add(departure);
+ }
+
+ XmlPullUtil.requireSkip(pp, "Journey");
+ }
+
+ if (stationBoardHasStationTable)
+ XmlPullUtil.exit(pp, "StationTable");
+
+ XmlPullUtil.requireEndDocument(pp);
+
+ // sort departures
+ for (final StationDepartures stationDepartures : r.stationDepartures)
+ Collections.sort(stationDepartures.departures, Departure.TIME_COMPARATOR);
+
+ result.set(r);
+ } catch (final XmlPullParserException x) {
+ throw new ParserException("cannot parse xml: " + firstChars, x);
+ }
+ }
+ }, url);
+
+ return result.get();
+ }
+
+ protected void addCustomReplaces(final StringReplaceReader reader) {
+ }
+
+ @Override
+ public QueryTripsResult queryTrips(final Location from, final @Nullable Location via, final Location to,
+ final Date date, final boolean dep, final @Nullable Set products,
+ final @Nullable Optimize optimize, final @Nullable WalkSpeed walkSpeed,
+ final @Nullable Accessibility accessibility, final @Nullable Set