de.schildbach.pte.AbstractEfaProvider.java Source code

Java tutorial

Introduction

Here is the source code for de.schildbach.pte.AbstractEfaProvider.java

Source

/*
 * Copyright 2010-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 <http://www.gnu.org/licenses/>.
 */

package de.schildbach.pte;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
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.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 javax.annotation.Nullable;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;

import de.schildbach.pte.dto.Departure;
import de.schildbach.pte.dto.Fare;
import de.schildbach.pte.dto.Fare.Type;
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.SuggestLocationsResult;
import de.schildbach.pte.dto.SuggestedLocation;
import de.schildbach.pte.dto.Trip;
import de.schildbach.pte.dto.Trip.Leg;
import de.schildbach.pte.exception.InvalidDataException;
import de.schildbach.pte.exception.ParserException;
import de.schildbach.pte.util.HttpClient;
import de.schildbach.pte.util.ParserUtils;
import de.schildbach.pte.util.XmlPullUtil;

import okhttp3.HttpUrl;
import okhttp3.ResponseBody;

/**
 * @author Andreas Schildbach
 */
public abstract class AbstractEfaProvider extends AbstractNetworkProvider {
    protected static final String DEFAULT_DEPARTURE_MONITOR_ENDPOINT = "XSLT_DM_REQUEST";
    protected static final String DEFAULT_TRIP_ENDPOINT = "XSLT_TRIP_REQUEST2";
    protected static final String DEFAULT_STOPFINDER_ENDPOINT = "XML_STOPFINDER_REQUEST";
    protected static final String DEFAULT_COORD_ENDPOINT = "XML_COORD_REQUEST";

    protected static final String SERVER_PRODUCT = "efa";

    private final HttpUrl departureMonitorEndpoint;
    private final HttpUrl tripEndpoint;
    private final HttpUrl stopFinderEndpoint;
    private final HttpUrl coordEndpoint;

    private String language = "de";
    private boolean needsSpEncId = false;
    private boolean includeRegionId = true;
    private boolean useProxFootSearch = true;
    private @Nullable String httpReferer = null;
    private @Nullable String httpRefererTrip = null;
    private boolean httpPost = false;
    private boolean useRouteIndexAsTripId = true;
    private boolean useLineRestriction = true;
    private boolean useStringCoordListOutputFormat = true;
    private float fareCorrectionFactor = 1f;

    private final XmlPullParserFactory parserFactory;

    private static final Logger log = LoggerFactory.getLogger(AbstractEfaProvider.class);

    @SuppressWarnings("serial")
    private static class Context implements QueryTripsContext {
        private final String context;

        private Context(final String context) {
            this.context = context;
        }

        @Override
        public boolean canQueryLater() {
            return context != null;
        }

        @Override
        public boolean canQueryEarlier() {
            return false; // TODO enable earlier querying
        }

        @Override
        public String toString() {
            return getClass().getName() + "[" + context + "]";
        }
    }

    public AbstractEfaProvider(final NetworkId network, final HttpUrl apiBase) {
        this(network, apiBase, null, null, null, null);
    }

    public AbstractEfaProvider(final NetworkId network, final HttpUrl apiBase,
            final String departureMonitorEndpoint, final String tripEndpoint, final String stopFinderEndpoint,
            final String coordEndpoint) {
        this(network,
                apiBase.newBuilder()
                        .addPathSegment(departureMonitorEndpoint != null ? departureMonitorEndpoint
                                : DEFAULT_DEPARTURE_MONITOR_ENDPOINT)
                        .build(),
                apiBase.newBuilder().addPathSegment(tripEndpoint != null ? tripEndpoint : DEFAULT_TRIP_ENDPOINT)
                        .build(),
                apiBase.newBuilder()
                        .addPathSegment(
                                stopFinderEndpoint != null ? stopFinderEndpoint : DEFAULT_STOPFINDER_ENDPOINT)
                        .build(),
                apiBase.newBuilder().addPathSegment(coordEndpoint != null ? coordEndpoint : DEFAULT_COORD_ENDPOINT)
                        .build());
    }

