/* * 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.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Currency; import java.util.Date; import java.util.EnumSet; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; 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.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Strings; import de.schildbach.pte.dto.Departure; import de.schildbach.pte.dto.Fare; import de.schildbach.pte.dto.Line; import de.schildbach.pte.dto.Line.Attr; import de.schildbach.pte.dto.Location; import de.schildbach.pte.dto.LocationType; import de.schildbach.pte.dto.NearbyLocationsResult; import de.schildbach.pte.dto.Point; import de.schildbach.pte.dto.Position; import de.schildbach.pte.dto.Product; import de.schildbach.pte.dto.QueryDeparturesResult; import de.schildbach.pte.dto.QueryTripsContext; import de.schildbach.pte.dto.QueryTripsResult; import de.schildbach.pte.dto.ResultHeader; import de.schildbach.pte.dto.StationDepartures; import de.schildbach.pte.dto.Stop; import de.schildbach.pte.dto.SuggestLocationsResult; import de.schildbach.pte.dto.SuggestedLocation; import de.schildbach.pte.dto.Trip; import de.schildbach.pte.exception.ParserException; import de.schildbach.pte.exception.SessionExpiredException; import de.schildbach.pte.util.HttpClient; import de.schildbach.pte.util.LittleEndianDataInputStream; import de.schildbach.pte.util.ParserUtils; import de.schildbach.pte.util.StringReplaceReader; import de.schildbach.pte.util.XmlPullUtil; /** * @author Andreas Schildbach */ public abstract class AbstractHafasProvider extends AbstractNetworkProvider { protected final static String SERVER_PRODUCT = "hafas"; private static final String REQC_PROD = "hafas"; protected static final int DEFAULT_MAX_DEPARTURES = 100; protected static final int DEFAULT_MAX_LOCATIONS = 50; protected String stationBoardEndpoint; protected String getStopEndpoint; protected String queryEndpoint; protected final String mgateEndpoint; private @Nullable String extXmlEndpoint = null; private Product[] productsMap; private @Nullable String accessId = null; private @Nullable String clientType = "ANDROID"; private @Nullable String jsonApiVersion; private @Nullable String jsonApiAuthorization; private @Nullable String jsonApiClient; private Charset jsonGetStopsEncoding = Charsets.ISO_8859_1; private boolean jsonGetStopsUseWeight = true; private Charset jsonNearbyLocationsEncoding = Charsets.ISO_8859_1; private boolean dominantPlanStopTime = false; private boolean useIso8601 = false; private boolean stationBoardHasStationTable = true; private boolean stationBoardHasLocation = false; private boolean stationBoardCanDoEquivs = true; @SuppressWarnings("serial") private static class Context implements QueryTripsContext { public final String laterContext; public final String earlierContext; public final int sequence; public Context(final String laterContext, final String earlierContext, final int sequence) { this.laterContext = laterContext; this.earlierContext = earlierContext; this.sequence = sequence; } public boolean canQueryLater() { return laterContext != null; } public boolean canQueryEarlier() { return earlierContext != null; } } @SuppressWarnings("serial") public static class JsonContext implements QueryTripsContext { public final Location from, to; public final Date date; public final boolean dep; public final Set products; public final String laterContext, earlierContext; public JsonContext(final Location from, final Location to, final Date date, final boolean dep, final Set products, final String laterContext, final String earlierContext) { this.from = from; this.to = to; this.date = date; this.dep = dep; this.products = products; this.laterContext = laterContext; this.earlierContext = earlierContext; } public boolean canQueryLater() { return laterContext != null; } public boolean canQueryEarlier() { return earlierContext != null; } } @SuppressWarnings("serial") public static class QueryTripsBinaryContext implements QueryTripsContext { public final String ident; public final int seqNr; public final String ld; public final int usedBufferSize; private final boolean canQueryMore; public QueryTripsBinaryContext(final String ident, final int seqNr, final String ld, final int usedBufferSize, final boolean canQueryMore) { this.ident = ident; this.seqNr = seqNr; this.ld = ld; this.usedBufferSize = usedBufferSize; this.canQueryMore = canQueryMore; } public boolean canQueryLater() { return canQueryMore; } public boolean canQueryEarlier() { return canQueryMore; } } public AbstractHafasProvider(final NetworkId network, final String apiBase, final String apiLanguage, final Product[] productsMap) { super(network); this.stationBoardEndpoint = apiBase + "stboard.exe/" + apiLanguage; this.getStopEndpoint = apiBase + "ajax-getstop.exe/" + apiLanguage; this.queryEndpoint = apiBase + "query.exe/" + apiLanguage; this.mgateEndpoint = apiBase + "mgate.exe"; this.productsMap = productsMap; } protected void setStationBoardEndpoint(final String stationBoardEndpoint) { this.stationBoardEndpoint = stationBoardEndpoint; } protected void setGetStopEndpoint(final String getStopEndpoint) { this.getStopEndpoint = getStopEndpoint; } protected void setQueryEndpoint(final String queryEndpoint) { this.queryEndpoint = queryEndpoint; } protected void setExtXmlEndpoint(final String extXmlEndpoint) { this.extXmlEndpoint = extXmlEndpoint; } protected void setAccessId(final String accessId) { this.accessId = accessId; } protected void setClientType(final String clientType) { this.clientType = clientType; } protected void setJsonApiVersion(final String jsonApiVersion) { this.jsonApiVersion = jsonApiVersion; } protected void setJsonApiAuthorization(final String jsonApiAuthorization) { this.jsonApiAuthorization = jsonApiAuthorization; } protected void setJsonApiClient(final String jsonApiClient) { this.jsonApiClient = jsonApiClient; } protected void setDominantPlanStopTime(final boolean dominantPlanStopTime) { this.dominantPlanStopTime = dominantPlanStopTime; } protected void setJsonGetStopsEncoding(final Charset jsonGetStopsEncoding) { this.jsonGetStopsEncoding = jsonGetStopsEncoding; } protected void setJsonGetStopsUseWeight(final boolean jsonGetStopsUseWeight) { this.jsonGetStopsUseWeight = jsonGetStopsUseWeight; } protected void setJsonNearbyLocationsEncoding(final Charset jsonNearbyLocationsEncoding) { this.jsonNearbyLocationsEncoding = jsonNearbyLocationsEncoding; } protected void setUseIso8601(final boolean useIso8601) { this.useIso8601 = useIso8601; } protected void setStationBoardHasStationTable(final boolean stationBoardHasStationTable) { this.stationBoardHasStationTable = stationBoardHasStationTable; } protected void setStationBoardHasLocation(final boolean stationBoardHasLocation) { this.stationBoardHasLocation = stationBoardHasLocation; } protected void setStationBoardCanDoEquivs(final boolean canDoEquivs) { this.stationBoardCanDoEquivs = canDoEquivs; } @Override protected boolean hasCapability(final Capability capability) { return true; } protected final CharSequence productsString(final Set products) { final StringBuilder productsStr = new StringBuilder(productsMap.length); for (int i = 0; i < productsMap.length; i++) { if (productsMap[i] != null && products.contains(productsMap[i])) productsStr.append('1'); else productsStr.append('0'); } return productsStr; } protected final CharSequence allProductsString() { final StringBuilder productsStr = new StringBuilder(productsMap.length); for (int i = 0; i < productsMap.length; i++) productsStr.append('1'); return productsStr; } protected final int allProductsInt() { return (1 << productsMap.length) - 1; } protected final Product intToProduct(final int productInt) { final int allProductsInt = allProductsInt(); checkArgument(productInt < allProductsInt, "value " + productInt + " must be smaller than " + allProductsInt); int value = productInt; Product product = null; for (int i = productsMap.length - 1; i >= 0; i--) { final int v = 1 << i; if (value >= v) { if (product != null) throw new IllegalArgumentException("ambigous value: " + productInt); product = productsMap[i]; value -= v; } } checkState(value == 0); return product; } protected final Set intToProducts(int value) { final int allProductsInt = allProductsInt(); checkArgument(value <= allProductsInt, "value " + value + " cannot be greater than " + allProductsInt); final Set products = EnumSet.noneOf(Product.class); for (int i = productsMap.length - 1; i >= 0; i--) { final int v = 1 << i; if (value >= v) { final Product product = checkNotNull(productsMap[i], "unknown product " + i); products.add(product); value -= v; } } checkState(value == 0); return products; } protected static final Pattern P_SPLIT_NAME_FIRST_COMMA = Pattern.compile("([^,]*), (.*)"); protected static final Pattern P_SPLIT_NAME_LAST_COMMA = Pattern.compile("(.*), ([^,]*)"); protected static final Pattern P_SPLIT_NAME_PAREN = Pattern.compile("(.*) \\((.{3,}?)\\)"); protected String[] splitStationName(final String name) { return new String[] { null, name }; } protected String[] splitPOI(final String poi) { return new String[] { null, poi }; } protected String[] splitAddress(final String address) { return new String[] { null, address }; } private final String wrapReqC(final CharSequence request, final Charset encoding) { return "" // + "" // + request // + ""; } private final Location parseStation(final XmlPullParser pp) { final String type = pp.getName(); if ("Station".equals(type)) { final String name = XmlPullUtil.attr(pp, "name"); final String id = XmlPullUtil.attr(pp, "externalStationNr"); final int x = XmlPullUtil.intAttr(pp, "x"); final int y = XmlPullUtil.intAttr(pp, "y"); final String[] placeAndName = splitStationName(name); return new Location(LocationType.STATION, id, y, x, placeAndName[0], placeAndName[1]); } throw new IllegalStateException("cannot handle: " + type); } private static final Location parsePoi(final XmlPullParser pp) { final String type = pp.getName(); if ("Poi".equals(type)) { String name = XmlPullUtil.attr(pp, "name"); if (name.equals("unknown")) name = null; final int x = XmlPullUtil.intAttr(pp, "x"); final int y = XmlPullUtil.intAttr(pp, "y"); return new Location(LocationType.POI, null, y, x, null, name); } throw new IllegalStateException("cannot handle: " + type); } private final Location parseAddress(final XmlPullParser pp) { final String type = pp.getName(); if ("Address".equals(type)) { String name = XmlPullUtil.attr(pp, "name"); if (name.equals("unknown")) name = null; final int x = XmlPullUtil.intAttr(pp, "x"); final int y = XmlPullUtil.intAttr(pp, "y"); final String[] placeAndName = splitAddress(name); return new Location(LocationType.ADDRESS, null, y, x, placeAndName[0], placeAndName[1]); } throw new IllegalStateException("cannot handle: " + type); } private final Position parsePlatform(final XmlPullParser pp) throws XmlPullParserException, IOException { XmlPullUtil.enter(pp, "Platform"); final String platformText = XmlPullUtil.valueTag(pp, "Text"); XmlPullUtil.skipExit(pp, "Platform"); if (platformText == null || platformText.length() == 0) return null; else return parsePosition(platformText); } public SuggestLocationsResult suggestLocations(final CharSequence constraint) throws IOException { final StringBuilder uri = new StringBuilder(getStopEndpoint); appendJsonGetStopsParameters(uri, checkNotNull(constraint), 0); return jsonGetStops(uri.toString()); } protected void appendJsonGetStopsParameters(final StringBuilder uri, final CharSequence constraint, final int maxStops) { uri.append("?getstop=1"); uri.append("&REQ0JourneyStopsS0A=255"); uri.append("&REQ0JourneyStopsS0G=").append(ParserUtils.urlEncode(constraint.toString(), jsonGetStopsEncoding)).append("?"); if (maxStops > 0) uri.append("&REQ0JourneyStopsB=").append(maxStops); uri.append("&js=true"); } private static final Pattern P_AJAX_GET_STOPS_JSON = Pattern.compile("SLs\\.sls\\s*=\\s*(.*?);\\s*SLs\\.showSuggestion\\(\\);", Pattern.DOTALL); private static final Pattern P_AJAX_GET_STOPS_ID = Pattern.compile(".*?@L=0*(\\d+)@.*?"); protected final SuggestLocationsResult jsonGetStops(final String uri) throws IOException { final CharSequence page = httpClient.get(uri, jsonGetStopsEncoding); final Matcher mJson = P_AJAX_GET_STOPS_JSON.matcher(page); if (mJson.matches()) { final String json = mJson.group(1); final List locations = new ArrayList(); try { final JSONObject head = new JSONObject(json); final JSONArray aSuggestions = head.getJSONArray("suggestions"); for (int i = 0; i < aSuggestions.length(); i++) { final JSONObject suggestion = aSuggestions.optJSONObject(i); if (suggestion != null) { final int type = suggestion.getInt("type"); final String value = suggestion.getString("value"); final int lat = suggestion.optInt("ycoord"); final int lon = suggestion.optInt("xcoord"); final int weight = jsonGetStopsUseWeight ? suggestion.getInt("weight") : -i; String localId = null; final Matcher m = P_AJAX_GET_STOPS_ID.matcher(suggestion.getString("id")); if (m.matches()) localId = m.group(1); final Location location; if (type == 1) // station { final String[] placeAndName = splitStationName(value); location = new Location(LocationType.STATION, localId, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 2) // address { final String[] placeAndName = splitAddress(value); location = new Location(LocationType.ADDRESS, null, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 4) // poi { final String[] placeAndName = splitPOI(value); location = new Location(LocationType.POI, localId, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 128) // crossing { final String[] placeAndName = splitAddress(value); location = new Location(LocationType.ADDRESS, localId, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 87) { location = null; // don't know what to do } else { throw new IllegalStateException("unknown type " + type + " on " + uri); } if (location != null) { final SuggestedLocation suggestedLocation = new SuggestedLocation(location, weight); locations.add(suggestedLocation); } } } return new SuggestLocationsResult(new ResultHeader(network, SERVER_PRODUCT), locations); } catch (final JSONException x) { throw new RuntimeException("cannot parse: '" + json + "' on " + uri, x); } } else { throw new RuntimeException("cannot parse: '" + page + "' on " + uri); } } public QueryDeparturesResult queryDepartures(final String stationId, final @Nullable Date time, final int maxDepartures, final boolean equivs) throws IOException { checkNotNull(Strings.emptyToNull(stationId)); final StringBuilder uri = new StringBuilder(stationBoardEndpoint); appendXmlStationBoardParameters(uri, time, stationId, maxDepartures, equivs, "vs_java3"); return xmlStationBoard(uri.toString(), stationId); } protected void appendXmlStationBoardParameters(final StringBuilder uri, final @Nullable Date time, final String stationId, final int maxDepartures, final boolean equivs, final @Nullable String styleSheet) { uri.append("?productsFilter=").append(allProductsString()); uri.append("&boardType=dep"); if (stationBoardCanDoEquivs) uri.append("&disableEquivs=").append(equivs ? "0" : "1"); uri.append("&maxJourneys=").append(maxDepartures > 0 ? maxDepartures : DEFAULT_MAX_DEPARTURES); uri.append("&input=").append(normalizeStationId(stationId)); appendDateTimeParameters(uri, time, "date", "time"); if (clientType != null) uri.append("&clientType=").append(ParserUtils.urlEncode(clientType)); if (styleSheet != null) uri.append("&L=").append(styleSheet); uri.append("&hcount=0"); // prevents showing old departures uri.append("&start=yes"); } protected void appendDateTimeParameters(final StringBuilder uri, final Date time, final String dateParamName, final String timeParamName) { final Calendar c = new GregorianCalendar(timeZone); c.setTime(time); final int year = c.get(Calendar.YEAR); final int month = c.get(Calendar.MONTH) + 1; final int day = c.get(Calendar.DAY_OF_MONTH); final int hour = c.get(Calendar.HOUR_OF_DAY); final int minute = c.get(Calendar.MINUTE); uri.append('&').append(dateParamName).append('='); uri.append(ParserUtils.urlEncode(useIso8601 ? String.format(Locale.ENGLISH, "%04d-%02d-%02d", year, month, day) : String.format( Locale.ENGLISH, "%02d.%02d.%02d", day, month, year - 2000))); uri.append('&').append(timeParamName).append('='); uri.append(ParserUtils.urlEncode(String.format(Locale.ENGLISH, "%02d:%02d", hour, minute))); } private static final Pattern P_XML_STATION_BOARD_DELAY = Pattern.compile("(?:-|k\\.A\\.?|cancel|\\+?\\s*(\\d+))"); protected final QueryDeparturesResult xmlStationBoard(final String uri, final String stationId) throws IOException { final String normalizedStationId = normalizeStationId(stationId); StringReplaceReader reader = null; String firstChars = null; try { // work around unparsable XML final InputStream is = httpClient.getInputStream(uri); firstChars = HttpClient.peekFirstChars(is); reader = new StringReplaceReader(new InputStreamReader(is, Charsets.ISO_8859_1), " & ", " & "); reader.replace("", " "); reader.replace("", " "); reader.replace("", " "); reader.replace("", " "); reader.replace("", " "); reader.replace("", " "); reader.replace("
", " "); reader.replace(" ->", " →"); // right arrow reader.replace(" <-", " ←"); // left arrow reader.replace(" <> ", " ↔ "); // left-right arrow addCustomReplaces(reader); final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); final XmlPullParser pp = factory.newPullParser(); pp.setInput(reader); pp.nextTag(); final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT); final QueryDeparturesResult result = new QueryDeparturesResult(header); if (XmlPullUtil.test(pp, "Err")) { final String code = XmlPullUtil.attr(pp, "code"); final String text = XmlPullUtil.attr(pp, "text"); if (code.equals("H730")) // Your input is not valid return new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION); if (code.equals("H890")) { result.stationDepartures.add(new StationDepartures(new Location(LocationType.STATION, normalizedStationId), Collections . emptyList(), null)); return result; } throw new IllegalArgumentException("unknown error " + code + ", " + text); } String[] stationPlaceAndName = null; if (stationBoardHasStationTable) XmlPullUtil.enter(pp, "StationTable"); else checkState(!XmlPullUtil.test(pp, "StationTable")); if (stationBoardHasLocation) { XmlPullUtil.require(pp, "St"); final String evaId = XmlPullUtil.attr(pp, "evaId"); if (evaId != null) { if (!evaId.equals(normalizedStationId)) throw new IllegalStateException("stationId: " + normalizedStationId + ", evaId: " + evaId); final String name = XmlPullUtil.attr(pp, "name"); if (name != null) stationPlaceAndName = splitStationName(name.trim()); } XmlPullUtil.requireSkip(pp, "St"); } else { checkState(!XmlPullUtil.test(pp, "St")); } while (XmlPullUtil.test(pp, "Journey")) { final String fpTime = XmlPullUtil.attr(pp, "fpTime"); final String fpDate = XmlPullUtil.attr(pp, "fpDate"); final String delay = XmlPullUtil.attr(pp, "delay"); final String eDelay = XmlPullUtil.optAttr(pp, "e_delay", null); final String platform = XmlPullUtil.optAttr(pp, "platform", null); // TODO newpl final String targetLoc = XmlPullUtil.optAttr(pp, "targetLoc", null); // TODO hafasname final String dirnr = XmlPullUtil.optAttr(pp, "dirnr", null); final String prod = XmlPullUtil.attr(pp, "prod"); final String classStr = XmlPullUtil.optAttr(pp, "class", null); final String dir = XmlPullUtil.optAttr(pp, "dir", null); final String capacityStr = XmlPullUtil.optAttr(pp, "capacity", null); final String depStation = XmlPullUtil.optAttr(pp, "depStation", null); final String delayReason = XmlPullUtil.optAttr(pp, "delayReason", null); // TODO is_reachable // TODO disableTrainInfo // TODO lineFG/lineBG (ZVV) final String administration = normalizeLineAdministration(XmlPullUtil.optAttr(pp, "administration", null)); if (!"cancel".equals(delay) && !"cancel".equals(eDelay)) { final Calendar plannedTime = new GregorianCalendar(timeZone); plannedTime.clear(); ParserUtils.parseEuropeanTime(plannedTime, fpTime); if (fpDate.length() == 8) ParserUtils.parseGermanDate(plannedTime, fpDate); else if (fpDate.length() == 10) ParserUtils.parseIsoDate(plannedTime, fpDate); else throw new IllegalStateException("cannot parse: '" + fpDate + "'"); final Calendar predictedTime; if (eDelay != null) { predictedTime = new GregorianCalendar(timeZone); predictedTime.setTimeInMillis(plannedTime.getTimeInMillis()); predictedTime.add(Calendar.MINUTE, Integer.parseInt(eDelay)); } else if (delay != null) { final Matcher m = P_XML_STATION_BOARD_DELAY.matcher(delay); if (m.matches()) { if (m.group(1) != null) { predictedTime = new GregorianCalendar(timeZone); predictedTime.setTimeInMillis(plannedTime.getTimeInMillis()); predictedTime.add(Calendar.MINUTE, Integer.parseInt(m.group(1))); } else { predictedTime = null; } } else { throw new RuntimeException("cannot parse delay: '" + delay + "'"); } } else { predictedTime = null; } final Position position = parsePosition(ParserUtils.resolveEntities(platform)); final String destinationName; if (dir != null) destinationName = dir.trim(); else if (targetLoc != null) destinationName = targetLoc.trim(); else destinationName = null; final Location destination; if (dirnr != null) { final String[] destinationPlaceAndName = splitStationName(destinationName); destination = new Location(LocationType.STATION, dirnr, destinationPlaceAndName[0], destinationPlaceAndName[1]); } else { destination = new Location(LocationType.ANY, null, null, destinationName); } final Line prodLine = parseLineAndType(prod); final Line line; if (classStr != null) { final Product product = intToProduct(Integer.parseInt(classStr)); if (product == null) throw new IllegalArgumentException(); // could check for type consistency here final Set attrs = prodLine.attrs; if (attrs != null) line = newLine(administration, product, prodLine.label, null, attrs.toArray(new Line.Attr[0])); else line = newLine(administration, product, prodLine.label, null); } else { final Set attrs = prodLine.attrs; if (attrs != null) line = newLine(administration, prodLine.product, prodLine.label, null, attrs.toArray(new Line.Attr[0])); else line = newLine(administration, prodLine.product, prodLine.label, null); } final int[] capacity; if (capacityStr != null && !"0|0".equals(capacityStr)) { final String[] capacityParts = capacityStr.split("\\|"); capacity = new int[] { Integer.parseInt(capacityParts[0]), Integer.parseInt(capacityParts[1]) }; } else { capacity = null; } final String message; if (delayReason != null) { final String msg = delayReason.trim(); message = msg.length() > 0 ? msg : null; } else { message = null; } final Departure departure = new Departure(plannedTime.getTime(), predictedTime != null ? predictedTime.getTime() : null, line, position, destination, capacity, message); final Location location; if (!stationBoardCanDoEquivs || depStation == null) { location = new Location(LocationType.STATION, normalizedStationId, stationPlaceAndName != null ? stationPlaceAndName[0] : null, stationPlaceAndName != null ? stationPlaceAndName[1] : null); } else { final String[] depPlaceAndName = splitStationName(depStation); location = new Location(LocationType.STATION, null, depPlaceAndName[0], depPlaceAndName[1]); } StationDepartures stationDepartures = findStationDepartures(result.stationDepartures, location); if (stationDepartures == null) { stationDepartures = new StationDepartures(location, new ArrayList(8), null); result.stationDepartures.add(stationDepartures); } stationDepartures.departures.add(departure); } XmlPullUtil.requireSkip(pp, "Journey"); } if (stationBoardHasStationTable) XmlPullUtil.exit(pp, "StationTable"); XmlPullUtil.requireEndDocument(pp); // sort departures for (final StationDepartures stationDepartures : result.stationDepartures) Collections.sort(stationDepartures.departures, Departure.TIME_COMPARATOR); return result; } catch (final XmlPullParserException x) { throw new ParserException("cannot parse xml: " + firstChars, x); } finally { if (reader != null) reader.close(); } } private StationDepartures findStationDepartures(final List stationDepartures, final Location location) { for (final StationDepartures stationDeparture : stationDepartures) if (stationDeparture.location.equals(location)) return stationDeparture; return null; } protected void addCustomReplaces(final StringReplaceReader reader) { } protected final NearbyLocationsResult jsonLocGeoPos(final EnumSet types, final int lat, final int lon) throws IOException { final boolean getPOIs = types.contains(LocationType.POI); final String request = wrapJsonApiRequest("LocGeoPos", "{\"ring\":" // + "{\"cCrd\":{\"x\":" + lon + ",\"y\":" + lat + "}}," // + "\"getPOIs\":" + getPOIs + "}", // false); final String uri = checkNotNull(mgateEndpoint); final CharSequence page = httpClient.get(uri, request, "application/json", Charsets.UTF_8); try { final JSONObject head = new JSONObject(page.toString()); final String headErr = head.optString("err", null); if (headErr != null) throw new RuntimeException(headErr); final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT, head.getString("ver"), 0, null); final JSONArray svcResList = head.getJSONArray("svcResL"); checkState(svcResList.length() == 1); final JSONObject svcRes = svcResList.optJSONObject(0); checkState("LocGeoPos".equals(svcRes.getString("meth"))); final String err = svcRes.getString("err"); if (!"OK".equals(err)) { final String errTxt = svcRes.getString("errTxt"); throw new RuntimeException(err + ": " + errTxt); } final JSONObject res = svcRes.getJSONObject("res"); final JSONObject common = res.getJSONObject("common"); /* final List remarks = */ parseRemList(common.getJSONArray("remL")); final JSONArray locL = res.optJSONArray("locL"); final List locations; if (locL != null) { locations = parseLocList(locL); // filter unwanted location types for (Iterator i = locations.iterator(); i.hasNext();) { final Location location = i.next(); if (!types.contains(location.type)) i.remove(); } } else { locations = Collections.emptyList(); } return new NearbyLocationsResult(header, locations); } catch (final JSONException x) { throw new ParserException("cannot parse json: '" + page + "' on " + uri, x); } } protected final QueryDeparturesResult jsonStationBoard(final String stationId, final @Nullable Date time, final int maxDepartures, final boolean equivs) throws IOException { final Calendar c = new GregorianCalendar(timeZone); c.setTime(time); final CharSequence jsonDate = jsonDate(c); final CharSequence jsonTime = jsonTime(c); final CharSequence normalizedStationId = normalizeStationId(stationId); final CharSequence stbFltrEquiv = Boolean.toString(!equivs); final CharSequence maxJny = Integer.toString(maxDepartures != 0 ? maxDepartures : DEFAULT_MAX_DEPARTURES); final CharSequence getPasslist = Boolean.toString(true); // traffic expensive final String request = wrapJsonApiRequest("StationBoard", "{\"type\":\"DEP\"," // + "\"date\":\"" + jsonDate + "\"," // + "\"time\":\"" + jsonTime + "\"," // + "\"stbLoc\":{\"type\":\"S\"," + "\"state\":\"F\"," // F/M + "\"extId\":" + JSONObject.quote(normalizedStationId.toString()) + "}," // + "\"stbFltrEquiv\":" + stbFltrEquiv + ",\"maxJny\":" + maxJny + ",\"getPasslist\":" + getPasslist + "}", false); final String uri = checkNotNull(mgateEndpoint); final CharSequence page = httpClient.get(uri, request, "application/json", Charsets.UTF_8); try { final JSONObject head = new JSONObject(page.toString()); final String headErr = head.optString("err", null); if (headErr != null) throw new RuntimeException(headErr); final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT, head.getString("ver"), 0, null); final QueryDeparturesResult result = new QueryDeparturesResult(header); final JSONArray svcResList = head.getJSONArray("svcResL"); checkState(svcResList.length() == 1); final JSONObject svcRes = svcResList.optJSONObject(0); checkState("StationBoard".equals(svcRes.getString("meth"))); final String err = svcRes.getString("err"); if (!"OK".equals(err)) { final String errTxt = svcRes.getString("errTxt"); if ("LOCATION".equals(err) && "HCI Service: location missing or invalid".equals(errTxt)) return new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION); else throw new RuntimeException(err + ": " + errTxt); } final JSONObject res = svcRes.getJSONObject("res"); final JSONObject common = res.getJSONObject("common"); /* final List remarks = */ parseRemList(common.getJSONArray("remL")); final List operators = parseOpList(common.getJSONArray("opL")); final List lines = parseProdList(common.getJSONArray("prodL"), operators); final JSONArray locList = common.getJSONArray("locL"); final List locations = parseLocList(locList); final JSONArray jnyList = res.optJSONArray("jnyL"); if (jnyList != null) { for (int iJny = 0; iJny < jnyList.length(); iJny++) { final JSONObject jny = jnyList.getJSONObject(iJny); final JSONObject stbStop = jny.getJSONObject("stbStop"); final String stbStopPlatformS = stbStop.optString("dPlatfS", null); c.clear(); ParserUtils.parseIsoDate(c, jny.getString("date")); final Date baseDate = c.getTime(); final Date plannedTime = parseJsonTime(c, baseDate, stbStop.getString("dTimeS")); final Date predictedTime = parseJsonTime(c, baseDate, stbStop.optString("dTimeR", null)); final Line line = lines.get(stbStop.getInt("dProdX")); final Location location = equivs ? locations.get(stbStop.getInt("locX")) : new Location(LocationType.STATION, stationId); final Position position = normalizePosition(stbStopPlatformS); final String jnyDirTxt = jny.getString("dirTxt"); final JSONArray stopList = jny.optJSONArray("stopL"); final Location destination; if (stopList != null) { final int lastStopIdx = stopList.getJSONObject(stopList.length() - 1).getInt("locX"); final String lastStopName = locList.getJSONObject(lastStopIdx).getString("name"); if (jnyDirTxt.equals(lastStopName)) destination = locations.get(lastStopIdx); else destination = new Location(LocationType.ANY, null, null, jnyDirTxt); } else { destination = new Location(LocationType.ANY, null, null, jnyDirTxt); } final Departure departure = new Departure(plannedTime, predictedTime, line, position, destination, null, null); StationDepartures stationDepartures = findStationDepartures(result.stationDepartures, location); if (stationDepartures == null) { stationDepartures = new StationDepartures(location, new ArrayList(8), null); result.stationDepartures.add(stationDepartures); } stationDepartures.departures.add(departure); } } // sort departures for (final StationDepartures stationDepartures : result.stationDepartures) Collections.sort(stationDepartures.departures, Departure.TIME_COMPARATOR); return result; } catch (final JSONException x) { throw new ParserException("cannot parse json: '" + page + "' on " + uri, x); } } protected final SuggestLocationsResult jsonLocMatch(final CharSequence constraint) throws IOException { final String request = wrapJsonApiRequest("LocMatch", "{\"input\":{\"field\":\"S\",\"loc\":{\"name\":" + JSONObject.quote(checkNotNull(constraint).toString()) + ",\"meta\":false},\"maxLoc\":" + DEFAULT_MAX_LOCATIONS + "}}", true); final String uri = checkNotNull(mgateEndpoint); final CharSequence page = httpClient.get(uri, request, "application/json", Charsets.UTF_8); try { final JSONObject head = new JSONObject(page.toString()); final String headErr = head.optString("err", null); if (headErr != null) throw new RuntimeException(headErr); final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT, head.getString("ver"), 0, null); final JSONArray svcResList = head.getJSONArray("svcResL"); checkState(svcResList.length() == 1); final JSONObject svcRes = svcResList.optJSONObject(0); checkState("LocMatch".equals(svcRes.getString("meth"))); final String err = svcRes.getString("err"); if (!"OK".equals(err)) { final String errTxt = svcRes.getString("errTxt"); throw new RuntimeException(err + ": " + errTxt); } final JSONObject res = svcRes.getJSONObject("res"); final JSONObject common = res.getJSONObject("common"); /* final List remarks = */ parseRemList(common.getJSONArray("remL")); final JSONObject match = res.getJSONObject("match"); final List locations = parseLocList(match.optJSONArray("locL")); final List suggestedLocations = new ArrayList(locations.size()); for (final Location location : locations) suggestedLocations.add(new SuggestedLocation(location)); // TODO weight return new SuggestLocationsResult(header, suggestedLocations); } catch (final JSONException x) { throw new ParserException("cannot parse json: '" + page + "' on " + uri, x); } } private static final Joiner JOINER = Joiner.on(' ').skipNulls(); protected final QueryTripsResult jsonTripSearch(Location from, Location to, final Date time, final boolean dep, final @Nullable Set products, final String moreContext) throws IOException { if (!from.hasId() && from.hasName()) { final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT); final List locations = suggestLocations(JOINER.join(from.place, from.name)).getLocations(); if (locations.isEmpty()) return new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_FROM); if (locations.size() > 1) return new QueryTripsResult(header, locations, null, null); from = locations.get(0); } if (!to.hasId() && to.hasName()) { final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT); final List locations = suggestLocations(JOINER.join(to.place, to.name)).getLocations(); if (locations.isEmpty()) return new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_TO); if (locations.size() > 1) return new QueryTripsResult(header, null, null, locations); to = locations.get(0); } final Calendar c = new GregorianCalendar(timeZone); c.setTime(time); final CharSequence outDate = jsonDate(c); final CharSequence outTime = jsonTime(c); final CharSequence outFrwd = Boolean.toString(dep); final CharSequence jnyFltr = productsString(products); final CharSequence jsonContext = moreContext != null ? "\"ctxScr\":" + JSONObject.quote(moreContext) + "," : ""; final String request = wrapJsonApiRequest("TripSearch", "{" // + jsonContext // + "\"depLocL\":[" + jsonLocation(from) + "]," // + "\"arrLocL\":[" + jsonLocation(to) + "]," // + "\"outDate\":\"" + outDate + "\"," // + "\"outTime\":\"" + outTime + "\"," // + "\"outFrwd\":" + outFrwd + "," // + "\"jnyFltrL\":[{\"value\":\"" + jnyFltr + "\",\"mode\":\"BIT\",\"type\":\"PROD\"}]," // + "\"gisFltrL\":[{\"mode\":\"FB\",\"profile\":{\"type\":\"F\",\"linDistRouting\":false,\"maxdist\":2000},\"type\":\"P\"}]," // + "\"getPolyline\":false,\"getPasslist\":true,\"liveSearch\":false,\"getIST\":false,\"getEco\":false,\"extChgTime\":-1,\"economic\":false}", // false); final String uri = checkNotNull(mgateEndpoint); final CharSequence page = httpClient.get(uri, request, "application/json", Charsets.UTF_8); try { final JSONObject head = new JSONObject(page.toString()); final String headErr = head.optString("err", null); if (headErr != null) throw new RuntimeException(headErr); final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT, head.getString("ver"), 0, null); final JSONArray svcResList = head.getJSONArray("svcResL"); checkState(svcResList.length() == 1); final JSONObject svcRes = svcResList.optJSONObject(0); checkState("TripSearch".equals(svcRes.getString("meth"))); final String err = svcRes.getString("err"); if (!"OK".equals(err)) { if ("H890".equals(err)) // No connections found. return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS); if ("H891".equals(err)) // No route found (try entering an intermediate station). return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS); if ("H895".equals(err)) // Departure/Arrival are too near. return new QueryTripsResult(header, QueryTripsResult.Status.TOO_CLOSE); if ("H9220".equals(err)) // Nearby to the given address stations could not be found. return new QueryTripsResult(header, QueryTripsResult.Status.UNRESOLVABLE_ADDRESS); if ("H9360".equals(err)) // Date outside of the timetable period. return new QueryTripsResult(header, QueryTripsResult.Status.INVALID_DATE); final String errTxt = svcRes.getString("errTxt"); throw new RuntimeException(err + ": " + errTxt); } final JSONObject res = svcRes.getJSONObject("res"); final JSONObject common = res.getJSONObject("common"); /* final List remarks = */ parseRemList(common.getJSONArray("remL")); final List locations = parseLocList(common.getJSONArray("locL")); final List operators = parseOpList(common.getJSONArray("opL")); final List lines = parseProdList(common.getJSONArray("prodL"), operators); final JSONArray outConList = res.optJSONArray("outConL"); final List trips = new ArrayList(outConList.length()); for (int iOutCon = 0; iOutCon < outConList.length(); iOutCon++) { final JSONObject outCon = outConList.getJSONObject(iOutCon); final Location tripFrom = locations.get(outCon.getJSONObject("dep").getInt("locX")); final Location tripTo = locations.get(outCon.getJSONObject("arr").getInt("locX")); c.clear(); ParserUtils.parseIsoDate(c, outCon.getString("date")); final Date baseDate = c.getTime(); final JSONArray secList = outCon.optJSONArray("secL"); final List legs = new ArrayList(secList.length()); for (int iSec = 0; iSec < secList.length(); iSec++) { final JSONObject sec = secList.getJSONObject(iSec); final String secType = sec.getString("type"); final JSONObject secDep = sec.getJSONObject("dep"); final Stop departureStop = parseJsonStop(secDep, locations, c, baseDate); final JSONObject secArr = sec.getJSONObject("arr"); final Stop arrivalStop = parseJsonStop(secArr, locations, c, baseDate); final Trip.Leg leg; if ("JNY".equals(secType)) { final JSONObject jny = sec.getJSONObject("jny"); final Line line = lines.get(jny.getInt("prodX")); final String dirTxt = jny.optString("dirTxt", null); final Location destination = dirTxt != null ? new Location(LocationType.ANY, null, null, dirTxt) : null; final JSONArray stopList = jny.getJSONArray("stopL"); checkState(stopList.length() >= 2); final List intermediateStops = new ArrayList(stopList.length()); for (int iStop = 1; iStop < stopList.length() - 1; iStop++) { final JSONObject stop = stopList.getJSONObject(iStop); final Stop intermediateStop = parseJsonStop(stop, locations, c, baseDate); intermediateStops.add(intermediateStop); } leg = new Trip.Public(line, destination, departureStop, arrivalStop, intermediateStops, null, null); } else if ("WALK".equals(secType) || "TRSF".equals(secType)) { final JSONObject gis = sec.getJSONObject("gis"); final int distance = gis.getInt("dist"); leg = new Trip.Individual(Trip.Individual.Type.WALK, departureStop.location, departureStop.getDepartureTime(), arrivalStop.location, arrivalStop.getArrivalTime(), null, distance); } else { throw new IllegalStateException("cannot handle type: " + secType); } legs.add(leg); } final JSONObject trfRes = outCon.optJSONObject("trfRes"); final List fares = new LinkedList(); if (trfRes != null) { final JSONArray fareSetList = trfRes.getJSONArray("fareSetL"); for (int iFareSet = 0; iFareSet < fareSetList.length(); iFareSet++) { final JSONObject fareSet = fareSetList.getJSONObject(iFareSet); final String network = fareSet.optString("name", null); if (network != null) { final JSONArray fareList = fareSet.getJSONArray("fareL"); for (int iFare = 0; iFare < fareList.length(); iFare++) { final JSONObject jsonFare = fareList.getJSONObject(iFare); final String name = jsonFare.getString("name"); if (name.endsWith("- Jahreskarte") || name.endsWith("- Monatskarte")) continue; final Currency currency = Currency.getInstance(jsonFare.getString("cur")); final float price = jsonFare.getInt("prc") / 100f; if (name.startsWith("Vollpreis - ")) fares.add(new Fare(network, Fare.Type.ADULT, currency, price, name.substring(12), null)); else if (name.startsWith("Kind - ")) fares.add(new Fare(network, Fare.Type.CHILD, currency, price, name.substring(7), null)); } } } } final Trip trip = new Trip(null, tripFrom, tripTo, legs, fares, null, null); trips.add(trip); } final JsonContext context = new JsonContext(from, to, time, dep, products, res.getString("outCtxScrF"), res.getString("outCtxScrB")); return new QueryTripsResult(header, null, from, null, to, context, trips); } catch (final JSONException x) { throw new ParserException("cannot parse json: '" + page + "' on " + uri, x); } } private String wrapJsonApiRequest(final String meth, final String req, final boolean formatted) { return "{" // + "\"auth\":" + checkNotNull(jsonApiAuthorization) + "," // + "\"client\":" + checkNotNull(jsonApiClient) + "," // + "\"ver\":\"" + checkNotNull(jsonApiVersion) + "\",\"lang\":\"eng\"," // + "\"svcReqL\":[{\"cfg\":{\"polyEnc\":\"GPA\"},\"meth\":\"" + meth + "\",\"req\":" + req + "}]," // + "\"formatted\":" + formatted + "}"; } private String jsonLocation(final Location location) { if (location.type == LocationType.STATION && location.hasId()) return "{\"type\":\"S\",\"extId\":" + JSONObject.quote(location.id) + "}"; else if (location.type == LocationType.ADDRESS && location.hasId()) return "{\"type\":\"A\",\"lid\":" + JSONObject.quote(location.id) + "}"; else throw new IllegalArgumentException("cannot handle: " + location); } private CharSequence jsonDate(final Calendar time) { final int year = time.get(Calendar.YEAR); final int month = time.get(Calendar.MONTH) + 1; final int day = time.get(Calendar.DAY_OF_MONTH); return String.format(Locale.ENGLISH, "%04d%02d%02d", year, month, day); } private CharSequence jsonTime(final Calendar time) { final int hour = time.get(Calendar.HOUR_OF_DAY); final int minute = time.get(Calendar.MINUTE); return String.format(Locale.ENGLISH, "%02d%02d00", hour, minute); } private static final Pattern P_JSON_TIME = Pattern.compile("(\\d{2})?(\\d{2})(\\d{2})(\\d{2})"); private final Date parseJsonTime(final Calendar calendar, final Date baseDate, final CharSequence str) { if (str == null) return null; final Matcher m = P_JSON_TIME.matcher(str); if (m.matches()) { calendar.setTime(baseDate); if (m.group(1) != null) calendar.add(Calendar.DAY_OF_YEAR, Integer.parseInt(m.group(1))); calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(2))); calendar.set(Calendar.MINUTE, Integer.parseInt(m.group(3))); calendar.set(Calendar.SECOND, Integer.parseInt(m.group(4))); return calendar.getTime(); } throw new RuntimeException("cannot parse: '" + str + "'"); } private Stop parseJsonStop(final JSONObject json, final List locations, final Calendar c, final Date baseDate) throws JSONException { final Location location = locations.get(json.getInt("locX")); final boolean arrivalCancelled = json.optBoolean("aCncl", false); final Date plannedArrivalTime = parseJsonTime(c, baseDate, json.optString("aTimeS", null)); final Date predictedArrivalTime = parseJsonTime(c, baseDate, json.optString("aTimeR", null)); final Position plannedArrivalPosition = normalizePosition(json.optString("aPlatfS", null)); final Position predictedArrivalPosition = normalizePosition(json.optString("aPlatfR", null)); final boolean departureCancelled = json.optBoolean("dCncl", false); final Date plannedDepartureTime = parseJsonTime(c, baseDate, json.optString("dTimeS", null)); final Date predictedDepartureTime = parseJsonTime(c, baseDate, json.optString("dTimeR", null)); final Position plannedDeparturePosition = normalizePosition(json.optString("dPlatfS", null)); final Position predictedDeparturePosition = normalizePosition(json.optString("dPlatfR", null)); return new Stop(location, plannedArrivalTime, predictedArrivalTime, plannedArrivalPosition, predictedArrivalPosition, arrivalCancelled, plannedDepartureTime, predictedDepartureTime, plannedDeparturePosition, predictedDeparturePosition, departureCancelled); } private List parseRemList(final JSONArray remList) throws JSONException { final List remarks = new ArrayList(remList.length()); for (int i = 0; i < remList.length(); i++) { final JSONObject rem = remList.getJSONObject(i); final String code = rem.getString("code"); final String txt = rem.getString("txtN"); remarks.add(new String[] { code, txt }); } return remarks; } private List parseLocList(final JSONArray locList) throws JSONException { final List locations = new ArrayList(locList.length()); for (int iLoc = 0; iLoc < locList.length(); iLoc++) { final JSONObject loc = locList.getJSONObject(iLoc); final String type = loc.getString("type"); final JSONObject crd = loc.getJSONObject("crd"); if ("S".equals(type)) { final String[] placeAndName = splitStationName(loc.getString("name")); final int pCls = loc.optInt("pCls", -1); final Set products = pCls != -1 ? intToProducts(pCls) : null; final String id = normalizeStationId(loc.getString("extId")); locations.add(new Location(LocationType.STATION, id, crd.getInt("y"), crd.getInt("x"), placeAndName[0], placeAndName[1], products)); } else if ("P".equals(type)) { final String[] placeAndName = splitPOI(loc.getString("name")); final String id = normalizeStationId(loc.getString("extId")); locations.add(new Location(LocationType.POI, id, crd.getInt("y"), crd.getInt("x"), placeAndName[0], placeAndName[1])); } else if ("A".equals(type)) { final String[] placeAndName = splitAddress(loc.getString("name")); final String id = loc.getString("lid"); locations.add(new Location(LocationType.ADDRESS, id, crd.getInt("y"), crd.getInt("x"), placeAndName[0], placeAndName[1])); } else { throw new RuntimeException("Unknown type " + type + ": " + loc); } } return locations; } private List parseOpList(final JSONArray opList) throws JSONException { final List operators = new ArrayList(opList.length()); for (int i = 0; i < opList.length(); i++) { final JSONObject op = opList.getJSONObject(i); final String operator = op.getString("name"); operators.add(operator); } return operators; } private List parseProdList(final JSONArray prodList, final List operators) throws JSONException { final List lines = new ArrayList(prodList.length()); for (int iProd = 0; iProd < prodList.length(); iProd++) { final JSONObject prod = prodList.getJSONObject(iProd); final int oprIndex = prod.optInt("oprX", -1); final String operator = oprIndex != -1 ? operators.get(oprIndex) : null; final int cls = prod.optInt("cls", -1); final Product product = cls != -1 ? intToProduct(cls) : null; final String name = prod.getString("name"); final String normalizedName; if (product == Product.BUS && name.startsWith("Bus ")) normalizedName = name.substring(4); else if (product == Product.TRAM && name.startsWith("Tram ")) normalizedName = name.substring(5); else if (product == Product.SUBURBAN_TRAIN && name.startsWith("S ")) normalizedName = "S" + name.substring(2); else normalizedName = name; final Line line = new Line(null, operator, product, normalizedName, lineStyle(operator, product, normalizedName)); lines.add(line); } return lines; } public QueryTripsResult queryTrips(final Location from, final @Nullable Location via, final Location to, final Date date, final boolean dep, final @Nullable Set products, final @Nullable Optimize optimize, final @Nullable WalkSpeed walkSpeed, final @Nullable Accessibility accessibility, final @Nullable Set