Java tutorial
/* * Copyright 2010-2017 the original author or authors. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.schildbach.pte; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import javax.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import com.google.common.base.Charsets; import com.google.common.base.Strings; import de.schildbach.pte.dto.Departure; import de.schildbach.pte.dto.Line; import de.schildbach.pte.dto.Line.Attr; import de.schildbach.pte.dto.Location; import de.schildbach.pte.dto.LocationType; import de.schildbach.pte.dto.NearbyLocationsResult; import de.schildbach.pte.dto.Point; import de.schildbach.pte.dto.Position; import de.schildbach.pte.dto.Product; import de.schildbach.pte.dto.QueryDeparturesResult; import de.schildbach.pte.dto.QueryTripsContext; import de.schildbach.pte.dto.QueryTripsResult; import de.schildbach.pte.dto.ResultHeader; import de.schildbach.pte.dto.StationDepartures; import de.schildbach.pte.dto.Stop; import de.schildbach.pte.dto.SuggestLocationsResult; import de.schildbach.pte.dto.SuggestedLocation; import de.schildbach.pte.dto.Trip; import de.schildbach.pte.exception.ParserException; import de.schildbach.pte.exception.SessionExpiredException; import de.schildbach.pte.util.HttpClient; import de.schildbach.pte.util.LittleEndianDataInputStream; import de.schildbach.pte.util.ParserUtils; import de.schildbach.pte.util.StringReplaceReader; import de.schildbach.pte.util.XmlPullUtil; import okhttp3.HttpUrl; import okhttp3.ResponseBody; /** * @author Andreas Schildbach */ public abstract class AbstractHafasLegacyProvider extends AbstractHafasProvider { private static final String REQC_PROD = "hafas"; protected HttpUrl stationBoardEndpoint; protected HttpUrl getStopEndpoint; protected HttpUrl queryEndpoint; private @Nullable HttpUrl extXmlEndpoint = null; protected final String apiLanguage; private @Nullable String accessId = null; private @Nullable String clientType = "ANDROID"; private boolean jsonGetStopsUseWeight = true; private Charset jsonNearbyLocationsEncoding = Charsets.ISO_8859_1; private boolean dominantPlanStopTime = false; private boolean useIso8601 = false; private boolean stationBoardHasStationTable = true; private boolean stationBoardHasLocation = false; private boolean stationBoardCanDoEquivs = true; @SuppressWarnings("serial") private static class Context implements QueryTripsContext { public final String laterContext; public final String earlierContext; public final int sequence; public Context(final String laterContext, final String earlierContext, final int sequence) { this.laterContext = laterContext; this.earlierContext = earlierContext; this.sequence = sequence; } @Override public boolean canQueryLater() { return laterContext != null; } @Override public boolean canQueryEarlier() { return earlierContext != null; } } @SuppressWarnings("serial") public static class QueryTripsBinaryContext implements QueryTripsContext { public final String ident; public final int seqNr; public final String ld; public final int usedBufferSize; private final boolean canQueryMore; public QueryTripsBinaryContext(final String ident, final int seqNr, final String ld, final int usedBufferSize, final boolean canQueryMore) { this.ident = ident; this.seqNr = seqNr; this.ld = ld; this.usedBufferSize = usedBufferSize; this.canQueryMore = canQueryMore; } @Override public boolean canQueryLater() { return canQueryMore; } @Override public boolean canQueryEarlier() { return canQueryMore; } } public AbstractHafasLegacyProvider(final NetworkId network, final HttpUrl apiBase, final String apiLanguage, final Product[] productsMap) { super(network, productsMap); this.stationBoardEndpoint = apiBase.newBuilder().addPathSegment("stboard.exe").build(); this.getStopEndpoint = apiBase.newBuilder().addPathSegment("ajax-getstop.exe").build(); this.queryEndpoint = apiBase.newBuilder().addPathSegment("query.exe").build(); this.apiLanguage = apiLanguage; } protected AbstractHafasProvider setStationBoardEndpoint(final HttpUrl stationBoardEndpoint) { this.stationBoardEndpoint = stationBoardEndpoint; return this; } protected AbstractHafasProvider setGetStopEndpoint(final HttpUrl getStopEndpoint) { this.getStopEndpoint = getStopEndpoint; return this; } protected AbstractHafasProvider setQueryEndpoint(final HttpUrl queryEndpoint) { this.queryEndpoint = queryEndpoint; return this; } protected AbstractHafasProvider setExtXmlEndpoint(final HttpUrl extXmlEndpoint) { this.extXmlEndpoint = extXmlEndpoint; return this; } protected AbstractHafasProvider setAccessId(final String accessId) { this.accessId = accessId; return this; } protected AbstractHafasProvider setClientType(final String clientType) { this.clientType = clientType; return this; } protected AbstractHafasProvider setDominantPlanStopTime(final boolean dominantPlanStopTime) { this.dominantPlanStopTime = dominantPlanStopTime; return this; } protected AbstractHafasProvider setJsonGetStopsUseWeight(final boolean jsonGetStopsUseWeight) { this.jsonGetStopsUseWeight = jsonGetStopsUseWeight; return this; } protected AbstractHafasProvider setJsonNearbyLocationsEncoding(final Charset jsonNearbyLocationsEncoding) { this.jsonNearbyLocationsEncoding = jsonNearbyLocationsEncoding; return this; } protected AbstractHafasProvider setUseIso8601(final boolean useIso8601) { this.useIso8601 = useIso8601; return this; } protected AbstractHafasProvider setStationBoardHasStationTable(final boolean stationBoardHasStationTable) { this.stationBoardHasStationTable = stationBoardHasStationTable; return this; } protected AbstractHafasProvider setStationBoardHasLocation(final boolean stationBoardHasLocation) { this.stationBoardHasLocation = stationBoardHasLocation; return this; } protected AbstractHafasProvider setStationBoardCanDoEquivs(final boolean canDoEquivs) { this.stationBoardCanDoEquivs = canDoEquivs; return this; } private final String wrapReqC(final CharSequence request, final Charset encoding) { return "<?xml version=\"1.0\" encoding=\"" + (encoding != null ? encoding.name() : "iso-8859-1") + "\"?>" // + "<ReqC ver=\"1.1\" prod=\"" + REQC_PROD + "\" lang=\"DE\"" + (accessId != null ? " accessId=\"" + accessId + "\"" : "") + ">" // + request // + "</ReqC>"; } private final Location parseStation(final XmlPullParser pp) { final String type = pp.getName(); if ("Station".equals(type)) { final String name = XmlPullUtil.attr(pp, "name"); final String id = XmlPullUtil.attr(pp, "externalStationNr"); final int x = XmlPullUtil.intAttr(pp, "x"); final int y = XmlPullUtil.intAttr(pp, "y"); final String[] placeAndName = splitStationName(name); return new Location(LocationType.STATION, id, y, x, placeAndName[0], placeAndName[1]); } throw new IllegalStateException("cannot handle: " + type); } private static final Location parsePoi(final XmlPullParser pp) { final String type = pp.getName(); if ("Poi".equals(type)) { String name = XmlPullUtil.attr(pp, "name"); if (name.equals("unknown")) name = null; final int x = XmlPullUtil.intAttr(pp, "x"); final int y = XmlPullUtil.intAttr(pp, "y"); return new Location(LocationType.POI, null, y, x, null, name); } throw new IllegalStateException("cannot handle: " + type); } private final Location parseAddress(final XmlPullParser pp) { final String type = pp.getName(); if ("Address".equals(type)) { String name = XmlPullUtil.attr(pp, "name"); if (name.equals("unknown")) name = null; final int x = XmlPullUtil.intAttr(pp, "x"); final int y = XmlPullUtil.intAttr(pp, "y"); final String[] placeAndName = splitAddress(name); return new Location(LocationType.ADDRESS, null, y, x, placeAndName[0], placeAndName[1]); } throw new IllegalStateException("cannot handle: " + type); } private final Position parsePlatform(final XmlPullParser pp) throws XmlPullParserException, IOException { XmlPullUtil.enter(pp, "Platform"); final String platformText = XmlPullUtil.valueTag(pp, "Text"); XmlPullUtil.skipExit(pp, "Platform"); if (platformText == null || platformText.length() == 0) return null; else return parsePosition(platformText); } @Override public SuggestLocationsResult suggestLocations(final CharSequence constraint) throws IOException { final HttpUrl.Builder url = getStopEndpoint.newBuilder().addPathSegment(apiLanguage); appendJsonGetStopsParameters(url, checkNotNull(constraint), 0); return jsonGetStops(url.build()); } protected void appendJsonGetStopsParameters(final HttpUrl.Builder url, final CharSequence constraint, final int maxStops) { url.addQueryParameter("getstop", "1"); url.addQueryParameter("REQ0JourneyStopsS0A", "255"); url.addEncodedQueryParameter("REQ0JourneyStopsS0G", ParserUtils.urlEncode(constraint.toString() + "?", requestUrlEncoding)); if (maxStops > 0) url.addQueryParameter("REQ0JourneyStopsB", Integer.toString(maxStops)); url.addQueryParameter("js", "true"); } private static final Pattern P_AJAX_GET_STOPS_JSON = Pattern .compile("SLs\\.sls\\s*=\\s*(.*?);\\s*SLs\\.showSuggestion\\(\\);", Pattern.DOTALL); private static final Pattern P_AJAX_GET_STOPS_ID = Pattern.compile(".*?@L=0*(\\d+)@.*?"); protected final SuggestLocationsResult jsonGetStops(final HttpUrl url) throws IOException { final CharSequence page = httpClient.get(url); final Matcher mJson = P_AJAX_GET_STOPS_JSON.matcher(page); if (mJson.matches()) { final String json = mJson.group(1); final List<SuggestedLocation> locations = new ArrayList<>(); try { final JSONObject head = new JSONObject(json); final JSONArray aSuggestions = head.getJSONArray("suggestions"); for (int i = 0; i < aSuggestions.length(); i++) { final JSONObject suggestion = aSuggestions.optJSONObject(i); if (suggestion != null) { final int type = suggestion.getInt("type"); final String value = suggestion.getString("value"); final int lat = suggestion.optInt("ycoord"); final int lon = suggestion.optInt("xcoord"); final int weight = jsonGetStopsUseWeight ? suggestion.getInt("weight") : -i; String localId = null; final Matcher m = P_AJAX_GET_STOPS_ID.matcher(suggestion.getString("id")); if (m.matches()) localId = m.group(1); final Location location; if (type == 1) // station { final String[] placeAndName = splitStationName(value); location = new Location(LocationType.STATION, localId, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 2) // address { final String[] placeAndName = splitAddress(value); location = new Location(LocationType.ADDRESS, null, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 4) // poi { final String[] placeAndName = splitPOI(value); location = new Location(LocationType.POI, localId, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 128) // crossing { final String[] placeAndName = splitAddress(value); location = new Location(LocationType.ADDRESS, localId, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 87) { location = null; // don't know what to do } else { throw new IllegalStateException("unknown type " + type + " on " + url); } if (location != null) { final SuggestedLocation suggestedLocation = new SuggestedLocation(location, weight); locations.add(suggestedLocation); } } } return new SuggestLocationsResult(new ResultHeader(network, SERVER_PRODUCT), locations); } catch (final JSONException x) { throw new RuntimeException("cannot parse: '" + json + "' on " + url, x); } } else { throw new RuntimeException("cannot parse: '" + page + "' on " + url); } } @Override public QueryDeparturesResult queryDepartures(final String stationId, final @Nullable Date time, final int maxDepartures, final boolean equivs) throws IOException { checkNotNull(Strings.emptyToNull(stationId)); final HttpUrl.Builder url = stationBoardEndpoint.newBuilder().addPathSegment(apiLanguage); appendXmlStationBoardParameters(url, time, stationId, maxDepartures, equivs, "vs_java3"); return xmlStationBoard(url.build(), stationId); } protected void appendXmlStationBoardParameters(final HttpUrl.Builder url, final @Nullable Date time, final String stationId, final int maxDepartures, final boolean equivs, final @Nullable String styleSheet) { url.addQueryParameter("productsFilter", allProductsString().toString()); url.addQueryParameter("boardType", "dep"); if (stationBoardCanDoEquivs) url.addQueryParameter("disableEquivs", equivs ? "0" : "1"); url.addQueryParameter("maxJourneys", Integer.toString(maxDepartures > 0 ? maxDepartures : DEFAULT_MAX_DEPARTURES)); url.addEncodedQueryParameter("input", ParserUtils.urlEncode(normalizeStationId(stationId), requestUrlEncoding)); appendDateTimeParameters(url, time, "date", "time"); if (clientType != null) url.addEncodedQueryParameter("clientType", ParserUtils.urlEncode(clientType, requestUrlEncoding)); if (styleSheet != null) url.addQueryParameter("L", styleSheet); url.addQueryParameter("hcount", "0"); // prevents showing old departures url.addQueryParameter("start", "yes"); } protected void appendDateTimeParameters(final HttpUrl.Builder url, final Date time, final String dateParamName, final String timeParamName) { final Calendar c = new GregorianCalendar(timeZone); c.setTime(time); final int year = c.get(Calendar.YEAR); final int month = c.get(Calendar.MONTH) + 1; final int day = c.get(Calendar.DAY_OF_MONTH); final int hour = c.get(Calendar.HOUR_OF_DAY); final int minute = c.get(Calendar.MINUTE); url.addEncodedQueryParameter(dateParamName, ParserUtils.urlEncode( useIso8601 ? String.format(Locale.ENGLISH, "%04d-%02d-%02d", year, month, day) : String.format(Locale.ENGLISH, "%02d.%02d.%02d", day, month, year - 2000), requestUrlEncoding)); url.addEncodedQueryParameter(timeParamName, ParserUtils .urlEncode(String.format(Locale.ENGLISH, "%02d:%02d", hour, minute), requestUrlEncoding)); } private static final Pattern P_XML_STATION_BOARD_DELAY = Pattern .compile("(?:-|k\\.A\\.?|cancel|([+-]?\\s*\\d+))"); protected final QueryDeparturesResult xmlStationBoard(final HttpUrl url, final String stationId) throws IOException { final String normalizedStationId = normalizeStationId(stationId); final AtomicReference<QueryDeparturesResult> result = new AtomicReference<>(); httpClient.getInputStream(new HttpClient.Callback() { @Override public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException { StringReplaceReader reader = null; String firstChars = null; // work around unparsable XML reader = new StringReplaceReader(body.charStream(), " & ", " & "); reader.replace("<b>", " "); reader.replace("</b>", " "); reader.replace("<u>", " "); reader.replace("</u>", " "); reader.replace("<i>", " "); reader.replace("</i>", " "); reader.replace("<br />", " "); reader.replace(" ->", " →"); // right arrow reader.replace(" <-", " ←"); // left arrow reader.replace(" <> ", " ↔ "); // left-right arrow addCustomReplaces(reader); try { final XmlPullParserFactory factory = XmlPullParserFactory .newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); final XmlPullParser pp = factory.newPullParser(); pp.setInput(reader); pp.nextTag(); final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT); final QueryDeparturesResult r = new QueryDeparturesResult(header); if (XmlPullUtil.test(pp, "Err")) { final String code = XmlPullUtil.attr(pp, "code"); final String text = XmlPullUtil.attr(pp, "text"); if (code.equals("H730")) { result.set(new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION)); return; } if (code.equals("H890")) { r.stationDepartures.add( new StationDepartures(new Location(LocationType.STATION, normalizedStationId), Collections.<Departure>emptyList(), null)); result.set(r); return; } throw new IllegalArgumentException("unknown error " + code + ", " + text); } String[] stationPlaceAndName = null; if (stationBoardHasStationTable) XmlPullUtil.enter(pp, "StationTable"); else checkState(!XmlPullUtil.test(pp, "StationTable")); if (stationBoardHasLocation) { XmlPullUtil.require(pp, "St"); final String evaId = XmlPullUtil.attr(pp, "evaId"); if (evaId != null) { if (!evaId.equals(normalizedStationId)) throw new IllegalStateException( "stationId: " + normalizedStationId + ", evaId: " + evaId); final String name = XmlPullUtil.attr(pp, "name"); if (name != null) stationPlaceAndName = splitStationName(name.trim()); } XmlPullUtil.requireSkip(pp, "St"); } else { checkState(!XmlPullUtil.test(pp, "St")); } while (XmlPullUtil.test(pp, "Journey")) { final String fpTime = XmlPullUtil.attr(pp, "fpTime"); final String fpDate = XmlPullUtil.attr(pp, "fpDate"); final String delay = XmlPullUtil.attr(pp, "delay"); final String eDelay = XmlPullUtil.optAttr(pp, "e_delay", null); final String platform = XmlPullUtil.optAttr(pp, "platform", null); // TODO newpl final String targetLoc = XmlPullUtil.optAttr(pp, "targetLoc", null); // TODO hafasname final String dirnr = XmlPullUtil.optAttr(pp, "dirnr", null); final String prod = XmlPullUtil.attr(pp, "prod"); final String classStr = XmlPullUtil.optAttr(pp, "class", null); final String dir = XmlPullUtil.optAttr(pp, "dir", null); final String capacityStr = XmlPullUtil.optAttr(pp, "capacity", null); final String depStation = XmlPullUtil.optAttr(pp, "depStation", null); final String delayReason = XmlPullUtil.optAttr(pp, "delayReason", null); // TODO is_reachable // TODO disableTrainInfo // TODO lineFG/lineBG (ZVV) final String administration = normalizeLineAdministration( XmlPullUtil.optAttr(pp, "administration", null)); if (!"cancel".equals(delay) && !"cancel".equals(eDelay)) { final Calendar plannedTime = new GregorianCalendar(timeZone); plannedTime.clear(); parseXmlStationBoardDate(plannedTime, fpDate); parseXmlStationBoardTime(plannedTime, fpTime); 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<Attr> 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<Attr> attrs = prodLine.attrs; if (attrs != null) line = newLine(administration, prodLine.product, prodLine.label, null, attrs.toArray(new Line.Attr[0])); else line = newLine(administration, prodLine.product, prodLine.label, null); } final int[] capacity; if (capacityStr != null && !"0|0".equals(capacityStr)) { final String[] capacityParts = capacityStr.split("\\|"); capacity = new int[] { Integer.parseInt(capacityParts[0]), Integer.parseInt(capacityParts[1]) }; } else { capacity = null; } final String message; if (delayReason != null) { final String msg = delayReason.trim(); message = msg.length() > 0 ? msg : null; } else { message = null; } final Departure departure = new Departure(plannedTime.getTime(), predictedTime != null ? predictedTime.getTime() : null, line, position, destination, capacity, message); final Location location; if (!stationBoardCanDoEquivs || depStation == null) { location = new Location(LocationType.STATION, normalizedStationId, stationPlaceAndName != null ? stationPlaceAndName[0] : null, stationPlaceAndName != null ? stationPlaceAndName[1] : null); } else { final String[] depPlaceAndName = splitStationName(depStation); location = new Location(LocationType.STATION, null, depPlaceAndName[0], depPlaceAndName[1]); } StationDepartures stationDepartures = findStationDepartures(r.stationDepartures, location); if (stationDepartures == null) { stationDepartures = new StationDepartures(location, new ArrayList<Departure>(8), null); r.stationDepartures.add(stationDepartures); } stationDepartures.departures.add(departure); } XmlPullUtil.requireSkip(pp, "Journey"); } if (stationBoardHasStationTable) XmlPullUtil.exit(pp, "StationTable"); XmlPullUtil.requireEndDocument(pp); // sort departures for (final StationDepartures stationDepartures : r.stationDepartures) Collections.sort(stationDepartures.departures, Departure.TIME_COMPARATOR); result.set(r); } catch (final XmlPullParserException x) { throw new ParserException("cannot parse xml: " + firstChars, x); } } }, url); return result.get(); } protected void parseXmlStationBoardDate(final Calendar calendar, final String dateStr) { if (dateStr.length() == 8) ParserUtils.parseGermanDate(calendar, dateStr); else if (dateStr.length() == 10) ParserUtils.parseIsoDate(calendar, dateStr); else throw new IllegalStateException("cannot parse: '" + dateStr + "'"); } protected void parseXmlStationBoardTime(final Calendar calendar, final String timeStr) { ParserUtils.parseEuropeanTime(calendar, timeStr); } protected void addCustomReplaces(final StringReplaceReader reader) { } @Override public QueryTripsResult queryTrips(final Location from, final @Nullable Location via, final Location to, final Date date, final boolean dep, final @Nullable Set<Product> products, final @Nullable Optimize optimize, final @Nullable WalkSpeed walkSpeed, final @Nullable Accessibility accessibility, final @Nullable Set<Option> options) throws IOException { return queryTripsBinary(from, via, to, date, dep, products, walkSpeed, accessibility, options); } @Override public QueryTripsResult queryMoreTrips(final QueryTripsContext context, final boolean later) throws IOException { return queryMoreTripsBinary(context, later); } protected final QueryTripsResult queryTripsXml(Location from, @Nullable Location via, Location to, final Date date, final boolean dep, final @Nullable Set<Product> products, final @Nullable WalkSpeed walkSpeed, final @Nullable Accessibility accessibility, final @Nullable Set<Option> options) throws IOException { final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT); if (!from.isIdentified()) { final List<Location> locations = suggestLocations(from.name).getLocations(); if (locations.isEmpty()) return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS); // TODO if (locations.size() > 1) return new QueryTripsResult(header, locations, null, null); from = locations.get(0); } if (via != null && !via.isIdentified()) { final List<Location> locations = suggestLocations(via.name).getLocations(); if (locations.isEmpty()) return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS); // TODO if (locations.size() > 1) return new QueryTripsResult(header, null, locations, null); via = locations.get(0); } if (!to.isIdentified()) { final List<Location> locations = suggestLocations(to.name).getLocations(); if (locations.isEmpty()) return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS); // TODO if (locations.size() > 1) return new QueryTripsResult(header, null, null, locations); to = locations.get(0); } final Calendar c = new GregorianCalendar(timeZone); c.setTime(date); final CharSequence productsStr; if (products != null) productsStr = productsString(products); else productsStr = allProductsString(); final char bikeChar = (options != null && options.contains(Option.BIKE)) ? '1' : '0'; final StringBuilder conReq = new StringBuilder("<ConReq deliverPolyline=\"1\">"); conReq.append("<Start>").append(locationXml(from)); conReq.append("<Prod prod=\"").append(productsStr).append("\" bike=\"").append(bikeChar) .append("\" couchette=\"0\" direct=\"0\" sleeper=\"0\"/>"); conReq.append("</Start>"); if (via != null) { conReq.append("<Via>").append(locationXml(via)); if (via.type != LocationType.ADDRESS) conReq.append("<Prod prod=\"").append(productsStr).append("\" bike=\"").append(bikeChar) .append("\" couchette=\"0\" direct=\"0\" sleeper=\"0\"/>"); conReq.append("</Via>"); } conReq.append("<Dest>").append(locationXml(to)).append("</Dest>"); conReq.append("<ReqT a=\"").append(dep ? 0 : 1).append("\" date=\"") .append(String.format(Locale.ENGLISH, "%04d.%02d.%02d", c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH))) .append("\" time=\"").append(String.format(Locale.ENGLISH, "%02d:%02d", c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)) + "\"/>"); conReq.append("<RFlags"); // number of trips backwards conReq.append(" b=\"").append(0).append("\""); // number of trips forwards conReq.append(" f=\"").append(numTripsRequested).append("\""); // percentual extension of change time conReq.append(" chExtension=\"").append(walkSpeed == WalkSpeed.SLOW ? 50 : 0).append("\""); // TODO nrChanges: max number of changes conReq.append(" sMode=\"N\"/>"); conReq.append("</ConReq>"); return queryTripsXml(null, true, conReq, from, via, to); } protected final QueryTripsResult queryMoreTripsXml(final QueryTripsContext contextObj, final boolean later) throws IOException { final Context context = (Context) contextObj; final StringBuilder conScrReq = new StringBuilder("<ConScrReq scrDir=\"").append(later ? 'F' : 'B') .append("\" nrCons=\"").append(numTripsRequested).append("\">"); conScrReq.append("<ConResCtxt>").append(later ? context.laterContext : context.earlierContext) .append("</ConResCtxt>"); conScrReq.append("</ConScrReq>"); return queryTripsXml(context, later, conScrReq, null, null, null); } private QueryTripsResult queryTripsXml(final Context previousContext, final boolean later, final CharSequence conReq, final Location from, final @Nullable Location via, final Location to) throws IOException { final String request = wrapReqC(conReq, null); final HttpUrl endpoint = extXmlEndpoint != null ? extXmlEndpoint : queryEndpoint.newBuilder().addPathSegment(apiLanguage).build(); final AtomicReference<QueryTripsResult> result = new AtomicReference<>(); httpClient.getInputStream(new HttpClient.Callback() { @Override public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException { try { final XmlPullParserFactory factory = XmlPullParserFactory .newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); final XmlPullParser pp = factory.newPullParser(); pp.setInput(body.charStream()); XmlPullUtil.require(pp, "ResC"); final String product = XmlPullUtil.attr(pp, "prod").split(" ")[0]; final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT, product, null, 0, null); XmlPullUtil.enter(pp, "ResC"); if (XmlPullUtil.test(pp, "Err")) { final String code = XmlPullUtil.attr(pp, "code"); if (code.equals("I3")) { result.set(new QueryTripsResult(header, QueryTripsResult.Status.INVALID_DATE)); return; } if (code.equals("F1")) { result.set(new QueryTripsResult(header, QueryTripsResult.Status.SERVICE_DOWN)); return; } throw new IllegalStateException("error " + code + " " + XmlPullUtil.attr(pp, "text")); } XmlPullUtil.enter(pp, "ConRes"); if (XmlPullUtil.test(pp, "Err")) { final String code = XmlPullUtil.attr(pp, "code"); log.debug("Hafas error: {}", code); if (code.equals("K9260")) { // Unknown departure station result.set(new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_FROM)); return; } if (code.equals("K9280")) { // Unknown intermediate station result.set(new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_VIA)); return; } if (code.equals("K9300")) { // Unknown arrival station result.set(new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_TO)); return; } if (code.equals("K9360")) { // Date outside of the timetable period result.set(new QueryTripsResult(header, QueryTripsResult.Status.INVALID_DATE)); return; } if (code.equals("K9380")) { // Dep./Arr./Intermed. or equivalent station defined more than once result.set(new QueryTripsResult(header, QueryTripsResult.Status.TOO_CLOSE)); return; } if (code.equals("K895")) { // Departure/Arrival are too near result.set(new QueryTripsResult(header, QueryTripsResult.Status.TOO_CLOSE)); return; } if (code.equals("K9220")) { // Nearby to the given address stations could not be found result.set(new QueryTripsResult(header, QueryTripsResult.Status.UNRESOLVABLE_ADDRESS)); return; } if (code.equals("K9240")) { // Internal error result.set(new QueryTripsResult(header, QueryTripsResult.Status.SERVICE_DOWN)); return; } if (code.equals("K890")) { // No connections found result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } if (code.equals("K891")) { // No route found (try entering an intermediate station) result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } if (code.equals("K899")) { // An error occurred result.set(new QueryTripsResult(header, QueryTripsResult.Status.SERVICE_DOWN)); return; } if (code.equals("K1:890")) { // Unsuccessful or incomplete search (direction: forward) result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } if (code.equals("K2:890")) { // Unsuccessful or incomplete search (direction: backward) result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } throw new IllegalStateException("error " + code + " " + XmlPullUtil.attr(pp, "text")); } // H9380 Dep./Arr./Intermed. or equivalent stations defined more than once // H9360 Error in data field // H9320 The input is incorrect or incomplete // H9300 Unknown arrival station // H9280 Unknown intermediate station // H9260 Unknown departure station // H9250 Part inquiry interrupted // H9240 Unsuccessful search // H9230 An internal error occurred // H9220 Nearby to the given address stations could not be found // H900 Unsuccessful or incomplete search (timetable change) // H892 Inquiry too complex (try entering less intermediate stations) // H891 No route found (try entering an intermediate station) // H890 Unsuccessful search. // H500 Because of too many trains the connection is not complete // H460 One or more stops are passed through multiple times. // H455 Prolonged stop // H410 Display may be incomplete due to change of timetable // H390 Departure/Arrival replaced by an equivalent station // H895 Departure/Arrival are too near // H899 Unsuccessful or incomplete search (timetable change final String c = XmlPullUtil.optValueTag(pp, "ConResCtxt", null); final Context context; if (previousContext == null) context = new Context(c, c, 0); else if (later) context = new Context(c, previousContext.earlierContext, previousContext.sequence + 1); else context = new Context(previousContext.laterContext, c, previousContext.sequence + 1); XmlPullUtil.enter(pp, "ConnectionList"); final List<Trip> trips = new ArrayList<>(); while (XmlPullUtil.test(pp, "Connection")) { final String id = context.sequence + "/" + XmlPullUtil.attr(pp, "id"); XmlPullUtil.enter(pp, "Connection"); while (pp.getName().equals("RtStateList")) XmlPullUtil.next(pp); XmlPullUtil.enter(pp, "Overview"); final Calendar currentDate = new GregorianCalendar(timeZone); currentDate.clear(); parseDate(currentDate, XmlPullUtil.valueTag(pp, "Date")); XmlPullUtil.enter(pp, "Departure"); XmlPullUtil.enter(pp, "BasicStop"); while (pp.getName().equals("StAttrList")) XmlPullUtil.next(pp); final Location departureLocation = parseLocation(pp); XmlPullUtil.enter(pp, "Dep"); XmlPullUtil.skipExit(pp, "Dep"); final int[] capacity; if (XmlPullUtil.test(pp, "StopPrognosis")) { XmlPullUtil.enter(pp, "StopPrognosis"); XmlPullUtil.optSkip(pp, "Arr"); XmlPullUtil.optSkip(pp, "Dep"); XmlPullUtil.enter(pp, "Status"); XmlPullUtil.skipExit(pp, "Status"); final int capacity1st = Integer .parseInt(XmlPullUtil.optValueTag(pp, "Capacity1st", "0")); final int capacity2nd = Integer .parseInt(XmlPullUtil.optValueTag(pp, "Capacity2nd", "0")); if (capacity1st > 0 || capacity2nd > 0) capacity = new int[] { capacity1st, capacity2nd }; else capacity = null; XmlPullUtil.skipExit(pp, "StopPrognosis"); } else { capacity = null; } XmlPullUtil.skipExit(pp, "BasicStop"); XmlPullUtil.skipExit(pp, "Departure"); XmlPullUtil.enter(pp, "Arrival"); XmlPullUtil.enter(pp, "BasicStop"); while (pp.getName().equals("StAttrList")) XmlPullUtil.next(pp); final Location arrivalLocation = parseLocation(pp); XmlPullUtil.skipExit(pp, "BasicStop"); XmlPullUtil.skipExit(pp, "Arrival"); final int numTransfers = Integer.parseInt(XmlPullUtil.valueTag(pp, "Transfers")); XmlPullUtil.skipExit(pp, "Overview"); final List<Trip.Leg> legs = new ArrayList<>(4); XmlPullUtil.enter(pp, "ConSectionList"); final Calendar time = new GregorianCalendar(timeZone); while (XmlPullUtil.test(pp, "ConSection")) { XmlPullUtil.enter(pp, "ConSection"); // departure XmlPullUtil.enter(pp, "Departure"); XmlPullUtil.enter(pp, "BasicStop"); while (pp.getName().equals("StAttrList")) XmlPullUtil.next(pp); final Location sectionDepartureLocation = parseLocation(pp); XmlPullUtil.optSkip(pp, "Arr"); XmlPullUtil.enter(pp, "Dep"); time.setTimeInMillis(currentDate.getTimeInMillis()); parseTime(time, XmlPullUtil.valueTag(pp, "Time")); final Date departureTime = time.getTime(); final Position departurePos = parsePlatform(pp); XmlPullUtil.skipExit(pp, "Dep"); XmlPullUtil.skipExit(pp, "BasicStop"); XmlPullUtil.skipExit(pp, "Departure"); // journey final Line line; Location destination = null; List<Stop> intermediateStops = null; final String tag = pp.getName(); if (tag.equals("Journey")) { XmlPullUtil.enter(pp, "Journey"); while (pp.getName().equals("JHandle")) XmlPullUtil.next(pp); XmlPullUtil.enter(pp, "JourneyAttributeList"); boolean wheelchairAccess = false; String name = null; String category = null; String shortCategory = null; while (XmlPullUtil.test(pp, "JourneyAttribute")) { XmlPullUtil.enter(pp, "JourneyAttribute"); XmlPullUtil.require(pp, "Attribute"); final String attrName = XmlPullUtil.attr(pp, "type"); final String code = XmlPullUtil.optAttr(pp, "code", null); XmlPullUtil.enter(pp, "Attribute"); final Map<String, String> attributeVariants = parseAttributeVariants(pp); XmlPullUtil.skipExit(pp, "Attribute"); XmlPullUtil.skipExit(pp, "JourneyAttribute"); if ("bf".equals(code)) { wheelchairAccess = true; } else if ("NAME".equals(attrName)) { name = attributeVariants.get("NORMAL"); } else if ("CATEGORY".equals(attrName)) { shortCategory = attributeVariants.get("SHORT"); category = attributeVariants.get("NORMAL"); // longCategory = attributeVariants.get("LONG"); } else if ("DIRECTION".equals(attrName)) { final String[] destinationPlaceAndName = splitStationName( attributeVariants.get("NORMAL")); destination = new Location(LocationType.ANY, null, destinationPlaceAndName[0], destinationPlaceAndName[1]); } } XmlPullUtil.skipExit(pp, "JourneyAttributeList"); if (XmlPullUtil.test(pp, "PassList")) { intermediateStops = new LinkedList<>(); XmlPullUtil.enter(pp, "PassList"); while (XmlPullUtil.test(pp, "BasicStop")) { XmlPullUtil.enter(pp, "BasicStop"); while (XmlPullUtil.test(pp, "StAttrList")) XmlPullUtil.next(pp); final Location location = parseLocation(pp); if (location.id != sectionDepartureLocation.id) { Date stopArrivalTime = null; Date stopDepartureTime = null; Position stopArrivalPosition = null; Position stopDeparturePosition = null; if (XmlPullUtil.test(pp, "Arr")) { XmlPullUtil.enter(pp, "Arr"); time.setTimeInMillis(currentDate.getTimeInMillis()); parseTime(time, XmlPullUtil.valueTag(pp, "Time")); stopArrivalTime = time.getTime(); stopArrivalPosition = parsePlatform(pp); XmlPullUtil.skipExit(pp, "Arr"); } if (XmlPullUtil.test(pp, "Dep")) { XmlPullUtil.enter(pp, "Dep"); time.setTimeInMillis(currentDate.getTimeInMillis()); parseTime(time, XmlPullUtil.valueTag(pp, "Time")); stopDepartureTime = time.getTime(); stopDeparturePosition = parsePlatform(pp); XmlPullUtil.skipExit(pp, "Dep"); } intermediateStops.add(new Stop(location, stopArrivalTime, stopArrivalPosition, stopDepartureTime, stopDeparturePosition)); } XmlPullUtil.skipExit(pp, "BasicStop"); } XmlPullUtil.skipExit(pp, "PassList"); } XmlPullUtil.skipExit(pp, "Journey"); if (category == null) category = shortCategory; line = parseLine(category, name, wheelchairAccess); } else if (tag.equals("Walk") || tag.equals("Transfer") || tag.equals("GisRoute")) { XmlPullUtil.enter(pp); XmlPullUtil.enter(pp, "Duration"); XmlPullUtil.skipExit(pp, "Duration"); XmlPullUtil.skipExit(pp); line = null; } else { throw new IllegalStateException("cannot handle: " + pp.getName()); } // polyline final List<Point> path; if (XmlPullUtil.test(pp, "Polyline")) { path = new LinkedList<>(); XmlPullUtil.enter(pp, "Polyline"); while (XmlPullUtil.test(pp, "Point")) { final int x = XmlPullUtil.intAttr(pp, "x"); final int y = XmlPullUtil.intAttr(pp, "y"); path.add(new Point(y, x)); XmlPullUtil.next(pp); } XmlPullUtil.skipExit(pp, "Polyline"); } else { path = null; } // arrival XmlPullUtil.enter(pp, "Arrival"); XmlPullUtil.enter(pp, "BasicStop"); while (pp.getName().equals("StAttrList")) XmlPullUtil.next(pp); final Location sectionArrivalLocation = parseLocation(pp); XmlPullUtil.enter(pp, "Arr"); time.setTimeInMillis(currentDate.getTimeInMillis()); parseTime(time, XmlPullUtil.valueTag(pp, "Time")); final Date arrivalTime = time.getTime(); final Position arrivalPos = parsePlatform(pp); XmlPullUtil.skipExit(pp, "Arr"); XmlPullUtil.skipExit(pp, "BasicStop"); XmlPullUtil.skipExit(pp, "Arrival"); // remove last intermediate if (intermediateStops != null) if (!intermediateStops.isEmpty()) if (!intermediateStops.get(intermediateStops.size() - 1).location .equals(sectionArrivalLocation)) intermediateStops.remove(intermediateStops.size() - 1); XmlPullUtil.skipExit(pp, "ConSection"); if (line != null) { final Stop departure = new Stop(sectionDepartureLocation, true, departureTime, null, departurePos, null); final Stop arrival = new Stop(sectionArrivalLocation, false, arrivalTime, null, arrivalPos, null); legs.add(new Trip.Public(line, destination, departure, arrival, intermediateStops, path, null)); } else { if (legs.size() > 0 && legs.get(legs.size() - 1) instanceof Trip.Individual) { final Trip.Individual lastIndividualLeg = (Trip.Individual) legs .remove(legs.size() - 1); legs.add(new Trip.Individual(Trip.Individual.Type.WALK, lastIndividualLeg.departure, lastIndividualLeg.departureTime, sectionArrivalLocation, arrivalTime, null, 0)); } else { legs.add( new Trip.Individual(Trip.Individual.Type.WALK, sectionDepartureLocation, departureTime, sectionArrivalLocation, arrivalTime, null, 0)); } } } XmlPullUtil.skipExit(pp, "ConSectionList"); XmlPullUtil.skipExit(pp, "Connection"); trips.add(new Trip(id, departureLocation, arrivalLocation, legs, null, capacity, numTransfers)); } XmlPullUtil.skipExit(pp, "ConnectionList"); result.set(new QueryTripsResult(header, null, from, via, to, context, trips)); } catch (final XmlPullParserException x) { throw new ParserException("cannot parse xml: " + bodyPeek, x); } } }, endpoint, request, "application/xml", null); return result.get(); } private final Location parseLocation(final XmlPullParser pp) throws XmlPullParserException, IOException { final Location location; if (pp.getName().equals("Station")) location = parseStation(pp); else if (pp.getName().equals("Poi")) location = parsePoi(pp); else if (pp.getName().equals("Address")) location = parseAddress(pp); else throw new IllegalStateException("cannot parse: " + pp.getName()); XmlPullUtil.next(pp); return location; } private final Map<String, String> parseAttributeVariants(final XmlPullParser pp) throws XmlPullParserException, IOException { final Map<String, String> attributeVariants = new HashMap<>(); while (XmlPullUtil.test(pp, "AttributeVariant")) { final String type = XmlPullUtil.attr(pp, "type"); XmlPullUtil.enter(pp, "AttributeVariant"); final String value = XmlPullUtil.optValueTag(pp, "Text", null); XmlPullUtil.skipExit(pp, "AttributeVariant"); attributeVariants.put(type, value); } return attributeVariants; } private static final Pattern P_DATE = Pattern.compile("(\\d{4})(\\d{2})(\\d{2})"); private static final void parseDate(final Calendar calendar, final CharSequence str) { final Matcher m = P_DATE.matcher(str); if (!m.matches()) throw new RuntimeException("cannot parse: '" + str + "'"); calendar.set(Calendar.YEAR, Integer.parseInt(m.group(1))); calendar.set(Calendar.MONTH, Integer.parseInt(m.group(2)) - 1); calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(m.group(3))); } private static final Pattern P_TIME = Pattern.compile("(\\d+)d(\\d+):(\\d{2}):(\\d{2})"); private static void parseTime(final Calendar calendar, final CharSequence str) { final Matcher m = P_TIME.matcher(str); if (!m.matches()) throw new IllegalArgumentException("cannot parse: '" + str + "'"); 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))); calendar.set(Calendar.MILLISECOND, 0); calendar.add(Calendar.DAY_OF_MONTH, Integer.parseInt(m.group(1))); } private static final String locationXml(final Location location) { if (location.type == LocationType.STATION && location.hasId()) return "<Station externalId=\"" + normalizeStationId(location.id) + "\" />"; else if (location.type == LocationType.POI && location.hasLocation()) return "<Poi type=\"WGS84\" x=\"" + location.lon + "\" y=\"" + location.lat + "\" />"; else if (location.type == LocationType.ADDRESS && location.hasLocation()) return "<Address type=\"WGS84\" x=\"" + location.lon + "\" y=\"" + location.lat + "\" name=\"" + (location.place != null ? location.place + ", " : "") + location.name + "\" />"; else if (location.type == LocationType.COORD && location.hasLocation()) return "<Coord type=\"WGS84\" x=\"" + location.lon + "\" y=\"" + location.lat + "\" />"; else throw new IllegalArgumentException("cannot handle: " + location); } protected final String locationId(final Location location) { final StringBuilder id = new StringBuilder(); id.append("A=").append(locationType(location)); if (location.type == LocationType.STATION && location.hasId()) { id.append("@L=").append(normalizeStationId(location.id)); } else if (location.hasLocation()) { id.append("@X=").append(location.lon); id.append("@Y=").append(location.lat); id.append("@O=").append(location.name != null ? location.name : String.format(Locale.ENGLISH, "%.6f, %.6f", location.lat / 1E6, location.lon / 1E6)); } else if (location.name != null) { id.append("@G=").append(location.name); if (location.type != LocationType.ANY) id.append('!'); } return id.toString(); } protected static final int locationType(final Location location) { final LocationType type = location.type; if (type == LocationType.STATION) return 1; if (type == LocationType.POI) return 4; if (type == LocationType.COORD || (type == LocationType.ADDRESS && location.hasLocation())) return 16; if (type == LocationType.ADDRESS && location.name != null) return 2; if (type == LocationType.ANY) return 255; throw new IllegalArgumentException(location.type.toString()); } protected void appendQueryTripsBinaryParameters(final HttpUrl.Builder url, final Location from, final @Nullable Location via, final Location to, final Date date, final boolean dep, final @Nullable Set<Product> products, final @Nullable Accessibility accessibility, final @Nullable Set<Option> options) { url.addQueryParameter("start", "Suchen"); url.addEncodedQueryParameter("REQ0JourneyStopsS0ID", ParserUtils.urlEncode(locationId(from), requestUrlEncoding)); url.addEncodedQueryParameter("REQ0JourneyStopsZ0ID", ParserUtils.urlEncode(locationId(to), requestUrlEncoding)); if (via != null) { // workaround, for there does not seem to be a REQ0JourneyStops1.0ID parameter url.addQueryParameter("REQ0JourneyStops1.0A", Integer.toString(locationType(via))); if (via.type == LocationType.STATION && via.hasId()) { url.addQueryParameter("REQ0JourneyStops1.0L", via.id); } else if (via.hasLocation()) { url.addQueryParameter("REQ0JourneyStops1.0X", Integer.toString(via.lon)); url.addQueryParameter("REQ0JourneyStops1.0Y", Integer.toString(via.lat)); if (via.name == null) url.addQueryParameter("REQ0JourneyStops1.0O", String.format(Locale.ENGLISH, "%.6f, %.6f", via.lat / 1E6, via.lon / 1E6)); } else if (via.name != null) { url.addEncodedQueryParameter("REQ0JourneyStops1.0G", ParserUtils .urlEncode(via.name + (via.type != LocationType.ANY ? "!" : ""), requestUrlEncoding)); } } url.addQueryParameter("REQ0HafasSearchForw", dep ? "1" : "0"); appendDateTimeParameters(url, date, "REQ0JourneyDate", "REQ0JourneyTime"); final CharSequence productsStr; if (products != null) productsStr = productsString(products); else productsStr = allProductsString(); url.addQueryParameter("REQ0JourneyProduct_prod_list_1", productsStr.toString()); if (accessibility != null && accessibility != Accessibility.NEUTRAL) { if (accessibility == Accessibility.LIMITED) url.addQueryParameter("REQ0AddParamBaimprofile", "1"); else if (accessibility == Accessibility.BARRIER_FREE) url.addQueryParameter("REQ0AddParamBaimprofile", "0"); } if (options != null && options.contains(Option.BIKE)) url.addQueryParameter("REQ0JourneyProduct_opt3", "1"); appendCommonQueryTripsBinaryParameters(url); } protected void appendCommonQueryTripsBinaryParameters(final HttpUrl.Builder url) { url.addQueryParameter("h2g-direct", "11"); if (clientType != null) url.addEncodedQueryParameter("clientType", ParserUtils.urlEncode(clientType, requestUrlEncoding)); } private final static int QUERY_TRIPS_BINARY_BUFFER_SIZE = 384 * 1024; protected final QueryTripsResult queryTripsBinary(Location from, @Nullable Location via, Location to, final Date date, final boolean dep, final @Nullable Set<Product> products, final @Nullable WalkSpeed walkSpeed, final @Nullable Accessibility accessibility, final @Nullable Set<Option> options) throws IOException { final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT); if (!from.isIdentified()) { final List<Location> locations = suggestLocations(from.name).getLocations(); if (locations.isEmpty()) return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS); // TODO if (locations.size() > 1) return new QueryTripsResult(header, locations, null, null); from = locations.get(0); } if (via != null && !via.isIdentified()) { final List<Location> locations = suggestLocations(via.name).getLocations(); if (locations.isEmpty()) return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS); // TODO if (locations.size() > 1) return new QueryTripsResult(header, null, locations, null); via = locations.get(0); } if (!to.isIdentified()) { final List<Location> locations = suggestLocations(to.name).getLocations(); if (locations.isEmpty()) return new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS); // TODO if (locations.size() > 1) return new QueryTripsResult(header, null, null, locations); to = locations.get(0); } final HttpUrl.Builder url = queryEndpoint.newBuilder().addPathSegment(apiLanguage); appendQueryTripsBinaryParameters(url, from, via, to, date, dep, products, accessibility, options); return queryTripsBinary(url.build(), from, via, to, QUERY_TRIPS_BINARY_BUFFER_SIZE); } protected void appendQueryMoreTripsBinaryParameters(final HttpUrl.Builder url, final QueryTripsBinaryContext context, final boolean later) { url.addQueryParameter("seqnr", Integer.toString(context.seqNr)); url.addQueryParameter("ident", context.ident); if (context.ld != null) url.addQueryParameter("ld", context.ld); url.addQueryParameter("REQ0HafasScrollDir", later ? "1" : "2"); appendCommonQueryTripsBinaryParameters(url); } protected QueryTripsResult queryMoreTripsBinary(final QueryTripsContext contextObj, final boolean later) throws IOException { final QueryTripsBinaryContext context = (QueryTripsBinaryContext) contextObj; final HttpUrl.Builder url = queryEndpoint.newBuilder().addPathSegment(apiLanguage); appendQueryMoreTripsBinaryParameters(url, context, later); return queryTripsBinary(url.build(), null, null, null, QUERY_TRIPS_BINARY_BUFFER_SIZE + context.usedBufferSize); } private class CustomBufferedInputStream extends BufferedInputStream { public CustomBufferedInputStream(final InputStream in) { super(in); } public int getCount() { return count; } } private QueryTripsResult queryTripsBinary(final HttpUrl url, final Location from, final @Nullable Location via, final Location to, final int expectedBufferSize) throws IOException { /* * Many thanks to Malte Starostik and Robert, who helped a lot with analyzing this API! */ final AtomicReference<QueryTripsResult> result = new AtomicReference<>(); httpClient.getInputStream(new HttpClient.Callback() { @Override public void onSuccessful(final CharSequence bodyPeek, final ResponseBody body) throws IOException { final CustomBufferedInputStream bis = new CustomBufferedInputStream( new GZIPInputStream(body.byteStream())); // initialize input stream final LittleEndianDataInputStream is = new LittleEndianDataInputStream(bis); is.mark(expectedBufferSize); // quick check of status final int version = is.readShortReverse(); if (version != 6 && version != 5) throw new IllegalStateException("unknown version: " + version + ", first chars: " + bodyPeek); final ResultHeader header = new ResultHeader(network, SERVER_PRODUCT, Integer.toString(version), null, 0, null); // quick seek for pointers is.reset(); is.skipBytes(0x20); final int serviceDaysTablePtr = is.readIntReverse(); final int stringTablePtr = is.readIntReverse(); is.reset(); is.skipBytes(0x36); final int stationTablePtr = is.readIntReverse(); final int commentTablePtr = is.readIntReverse(); is.reset(); is.skipBytes(0x46); final int extensionHeaderPtr = is.readIntReverse(); // read strings final StringTable strings = new StringTable(is, stringTablePtr, serviceDaysTablePtr - stringTablePtr); is.reset(); is.skipBytes(extensionHeaderPtr); // read extension header final int extensionHeaderLength = is.readIntReverse(); if (extensionHeaderLength < 0x2c) throw new IllegalStateException("too short: " + extensionHeaderLength); is.skipBytes(12); final int errorCode = is.readShortReverse(); if (errorCode == 0) { // string encoding is.skipBytes(14); final Charset stringEncoding = Charset.forName(strings.read(is)); strings.setEncoding(stringEncoding); // read number of trips is.reset(); is.skipBytes(30); final int numTrips = is.readShortReverse(); if (numTrips == 0) { result.set(new QueryTripsResult(header, url.toString(), from, via, to, null, new LinkedList<Trip>())); return; } // read rest of header is.reset(); is.skipBytes(0x02); final Location resDeparture = location(is, strings); final Location resArrival = location(is, strings); is.skipBytes(10); final long resDate = date(is); /* final long resDate30 = */date(is); is.reset(); is.skipBytes(extensionHeaderPtr + 0x8); final int seqNr = is.readShortReverse(); if (seqNr == 0) throw new SessionExpiredException(); else if (seqNr < 0) throw new IllegalStateException("illegal sequence number: " + seqNr); final String requestId = strings.read(is); final int tripDetailsPtr = is.readIntReverse(); if (tripDetailsPtr == 0) throw new IllegalStateException("no connection details"); is.skipBytes(4); final int disruptionsPtr = is.readIntReverse(); is.skipBytes(10); final String ld = strings.read(is); final int attrsOffset = is.readIntReverse(); final int tripAttrsPtr; if (extensionHeaderLength >= 0x30) { if (extensionHeaderLength < 0x32) throw new IllegalArgumentException("too short: " + extensionHeaderLength); is.reset(); is.skipBytes(extensionHeaderPtr + 0x2c); tripAttrsPtr = is.readIntReverse(); } else { tripAttrsPtr = 0; } // determine stops offset is.reset(); is.skipBytes(tripDetailsPtr); final int tripDetailsVersion = is.readShortReverse(); if (tripDetailsVersion != 1) throw new IllegalStateException("unknown trip details version: " + tripDetailsVersion); is.skipBytes(0x02); final int tripDetailsIndexOffset = is.readShortReverse(); final int tripDetailsLegOffset = is.readShortReverse(); final int tripDetailsLegSize = is.readShortReverse(); final int stopsSize = is.readShortReverse(); final int stopsOffset = is.readShortReverse(); // read stations final StationTable stations = new StationTable(is, stationTablePtr, commentTablePtr - stationTablePtr, strings); // read comments final CommentTable comments = new CommentTable(is, commentTablePtr, tripDetailsPtr - commentTablePtr, strings); final List<Trip> trips = new ArrayList<>(numTrips); // read trips for (int iTrip = 0; iTrip < numTrips; iTrip++) { is.reset(); is.skipBytes(0x4a + iTrip * 12); final int serviceDaysTableOffset = is.readShortReverse(); final int legsOffset = is.readIntReverse(); final int numLegs = is.readShortReverse(); final int numChanges = is.readShortReverse(); /* final long duration = time(is, 0, 0); */is.readShortReverse(); is.reset(); is.skipBytes(serviceDaysTablePtr + serviceDaysTableOffset); /* final String serviceDaysText = */strings.read(is); final int serviceBitBase = is.readShortReverse(); final int serviceBitLength = is.readShortReverse(); int tripDayOffset = serviceBitBase * 8; for (int i = 0; i < serviceBitLength; i++) { int serviceBits = is.read(); if (serviceBits == 0) { tripDayOffset += 8; continue; } while ((serviceBits & 0x80) == 0) { serviceBits = serviceBits << 1; tripDayOffset++; } break; } is.reset(); is.skipBytes(tripDetailsPtr + tripDetailsIndexOffset + iTrip * 2); final int tripDetailsOffset = is.readShortReverse(); is.reset(); is.skipBytes(tripDetailsPtr + tripDetailsOffset); final int realtimeStatus = is.readShortReverse(); /* final short delay = */is.readShortReverse(); /* final int legIndex = */is.readShortReverse(); is.skipBytes(2); // 0xffff /* final int legStatus = */is.readShortReverse(); is.skipBytes(2); // 0x0000 String connectionId = null; if (tripAttrsPtr != 0) { is.reset(); is.skipBytes(tripAttrsPtr + iTrip * 2); final int tripAttrsIndex = is.readShortReverse(); is.reset(); is.skipBytes(attrsOffset + tripAttrsIndex * 4); while (true) { final String key = strings.read(is); if (key == null) break; else if (key.equals("ConnectionId")) connectionId = strings.read(is); else is.skipBytes(2); } } final List<Trip.Leg> legs = new ArrayList<>(numLegs); for (int iLegs = 0; iLegs < numLegs; iLegs++) { is.reset(); is.skipBytes(0x4a + legsOffset + iLegs * 20); final long plannedDepartureTime = time(is, resDate, tripDayOffset); final Location departureLocation = stations.read(is); final long plannedArrivalTime = time(is, resDate, tripDayOffset); final Location arrivalLocation = stations.read(is); final int type = is.readShortReverse(); final String lineName = strings.read(is); final Position plannedDeparturePosition = normalizePosition(strings.read(is)); final Position plannedArrivalPosition = normalizePosition(strings.read(is)); final int legAttrIndex = is.readShortReverse(); final List<Line.Attr> lineAttrs = new ArrayList<>(); String lineComment = null; boolean lineOnDemand = false; for (final String comment : comments.read(is)) { if (comment.startsWith("bf ")) { lineAttrs.add(Line.Attr.WHEEL_CHAIR_ACCESS); } else if (comment.startsWith("FA ") || comment.startsWith("FB ") || comment.startsWith("FR ")) { lineAttrs.add(Line.Attr.BICYCLE_CARRIAGE); } else if (comment.startsWith("$R ") || comment.startsWith("ga ") || comment.startsWith("ja ") || comment.startsWith("Vs ") || comment.startsWith("mu ") || comment.startsWith("mx ")) { lineOnDemand = true; lineComment = comment.substring(5); } } is.reset(); is.skipBytes(attrsOffset + legAttrIndex * 4); String directionStr = null; int lineClass = 0; String lineCategory = null; String routingType = null; String lineNetwork = null; while (true) { final String key = strings.read(is); if (key == null) break; else if (key.equals("Direction")) directionStr = strings.read(is); else if (key.equals("Class")) lineClass = Integer.parseInt(strings.read(is)); else if (key.equals("Category")) lineCategory = strings.read(is); // else if (key.equals("Operator")) // lineOperator = strings.read(is); else if (key.equals("GisRoutingType")) routingType = strings.read(is); else if (key.equals("AdminCode")) lineNetwork = normalizeLineAdministration(strings.read(is)); else is.skipBytes(2); } if (lineCategory == null && lineName != null) lineCategory = categoryFromName(lineName); is.reset(); is.skipBytes(tripDetailsPtr + tripDetailsOffset + tripDetailsLegOffset + iLegs * tripDetailsLegSize); if (tripDetailsLegSize != 16) throw new IllegalStateException( "unhandled trip details leg size: " + tripDetailsLegSize); final long predictedDepartureTime = time(is, resDate, tripDayOffset); final long predictedArrivalTime = time(is, resDate, tripDayOffset); final Position predictedDeparturePosition = normalizePosition(strings.read(is)); final Position predictedArrivalPosition = normalizePosition(strings.read(is)); final int bits = is.readShortReverse(); final boolean arrivalCancelled = (bits & 0x10) != 0; final boolean departureCancelled = (bits & 0x20) != 0; is.readShort(); final int firstStopIndex = is.readShortReverse(); final int numStops = is.readShortReverse(); is.reset(); is.skipBytes(disruptionsPtr); String disruptionText = null; if (is.readShortReverse() == 1) { is.reset(); is.skipBytes(disruptionsPtr + 2 + iTrip * 2); int disruptionsOffset = is.readShortReverse(); while (disruptionsOffset != 0) { is.reset(); is.skipBytes(disruptionsPtr + disruptionsOffset); strings.read(is); // "0" final int disruptionLeg = is.readShortReverse(); is.skipBytes(2); // bitmaske strings.read(is); // start of line strings.read(is); // end of line strings.read(is); // id /* final String disruptionTitle = */strings.read(is); final String disruptionShortText = ParserUtils.formatHtml(strings.read(is)); disruptionsOffset = is.readShortReverse(); // next if (iLegs == disruptionLeg) { final int disruptionAttrsIndex = is.readShortReverse(); is.reset(); is.skipBytes(attrsOffset + disruptionAttrsIndex * 4); while (true) { final String key = strings.read(is); if (key == null) break; else if (key.equals("Text")) disruptionText = ParserUtils.resolveEntities(strings.read(is)); else is.skipBytes(2); } if (disruptionShortText != null) disruptionText = disruptionShortText; } } } List<Stop> intermediateStops = null; if (numStops > 0) { is.reset(); is.skipBytes(tripDetailsPtr + stopsOffset + firstStopIndex * stopsSize); if (stopsSize != 26) throw new IllegalStateException("unhandled stops size: " + stopsSize); intermediateStops = new ArrayList<>(numStops); for (int iStop = 0; iStop < numStops; iStop++) { final long plannedStopDepartureTime = time(is, resDate, tripDayOffset); final Date plannedStopDepartureDate = plannedStopDepartureTime != 0 ? new Date(plannedStopDepartureTime) : null; final long plannedStopArrivalTime = time(is, resDate, tripDayOffset); final Date plannedStopArrivalDate = plannedStopArrivalTime != 0 ? new Date(plannedStopArrivalTime) : null; final Position plannedStopDeparturePosition = normalizePosition( strings.read(is)); final Position plannedStopArrivalPosition = normalizePosition(strings.read(is)); is.readInt(); final long predictedStopDepartureTime = time(is, resDate, tripDayOffset); final Date predictedStopDepartureDate = predictedStopDepartureTime != 0 ? new Date(predictedStopDepartureTime) : null; final long predictedStopArrivalTime = time(is, resDate, tripDayOffset); final Date predictedStopArrivalDate = predictedStopArrivalTime != 0 ? new Date(predictedStopArrivalTime) : null; final Position predictedStopDeparturePosition = normalizePosition( strings.read(is)); final Position predictedStopArrivalPosition = normalizePosition( strings.read(is)); final int stopBits = is.readShortReverse(); final boolean stopArrivalCancelled = (stopBits & 0x10) != 0; final boolean stopDepartureCancelled = (stopBits & 0x20) != 0; is.readShort(); final Location stopLocation = stations.read(is); final boolean validPredictedDate = !dominantPlanStopTime || (plannedStopArrivalDate != null && plannedStopDepartureDate != null); final Stop stop = new Stop(stopLocation, plannedStopArrivalDate, validPredictedDate ? predictedStopArrivalDate : null, plannedStopArrivalPosition, predictedStopArrivalPosition, stopArrivalCancelled, plannedStopDepartureDate, validPredictedDate ? predictedStopDepartureDate : null, plannedStopDeparturePosition, predictedStopDeparturePosition, stopDepartureCancelled); intermediateStops.add(stop); } } final Trip.Leg leg; if (type == 1 /* Fussweg */ || type == 3 /* Uebergang */ || type == 4 /* Uebergang */) { final Trip.Individual.Type individualType; if (routingType == null) individualType = type == 1 ? Trip.Individual.Type.WALK : Trip.Individual.Type.TRANSFER; else if ("FOOT".equals(routingType)) individualType = Trip.Individual.Type.WALK; else if ("BIKE".equals(routingType)) individualType = Trip.Individual.Type.BIKE; else if ("CAR".equals(routingType) || "P+R".equals(routingType)) individualType = Trip.Individual.Type.CAR; else throw new IllegalStateException("unknown routingType: " + routingType); final Date departureTime = new Date( predictedDepartureTime != 0 ? predictedDepartureTime : plannedDepartureTime); final Date arrivalTime = new Date( predictedArrivalTime != 0 ? predictedArrivalTime : plannedArrivalTime); final Trip.Leg lastLeg = legs.size() > 0 ? legs.get(legs.size() - 1) : null; if (lastLeg != null && lastLeg instanceof Trip.Individual && ((Trip.Individual) lastLeg).type == individualType) { final Trip.Individual lastIndividualLeg = (Trip.Individual) legs .remove(legs.size() - 1); leg = new Trip.Individual(individualType, lastIndividualLeg.departure, lastIndividualLeg.departureTime, arrivalLocation, arrivalTime, null, 0); } else { leg = new Trip.Individual(individualType, departureLocation, departureTime, arrivalLocation, arrivalTime, null, 0); } } else if (type == 2) { final Product lineProduct; if (lineOnDemand) lineProduct = Product.ON_DEMAND; else if (lineClass != 0) lineProduct = intToProduct(lineClass); else lineProduct = normalizeType(lineCategory); final Line line = newLine(lineNetwork, lineProduct, normalizeLineName(lineName), lineComment, lineAttrs.toArray(new Line.Attr[0])); final Location direction; if (directionStr != null) { final String[] directionPlaceAndName = splitStationName(directionStr); direction = new Location(LocationType.ANY, null, directionPlaceAndName[0], directionPlaceAndName[1]); } else { direction = null; } final Stop departure = new Stop(departureLocation, true, plannedDepartureTime != 0 ? new Date(plannedDepartureTime) : null, predictedDepartureTime != 0 ? new Date(predictedDepartureTime) : null, plannedDeparturePosition, predictedDeparturePosition, departureCancelled); final Stop arrival = new Stop(arrivalLocation, false, plannedArrivalTime != 0 ? new Date(plannedArrivalTime) : null, predictedArrivalTime != 0 ? new Date(predictedArrivalTime) : null, plannedArrivalPosition, predictedArrivalPosition, arrivalCancelled); leg = new Trip.Public(line, direction, departure, arrival, intermediateStops, null, disruptionText); } else { throw new IllegalStateException("unhandled type: " + type); } legs.add(leg); } final Trip trip = new Trip(connectionId, resDeparture, resArrival, legs, null, null, (int) numChanges); if (realtimeStatus != 2) // Verbindung fllt aus trips.add(trip); } // if result is only one single individual leg, don't query for more final boolean canQueryMore = trips.size() != 1 || trips.get(0).legs.size() != 1 || !(trips.get(0).legs.get(0) instanceof Trip.Individual); result.set(new QueryTripsResult(header, url.toString(), from, via, to, new QueryTripsBinaryContext(requestId, seqNr, ld, bis.getCount(), canQueryMore), trips)); } else { log.debug("Hafas error: {}", errorCode); if (errorCode == 1) { throw new SessionExpiredException(); } else if (errorCode == 2) { // F2: Your search results could not be stored internally. throw new SessionExpiredException(); } else if (errorCode == 8) { result.set(new QueryTripsResult(header, QueryTripsResult.Status.AMBIGUOUS)); return; } else if (errorCode == 13) { // IN13: Our booking system is currently being used by too many users at the same // time. result.set(new QueryTripsResult(header, QueryTripsResult.Status.SERVICE_DOWN)); return; } else if (errorCode == 19) { result.set(new QueryTripsResult(header, QueryTripsResult.Status.SERVICE_DOWN)); return; } else if (errorCode == 207) { // H207: Unfortunately your connection request can currently not be processed. result.set(new QueryTripsResult(header, QueryTripsResult.Status.SERVICE_DOWN)); return; } else if (errorCode == 887) { // H887: Your inquiry was too complex. Please try entering less intermediate stations. result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } else if (errorCode == 890) { // H890: No connections have been found that correspond to your request. It is // possible // that the requested service does not operate from or to the places you stated on the // requested date of travel. result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } else if (errorCode == 891) { // H891: Unfortunately there was no route found. Missing timetable data could be the // reason. result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } else if (errorCode == 892) { // H892: Your inquiry was too complex. Please try entering less intermediate stations. result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } else if (errorCode == 899) { // H899: there was an unsuccessful or incomplete search due to a timetable change. result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } else if (errorCode == 900) { // Unsuccessful or incomplete search (timetable change) result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } else if (errorCode == 9220) { // H9220: Nearby to the given address stations could not be found. result.set(new QueryTripsResult(header, QueryTripsResult.Status.UNRESOLVABLE_ADDRESS)); return; } else if (errorCode == 9240) { // H9240: Unfortunately there was no route found. Perhaps your start or destination is // not // served at all or with the selected means of transport on the required date/time. result.set(new QueryTripsResult(header, QueryTripsResult.Status.NO_TRIPS)); return; } else if (errorCode == 9260) { // H9260: Unknown departure station result.set(new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_FROM)); return; } else if (errorCode == 9280) { // H9280: Unknown intermediate station result.set(new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_VIA)); return; } else if (errorCode == 9300) { // H9300: Unknown arrival station result.set(new QueryTripsResult(header, QueryTripsResult.Status.UNKNOWN_TO)); return; } else if (errorCode == 9320) { // The input is incorrect or incomplete result.set(new QueryTripsResult(header, QueryTripsResult.Status.INVALID_DATE)); return; } else if (errorCode == 9360) { // H9360: Unfortunately your connection request can currently not be processed. result.set(new QueryTripsResult(header, QueryTripsResult.Status.INVALID_DATE)); return; } else if (errorCode == 9380) { // H9380: Dep./Arr./Intermed. or equivalent station defined more than once result.set(new QueryTripsResult(header, QueryTripsResult.Status.TOO_CLOSE)); return; } else if (errorCode == 895) { // H895: Departure/Arrival are too near result.set(new QueryTripsResult(header, QueryTripsResult.Status.TOO_CLOSE)); return; } else if (errorCode == 65535) { result.set(new QueryTripsResult(header, QueryTripsResult.Status.SERVICE_DOWN)); return; } else { throw new IllegalStateException("error " + errorCode + " on " + url); } } } }, url); return result.get(); } private Location location(final LittleEndianDataInputStream is, final StringTable strings) throws IOException { final String name = strings.read(is); is.readShort(); final int type = is.readShortReverse(); final int lon = is.readIntReverse(); final int lat = is.readIntReverse(); if (type == 1) { final String[] placeAndName = splitStationName(name); return new Location(LocationType.STATION, null, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 2) { final String[] placeAndName = splitAddress(name); return new Location(LocationType.ADDRESS, null, lat, lon, placeAndName[0], placeAndName[1]); } else if (type == 3) { final String[] placeAndName = splitPOI(name); return new Location(LocationType.POI, null, lat, lon, placeAndName[0], placeAndName[1]); } else { throw new IllegalStateException("unknown type: " + type + " " + name); } } private long date(final LittleEndianDataInputStream is) throws IOException { final int days = is.readShortReverse(); final Calendar date = new GregorianCalendar(timeZone); date.clear(); date.set(Calendar.YEAR, 1980); date.set(Calendar.DAY_OF_YEAR, days); return date.getTimeInMillis(); } private long time(final LittleEndianDataInputStream is, final long baseDate, final int dayOffset) throws IOException { final int value = is.readShortReverse(); if (value == 0xffff) return 0; final int hours = value / 100; final int minutes = value % 100; if (minutes < 0 || minutes > 60) throw new IllegalStateException("minutes out of range: " + minutes); final Calendar time = new GregorianCalendar(timeZone); time.setTimeInMillis(baseDate); if (time.get(Calendar.HOUR) != 0 || time.get(Calendar.MINUTE) != 0) throw new IllegalStateException("baseDate not on date boundary: " + baseDate); time.add(Calendar.DAY_OF_YEAR, dayOffset); time.set(Calendar.HOUR, hours); time.set(Calendar.MINUTE, minutes); return time.getTimeInMillis(); } private static class StringTable { private Charset encoding = Charsets.US_ASCII; private final byte[] table; public StringTable(final DataInputStream is, final int stringTablePtr, final int length) throws IOException { is.reset(); is.skipBytes(stringTablePtr); table = new byte[length]; is.readFully(table); } public void setEncoding(final Charset encoding) { this.encoding = encoding; } public String read(final LittleEndianDataInputStream is) throws IOException { final int pointer = is.readShortReverse(); if (pointer == 0) return null; if (pointer >= table.length) throw new IllegalStateException( "pointer " + pointer + " cannot exceed strings table size " + table.length); final InputStreamReader reader = new InputStreamReader( new ByteArrayInputStream(table, pointer, table.length - pointer), encoding); try { final StringBuilder builder = new StringBuilder(); int c; while ((c = reader.read()) != 0) builder.append((char) c); return builder.toString().trim(); } finally { reader.close(); } } } private static class CommentTable { private final StringTable strings; private final byte[] table; public CommentTable(final DataInputStream is, final int commentTablePtr, final int length, final StringTable strings) throws IOException { is.reset(); is.skipBytes(commentTablePtr); table = new byte[length]; is.readFully(table); this.strings = strings; } public String[] read(final LittleEndianDataInputStream is) throws IOException { final int pointer = is.readShortReverse(); if (pointer >= table.length) throw new IllegalStateException( "pointer " + pointer + " cannot exceed comments table size " + table.length); final LittleEndianDataInputStream commentsInputStream = new LittleEndianDataInputStream( new ByteArrayInputStream(table, pointer, table.length - pointer)); try { final int numComments = commentsInputStream.readShortReverse(); final String[] comments = new String[numComments]; for (int i = 0; i < numComments; i++) comments[i] = strings.read(commentsInputStream); return comments; } finally { commentsInputStream.close(); } } } private class StationTable { private final StringTable strings; private final byte[] table; public StationTable(final DataInputStream is, final int stationTablePtr, final int length, final StringTable strings) throws IOException { is.reset(); is.skipBytes(stationTablePtr); table = new byte[length]; is.readFully(table); this.strings = strings; } private Location read(final LittleEndianDataInputStream is) throws IOException { final int index = is.readShortReverse(); final int ptr = index * 14; if (ptr >= table.length) throw new IllegalStateException( "pointer " + ptr + " cannot exceed stations table size " + table.length); final LittleEndianDataInputStream stationInputStream = new LittleEndianDataInputStream( new ByteArrayInputStream(table, ptr, 14)); try { final String[] placeAndName = splitStationName(strings.read(stationInputStream)); final int id = stationInputStream.readIntReverse(); final int lon = stationInputStream.readIntReverse(); final int lat = stationInputStream.readIntReverse(); return new Location(LocationType.STATION, id != 0 ? Integer.toString(id) : null, lat, lon, placeAndName[0], placeAndName[1]); } finally { stationInputStream.close(); } } } @Override public NearbyLocationsResult queryNearbyLocations(final EnumSet<LocationType> types, final Location location, final int maxDistance, final int maxLocations) throws IOException { if (location.hasLocation()) return nearbyLocationsByCoordinate(types, location.lat, location.lon, maxDistance, maxLocations); else if (location.type == LocationType.STATION && location.hasId()) return nearbyStationsById(location.id, maxDistance); else throw new IllegalArgumentException("cannot handle: " + location); } protected final NearbyLocationsResult nearbyLocationsByCoordinate(final EnumSet<LocationType> types, final int lat, final int lon, final int maxDistance, final int maxLocations) throws IOException { if (types.contains(LocationType.STATION)) { final HttpUrl.Builder url = queryEndpoint.newBuilder().addPathSegment(apiLanguage + "y"); appendJsonNearbyStationsParameters(url, lat, lon, maxDistance, maxLocations); return jsonNearbyLocations(url.build()); } else if (types.contains(LocationType.POI)) { final HttpUrl.Builder url = queryEndpoint.newBuilder().addPathSegment(apiLanguage + "y"); appendJsonNearbyPOIsParameters(url, lat, lon, maxDistance, maxLocations); return jsonNearbyLocations(url.build()); } else { return new NearbyLocationsResult(null, Collections.<Location>emptyList()); } } protected NearbyLocationsResult nearbyStationsById(final String id, final int maxDistance) throws IOException { final HttpUrl.Builder url = stationBoardEndpoint.newBuilder().addPathSegment(apiLanguage); appendXmlNearbyStationsParameters(url, id); return xmlNearbyStations(url.build()); } protected final void appendXmlNearbyStationsParameters(final HttpUrl.Builder url, final String stationId) { url.addQueryParameter("productsFilter", allProductsString().toString()); url.addQueryParameter("boardType", "dep"); url.addEncodedQueryParameter("input", ParserUtils.urlEncode(normalizeStationId(stationId), requestUrlEncoding)); url.addQueryParameter("sTI", "1"); url.addQueryParameter("start", "yes"); url.addQueryParameter("hcount", "0"); url.addQueryParameter("L", "vs_java3"); if (clientType != null) url.addEncodedQueryParameter("clientType", ParserUtils.urlEncode(clientType, requestUrlEncoding)); } private static final Pattern P_XML_NEARBY_STATIONS_COARSE = Pattern.compile("\\G<\\s*St\\s*(.*?)/?>(?:\n|\\z)", Pattern.DOTALL); private static final Pattern P_XML_NEARBY_STATIONS_FINE = Pattern.compile("" // + "evaId=\"(\\d+)\"\\s*" // id + "name=\"([^\"]+)\".*?" // name + "(?:x=\"(\\d+)\"\\s*)?" // x + "(?:y=\"(\\d+)\"\\s*)?" // y , Pattern.DOTALL); private static final Pattern P_XML_NEARBY_STATIONS_MESSAGES = Pattern .compile("<Err code=\"([^\"]*)\" text=\"([^\"]*)\""); protected final NearbyLocationsResult xmlNearbyStations(final HttpUrl url) throws IOException { // scrape page final CharSequence page = httpClient.get(url); final List<Location> stations = new ArrayList<>(); // parse page final Matcher mMessage = P_XML_NEARBY_STATIONS_MESSAGES.matcher(page); if (mMessage.find()) { final String code = mMessage.group(1); final String text = mMessage.group(2); if (code.equals("H730")) // Your input is not valid return new NearbyLocationsResult(null, NearbyLocationsResult.Status.INVALID_ID); if (code.equals("H890")) // No trains in result return new NearbyLocationsResult(null, stations); throw new IllegalArgumentException("unknown error " + code + ", " + text); } final Matcher mCoarse = P_XML_NEARBY_STATIONS_COARSE.matcher(page); while (mCoarse.find()) { final Matcher mFine = P_XML_NEARBY_STATIONS_FINE.matcher(mCoarse.group(1)); if (mFine.matches()) { final String parsedId = mFine.group(1); final String parsedName = ParserUtils.resolveEntities(mFine.group(2)).trim(); final int parsedLon; final int parsedLat; if (mFine.group(3) != null && mFine.group(4) != null) { parsedLon = Integer.parseInt(mFine.group(3)); parsedLat = Integer.parseInt(mFine.group(4)); } else { parsedLon = 0; parsedLat = 0; } final String[] placeAndName = splitStationName(parsedName); stations.add(new Location(LocationType.STATION, parsedId, parsedLat, parsedLon, placeAndName[0], placeAndName[1])); } else { throw new IllegalArgumentException("cannot parse '" + mCoarse.group(1) + "' on " + url); } } return new NearbyLocationsResult(null, stations); } protected void appendJsonNearbyStationsParameters(final HttpUrl.Builder url, final int lat, final int lon, final int maxDistance, final int maxStations) { url.addQueryParameter("performLocating", "2"); url.addQueryParameter("tpl", "stop2json"); url.addQueryParameter("look_stopclass", Integer.toString(allProductsInt())); url.addQueryParameter("look_nv", "get_stopweight|yes"); // get_shortjson|yes // get_lines|yes // combinemode|2 // density|80 // get_stopweight|yes // get_infotext|yes url.addQueryParameter("look_x", Integer.toString(lon)); url.addQueryParameter("look_y", Integer.toString(lat)); url.addQueryParameter("look_maxno", Integer.toString(maxStations != 0 ? maxStations : 200)); url.addQueryParameter("look_maxdist", Integer.toString(maxDistance != 0 ? maxDistance : 5000)); } protected void appendJsonNearbyPOIsParameters(final HttpUrl.Builder url, final int lat, final int lon, final int maxDistance, final int maxStations) { url.addQueryParameter("performLocating", "4"); url.addQueryParameter("tpl", "poi2json"); url.addQueryParameter("look_pois", ""); // all categories url.addQueryParameter("look_x", Integer.toString(lon)); url.addQueryParameter("look_y", Integer.toString(lat)); url.addQueryParameter("look_maxno", Integer.toString(maxStations != 0 ? maxStations : 200)); url.addQueryParameter("look_maxdist", Integer.toString(maxDistance != 0 ? maxDistance : 5000)); } protected final NearbyLocationsResult jsonNearbyLocations(final HttpUrl url) throws IOException { final CharSequence page = httpClient.get(url); try { final JSONObject head = new JSONObject(page.toString()); final int error = head.getInt("error"); if (error == 0) { final List<Location> locations = new LinkedList<>(); final JSONArray aStops = head.optJSONArray("stops"); if (aStops != null) { final int nStops = aStops.length(); for (int i = 0; i < nStops; i++) { final JSONObject stop = aStops.optJSONObject(i); final String id = stop.getString("extId"); // final String name = ParserUtils.resolveEntities(stop.getString("name")); final String urlname = ParserUtils.urlDecode(stop.getString("urlname"), jsonNearbyLocationsEncoding); final int lat = stop.getInt("y"); final int lon = stop.getInt("x"); final int prodclass = stop.optInt("prodclass", -1); final int stopWeight = stop.optInt("stopweight", -1); if (stopWeight != 0) { final String[] placeAndName = splitStationName(urlname); final Set<Product> products = prodclass != -1 ? intToProducts(prodclass) : null; locations.add(new Location(LocationType.STATION, id, lat, lon, placeAndName[0], placeAndName[1], products)); } } } final JSONArray aPOIs = head.optJSONArray("pois"); if (aPOIs != null) { final int nPOIs = aPOIs.length(); for (int i = 0; i < nPOIs; i++) { final JSONObject poi = aPOIs.optJSONObject(i); final String id = poi.getString("extId"); // final String name = ParserUtils.resolveEntities(stop.getString("name")); final String urlname = ParserUtils.urlDecode(poi.getString("urlname"), jsonNearbyLocationsEncoding); final int lat = poi.getInt("y"); final int lon = poi.getInt("x"); final String[] placeAndName = splitPOI(urlname); locations.add( new Location(LocationType.POI, id, lat, lon, placeAndName[0], placeAndName[1])); } } return new NearbyLocationsResult(null, locations); } else { log.debug("Hafas error: {}", error); if (error == 2) return new NearbyLocationsResult(null, NearbyLocationsResult.Status.SERVICE_DOWN); else throw new RuntimeException("unknown error: " + error); } } catch (final JSONException x) { x.printStackTrace(); throw new RuntimeException("cannot parse: '" + page + "' on " + url, x); } } protected AbstractHafasProvider setHtmlNearbyStationsPattern(final Pattern htmlNearbyStationsPattern) { this.htmlNearbyStationsPattern = htmlNearbyStationsPattern; return this; } private Pattern htmlNearbyStationsPattern = Pattern.compile("<tr class=\"(zebra[^\"]*)\">(.*?)</tr>", Pattern.DOTALL); private static final Pattern P_HTML_NEARBY_FINE_COORDS = Pattern .compile("REQMapRoute0\\.Location0\\.X=(-?\\d+)&(?:amp;)?REQMapRoute0\\.Location0\\.Y=(-?\\d+)&"); private static final Pattern P_HTML_NEARBY_STATIONS_FINE_LOCATION = Pattern .compile("[\\?&;]input=(\\d+)&[^\"]*\">([^<]*)<"); private static final Pattern P_HTML_NEARBY_STATIONS_MESSAGES = Pattern .compile("(Ihre Eingabe kann nicht interpretiert werden)"); protected final NearbyLocationsResult htmlNearbyStations(final HttpUrl url) throws IOException { final List<Location> stations = new ArrayList<>(); final CharSequence page = httpClient.get(url); String oldZebra = null; final Matcher mCoarse = htmlNearbyStationsPattern.matcher(page); final Matcher mMessage = P_HTML_NEARBY_STATIONS_MESSAGES.matcher(page); if (mMessage.find()) { if (mMessage.group(1) != null) return new NearbyLocationsResult(null, NearbyLocationsResult.Status.INVALID_ID); } while (mCoarse.find()) { final String zebra = mCoarse.group(1); if (oldZebra != null && zebra.equals(oldZebra)) throw new IllegalArgumentException("missed row? last:" + zebra); else oldZebra = zebra; final Matcher mFineLocation = P_HTML_NEARBY_STATIONS_FINE_LOCATION.matcher(mCoarse.group(2)); if (mFineLocation.find()) { int parsedLon = 0; int parsedLat = 0; final String parsedId = mFineLocation.group(1); final String parsedName = ParserUtils.resolveEntities(mFineLocation.group(2)); final Matcher mFineCoords = P_HTML_NEARBY_FINE_COORDS.matcher(mCoarse.group(2)); if (mFineCoords.find()) { parsedLon = Integer.parseInt(mFineCoords.group(1)); parsedLat = Integer.parseInt(mFineCoords.group(2)); } final String[] placeAndName = splitStationName(parsedName); stations.add(new Location(LocationType.STATION, parsedId, parsedLat, parsedLon, placeAndName[0], placeAndName[1])); } else { throw new IllegalArgumentException("cannot parse '" + mCoarse.group(2) + "' on " + url); } } return new NearbyLocationsResult(null, stations); } private static final Pattern P_LINE_SBAHN = Pattern.compile("SN?\\d*"); private static final Pattern P_LINE_TRAM = Pattern.compile("STR\\w{0,5}"); private static final Pattern P_LINE_BUS = Pattern.compile("BUS\\w{0,5}"); private static final Pattern P_LINE_TAXI = Pattern.compile("TAX\\w{0,5}"); protected Product normalizeType(final String type) { final String ucType = type.toUpperCase(); // Intercity if ("EC".equals(ucType)) // EuroCity return Product.HIGH_SPEED_TRAIN; if ("EN".equals(ucType)) // EuroNight return Product.HIGH_SPEED_TRAIN; if ("D".equals(ucType)) // EuroNight, Sitzwagenabteil return Product.HIGH_SPEED_TRAIN; if ("EIC".equals(ucType)) // Ekspres InterCity, Polen return Product.HIGH_SPEED_TRAIN; if ("ICE".equals(ucType)) // InterCityExpress return Product.HIGH_SPEED_TRAIN; if ("IC".equals(ucType)) // InterCity return Product.HIGH_SPEED_TRAIN; if ("ICT".equals(ucType)) // InterCity return Product.HIGH_SPEED_TRAIN; if ("ICN".equals(ucType)) // InterCityNight return Product.HIGH_SPEED_TRAIN; if ("ICD".equals(ucType)) // Intercity direkt Amsterdam-Breda return Product.HIGH_SPEED_TRAIN; if ("CNL".equals(ucType)) // CityNightLine return Product.HIGH_SPEED_TRAIN; if ("MT".equals(ucType)) // Schnee-Express return Product.HIGH_SPEED_TRAIN; if ("OEC".equals(ucType)) // BB-EuroCity return Product.HIGH_SPEED_TRAIN; if ("OIC".equals(ucType)) // BB-InterCity return Product.HIGH_SPEED_TRAIN; if ("RJ".equals(ucType)) // RailJet, sterreichische Bundesbahnen return Product.HIGH_SPEED_TRAIN; if ("WB".equals(ucType)) // westbahn return Product.HIGH_SPEED_TRAIN; if ("THA".equals(ucType)) // Thalys return Product.HIGH_SPEED_TRAIN; if ("TGV".equals(ucType)) // Train Grande Vitesse return Product.HIGH_SPEED_TRAIN; if ("DNZ".equals(ucType)) // Nacht-Schnellzug return Product.HIGH_SPEED_TRAIN; if ("AIR".equals(ucType)) // Generic Flight return Product.HIGH_SPEED_TRAIN; if ("ECB".equals(ucType)) // EC, Verona-Mnchen return Product.HIGH_SPEED_TRAIN; if ("LYN".equals(ucType)) // Dnemark return Product.HIGH_SPEED_TRAIN; if ("NZ".equals(ucType)) // Schweden, Nacht return Product.HIGH_SPEED_TRAIN; if ("INZ".equals(ucType)) // Nacht return Product.HIGH_SPEED_TRAIN; if ("RHI".equals(ucType)) // ICE return Product.HIGH_SPEED_TRAIN; if ("RHT".equals(ucType)) // TGV return Product.HIGH_SPEED_TRAIN; if ("TGD".equals(ucType)) // TGV return Product.HIGH_SPEED_TRAIN; if ("IRX".equals(ucType)) // IC return Product.HIGH_SPEED_TRAIN; if ("ES".equals(ucType)) // Eurostar Italia return Product.HIGH_SPEED_TRAIN; if ("EST".equals(ucType)) // Eurostar Frankreich return Product.HIGH_SPEED_TRAIN; if ("EM".equals(ucType)) // Euromed, Barcelona-Alicante, Spanien return Product.HIGH_SPEED_TRAIN; if ("A".equals(ucType)) // Spain, Highspeed return Product.HIGH_SPEED_TRAIN; if ("AVE".equals(ucType)) // Alta Velocidad Espaola, Spanien return Product.HIGH_SPEED_TRAIN; if ("ARC".equals(ucType)) // Arco (Renfe), Spanien return Product.HIGH_SPEED_TRAIN; if ("ALS".equals(ucType)) // Alaris (Renfe), Spanien return Product.HIGH_SPEED_TRAIN; if ("ATR".equals(ucType)) // Altaria (Renfe), Spanien return Product.REGIONAL_TRAIN; if ("TAL".equals(ucType)) // Talgo, Spanien return Product.HIGH_SPEED_TRAIN; if ("TLG".equals(ucType)) // Spanien, Madrid return Product.HIGH_SPEED_TRAIN; if ("HOT".equals(ucType)) // Spanien, Nacht return Product.HIGH_SPEED_TRAIN; if ("X2".equals(ucType)) // X2000 Neigezug, Schweden return Product.HIGH_SPEED_TRAIN; if ("X".equals(ucType)) // InterConnex return Product.HIGH_SPEED_TRAIN; if ("FYR".equals(ucType)) // Fyra, Amsterdam-Schiphol-Rotterdam return Product.HIGH_SPEED_TRAIN; if ("FYRA".equals(ucType)) // Fyra, Amsterdam-Schiphol-Rotterdam return Product.HIGH_SPEED_TRAIN; if ("SC".equals(ucType)) // SuperCity, Tschechien return Product.HIGH_SPEED_TRAIN; if ("LE".equals(ucType)) // LEO Express, Prag return Product.HIGH_SPEED_TRAIN; if ("FLUG".equals(ucType)) return Product.HIGH_SPEED_TRAIN; if ("TLK".equals(ucType)) // Tanie Linie Kolejowe, Polen return Product.HIGH_SPEED_TRAIN; if ("PKP".equals(ucType)) // Polskie Koleje Pastwowe (Polnische Staatsbahnen) return Product.HIGH_SPEED_TRAIN; if ("EIP".equals(ucType)) // Express Intercity Premium return Product.HIGH_SPEED_TRAIN; if ("INT".equals(ucType)) // Zrich-Brssel - Budapest-Istanbul return Product.HIGH_SPEED_TRAIN; if ("HKX".equals(ucType)) // Hamburg-Koeln-Express return Product.HIGH_SPEED_TRAIN; if ("LOC".equals(ucType)) // Locomore return Product.HIGH_SPEED_TRAIN; // Regional if ("ZUG".equals(ucType)) // Generic Train return Product.REGIONAL_TRAIN; if ("R".equals(ucType)) // Generic Regional Train return Product.REGIONAL_TRAIN; if ("DPN".equals(ucType)) // Dritter Personen Nahverkehr return Product.REGIONAL_TRAIN; if ("RB".equals(ucType)) // RegionalBahn return Product.REGIONAL_TRAIN; if ("RE".equals(ucType)) // RegionalExpress return Product.REGIONAL_TRAIN; if ("ER".equals(ucType)) return Product.REGIONAL_TRAIN; if ("DB".equals(ucType)) return Product.REGIONAL_TRAIN; if ("IR".equals(ucType)) // Interregio return Product.REGIONAL_TRAIN; if ("IRE".equals(ucType)) // Interregio Express return Product.REGIONAL_TRAIN; if ("HEX".equals(ucType)) // Harz-Berlin-Express, Veolia return Product.REGIONAL_TRAIN; if ("WFB".equals(ucType)) // Westfalenbahn return Product.REGIONAL_TRAIN; if ("RT".equals(ucType)) // RegioTram return Product.REGIONAL_TRAIN; if ("REX".equals(ucType)) // RegionalExpress, sterreich return Product.REGIONAL_TRAIN; if ("OS".equals(ucType)) // Osobn vlak, Slovakia oder Osobn vlak, Czech Republic return Product.REGIONAL_TRAIN; if ("SP".equals(ucType)) // Spn vlak, Czech Republic return Product.REGIONAL_TRAIN; if ("RX".equals(ucType)) // Express, Czech Republic return Product.REGIONAL_TRAIN; if ("EZ".equals(ucType)) // BB ErlebnisBahn return Product.REGIONAL_TRAIN; if ("ARZ".equals(ucType)) // Auto-Reisezug Brig - Iselle di Trasquera return Product.REGIONAL_TRAIN; if ("OE".equals(ucType)) // Ostdeutsche Eisenbahn return Product.REGIONAL_TRAIN; if ("MR".equals(ucType)) // Mrkische Regionalbahn return Product.REGIONAL_TRAIN; if ("PE".equals(ucType)) // Prignitzer Eisenbahn GmbH return Product.REGIONAL_TRAIN; if ("NE".equals(ucType)) // NEB Betriebsgesellschaft mbH return Product.REGIONAL_TRAIN; if ("MRB".equals(ucType)) // Mitteldeutsche Regiobahn return Product.REGIONAL_TRAIN; if ("ERB".equals(ucType)) // eurobahn (Keolis Deutschland) return Product.REGIONAL_TRAIN; if ("HLB".equals(ucType)) // Hessische Landesbahn return Product.REGIONAL_TRAIN; if ("VIA".equals(ucType)) return Product.REGIONAL_TRAIN; if ("HSB".equals(ucType)) // Harzer Schmalspurbahnen return Product.REGIONAL_TRAIN; if ("OSB".equals(ucType)) // Ortenau-S-Bahn return Product.REGIONAL_TRAIN; if ("VBG".equals(ucType)) // Vogtlandbahn return Product.REGIONAL_TRAIN; if ("AKN".equals(ucType)) // AKN Eisenbahn AG return Product.REGIONAL_TRAIN; if ("OLA".equals(ucType)) // Ostseeland Verkehr return Product.REGIONAL_TRAIN; if ("UBB".equals(ucType)) // Usedomer Bderbahn return Product.REGIONAL_TRAIN; if ("PEG".equals(ucType)) // Prignitzer Eisenbahn return Product.REGIONAL_TRAIN; if ("NWB".equals(ucType)) // NordWestBahn return Product.REGIONAL_TRAIN; if ("CAN".equals(ucType)) // cantus Verkehrsgesellschaft return Product.REGIONAL_TRAIN; if ("BRB".equals(ucType)) // ABELLIO Rail return Product.REGIONAL_TRAIN; if ("SBB".equals(ucType)) // Schweizerische Bundesbahnen return Product.REGIONAL_TRAIN; if ("VEC".equals(ucType)) // vectus Verkehrsgesellschaft return Product.REGIONAL_TRAIN; if ("TLX".equals(ucType)) // Trilex (Vogtlandbahn) return Product.REGIONAL_TRAIN; if ("TL".equals(ucType)) // Trilex (Vogtlandbahn) return Product.REGIONAL_TRAIN; if ("HZL".equals(ucType)) // Hohenzollerische Landesbahn return Product.REGIONAL_TRAIN; if ("ABR".equals(ucType)) // Bayerische Regiobahn return Product.REGIONAL_TRAIN; if ("CB".equals(ucType)) // City Bahn Chemnitz return Product.REGIONAL_TRAIN; if ("WEG".equals(ucType)) // Wrttembergische Eisenbahn-Gesellschaft return Product.REGIONAL_TRAIN; if ("NEB".equals(ucType)) // Niederbarnimer Eisenbahn return Product.REGIONAL_TRAIN; if ("ME".equals(ucType)) // metronom Eisenbahngesellschaft return Product.REGIONAL_TRAIN; if ("MER".equals(ucType)) // metronom regional return Product.REGIONAL_TRAIN; if ("ALX".equals(ucType)) // Arriva-Lnderbahn-Express return Product.REGIONAL_TRAIN; if ("EB".equals(ucType)) // Erfurter Bahn return Product.REGIONAL_TRAIN; if ("EBX".equals(ucType)) // Erfurter Bahn return Product.REGIONAL_TRAIN; if ("VEN".equals(ucType)) // Rhenus Veniro return Product.REGIONAL_TRAIN; if ("BOB".equals(ucType)) // Bayerische Oberlandbahn return Product.REGIONAL_TRAIN; if ("SBS".equals(ucType)) // Stdtebahn Sachsen return Product.REGIONAL_TRAIN; if ("SES".equals(ucType)) // Stdtebahn Sachsen Express return Product.REGIONAL_TRAIN; if ("EVB".equals(ucType)) // Eisenbahnen und Verkehrsbetriebe Elbe-Weser return Product.REGIONAL_TRAIN; if ("STB".equals(ucType)) // Sd-Thringen-Bahn return Product.REGIONAL_TRAIN; if ("AG".equals(ucType)) // Ingolstadt-Landshut return Product.REGIONAL_TRAIN; if ("PRE".equals(ucType)) // Pressnitztalbahn return Product.REGIONAL_TRAIN; if ("DBG".equals(ucType)) // Dllnitzbahn GmbH return Product.REGIONAL_TRAIN; if ("SHB".equals(ucType)) // Schleswig-Holstein-Bahn return Product.REGIONAL_TRAIN; if ("NOB".equals(ucType)) // Nord-Ostsee-Bahn return Product.REGIONAL_TRAIN; if ("RTB".equals(ucType)) // Rurtalbahn return Product.REGIONAL_TRAIN; if ("BLB".equals(ucType)) // Berchtesgadener Land Bahn return Product.REGIONAL_TRAIN; if ("NBE".equals(ucType)) // Nordbahn Eisenbahngesellschaft return Product.REGIONAL_TRAIN; if ("SOE".equals(ucType)) // Schsisch-Oberlausitzer Eisenbahngesellschaft return Product.REGIONAL_TRAIN; if ("SDG".equals(ucType)) // Schsische Dampfeisenbahngesellschaft return Product.REGIONAL_TRAIN; if ("VE".equals(ucType)) // Lutherstadt Wittenberg return Product.REGIONAL_TRAIN; if ("DAB".equals(ucType)) // Daadetalbahn return Product.REGIONAL_TRAIN; if ("WTB".equals(ucType)) // Wutachtalbahn e.V. return Product.REGIONAL_TRAIN; if ("BE".equals(ucType)) // Grensland-Express return Product.REGIONAL_TRAIN; if ("ARR".equals(ucType)) // Ostfriesland return Product.REGIONAL_TRAIN; if ("HTB".equals(ucType)) // Hrseltalbahn return Product.REGIONAL_TRAIN; if ("FEG".equals(ucType)) // Freiberger Eisenbahngesellschaft return Product.REGIONAL_TRAIN; if ("NEG".equals(ucType)) // Norddeutsche Eisenbahngesellschaft Niebll return Product.REGIONAL_TRAIN; if ("RBG".equals(ucType)) // Regental Bahnbetriebs GmbH return Product.REGIONAL_TRAIN; if ("MBB".equals(ucType)) // Mecklenburgische Bderbahn Molli return Product.REGIONAL_TRAIN; if ("VEB".equals(ucType)) // Vulkan-Eifel-Bahn Betriebsgesellschaft return Product.REGIONAL_TRAIN; if ("LEO".equals(ucType)) // Chiemgauer Lokalbahn return Product.REGIONAL_TRAIN; if ("VX".equals(ucType)) // Vogtland Express return Product.REGIONAL_TRAIN; if ("MSB".equals(ucType)) // Mainschleifenbahn return Product.REGIONAL_TRAIN; if ("P".equals(ucType)) // Kasbachtalbahn return Product.REGIONAL_TRAIN; if ("BA".equals(ucType)) // chsle-Bahn Betriebsgesellschaft return Product.REGIONAL_TRAIN; if ("KTB".equals(ucType)) // Kandertalbahn return Product.REGIONAL_TRAIN; if ("ERX".equals(ucType)) // erixx return Product.REGIONAL_TRAIN; if ("ATZ".equals(ucType)) // Autotunnelzug return Product.REGIONAL_TRAIN; if ("ATB".equals(ucType)) // Autoschleuse Tauernbahn return Product.REGIONAL_TRAIN; if ("CAT".equals(ucType)) // City Airport Train return Product.REGIONAL_TRAIN; if ("EXTRA".equals(ucType) || "EXT".equals(ucType)) // Extrazug return Product.REGIONAL_TRAIN; if ("KD".equals(ucType)) // Koleje Dolnolskie (Niederschlesische Eisenbahn) return Product.REGIONAL_TRAIN; if ("KM".equals(ucType)) // Koleje Mazowieckie return Product.REGIONAL_TRAIN; if ("EX".equals(ucType)) // Polen return Product.REGIONAL_TRAIN; if ("PCC".equals(ucType)) // PCC Rail, Polen return Product.REGIONAL_TRAIN; if ("ZR".equals(ucType)) // ZSR (Slovakian Republic Railways) return Product.REGIONAL_TRAIN; if ("RNV".equals(ucType)) // Rhein-Neckar-Verkehr GmbH return Product.REGIONAL_TRAIN; if ("DWE".equals(ucType)) // Dessau-Wrlitzer Eisenbahn return Product.REGIONAL_TRAIN; if ("BKB".equals(ucType)) // Buckower Kleinbahn return Product.REGIONAL_TRAIN; if ("GEX".equals(ucType)) // Glacier Express return Product.REGIONAL_TRAIN; if ("M".equals(ucType)) // Meridian return Product.REGIONAL_TRAIN; if ("WBA".equals(ucType)) // Waldbahn return Product.REGIONAL_TRAIN; if ("BEX".equals(ucType)) // Bernina Express return Product.REGIONAL_TRAIN; if ("VAE".equals(ucType)) // Voralpen-Express return Product.REGIONAL_TRAIN; if ("OPB".equals(ucType)) // oberpfalzbahn return Product.REGIONAL_TRAIN; if ("OPX".equals(ucType)) // oberpfalz-express return Product.REGIONAL_TRAIN; if ("TER".equals(ucType)) // Transport express rgional return Product.REGIONAL_TRAIN; if ("ENO".equals(ucType)) return Product.REGIONAL_TRAIN; if ("THU".equals(ucType)) // Thurbo AG return Product.REGIONAL_TRAIN; if ("GW".equals(ucType)) // gwtr.cz return Product.REGIONAL_TRAIN; if ("SE".equals(ucType)) // ABELLIO Rail Mitteldeutschland GmbH return Product.REGIONAL_TRAIN; if ("UEX".equals(ucType)) // Slovenia return Product.REGIONAL_TRAIN; // Suburban Trains if (P_LINE_SBAHN.matcher(ucType).matches()) // Generic (Night) S-Bahn return Product.SUBURBAN_TRAIN; if ("S-BAHN".equals(ucType)) return Product.SUBURBAN_TRAIN; if ("BSB".equals(ucType)) // Breisgau S-Bahn return Product.SUBURBAN_TRAIN; if ("SWE".equals(ucType)) // Sdwestdeutsche Verkehrs-AG, Ortenau-S-Bahn return Product.SUBURBAN_TRAIN; if ("RER".equals(ucType)) // Rseau Express Rgional, Frankreich return Product.SUBURBAN_TRAIN; if ("WKD".equals(ucType)) // Warszawska Kolej Dojazdowa (Warsaw Suburban Railway) return Product.SUBURBAN_TRAIN; if ("SKM".equals(ucType)) // Szybka Kolej Miejska Tricity return Product.SUBURBAN_TRAIN; if ("SKW".equals(ucType)) // Szybka Kolej Miejska Warschau return Product.SUBURBAN_TRAIN; // Subway if ("U".equals(ucType)) // Generic U-Bahn return Product.SUBWAY; if ("MET".equals(ucType)) return Product.SUBWAY; if ("METRO".equals(ucType)) return Product.SUBWAY; // Tram if (P_LINE_TRAM.matcher(ucType).matches()) // Generic Tram return Product.TRAM; if ("NFT".equals(ucType)) // Niederflur-Tram return Product.TRAM; if ("TRAM".equals(ucType)) return Product.TRAM; if ("TRA".equals(ucType)) return Product.TRAM; if ("WLB".equals(ucType)) // Wiener Lokalbahnen return Product.TRAM; if ("STRWLB".equals(ucType)) // Wiener Lokalbahnen return Product.TRAM; if ("SCHW-B".equals(ucType)) // Schwebebahn, gilt als "Straenbahn besonderer Bauart" return Product.TRAM; // Bus if (P_LINE_BUS.matcher(ucType).matches()) // Generic Bus return Product.BUS; if ("NFB".equals(ucType)) // Niederflur-Bus return Product.BUS; if ("SEV".equals(ucType)) // Schienen-Ersatz-Verkehr return Product.BUS; if ("BUSSEV".equals(ucType)) // Schienen-Ersatz-Verkehr return Product.BUS; if ("BSV".equals(ucType)) // Bus SEV return Product.BUS; if ("FB".equals(ucType)) // Fernbus? Luxemburg-Saarbrcken return Product.BUS; if ("EXB".equals(ucType)) // Expressbus Mnchen-Prag? return Product.BUS; if ("ICB".equals(ucType)) // BB ICBus return Product.BUS; if ("TRO".equals(ucType)) // Trolleybus return Product.BUS; if ("RFB".equals(ucType)) // Rufbus return Product.BUS; if ("RUF".equals(ucType)) // Rufbus return Product.BUS; if (P_LINE_TAXI.matcher(ucType).matches()) // Generic Taxi return Product.BUS; if ("RFT".equals(ucType)) // Ruftaxi return Product.BUS; if ("LT".equals(ucType)) // Linien-Taxi return Product.BUS; if ("NB".equals(ucType)) // Nachtbus Zrich return Product.BUS; if ("POSTBUS".equals(ucType)) return Product.BUS; // Phone if ("RUFBUS".equals(ucType)) return Product.ON_DEMAND; if (ucType.startsWith("AST")) // Anruf-Sammel-Taxi return Product.ON_DEMAND; if (ucType.startsWith("ALT")) // Anruf-Linien-Taxi return Product.ON_DEMAND; if (ucType.startsWith("BUXI")) // Bus-Taxi (Schweiz) return Product.ON_DEMAND; if ("TB".equals(ucType)) // Taxi-Bus? return Product.ON_DEMAND; // Ferry if ("SCHIFF".equals(ucType)) return Product.FERRY; if ("FHRE".equals(ucType)) return Product.FERRY; if ("FH".equals(ucType)) return Product.FERRY; if ("FAE".equals(ucType)) return Product.FERRY; if ("SCH".equals(ucType)) // Schiff return Product.FERRY; if ("AS".equals(ucType)) // SyltShuttle return Product.FERRY; if ("AZS".equals(ucType)) // Autozug Sylt Shuttle return Product.FERRY; if ("KAT".equals(ucType)) // Katamaran, e.g. Friedrichshafen - Konstanz return Product.FERRY; if ("BAT".equals(ucType)) // Boots Anlege Terminal? return Product.FERRY; if ("BAV".equals(ucType)) // Boots Anlege? return Product.FERRY; // Cable Car if ("SEILBAHN".equals(ucType)) return Product.CABLECAR; if ("SB".equals(ucType)) // Seilbahn return Product.CABLECAR; if ("ZAHNR".equals(ucType)) // Zahnradbahn, u.a. Zugspitzbahn return Product.CABLECAR; if ("GB".equals(ucType)) // Gondelbahn return Product.CABLECAR; if ("LB".equals(ucType)) // Luftseilbahn return Product.CABLECAR; if ("FUN".equals(ucType)) // Funiculaire (Standseilbahn) return Product.CABLECAR; if ("SL".equals(ucType)) // Sessel-Lift return Product.CABLECAR; // Unknown product if ("E".equals(ucType)) return null; throw new IllegalStateException("cannot normalize type '" + type + "'"); } private static final Pattern P_NORMALIZE_LINE_NAME_BUS = Pattern.compile("bus\\s+(.*)", Pattern.CASE_INSENSITIVE); protected static final Pattern P_NORMALIZE_LINE = Pattern .compile("([A-Za-z/]+)[\\s-]*([^#]*).*"); protected String normalizeLineName(final String lineName) { final Matcher mBus = P_NORMALIZE_LINE_NAME_BUS.matcher(lineName); if (mBus.matches()) return mBus.group(1); final Matcher m = P_NORMALIZE_LINE.matcher(lineName); if (m.matches()) return m.group(1) + m.group(2); return lineName; } private static final Pattern P_NORMALIZE_LINE_ADMINISTRATION = Pattern.compile("([^_]*)_*"); private final String normalizeLineAdministration(final String administration) { if (administration == null) return null; final Matcher m = P_NORMALIZE_LINE_ADMINISTRATION.matcher(administration); if (m.find()) return m.group(1); else return administration; } private static final Pattern P_CATEGORY_FROM_NAME = Pattern .compile("([A-Za-z]+).*"); protected final String categoryFromName(final String lineName) { final Matcher m = P_CATEGORY_FROM_NAME.matcher(lineName); if (m.matches()) return m.group(1); else return lineName; } private static final Pattern P_NORMALIZE_LINE_BUS = Pattern.compile("(?:Bus|BUS)\\s*(.*)"); private static final Pattern P_NORMALIZE_LINE_TRAM = Pattern.compile("(?:Tram|Tra|Str|STR)\\s*(.*)"); protected Line parseLine(final String type, final String normalizedName, final boolean wheelchairAccess) { if (normalizedName != null) { final Matcher mBus = P_NORMALIZE_LINE_BUS.matcher(normalizedName); if (mBus.matches()) return newLine(Product.BUS, mBus.group(1), null); final Matcher mTram = P_NORMALIZE_LINE_TRAM.matcher(normalizedName); if (mTram.matches()) return newLine(Product.TRAM, mTram.group(1), null); } final Product normalizedType = normalizeType(type); final Line.Attr[] attrs; if (wheelchairAccess) attrs = new Line.Attr[] { Line.Attr.WHEEL_CHAIR_ACCESS }; else attrs = new Line.Attr[0]; if (normalizedName != null) { final Matcher m = P_NORMALIZE_LINE.matcher(normalizedName); final String strippedLine = m.matches() ? m.group(1) + m.group(2) : normalizedName; return newLine(normalizedType, strippedLine, null, attrs); } else { return newLine(normalizedType, null, null, attrs); } } protected static final Pattern P_NORMALIZE_LINE_AND_TYPE = Pattern.compile("([^#]*)#(.*)"); private static final Pattern P_NORMALIZE_LINE_NUMBER = Pattern.compile("\\d{2,5}"); protected static final Pattern P_LINE_RUSSIA = Pattern.compile( "\\d{3}(?:AJ|BJ|CJ|DJ|EJ|FJ|GJ|IJ|KJ|LJ|NJ|MJ|OJ|RJ|SJ|TJ|UJ|VJ|ZJ|CH|KH|ZH|EI|JA|JI|MZ|SH|SZ|PC|Y)"); protected Line parseLineAndType(final String lineAndType) { final Matcher mLineAndType = P_NORMALIZE_LINE_AND_TYPE.matcher(lineAndType); if (mLineAndType.matches()) { final String number = mLineAndType.group(1); final String type = mLineAndType.group(2); if (type.length() == 0) { if (number.length() == 0) return newLine(null, null, null); if (P_NORMALIZE_LINE_NUMBER.matcher(number).matches()) return newLine(null, number, null); if (P_LINE_RUSSIA.matcher(number).matches()) return newLine(Product.REGIONAL_TRAIN, number, null); } else { final Product normalizedType = normalizeType(type); if (normalizedType != null) { if (normalizedType == Product.BUS) { final Matcher mBus = P_NORMALIZE_LINE_BUS.matcher(number); if (mBus.matches()) return newLine(Product.BUS, mBus.group(1), null); } if (normalizedType == Product.TRAM) { final Matcher mTram = P_NORMALIZE_LINE_TRAM.matcher(number); if (mTram.matches()) return newLine(Product.TRAM, mTram.group(1), null); } } return newLine(normalizedType, number.replaceAll("\\s+", ""), null); } throw new IllegalStateException( "cannot normalize type '" + type + "' number '" + number + "' line#type '" + lineAndType + "'"); } throw new IllegalStateException("cannot normalize line#type '" + lineAndType + "'"); } protected Line newLine(final Product product, final String normalizedName, final String comment, final Line.Attr... attrs) { return newLine(null, product, normalizedName, comment, attrs); } protected Line newLine(final String network, final Product product, final String normalizedName, final String comment, final Line.Attr... attrs) { if (attrs.length == 0) { return new Line(null, network, product, normalizedName, lineStyle(network, product, normalizedName), comment); } else { final Set<Line.Attr> attrSet = new HashSet<>(); for (final Line.Attr attr : attrs) attrSet.add(attr); return new Line(null, network, product, normalizedName, lineStyle(network, product, normalizedName), attrSet, comment); } } }