/*
* 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 .
*/
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 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 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 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 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.optSkip(pp, "ers");
final List 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 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 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 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 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 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 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 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 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 ");
final String usage = XmlPullUtil.attr(pp, "usage");
if (expectedUsage != null && !usage.equals(expectedUsage))
throw new IllegalStateException("expecting ");
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 ");
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 ");
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.COORD;
place = null;
name = null;
} 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 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 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 ownStation = new AtomicReference<>();
final List 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("Straßenbahn"))
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-Köln-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-München
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 ("LCM".equals(trainType) && "Locomore".equals(trainName) && trainNum != null)
return new Line(id, network, Product.HIGH_SPEED_TRAIN, "LCM" + trainNum);
if ("Locomore".equals(longName))
return new Line(id, network, Product.HIGH_SPEED_TRAIN, "LOC" + Strings.nullToEmpty(trainNum));
if ("NJ".equals(trainType) && trainNum != null) // NightJet
return new Line(id, network, Product.HIGH_SPEED_TRAIN, "NJ" + 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) && "Märkische 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 Bäderbahn".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) || "Süd-Thüringen-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 Bäderbahn 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)) // Spěšný 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 - Länderbahn 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) || "Sächsisch-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 Sächsische 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) || "Städtebahn Sachsen".equals(trainName))
return new Line(id, network, Product.REGIONAL_TRAIN, "SBS" + trainNum);
if ("SES".equals(trainType) || "Städteexpress Sachsen".equals(trainName))
return new Line(id, network, Product.REGIONAL_TRAIN, "SES" + trainNum);
if ("SB-".equals(trainType)) // Städtebahn 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) || "Südwestdeutsche Verkehrs-AG".equals(trainName)) && trainNum != null)
return new Line(id, network, Product.REGIONAL_TRAIN, "SWE" + trainNum);
if ("SWEG-Zug".equals(trainName)) // Südwestdeutschen 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 Dolnośląskie (Niederschlesische Eisenbahn)
return new Line(id, network, Product.REGIONAL_TRAIN, symbol);
if ("Koleje Dolnoslaskie".equals(trainName) && symbol != null) // Koleje Dolnośląskie
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-Wörlitzer 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 ("ÖB".equals(trainType) && "Öchsle-Bahn-Betriebsgesellschaft mbH".equals(trainName) && trainNum != null)
return new Line(id, network, Product.REGIONAL_TRAIN, "ÖB" + 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) || "Döllnitzbahn".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) // Réseau Express Régional
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);
if ("PPN".equals(trainType) && "Osobowy".equals(trainName) && trainNum != null)
return new Line(id, network, null, "PPN" + trainNum);
// 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 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(),
new LinkedList()));
}
});
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(), new LinkedList());
final List 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(), new LinkedList());
}
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 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);
if (XmlPullUtil.optEnter(pp, "ers")) {
XmlPullUtil.enter(pp, "err");
final String mod = XmlPullUtil.valueTag(pp, "mod");
final String co = XmlPullUtil.valueTag(pp, "co");
XmlPullUtil.optValueTag(pp, "u", null);
if ("-2000".equals(co)) { // STOP_INVALID
result.set(new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION));
} else if ("-4050".equals(co)) { // NO_SERVINGLINES
result.set(r);
} else {
log.debug("EFA error: {} {}", co, mod);
result.set(new QueryDeparturesResult(header, QueryDeparturesResult.Status.SERVICE_DOWN));
}
XmlPullUtil.exit(pp, "err");
XmlPullUtil.exit(pp, "ers");
} else 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(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);
XmlPullUtil.optValueTag(pp, "trainType", 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, "routeDesc", null);
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, 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 products, final @Nullable Optimize optimize,
final @Nullable WalkSpeed walkSpeed, final @Nullable Accessibility accessibility,
final @Nullable Set