    public AbstractEfaProvider(final NetworkId network, final HttpUrl departureMonitorEndpoint,
            final HttpUrl tripEndpoint, final HttpUrl stopFinderEndpoint, final HttpUrl coordEndpoint) {
        super(network);

        try {
            parserFactory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME),
                    null);
        } catch (final XmlPullParserException x) {
            throw new RuntimeException(x);
        }

        this.departureMonitorEndpoint = departureMonitorEndpoint;
        this.tripEndpoint = tripEndpoint;
        this.stopFinderEndpoint = stopFinderEndpoint;
        this.coordEndpoint = coordEndpoint;
    }

    protected AbstractEfaProvider setLanguage(final String language) {
        this.language = language;
        return this;
    }

    protected AbstractEfaProvider setHttpReferer(final String httpReferer) {
        this.httpReferer = httpReferer;
        this.httpRefererTrip = httpReferer;
        return this;
    }

    public AbstractEfaProvider setHttpRefererTrip(final String httpRefererTrip) {
        this.httpRefererTrip = httpRefererTrip;
        return this;
    }

    protected AbstractEfaProvider setHttpPost(final boolean httpPost) {
        this.httpPost = httpPost;
        return this;
    }

    protected AbstractEfaProvider setIncludeRegionId(final boolean includeRegionId) {
        this.includeRegionId = includeRegionId;
        return this;
    }

    protected AbstractEfaProvider setUseProxFootSearch(final boolean useProxFootSearch) {
        this.useProxFootSearch = useProxFootSearch;
        return this;
    }

    protected AbstractEfaProvider setUseRouteIndexAsTripId(final boolean useRouteIndexAsTripId) {
        this.useRouteIndexAsTripId = useRouteIndexAsTripId;
        return this;
    }

    protected AbstractEfaProvider setUseLineRestriction(final boolean useLineRestriction) {
        this.useLineRestriction = useLineRestriction;
        return this;
    }

    protected AbstractEfaProvider setUseStringCoordListOutputFormat(final boolean useStringCoordListOutputFormat) {
        this.useStringCoordListOutputFormat = useStringCoordListOutputFormat;
        return this;
    }

    protected AbstractEfaProvider setNeedsSpEncId(final boolean needsSpEncId) {
        this.needsSpEncId = needsSpEncId;
        return this;
    }

    protected AbstractEfaProvider setFareCorrectionFactor(final float fareCorrectionFactor) {
        this.fareCorrectionFactor = fareCorrectionFactor;
        return this;
    }

    @Override
    protected boolean hasCapability(final Capability capability) {
        return true;
    }

    private final void appendCommonRequestParams(final HttpUrl.Builder url, final String outputFormat) {
        url.addEncodedQueryParameter("outputFormat", outputFormat);
        url.addEncodedQueryParameter("language", language);
        url.addEncodedQueryParameter("stateless", "1");
        url.addEncodedQueryParameter("coordOutputFormat", "WGS84");
    }

    protected SuggestLocationsResult jsonStopfinderRequest(final Location constraint) throws IOException {
        final HttpUrl.Builder url = stopFinderEndpoint.newBuilder();
        appendStopfinderRequestParameters(url, constraint, "JSON");
        final CharSequence page;
        if (httpPost)
            page = httpClient.get(url.build(), url.build().encodedQuery(), "application/x-www-form-urlencoded");
        else
            page = httpClient.get(url.build());
        final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT);

        try {
            final List<SuggestedLocation> locations = new ArrayList<>();

            final JSONObject head = new JSONObject(page.toString());
            final JSONObject stopFinder = head.optJSONObject("stopFinder");
            final JSONArray stops;
            if (stopFinder == null) {
                stops = head.getJSONArray("stopFinder");
            } else {
                final JSONArray messages = stopFinder.optJSONArray("message");
                if (messages != null) {
                    for (int i = 0; i < messages.length(); i++) {
                        final JSONObject message = messages.optJSONObject(i);
                        final String messageName = message.getString("name");
                        final String messageValue = Strings.emptyToNull(message.getString("value"));
                        if ("code".equals(messageName) && !"-8010".equals(messageValue)
                                && !"-8011".equals(messageValue))
                            return new SuggestLocationsResult(header, SuggestLocationsResult.Status.SERVICE_DOWN);
                    }
                }

                final JSONObject points = stopFinder.optJSONObject("points");
                if (points != null) {
                    final JSONObject stop = points.getJSONObject("point");
                    final SuggestedLocation location = parseJsonStop(stop);
                    locations.add(location);
                    return new SuggestLocationsResult(header, locations);
                }

                stops = stopFinder.optJSONArray("points");
                if (stops == null)
                    return new SuggestLocationsResult(header, locations);
            }

            final int nStops = stops.length();

            for (int i = 0; i < nStops; i++) {
                final JSONObject stop = stops.optJSONObject(i);
                final SuggestedLocation location = parseJsonStop(stop);
                locations.add(location);
            }

            return new SuggestLocationsResult(header, locations);
        } catch (final JSONException x) {
            throw new RuntimeException("cannot parse: '" + page + "' on " + url, x);
        }
    }

    private SuggestedLocation parseJsonStop(final JSONObject stop) throws JSONException {
        String type = stop.getString("type");
        if ("any".equals(type))
            type = stop.getString("anyType");
        final String id = stop.getString("stateless");
        final String name = normalizeLocationName(stop.optString("name"));
        final String object = normalizeLocationName(stop.optString("object"));
        final String postcode = stop.optString("postcode");
        final int quality = stop.getInt("quality");
        final JSONObject ref = stop.getJSONObject("ref");
        String place = ref.getString("place");
        if (place != null && place.length() == 0)
            place = null;
        final Point coord = parseCoord(ref.optString("coords", null));

        final Location location;
        if ("stop".equals(type))
            location = new Location(LocationType.STATION, id, coord, place, object);
        else if ("poi".equals(type))
            location = new Location(LocationType.POI, id, coord, place, object);
        else if ("crossing".equals(type))
            location = new Location(LocationType.ADDRESS, id, coord, place, object);
        else if ("street".equals(type) || "address".equals(type) || "singlehouse".equals(type)
                || "buildingname".equals(type))
            location = new Location(LocationType.ADDRESS, id, coord, place, name);
        else if ("postcode".equals(type))
            location = new Location(LocationType.ADDRESS, id, coord, place, postcode);
        else
            throw new JSONException("unknown type: " + type);

        return new SuggestedLocation(location, quality);
    }

    private void appendStopfinderRequestParameters(final HttpUrl.Builder url, final Location constraint,
            final String outputFormat) {
        appendCommonRequestParams(url, outputFormat);
        url.addEncodedQueryParameter("locationServerActive", "1");
        if (includeRegionId)
            url.addEncodedQueryParameter("regionID_sf", "1"); // prefer own region
        appendLocationParams(url, constraint, "sf");
        if (constraint.type == LocationType.ANY) {
            if (needsSpEncId)
                url.addEncodedQueryParameter("SpEncId", "0");
            // 1=place 2=stop 4=street 8=address 16=crossing 32=poi 64=postcode
            url.addEncodedQueryParameter("anyObjFilter_sf", Integer.toString(2 + 4 + 8 + 16 + 32 + 64));
            url.addEncodedQueryParameter("reducedAnyPostcodeObjFilter_sf", "64");
            url.addEncodedQueryParameter("reducedAnyTooManyObjFilter_sf", "2");
            url.addEncodedQueryParameter("useHouseNumberList", "true");
            url.addEncodedQueryParameter("anyMaxSizeHitList", "500");
        }
    }

    protected SuggestLocationsResult xmlStopfinderRequest(final Location constraint) throws IOException {
        final HttpUrl.Builder url = stopFinderEndpoint.newBuilder();
        appendStopfinderRequestParameters(url, constraint, "XML");
        final AtomicReference<SuggestLocationsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    final XmlPullParser pp = parserFactory.newPullParser();
                    pp.setInput(body.byteStream(), null); // Read encoding from XML declaration
                    final ResultHeader header = enterItdRequest(pp);

                    final List<SuggestedLocation> locations = new ArrayList<>();

                    XmlPullUtil.enter(pp, "itdStopFinderRequest");

                    processItdOdv(pp, "sf", new ProcessItdOdvCallback() {
                        @Override
                        public void location(final String nameState, final Location location,
                                final int matchQuality) {
                            locations.add(new SuggestedLocation(location, matchQuality));
                        }
                    });

                    XmlPullUtil.skipExit(pp, "itdStopFinderRequest");

                    result.set(new SuggestLocationsResult(header, locations));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpReferer);
        else
            httpClient.getInputStream(callback, url.build(), httpReferer);

        return result.get();
    }

    protected SuggestLocationsResult mobileStopfinderRequest(final Location constraint) throws IOException {
        final HttpUrl.Builder url = stopFinderEndpoint.newBuilder();
        appendStopfinderRequestParameters(url, constraint, "XML");
        final AtomicReference<SuggestLocationsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    final XmlPullParser pp = parserFactory.newPullParser();
                    pp.setInput(body.byteStream(), null); // Read encoding from XML declaration
                    final ResultHeader header = enterEfa(pp);

                    final List<SuggestedLocation> locations = new ArrayList<>();

                    XmlPullUtil.require(pp, "sf");
                    if (XmlPullUtil.optEnter(pp, "sf")) {
                        while (XmlPullUtil.optEnter(pp, "p")) {
                            final String name = normalizeLocationName(XmlPullUtil.valueTag(pp, "n"));
                            final String u = XmlPullUtil.valueTag(pp, "u");
                            if (!"sf".equals(u))
                                throw new RuntimeException("unknown usage: " + u);
                            final String ty = XmlPullUtil.valueTag(pp, "ty");
                            final LocationType type;
                            if ("stop".equals(ty))
                                type = LocationType.STATION;
                            else if ("poi".equals(ty))
                                type = LocationType.POI;
                            else if ("loc".equals(ty))
                                type = LocationType.COORD;
                            else if ("street".equals(ty))
                                type = LocationType.ADDRESS;
                            else if ("singlehouse".equals(ty))
                                type = LocationType.ADDRESS;
                            else
                                throw new RuntimeException("unknown type: " + ty);

                            XmlPullUtil.enter(pp, "r");

                            final String id = XmlPullUtil.valueTag(pp, "id");
                            XmlPullUtil.optValueTag(pp, "gid", null);
                            XmlPullUtil.valueTag(pp, "stateless");
                            XmlPullUtil.valueTag(pp, "omc");
                            final String place = normalizeLocationName(XmlPullUtil.optValueTag(pp, "pc", null));
                            XmlPullUtil.valueTag(pp, "pid");
                            final Point coord = parseCoord(XmlPullUtil.optValueTag(pp, "c", null));

                            XmlPullUtil.skipExit(pp, "r");

                            final String qal = XmlPullUtil.optValueTag(pp, "qal", null);
                            final int quality = qal != null ? Integer.parseInt(qal) : 0;

                            XmlPullUtil.skipExit(pp, "p");

                            final Location location = new Location(type, type == LocationType.STATION ? id : null,
                                    coord, place, name);
                            final SuggestedLocation locationAndQuality = new SuggestedLocation(location, quality);
                            locations.add(locationAndQuality);
                        }

                        XmlPullUtil.skipExit(pp, "sf");
                    }

                    result.set(new SuggestLocationsResult(header, locations));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpReferer);
        else
            httpClient.getInputStream(callback, url.build(), httpReferer);

        return result.get();
    }

    private void appendXmlCoordRequestParameters(final HttpUrl.Builder url, final EnumSet<LocationType> types,
            final int lat, final int lon, final int maxDistance, final int maxLocations) {
        appendCommonRequestParams(url, "XML");
        url.addEncodedQueryParameter("coord", ParserUtils.urlEncode(
                String.format(Locale.ENGLISH, "%2.6f:%2.6f:WGS84", latLonToDouble(lon), latLonToDouble(lat)),
                requestUrlEncoding));
        if (useStringCoordListOutputFormat)
            url.addEncodedQueryParameter("coordListOutputFormat", "STRING");
        url.addEncodedQueryParameter("max", Integer.toString(maxLocations != 0 ? maxLocations : 50));
        url.addEncodedQueryParameter("inclFilter", "1");
        int i = 1;
        for (final LocationType type : types) {
            url.addEncodedQueryParameter("radius_" + i, Integer.toString(maxDistance != 0 ? maxDistance : 1320));
            if (type == LocationType.STATION)
                url.addEncodedQueryParameter("type_" + i, "STOP");
            else if (type == LocationType.POI)
                url.addEncodedQueryParameter("type_" + i, "POI_POINT");
            else
                throw new IllegalArgumentException("cannot handle location type: " + type);
            i++;
        }
    }

    protected NearbyLocationsResult xmlCoordRequest(final EnumSet<LocationType> types, final int lat, final int lon,
            final int maxDistance, final int maxStations) throws IOException {
        final HttpUrl.Builder url = coordEndpoint.newBuilder();
        appendXmlCoordRequestParameters(url, types, lat, lon, maxDistance, maxStations);
        final AtomicReference<NearbyLocationsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    final XmlPullParser pp = parserFactory.newPullParser();
                    pp.setInput(body.byteStream(), null); // Read encoding from XML declaration
                    final ResultHeader header = enterItdRequest(pp);

                    XmlPullUtil.enter(pp, "itdCoordInfoRequest");

                    XmlPullUtil.enter(pp, "itdCoordInfo");

                    XmlPullUtil.enter(pp, "coordInfoRequest");
                    XmlPullUtil.skipExit(pp, "coordInfoRequest");

                    final List<Location> locations = new ArrayList<>();

                    if (XmlPullUtil.optEnter(pp, "coordInfoItemList")) {
                        while (XmlPullUtil.test(pp, "coordInfoItem")) {
                            final String type = XmlPullUtil.attr(pp, "type");
                            final LocationType locationType;
                            if ("STOP".equals(type))
                                locationType = LocationType.STATION;
                            else if ("POI_POINT".equals(type))
                                locationType = LocationType.POI;
                            else
                                throw new IllegalStateException("unknown type: " + type);

                            String id = XmlPullUtil.optAttr(pp, "stateless", null);
                            if (id == null)
                                id = XmlPullUtil.attr(pp, "id");

                            final String name = normalizeLocationName(XmlPullUtil.optAttr(pp, "name", null));
                            final String place = normalizeLocationName(XmlPullUtil.optAttr(pp, "locality", null));

                            XmlPullUtil.enter(pp, "coordInfoItem");

                            // FIXME this is always only one coordinate
                            final List<Point> path = processItdPathCoordinates(pp);
                            final Point coord = path != null ? path.get(0) : null;

                            XmlPullUtil.skipExit(pp, "coordInfoItem");

                            if (name != null)
                                locations.add(new Location(locationType, id, coord, place, name));
                        }

                        XmlPullUtil.skipExit(pp, "coordInfoItemList");
                    }

                    result.set(new NearbyLocationsResult(header, locations));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpReferer);
        else
            httpClient.getInputStream(callback, url.build(), httpReferer);

        return result.get();
    }

    protected NearbyLocationsResult mobileCoordRequest(final EnumSet<LocationType> types, final int lat,
            final int lon, final int maxDistance, final int maxStations) throws IOException {
        final HttpUrl.Builder url = coordEndpoint.newBuilder();
        appendXmlCoordRequestParameters(url, types, lat, lon, maxDistance, maxStations);
        final AtomicReference<NearbyLocationsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    final XmlPullParser pp = parserFactory.newPullParser();
                    pp.setInput(body.byteStream(), null); // Read encoding from XML declaration
                    final ResultHeader header = enterEfa(pp);

                    XmlPullUtil.enter(pp, "ci");

                    XmlPullUtil.enter(pp, "request");
                    XmlPullUtil.skipExit(pp, "request");

                    final List<Location> stations = new ArrayList<>();

                    if (XmlPullUtil.optEnter(pp, "pis")) {
                        while (XmlPullUtil.optEnter(pp, "pi")) {
                            final String name = normalizeLocationName(XmlPullUtil.optValueTag(pp, "de", null));
                            final String type = XmlPullUtil.valueTag(pp, "ty");
                            final LocationType locationType;
                            if ("STOP".equals(type))
                                locationType = LocationType.STATION;
                            else if ("POI_POINT".equals(type))
                                locationType = LocationType.POI;
                            else
                                throw new IllegalStateException("unknown type: " + type);

                            final String id = XmlPullUtil.valueTag(pp, "id");
                            XmlPullUtil.valueTag(pp, "omc");
                            XmlPullUtil.optValueTag(pp, "pid", null);
                            final String place = normalizeLocationName(XmlPullUtil.valueTag(pp, "locality"));
                            XmlPullUtil.valueTag(pp, "layer");
                            XmlPullUtil.valueTag(pp, "gisID");
                            XmlPullUtil.valueTag(pp, "ds");
                            XmlPullUtil.valueTag(pp, "stateless");
                            final Point coord = parseCoord(XmlPullUtil.valueTag(pp, "c"));

                            final Location location;
                            if (name != null)
                                location = new Location(locationType, id, coord, place, name);
                            else
                                location = new Location(locationType, id, coord, null, place);
                            stations.add(location);

                            XmlPullUtil.skipExit(pp, "pi");
                        }

                        XmlPullUtil.skipExit(pp, "pis");
                    }

                    XmlPullUtil.skipExit(pp, "ci");

                    result.set(new NearbyLocationsResult(header, stations));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpReferer);
        else
            httpClient.getInputStream(callback, url.build(), httpReferer);

        return result.get();
    }

    @Override
    public SuggestLocationsResult suggestLocations(final CharSequence constraint) throws IOException {
        return jsonStopfinderRequest(new Location(LocationType.ANY, null, null, constraint.toString()));
    }

    private interface ProcessItdOdvCallback {
        void location(String nameState, Location location, int matchQuality);
    }

    private String processItdOdv(final XmlPullParser pp, final String expectedUsage,
            final ProcessItdOdvCallback callback) throws XmlPullParserException, IOException {
        if (!XmlPullUtil.test(pp, "itdOdv"))
            throw new IllegalStateException("expecting <itdOdv />");

        final String usage = XmlPullUtil.attr(pp, "usage");
        if (expectedUsage != null && !usage.equals(expectedUsage))
            throw new IllegalStateException("expecting <itdOdv usage=\"" + expectedUsage + "\" />");

        final String type = XmlPullUtil.attr(pp, "type");

        XmlPullUtil.enter(pp, "itdOdv");

        final String place = processItdOdvPlace(pp);

        XmlPullUtil.require(pp, "itdOdvName");
        final String nameState = XmlPullUtil.attr(pp, "state");
        XmlPullUtil.enter(pp, "itdOdvName");

        XmlPullUtil.optSkip(pp, "itdMessage");

        if ("identified".equals(nameState)) {
            final Location location = processOdvNameElem(pp, type, place);
            if (location != null)
                callback.location(nameState, location, Integer.MAX_VALUE);
        } else if ("list".equals(nameState)) {
            while (XmlPullUtil.test(pp, "odvNameElem")) {
                final int matchQuality = XmlPullUtil.intAttr(pp, "matchQuality");
                final Location location = processOdvNameElem(pp, type, place);
                if (location != null)
                    callback.location(nameState, location, matchQuality);
            }
        } else if ("notidentified".equals(nameState) || "empty".equals(nameState)) {
            XmlPullUtil.optSkip(pp, "odvNameElem");
        } else {
            throw new RuntimeException("cannot handle nameState '" + nameState + "'");
        }

        XmlPullUtil.optSkipMultiple(pp, "infoLink");
        XmlPullUtil.optSkip(pp, "odvNameInput");

        XmlPullUtil.exit(pp, "itdOdvName");

        XmlPullUtil.optSkip(pp, "odvInfoList");

        XmlPullUtil.optSkip(pp, "itdPoiHierarchyRoot");

        if (XmlPullUtil.optEnter(pp, "itdOdvAssignedStops")) {
            while (XmlPullUtil.test(pp, "itdOdvAssignedStop")) {
                final Location stop = processItdOdvAssignedStop(pp);

                if (stop != null)
                    callback.location("assigned", stop, 0);
            }

            XmlPullUtil.exit(pp, "itdOdvAssignedStops");
        }

        XmlPullUtil.optSkip(pp, "itdServingModes");

        XmlPullUtil.optSkip(pp, "genAttrList");

        XmlPullUtil.exit(pp, "itdOdv");

        return nameState;
    }

    private String processItdOdvPlace(final XmlPullParser pp) throws XmlPullParserException, IOException {
        if (!XmlPullUtil.test(pp, "itdOdvPlace"))
            throw new IllegalStateException("expecting <itdOdvPlace />");

        final String placeState = XmlPullUtil.attr(pp, "state");

        XmlPullUtil.enter(pp, "itdOdvPlace");
        String place = null;
        if ("identified".equals(placeState)) {
            if (XmlPullUtil.test(pp, "odvPlaceElem"))
                place = normalizeLocationName(XmlPullUtil.valueTag(pp, "odvPlaceElem"));
        }
        XmlPullUtil.skipExit(pp, "itdOdvPlace");

        return place;
    }

    private Location processOdvNameElem(final XmlPullParser pp, String type, final String defaultPlace)
            throws XmlPullParserException, IOException {
        if (!XmlPullUtil.test(pp, "odvNameElem"))
            throw new IllegalStateException("expecting <odvNameElem />");

        if ("any".equals(type))
            type = XmlPullUtil.attr(pp, "anyType");
        final String id = XmlPullUtil.attr(pp, "stateless");
        final String locality = normalizeLocationName(XmlPullUtil.optAttr(pp, "locality", null));
        final String objectName = normalizeLocationName(XmlPullUtil.optAttr(pp, "objectName", null));
        final String buildingName = XmlPullUtil.optAttr(pp, "buildingName", null);
        final String buildingNumber = XmlPullUtil.optAttr(pp, "buildingNumber", null);
        final String postCode = XmlPullUtil.optAttr(pp, "postCode", null);
        final String streetName = XmlPullUtil.optAttr(pp, "streetName", null);
        final Point coord = processCoordAttr(pp);

        XmlPullUtil.enter(pp, "odvNameElem");
        XmlPullUtil.optSkip(pp, "itdMapItemList");
        final String nameElem;
        if (pp.getEventType() == XmlPullParser.TEXT) {
            nameElem = normalizeLocationName(pp.getText());
            pp.next();
        } else {
            nameElem = null;
        }
        XmlPullUtil.exit(pp, "odvNameElem");

        final LocationType locationType;
        final String place;
        final String name;

        if ("stop".equals(type)) {
            locationType = LocationType.STATION;
            place = locality != null ? locality : defaultPlace;
            name = objectName != null ? objectName : nameElem;
        } else if ("poi".equals(type)) {
            locationType = LocationType.POI;
            place = locality != null ? locality : defaultPlace;
            name = objectName != null ? objectName : nameElem;
        } else if ("loc".equals(type)) {
            if (coord != null) {
                locationType = LocationType.COORD;
                place = null;
                name = null;
            } else {
                locationType = LocationType.ADDRESS;
                place = null;
                name = locality;
            }
        } else if ("address".equals(type) || "singlehouse".equals(type)) {
            locationType = LocationType.ADDRESS;
            place = locality != null ? locality : defaultPlace;
            name = objectName + (buildingNumber != null ? " " + buildingNumber : "");
        } else if ("street".equals(type) || "crossing".equals(type)) {
            locationType = LocationType.ADDRESS;
            place = locality != null ? locality : defaultPlace;
            name = objectName != null ? objectName : nameElem;
        } else if ("postcode".equals(type)) {
            locationType = LocationType.ADDRESS;
            place = locality != null ? locality : defaultPlace;
            name = postCode;
        } else if ("buildingname".equals(type)) {
            locationType = LocationType.ADDRESS;
            place = locality != null ? locality : defaultPlace;
            name = buildingName != null ? buildingName : streetName;
        } else if ("coord".equals(type)) {
            locationType = LocationType.ADDRESS;
            place = defaultPlace;
            name = nameElem;
        } else {
            throw new IllegalArgumentException("unknown type/anyType: " + type);
        }

        return new Location(locationType, id, coord, place, name);
    }

    private Location processItdOdvAssignedStop(final XmlPullParser pp) throws XmlPullParserException, IOException {
        final String id = XmlPullUtil.attr(pp, "stopID");
        final Point coord = processCoordAttr(pp);
        final String place = normalizeLocationName(XmlPullUtil.optAttr(pp, "place", null));
        final String name = normalizeLocationName(XmlPullUtil.optValueTag(pp, "itdOdvAssignedStop", null));

        if (name != null)
            return new Location(LocationType.STATION, id, coord, place, name);
        else
            return null;
    }

    @Override
    public NearbyLocationsResult queryNearbyLocations(final EnumSet<LocationType> types, final Location location,
            final int maxDistance, final int maxLocations) throws IOException {
        if (location.hasLocation())
            return xmlCoordRequest(types, location.lat, location.lon, maxDistance, maxLocations);

        if (location.type != LocationType.STATION)
            throw new IllegalArgumentException("cannot handle: " + location.type);

        if (!location.hasId())
            throw new IllegalArgumentException("at least one of stationId or lat/lon must be given");

        return nearbyStationsRequest(location.id, maxLocations);
    }

    private NearbyLocationsResult nearbyStationsRequest(final String stationId, final int maxLocations)
            throws IOException {
        final HttpUrl.Builder url = departureMonitorEndpoint.newBuilder();
        appendCommonRequestParams(url, "XML");
        url.addEncodedQueryParameter("type_dm", "stop");
        url.addEncodedQueryParameter("name_dm",
                ParserUtils.urlEncode(normalizeStationId(stationId), requestUrlEncoding));
        url.addEncodedQueryParameter("itOptionsActive", "1");
        url.addEncodedQueryParameter("ptOptionsActive", "1");
        if (useProxFootSearch)
            url.addEncodedQueryParameter("useProxFootSearch", "1");
        url.addEncodedQueryParameter("mergeDep", "1");
        url.addEncodedQueryParameter("useAllStops", "1");
        url.addEncodedQueryParameter("mode", "direct");
        final AtomicReference<NearbyLocationsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    final XmlPullParser pp = parserFactory.newPullParser();
                    pp.setInput(body.byteStream(), null); // Read encoding from XML declaration
                    final ResultHeader header = enterItdRequest(pp);

                    XmlPullUtil.enter(pp, "itdDepartureMonitorRequest");

                    final AtomicReference<Location> ownStation = new AtomicReference<>();
                    final List<Location> stations = new ArrayList<>();

                    final String nameState = processItdOdv(pp, "dm", new ProcessItdOdvCallback() {
                        @Override
                        public void location(final String nameState, final Location location,
                                final int matchQuality) {
                            if (location.type == LocationType.STATION) {
                                if ("identified".equals(nameState))
                                    ownStation.set(location);
                                else if ("assigned".equals(nameState))
                                    stations.add(location);
                            } else {
                                throw new IllegalStateException("cannot handle: " + location.type);
                            }
                        }
                    });

                    if ("notidentified".equals(nameState)) {
                        result.set(new NearbyLocationsResult(header, NearbyLocationsResult.Status.INVALID_ID));
                        return;
                    }

                    if (ownStation.get() != null && !stations.contains(ownStation))
                        stations.add(ownStation.get());

                    if (maxLocations == 0 || maxLocations >= stations.size())
                        result.set(new NearbyLocationsResult(header, stations));
                    else
                        result.set(new NearbyLocationsResult(header, stations.subList(0, maxLocations)));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpReferer);
        else
            httpClient.getInputStream(callback, url.build(), httpReferer);

        return result.get();
    }

    private static final Pattern P_LINE_RE = Pattern.compile("RE ?\\d+");
    private static final Pattern P_LINE_RB = Pattern.compile("RB ?\\d+");
    private static final Pattern P_LINE_R = Pattern.compile("R ?\\d+");
    private static final Pattern P_LINE_S = Pattern.compile("S ?\\d+");
    private static final Pattern P_LINE_S_DB = Pattern.compile("(S\\d+) \\((?:DB Regio AG)\\)");
    private static final Pattern P_LINE_NUMBER = Pattern.compile("\\d+");

    protected Line parseLine(final @Nullable String id, final @Nullable String network, final @Nullable String mot,
            @Nullable String symbol, final @Nullable String name, final @Nullable String longName,
            final @Nullable String trainType, final @Nullable String trainNum, final @Nullable String trainName) {
        if (mot == null) {
            if (trainName != null) {
                final String str = Strings.nullToEmpty(name);
                if (trainName.equals("S-Bahn"))
                    return new Line(id, network, Product.SUBURBAN_TRAIN, str);
                if (trainName.equals("U-Bahn"))
                    return new Line(id, network, Product.SUBWAY, str);
                if (trainName.equals("Straenbahn"))
                    return new Line(id, network, Product.TRAM, str);
                if (trainName.equals("Badner Bahn"))
                    return new Line(id, network, Product.TRAM, str);
                if (trainName.equals("Stadtbus"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("Citybus"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("Regionalbus"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("BB-Postbus"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("Autobus"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("Discobus"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("Nachtbus"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("Anrufsammeltaxi"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("Ersatzverkehr"))
                    return new Line(id, network, Product.BUS, str);
                if (trainName.equals("Vienna Airport Lines"))
                    return new Line(id, network, Product.BUS, str);
            }
        } else if ("0".equals(mot)) {
            final String trainNumStr = Strings.nullToEmpty(trainNum);

            if (("EC".equals(trainType) || "EuroCity".equals(trainName) || "Eurocity".equals(trainName))
                    && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "EC" + trainNum);
            if (("EN".equals(trainType) || "EuroNight".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "EN" + trainNum);
            if (("IC".equals(trainType) || "InterCity".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "IC" + trainNum);
            if (("ICE".equals(trainType) || "ICE".equals(trainName) || "Intercity-Express".equals(trainName))
                    && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "ICE" + trainNum);
            if (("ICN".equals(trainType) || "InterCityNight".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "ICN" + trainNum);
            if (("X".equals(trainType) || "InterConnex".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "X" + trainNum);
            if (("CNL".equals(trainType) || "CityNightLine".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "CNL" + trainNum);
            if (("THA".equals(trainType) || "Thalys".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "THA" + trainNum);
            if ("RHI".equals(trainType) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "RHI" + trainNum);
            if (("TGV".equals(trainType) || "TGV".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "TGV" + trainNum);
            if ("TGD".equals(trainType) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "TGD" + trainNum);
            if ("INZ".equals(trainType) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "INZ" + trainNum);
            if (("RJ".equals(trainType) || "railjet".equals(trainName)) && trainNum != null) // railjet
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "RJ" + trainNum);
            if (("WB".equals(trainType) || "WESTbahn".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "WB" + trainNum);
            if (("HKX".equals(trainType) || "Hamburg-Kln-Express".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "HKX" + trainNum);
            if ("INT".equals(trainType) && trainNum != null) // SVV, VAGFR
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "INT" + trainNum);
            if (("SC".equals(trainType) || "SC Pendolino".equals(trainName)) && trainNum != null) // SuperCity
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "SC" + trainNum);
            if ("ECB".equals(trainType) && trainNum != null) // EC, Verona-Mnchen
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "ECB" + trainNum);
            if ("ES".equals(trainType) && trainNum != null) // Eurostar Italia
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "ES" + trainNum);
            if (("EST".equals(trainType) || "EUROSTAR".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "EST" + trainNum);
            if ("EIC".equals(trainType) && trainNum != null) // Ekspres InterCity, Polen
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "EIC" + trainNum);
            if ("MT".equals(trainType) && "Schnee-Express".equals(trainName) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "MT" + trainNum);
            if (("TLK".equals(trainType) || "Tanie Linie Kolejowe".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "TLK" + trainNum);
            if ("DNZ".equals(trainType) && trainNum != null) // Nacht-Schnellzug
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "DNZ" + trainNum);
            if ("AVE".equals(trainType) && trainNum != null) // klimatisierter Hochgeschwindigkeitszug
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "DNZ" + trainNum);
            if ("ARC".equals(trainType) && trainNum != null) // Arco/Alvia/Avant (Renfe), Spanien
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "ARC" + trainNum);
            if ("HOT".equals(trainType) && trainNum != null) // Spanien, Nacht
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "HOT" + trainNum);
            if ("Locomore".equals(longName))
                return new Line(id, network, Product.HIGH_SPEED_TRAIN, "LOC" + Strings.nullToEmpty(trainNum));

            if ("IR".equals(trainType) || "Interregio".equals(trainName) || "InterRegio".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "IR" + trainNum);
            if ("IRE".equals(trainType) || "Interregio-Express".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "IRE" + trainNum);
            if ("InterRegioExpress".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "IRE" + trainNumStr);
            if ("RE".equals(trainType) || "Regional-Express".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "RE" + trainNum);
            if (trainType == null && trainNum != null && P_LINE_RE.matcher(trainNum).matches())
                return new Line(id, network, Product.REGIONAL_TRAIN, trainNum);
            if ("RE6a".equals(trainNum) && trainType == null && trainName == null)
                return new Line(id, network, Product.REGIONAL_TRAIN, trainNum);
            if ("RE3 / RB30".equals(trainNum) && trainType == null && trainName == null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "RE3/RB30");
            if ("Regionalexpress".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("R-Bahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("RB-Bahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if (trainType == null && "RB67/71".equals(trainNum))
                return new Line(id, network, Product.REGIONAL_TRAIN, trainNum);
            if (trainType == null && "RB65/68".equals(trainNum))
                return new Line(id, network, Product.REGIONAL_TRAIN, trainNum);
            if ("RE-Bahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("REX".equals(trainType)) // RegionalExpress, sterreich
                return new Line(id, network, Product.REGIONAL_TRAIN, "REX" + trainNum);
            if (("RB".equals(trainType) || "Regionalbahn".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "RB" + trainNum);
            if (trainType == null && trainNum != null && P_LINE_RB.matcher(trainNum).matches())
                return new Line(id, network, Product.REGIONAL_TRAIN, trainNum);
            if ("Abellio-Zug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("Westfalenbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("Chiemseebahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("R".equals(trainType) || "Regionalzug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "R" + trainNum);
            if (trainType == null && trainNum != null && P_LINE_R.matcher(trainNum).matches())
                return new Line(id, network, Product.REGIONAL_TRAIN, trainNum);
            if ("D".equals(trainType) || "Schnellzug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "D" + trainNum);
            if ("E".equals(trainType) || "Eilzug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "E" + trainNum);
            if ("WFB".equals(trainType) || "WestfalenBahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "WFB" + trainNum);
            if (("NWB".equals(trainType) || "NordWestBahn".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "NWB" + trainNum);
            if ("WES".equals(trainType) || "Westbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "WES" + trainNum);
            if ("ERB".equals(trainType) || "eurobahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "ERB" + trainNum);
            if ("CAN".equals(trainType) || "cantus Verkehrsgesellschaft".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "CAN" + trainNum);
            if ("HEX".equals(trainType) || "Veolia Verkehr Sachsen-Anhalt".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "HEX" + trainNum);
            if ("EB".equals(trainType) || "Erfurter Bahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "EB" + trainNum);
            if ("Erfurter Bahn".equals(longName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "EB");
            if ("EBx".equals(trainType) || "Erfurter Bahn Express".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "EBx" + trainNum);
            if ("Erfurter Bahn Express".equals(longName) && symbol == null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "EBx");
            if ("MR".equals(trainType) && "Mrkische Regiobahn".equals(trainName) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "MR" + trainNum);
            if ("MRB".equals(trainType) || "Mitteldeutsche Regiobahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "MRB" + trainNum);
            if ("ABR".equals(trainType) || "ABELLIO Rail NRW GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "ABR" + trainNum);
            if ("NEB".equals(trainType) || "NEB Niederbarnimer Eisenbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "NEB" + trainNum);
            if ("OE".equals(trainType) || "Ostdeutsche Eisenbahn GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "OE" + trainNum);
            if ("Ostdeutsche Eisenbahn GmbH".equals(longName) && symbol == null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "OE");
            if ("ODE".equals(trainType) && symbol != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("OLA".equals(trainType) || "Ostseeland Verkehr GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "OLA" + trainNum);
            if ("UBB".equals(trainType) || "Usedomer Bderbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "UBB" + trainNum);
            if ("EVB".equals(trainType) || "ELBE-WESER GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "EVB" + trainNum);
            if ("RTB".equals(trainType) || "Rurtalbahn GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "RTB" + trainNum);
            if ("STB".equals(trainType) || "Sd-Thringen-Bahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "STB" + trainNum);
            if ("HTB".equals(trainType) || "Hellertalbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "HTB" + trainNum);
            if ("VBG".equals(trainType) || "Vogtlandbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "VBG" + trainNum);
            if ("CB".equals(trainType) || "City-Bahn Chemnitz".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "CB" + trainNum);
            if (trainType == null && ("C11".equals(trainNum) || "C13".equals(trainNum) || "C14".equals(trainNum)
                    || "C15".equals(trainNum)))
                return new Line(id, network, Product.REGIONAL_TRAIN, trainNum);
            if ("VEC".equals(trainType) || "vectus Verkehrsgesellschaft".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "VEC" + trainNum);
            if ("HzL".equals(trainType) || "Hohenzollerische Landesbahn AG".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "HzL" + trainNum);
            if ("SBB".equals(trainType) || "SBB GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "SBB" + trainNum);
            if ("MBB".equals(trainType) || "Mecklenburgische Bderbahn Molli".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "MBB" + trainNum);
            if ("OS".equals(trainType)) // Osobn vlak
                return new Line(id, network, Product.REGIONAL_TRAIN, "OS" + trainNum);
            if ("SP".equals(trainType) || "Sp".equals(trainType)) // Spn vlak
                return new Line(id, network, Product.REGIONAL_TRAIN, "SP" + trainNum);
            if ("Dab".equals(trainType) || "Daadetalbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "Dab" + trainNum);
            if ("FEG".equals(trainType) || "Freiberger Eisenbahngesellschaft".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "FEG" + trainNum);
            if ("ARR".equals(trainType) || "ARRIVA".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "ARR" + trainNum);
            if ("HSB".equals(trainType) || "Harzer Schmalspurbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "HSB" + trainNum);
            if ("ALX".equals(trainType) || "alex - Lnderbahn und Vogtlandbahn GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "ALX" + trainNum);
            if ("EX".equals(trainType) || "Fatra".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "EX" + trainNum);
            if ("ME".equals(trainType) || "metronom".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "ME" + trainNum);
            if ("metronom".equals(longName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "ME");
            if ("MEr".equals(trainType))
                return new Line(id, network, Product.REGIONAL_TRAIN, "MEr" + trainNum);
            if ("AKN".equals(trainType) || "AKN Eisenbahn AG".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "AKN" + trainNum);
            if ("SOE".equals(trainType) || "Schsisch-Oberlausitzer Eisenbahngesellschaft".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "SOE" + trainNum);
            if ("VIA".equals(trainType) || "VIAS GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "VIA" + trainNum);
            if ("BRB".equals(trainType) || "Bayerische Regiobahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "BRB" + trainNum);
            if ("BLB".equals(trainType) || "Berchtesgadener Land Bahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "BLB" + trainNum);
            if ("HLB".equals(trainType) || "Hessische Landesbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "HLB" + trainNum);
            if ("NOB".equals(trainType) || "NordOstseeBahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "NOB" + trainNum);
            if ("NBE".equals(trainType) || "Nordbahn Eisenbahngesellschaft".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "NBE" + trainNum);
            if ("VEN".equals(trainType) || "Rhenus Veniro".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "VEN" + trainType);
            if ("DPN".equals(trainType) || "Nahreisezug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "DPN" + trainNum);
            if ("RBG".equals(trainType) || "Regental Bahnbetriebs GmbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "RBG" + trainNum);
            if ("BOB".equals(trainType) || "Bodensee-Oberschwaben-Bahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "BOB" + trainNum);
            if ("VE".equals(trainType) || "Vetter".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "VE" + trainNum);
            if ("SDG".equals(trainType) || "SDG Schsische Dampfeisenbahngesellschaft mbH".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "SDG" + trainNum);
            if ("PRE".equals(trainType) || "Pressnitztalbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "PRE" + trainNum);
            if ("VEB".equals(trainType) || "Vulkan-Eifel-Bahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "VEB" + trainNum);
            if ("neg".equals(trainType) || "Norddeutsche Eisenbahn Gesellschaft".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "neg" + trainNum);
            if ("AVG".equals(trainType) || "Felsenland-Express".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "AVG" + trainNum);
            if ("P".equals(trainType) || "BayernBahn Betriebs-GmbH".equals(trainName)
                    || "Brohltalbahn".equals(trainName) || "Kasbachtalbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "P" + trainNum);
            if ("SBS".equals(trainType) || "Stdtebahn Sachsen".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "SBS" + trainNum);
            if ("SES".equals(trainType) || "Stdteexpress Sachsen".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "SES" + trainNum);
            if ("SB-".equals(trainType)) // Stdtebahn Sachsen
                return new Line(id, network, Product.REGIONAL_TRAIN, "SB" + trainNum);
            if ("ag".equals(trainType)) // agilis
                return new Line(id, network, Product.REGIONAL_TRAIN, "ag" + trainNum);
            if ("agi".equals(trainType) || "agilis".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "agi" + trainNum);
            if ("as".equals(trainType) || "agilis-Schnellzug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "as" + trainNum);
            if ("TLX".equals(trainType) || "TRILEX".equals(trainName)) // Trilex (Vogtlandbahn)
                return new Line(id, network, Product.REGIONAL_TRAIN, "TLX" + trainNum);
            if ("MSB".equals(trainType) || "Mainschleifenbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "MSB" + trainNum);
            if ("BE".equals(trainType) || "Bentheimer Eisenbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "BE" + trainNum);
            if ("erx".equals(trainType) || "erixx - Der Heidesprinter".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "erx" + trainNum);
            if (("ERX".equals(trainType) || "Erixx".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "ERX" + trainNum);
            if (("SWE".equals(trainType) || "Sdwestdeutsche Verkehrs-AG".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "SWE" + trainNum);
            if ("SWEG-Zug".equals(trainName)) // Sdwestdeutschen Verkehrs-Aktiengesellschaft
                return new Line(id, network, Product.REGIONAL_TRAIN, "SWEG" + trainNum);
            if ("SWEG-Zug".equals(longName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "SWEG");
            if ("EGP Eisenbahngesellschaft Potsdam".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "EGP" + trainNumStr);
            if ("BB".equals(trainType) || "BB".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "BB" + trainNum);
            if ("CAT".equals(trainType)) // City Airport Train Wien
                return new Line(id, network, Product.REGIONAL_TRAIN, "CAT" + trainNum);
            if ("DZ".equals(trainType) || "Dampfzug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "DZ" + trainNum);
            if ("CD".equals(trainType)) // Tschechien
                return new Line(id, network, Product.REGIONAL_TRAIN, "CD" + trainNum);
            if ("VR".equals(trainType)) // Polen
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("PR".equals(trainType)) // Polen
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("KD".equals(trainType)) // Koleje Dolnolskie (Niederschlesische Eisenbahn)
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("Koleje Dolnoslaskie".equals(trainName) && symbol != null) // Koleje Dolnolskie
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("OO".equals(trainType) || "Ordinary passenger (o.pas.)".equals(trainName)) // GB
                return new Line(id, network, Product.REGIONAL_TRAIN, "OO" + trainNum);
            if ("XX".equals(trainType) || "Express passenger    (ex.pas.)".equals(trainName)) // GB
                return new Line(id, network, Product.REGIONAL_TRAIN, "XX" + trainNum);
            if ("XZ".equals(trainType) || "Express passenger sleeper".equals(trainName)) // GB
                return new Line(id, network, Product.REGIONAL_TRAIN, "XZ" + trainNum);
            if ("ATB".equals(trainType)) // Autoschleuse Tauernbahn
                return new Line(id, network, Product.REGIONAL_TRAIN, "ATB" + trainNum);
            if ("ATZ".equals(trainType)) // Autozug
                return new Line(id, network, Product.REGIONAL_TRAIN, "ATZ" + trainNum);
            if ("AZ".equals(trainType) || "Auto-Zug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "AZ" + trainNum);
            if ("DWE".equals(trainType) || "Dessau-Wrlitzer Eisenbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "DWE" + trainNum);
            if ("KTB".equals(trainType) || "Kandertalbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "KTB" + trainNum);
            if ("CBC".equals(trainType) || "CBC".equals(trainName)) // City-Bahn Chemnitz
                return new Line(id, network, Product.REGIONAL_TRAIN, "CBC" + trainNum);
            if ("Bernina Express".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, trainNum);
            if ("STR".equals(trainType)) // Harzquerbahn, Nordhausen
                return new Line(id, network, Product.REGIONAL_TRAIN, "STR" + trainNum);
            if ("EXT".equals(trainType) || "Extrazug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "EXT" + trainNum);
            if ("Heritage Railway".equals(trainName)) // GB
                return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
            if ("WTB".equals(trainType) || "Wutachtalbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "WTB" + trainNum);
            if ("DB".equals(trainType) || "DB Regio".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "DB" + trainNum);
            if ("M".equals(trainType) && "Meridian".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "M" + trainNum);
            if ("M".equals(trainType) && "Messezug".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "M" + trainNum);
            if ("EZ".equals(trainType)) // BB Erlebniszug
                return new Line(id, network, Product.REGIONAL_TRAIN, "EZ" + trainNum);
            if ("DPF".equals(trainType))
                return new Line(id, network, Product.REGIONAL_TRAIN, "DPF" + trainNum);
            if ("WBA".equals(trainType) || "Waldbahn".equals(trainName))
                return new Line(id, network, Product.REGIONAL_TRAIN, "WBA" + trainNum);
            if ("BA".equals(trainType) && trainNum != null) // Eisenbahn-Betriebsgesellschaft Ochsenhausen
                return new Line(id, network, Product.REGIONAL_TRAIN, "BA" + trainNum);
            if (("UEF".equals(trainType) || "Ulmer Eisenbahnfreunde".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "UEF" + trainNum);
            if (("DBG".equals(trainType) || "Dllnitzbahn".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "DBG" + trainNum);
            if (("TL".equals(trainType) || "Trilex".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "TL" + trainNum);
            if (("OPB".equals(trainType) || "oberpfalzbahn".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "OPB" + trainNum);
            if (("OPX".equals(trainType) || "oberpfalz-express".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "OPX" + trainNum);
            if (("LEO".equals(trainType) || "Chiemgauer Lokalbahn".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "LEO" + trainNum);
            if (("VAE".equals(trainType) || "Voralpen-Express".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "VAE" + trainNum);
            if (("V6".equals(trainType) || "vlexx".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "vlexx" + trainNum);
            if (("ARZ".equals(trainType) || "Autoreisezug".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "ARZ" + trainNum);
            if ("RR".equals(trainType))
                return new Line(id, network, Product.REGIONAL_TRAIN, "RR" + Strings.nullToEmpty(trainNum));
            if (("TER".equals(trainType) || "Train Express Regional".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "TER" + trainNum);
            if (("ENO".equals(trainType) || "enno".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "ENO" + trainNum);
            if ("enno".equals(longName) && symbol == null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "enno");
            if (("PLB".equals(trainType) || "Pinzgauer Lokalbahn".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "PLB" + trainNum);
            if (("NX".equals(trainType) || "National Express".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "NX" + trainNum);
            if (("SE".equals(trainType) || "ABELLIO Rail Mitteldeutschland GmbH".equals(trainName))
                    && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "SE" + trainNum);

            if (("BSB".equals(trainType) || "Breisgau-S-Bahn Gmbh".equals(trainName)) && trainNum != null)
                return new Line(id, network, Product.REGIONAL_TRAIN, "BSB" + trainNum);
            if ("BSB-Zug".equals(trainName) && trainNum != null) // Breisgau-S-Bahn
                return new Line(id, network, Product.SUBURBAN_TRAIN, trainNum);
            if ("BSB-Zug".equals(trainName) && trainNum == null)
                return new Line(id, network, Product.SUBURBAN_TRAIN, "BSB");
            if ("BSB-Zug".equals(longName))
                return new Line(id, network, Product.SUBURBAN_TRAIN, "BSB");
            if ("RSB".equals(trainType)) // Regionalschnellbahn, Wien
                return new Line(id, network, Product.SUBURBAN_TRAIN, "RSB" + trainNum);
            if ("RER".equals(trainName) && symbol != null && symbol.length() == 1) // Rseau Express Rgional
                return new Line(id, network, Product.SUBURBAN_TRAIN, symbol);
            if ("S".equals(trainType))
                return new Line(id, network, Product.SUBURBAN_TRAIN, "S" + trainNum);
            if ("S-Bahn".equals(trainName))
                return new Line(id, network, Product.SUBURBAN_TRAIN, "S" + trainNumStr);

            if ("RT".equals(trainType) || "RegioTram".equals(trainName))
                return new Line(id, network, Product.TRAM, "RT" + trainNum);

            if ("Bus".equals(trainType) && trainNum != null)
                return new Line(id, network, Product.BUS, trainNum);
            if ("Bus".equals(longName) && symbol == null)
                return new Line(id, network, Product.BUS, longName);
            if ("SEV".equals(trainType) || "SEV".equals(trainNum) || "SEV".equals(trainName) || "SEV".equals(symbol)
                    || "BSV".equals(trainType) || "Ersatzverkehr".equals(trainName)
                    || "Schienenersatzverkehr".equals(trainName))
                return new Line(id, network, Product.BUS, "SEV" + trainNumStr);
            if ("Bus replacement".equals(trainName)) // GB
                return new Line(id, network, Product.BUS, "BR");
            if ("BR".equals(trainType) && trainName != null && trainName.startsWith("Bus")) // GB
                return new Line(id, network, Product.BUS, "BR" + trainNum);
            if ("EXB".equals(trainType) && trainNum != null)
                return new Line(id, network, Product.BUS, "EXB" + trainNum);

            if ("GB".equals(trainType)) // Gondelbahn
                return new Line(id, network, Product.CABLECAR, "GB" + trainNum);
            if ("SB".equals(trainType)) // Seilbahn
                return new Line(id, network, Product.SUBURBAN_TRAIN, "SB" + trainNum);

            if ("Zug".equals(trainName) && symbol != null)
                return new Line(id, network, null, symbol);
            if ("Zug".equals(longName) && symbol == null)
                return new Line(id, network, null, "Zug");
            if ("Zuglinie".equals(trainName) && symbol != null)
                return new Line(id, network, null, symbol);
            if ("ZUG".equals(trainType) && trainNum != null)
                return new Line(id, network, null, trainNum);
            if (symbol != null && P_LINE_NUMBER.matcher(symbol).matches() && trainType == null && trainName == null)
                return new Line(id, network, null, symbol);
            if ("N".equals(trainType) && trainName == null && symbol == null)
                return new Line(id, network, null, "N" + trainNum);
            if ("Train".equals(trainName))
                return new Line(id, network, null, null);

            // generic
            if (trainName != null && trainType == null && trainNum == null)
                return new Line(id, network, null, trainName);
        } else if ("1".equals(mot)) {
            if (symbol != null && P_LINE_S.matcher(symbol).matches())
                return new Line(id, network, Product.SUBURBAN_TRAIN, symbol);
            if (name != null && P_LINE_S.matcher(name).matches())
                return new Line(id, network, Product.SUBURBAN_TRAIN, name);
            if ("S-Bahn".equals(trainName))
                return new Line(id, network, Product.SUBURBAN_TRAIN, "S" + Strings.nullToEmpty(trainNum));
            if (symbol != null && symbol.equals(name)) {
                final Matcher m = P_LINE_S_DB.matcher(symbol);
                if (m.matches())
                    return new Line(id, network, Product.SUBURBAN_TRAIN, m.group(1));
            }
            if ("REX".equals(trainType))
                return new Line(id, network, Product.REGIONAL_TRAIN, "REX" + Strings.nullToEmpty(trainNum));
            return new Line(id, network, Product.SUBURBAN_TRAIN, ParserUtils.firstNotEmpty(symbol, name));
        } else if ("2".equals(mot)) {
            return new Line(id, network, Product.SUBWAY, name);
        } else if ("3".equals(mot) || "4".equals(mot)) {
            return new Line(id, network, Product.TRAM, name);
        } else if ("5".equals(mot) || "6".equals(mot) || "7".equals(mot) || "10".equals(mot)) {
            if ("Schienenersatzverkehr".equals(name))
                return new Line(id, network, Product.BUS, "SEV");
            else
                return new Line(id, network, Product.BUS, name);
        } else if ("8".equals(mot)) {
            return new Line(id, network, Product.CABLECAR, name);
        } else if ("9".equals(mot)) {
            return new Line(id, network, Product.FERRY, name);
        } else if ("11".equals(mot)) {
            return new Line(id, network, null, ParserUtils.firstNotEmpty(symbol, name));
        } else if ("13".equals(mot)) {
            if (("S-Bahn".equals(trainName) || (longName != null && longName.startsWith("S-Bahn")))
                    && symbol != null)
                return new Line(id, network, Product.SUBURBAN_TRAIN, symbol);
        } else if ("17".equals(mot)) {
            if (trainNum == null && trainName != null && trainName.startsWith("Schienenersatz"))
                return new Line(id, network, Product.BUS, "SEV");
        }

        throw new IllegalStateException("cannot normalize mot='" + mot + "' symbol='" + symbol + "' name='" + name
                + "' long='" + longName + "' trainType='" + trainType + "' trainNum='" + trainNum + "' trainName='"
                + trainName + "'");
    }

    @Override
    public QueryDeparturesResult queryDepartures(final String stationId, final @Nullable Date time,
            final int maxDepartures, final boolean equivs) throws IOException {
        checkNotNull(Strings.emptyToNull(stationId));

        return xsltDepartureMonitorRequest(stationId, time, maxDepartures, equivs);
    }

    protected void appendXsltDepartureMonitorRequestParameters(final HttpUrl.Builder url, final String stationId,
            final @Nullable Date time, final int maxDepartures, final boolean equivs) {
        appendCommonRequestParams(url, "XML");
        url.addEncodedQueryParameter("type_dm", "stop");
        url.addEncodedQueryParameter("name_dm",
                ParserUtils.urlEncode(normalizeStationId(stationId), requestUrlEncoding));
        if (time != null)
            appendItdDateTimeParameters(url, time);
        url.addEncodedQueryParameter("useRealtime", "1");
        url.addEncodedQueryParameter("mode", "direct");
        url.addEncodedQueryParameter("ptOptionsActive", "1");
        url.addEncodedQueryParameter("deleteAssignedStops_dm", equivs ? "0" : "1");
        if (useProxFootSearch)
            url.addEncodedQueryParameter("useProxFootSearch", equivs ? "1" : "0");
        url.addEncodedQueryParameter("mergeDep", "1"); // merge departures
        if (maxDepartures > 0)
            url.addEncodedQueryParameter("limit", Integer.toString(maxDepartures));
    }

    private final void appendItdDateTimeParameters(final HttpUrl.Builder url, final Date time) {
        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("itdDate", String.format(Locale.ENGLISH, "%04d%02d%02d", year, month, day));
        url.addEncodedQueryParameter("itdTime", String.format(Locale.ENGLISH, "%02d%02d", hour, minute));
    }

    private QueryDeparturesResult xsltDepartureMonitorRequest(final String stationId, final @Nullable Date time,
            final int maxDepartures, final boolean equivs) throws IOException {
        final HttpUrl.Builder url = departureMonitorEndpoint.newBuilder();
        appendXsltDepartureMonitorRequestParameters(url, stationId, time, maxDepartures, equivs);
        final AtomicReference<QueryDeparturesResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    final XmlPullParser pp = parserFactory.newPullParser();
                    pp.setInput(body.byteStream(), null); // Read encoding from XML declaration
                    final ResultHeader header = enterItdRequest(pp);

                    final QueryDeparturesResult r = new QueryDeparturesResult(header);

                    XmlPullUtil.enter(pp, "itdDepartureMonitorRequest");
                    XmlPullUtil.optSkipMultiple(pp, "itdMessage");

                    final String nameState = processItdOdv(pp, "dm", new ProcessItdOdvCallback() {
                        @Override
                        public void location(final String nameState, final Location location,
                                final int matchQuality) {
                            if (location.type == LocationType.STATION)
                                if (findStationDepartures(r.stationDepartures, location.id) == null)
                                    r.stationDepartures.add(new StationDepartures(location,
                                            new LinkedList<Departure>(), new LinkedList<LineDestination>()));
                        }
                    });

                    if ("notidentified".equals(nameState) || "list".equals(nameState)) {
                        result.set(new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION));
                        return;
                    }

                    XmlPullUtil.optSkip(pp, "itdDateTime");

                    XmlPullUtil.optSkip(pp, "itdDMDateTime");

                    XmlPullUtil.optSkip(pp, "itdDateRange");

                    XmlPullUtil.optSkip(pp, "itdTripOptions");

                    XmlPullUtil.optSkip(pp, "itdMessage");

                    if (XmlPullUtil.test(pp, "itdServingLines")) {
                        if (!pp.isEmptyElementTag()) {
                            XmlPullUtil.enter(pp, "itdServingLines");
                            while (XmlPullUtil.test(pp, "itdServingLine")) {
                                final String assignedStopId = XmlPullUtil.optAttr(pp, "assignedStopID", null);
                                final LineDestinationAndCancelled lineDestinationAndCancelled = processItdServingLine(
                                        pp);
                                final LineDestination lineDestination = new LineDestination(
                                        lineDestinationAndCancelled.line, lineDestinationAndCancelled.destination);

                                StationDepartures assignedStationDepartures;
                                if (assignedStopId == null)
                                    assignedStationDepartures = r.stationDepartures.get(0);
                                else
                                    assignedStationDepartures = findStationDepartures(r.stationDepartures,
                                            assignedStopId);

                                if (assignedStationDepartures == null)
                                    assignedStationDepartures = new StationDepartures(
                                            new Location(LocationType.STATION, assignedStopId),
                                            new LinkedList<Departure>(), new LinkedList<LineDestination>());

                                final List<LineDestination> assignedStationDeparturesLines = checkNotNull(
                                        assignedStationDepartures.lines);
                                if (!assignedStationDeparturesLines.contains(lineDestination))
                                    assignedStationDeparturesLines.add(lineDestination);
                            }
                            XmlPullUtil.skipExit(pp, "itdServingLines");
                        } else {
                            XmlPullUtil.next(pp);
                        }
                    } else {
                        result.set(new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION));
                        return;
                    }

                    XmlPullUtil.require(pp, "itdDepartureList");
                    if (XmlPullUtil.optEnter(pp, "itdDepartureList")) {
                        final Calendar plannedDepartureTime = new GregorianCalendar(timeZone);
                        final Calendar predictedDepartureTime = new GregorianCalendar(timeZone);

                        while (XmlPullUtil.test(pp, "itdDeparture")) {
                            final String assignedStopId = XmlPullUtil.attr(pp, "stopID");

                            StationDepartures assignedStationDepartures = findStationDepartures(r.stationDepartures,
                                    assignedStopId);
                            if (assignedStationDepartures == null) {
                                final Point coord = processCoordAttr(pp);

                                // final String name = normalizeLocationName(XmlPullUtil.attr(pp, "nameWO"));

                                assignedStationDepartures = new StationDepartures(
                                        new Location(LocationType.STATION, assignedStopId, coord),
                                        new LinkedList<Departure>(), new LinkedList<LineDestination>());
                            }

                            final Position position = parsePosition(XmlPullUtil.optAttr(pp, "platformName", null));

                            XmlPullUtil.enter(pp, "itdDeparture");

                            XmlPullUtil.require(pp, "itdDateTime");
                            plannedDepartureTime.clear();
                            processItdDateTime(pp, plannedDepartureTime);

                            predictedDepartureTime.clear();
                            if (XmlPullUtil.test(pp, "itdRTDateTime"))
                                processItdDateTime(pp, predictedDepartureTime);

                            XmlPullUtil.optSkip(pp, "itdFrequencyInfo");

                            XmlPullUtil.require(pp, "itdServingLine");
                            final boolean isRealtime = XmlPullUtil.attr(pp, "realtime").equals("1");
                            final LineDestinationAndCancelled lineDestinationAndCancelled = processItdServingLine(
                                    pp);

                            if (isRealtime && !predictedDepartureTime.isSet(Calendar.HOUR_OF_DAY))
                                predictedDepartureTime.setTimeInMillis(plannedDepartureTime.getTimeInMillis());

                            XmlPullUtil.skipExit(pp, "itdDeparture");

                            if (!lineDestinationAndCancelled.cancelled) {
                                final Departure departure = new Departure(plannedDepartureTime.getTime(),
                                        predictedDepartureTime.isSet(Calendar.HOUR_OF_DAY)
                                                ? predictedDepartureTime.getTime()
                                                : null,
                                        lineDestinationAndCancelled.line, position,
                                        lineDestinationAndCancelled.destination, null, null);
                                assignedStationDepartures.departures.add(departure);
                            }
                        }

                        XmlPullUtil.skipExit(pp, "itdDepartureList");
                    }

                    result.set(r);
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpReferer);
        else
            httpClient.getInputStream(callback, url.build(), httpReferer);

        return result.get();
    }

    protected QueryDeparturesResult queryDeparturesMobile(final String stationId, final @Nullable Date time,
            final int maxDepartures, final boolean equivs) throws IOException {
        final HttpUrl.Builder url = departureMonitorEndpoint.newBuilder();
        appendXsltDepartureMonitorRequestParameters(url, stationId, time, maxDepartures, equivs);
        final AtomicReference<QueryDeparturesResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    final XmlPullParser pp = parserFactory.newPullParser();
                    pp.setInput(body.byteStream(), null); // Read encoding from XML declaration
                    final ResultHeader header = enterEfa(pp);
                    final QueryDeparturesResult r = new QueryDeparturesResult(header);

                    XmlPullUtil.require(pp, "dps");
                    if (XmlPullUtil.optEnter(pp, "dps")) {
                        final Calendar plannedDepartureTime = new GregorianCalendar(timeZone);
                        final Calendar predictedDepartureTime = new GregorianCalendar(timeZone);

                        while (XmlPullUtil.optEnter(pp, "dp")) {
                            // misc
                            /* final String stationName = */normalizeLocationName(XmlPullUtil.valueTag(pp, "n"));
                            /* final boolean isRealtime = */XmlPullUtil.valueTag(pp, "realtime").equals("1");

                            XmlPullUtil.optSkip(pp, "dt");

                            // time
                            parseMobileSt(pp, plannedDepartureTime, predictedDepartureTime);

                            final LineDestination lineDestination = parseMobileM(pp, true);

                            XmlPullUtil.enter(pp, "r");
                            final String assignedId = XmlPullUtil.valueTag(pp, "id");
                            XmlPullUtil.valueTag(pp, "a");
                            final Position position = parsePosition(XmlPullUtil.optValueTag(pp, "pl", null));
                            XmlPullUtil.skipExit(pp, "r");

                            /* final Point positionCoordinate = */parseCoord(
                                    XmlPullUtil.optValueTag(pp, "c", null));

                            // TODO messages

                            StationDepartures stationDepartures = findStationDepartures(r.stationDepartures,
                                    assignedId);
                            if (stationDepartures == null) {
                                stationDepartures = new StationDepartures(
                                        new Location(LocationType.STATION, assignedId),
                                        new ArrayList<Departure>(maxDepartures), null);
                                r.stationDepartures.add(stationDepartures);
                            }

                            stationDepartures.departures.add(new Departure(plannedDepartureTime.getTime(),
                                    predictedDepartureTime.isSet(Calendar.HOUR_OF_DAY)
                                            ? predictedDepartureTime.getTime()
                                            : null,
                                    lineDestination.line, position, lineDestination.destination, null, null));

                            XmlPullUtil.skipExit(pp, "dp");
                        }

                        XmlPullUtil.skipExit(pp, "dps");

                        result.set(r);
                    } else {
                        result.set(new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION));
                    }
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpReferer);
        else
            httpClient.getInputStream(callback, url.build(), httpReferer);

        return result.get();
    }

    private static final Pattern P_MOBILE_M_SYMBOL = Pattern.compile("([^\\s]*)\\s+([^\\s]*)");

    private LineDestination parseMobileM(final XmlPullParser pp, final boolean tyOrCo)
            throws XmlPullParserException, IOException {
        XmlPullUtil.enter(pp, "m");

        final String n = XmlPullUtil.optValueTag(pp, "n", null);
        final String productNu = XmlPullUtil.valueTag(pp, "nu");
        final String ty = XmlPullUtil.valueTag(pp, "ty");

        final Line line;
        final Location destination;
        if ("100".equals(ty) || "99".equals(ty)) {
            destination = null;
            line = Line.FOOTWAY;
        } else if ("105".equals(ty)) {
            destination = null;
            line = Line.TRANSFER;
        } else if ("98".equals(ty)) {
            destination = null;
            line = Line.SECURE_CONNECTION;
        } else if ("97".equals(ty)) {
            destination = null;
            line = Line.DO_NOT_CHANGE;
        } else {
            final String co = XmlPullUtil.valueTag(pp, "co");
            final String productType = tyOrCo ? ty : co;
            XmlPullUtil.optValueTag(pp, "prid", null);
            final String destinationName = normalizeLocationName(XmlPullUtil.optValueTag(pp, "des", null));
            destination = destinationName != null ? new Location(LocationType.ANY, null, null, destinationName)
                    : null;
            XmlPullUtil.optValueTag(pp, "dy", null);
            final String de = XmlPullUtil.optValueTag(pp, "de", null);
            final String productName = n != null ? n : de;
            XmlPullUtil.optValueTag(pp, "tco", null);
            final String lineId = parseMobileDv(pp);

            final String symbol;
            if (productName != null && productNu == null)
                symbol = productName;
            else if (productName != null && productNu.endsWith(" " + productName))
                symbol = productNu.substring(0, productNu.length() - productName.length() - 1);
            else
                symbol = productNu;

            final String trainType;
            final String trainNum;
            final Matcher mSymbol = P_MOBILE_M_SYMBOL.matcher(symbol);
            if (mSymbol.matches()) {
                trainType = mSymbol.group(1);
                trainNum = mSymbol.group(2);
            } else {
                trainType = null;
                trainNum = null;
            }

            final String network = lineId.substring(0, lineId.indexOf(':'));
            final Line parsedLine = parseLine(lineId, network, productType, symbol, symbol, null, trainType,
                    trainNum, productName);
            line = new Line(parsedLine.id, parsedLine.network, parsedLine.product, parsedLine.label,
                    lineStyle(parsedLine.network, parsedLine.product, parsedLine.label));
        }

        XmlPullUtil.skipExit(pp, "m");

        return new LineDestination(line, destination);
    }

    private String parseMobileDv(final XmlPullParser pp) throws XmlPullParserException, IOException {
        XmlPullUtil.enter(pp, "dv");
        XmlPullUtil.optValueTag(pp, "branch", null);
        final String lineIdLi = XmlPullUtil.valueTag(pp, "li");
        final String lineIdSu = XmlPullUtil.valueTag(pp, "su");
        final String lineIdPr = XmlPullUtil.valueTag(pp, "pr");
        final String lineIdDct = XmlPullUtil.valueTag(pp, "dct");
        final String lineIdNe = XmlPullUtil.valueTag(pp, "ne");
        XmlPullUtil.skipExit(pp, "dv");

        return lineIdNe + ":" + lineIdLi + ":" + lineIdSu + ":" + lineIdDct + ":" + lineIdPr;
    }

    private void parseMobileSt(final XmlPullParser pp, final Calendar plannedDepartureTime,
            final Calendar predictedDepartureTime) throws XmlPullParserException, IOException {
        XmlPullUtil.enter(pp, "st");

        plannedDepartureTime.clear();
        ParserUtils.parseIsoDate(plannedDepartureTime, XmlPullUtil.valueTag(pp, "da"));
        ParserUtils.parseIsoTime(plannedDepartureTime, XmlPullUtil.valueTag(pp, "t"));

        predictedDepartureTime.clear();
        if (XmlPullUtil.test(pp, "rda")) {
            ParserUtils.parseIsoDate(predictedDepartureTime, XmlPullUtil.valueTag(pp, "rda"));
            ParserUtils.parseIsoTime(predictedDepartureTime, XmlPullUtil.valueTag(pp, "rt"));
        }

        XmlPullUtil.skipExit(pp, "st");
    }

    private StationDepartures findStationDepartures(final List<StationDepartures> stationDepartures,
            final String id) {
        for (final StationDepartures stationDeparture : stationDepartures)
            if (id.equals(stationDeparture.location.id))
                return stationDeparture;

        return null;
    }

    private Location processItdPointAttributes(final XmlPullParser pp) {
        final String id = XmlPullUtil.attr(pp, "stopID");

        String place = normalizeLocationName(XmlPullUtil.optAttr(pp, "locality", null));
        if (place == null)
            place = normalizeLocationName(XmlPullUtil.optAttr(pp, "place", null));

        String name = normalizeLocationName(XmlPullUtil.optAttr(pp, "nameWO", null));
        if (name == null)
            name = normalizeLocationName(XmlPullUtil.optAttr(pp, "name", null));

        final Point coord = processCoordAttr(pp);

        return new Location(LocationType.STATION, id, coord, place, name);
    }

    private boolean processItdDateTime(final XmlPullParser pp, final Calendar calendar)
            throws XmlPullParserException, IOException {
        XmlPullUtil.enter(pp);
        calendar.clear();
        final boolean success = processItdDate(pp, calendar);
        if (success)
            processItdTime(pp, calendar);
        XmlPullUtil.skipExit(pp);

        return success;
    }

    private boolean processItdDate(final XmlPullParser pp, final Calendar calendar)
            throws XmlPullParserException, IOException {
        XmlPullUtil.require(pp, "itdDate");
        final int year = XmlPullUtil.intAttr(pp, "year");
        final int month = XmlPullUtil.intAttr(pp, "month") - 1;
        final int day = XmlPullUtil.intAttr(pp, "day");
        final int weekday = XmlPullUtil.intAttr(pp, "weekday");
        XmlPullUtil.next(pp);

        if (weekday < 0)
            return false;
        if (year == 0)
            return false;
        if (year < 1900 || year > 2100)
            throw new InvalidDataException("invalid year: " + year);
        if (month < 0 || month > 11)
            throw new InvalidDataException("invalid month: " + month);
        if (day < 1 || day > 31)
            throw new InvalidDataException("invalid day: " + day);

        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, month);
        calendar.set(Calendar.DAY_OF_MONTH, day);
        return true;
    }

    private void processItdTime(final XmlPullParser pp, final Calendar calendar)
            throws XmlPullParserException, IOException {
        XmlPullUtil.require(pp, "itdTime");
        calendar.set(Calendar.HOUR_OF_DAY, XmlPullUtil.intAttr(pp, "hour"));
        calendar.set(Calendar.MINUTE, XmlPullUtil.intAttr(pp, "minute"));
        XmlPullUtil.next(pp);
    }

    private static class LineDestinationAndCancelled {
        public final Line line;
        public final Location destination;
        public final boolean cancelled;

        public LineDestinationAndCancelled(final Line line, final Location destination, final boolean cancelled) {
            this.line = line;
            this.destination = destination;
            this.cancelled = cancelled;
        }
    }

    private LineDestinationAndCancelled processItdServingLine(final XmlPullParser pp)
            throws XmlPullParserException, IOException {
        XmlPullUtil.require(pp, "itdServingLine");

        final String destinationName = normalizeLocationName(XmlPullUtil.optAttr(pp, "direction", null));
        final String destinationIdStr = XmlPullUtil.optAttr(pp, "destID", null);
        final String destinationId = !"-1".equals(destinationIdStr) ? destinationIdStr : null;
        final Location destination;
        if (destinationId != null)
            destination = new Location(LocationType.STATION, destinationId, null, destinationName);
        else if (destinationId == null && destinationName != null)
            destination = new Location(LocationType.ANY, null, null, destinationName);
        else
            destination = null;

        final String slMotType = XmlPullUtil.attr(pp, "motType");
        final String slSymbol = XmlPullUtil.optAttr(pp, "symbol", null);
        final String slNumber = XmlPullUtil.optAttr(pp, "number", null);
        final String slStateless = XmlPullUtil.optAttr(pp, "stateless", null);
        final String slTrainType = XmlPullUtil.optAttr(pp, "trainType", null);
        final String slTrainName = XmlPullUtil.optAttr(pp, "trainName", null);
        final String slTrainNum = XmlPullUtil.optAttr(pp, "trainNum", null);

        XmlPullUtil.enter(pp, "itdServingLine");
        String itdTrainName = null;
        String itdTrainType = null;
        String itdMessage = null;
        String itdDelay = null;
        if (XmlPullUtil.test(pp, "itdTrain")) {
            itdTrainName = XmlPullUtil.optAttr(pp, "name", null);
            itdTrainType = XmlPullUtil.attr(pp, "type");
            itdDelay = XmlPullUtil.optAttr(pp, "delay", null);
            XmlPullUtil.requireSkip(pp, "itdTrain");
        }
        if (XmlPullUtil.test(pp, "itdNoTrain")) {
            itdTrainName = XmlPullUtil.optAttr(pp, "name", null);
            itdTrainType = XmlPullUtil.optAttr(pp, "type", null);
            itdDelay = XmlPullUtil.optAttr(pp, "delay", null);
            if (!pp.isEmptyElementTag()) {
                final String text = XmlPullUtil.valueTag(pp, "itdNoTrain");
                if (itdTrainName != null && itdTrainName.toLowerCase().contains("ruf"))
                    itdMessage = text;
                else if (text != null && text.toLowerCase().contains("ruf"))
                    itdMessage = text;
            } else {
                XmlPullUtil.next(pp);
            }
        }

        XmlPullUtil.require(pp, "motDivaParams");
        final String divaNetwork = XmlPullUtil.optAttr(pp, "network", null);

        XmlPullUtil.skipExit(pp, "itdServingLine");

        final String trainType = ParserUtils.firstNotEmpty(slTrainType, itdTrainType);
        final String trainName = ParserUtils.firstNotEmpty(slTrainName, itdTrainName);
        final Line slLine = parseLine(slStateless, divaNetwork, slMotType, slSymbol, slNumber, slNumber, trainType,
                slTrainNum, trainName);

        final Line line = new Line(slLine.id, slLine.network, slLine.product, slLine.label,
                lineStyle(slLine.network, slLine.product, slLine.label), itdMessage);
        final boolean cancelled = "-9999".equals(itdDelay);
        return new LineDestinationAndCancelled(line, destination, cancelled);
    }

    private static final Pattern P_STATION_NAME_WHITESPACE = Pattern.compile("\\s+");

    protected String normalizeLocationName(final String name) {
        if (Strings.isNullOrEmpty(name))
            return null;

        return P_STATION_NAME_WHITESPACE.matcher(name).replaceAll(" ");
    }

    protected static double latLonToDouble(final int value) {
        return (double) value / 1000000;
    }

    protected void appendXsltTripRequestParameters(final HttpUrl.Builder url, final Location from,
            final @Nullable Location via, final Location to, final Date time, final boolean dep,
            final @Nullable Collection<Product> products, final @Nullable Optimize optimize,
            final @Nullable WalkSpeed walkSpeed, final @Nullable Accessibility accessibility,
            final @Nullable Set<Option> options) {
        appendCommonRequestParams(url, "XML");

        url.addEncodedQueryParameter("sessionID", "0");
        url.addEncodedQueryParameter("requestID", "0");

        appendCommonXsltTripRequest2Params(url);

        appendLocationParams(url, from, "origin");
        appendLocationParams(url, to, "destination");
        if (via != null)
            appendLocationParams(url, via, "via");

        appendItdDateTimeParameters(url, time);

        url.addEncodedQueryParameter("itdTripDateTimeDepArr", dep ? "dep" : "arr");

        url.addEncodedQueryParameter("calcNumberOfTrips", Integer.toString(numTripsRequested));

        url.addEncodedQueryParameter("ptOptionsActive", "1"); // enable public transport options
        url.addEncodedQueryParameter("itOptionsActive", "1"); // enable individual transport options

        if (optimize == Optimize.LEAST_DURATION)
            url.addEncodedQueryParameter("routeType", "LEASTTIME");
        else if (optimize == Optimize.LEAST_CHANGES)
            url.addEncodedQueryParameter("routeType", "LEASTINTERCHANGE");
        else if (optimize == Optimize.LEAST_WALKING)
            url.addEncodedQueryParameter("routeType", "LEASTWALKING");
        else if (optimize != null)
            log.info("Cannot handle " + optimize + ", ignoring.");

        url.addEncodedQueryParameter("changeSpeed", WALKSPEED_MAP.get(walkSpeed));

        if (accessibility == Accessibility.BARRIER_FREE)
            url.addEncodedQueryParameter("imparedOptionsActive", "1").addEncodedQueryParameter("wheelchair", "on")
                    .addEncodedQueryParameter("noSolidStairs", "on");
        else if (accessibility == Accessibility.LIMITED)
            url.addEncodedQueryParameter("imparedOptionsActive", "1").addEncodedQueryParameter("wheelchair", "on")
                    .addEncodedQueryParameter("lowPlatformVhcl", "on")
                    .addEncodedQueryParameter("noSolidStairs", "on");

        if (products != null) {
            url.addEncodedQueryParameter("includedMeans", "checkbox");

            boolean hasI = false;
            for (final Product p : products) {
                if (p == Product.HIGH_SPEED_TRAIN || p == Product.REGIONAL_TRAIN) {
                    url.addEncodedQueryParameter("inclMOT_0", "on");
                    if (p == Product.HIGH_SPEED_TRAIN)
                        hasI = true;
                }

                if (p == Product.SUBURBAN_TRAIN)
                    url.addEncodedQueryParameter("inclMOT_1", "on");

                if (p == Product.SUBWAY)
                    url.addEncodedQueryParameter("inclMOT_2", "on");

                if (p == Product.TRAM)
                    url.addEncodedQueryParameter("inclMOT_3", "on").addEncodedQueryParameter("inclMOT_4", "on");

                if (p == Product.BUS)
                    url.addEncodedQueryParameter("inclMOT_5", "on").addEncodedQueryParameter("inclMOT_6", "on")
                            .addEncodedQueryParameter("inclMOT_7", "on");

                if (p == Product.ON_DEMAND)
                    url.addEncodedQueryParameter("inclMOT_10", "on");

                if (p == Product.FERRY)
                    url.addEncodedQueryParameter("inclMOT_9", "on");

                if (p == Product.CABLECAR)
                    url.addEncodedQueryParameter("inclMOT_8", "on");
            }

            // workaround for highspeed trains: fails when you want highspeed, but not regional
            if (useLineRestriction && !hasI)
                url.addEncodedQueryParameter("lineRestriction", "403"); // means: all but ice
        }

        if (useProxFootSearch)
            url.addEncodedQueryParameter("useProxFootSearch", "1"); // walk if it makes journeys quicker
        url.addEncodedQueryParameter("trITMOTvalue100", "10"); // maximum time to walk to first or from last
                                                               // stop

        if (options != null && options.contains(Option.BIKE))
            url.addEncodedQueryParameter("bikeTakeAlong", "1");

        url.addEncodedQueryParameter("locationServerActive", "1");
        url.addEncodedQueryParameter("useRealtime", "1");
        url.addEncodedQueryParameter("nextDepsPerLeg", "1"); // next departure in case previous was missed
    }

    private HttpUrl commandLink(final String sessionId, final String requestId) {
        final HttpUrl.Builder url = tripEndpoint.newBuilder();
        url.addEncodedQueryParameter("sessionID", sessionId);
        url.addEncodedQueryParameter("requestID", requestId);
        url.addEncodedQueryParameter("calcNumberOfTrips", Integer.toString(numTripsRequested));
        appendCommonXsltTripRequest2Params(url);
        return url.build();
    }

    private final void appendCommonXsltTripRequest2Params(final HttpUrl.Builder url) {
        if (useStringCoordListOutputFormat)
            url.addEncodedQueryParameter("coordListOutputFormat", "STRING");
    }

    @Override
    public QueryTripsResult queryTrips(final Location from, final @Nullable Location via, final Location to,
            final Date date, final boolean dep, final @Nullable Set<Product> products,
            final @Nullable Optimize optimize, final @Nullable WalkSpeed walkSpeed,
            final @Nullable Accessibility accessibility, final @Nullable Set<Option> options) throws IOException {
        final HttpUrl.Builder url = tripEndpoint.newBuilder();
        appendXsltTripRequestParameters(url, from, via, to, date, dep, products, optimize, walkSpeed, accessibility,
                options);
        final AtomicReference<QueryTripsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    result.set(queryTrips(url.build(), body.byteStream()));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                } catch (final RuntimeException x) {
                    throw new RuntimeException("uncategorized problem while processing " + url, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpRefererTrip);
        else
            httpClient.getInputStream(callback, url.build(), httpRefererTrip);

        return result.get();
    }

    protected QueryTripsResult queryTripsMobile(final Location from, final @Nullable Location via,
            final Location to, final Date date, final boolean dep, final @Nullable Collection<Product> products,
            final @Nullable Optimize optimize, final @Nullable WalkSpeed walkSpeed,
            final @Nullable Accessibility accessibility, final @Nullable Set<Option> options) throws IOException {
        final HttpUrl.Builder url = tripEndpoint.newBuilder();
        appendXsltTripRequestParameters(url, from, via, to, date, dep, products, optimize, walkSpeed, accessibility,
                options);
        final AtomicReference<QueryTripsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    result.set(queryTripsMobile(url.build(), from, via, to, body.byteStream()));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                } catch (final RuntimeException x) {
                    throw new RuntimeException("uncategorized problem while processing " + url, x);
                }
            }
        };

        if (httpPost)
            httpClient.getInputStream(callback, url.build(), url.build().encodedQuery(),
                    "application/x-www-form-urlencoded", httpRefererTrip);
        else
            httpClient.getInputStream(callback, url.build(), httpRefererTrip);

        return result.get();
    }

    @Override
    public QueryTripsResult queryMoreTrips(final QueryTripsContext contextObj, final boolean later)
            throws IOException {
        final Context context = (Context) contextObj;
        final HttpUrl commandUrl = HttpUrl.parse(context.context);
        final HttpUrl.Builder url = commandUrl.newBuilder();
        url.addEncodedQueryParameter("command", later ? "tripNext" : "tripPrev");
        final AtomicReference<QueryTripsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    result.set(queryTrips(url.build(), body.byteStream()));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                } catch (final RuntimeException x) {
                    throw new RuntimeException("uncategorized problem while processing " + url, x);
                }
            }
        };

        httpClient.getInputStream(callback, url.build(), httpRefererTrip);

        return result.get();
    }

    protected QueryTripsResult queryMoreTripsMobile(final QueryTripsContext contextObj, final boolean later)
            throws IOException {
        final Context context = (Context) contextObj;
        final HttpUrl commandUrl = HttpUrl.parse(context.context);
        final HttpUrl.Builder url = commandUrl.newBuilder();
        url.addEncodedQueryParameter("command", later ? "tripNext" : "tripPrev");
        final AtomicReference<QueryTripsResult> result = new AtomicReference<>();

        final HttpClient.Callback callback = new HttpClient.Callback() {
            @Override
            public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException {
                try {
                    result.set(queryTripsMobile(url.build(), null, null, null, body.byteStream()));
                } catch (final XmlPullParserException x) {
                    throw new ParserException("cannot parse xml: " + bodyPeek, x);
                } catch (final RuntimeException x) {
                    throw new RuntimeException("uncategorized problem while processing " + url, x);
                }
            }
        };

        httpClient.getInputStream(callback, url.build(), httpRefererTrip);

        return result.get();
    }

    private QueryTripsResult queryTrips(final HttpUrl url, final InputStream is)
            throws XmlPullParserException, IOException {
        final XmlPullParser pp = parserFactory.newPullParser();
        pp.setInput(is, null); // Read encoding from XML declaration
        final ResultHeader header = enterItdRequest(pp);
        final Object context = header.context;

        XmlPullUtil.require(pp, "itdTripRequest");
        final String requestId = XmlPullUtil.attr(pp, "requestID");
        XmlPullUtil.enter(pp, "itdTripRequest");

        while (XmlPullUtil.test(pp, "itdMessage")) {
            final int code = XmlPullUtil.intAttr(pp, "code");
            if (code == -4000) // no trips
                return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS);
            XmlPullUtil.next(pp);
        }
        XmlPullUtil.optSkip(pp, "itdPrintConfiguration");
        XmlPullUtil.optSkip(pp, "itdAddress");

        List<Location> ambiguousFrom = null, ambiguousTo = null, ambiguousVia = null;
        Location from = null, via = null, to = null;

        while (XmlPullUtil.test(pp, "itdOdv")) {
            final String usage = XmlPullUtil.attr(pp, "usage");

            final List<Location> locations = new ArrayList<>();
            final String nameState = processItdOdv(pp, usage, new ProcessItdOdvCallback() {
                @Override
                public void location(final String nameState, final Location location, final int matchQuality) {
                    locations.add(location);
                }
            });

            if ("list".equals(nameState)) {
                if ("origin".equals(usage))
                    ambiguousFrom = locations;
                else if ("via".equals(usage))
                    ambiguousVia = locations;
                else if ("destination".equals(usage))
                    ambiguousTo = locations;
                else
                    throw new IllegalStateException("unknown usage: " + usage);
            } else if ("identified".equals(nameState)) {
                if ("origin".equals(usage))
                    from = locations.get(0);
                else if ("via".equals(usage))
                    via = locations.get(0);
                else if ("destination".equals(usage))
                    to = locations.get(0);
                else
                    throw new IllegalStateException("unknown usage: " + usage);
            } else if ("notidentified".equals(nameState)) {
                if ("origin".equals(usage))
                    return new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_FROM);
                else if ("via".equals(usage))
                    return new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_VIA);
                else if ("destination".equals(usage))
                    return new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_TO);
                else
                    throw new IllegalStateException("unknown usage: " + usage);
            }
        }

        if (ambiguousFrom != null || ambiguousTo != null || ambiguousVia != null)
            return new QueryTripsResult(header, ambiguousFrom, ambiguousVia, ambiguousTo);

        XmlPullUtil.optSkip(pp, "itdAddOdvSeq");
        XmlPullUtil.enter(pp, "itdTripDateTime");
        XmlPullUtil.enter(pp, "itdDateTime");
        XmlPullUtil.require(pp, "itdDate");
        if (XmlPullUtil.optEnter(pp, "itdDate")) {
            if (XmlPullUtil.test(pp, "itdMessage")) {
                final String message = XmlPullUtil.nextText(pp, null, "itdMessage");

                if ("invalid date".equals(message))
                    return new QueryTripsResult(header, QueryTripsResult.Status.INVALID_DATE);
                else
                    throw new IllegalStateException("unknown message: " + message);
            }
            XmlPullUtil.skipExit(pp, "itdDate");
        }
        XmlPullUtil.skipExit(pp, "itdDateTime");
        XmlPullUtil.skipExit(pp, "itdTripDateTime");

        XmlPullUtil.requireSkip(pp, "itdTripOptions");
        XmlPullUtil.optSkipMultiple(pp, "omcTaxi");

        final List<Trip> trips = new ArrayList<>();

        XmlPullUtil.require(pp, "itdItinerary");
        if (XmlPullUtil.optEnter(pp, "itdItinerary")) {
            XmlPullUtil.optSkip(pp, "itdLegTTs");

            if (XmlPullUtil.optEnter(pp, "itdRouteList")) {
                final Calendar calendar = new GregorianCalendar(timeZone);

                while (XmlPullUtil.test(pp, "itdRoute")) {
                    final String id;
                    if (useRouteIndexAsTripId) {
                        final String routeIndex = XmlPullUtil.optAttr(pp, "routeIndex", null);
                        final String routeTripIndex = XmlPullUtil.optAttr(pp, "routeTripIndex", null);
                        if (routeIndex != null && routeTripIndex != null)
                            id = routeIndex + "-" + routeTripIndex;
                        else
                            id = null;
                    } else {
                        id = null;
                    }
                    final int numChanges = XmlPullUtil.intAttr(pp, "changes");
                    XmlPullUtil.enter(pp, "itdRoute");

                    XmlPullUtil.optSkipMultiple(pp, "itdDateTime");
                    XmlPullUtil.optSkip(pp, "itdMapItemList");

                    XmlPullUtil.enter(pp, "itdPartialRouteList");
                    final List<Trip.Leg> legs = new LinkedList<>();
                    Location firstDepartureLocation = null;
                    Location lastArrivalLocation = null;

                    boolean cancelled = false;

                    while (XmlPullUtil.test(pp, "itdPartialRoute")) {
                        final String itdPartialRouteType = XmlPullUtil.attr(pp, "type");
                        final int distance = XmlPullUtil.optIntAttr(pp, "distance", 0);
                        XmlPullUtil.enter(pp, "itdPartialRoute");

                        XmlPullUtil.test(pp, "itdPoint");
                        if (!"departure".equals(XmlPullUtil.attr(pp, "usage")))
                            throw new IllegalStateException();
                        final Location departureLocation = processItdPointAttributes(pp);
                        if (firstDepartureLocation == null)
                            firstDepartureLocation = departureLocation;
                        final Position departurePosition = parsePosition(
                                XmlPullUtil.optAttr(pp, "platformName", null));
                        XmlPullUtil.enter(pp, "itdPoint");
                        XmlPullUtil.optSkip(pp, "itdMapItemList");
                        XmlPullUtil.require(pp, "itdDateTime");
                        processItdDateTime(pp, calendar);
                        final Date departureTime = calendar.getTime();
                        final Date departureTargetTime;
                        if (XmlPullUtil.test(pp, "itdDateTimeTarget")) {
                            processItdDateTime(pp, calendar);
                            departureTargetTime = calendar.getTime();
                        } else {
                            departureTargetTime = null;
                        }
                        XmlPullUtil.skipExit(pp, "itdPoint");

                        XmlPullUtil.test(pp, "itdPoint");
                        if (!"arrival".equals(XmlPullUtil.attr(pp, "usage")))
                            throw new IllegalStateException();
                        final Location arrivalLocation = processItdPointAttributes(pp);
                        lastArrivalLocation = arrivalLocation;
                        final Position arrivalPosition = parsePosition(
                                XmlPullUtil.optAttr(pp, "platformName", null));
                        XmlPullUtil.enter(pp, "itdPoint");
                        XmlPullUtil.optSkip(pp, "itdMapItemList");
                        XmlPullUtil.require(pp, "itdDateTime");
                        processItdDateTime(pp, calendar);
                        final Date arrivalTime = calendar.getTime();
                        final Date arrivalTargetTime;
                        if (XmlPullUtil.test(pp, "itdDateTimeTarget")) {
                            processItdDateTime(pp, calendar);
                            arrivalTargetTime = calendar.getTime();
                        } else {
                            arrivalTargetTime = null;
                        }
                        XmlPullUtil.skipExit(pp, "itdPoint");

                        XmlPullUtil.test(pp, "itdMeansOfTransport");

                        final String itdMeansOfTransportProductName = XmlPullUtil.optAttr(pp, "productName", null);
                        final int itdMeansOfTransportType = XmlPullUtil.intAttr(pp, "type");

                        if (itdMeansOfTransportType <= 16) {
                            cancelled |= processPublicLeg(pp, legs, calendar, departureTime, departureTargetTime,
                                    departureLocation, departurePosition, arrivalTime, arrivalTargetTime,
                                    arrivalLocation, arrivalPosition);
                        } else if (itdMeansOfTransportType == 97
                                && "nicht umsteigen".equals(itdMeansOfTransportProductName)) {
                            // ignore
                            XmlPullUtil.enter(pp, "itdMeansOfTransport");
                            XmlPullUtil.skipExit(pp, "itdMeansOfTransport");
                        } else if (itdMeansOfTransportType == 98
                                && "gesicherter Anschluss".equals(itdMeansOfTransportProductName)) {
                            // ignore
                            XmlPullUtil.enter(pp, "itdMeansOfTransport");
                            XmlPullUtil.skipExit(pp, "itdMeansOfTransport");
                        } else if (itdMeansOfTransportType == 99
                                && "Fussweg".equals(itdMeansOfTransportProductName)) {
                            processIndividualLeg(pp, legs, Trip.Individual.Type.WALK, distance, departureTime,
                                    departureLocation, arrivalTime, arrivalLocation);
                        } else if (itdMeansOfTransportType == 100 && (itdMeansOfTransportProductName == null
                                || "Fussweg".equals(itdMeansOfTransportProductName))) {
                            processIndividualLeg(pp, legs, Trip.Individual.Type.WALK, distance, departureTime,
                                    departureLocation, arrivalTime, arrivalLocation);
                        } else if (itdMeansOfTransportType == 105
                                && "Taxi".equals(itdMeansOfTransportProductName)) {
                            processIndividualLeg(pp, legs, Trip.Individual.Type.CAR, distance, departureTime,
                                    departureLocation, arrivalTime, arrivalLocation);
                        } else {
                            throw new IllegalStateException(
                                    MoreObjects.toStringHelper("").add("itdPartialRoute.type", itdPartialRouteType)
                                            .add("itdMeansOfTransport.type", itdMeansOfTransportType)
                                            .add("itdMeansOfTransport.productName", itdMeansOfTransportProductName)
                                            .toString());
                        }

                        XmlPullUtil.skipExit(pp, "itdPartialRoute");
                    }

                    XmlPullUtil.skipExit(pp, "itdPartialRouteList");

                    final List<Fare> fares = new ArrayList<>(2);
                    if (XmlPullUtil.optEnter(pp, "itdFare")) {
                        if (XmlPullUtil.test(pp, "itdSingleTicket")) {
                            final String net = XmlPullUtil.optAttr(pp, "net", null);
                            if (net != null) {
                                final Currency currency = parseCurrency(XmlPullUtil.attr(pp, "currency"));
                                final String fareAdult = XmlPullUtil.optAttr(pp, "fareAdult", null);
                                final String fareChild = XmlPullUtil.optAttr(pp, "fareChild", null);
                                final String unitName = XmlPullUtil.optAttr(pp, "unitName", null);
                                final String unitsAdult = XmlPullUtil.optAttr(pp, "unitsAdult", null);
                                final String unitsChild = XmlPullUtil.optAttr(pp, "unitsChild", null);
                                final String levelAdult = XmlPullUtil.optAttr(pp, "levelAdult", null);
                                final String levelChild = XmlPullUtil.optAttr(pp, "levelChild", null);
                                if (fareAdult != null)
                                    fares.add(new Fare(net.toUpperCase(), Type.ADULT, currency,
                                            Float.parseFloat(fareAdult) * fareCorrectionFactor,
                                            levelAdult != null ? null : unitName,
                                            levelAdult != null ? levelAdult : unitsAdult));
                                if (fareChild != null)
                                    fares.add(new Fare(net.toUpperCase(), Type.CHILD, currency,
                                            Float.parseFloat(fareChild) * fareCorrectionFactor,
                                            levelChild != null ? null : unitName,
                                            levelChild != null ? levelChild : unitsChild));

                                if (XmlPullUtil.optEnter(pp, "itdSingleTicket")) {
                                    if (XmlPullUtil.optEnter(pp, "itdGenericTicketList")) {
                                        while (XmlPullUtil.test(pp, "itdGenericTicketGroup")) {
                                            final Fare fare = processItdGenericTicketGroup(pp, net.toUpperCase(),
                                                    currency);
                                            if (fare != null)
                                                fares.add(fare);
                                        }
                                        XmlPullUtil.skipExit(pp, "itdGenericTicketList");
                                    }
                                    XmlPullUtil.skipExit(pp, "itdSingleTicket");
                                }
                            }
                        }
                        XmlPullUtil.skipExit(pp, "itdFare");
                    }

                    XmlPullUtil.skipExit(pp, "itdRoute");

                    final Trip trip = new Trip(id, firstDepartureLocation, lastArrivalLocation, legs,
                            fares.isEmpty() ? null : fares, null, numChanges);

                    if (!cancelled)
                        trips.add(trip);
                }

                XmlPullUtil.skipExit(pp, "itdRouteList");
            }
            XmlPullUtil.skipExit(pp, "itdItinerary");
        }

        return new QueryTripsResult(header, url.toString(), from, via, to,
                new Context(commandLink((String) context, requestId).toString()), trips);
    }

    private void processIndividualLeg(final XmlPullParser pp, final List<Leg> legs,
            final Trip.Individual.Type individualType, final int distance, final Date departureTime,
            final Location departureLocation, final Date arrivalTime, final Location arrivalLocation)
            throws XmlPullParserException, IOException {
        XmlPullUtil.enter(pp, "itdMeansOfTransport");
        XmlPullUtil.skipExit(pp, "itdMeansOfTransport");

        XmlPullUtil.optSkip(pp, "itdStopSeq");
        XmlPullUtil.optSkip(pp, "itdFootPathInfo");

        List<Point> path = null;
        if (XmlPullUtil.test(pp, "itdPathCoordinates"))
            path = processItdPathCoordinates(pp);

        final Trip.Leg lastLeg = legs.size() > 0 ? legs.get(legs.size() - 1) : null;
        if (lastLeg != null && lastLeg instanceof Trip.Individual
                && ((Trip.Individual) lastLeg).type == individualType) {
            final Trip.Individual lastIndividual = (Trip.Individual) legs.remove(legs.size() - 1);
            if (path != null && lastIndividual.path != null)
                path.addAll(0, lastIndividual.path);
            legs.add(new Trip.Individual(individualType, lastIndividual.departure, lastIndividual.departureTime,
                    arrivalLocation, arrivalTime, path, distance));
        } else {
            legs.add(new Trip.Individual(individualType, departureLocation, departureTime, arrivalLocation,
                    arrivalTime, path, distance));
        }
    }

    private boolean processPublicLeg(final XmlPullParser pp, final List<Leg> legs, final Calendar calendar,
            final Date departureTime, final Date departureTargetTime, final Location departureLocation,
            final Position departurePosition, final Date arrivalTime, final Date arrivalTargetTime,
            final Location arrivalLocation, final Position arrivalPosition)
            throws XmlPullParserException, IOException {
        final String destinationName = normalizeLocationName(XmlPullUtil.optAttr(pp, "destination", null));
        final String destinationId = XmlPullUtil.optAttr(pp, "destID", null);
        final Location destination;
        if (destinationId != null)
            destination = new Location(LocationType.STATION, destinationId, null, destinationName);
        else if (destinationId == null && destinationName != null)
            destination = new Location(LocationType.ANY, null, null, destinationName);
        else
            destination = null;

        final String motSymbol = XmlPullUtil.optAttr(pp, "symbol", null);
        final String motType = XmlPullUtil.optAttr(pp, "motType", null);
        final String motShortName = XmlPullUtil.optAttr(pp, "shortname", null);
        final String motName = XmlPullUtil.attr(pp, "name");
        final String motTrainName = XmlPullUtil.optAttr(pp, "trainName", null);
        final String motTrainType = XmlPullUtil.optAttr(pp, "trainType", null);

        XmlPullUtil.enter(pp, "itdMeansOfTransport");
        XmlPullUtil.require(pp, "motDivaParams");
        final String divaNetwork = XmlPullUtil.attr(pp, "network");
        final String divaLine = XmlPullUtil.attr(pp, "line");
        final String divaSupplement = XmlPullUtil.optAttr(pp, "supplement", "");
        final String divaDirection = XmlPullUtil.attr(pp, "direction");
        final String divaProject = XmlPullUtil.optAttr(pp, "project", "");
        final String lineId = divaNetwork + ':' + divaLine + ':' + divaSupplement + ':' + divaDirection + ':'
                + divaProject;
        XmlPullUtil.skipExit(pp, "itdMeansOfTransport");

        final Line line;
        if ("AST".equals(motSymbol))
            line = new Line(null, divaNetwork, Product.BUS, "AST");
        else
            line = parseLine(lineId, divaNetwork, motType, motSymbol, motShortName, motName, motTrainType,
                    motShortName, motTrainName);

        final Integer departureDelay;
        final Integer arrivalDelay;
        final boolean cancelled;
        if (XmlPullUtil.test(pp, "itdRBLControlled")) {
            departureDelay = XmlPullUtil.optIntAttr(pp, "delayMinutes", 0);
            arrivalDelay = XmlPullUtil.optIntAttr(pp, "delayMinutesArr", 0);
            cancelled = departureDelay == -9999 || arrivalDelay == -9999;

            XmlPullUtil.next(pp);
        } else {
            departureDelay = null;
            arrivalDelay = null;
            cancelled = false;
        }

        boolean lowFloorVehicle = false;
        String message = null;
        if (XmlPullUtil.optEnter(pp, "itdInfoTextList")) {
            while (XmlPullUtil.test(pp, "infoTextListElem")) {
                final String text = XmlPullUtil.valueTag(pp, "infoTextListElem");
                if (text != null) {
                    final String lcText = text.toLowerCase();
                    if (lcText.startsWith("niederflurwagen")) // KVV
                        lowFloorVehicle = true;
                    else if (lcText.contains("ruf") || lcText.contains("anmeld")) // Bedarfsverkehr
                        message = text;
                }
            }
            XmlPullUtil.skipExit(pp, "itdInfoTextList");
        }

        XmlPullUtil.optSkip(pp, "itdFootPathInfo");

        while (XmlPullUtil.optEnter(pp, "infoLink")) {
            XmlPullUtil.optSkip(pp, "paramList");
            final String infoLinkText = XmlPullUtil.valueTag(pp, "infoLinkText");
            if (message == null)
                message = infoLinkText;
            XmlPullUtil.skipExit(pp, "infoLink");
        }

        List<Stop> intermediateStops = null;
        if (XmlPullUtil.optEnter(pp, "itdStopSeq")) {
            intermediateStops = new LinkedList<>();
            while (XmlPullUtil.test(pp, "itdPoint")) {
                final Location stopLocation = processItdPointAttributes(pp);

                final Position stopPosition = parsePosition(XmlPullUtil.optAttr(pp, "platformName", null));

                XmlPullUtil.enter(pp, "itdPoint");
                XmlPullUtil.optSkip(pp, "genAttrList");
                XmlPullUtil.require(pp, "itdDateTime");

                final Date plannedStopArrivalTime;
                final Date predictedStopArrivalTime;
                if (processItdDateTime(pp, calendar)) {
                    plannedStopArrivalTime = calendar.getTime();
                    if (arrivalDelay != null) {
                        calendar.add(Calendar.MINUTE, arrivalDelay);
                        predictedStopArrivalTime = calendar.getTime();
                    } else {
                        predictedStopArrivalTime = null;
                    }
                } else {
                    plannedStopArrivalTime = null;
                    predictedStopArrivalTime = null;
                }

                final Date plannedStopDepartureTime;
                final Date predictedStopDepartureTime;
                if (XmlPullUtil.test(pp, "itdDateTime") && processItdDateTime(pp, calendar)) {
                    plannedStopDepartureTime = calendar.getTime();
                    if (departureDelay != null) {
                        calendar.add(Calendar.MINUTE, departureDelay);
                        predictedStopDepartureTime = calendar.getTime();
                    } else {
                        predictedStopDepartureTime = null;
                    }
                } else {
                    plannedStopDepartureTime = null;
                    predictedStopDepartureTime = null;
                }

                final Stop stop = new Stop(stopLocation, plannedStopArrivalTime, predictedStopArrivalTime,
                        stopPosition, null, plannedStopDepartureTime, predictedStopDepartureTime, stopPosition,
                        null);

                intermediateStops.add(stop);

                XmlPullUtil.skipExit(pp, "itdPoint");
            }
            XmlPullUtil.skipExit(pp, "itdStopSeq");

            // remove first and last, because they are not intermediate
            final int size = intermediateStops.size();
            if (size >= 2) {
                if (!intermediateStops.get(size - 1).location.equals(arrivalLocation))
                    throw new IllegalStateException();
                intermediateStops.remove(size - 1);

                if (!intermediateStops.get(0).location.equals(departureLocation))
                    throw new IllegalStateException();
                intermediateStops.remove(0);
            }
        }

        List<Point> path = null;
        if (XmlPullUtil.test(pp, "itdPathCoordinates"))
            path = processItdPathCoordinates(pp);

        boolean wheelChairAccess = false;
        if (XmlPullUtil.optEnter(pp, "genAttrList")) {
            while (XmlPullUtil.optEnter(pp, "genAttrElem")) {
                final String name = XmlPullUtil.valueTag(pp, "name");
                final String value = XmlPullUtil.valueTag(pp, "value");
                XmlPullUtil.skipExit(pp, "genAttrElem");

                // System.out.println("genAttrElem: name='" + name + "' value='" + value + "'");

                if ("PlanWheelChairAccess".equals(name) && "1".equals(value))
                    wheelChairAccess = true;
            }
            XmlPullUtil.skipExit(pp, "genAttrList");
        }

        if (XmlPullUtil.optEnter(pp, "nextDeps")) {
            while (XmlPullUtil.test(pp, "itdDateTime")) {
                processItdDateTime(pp, calendar);
                /* final Date nextDepartureTime = */calendar.getTime();
            }
            XmlPullUtil.skipExit(pp, "nextDeps");
        }

        final Set<Line.Attr> lineAttrs = new HashSet<>();
        if (wheelChairAccess || lowFloorVehicle)
            lineAttrs.add(Line.Attr.WHEEL_CHAIR_ACCESS);
        final Line styledLine = new Line(line.id, line.network, line.product, line.label,
                lineStyle(line.network, line.product, line.label), lineAttrs);

        final Stop departure = new Stop(departureLocation, true,
                departureTargetTime != null ? departureTargetTime : departureTime,
                departureTime != null ? departureTime : null, departurePosition, null);
        final Stop arrival = new Stop(arrivalLocation, false,
                arrivalTargetTime != null ? arrivalTargetTime : arrivalTime,
                arrivalTime != null ? arrivalTime : null, arrivalPosition, null);

        legs.add(new Trip.Public(styledLine, destination, departure, arrival, intermediateStops, path, message));

        return cancelled;
    }

    private QueryTripsResult queryTripsMobile(final HttpUrl url, final Location from, final @Nullable Location via,
            final Location to, final InputStream is) throws XmlPullParserException, IOException {
        final XmlPullParser pp = parserFactory.newPullParser();
        pp.setInput(is, null); // Read encoding from XML declaration
        final ResultHeader header = enterEfa(pp);

        final Calendar plannedTimeCal = new GregorianCalendar(timeZone);
        final Calendar predictedTimeCal = new GregorianCalendar(timeZone);

        final List<Trip> trips = new ArrayList<>();

        if (XmlPullUtil.optEnter(pp, "ts")) {
            while (XmlPullUtil.optEnter(pp, "tp")) {
                XmlPullUtil.optSkip(pp, "attrs");

                XmlPullUtil.valueTag(pp, "d"); // duration
                final int numChanges = Integer.parseInt(XmlPullUtil.valueTag(pp, "ic"));
                final String tripId = XmlPullUtil.valueTag(pp, "de");
                XmlPullUtil.optValueTag(pp, "optval", null);
                XmlPullUtil.optValueTag(pp, "alt", null);
                XmlPullUtil.optValueTag(pp, "gix", null);

                XmlPullUtil.enter(pp, "ls");

                final List<Trip.Leg> legs = new LinkedList<>();
                Location firstDepartureLocation = null;
                Location lastArrivalLocation = null;

                while (XmlPullUtil.test(pp, "l")) {
                    XmlPullUtil.enter(pp, "l");

                    XmlPullUtil.enter(pp, "ps");

                    Stop departure = null;
                    Stop arrival = null;

                    while (XmlPullUtil.optEnter(pp, "p")) {
                        final String name = XmlPullUtil.valueTag(pp, "n");
                        final String usage = XmlPullUtil.valueTag(pp, "u");
                        XmlPullUtil.optValueTag(pp, "de", null);

                        XmlPullUtil.requireSkip(pp, "dt");

                        parseMobileSt(pp, plannedTimeCal, predictedTimeCal);

                        XmlPullUtil.requireSkip(pp, "lis");

                        XmlPullUtil.enter(pp, "r");
                        final String id = XmlPullUtil.valueTag(pp, "id");
                        XmlPullUtil.optValueTag(pp, "a", null);
                        final Position position = parsePosition(XmlPullUtil.optValueTag(pp, "pl", null));
                        final String place = normalizeLocationName(XmlPullUtil.optValueTag(pp, "pc", null));
                        final Point coord = parseCoord(XmlPullUtil.optValueTag(pp, "c", null));
                        XmlPullUtil.skipExit(pp, "r");

                        final Location location;
                        if (id.equals("99999997") || id.equals("99999998"))
                            location = new Location(LocationType.ADDRESS, null, coord, place, name);
                        else
                            location = new Location(LocationType.STATION, id, coord, place, name);

                        XmlPullUtil.skipExit(pp, "p");

                        final Date plannedTime = plannedTimeCal.isSet(Calendar.HOUR_OF_DAY)
                                ? plannedTimeCal.getTime()
                                : null;
                        final Date predictedTime = predictedTimeCal.isSet(Calendar.HOUR_OF_DAY)
                                ? predictedTimeCal.getTime()
                                : null;

                        if ("departure".equals(usage)) {
                            departure = new Stop(location, true, plannedTime, predictedTime, position, null);
                            if (firstDepartureLocation == null)
                                firstDepartureLocation = location;
                        } else if ("arrival".equals(usage)) {
                            arrival = new Stop(location, false, plannedTime, predictedTime, position, null);
                            lastArrivalLocation = location;
                        } else {
                            throw new IllegalStateException("unknown usage: " + usage);
                        }
                    }

                    checkState(departure != null);
                    checkState(arrival != null);

                    XmlPullUtil.skipExit(pp, "ps");

                    final boolean isRealtime = XmlPullUtil.valueTag(pp, "realtime").equals("1");

                    final LineDestination lineDestination = parseMobileM(pp, false);

                    final List<Point> path;
                    if (XmlPullUtil.test(pp, "pt"))
                        path = processCoordinateStrings(pp, "pt");
                    else
                        path = null;

                    final List<Stop> intermediateStops;
                    XmlPullUtil.require(pp, "pss");
                    if (XmlPullUtil.optEnter(pp, "pss")) {
                        intermediateStops = new LinkedList<>();

                        while (XmlPullUtil.test(pp, "s")) {
                            plannedTimeCal.clear();
                            predictedTimeCal.clear();

                            final String s = XmlPullUtil.valueTag(pp, "s");
                            final String[] intermediateParts = s.split(";");
                            final String id = intermediateParts[0];
                            if (!id.equals(departure.location.id) && !id.equals(arrival.location.id)) {
                                final String name = normalizeLocationName(intermediateParts[1]);

                                if (!("0000-1".equals(intermediateParts[2])
                                        && "000-1".equals(intermediateParts[3]))) {
                                    ParserUtils.parseIsoDate(plannedTimeCal, intermediateParts[2]);
                                    ParserUtils.parseIsoTime(plannedTimeCal, intermediateParts[3]);

                                    if (isRealtime) {
                                        ParserUtils.parseIsoDate(predictedTimeCal, intermediateParts[2]);
                                        ParserUtils.parseIsoTime(predictedTimeCal, intermediateParts[3]);

                                        if (intermediateParts.length > 5 && intermediateParts[5].length() > 0) {
                                            final int delay = Integer.parseInt(intermediateParts[5]);
                                            predictedTimeCal.add(Calendar.MINUTE, delay);
                                        }
                                    }
                                }
                                final String coordPart = intermediateParts[4];

                                final Point coords;
                                if (!"::".equals(coordPart)) {
                                    final String[] coordParts = coordPart.split(":");
                                    if ("WGS84".equals(coordParts[2])) {
                                        final int lat = (int) Math.round(Double.parseDouble(coordParts[1]));
                                        final int lon = (int) Math.round(Double.parseDouble(coordParts[0]));
                                        coords = new Point(lat, lon);
                                    } else {
                                        coords = null;
                                    }
                                } else {
                                    coords = null;
                                }
                                final Location location = new Location(LocationType.STATION, id, coords, null,
                                        name);

                                final Date plannedTime = plannedTimeCal.isSet(Calendar.HOUR_OF_DAY)
                                        ? plannedTimeCal.getTime()
                                        : null;
                                final Date predictedTime = predictedTimeCal.isSet(Calendar.HOUR_OF_DAY)
                                        ? predictedTimeCal.getTime()
                                        : null;
                                final Stop stop = new Stop(location, false, plannedTime, predictedTime, null, null);

                                intermediateStops.add(stop);
                            }
                        }

                        XmlPullUtil.skipExit(pp, "pss");
                    } else {
                        intermediateStops = null;
                    }

                    XmlPullUtil.optSkip(pp, "interchange");

                    XmlPullUtil.requireSkip(pp, "ns");
                    // TODO messages

                    XmlPullUtil.skipExit(pp, "l");

                    if (lineDestination.line == Line.FOOTWAY) {
                        legs.add(new Trip.Individual(Trip.Individual.Type.WALK, departure.location,
                                departure.getDepartureTime(), arrival.location, arrival.getArrivalTime(), path, 0));
                    } else if (lineDestination.line == Line.TRANSFER) {
                        legs.add(new Trip.Individual(Trip.Individual.Type.TRANSFER, departure.location,
                                departure.getDepartureTime(), arrival.location, arrival.getArrivalTime(), path, 0));
                    } else if (lineDestination.line == Line.SECURE_CONNECTION
                            || lineDestination.line == Line.DO_NOT_CHANGE) {
                        // ignore
                    } else {
                        legs.add(new Trip.Public(lineDestination.line, lineDestination.destination, departure,
                                arrival, intermediateStops, path, null));
                    }
                }

                XmlPullUtil.skipExit(pp, "ls");

                XmlPullUtil.optSkip(pp, "seqroutes");

                final List<Fare> fares;
                if (XmlPullUtil.optEnter(pp, "tcs")) {
                    fares = new ArrayList<>(2);
                    XmlPullUtil.optSkipMultiple(pp, "tc"); // TODO fares
                    XmlPullUtil.skipExit(pp, "tcs");
                } else {
                    fares = null;
                }

                final Trip trip = new Trip(tripId, firstDepartureLocation, lastArrivalLocation, legs, fares, null,
                        numChanges);
                trips.add(trip);

                XmlPullUtil.skipExit(pp, "tp");
            }

            XmlPullUtil.skipExit(pp, "ts");
        }

        if (trips.size() > 0) {
            final String[] context = (String[]) header.context;
            return new QueryTripsResult(header, url.toString(), from, via, to,
                    new Context(commandLink(context[0], context[1]).toString()), trips);
        } else {
            return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS);
        }
    }

    private List<Point> processItdPathCoordinates(final XmlPullParser pp)
            throws XmlPullParserException, IOException {
        XmlPullUtil.enter(pp, "itdPathCoordinates");
        final List<Point> path;

        final String ellipsoid = XmlPullUtil.valueTag(pp, "coordEllipsoid");
        if ("WGS84".equals(ellipsoid)) {
            final String type = XmlPullUtil.valueTag(pp, "coordType");
            if (!"GEO_DECIMAL".equals(type))
                throw new IllegalStateException("unknown type: " + type);
            if (XmlPullUtil.test(pp, "itdCoordinateString")) {
                path = processCoordinateStrings(pp, "itdCoordinateString");
            } else if (XmlPullUtil.test(pp, "itdCoordinateBaseElemList")) {
                path = processCoordinateBaseElems(pp);
            } else {
                throw new IllegalStateException(pp.getPositionDescription());
            }
        } else {
            return null;
        }

        XmlPullUtil.skipExit(pp, "itdPathCoordinates");
        return path;
    }

    private List<Point> processCoordinateStrings(final XmlPullParser pp, final String tag)
            throws XmlPullParserException, IOException {
        final List<Point> path = new LinkedList<>();

        final String value = XmlPullUtil.valueTag(pp, tag);
        for (final String coordStr : value.split(" +"))
            path.add(parseCoord(coordStr));

        return path;
    }

    private List<Point> processCoordinateBaseElems(final XmlPullParser pp)
            throws XmlPullParserException, IOException {
        final List<Point> path = new LinkedList<>();

        XmlPullUtil.enter(pp, "itdCoordinateBaseElemList");

        while (XmlPullUtil.optEnter(pp, "itdCoordinateBaseElem")) {
            final int lon = (int) Math.round(Double.parseDouble(XmlPullUtil.valueTag(pp, "x")));
            final int lat = (int) Math.round(Double.parseDouble(XmlPullUtil.valueTag(pp, "y")));
            path.add(new Point(lat, lon));

            XmlPullUtil.skipExit(pp, "itdCoordinateBaseElem");
        }

        XmlPullUtil.skipExit(pp, "itdCoordinateBaseElemList");

        return path;
    }

    private Point parseCoord(final String coordStr) {
        if (coordStr == null)
            return null;

        final String[] parts = coordStr.split(",");
        final int lat = (int) Math.round(Double.parseDouble(parts[1]));
        final int lon = (int) Math.round(Double.parseDouble(parts[0]));
        return new Point(lat, lon);
    }

    private Point processCoordAttr(final XmlPullParser pp) {
        final String mapName = XmlPullUtil.optAttr(pp, "mapName", null);
        final int x = (int) Math.round(XmlPullUtil.optFloatAttr(pp, "x", 0));
        final int y = (int) Math.round(XmlPullUtil.optFloatAttr(pp, "y", 0));

        if (mapName == null || (x == 0 && y == 0))
            return null;

        if (!"WGS84".equals(mapName))
            return null;

        return new Point(y, x);
    }

    private Fare processItdGenericTicketGroup(final XmlPullParser pp, final String net, final Currency currency)
            throws XmlPullParserException, IOException {
        XmlPullUtil.enter(pp, "itdGenericTicketGroup");

        Type type = null;
        float fare = 0;

        while (XmlPullUtil.optEnter(pp, "itdGenericTicket")) {
            final String key = XmlPullUtil.valueTag(pp, "ticket");
            final String value = XmlPullUtil.valueTag(pp, "value");

            if (key.equals("FOR_RIDER")) {
                final String typeStr = value.split(" ")[0].toUpperCase();
                if (typeStr.equals("REGULAR"))
                    type = Type.ADULT;
                else
                    type = Type.valueOf(typeStr);
            } else if (key.equals("PRICE")) {
                fare = Float.parseFloat(value) * fareCorrectionFactor;
            }

            XmlPullUtil.skipExit(pp, "itdGenericTicket");
        }

        XmlPullUtil.skipExit(pp, "itdGenericTicketGroup");

        if (type != null)
            return new Fare(net, type, currency, fare, null, null);
        else
            return null;
    }

    private Currency parseCurrency(final String currencyStr) {
        if (currencyStr.equals("US$"))
            return Currency.getInstance("USD");
        if (currencyStr.equals("Dirham"))
            return Currency.getInstance("AED");
        return Currency.getInstance(currencyStr);
    }

    private static final Pattern P_POSITION = Pattern.compile(
            "(?:Gleis|Gl\\.|Bahnsteig|Bstg\\.|Bussteig|Busstg\\.|Steig|Hp\\.|Stop|Pos\\.|Zone|Platform|Stand|Bay|Stance)?\\s*(.+)",
            Pattern.CASE_INSENSITIVE);

    @Override
    protected Position parsePosition(final String position) {
        if (position == null)
            return null;

        if (position.startsWith("Ri.") || position.startsWith("Richtung "))
            return null;

        final Matcher m = P_POSITION.matcher(position);
        if (m.matches())
            return super.parsePosition(m.group(1));

        return super.parsePosition(position);
    }

    private void appendLocationParams(final HttpUrl.Builder url, final Location location,
            final String paramSuffix) {
        final String name = locationValue(location);
        if ((location.type == LocationType.ADDRESS || location.type == LocationType.COORD)
                && location.hasLocation()) {
            url.addEncodedQueryParameter("type_" + paramSuffix, "coord");
            url.addEncodedQueryParameter("name_" + paramSuffix, ParserUtils.urlEncode(
                    String.format(Locale.ENGLISH, "%.6f:%.6f", location.lon / 1E6, location.lat / 1E6) + ":WGS84",
                    requestUrlEncoding));
        } else if (name != null) {
            url.addEncodedQueryParameter("type_" + paramSuffix, locationTypeValue(location));
            url.addEncodedQueryParameter("name_" + paramSuffix, ParserUtils.urlEncode(name, requestUrlEncoding));
        } else {
            throw new IllegalArgumentException("cannot append location: " + location);
        }
    }

    private static String locationTypeValue(final Location location) {
        final LocationType type = location.type;
        if (type == LocationType.STATION)
            return "stop";
        if (type == LocationType.ADDRESS)
            return "any"; // strange, matches with anyObjFilter
        if (type == LocationType.COORD)
            return "coord";
        if (type == LocationType.POI)
            return "poi";
        if (type == LocationType.ANY)
            return "any";
        throw new IllegalArgumentException(type.toString());
    }

    private static @Nullable String locationValue(final Location location) {
        if (location.type == LocationType.STATION && location.hasId())
            return normalizeStationId(location.id);
        else if (location.type == LocationType.POI && location.hasId())
            return location.id;
        else
            return location.name;
    }

    private static final Map<WalkSpeed, String> WALKSPEED_MAP = new HashMap<>();

    static {
        WALKSPEED_MAP.put(WalkSpeed.SLOW, "slow");
        WALKSPEED_MAP.put(WalkSpeed.NORMAL, "normal");
        WALKSPEED_MAP.put(WalkSpeed.FAST, "fast");
    }

    private ResultHeader enterItdRequest(final XmlPullParser pp) throws XmlPullParserException, IOException {
        if (pp.getEventType() != XmlPullParser.START_DOCUMENT)
            throw new ParserException("start of document expected");

        pp.next();

        if (pp.getEventType() == XmlPullParser.DOCDECL)
            pp.next();

        if (pp.getEventType() == XmlPullParser.END_DOCUMENT)
            throw new ParserException("empty document");

        XmlPullUtil.require(pp, "itdRequest");

        final String serverVersion = XmlPullUtil.attr(pp, "version");
        final String now = XmlPullUtil.optAttr(pp, "now", null);
        final String sessionId = XmlPullUtil.attr(pp, "sessionID");
        final String serverId = XmlPullUtil.attr(pp, "serverID");

        final long serverTime;
        if (now != null) {
            final Calendar calendar = new GregorianCalendar(timeZone);
            ParserUtils.parseIsoDate(calendar, now.substring(0, 10));
            ParserUtils.parseEuropeanTime(calendar, now.substring(11));
            serverTime = calendar.getTimeInMillis();
        } else {
            serverTime = 0;
        }

        final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT, serverVersion, serverId, serverTime,
                sessionId);

        XmlPullUtil.enter(pp, "itdRequest");

        XmlPullUtil.optSkip(pp, "itdMessageList");
        XmlPullUtil.optSkip(pp, "clientHeaderLines");
        XmlPullUtil.optSkip(pp, "itdVersionInfo");
        XmlPullUtil.optSkip(pp, "itdLayoutParams");
        XmlPullUtil.optSkip(pp, "itdInfoLinkList");
        XmlPullUtil.optSkip(pp, "serverMetaInfo");

        return header;
    }

    private ResultHeader enterEfa(final XmlPullParser pp) throws XmlPullParserException, IOException {
        if (pp.getEventType() != XmlPullParser.START_DOCUMENT)
            throw new ParserException("start of document expected");

        pp.next();

        if (pp.getEventType() == XmlPullParser.END_DOCUMENT)
            throw new ParserException("empty document");

        XmlPullUtil.enter(pp, "efa");

        final String now = XmlPullUtil.valueTag(pp, "now");
        final Calendar serverTime = new GregorianCalendar(timeZone);
        ParserUtils.parseIsoDate(serverTime, now.substring(0, 10));
        ParserUtils.parseEuropeanTime(serverTime, now.substring(11));

        final Map<String, String> params = processPas(pp);
        final String requestId = params.get("requestID");
        final String sessionId = params.get("sessionID");
        final String serverId = params.get("serverID");

        final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT, null, serverId,
                serverTime.getTimeInMillis(), new String[] { sessionId, requestId });

        return header;
    }

    private Map<String, String> processPas(final XmlPullParser pp) throws XmlPullParserException, IOException {
        final Map<String, String> params = new HashMap<>();

        XmlPullUtil.enter(pp, "pas");

        while (XmlPullUtil.optEnter(pp, "pa")) {
            final String name = XmlPullUtil.valueTag(pp, "n");
            final String value = XmlPullUtil.valueTag(pp, "v");
            params.put(name, value);
            XmlPullUtil.skipExit(pp, "pa");
        }

        XmlPullUtil.skipExit(pp, "pas");

        return params;
    }
}