diff --git a/src/de/schildbach/pte/BahnProvider.java b/src/de/schildbach/pte/BahnProvider.java new file mode 100644 index 00000000..154a9c94 --- /dev/null +++ b/src/de/schildbach/pte/BahnProvider.java @@ -0,0 +1,761 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Andreas Schildbach + */ +public final class BahnProvider implements NetworkProvider +{ + public static final String NETWORK_ID = "mobile.bahn.de"; + + private static final long PARSER_DAY_ROLLOVER_THRESHOLD_MS = 12 * 60 * 60 * 1000; + + public boolean hasCapabilities(Capability... capabilities) + { + return true; + } + + private static final String NAME_URL = "http://mobile.bahn.de/bin/mobil/bhftafel.exe/dox?input="; + private static final Pattern P_SINGLE_NAME = Pattern + .compile(".*.*", Pattern.DOTALL); + private static final Pattern P_MULTI_NAME = Pattern.compile("", Pattern.DOTALL); + + public List autoCompleteStationName(CharSequence constraint) throws IOException + { + final CharSequence page = ParserUtils.scrape(NAME_URL + ParserUtils.urlEncode(constraint.toString())); + + final List names = new ArrayList(); + + final Matcher mSingle = P_SINGLE_NAME.matcher(page); + if (mSingle.matches()) + { + names.add(ParserUtils.resolveEntities(mSingle.group(1))); + } + else + { + final Matcher mMulti = P_MULTI_NAME.matcher(page); + while (mMulti.find()) + names.add(ParserUtils.resolveEntities(mMulti.group(1))); + } + + return names; + } + + private final static Pattern P_NEARBY_STATIONS = Pattern + .compile("(.+?)"); + + public List nearbyStations(final double lat, final double lon, final int maxDistance, final int maxStations) throws IOException + { + final String url = "http://mobile.bahn.de/bin/mobil/query.exe/dox" + "?performLocating=2&tpl=stopsnear&look_maxdist=" + + (maxDistance > 0 ? maxDistance : 5000) + "&look_stopclass=1023" + "&look_x=" + latLonToInt(lon) + "&look_y=" + latLonToInt(lat); + final CharSequence page = ParserUtils.scrape(url); + + final List stations = new ArrayList(); + + final Matcher m = P_NEARBY_STATIONS.matcher(page); + while (m.find()) + { + final int sId = Integer.parseInt(m.group(3)); + + final double sLon = latLonToDouble(Integer.parseInt(m.group(1))); + final double sLat = latLonToDouble(Integer.parseInt(m.group(2))); + final int sDist = Integer.parseInt(m.group(4)); + final String sName = ParserUtils.resolveEntities(m.group(5).trim()); + + final Station station = new Station(sId, sName, sLat, sLon, sDist, null, null); + stations.add(station); + } + + if (maxStations == 0 || maxStations >= stations.size()) + return stations; + else + return stations.subList(0, maxStations); + } + + private static int latLonToInt(double value) + { + return (int) (value * 1000000); + } + + private static double latLonToDouble(int value) + { + return (double) value / 1000000; + } + + public String connectionsQueryUri(final String from, final String via, final String to, final Date date, final boolean dep) + { + final DateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yy"); + final DateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm"); + final StringBuilder uri = new StringBuilder(); + + uri.append("http://mobile.bahn.de/bin/mobil/query.exe/dox"); + uri.append("?REQ0HafasOptimize1=0:1"); + uri.append("&REQ0JourneyStopsS0G=").append(ParserUtils.urlEncode(from)); + uri.append("&REQ0JourneyStopsS0A=1"); + uri.append("&REQ0JourneyStopsS0ALocation=1"); + uri.append("&REQ0JourneyStopsS0AAddress=1"); + if (via != null) + { + uri.append("&REQ0JourneyStops1.0G=").append(ParserUtils.urlEncode(via)); + uri.append("&REQ0JourneyStops1.0A=1"); + uri.append("&REQ0JourneyStops1.0ALocation=1"); + uri.append("&REQ0JourneyStops1.0AAddress=1"); + } + uri.append("&REQ0JourneyStopsZ0G=").append(ParserUtils.urlEncode(to)); + uri.append("&REQ0JourneyStopsZ0A=1"); + uri.append("&REQ0JourneyStopsZ0ALocation=1"); + uri.append("&REQ0JourneyStopsZ0AAddress=1"); + uri.append("&REQ0HafasSearchForw=").append(dep ? "1" : "0"); + uri.append("&REQ0JourneyDate=").append(ParserUtils.urlEncode(DATE_FORMAT.format(date))); + uri.append("&REQ0JourneyTime=").append(ParserUtils.urlEncode(TIME_FORMAT.format(date))); + uri.append("&REQ0Tariff_Class=2"); + uri.append("&REQ0Tariff_TravellerAge.1=35"); + uri.append("&REQ0Tariff_TravellerReductionClass.1=0"); + uri.append("&existOptimizePrice=1"); + uri.append("&existProductNahverkehr=yes"); + uri.append("&start=Suchen"); + + return uri.toString(); + } + + private static final Pattern P_PRE_ADDRESS = Pattern.compile( + "", Pattern.DOTALL); + private static final Pattern P_ADDRESSES = Pattern.compile("\\s*(.*?)\\s*", Pattern.DOTALL); + private static final Pattern P_CHECK_CONNECTIONS_ERROR = Pattern + .compile("(?:(zu dicht beieinander|mehrfach vorhanden oder identisch)|(leider konnte zu Ihrer Anfrage keine Verbindung gefunden werden))"); + + public CheckConnectionsQueryResult checkConnectionsQuery(final String uri) throws IOException + { + final CharSequence page = ParserUtils.scrape(uri); + + final Matcher mError = P_CHECK_CONNECTIONS_ERROR.matcher(page); + if (mError.find()) + { + if (mError.group(1) != null) + return CheckConnectionsQueryResult.TOO_CLOSE; + if (mError.group(2) != null) + return CheckConnectionsQueryResult.NO_CONNECTIONS; + } + + List fromAddresses = null; + List viaAddresses = null; + List toAddresses = null; + + final Matcher mPreAddress = P_PRE_ADDRESS.matcher(page); + while (mPreAddress.find()) + { + final String type = mPreAddress.group(1); + final String options = mPreAddress.group(2); + + final Matcher mAddresses = P_ADDRESSES.matcher(options); + final List addresses = new ArrayList(); + while (mAddresses.find()) + { + final String address = ParserUtils.resolveEntities(mAddresses.group(1)).trim(); + if (!addresses.contains(address)) + addresses.add(address); + } + + if (type.equals("REQ0JourneyStopsS0K")) + fromAddresses = addresses; + else if (type.equals("REQ0JourneyStopsZ0K")) + toAddresses = addresses; + else if (type.equals("REQ0JourneyStops1.0K")) + viaAddresses = addresses; + else + throw new IOException(type); + } + + if (fromAddresses != null || viaAddresses != null || toAddresses != null) + { + return new CheckConnectionsQueryResult(CheckConnectionsQueryResult.Status.AMBIGUOUS, fromAddresses, viaAddresses, toAddresses); + } + else + { + return CheckConnectionsQueryResult.OK; + } + } + + private static final Pattern P_CONNECTIONS_HEAD = Pattern.compile(".*" // + + "von: (.*?).*?" // from + + "nach: (.*?).*?" // to + + "Datum: .., (.*?).*?" // currentDate + + "(?:.*?Früher.*?)?" // linkEarlier + + "(?:.*?Später.*?)?" // linkLater + , Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_COARSE = Pattern.compile("(.+?)", Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_FINE = Pattern.compile(".*?" // url + + "(\\d+:\\d+)
(\\d+:\\d+)
.+?" // departureTime, arrivalTime + + "(.*?)
.*?" // line + , Pattern.DOTALL); + + public QueryConnectionsResult queryConnections(final String uri) throws IOException + { + final CharSequence page = ParserUtils.scrape(uri); + + final Matcher mHead = P_CONNECTIONS_HEAD.matcher(page); + if (mHead.matches()) + { + final String from = ParserUtils.resolveEntities(mHead.group(1)); + final String to = ParserUtils.resolveEntities(mHead.group(2)); + final Date currentDate = ParserUtils.parseDate(mHead.group(3)); + final String linkEarlier = mHead.group(4) != null ? ParserUtils.resolveEntities(mHead.group(4)) : null; + final String linkLater = mHead.group(5) != null ? ParserUtils.resolveEntities(mHead.group(5)) : null; + final List connections = new ArrayList(); + + final Matcher mConCoarse = P_CONNECTIONS_COARSE.matcher(page); + while (mConCoarse.find()) + { + final Matcher mConFine = P_CONNECTIONS_FINE.matcher(mConCoarse.group(1)); + if (mConFine.matches()) + { + final String link = ParserUtils.resolveEntities(mConFine.group(1)); + Date departureTime = ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mConFine.group(2))); + if (!connections.isEmpty()) + { + final long diff = ParserUtils.timeDiff(departureTime, + ((Connection.Trip) connections.get(connections.size() - 1).parts.get(0)).departureTime); + if (diff > PARSER_DAY_ROLLOVER_THRESHOLD_MS) + departureTime = ParserUtils.addDays(departureTime, -1); + else if (diff < -PARSER_DAY_ROLLOVER_THRESHOLD_MS) + departureTime = ParserUtils.addDays(departureTime, 1); + } + Date arrivalTime = ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mConFine.group(3))); + if (departureTime.after(arrivalTime)) + arrivalTime = ParserUtils.addDays(arrivalTime, 1); + String line = mConFine.group(4); + if (line != null && !line.contains(",")) + line = normalizeLine(line); + else + line = null; + final Connection connection = new Connection(link, departureTime, arrivalTime, from, to, new ArrayList(1)); + connection.parts.add(new Connection.Trip(departureTime, arrivalTime, line, line != null ? LINES.get(line.charAt(0)) : null)); + connections.add(connection); + } + else + { + throw new IllegalArgumentException("cannot parse '" + mConCoarse.group(1) + "' on " + uri); + } + } + + return new QueryConnectionsResult(from, to, currentDate, linkEarlier, linkLater, connections); + } + else + { + throw new IOException(page.toString()); + } + } + + private static final Pattern P_CONNECTION_DETAILS_HEAD = Pattern.compile(".*Verbindungsdetails.*", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_COARSE = Pattern.compile("
\n?(.+?)\n?
", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_FINE = Pattern.compile("\\s*(.+?)\\s*.*?" // departure + + "(?:" // + + "\\s*(.+?)\\s*.*?" // line + + "ab\\s+(?:.*?)?\\s*(\\d+:\\d+)\\s*(?:.*?)?" // departureTime + + "\\s*(Gl\\. .+?)?\\s*\n?" // departurePosition + + "am\\s+(\\d+\\.\\d+\\.\\d+).*?" // departureDate + + "\\s*(.+?)\\s*
.*?" // arrival + + "an\\s+(?:.*?)?\\s*(\\d+:\\d+)\\s*(?:.*?)?" // arrivalTime + + "\\s*(Gl\\. .+?)?\\s*\n?" // arrivalPosition + + "am\\s+(\\d+\\.\\d+\\.\\d+).*?" // arrivalDate + + "|" // + + "(\\d+) Min\\..*?" // footway + + "\\s*(.+?)\\s*
" // arrival + + ")", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_MESSAGES = Pattern + .compile("Dauer: \\d+:\\d+|(Anschlusszug nicht mehr rechtzeitig)|(Anschlusszug jedoch erreicht werden)|(nur teilweise dargestellt)|(Längerer Aufenthalt)|(äquivalentem Bahnhof)|(Bahnhof wird mehrfach durchfahren)"); + + public GetConnectionDetailsResult getConnectionDetails(final String uri) throws IOException + { + final CharSequence page = ParserUtils.scrape(uri); + + final Matcher mHead = P_CONNECTION_DETAILS_HEAD.matcher(page); + if (mHead.matches()) + { + final List parts = new ArrayList(4); + + Date firstDepartureTime = null; + String firstDeparture = null; + Date lastArrivalTime = null; + String lastArrival = null; + Connection.Trip lastTrip = null; + + final Matcher mDetCoarse = P_CONNECTION_DETAILS_COARSE.matcher(page); + while (mDetCoarse.find()) + { + final String section = mDetCoarse.group(1); + if (P_CONNECTION_DETAILS_MESSAGES.matcher(section).find()) + { + // ignore message for now + } + else + { + final Matcher mDetFine = P_CONNECTION_DETAILS_FINE.matcher(section); + if (mDetFine.matches()) + { + final String departure = ParserUtils.resolveEntities(mDetFine.group(1)); + if (departure != null && firstDeparture == null) + firstDeparture = departure; + + final String min = mDetFine.group(10); + if (min == null) + { + final String line = normalizeLine(ParserUtils.resolveEntities(mDetFine.group(2))); + + final Date departureTime = ParserUtils.parseTime(mDetFine.group(3)); + + final String departurePosition = mDetFine.group(4) != null ? ParserUtils.resolveEntities(mDetFine.group(4)) : null; + + final Date departureDate = ParserUtils.parseDate(mDetFine.group(5)); + + final String arrival = ParserUtils.resolveEntities(mDetFine.group(6)); + + final Date arrivalTime = ParserUtils.parseTime(mDetFine.group(7)); + + final String arrivalPosition = mDetFine.group(8) != null ? ParserUtils.resolveEntities(mDetFine.group(8)) : null; + + final Date arrivalDate = ParserUtils.parseDate(mDetFine.group(9)); + + final Date departureDateTime = ParserUtils.joinDateTime(departureDate, departureTime); + final Date arrivalDateTime = ParserUtils.joinDateTime(arrivalDate, arrivalTime); + lastTrip = new Connection.Trip(line, LINES.get(line.charAt(0)), null, departureDateTime, departurePosition, departure, + arrivalDateTime, arrivalPosition, arrival); + parts.add(lastTrip); + + if (firstDepartureTime == null) + firstDepartureTime = departureDateTime; + + lastArrival = arrival; + lastArrivalTime = arrivalDateTime; + } + else + { + final String arrival = ParserUtils.resolveEntities(mDetFine.group(11)); + + if (parts.size() > 0 && parts.get(parts.size() - 1) instanceof Connection.Footway) + { + final Connection.Footway lastFootway = (Connection.Footway) parts.remove(parts.size() - 1); + parts.add(new Connection.Footway(lastFootway.min + Integer.parseInt(min), lastFootway.departure, arrival)); + } + else + { + parts.add(new Connection.Footway(Integer.parseInt(min), departure, arrival)); + } + + lastArrival = arrival; + } + } + else + { + throw new IllegalArgumentException("cannot parse '" + section + "' on " + uri); + } + } + } + + // verify + if (firstDepartureTime == null || lastArrivalTime == null) + throw new IllegalStateException("could not parse all parts of:\n" + page + "\n" + parts); + + return new GetConnectionDetailsResult(new Date(), new Connection(uri, firstDepartureTime, lastArrivalTime, firstDeparture, lastArrival, + parts)); + } + else + { + throw new IOException(page.toString()); + } + } + + private static final String DEPARTURE_URL = "http://mobile.bahn.de/bin/mobil/bhftafel.exe/dox?start=&maxJourneys=20&boardType=Abfahrt&productsFilter=1111111111000000&input="; + + public String getDeparturesUri(String stationId) + { + return DEPARTURE_URL + stationId; + } + + private static final Pattern P_DEPARTURES_HEAD = Pattern.compile(".*
.*?" + + "\\n?(.+?)\\s*(?:- Aktuell)?\\n.*?" // + + "Abfahrt (\\d+:\\d+)\\n?Uhr, (\\d+\\.\\d+\\.\\d+)\\n?" // + + "
.*", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_COARSE = Pattern.compile("
(.+?)
", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_FINE = Pattern.compile(".*?(.*?).*?" + + ">>\\n?\\s*(.+?)\\s*\\n?
\\n?(\\d+:\\d+).*?", Pattern.DOTALL); + + public GetDeparturesResult getDepartures(final String stationId, final Product[] products, final int maxDepartures) throws IOException + { + final CharSequence page = ParserUtils.scrape(getDeparturesUri(stationId)); + + // parse page + final Matcher mHead = P_DEPARTURES_HEAD.matcher(page); + if (mHead.matches()) + { + final String location = ParserUtils.resolveEntities(mHead.group(1)); + final Date currentTime = ParserUtils.joinDateTime(ParserUtils.parseDate(mHead.group(3)), ParserUtils.parseTime(mHead.group(2))); + final List departures = new ArrayList(maxDepartures); + + // choose matcher + final Matcher mDepCoarse = P_DEPARTURES_COARSE.matcher(page); + while (mDepCoarse.find() && (maxDepartures == 0 || departures.size() < maxDepartures)) + { + final Matcher mDepFine = P_DEPARTURES_FINE.matcher(mDepCoarse.group(1)); + if (mDepFine.matches()) + { + final Departure dep = parseDeparture(mDepFine, currentTime); + if (products == null || filter(dep.line.charAt(0), products)) + if (!departures.contains(dep)) + departures.add(dep); + } + else + { + throw new IllegalArgumentException("cannot parse '" + mDepCoarse.group(1) + "' on " + stationId); + } + } + + return new GetDeparturesResult(location, currentTime, departures); + } + else + { + return GetDeparturesResult.NO_INFO; + } + } + + private static Departure parseDeparture(final Matcher mDep, final Date currentTime) + { + // line + String line = normalizeLine(ParserUtils.resolveEntities(mDep.group(1))); + if (line.length() == 0) + line = null; + final int[] lineColors = line != null ? LINES.get(line.charAt(0)) : null; + + // destination + final String destination = ParserUtils.resolveEntities(mDep.group(2)); + + // time + final Calendar current = new GregorianCalendar(); + current.setTime(currentTime); + final Calendar parsed = new GregorianCalendar(); + parsed.setTime(ParserUtils.parseTime(mDep.group(3))); + parsed.set(Calendar.YEAR, current.get(Calendar.YEAR)); + parsed.set(Calendar.MONTH, current.get(Calendar.MONTH)); + parsed.set(Calendar.DAY_OF_MONTH, current.get(Calendar.DAY_OF_MONTH)); + if (ParserUtils.timeDiff(parsed.getTime(), currentTime) < -PARSER_DAY_ROLLOVER_THRESHOLD_MS) + parsed.add(Calendar.DAY_OF_MONTH, 1); + + return new Departure(parsed.getTime(), line, lineColors, destination); + } + + private boolean filter(final char line, final Product[] products) + { + final Product lineProduct = Product.fromCode(line); + + for (final Product p : products) + if (lineProduct == p) + return true; + + return false; + } + + private static final Pattern P_NORMALIZE_LINE = Pattern.compile("([A-Za-zÄÖÜäöüß]+)[\\s-]*(.*)"); + private static final Pattern P_NORMALIZE_LINE_SBAHN = Pattern.compile("S\\w*\\d+"); + private static final Pattern P_NORMALIZE_LINE_NUMBER = Pattern.compile("\\d{4,5}"); + + private static String normalizeLine(final String line) + { + // TODO DPN Bad Reichenhall + // TODO EIC Polen? + // TODO EM East Midland? http://www.eastmidlandstrains.co.uk + + if (line.length() == 0) + return line; + + final Matcher m = P_NORMALIZE_LINE.matcher(line); + if (m.matches()) + { + final String type = m.group(1); + final String number = m.group(2).replace(" ", ""); + + if (type.equals("ICE")) // InterCityExpress + return "IICE" + number; + if (type.equals("IC")) // InterCity + return "IIC" + number; + if (type.equals("EC")) // EuroCity + return "IEC" + number; + if (type.equals("EN")) // EuroNight + return "IEN" + number; + if (type.equals("CNL")) // CityNightLine + return "ICNL" + number; + if (type.equals("X")) // InterConnex + return "IX" + number; + if (type.equals("TLK")) // Tanie Linie Kolejowe (Polen) + return "ITLK" + number; + if (type.equals("TGV")) // Train à Grande Vitesse + return "ITGV" + number; + if (type.equals("THA")) // Thalys + return "ITHA" + number; + if (type.equals("RJ")) // RailJet, Österreichische Bundesbahnen + return "IRJ" + number; + if (type.equals("OEC")) // ÖBB-EuroCity + return "IOEC" + number; + if (type.equals("OIC")) // ÖBB-InterCity + return "IOIC" + number; + if (type.equals("ICN")) // Intercity-Neigezug, Schweiz + return "IICN" + number; + if (type.equals("AVE")) // Alta Velocidad Española, Spanien + return "IAVE" + number; + if (type.equals("SC")) // SuperCity, cz + return "ISC" + number; + if (type.equals("EST")) // Eurostar + return "IEST" + number; + if (type.equals("HOT")) // Spanien, Nachtzug? + return "IHOT" + number; + if (type.equals("R")) + return "R" + number; + if (type.equals("IR")) // InterRegio + return "RIR" + number; + if (type.equals("D")) // D-Zug? + return "RD" + number; + if (type.equals("RB")) // RegionalBahn + return "RRB" + number; + if (type.equals("RE")) // RegionalExpress + return "RRE" + number; + if (type.equals("IRE")) // Interregio Express + return "RIRE" + number; + if (type.equals("RFB")) // Reichenbachfall-Bahn + return "RRFB" + number; + if (type.equals("VEC")) // vectus Verkehrsgesellschaft + return "RVEC" + number; + if (type.equals("HTB")) // Hörseltalbahn + return "RHTB" + number; + if (type.equals("HLB")) // Hessenbahn + return "RHLB" + number; + if (type.equals("MRB")) // Mitteldeutsche Regiobahn + return "RMRB" + number; + if (type.equals("VBG")) // Vogtlandbahn + return "RVBG" + number; + if (type.equals("VX")) // Vogtland Express + return "RVX" + number; + if (type.equals("HzL")) // Hohenzollerische Landesbahn + return "RHzL" + number; + if (type.equals("BOB")) // Bayerische Oberlandbahn + return "RBOB" + number; + if (type.equals("BRB")) // Bayerische Regiobahn + return "RBRB" + number; + if (type.equals("ALX")) // Arriva-Länderbahn-Express + return "RALX" + number; + if (type.equals("NWB")) // NordWestBahn + return "RNWB" + number; + if (type.equals("HEX")) // Harz-Berlin-Express, Veolia + return "RHEX" + number; + if (type.equals("PEG")) // Prignitzer Eisenbahn + return "RPEG" + number; + if (type.equals("STB")) // Süd-Thüringen-Bahn + return "RSTB" + number; + if (type.equals("HSB")) // Harzer Schmalspurbahnen + return "RHSB" + number; + if (type.equals("EVB")) // Eisenbahnen und Verkehrsbetriebe Elbe-Weser + return "REVB" + number; + if (type.equals("NOB")) // Nord-Ostsee-Bahn + return "RNOB" + number; + if (type.equals("WFB")) // Westfalenbahn + return "RWFB" + number; + if (type.equals("FEG")) // Freiberger Eisenbahngesellschaft + return "RFEG" + number; + if (type.equals("SHB")) // Schleswig-Holstein-Bahn + return "RSHB" + number; + if (type.equals("OSB")) // Ortenau-S-Bahn + return "ROSB" + number; + if (type.equals("WEG")) // Württembergische Eisenbahn-Gesellschaft + return "RWEG" + number; + if (type.equals("MR")) // Märkische Regionalbahn + return "RMR" + number; + if (type.equals("OE")) // Ostdeutsche Eisenbahn + return "ROE" + number; + if (type.equals("UBB")) // Usedomer Bäderbahn + return "RUBB" + number; + if (type.equals("NEB")) // Niederbarnimer Eisenbahn + return "RNEB" + number; + if (type.equals("AKN")) // AKN Eisenbahn AG + return "RAKN" + number; + if (type.equals("SBB")) // Schweizerische Bundesbahnen + return "RSBB" + number; + if (type.equals("OLA")) // Ostseeland Verkehr + return "ROLA" + number; + if (type.equals("ME")) // metronom + return "RME" + number; + if (type.equals("MEr")) // metronom regional + return "RMEr" + number; + if (type.equals("ERB")) // eurobahn (Keolis Deutschland) + return "RERB" + number; + if (type.equals("EB")) // Erfurter Bahn + return "REB" + number; + if (type.equals("VIA")) // VIAS + return "RVIA" + number; + if (type.equals("CAN")) // cantus Verkehrsgesellschaft + return "RCAN" + number; + if (type.equals("PEG")) // Prignitzer Eisenbahn + return "RPEG" + number; + if (type.equals("BLB")) // Berchtesgadener Land Bahn + return "RBLB" + number; + if (type.equals("PRE")) // Pressnitztalbahn + return "RPRE" + number; + if (type.equals("neg")) // Norddeutsche Eisenbahngesellschaft Niebüll + return "Rneg" + number; + if (type.equals("NBE")) // nordbahn + return "RNBE" + number; + if (type.equals("MBB")) // Mecklenburgische Bäderbahn Molli + return "RMBB" + number; + if (type.equals("ABR")) // Abellio Rail NRW + return "RABR" + number; + if (type.equals("ABG")) // Anhaltische Bahngesellschaft + return "RABG" + number; + if (type.equals("Sp")) // EgroNet? + return "RSp" + number; + if (type.equals("Os")) // EgroNet? + return "ROs" + number; + if (type.equals("REX")) // Österreich? + return "RREX" + number; + if (type.equals("SB")) // Säntis-Bahn, Schweiz - evtl. auch SaarBahn+Bus? + return "RSB" + number; + if (type.equals("LT")) + return "RLT" + number; + if (type.equals("CB")) // http://www.railfan.de/nebahn/morac.html + return "RCB" + number; + if (type.equals("SWE")) // SWEG + return "RSWE" + number; + if (type.equals("ÖBA")) // Öchsle-Bahn Betriebsgesellschaft + return "RÖBA" + number; + if (type.equals("RTB")) // Rurtalbahn + return "RRTB" + number; + if (type.equals("SOE")) // Sächsisch-Oberlausitzer Eisenbahngesellschaft + return "RSOE" + number; + if (type.equals("SBE")) // Sächsisch-Böhmische Eisenbahngesellschaft + return "RSBE" + number; + if (type.equals("Dab")) // Daadetalbahn + return "RDab" + number; + if (type.equals("SDG")) // Sächsische Dampfeisenbahngesellschaft + return "RSDG" + number; + if (type.equals("VEN")) // Rhenus Veniro + return "RVEN" + number; + if (type.equals("KD")) // Koleje Dolnośląskie + return "RKD" + number; + if (type.equals("ALS")) // Spanien + return "RALS" + number; + if (type.equals("TLG")) // Spanien + return "RTLG" + number; + if (type.equals("ARC")) // Spanien + return "RARC" + number; + if (type.equals("SKW")) // Polen + return "RSKW" + number; + if (type.equals("KM")) // Polen + return "RKM" + number; + if (type.equals("PCC")) // Polen + return "RPCC" + number; + if (type.equals("SKM")) // Polen + return "RSKM" + number; + if (type.equals("WKD")) // Warszawska Kolej Dojazdowa, Polen + return "RWKD" + number; + if (type.equals("LYN")) // Dänemark + return "RLYN" + number; + if (type.equals("EX")) // Norwegen + return "REX" + number; + if (type.equals("NZ")) // Norwegen + return "RNZ" + number; + if (type.equals("S")) + return "SS" + number; + if (type.equals("BSB")) // Breisgau S-Bahn + return "SBSB" + number; + if (type.equals("RER")) // Réseau Express Régional, Frankreich + return "SRER" + number; + if (type.equals("RSB")) // Schnellbahn Wien + return "SRSB" + number; + if (type.equals("CAT")) // City Airport Train, Schweden + return "SCAT" + number; + if (type.equals("U")) + return "UU" + number; + if (type.equals("STR")) + return "T" + number; + if (type.equals("RT")) // RegioTram + return "TRT" + number; + if (type.equals("Schw")) // Schwebebahn, gilt als "Straßenbahn besonderer Bauart" + return "TSchw" + number; + if (type.startsWith("Bus")) + return "B" + type.substring(3) + number; + if (type.startsWith("AST")) // Anruf-Sammel-Taxi + return "BAST" + type.substring(3) + number; + if (type.startsWith("ALT")) // Anruf-Linien-Taxi + return "BALT" + type.substring(3) + number; + if (type.startsWith("RFB")) // Rufbus + return "BRFB" + type.substring(3) + number; + if (type.equals("RNV")) // Rhein-Neckar-Verkehr GmbH - TODO aufteilen in Tram/Bus/Fähre + return "BRNV" + number; + if (type.equals("Fähre")) + return "F" + number; + if (type.equals("Fäh")) + return "F" + number; + if (type.equals("Schiff")) + return "FSchiff" + number; + if (type.equals("KAT")) // z.B. Friedrichshafen <-> Konstanz + return "FKAT" + number; + if (type.equals("ZahnR")) // Zahnradbahn, u.a. Zugspitzbahn + return "RZahnR" + number; + if (type.equals("Flug")) + return "IFlug" + number; + if (type.equals("E")) + { + if (P_NORMALIZE_LINE_SBAHN.matcher(number).matches()) + return "S" + number; + else + return "RE" + number; + } + + throw new IllegalStateException("cannot normalize type " + type + " number " + number + " line " + line); + } + else if (P_NORMALIZE_LINE_NUMBER.matcher(line).matches()) // Polen, Niederlande... leider unscharf + { + return "R" + line; + } + + throw new IllegalStateException("cannot normalize line " + line); + } + + public static final Map LINES = new HashMap(); + + static + { + LINES.put('I', new int[] { Color.WHITE, Color.RED, Color.RED }); + LINES.put('R', new int[] { Color.GRAY, Color.WHITE }); + LINES.put('S', new int[] { Color.parseColor("#006e34"), Color.WHITE }); + LINES.put('U', new int[] { Color.parseColor("#003090"), Color.WHITE }); + LINES.put('T', new int[] { Color.parseColor("#cc0000"), Color.WHITE }); + LINES.put('B', new int[] { Color.parseColor("#993399"), Color.WHITE }); + LINES.put('F', new int[] { Color.BLUE, Color.WHITE }); + } +} diff --git a/src/de/schildbach/pte/CheckConnectionsQueryResult.java b/src/de/schildbach/pte/CheckConnectionsQueryResult.java new file mode 100644 index 00000000..9ed9198e --- /dev/null +++ b/src/de/schildbach/pte/CheckConnectionsQueryResult.java @@ -0,0 +1,49 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.util.List; + +/** + * @author Andreas Schildbach + */ +public final class CheckConnectionsQueryResult +{ + public enum Status + { + OK, AMBIGUOUS, TOO_CLOSE, NO_CONNECTIONS; + } + + public static final CheckConnectionsQueryResult OK = new CheckConnectionsQueryResult(Status.OK, null, null, null); + public static final CheckConnectionsQueryResult TOO_CLOSE = new CheckConnectionsQueryResult(Status.TOO_CLOSE, null, null, null); + public static final CheckConnectionsQueryResult NO_CONNECTIONS = new CheckConnectionsQueryResult(Status.NO_CONNECTIONS, null, null, null); + + public final Status status; + public final List ambiguousFromAddresses; + public final List ambiguousViaAddresses; + public final List ambiguousToAddresses; + + public CheckConnectionsQueryResult(final Status status, final List ambiguousFromAddresses, final List ambiguousViaAddresses, + final List ambiguousToAddresses) + { + this.status = status; + this.ambiguousFromAddresses = ambiguousFromAddresses; + this.ambiguousViaAddresses = ambiguousViaAddresses; + this.ambiguousToAddresses = ambiguousToAddresses; + } +} diff --git a/src/de/schildbach/pte/Color.java b/src/de/schildbach/pte/Color.java new file mode 100644 index 00000000..0220d9bf --- /dev/null +++ b/src/de/schildbach/pte/Color.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +/** + * @author Andreas Schildbach + */ +public class Color +{ + public static final int BLACK = 0xFF000000; + public static final int DKGRAY = 0xFF444444; + public static final int GRAY = 0xFF888888; + public static final int LTGRAY = 0xFFCCCCCC; + public static final int WHITE = 0xFFFFFFFF; + public static final int RED = 0xFFFF0000; + public static final int GREEN = 0xFF00FF00; + public static final int BLUE = 0xFF0000FF; + public static final int YELLOW = 0xFFFFFF00; + public static final int CYAN = 0xFF00FFFF; + public static final int MAGENTA = 0xFFFF00FF; + public static final int TRANSPARENT = 0; + + public static int rgb(int red, int green, int blue) + { + return (0xFF << 24) | (red << 16) | (green << 8) | blue; + } + + public static int parseColor(String colorString) + { + if (colorString.charAt(0) == '#') + { + // Use a long to avoid rollovers on #ffXXXXXX + long color = Long.parseLong(colorString.substring(1), 16); + if (colorString.length() == 7) + { + // Set the alpha value + color |= 0x00000000ff000000; + } + else if (colorString.length() != 9) + { + throw new IllegalArgumentException("Unknown color"); + } + return (int) color; + } + throw new IllegalArgumentException("Unknown color"); + } +} diff --git a/src/de/schildbach/pte/Connection.java b/src/de/schildbach/pte/Connection.java new file mode 100644 index 00000000..75a0c59d --- /dev/null +++ b/src/de/schildbach/pte/Connection.java @@ -0,0 +1,158 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.util.Date; +import java.util.List; + +/** + * @author Andreas Schildbach + */ +public final class Connection +{ + final public String id; + final public String link; + final public Date departureTime; + final public Date arrivalTime; + final public String from; + final public String to; + final public List parts; + + public Connection(String link, Date departureTime, Date arrivalTime, String from, String to, List parts) + { + this.id = extractId(link); + this.link = link; + this.departureTime = departureTime; + this.from = from; + this.arrivalTime = arrivalTime; + this.to = to; + this.parts = parts; + } + + public static String extractId(String link) + { + return link.substring(link.length() - 10); + } + + @Override + public String toString() + { + final StringBuilder builder = new StringBuilder(getClass().getName() + "["); + builder.append("id=").append(id); + builder.append(",departureTime=").append(departureTime); + builder.append(",arrivalTime=").append(arrivalTime); + builder.append(",parts=").append(parts); + builder.append("]"); + return builder.toString(); + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (!(o instanceof Connection)) + return false; + final Connection other = (Connection) o; + return id.equals(other.id); + } + + @Override + public int hashCode() + { + return id.hashCode(); + } + + public static interface Part + { + } + + public final static class Trip implements Part + { + final public String line; + final public int[] lineColors; + final public String destination; + final public Date departureTime; + final public String departurePosition; + final public String departure; + final public Date arrivalTime; + final public String arrivalPosition; + final public String arrival; + + public Trip(final String line, final int[] lineColors, final String destination, final Date departureTime, final String departurePosition, + final String departure, final Date arrivalTime, final String arrivalPosition, final String arrival) + { + this.line = line; + this.lineColors = lineColors; + this.destination = destination; + this.departureTime = departureTime; + this.departurePosition = departurePosition; + this.departure = departure; + this.arrivalTime = arrivalTime; + this.arrivalPosition = arrivalPosition; + this.arrival = arrival; + } + + public Trip(final Date departureTime, final Date arrivalTime, final String line, final int[] lineColors) + { + this(line, lineColors, null, departureTime, null, null, arrivalTime, null, null); + } + + @Override + public String toString() + { + final StringBuilder builder = new StringBuilder(getClass().getName() + "["); + builder.append("line=").append(line); + builder.append(","); + builder.append("destination=").append(destination); + builder.append(","); + builder.append("departure=").append(departureTime).append("/").append(departurePosition).append("/").append(departure); + builder.append(","); + builder.append("arrival=").append(arrivalTime).append("/").append(arrivalPosition).append("/").append(arrival); + builder.append("]"); + return builder.toString(); + } + } + + public final static class Footway implements Part + { + final public int min; + final public String departure; + final public String arrival; + + public Footway(int min, String departure, String arrival) + { + this.min = min; + this.departure = departure; + this.arrival = arrival; + } + + @Override + public String toString() + { + final StringBuilder builder = new StringBuilder(getClass().getName() + "["); + builder.append("min=").append(min); + builder.append(","); + builder.append("departure=").append(departure); + builder.append(","); + builder.append("arrival=").append(arrival); + builder.append("]"); + return builder.toString(); + } + } +} diff --git a/src/de/schildbach/pte/Departure.java b/src/de/schildbach/pte/Departure.java new file mode 100644 index 00000000..1e1fcbea --- /dev/null +++ b/src/de/schildbach/pte/Departure.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.util.Date; + +/** + * @author Andreas Schildbach + */ +public final class Departure +{ + final public Date time; + final public String line; + final public int[] lineColors; + final public String destination; + + public Departure(final Date time, final String line, final int[] lineColors, final String destination) + { + this.time = time; + this.line = line; + this.lineColors = lineColors; + this.destination = destination; + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder("Departure("); + builder.append(time != null ? time : "null"); + builder.append(","); + builder.append(line != null ? line : "null"); + builder.append(","); + builder.append(destination != null ? destination : "null"); + builder.append(")"); + return builder.toString(); + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (!(o instanceof Departure)) + return false; + final Departure other = (Departure) o; + if (!this.time.equals(other.time)) + return false; + if (this.line == null && other.line != null) + return false; + if (other.line == null && this.line != null) + return false; + if (this.line != null && !this.line.equals(other.line)) + return false; + if (!this.destination.equals(other.destination)) + return false; + return true; + } + + @Override + public int hashCode() + { + int hashCode = time.hashCode(); + hashCode *= 29; + if (line != null) + hashCode += line.hashCode(); + hashCode *= 29; + hashCode += destination.hashCode(); + return hashCode; + } +} diff --git a/src/de/schildbach/pte/GetConnectionDetailsResult.java b/src/de/schildbach/pte/GetConnectionDetailsResult.java new file mode 100644 index 00000000..7ff79f61 --- /dev/null +++ b/src/de/schildbach/pte/GetConnectionDetailsResult.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.util.Date; + +/** + * @author Andreas Schildbach + */ +public final class GetConnectionDetailsResult +{ + public final Date currentDate; + public final Connection connection; + + public GetConnectionDetailsResult(Date currentDate, Connection connection) + { + this.currentDate = currentDate; + this.connection = connection; + } +} diff --git a/src/de/schildbach/pte/GetDeparturesResult.java b/src/de/schildbach/pte/GetDeparturesResult.java new file mode 100644 index 00000000..abe7be4c --- /dev/null +++ b/src/de/schildbach/pte/GetDeparturesResult.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.util.Date; +import java.util.List; + +/** + * @author Andreas Schildbach + */ +public final class GetDeparturesResult +{ + public static final GetDeparturesResult NO_INFO = new GetDeparturesResult(null, null, null); + public static final GetDeparturesResult SERVICE_DOWN = new GetDeparturesResult(null, null, null); + + public final String location; + public final Date currentTime; + public final List departures; + + public GetDeparturesResult(final String location, final Date currentTime, final List departures) + { + this.location = location; + this.currentTime = currentTime; + this.departures = departures; + } +} diff --git a/src/de/schildbach/pte/LocationUtils.java b/src/de/schildbach/pte/LocationUtils.java new file mode 100644 index 00000000..c69c4fe6 --- /dev/null +++ b/src/de/schildbach/pte/LocationUtils.java @@ -0,0 +1,114 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +/** + * @author Andreas Schildbach + */ +public final class LocationUtils +{ + /** + * @param lat1 + * latitude of origin point in decimal degrees + * @param lon1 + * longitude of origin point in deceimal degrees + * @param lat2 + * latitude of destination point in decimal degrees + * @param lon2 + * longitude of destination point in decimal degrees + * + * @return distance in meters + */ + public static float computeDistance(double lat1, double lon1, double lat2, double lon2) + { + // Based on http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf + // using the "Inverse Formula" (section 4) + + final int MAXITERS = 20; + // Convert lat/long to radians + lat1 *= Math.PI / 180.0; + lat2 *= Math.PI / 180.0; + lon1 *= Math.PI / 180.0; + lon2 *= Math.PI / 180.0; + + final double a = 6378137.0; // WGS84 major axis + final double b = 6356752.3142; // WGS84 semi-major axis + final double f = (a - b) / a; + final double aSqMinusBSqOverBSq = (a * a - b * b) / (b * b); + + final double L = lon2 - lon1; + double A = 0.0; + final double U1 = Math.atan((1.0 - f) * Math.tan(lat1)); + final double U2 = Math.atan((1.0 - f) * Math.tan(lat2)); + + final double cosU1 = Math.cos(U1); + final double cosU2 = Math.cos(U2); + final double sinU1 = Math.sin(U1); + final double sinU2 = Math.sin(U2); + final double cosU1cosU2 = cosU1 * cosU2; + final double sinU1sinU2 = sinU1 * sinU2; + + double sigma = 0.0; + double deltaSigma = 0.0; + double cosSqAlpha = 0.0; + double cos2SM = 0.0; + double cosSigma = 0.0; + double sinSigma = 0.0; + double cosLambda = 0.0; + double sinLambda = 0.0; + + double lambda = L; // initial guess + for (int iter = 0; iter < MAXITERS; iter++) + { + final double lambdaOrig = lambda; + cosLambda = Math.cos(lambda); + sinLambda = Math.sin(lambda); + final double t1 = cosU2 * sinLambda; + final double t2 = cosU1 * sinU2 - sinU1 * cosU2 * cosLambda; + final double sinSqSigma = t1 * t1 + t2 * t2; // (14) + sinSigma = Math.sqrt(sinSqSigma); + cosSigma = sinU1sinU2 + cosU1cosU2 * cosLambda; // (15) + sigma = Math.atan2(sinSigma, cosSigma); // (16) + final double sinAlpha = (sinSigma == 0) ? 0.0 : cosU1cosU2 * sinLambda / sinSigma; // (17) + cosSqAlpha = 1.0 - sinAlpha * sinAlpha; + cos2SM = (cosSqAlpha == 0) ? 0.0 : cosSigma - 2.0 * sinU1sinU2 / cosSqAlpha; // (18) + + final double uSquared = cosSqAlpha * aSqMinusBSqOverBSq; // defn + A = 1 + (uSquared / 16384.0) * // (3) + (4096.0 + uSquared * (-768 + uSquared * (320.0 - 175.0 * uSquared))); + final double B = (uSquared / 1024.0) * // (4) + (256.0 + uSquared * (-128.0 + uSquared * (74.0 - 47.0 * uSquared))); + final double C = (f / 16.0) * cosSqAlpha * (4.0 + f * (4.0 - 3.0 * cosSqAlpha)); // (10) + final double cos2SMSq = cos2SM * cos2SM; + deltaSigma = B + * sinSigma + * // (6) + (cos2SM + (B / 4.0) + * (cosSigma * (-1.0 + 2.0 * cos2SMSq) - (B / 6.0) * cos2SM * (-3.0 + 4.0 * sinSigma * sinSigma) * (-3.0 + 4.0 * cos2SMSq))); + + lambda = L + (1.0 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SM + C * cosSigma * (-1.0 + 2.0 * cos2SM * cos2SM))); // (11) + + final double delta = (lambda - lambdaOrig) / lambda; + + if (Math.abs(delta) < 1.0e-12) + break; + } + + return (float) (b * A * (sigma - deltaSigma)); + } +} diff --git a/src/de/schildbach/pte/MvvProvider.java b/src/de/schildbach/pte/MvvProvider.java new file mode 100644 index 00000000..68944573 --- /dev/null +++ b/src/de/schildbach/pte/MvvProvider.java @@ -0,0 +1,686 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Andreas Schildbach + */ +public class MvvProvider implements NetworkProvider +{ + public static final String NETWORK_ID = "efa.mvv-muenchen.de"; + + private static final long PARSER_DAY_ROLLOVER_THRESHOLD_MS = 12 * 60 * 60 * 1000; + + public boolean hasCapabilities(Capability... capabilities) + { + for (final Capability capability : capabilities) + if (capability == Capability.NEARBY_STATIONS) + return false; + + return true; + } + + private static final String AUTOCOMPLETE_NAME_URL = "http://efa.mvv-muenchen.de/mobile/XSLT_DM_REQUEST?anySigWhenPerfectNoOtherMatches=1&command=&itdLPxx_advancedOptions=0&itdLPxx_odvPPType=&language=de&limit=20&locationServerActive=1&nameInfo_dm=invalid&nameState_dm=empty&nameState_dm=empty&placeInfo_dm=invalid&placeState_dm=empty&place_dm=&reducedAnyPostcodeObjFilter_dm=64&reducedAnyTooManyObjFilter_dm=2&reducedAnyWithoutAddressObjFilter_dm=102&requestID=0&selectAssignedStops=1&sessionID=0&typeInfo_dm=invalid&type_dm=stop&useHouseNumberList_dm=1&name_dm="; + private static final Pattern P_SINGLE_NAME = Pattern.compile(".*Von:[\\xa0\\s]+(.+?)
.*", Pattern.DOTALL); + private static final Pattern P_MULTI_NAME = Pattern.compile("", Pattern.DOTALL); + + public List autoCompleteStationName(final CharSequence constraint) throws IOException + { + final CharSequence page = ParserUtils.scrape(AUTOCOMPLETE_NAME_URL + ParserUtils.urlEncode(constraint.toString())); + + final List names = new ArrayList(); + + final Matcher mSingle = P_SINGLE_NAME.matcher(page); + if (mSingle.matches()) + { + names.add(ParserUtils.resolveEntities(mSingle.group(1))); + } + else + { + final Matcher mMulti = P_MULTI_NAME.matcher(page); + while (mMulti.find()) + names.add(ParserUtils.resolveEntities(mMulti.group(1))); + } + + return names; + } + + public List nearbyStations(final double lat, final double lon, final int maxDistance, final int maxStations) throws IOException + { + throw new UnsupportedOperationException(); + } + + public String connectionsQueryUri(final String from, final String via, final String to, final Date date, final boolean dep) + { + final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); + final DateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy"); + final DateFormat MONTH_FORMAT = new SimpleDateFormat("M"); + final DateFormat DAY_FORMAT = new SimpleDateFormat("d"); + final DateFormat HOUR_FORMAT = new SimpleDateFormat("H"); + final DateFormat MINUTE_FORMAT = new SimpleDateFormat("m"); + + final StringBuilder uri = new StringBuilder("http://efa.mvv-muenchen.de/mobile/XSLT_TRIP_REQUEST2"); + uri.append("?language=de"); + uri.append("&sessionID=0"); + uri.append("&requestID=0"); + uri.append("&command="); + uri.append("&execInst="); + uri.append("&ptOptionsActive=1"); + uri.append("&itOptionsActive=1"); + uri.append("&imageFormat=PNG"); + uri.append("&imageWidth=400"); + uri.append("&imageHeight=300"); + uri.append("&imageOnly=1"); + uri.append("&imageNoTiles=1"); + uri.append("&itdLPxx_advancedOptions=0"); // LP=LayoutParams + uri.append("&itdLPxx_odvPPType="); + uri.append("&itdLPxx_execInst="); + uri.append("&itdDateDay=").append(ParserUtils.urlEncode(DAY_FORMAT.format(date))); + uri.append("&itdDateMonth=").append(ParserUtils.urlEncode(MONTH_FORMAT.format(date))); + uri.append("&itdDateYear=").append(ParserUtils.urlEncode(YEAR_FORMAT.format(date))); + uri.append("&locationServerActive=1"); + uri.append("&useProxFootSearch=1"); // Take stops close to the stop/start into account and possibly use them + // instead + uri.append("&anySigWhenPerfectNoOtherMatches=1"); + uri.append("&lineRestriction=403"); + + uri.append("&useHouseNumberList_origin=1"); + uri.append("&place_origin="); // coarse-grained location, e.g. city + uri.append("&placeState_origin=empty"); // empty|identified + uri.append("&nameState_origin=empty"); // empty|identified|list|notidentified + uri.append("&placeInfo_origin=invalid"); // invalid + uri.append("&nameInfo_origin=invalid"); // invalid + uri.append("&typeInfo_origin=invalid"); // invalid + uri.append("&reducedAnyWithoutAddressObjFilter_origin=102"); + uri.append("&reducedAnyPostcodeObjFilter_origin=64"); + uri.append("&reducedAnyTooManyObjFilter_origin=2"); + uri.append("&type_origin=stop"); // any|stop|poi|address + uri.append("&name_origin=").append(ParserUtils.urlEncode(from)); // fine-grained location, e.g. stop name + + uri.append("&useHouseNumberList_destination=1"); + uri.append("&place_destination="); // coarse-grained location, e.g. city + uri.append("&placeState_destination=empty"); // empty|identified + uri.append("&nameState_destination=empty"); // empty|identified|list|notidentified + uri.append("&placeInfo_destination=invalid"); // invalid + uri.append("&nameInfo_destination=invalid"); // invalid + uri.append("&typeInfo_destination=invalid"); // invalid + uri.append("&reducedAnyWithoutAddressObjFilter_destination=102"); + uri.append("&reducedAnyPostcodeObjFilter_destination=64"); + uri.append("&reducedAnyTooManyObjFilter_destination=2"); + uri.append("&type_destination=stop"); // any|stop|poi|address + uri.append("&name_destination=").append(ParserUtils.urlEncode(to)); // fine-grained location, e.g. stop name + + if (via != null) + { + uri.append("&useHouseNumberList_via=1"); + uri.append("&place_via="); + uri.append("&placeState_via=empty"); + uri.append("&nameState_via=empty"); + uri.append("&placeInfo_via=invalid"); + uri.append("&nameInfo_via=invalid"); + uri.append("&typeInfo_via=invalid"); + uri.append("&reducedAnyWithoutAddressObjFilter_via=102"); + uri.append("&reducedAnyPostcodeObjFilter_via=64"); + uri.append("&reducedAnyTooManyObjFilter_via=2"); + uri.append("&type_via=stop"); // any + uri.append("&name_via=").append(ParserUtils.urlEncode(via)); + } + + uri.append("&itdTripDateTimeDepArr=").append(dep ? "dep" : "arr"); + uri.append("&itdTimeHour=").append(ParserUtils.urlEncode(HOUR_FORMAT.format(date))); + uri.append("&itdTimeMinute=").append(ParserUtils.urlEncode(MINUTE_FORMAT.format(date))); + uri.append("&itdDate=").append(ParserUtils.urlEncode(DATE_FORMAT.format(date))); + + return uri.toString(); + } + + private static final Pattern P_PRE_ADDRESS = Pattern.compile("", + Pattern.DOTALL); + private static final Pattern P_ADDRESSES = Pattern.compile("\\s*(.*?)\\s*", Pattern.DOTALL); + private static final Pattern P_CHECK_CONNECTIONS_ERROR = Pattern.compile("(?:(xxxzudichtxxx)|(konnte keine Verbindung gefunden werden))", + Pattern.CASE_INSENSITIVE); + + public CheckConnectionsQueryResult checkConnectionsQuery(final String queryUri) throws IOException + { + CharSequence page = ParserUtils.scrape(queryUri); + while (page.length() == 0) + { + System.out.println("Got empty page, retrying..."); + page = ParserUtils.scrape(queryUri); + } + + final Matcher mError = P_CHECK_CONNECTIONS_ERROR.matcher(page); + if (mError.find()) + { + if (mError.group(1) != null) + return CheckConnectionsQueryResult.TOO_CLOSE; + if (mError.group(2) != null) + return CheckConnectionsQueryResult.NO_CONNECTIONS; + } + + List fromAddresses = null; + List viaAddresses = null; + List toAddresses = null; + + final Matcher mPreAddress = P_PRE_ADDRESS.matcher(page); + while (mPreAddress.find()) + { + final String type = mPreAddress.group(1); + final String options = mPreAddress.group(2); + + final Matcher mAddresses = P_ADDRESSES.matcher(options); + final List addresses = new ArrayList(); + while (mAddresses.find()) + { + final String address = ParserUtils.resolveEntities(mAddresses.group(1)).trim(); + if (!addresses.contains(address)) + addresses.add(address); + } + + if (type.equals("name_origin")) + fromAddresses = addresses; + else if (type.equals("name_destination")) + toAddresses = addresses; + else if (type.equals("name_via")) + viaAddresses = addresses; + else + throw new IOException(type); + } + + if (fromAddresses != null || viaAddresses != null || toAddresses != null) + { + return new CheckConnectionsQueryResult(CheckConnectionsQueryResult.Status.AMBIGUOUS, fromAddresses, viaAddresses, toAddresses); + } + else + { + return CheckConnectionsQueryResult.OK; + } + } + + private static final Pattern P_CONNECTIONS_HEAD = Pattern.compile(".*Von:[\\xa0\\s]+(.+?)
[\\xa0\\s]+" + + "Nach:[\\xa0\\s]+(.+?)
[\\xa0\\s]+" // + + "(?:itdTripRequestDetails/via:[\\xa0\\s]+(.+?)
[\\xa0\\s]+)?" // + + "Datum:[\\xa0\\s]+\\w{2}\\.,\\s(\\d+)\\.\\s(\\w{3})\\.[\\xa0\\s]+(\\d{4}).*?" + + "(?:.*?)?" // + + "(?:.*?)?", Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_COARSE = Pattern.compile("
(.+?)
", Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_FINE = Pattern.compile(".*?" // + + "(?:(\\d+)\\.(\\d+)\\. .*?)?" // date + + "
" // url + + "(?:" // + + "(\\d+:\\d+)[\\xa0\\s]+-[\\xa0\\s]+(\\d+:\\d+)" // departureTime, arrivalTime + + "|" + "Fußweg.*?Dauer:[\\xa0\\s]+(\\d+):(\\d+)" // + + ").*?", Pattern.DOTALL); + + public QueryConnectionsResult queryConnections(final String uri) throws IOException + { + CharSequence page = ParserUtils.scrape(uri); + while (page.length() == 0) + { + System.out.println("Got empty page, retrying..."); + page = ParserUtils.scrape(uri); + } + + final Matcher mHead = P_CONNECTIONS_HEAD.matcher(page); + if (mHead.matches()) + { + final String from = ParserUtils.resolveEntities(mHead.group(1)); + final String to = ParserUtils.resolveEntities(mHead.group(2)); + // final String via = ParserUtils.resolveEntities(mHead.group(3)); + final Date currentDate = parseDate(mHead.group(4), mHead.group(5), mHead.group(6)); + final String linkEarlier = mHead.group(7) != null ? "http://efa.mvv-muenchen.de/mobile/" + ParserUtils.resolveEntities(mHead.group(7)) + : null; + final String linkLater = mHead.group(8) != null ? "http://efa.mvv-muenchen.de/mobile/" + ParserUtils.resolveEntities(mHead.group(8)) + : null; + final List connections = new ArrayList(); + + final Matcher mConCoarse = P_CONNECTIONS_COARSE.matcher(page); + while (mConCoarse.find()) + { + final Matcher mConFine = P_CONNECTIONS_FINE.matcher(mConCoarse.group(1)); + if (mConFine.matches()) + { + final String link = "http://efa.mvv-muenchen.de/mobile/" + ParserUtils.resolveEntities(mConFine.group(3)); + + if (mConFine.group(6) == null) + { + Date date; + if (mConFine.group(1) != null) + date = parseDate(mConFine.group(1), mConFine.group(2), new SimpleDateFormat("yyyy").format(currentDate)); + else + date = currentDate; + Date departureTime = ParserUtils.joinDateTime(date, ParserUtils.parseTime(mConFine.group(4))); + if (!connections.isEmpty()) + { + final long diff = ParserUtils.timeDiff(departureTime, ((Connection.Trip) connections.get(connections.size() - 1).parts + .get(0)).departureTime); + if (diff > PARSER_DAY_ROLLOVER_THRESHOLD_MS) + departureTime = ParserUtils.addDays(departureTime, -1); + else if (diff < -PARSER_DAY_ROLLOVER_THRESHOLD_MS) + departureTime = ParserUtils.addDays(departureTime, 1); + } + Date arrivalTime = ParserUtils.joinDateTime(date, ParserUtils.parseTime(mConFine.group(5))); + if (departureTime.after(arrivalTime)) + arrivalTime = ParserUtils.addDays(arrivalTime, 1); + final Connection connection = new Connection(link, departureTime, arrivalTime, from, to, new ArrayList(1)); + connection.parts.add(new Connection.Trip(departureTime, arrivalTime, null, null)); + connections.add(connection); + } + else + { + final int min = Integer.parseInt(mConFine.group(6)) * 60 + Integer.parseInt(mConFine.group(7)); + final Calendar calendar = new GregorianCalendar(); + final Date departureTime = calendar.getTime(); + calendar.add(Calendar.MINUTE, min); + final Date arrivalTime = calendar.getTime(); + final Connection connection = new Connection(link, departureTime, arrivalTime, from, to, new ArrayList(1)); + connection.parts.add(new Connection.Footway(min, from, to)); + connections.add(connection); + } + } + else + { + throw new IllegalArgumentException("cannot parse '" + mConCoarse.group(1) + "' on " + uri); + } + } + + return new QueryConnectionsResult(from, to, currentDate, linkEarlier, linkLater, connections); + } + else + { + throw new IOException(page.toString()); + } + } + + private static final Pattern P_CONNECTION_DETAILS_HEAD = Pattern.compile(".*Detailansicht.*?" // + + "Datum:[\\xa0\\s]+\\w{2}\\.,\\s(\\d+)\\.\\s(\\w{3})\\.[\\xa0\\s]+(\\d{4}).*", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_COARSE = Pattern.compile("(.+?).*?" + + "(.+?).*?" // + + "(.+?)", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_FINE = Pattern.compile(".*?(?:" // + + "ab (\\d+:\\d+)\\s+(.*?)\\s*.*?" // + + "\\s*(.*?)\\s*
Richtung\\s*(.*?)\\s*.*?" // + + "an (\\d+:\\d+)\\s+(.*?)\\s* parts = new ArrayList(4); + + Date lastTime = currentDate; + + Date firstDepartureTime = null; + String firstDeparture = null; + Date lastArrivalTime = null; + String lastArrival = null; + + final Matcher mDetCoarse = P_CONNECTION_DETAILS_COARSE.matcher(page); + while (mDetCoarse.find()) + { + final String set = mDetCoarse.group(2) + mDetCoarse.group(3) + mDetCoarse.group(4); + final Matcher mDetFine = P_CONNECTION_DETAILS_FINE.matcher(set); + if (mDetFine.matches()) + { + if (mDetFine.group(8) == null) + { + final Date departureTime = upTime(lastTime, ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mDetFine.group(1)))); + + final String departure = ParserUtils.resolveEntities(mDetFine.group(2)); + if (departure != null && firstDeparture == null) + firstDeparture = departure; + + final String product = ParserUtils.resolveEntities(mDetFine.group(3)); + + final String line = ParserUtils.resolveEntities(mDetFine.group(4)); + + final String destination = ParserUtils.resolveEntities(mDetFine.group(5)); + + final Date arrivalTime = upTime(lastTime, ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mDetFine.group(6)))); + + final String arrival = ParserUtils.resolveEntities(mDetFine.group(7)); + + final String normalizedLine = normalizeLine(product, line); + + parts.add(new Connection.Trip(normalizedLine, LINES.get(normalizedLine), destination, departureTime, null, departure, + arrivalTime, null, arrival)); + + if (firstDepartureTime == null) + firstDepartureTime = departureTime; + + lastArrival = arrival; + lastArrivalTime = arrivalTime; + } + else + { + final String departure = ParserUtils.resolveEntities(mDetFine.group(8)); + if (departure != null && firstDeparture == null) + firstDeparture = departure; + + final String min = mDetFine.group(9); + + final String arrival = ParserUtils.resolveEntities(mDetFine.group(10)); + + if (parts.size() > 0 && parts.get(parts.size() - 1) instanceof Connection.Footway) + { + final Connection.Footway lastFootway = (Connection.Footway) parts.remove(parts.size() - 1); + parts.add(new Connection.Footway(lastFootway.min + Integer.parseInt(min), lastFootway.departure, arrival)); + } + else + { + parts.add(new Connection.Footway(Integer.parseInt(min), departure, arrival)); + } + + lastArrival = arrival; + } + } + else + { + throw new IllegalArgumentException("cannot parse '" + set + "' on " + uri); + } + } + + if (firstDepartureTime == null && lastArrivalTime == null && parts.size() == 1 && parts.get(0) instanceof Connection.Footway) + { + final Calendar calendar = new GregorianCalendar(); + firstDepartureTime = calendar.getTime(); + calendar.add(Calendar.MINUTE, ((Connection.Footway) parts.get(0)).min); + lastArrivalTime = calendar.getTime(); + } + + return new GetConnectionDetailsResult(new Date(), new Connection(uri, firstDepartureTime, lastArrivalTime, firstDeparture, lastArrival, + parts)); + } + else + { + if (P_CONNECTION_DETAILS_ERRORS.matcher(page).find()) + throw new SessionExpiredException(); + else + throw new IOException(page.toString()); + } + } + + private static Date upTime(final Date lastTime, Date time) + { + while (time.before(lastTime)) + time = ParserUtils.addDays(time, 1); + + lastTime.setTime(time.getTime()); + + return time; + } + + private static final String DEPARTURE_URL = "http://efa.mvv-muenchen.de/mobile/XSLT_DM_REQUEST?typeInfo_dm=stopID&mode=direct&nameInfo_dm="; + + public String getDeparturesUri(final String stationId) + { + return DEPARTURE_URL + stationId; + } + + private static final Pattern P_DEPARTURES_HEAD = Pattern.compile(".*Von:[\\xa0\\s]*(.*?)
.*?" // + + "Datum:[\\xa0\\s]*\\w{2}\\.,\\s(\\d+)\\.\\s(\\w{3})\\.[\\xa0\\s]+(\\d{4})
.*", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_COARSE = Pattern.compile("(.+?)", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_FINE = Pattern.compile(".*?" // + + "" // + + "(?:[\\xa0\\s]*[\\xa0\\s]*(\\d+)\\.(\\d+)\\.[\\xa0\\s]*)?" // date + + "(\\d+):(\\d+).*?" // time + + "(?:\"(.*?)\".*?)?" // product + + "\\s*([^<]*?)[\\xa0\\s]*(?:
.*?)?" // line + + "
\\s*(.*?)\\s*
.*?" // destination + + ".*?", Pattern.DOTALL); + + public GetDeparturesResult getDepartures(final String stationId, final Product[] products, final int maxDepartures) throws IOException + { + final CharSequence page = ParserUtils.scrape(getDeparturesUri(stationId)); + + final Matcher mHead = P_DEPARTURES_HEAD.matcher(page); + if (mHead.matches()) + { + final String location = ParserUtils.resolveEntities(mHead.group(1)); + final Date currentTime = parseDate(mHead.group(2), mHead.group(3), mHead.group(4)); + final List departures = new ArrayList(maxDepartures); + + final Calendar calendar = new GregorianCalendar(); + + final Matcher mDepCoarse = P_DEPARTURES_COARSE.matcher(page); + while (mDepCoarse.find() && (maxDepartures == 0 || departures.size() < maxDepartures)) + { + final Matcher mDepFine = P_DEPARTURES_FINE.matcher(mDepCoarse.group(1)); + if (mDepFine.matches()) + { + calendar.setTime(currentTime); + final String day = mDepFine.group(1); + if (day != null) + calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day)); + final String month = mDepFine.group(2); + if (month != null) + calendar.set(Calendar.MONTH, Integer.parseInt(month) - 1); + calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(mDepFine.group(3))); + calendar.set(Calendar.MINUTE, Integer.parseInt(mDepFine.group(4))); + final String normalizedLine = normalizeLine(mDepFine.group(5), mDepFine.group(6)); + final String destination = normalizeStationName(mDepFine.group(7)); + final Departure departure = new Departure(calendar.getTime(), normalizedLine, LINES.get(normalizedLine), destination); + departures.add(departure); + } + else + { + throw new IllegalArgumentException("cannot parse '" + mDepCoarse.group(1) + "' on " + stationId); + } + } + + return new GetDeparturesResult(location, currentTime, departures); + } + else + { + return GetDeparturesResult.NO_INFO; + } + } + + private static final Pattern P_STATION_NAME_WHITESPACE = Pattern.compile("\\s+"); + + private String normalizeStationName(String name) + { + return P_STATION_NAME_WHITESPACE.matcher(name).replaceAll(" "); + } + + private String normalizeLine(final String product, final String line) + { + if (product == null) + { + if (line.matches("\\d{2,4}") && Integer.parseInt(line) >= 30) + return "B" + line; + else if (LINES.containsKey("T" + line)) + return "T" + line; + else if (LINES.containsKey("S" + line)) + return "S" + line; + else if (LINES.containsKey("U" + line)) + return "U" + line; + + throw new IllegalStateException("cannot normalize null product, line " + line); + } + else if (product.equals("Bus")) + { + if (line.startsWith("Bus")) + return "B" + line.substring(4); + else if (line.startsWith("StadtBus")) + return "B" + line.substring(9); + else if (line.startsWith("MetroBus")) + return "B" + line.substring(9); + else if (line.startsWith("Regionalbus")) + return "B" + line.substring(12); + else + return "B" + line; + } + else if (product.equals("Tram")) + { + if (line.startsWith("Tram")) + return "T" + line.substring(5); + else + return "T" + line; + } + else if (product.equals("U-Bahn")) + { + if (line.startsWith("U-Bahn")) + return "U" + line.substring(7); + else + return "U" + line; + } + else if (product.equals("S-Bahn")) + { + if (line.startsWith("S-Bahn")) + return "S" + line.substring(7); + else + return "S" + line; + } + else if (product.equals("Zug")) + { + final String[] lineParts = line.split("\\s+"); + final String type = lineParts[0]; + final String number = lineParts[1]; + if (type.equals("EC")) + return "I" + type + number; + if (type.equals("IC")) + return "I" + type + number; + if (type.equals("ICE")) + return "I" + type + number; + if (type.equals("CNL")) + return "I" + type + number; + if (type.equals("RJ")) // Railjet, Österreich + return "I" + type + number; + if (type.equals("RB")) + return "R" + type + number; + if (type.equals("RE")) + return "R" + type + number; + if (type.equals("D")) + return "R" + type + number; + if (type.equals("BOB")) + return "R" + type + number; + if (type.equals("BRB")) // Bayerische Regiobahn + return "R" + type + number; + if (type.equals("ALX")) // Länderbahn und Vogtlandbahn + return "R" + type + number; + + throw new IllegalStateException("cannot normalize product " + product + " line " + line); + } + else if (product.equals("Schiff")) + { + return "F" + line; + } + else if (product.equals("Seilbahn")) // strangely marked as 'Seilbahn', but means 'Schienenersatzverkehr' + { + return "BSEV" + line; + } + + throw new IllegalStateException("cannot normalize product " + product + " line " + line); + } + + private static Date parseDate(final String day, final String month, final String year) + { + final Calendar calendar = new GregorianCalendar(); + calendar.clear(); + calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day)); + calendar.set(Calendar.MONTH, parseMonth(month)); + calendar.set(Calendar.YEAR, Integer.parseInt(year)); + return calendar.getTime(); + } + + private final static String[] MONTHS = new String[] { "Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez" }; + + private static int parseMonth(final String month) + { + for (int m = 0; m < MONTHS.length; m++) + if (MONTHS[m].equals(month)) + return m; + + throw new IllegalArgumentException("cannot parse month: " + month); + } + + public static final Map LINES = new HashMap(); + + static + { + LINES.put("I", new int[] { Color.WHITE, Color.RED, Color.RED }); // generic + LINES.put("R", new int[] { Color.WHITE, Color.RED, Color.RED }); // generic + LINES.put("S", new int[] { Color.parseColor("#006e34"), Color.WHITE }); // generic + LINES.put("U", new int[] { Color.parseColor("#003090"), Color.WHITE }); // generic + + LINES.put("SS1", new int[] { Color.parseColor("#00ccff"), Color.WHITE }); + LINES.put("SS2", new int[] { Color.parseColor("#66cc00"), Color.WHITE }); + LINES.put("SS3", new int[] { Color.parseColor("#880099"), Color.WHITE }); + LINES.put("SS4", new int[] { Color.parseColor("#ff0033"), Color.WHITE }); + LINES.put("SS6", new int[] { Color.parseColor("#00aa66"), Color.WHITE }); + LINES.put("SS7", new int[] { Color.parseColor("#993333"), Color.WHITE }); + LINES.put("SS8", new int[] { Color.BLACK, Color.parseColor("#ffcc00") }); + LINES.put("SS20", new int[] { Color.BLACK, Color.parseColor("#ffaaaa") }); + LINES.put("SS27", new int[] { Color.parseColor("#ffaaaa"), Color.WHITE }); + LINES.put("SA", new int[] { Color.parseColor("#231f20"), Color.WHITE }); + + LINES.put("T12", new int[] { Color.parseColor("#883388"), Color.WHITE }); + LINES.put("T15", new int[] { Color.parseColor("#3366CC"), Color.WHITE }); + LINES.put("T16", new int[] { Color.parseColor("#CC8833"), Color.WHITE }); + LINES.put("T17", new int[] { Color.parseColor("#993333"), Color.WHITE }); + LINES.put("T18", new int[] { Color.parseColor("#66bb33"), Color.WHITE }); + LINES.put("T19", new int[] { Color.parseColor("#cc0000"), Color.WHITE }); + LINES.put("T20", new int[] { Color.parseColor("#00bbee"), Color.WHITE }); + LINES.put("T21", new int[] { Color.parseColor("#33aa99"), Color.WHITE }); + LINES.put("T23", new int[] { Color.parseColor("#fff000"), Color.WHITE }); + LINES.put("T25", new int[] { Color.parseColor("#ff9999"), Color.WHITE }); + LINES.put("T27", new int[] { Color.parseColor("#ff6600"), Color.WHITE }); + LINES.put("TN17", new int[] { Color.parseColor("#999999"), Color.parseColor("#ffff00") }); + LINES.put("TN19", new int[] { Color.parseColor("#999999"), Color.parseColor("#ffff00") }); + LINES.put("TN20", new int[] { Color.parseColor("#999999"), Color.parseColor("#ffff00") }); + LINES.put("TN27", new int[] { Color.parseColor("#999999"), Color.parseColor("#ffff00") }); + + LINES.put("UU1", new int[] { Color.parseColor("#227700"), Color.WHITE }); + LINES.put("UU2", new int[] { Color.parseColor("#bb0000"), Color.WHITE }); + LINES.put("UU2E", new int[] { Color.parseColor("#bb0000"), Color.WHITE }); + LINES.put("UU3", new int[] { Color.parseColor("#ee8800"), Color.WHITE }); + LINES.put("UU4", new int[] { Color.parseColor("#00ccaa"), Color.WHITE }); + LINES.put("UU5", new int[] { Color.parseColor("#bb7700"), Color.WHITE }); + LINES.put("UU6", new int[] { Color.parseColor("#0000cc"), Color.WHITE }); + } +} diff --git a/src/de/schildbach/pte/NetworkProvider.java b/src/de/schildbach/pte/NetworkProvider.java new file mode 100644 index 00000000..c0b04ccd --- /dev/null +++ b/src/de/schildbach/pte/NetworkProvider.java @@ -0,0 +1,134 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.io.IOException; +import java.util.Date; +import java.util.List; + +/** + * Interface to be implemented by providers of transportation networks + * + * @author Andreas Schildbach + */ +public interface NetworkProvider +{ + public enum Capability + { + NEARBY_STATIONS, DEPARTURES, CONNECTIONS + } + + boolean hasCapabilities(Capability... capabilities); + + /** + * Meant for auto-completion of station names, like in an {@link android.widget.AutoCompleteTextView} + * + * @param constraint + * input by user so far + * @return auto-complete suggestions + * @throws IOException + */ + List autoCompleteStationName(CharSequence constraint) throws IOException; + + /** + * Determine stations near to given location + * + * @param lat + * latitude + * @param lon + * longitude + * @param maxDistance + * maximum distance in meters, or {@code 0} + * @param maxStations + * maximum number of stations, or {@code 0} + * @return nearby stations + * @throws IOException + */ + List nearbyStations(double lat, double lon, int maxDistance, int maxStations) throws IOException; + + /** + * Construct an Uri for querying connections + * + * @param from + * location to route from, mandatory + * @param via + * location to route via, may be {@code null} + * @param to + * location to route to, mandatory + * @param date + * desired date for departing, mandatory + * @param dep + * date is departure date? {@code true} for departure, {@code false} for arrival + * @return uri for querying connections + */ + String connectionsQueryUri(String from, String via, String to, Date date, boolean dep); + + /** + * Check if query is well defined, asking for any ambiguousnesses + * + * @param queryUri + * uri constructed by {@link NetworkProvider#connectionsQueryUri} + * @return result object that can contain alternatives to clear up ambiguousnesses + * @throws IOException + */ + CheckConnectionsQueryResult checkConnectionsQuery(String queryUri) throws IOException; + + /** + * Execute well-defined connections query + * + * @param queryUri + * uri constructed by {@link NetworkProvider#connectionsQueryUri} and optionally checked with + * {@link NetworkProvider#checkConnectionsQuery} + * @return result object containing possible connections + * @throws IOException + */ + QueryConnectionsResult queryConnections(String queryUri) throws IOException; + + /** + * Get details about a connection + * + * @param connectionUri + * uri returned via {@link NetworkProvider#queryConnections} + * @return result object containing the details of the connection + * @throws IOException + */ + GetConnectionDetailsResult getConnectionDetails(String connectionUri) throws IOException; + + /** + * Construct an Uri for getting departures + * + * @param stationId + * id of the station + * @return uri for getting departures + */ + String getDeparturesUri(String stationId); + + /** + * Get departures at a given station, probably live + * + * @param stationId + * id of the station + * @param products + * products to consider or {@code null} to consider all products + * @param maxDepartures + * maximum number of departures to get or {@code 0} + * @return result object containing the departures + * @throws IOException + */ + GetDeparturesResult getDepartures(String stationId, Product[] products, int maxDepartures) throws IOException; +} diff --git a/src/de/schildbach/pte/NetworkProviderFactory.java b/src/de/schildbach/pte/NetworkProviderFactory.java new file mode 100644 index 00000000..34223f4c --- /dev/null +++ b/src/de/schildbach/pte/NetworkProviderFactory.java @@ -0,0 +1,106 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; + +/** + * @author Andreas Schildbach + */ +public final class NetworkProviderFactory +{ + private static Reference vbbProviderRef; + private static Reference mvvProviderRef; + private static Reference bahnProviderRef; + private static Reference rmvProviderRef; + + public static synchronized NetworkProvider provider(final String networkId) + { + if (networkId.equals(VbbProvider.NETWORK_ID)) + { + if (vbbProviderRef != null) + { + final VbbProvider provider = vbbProviderRef.get(); + if (provider != null) + return provider; + } + + final VbbProvider provider = new VbbProvider(); + vbbProviderRef = new SoftReference(provider); + return provider; + } + else if (networkId.equals(MvvProvider.NETWORK_ID)) + { + if (mvvProviderRef != null) + { + final MvvProvider provider = mvvProviderRef.get(); + if (provider != null) + return provider; + } + + final MvvProvider provider = new MvvProvider(); + mvvProviderRef = new SoftReference(provider); + return provider; + } + else if (networkId.equals(BahnProvider.NETWORK_ID)) + { + if (bahnProviderRef != null) + { + final BahnProvider provider = bahnProviderRef.get(); + if (provider != null) + return provider; + } + + final BahnProvider provider = new BahnProvider(); + bahnProviderRef = new SoftReference(provider); + return provider; + } + else if (networkId.equals(RmvProvider.NETWORK_ID) || networkId.equals(RmvProvider.NETWORK_ID_ALT)) + { + if (rmvProviderRef != null) + { + final RmvProvider provider = rmvProviderRef.get(); + if (provider != null) + return provider; + } + + final RmvProvider provider = new RmvProvider(); + rmvProviderRef = new SoftReference(provider); + return provider; + } + else + { + throw new IllegalArgumentException(networkId); + } + } + + public static String networkId(final NetworkProvider provider) + { + if (provider instanceof MvvProvider) + return MvvProvider.NETWORK_ID; + else if (provider instanceof VbbProvider) + return VbbProvider.NETWORK_ID; + else if (provider instanceof BahnProvider) + return BahnProvider.NETWORK_ID; + else if (provider instanceof RmvProvider) + return RmvProvider.NETWORK_ID; + else + throw new IllegalArgumentException(provider.getClass().toString()); + } +} diff --git a/src/de/schildbach/pte/ParserUtils.java b/src/de/schildbach/pte/ParserUtils.java new file mode 100644 index 00000000..2f738ff9 --- /dev/null +++ b/src/de/schildbach/pte/ParserUtils.java @@ -0,0 +1,171 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Andreas Schildbach + */ +public final class ParserUtils +{ + private static final String SCRAPE_USER_AGENT = "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2) Gecko/20100115 Firefox/3.6 (.NET CLR 3.5.30729)"; + private static final int SCRAPE_INITIAL_CAPACITY = 4096; + + public static CharSequence scrape(final String url) throws IOException + { + final StringBuilder buffer = new StringBuilder(SCRAPE_INITIAL_CAPACITY); + final URLConnection connection = new URL(url).openConnection(); + connection.addRequestProperty("User-Agent", SCRAPE_USER_AGENT); + final Reader pageReader = new InputStreamReader(connection.getInputStream(), "ISO-8859-1"); + + final char[] buf = new char[SCRAPE_INITIAL_CAPACITY]; + while (true) + { + final int read = pageReader.read(buf); + if (read == -1) + break; + buffer.append(buf, 0, read); + } + + pageReader.close(); + return buffer; + } + + private static final Pattern P_ENTITY = Pattern.compile("&(?:#(x[\\da-f]+|\\d+)|(amp|quot|apos));"); + + public static String resolveEntities(final CharSequence str) + { + if (str == null) + return null; + + final Matcher matcher = P_ENTITY.matcher(str); + final StringBuilder builder = new StringBuilder(str.length()); + int pos = 0; + while (matcher.find()) + { + final char c; + final String code = matcher.group(1); + if (code != null) + { + if (code.charAt(0) == 'x') + c = (char) Integer.valueOf(code.substring(1), 16).intValue(); + else + c = (char) Integer.parseInt(code); + } + else + { + final String namedEntity = matcher.group(2); + if (namedEntity.equals("amp")) + c = '&'; + else if (namedEntity.equals("quot")) + c = '"'; + else if (namedEntity.equals("apos")) + c = '\''; + else + throw new IllegalStateException("unknown entity: " + namedEntity); + } + builder.append(str.subSequence(pos, matcher.start())); + builder.append(c); + pos = matcher.end(); + } + builder.append(str.subSequence(pos, str.length())); + return builder.toString(); + } + + public static Date parseDate(String str) + { + try + { + return new SimpleDateFormat("dd.MM.yy").parse(str); + } + catch (ParseException x) + { + throw new RuntimeException(x); + } + } + + public static Date parseTime(String str) + { + try + { + return new SimpleDateFormat("HH:mm").parse(str); + } + catch (ParseException x) + { + throw new RuntimeException(x); + } + } + + public static Date joinDateTime(Date date, Date time) + { + final Calendar cDate = new GregorianCalendar(); + cDate.setTime(date); + final Calendar cTime = new GregorianCalendar(); + cTime.setTime(time); + cTime.set(Calendar.YEAR, cDate.get(Calendar.YEAR)); + cTime.set(Calendar.MONTH, cDate.get(Calendar.MONTH)); + cTime.set(Calendar.DAY_OF_MONTH, cDate.get(Calendar.DAY_OF_MONTH)); + return cTime.getTime(); + } + + public static long timeDiff(Date d1, Date d2) + { + return d1.getTime() - d2.getTime(); + } + + public static Date addDays(Date time, int days) + { + final Calendar c = new GregorianCalendar(); + c.setTime(time); + c.add(Calendar.DAY_OF_YEAR, days); + return c.getTime(); + } + + public static void printGroups(Matcher m) + { + final int groupCount = m.groupCount(); + for (int i = 1; i <= groupCount; i++) + System.out.println("group " + i + ":'" + m.group(i) + "'"); + } + + public static String urlEncode(String part) + { + try + { + return URLEncoder.encode(part, "utf-8"); + } + catch (UnsupportedEncodingException x) + { + throw new RuntimeException(x); + } + } +} diff --git a/src/de/schildbach/pte/Product.java b/src/de/schildbach/pte/Product.java new file mode 100644 index 00000000..97bb9851 --- /dev/null +++ b/src/de/schildbach/pte/Product.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +/** + * @author Andreas Schildbach + */ +public enum Product +{ + HIGH_SPEED_TRAIN, REGIONAL_TRAIN, URBAN_TRAIN, SUBWAY, TRAM, BUS, FERRY; + + public static Product fromCode(char code) + { + if (code == 'I') + return Product.HIGH_SPEED_TRAIN; + else if (code == 'R') + return Product.REGIONAL_TRAIN; + else if (code == 'S') + return Product.URBAN_TRAIN; + else if (code == 'U') + return Product.SUBWAY; + else if (code == 'T') + return Product.TRAM; + else if (code == 'B') + return Product.BUS; + else if (code == 'F') + return Product.FERRY; + else + throw new IllegalArgumentException(Character.toString(code)); + } +} diff --git a/src/de/schildbach/pte/QueryConnectionsResult.java b/src/de/schildbach/pte/QueryConnectionsResult.java new file mode 100644 index 00000000..223e8c66 --- /dev/null +++ b/src/de/schildbach/pte/QueryConnectionsResult.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.util.Date; +import java.util.List; + +/** + * @author Andreas Schildbach + */ +public final class QueryConnectionsResult +{ + public final String from; + public final String to; + public final Date currentDate; + public final String linkEarlier; + public final String linkLater; + public final List connections; + + public QueryConnectionsResult(final String from, final String to, final Date currentDate, final String linkEarlier, final String linkLater, + final List connections) + { + this.from = from; + this.to = to; + this.currentDate = currentDate; + this.linkEarlier = linkEarlier; + this.linkLater = linkLater; + this.connections = connections; + } +} diff --git a/src/de/schildbach/pte/RmvProvider.java b/src/de/schildbach/pte/RmvProvider.java new file mode 100644 index 00000000..c7a195c4 --- /dev/null +++ b/src/de/schildbach/pte/RmvProvider.java @@ -0,0 +1,523 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Andreas Schildbach + */ +public class RmvProvider implements NetworkProvider +{ + public static final String NETWORK_ID = "mobil.rmv.de"; + public static final String NETWORK_ID_ALT = "www.rmv.de"; + + private static final long PARSER_DAY_ROLLOVER_THRESHOLD_MS = 12 * 60 * 60 * 1000; + + public boolean hasCapabilities(final Capability... capabilities) + { + return true; + } + + private static final String NAME_URL = "http://www.rmv.de/auskunft/bin/jp/stboard.exe/dox?input="; + private static final Pattern P_SINGLE_NAME = Pattern + .compile(".*.*", Pattern.DOTALL); + private static final Pattern P_MULTI_NAME = Pattern.compile("
\\s*(.*?)\\s*", Pattern.DOTALL); + + public List autoCompleteStationName(final CharSequence constraint) throws IOException + { + final CharSequence page = ParserUtils.scrape(NAME_URL + ParserUtils.urlEncode(constraint.toString())); + + final List names = new ArrayList(); + + final Matcher mSingle = P_SINGLE_NAME.matcher(page); + if (mSingle.matches()) + { + names.add(ParserUtils.resolveEntities(mSingle.group(1))); + } + else + { + final Matcher mMulti = P_MULTI_NAME.matcher(page); + while (mMulti.find()) + names.add(ParserUtils.resolveEntities(mMulti.group(1))); + } + + return names; + } + + private final static Pattern P_NEARBY_STATIONS = Pattern.compile("\\n" + + "(.+?)\\s*\\((\\d+) m/[A-Z]+\\)\\n", Pattern.DOTALL); + + public List nearbyStations(final double lat, final double lon, final int maxDistance, final int maxStations) throws IOException + { + final String url = "http://www.rmv.de/auskunft/bin/jp/stboard.exe/dox?input=" + lat + "%20" + lon; + final CharSequence page = ParserUtils.scrape(url); + + final List stations = new ArrayList(); + + final Matcher m = P_NEARBY_STATIONS.matcher(page); + while (m.find()) + { + final int sId = Integer.parseInt(m.group(1)); + final String sName = ParserUtils.resolveEntities(m.group(2)); + final int sDist = Integer.parseInt(m.group(3)); + + final Station station = new Station(sId, sName, 0, 0, sDist, null, null); + stations.add(station); + } + + if (maxStations == 0 || maxStations >= stations.size()) + return stations; + else + return stations.subList(0, maxStations); + } + + public String connectionsQueryUri(final String from, final String via, final String to, final Date date, final boolean dep) + { + final DateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yy"); + final DateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm"); + final StringBuilder uri = new StringBuilder(); + + uri.append("http://www.rmv.de/auskunft/bin/jp/query.exe/dox"); + uri.append("?REQ0HafasInitialSelection=0"); + uri.append("&REQ0HafasSearchForw=").append(dep ? "1" : "0"); + uri.append("&REQ0JourneyDate=").append(ParserUtils.urlEncode(DATE_FORMAT.format(date))); + uri.append("&REQ0JourneyTime=").append(ParserUtils.urlEncode(TIME_FORMAT.format(date))); + uri.append("&REQ0JourneyStopsS0G=").append(ParserUtils.urlEncode(from)); + uri.append("&REQ0JourneyStopsS0A=255"); + uri.append("&REQ0JourneyStopsZ0G=").append(ParserUtils.urlEncode(to)); + uri.append("&REQ0JourneyStopsZ0A=255"); + if (via != null) + { + uri.append("&REQ0JourneyStops1.0G=").append(ParserUtils.urlEncode(via)); + uri.append("&REQ0JourneyStops1.0A=255"); + } + uri.append("&start=Suchen"); + + return uri.toString(); + } + + private static final Pattern P_PRE_ADDRESS = Pattern.compile("(?:Geben Sie einen (Startort|Zielort) an.*?)?Bitte wählen Sie aus der Liste", + Pattern.DOTALL); + private static final Pattern P_ADDRESSES = Pattern.compile( + ".*?\\s*(.*?)\\s*.*?", Pattern.DOTALL); + private static final Pattern P_CHECK_CONNECTIONS_ERROR = Pattern.compile( + "(?:(mehrfach vorhanden oder identisch)|(keine Verbindung gefunden werden))", Pattern.CASE_INSENSITIVE); + + public CheckConnectionsQueryResult checkConnectionsQuery(final String queryUri) throws IOException + { + final CharSequence page = ParserUtils.scrape(queryUri); + + final Matcher mError = P_CHECK_CONNECTIONS_ERROR.matcher(page); + if (mError.find()) + { + if (mError.group(1) != null) + return CheckConnectionsQueryResult.TOO_CLOSE; + if (mError.group(2) != null) + return CheckConnectionsQueryResult.NO_CONNECTIONS; + } + + List fromAddresses = null; + List viaAddresses = null; + List toAddresses = null; + + final Matcher mPreAddress = P_PRE_ADDRESS.matcher(page); + while (mPreAddress.find()) + { + final String type = mPreAddress.group(1); + + final Matcher mAddresses = P_ADDRESSES.matcher(page); + final List addresses = new ArrayList(); + while (mAddresses.find()) + { + ParserUtils.printGroups(mAddresses); + + final String address = ParserUtils.resolveEntities(mAddresses.group(1)).trim(); + if (!addresses.contains(address)) + addresses.add(address); + } + + if (type == null) + viaAddresses = addresses; + else if (type.equals("Startort")) + fromAddresses = addresses; + else if (type.equals("Zielort")) + toAddresses = addresses; + else + throw new IOException(type); + } + + if (fromAddresses != null || viaAddresses != null || toAddresses != null) + { + return new CheckConnectionsQueryResult(CheckConnectionsQueryResult.Status.AMBIGUOUS, fromAddresses, viaAddresses, toAddresses); + } + else + { + return CheckConnectionsQueryResult.OK; + } + } + + private static final Pattern P_CONNECTIONS_HEAD = Pattern.compile(".*" // + + "Von: (.*?).*?" // + + "Nach: (.*?).*?" // + + "Datum: .., (\\d+\\..\\d+\\.\\d+).*?" // + + "(?:Früher.*?)?" // + + "(?:Später.*?)?", Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_COARSE = Pattern.compile("

(.+?)

", Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_FINE = Pattern.compile(".*?
" // url + + "(\\d+:\\d+)-(\\d+:\\d+)" // + + "(?: (.+?))?", Pattern.DOTALL); + + public QueryConnectionsResult queryConnections(final String uri) throws IOException + { + final CharSequence page = ParserUtils.scrape(uri); + + final Matcher mHead = P_CONNECTIONS_HEAD.matcher(page); + if (mHead.matches()) + { + final String from = ParserUtils.resolveEntities(mHead.group(1)); + final String to = ParserUtils.resolveEntities(mHead.group(2)); + final Date currentDate = ParserUtils.parseDate(mHead.group(3)); + final String linkEarlier = mHead.group(4) != null ? ParserUtils.resolveEntities(mHead.group(4)) : null; + final String linkLater = mHead.group(5) != null ? ParserUtils.resolveEntities(mHead.group(5)) : null; + final List connections = new ArrayList(); + + final Matcher mConCoarse = P_CONNECTIONS_COARSE.matcher(page); + while (mConCoarse.find()) + { + final Matcher mConFine = P_CONNECTIONS_FINE.matcher(mConCoarse.group(1)); + if (mConFine.matches()) + { + final String link = ParserUtils.resolveEntities(mConFine.group(1)); + Date departure = ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mConFine.group(2))); + if (!connections.isEmpty()) + { + final long diff = ParserUtils.timeDiff(departure, + ((Connection.Trip) connections.get(connections.size() - 1).parts.get(0)).departureTime); + if (diff > PARSER_DAY_ROLLOVER_THRESHOLD_MS) + departure = ParserUtils.addDays(departure, -1); + else if (diff < -PARSER_DAY_ROLLOVER_THRESHOLD_MS) + departure = ParserUtils.addDays(departure, 1); + } + Date arrival = ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mConFine.group(3))); + if (departure.after(arrival)) + arrival = ParserUtils.addDays(arrival, 1); + String line = mConFine.group(4); + if (line != null && !line.endsWith("Um.")) + line = normalizeLine(line); + else + line = null; + final Connection connection = new Connection(link, departure, arrival, from, to, new ArrayList(1)); + connection.parts.add(new Connection.Trip(departure, arrival, line, line != null ? LINES.get(line.charAt(0)) : null)); + connections.add(connection); + } + else + { + throw new IllegalArgumentException("cannot parse '" + mConCoarse.group(1) + "' on " + uri); + } + } + + return new QueryConnectionsResult(from, to, currentDate, linkEarlier, linkLater, connections); + } + else + { + throw new IOException(page.toString()); + } + } + + private static final Pattern P_CONNECTION_DETAILS_HEAD = Pattern.compile(".*?

\n?" // + + "- (.*?) -.*?" // firstDeparture + + "Abfahrt: (\\d+\\.\\d+\\.\\d+)
\n?"// date + + "Dauer: (\\d+:\\d+)
.*?" // duration + , Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_COARSE = Pattern.compile("/b> -\n?(.*?- .*?)<", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_FINE = Pattern.compile("
\n?" // + + "(?:(.*?) nach (.*?)\n?" // line, destination + + "
\n?" // + + "ab (\\d+:\\d+)\n?" // departureTime + + "(.*?)\\s*\n?" // departurePosition + + "
\n?" // + + "an (\\d+:\\d+)\n?" // arrivalTime + + "(.*?)\\s*\n?" // arrivalPosition + + "
\n?|" // + + "\n?" // + + "Fussweg\\s*\n?" // + + "\n?" // + + "(\\d+) Min.
\n?)" // footway + + "- (.*?)" // arrival + , Pattern.DOTALL); + + public GetConnectionDetailsResult getConnectionDetails(final String uri) throws IOException + { + final CharSequence page = ParserUtils.scrape(uri); + + final Matcher mHead = P_CONNECTION_DETAILS_HEAD.matcher(page); + if (mHead.matches()) + { + final String firstDeparture = ParserUtils.resolveEntities(mHead.group(1)); + final Date currentDate = ParserUtils.parseDate(mHead.group(2)); + final List parts = new ArrayList(4); + + Date firstDepartureTime = null; + Date lastArrivalTime = null; + String lastArrival = null; + Connection.Trip lastTrip = null; + + final Matcher mDetCoarse = P_CONNECTION_DETAILS_COARSE.matcher(page); + while (mDetCoarse.find()) + { + final Matcher mDetFine = P_CONNECTION_DETAILS_FINE.matcher(mDetCoarse.group(1)); + if (mDetFine.matches()) + { + final String departure = lastArrival != null ? lastArrival : firstDeparture; + + final String arrival = ParserUtils.resolveEntities(mDetFine.group(8)); + lastArrival = arrival; + + final String min = mDetFine.group(7); + if (min == null) + { + final String line = normalizeLine(ParserUtils.resolveEntities(mDetFine.group(1))); + + final String destination = ParserUtils.resolveEntities(mDetFine.group(2)); + + final Date departureTime = ParserUtils.parseTime(mDetFine.group(3)); + + final String departurePosition = ParserUtils.resolveEntities(mDetFine.group(4)); + + // final Date departureDate = ParserUtils.parseDate(mDet.group(5)); + + final Date arrivalTime = ParserUtils.parseTime(mDetFine.group(5)); + + final String arrivalPosition = ParserUtils.resolveEntities(mDetFine.group(6)); + + // final Date arrivalDate = ParserUtils.parseDate(mDet.group(9)); + + final Date departureDateTime = ParserUtils.joinDateTime(currentDate, departureTime); + final Date arrivalDateTime = ParserUtils.joinDateTime(currentDate, arrivalTime); + lastTrip = new Connection.Trip(line, LINES.get(line.charAt(0)), destination, departureDateTime, departurePosition, departure, + arrivalDateTime, arrivalPosition, arrival); + parts.add(lastTrip); + + if (firstDepartureTime == null) + firstDepartureTime = departureDateTime; + + lastArrivalTime = arrivalDateTime; + } + else + { + if (parts.size() > 0 && parts.get(parts.size() - 1) instanceof Connection.Footway) + { + final Connection.Footway lastFootway = (Connection.Footway) parts.remove(parts.size() - 1); + parts.add(new Connection.Footway(lastFootway.min + Integer.parseInt(min), lastFootway.departure, arrival)); + } + else + { + parts.add(new Connection.Footway(Integer.parseInt(min), departure, arrival)); + } + } + } + else + { + throw new IllegalArgumentException("cannot parse '" + mDetCoarse.group(1) + "' on " + uri); + } + } + + return new GetConnectionDetailsResult(currentDate, new Connection(uri, firstDepartureTime, lastArrivalTime, firstDeparture, lastArrival, + parts)); + } + else + { + throw new IOException(page.toString()); + } + } + + public String getDeparturesUri(final String stationId) + { + final DateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yy"); + final DateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm"); + final Date now = new Date(); + return "http://www.rmv.de/auskunft/bin/jp/stboard.exe/dox?input=" + stationId + "&boardType=dep&time=" + TIME_FORMAT.format(now) + "&date=" + + DATE_FORMAT.format(now) + "&start=yes"; + } + + private static final Pattern P_DEPARTURES_HEAD = Pattern.compile(".*

.*?" // + + "(.*?)
.*?" // + + "Abfahrt (\\d+:\\d+).*?" // + + "Uhr, (\\d+\\.\\d+\\.\\d+).*?" // + + "

.*", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_COARSE = Pattern.compile("

(.+?)

", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_FINE = Pattern.compile(".*?\\s*(.*?)\\s*.*?" // line + + ">>\n?" // + + "(.*?)\n?" // destination + + "
.*?" // + + "(\\d+:\\d+).*?" // time + + "(?:Gl\\. (\\d+)
.*?)?", Pattern.DOTALL); + + public GetDeparturesResult getDepartures(final String stationId, final Product[] products, final int maxDepartures) throws IOException + { + final CharSequence page = ParserUtils.scrape(getDeparturesUri(stationId)); + + // parse page + final Matcher mHead = P_DEPARTURES_HEAD.matcher(page); + if (mHead.matches()) + { + final String location = ParserUtils.resolveEntities(mHead.group(1)); + final Date currentTime = ParserUtils.joinDateTime(ParserUtils.parseDate(mHead.group(3)), ParserUtils.parseTime(mHead.group(2))); + final List departures = new ArrayList(maxDepartures); + + // choose matcher + final Matcher mDepCoarse = P_DEPARTURES_COARSE.matcher(page); + while (mDepCoarse.find() && (maxDepartures == 0 || departures.size() < maxDepartures)) + { + final Matcher mDepFine = P_DEPARTURES_FINE.matcher(mDepCoarse.group(1)); + if (mDepFine.matches()) + { + final Departure dep = parseDeparture(mDepFine, currentTime); + if (products == null || filter(dep.line.charAt(0), products)) + if (!departures.contains(dep)) + departures.add(dep); + } + else + { + throw new IllegalArgumentException("cannot parse '" + mDepCoarse.group(1) + "' on " + stationId); + } + } + + return new GetDeparturesResult(location, currentTime, departures); + } + else + { + return GetDeparturesResult.NO_INFO; + } + } + + private static Departure parseDeparture(final Matcher mDep, final Date currentTime) + { + // line + String line = normalizeLine(ParserUtils.resolveEntities(mDep.group(1))); + if (line.length() == 0) + line = null; + final int[] lineColors = line != null ? LINES.get(line.charAt(0)) : null; + + // destination + final String destination = ParserUtils.resolveEntities(mDep.group(2)); + + // time + final Calendar current = new GregorianCalendar(); + current.setTime(currentTime); + final Calendar parsed = new GregorianCalendar(); + parsed.setTime(ParserUtils.parseTime(mDep.group(3))); + parsed.set(Calendar.YEAR, current.get(Calendar.YEAR)); + parsed.set(Calendar.MONTH, current.get(Calendar.MONTH)); + parsed.set(Calendar.DAY_OF_MONTH, current.get(Calendar.DAY_OF_MONTH)); + if (ParserUtils.timeDiff(parsed.getTime(), currentTime) < -PARSER_DAY_ROLLOVER_THRESHOLD_MS) + parsed.add(Calendar.DAY_OF_MONTH, 1); + + return new Departure(parsed.getTime(), line, lineColors, destination); + } + + private static boolean filter(final char line, final Product[] products) + { + final Product lineProduct = Product.fromCode(line); + + for (final Product p : products) + if (lineProduct == p) + return true; + + return false; + } + + private static final Pattern P_NORMALIZE_LINE = Pattern.compile("([A-Za-zÄÖÜäöüß]+)[\\s-]*(.*)"); + + private static String normalizeLine(final String line) + { + if (line.length() == 0) + return line; + + final Matcher m = P_NORMALIZE_LINE.matcher(line); + if (m.matches()) + { + final String type = m.group(1); + final String number = m.group(2).replace(" ", ""); + + if (type.equals("ICE")) // InterCityExpress + return "IICE" + number; + if (type.equals("IC")) // InterCity + return "IIC" + number; + if (type.equals("EC")) // EuroCity + return "IEC" + number; + if (type.equals("EN")) // EuroNight + return "IEN" + number; + if (type.equals("CNL")) // CityNightLine + return "ICNL" + number; + if (type.equals("RB")) // RegionalBahn + return "RRB" + number; + if (type.equals("RE")) // RegionalExpress + return "RRE" + number; + if (type.equals("SE")) // StadtExpress + return "RSE" + number; + if (type.equals("R")) + return "R" + number; + if (type.equals("S")) + return "SS" + number; + if (type.equals("U")) + return "UU" + number; + if (type.equals("Tram")) + return "T" + number; + if (type.equals("RT")) // RegioTram + return "TRT" + number; + if (type.startsWith("Bus")) + return "B" + type.substring(3) + number; + if (type.startsWith("AST")) // Anruf-Sammel-Taxi + return "BAST" + type.substring(3) + number; + if (type.startsWith("ALT")) // Anruf-Linien-Taxi + return "BALT" + type.substring(3) + number; + if (type.equals("LTaxi")) + return "BLTaxi" + number; + + throw new IllegalStateException("cannot normalize type " + type + " number " + number + " line " + line); + } + + throw new IllegalStateException("cannot normalize line " + line); + } + + public static final Map LINES = new HashMap(); + + static + { + LINES.put('I', new int[] { Color.WHITE, Color.RED, Color.RED }); + LINES.put('R', new int[] { Color.GRAY, Color.WHITE }); + LINES.put('S', new int[] { Color.parseColor("#006e34"), Color.WHITE }); + LINES.put('U', new int[] { Color.parseColor("#003090"), Color.WHITE }); + LINES.put('T', new int[] { Color.parseColor("#cc0000"), Color.WHITE }); + LINES.put('B', new int[] { Color.parseColor("#993399"), Color.WHITE }); + LINES.put('F', new int[] { Color.BLUE, Color.WHITE }); + } +} diff --git a/src/de/schildbach/pte/SessionExpiredException.java b/src/de/schildbach/pte/SessionExpiredException.java new file mode 100644 index 00000000..f2dd6321 --- /dev/null +++ b/src/de/schildbach/pte/SessionExpiredException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.io.IOException; + +/** + * @author Andreas Schildbach + */ +public class SessionExpiredException extends IOException +{ +} diff --git a/src/de/schildbach/pte/StandardColors.java b/src/de/schildbach/pte/StandardColors.java new file mode 100644 index 00000000..6c39d184 --- /dev/null +++ b/src/de/schildbach/pte/StandardColors.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Andreas Schildbach + */ +public class StandardColors +{ + public static final Map LINES = new HashMap(); + + static + { + LINES.put('I', new int[] { Color.WHITE, Color.RED, Color.RED }); + LINES.put('R', new int[] { Color.GRAY, Color.WHITE }); + LINES.put('S', new int[] { Color.parseColor("#006e34"), Color.WHITE }); + LINES.put('U', new int[] { Color.parseColor("#003090"), Color.WHITE }); + LINES.put('T', new int[] { Color.parseColor("#cc0000"), Color.WHITE }); + LINES.put('B', new int[] { Color.parseColor("#993399"), Color.WHITE }); + LINES.put('F', new int[] { Color.BLUE, Color.WHITE }); + } +} diff --git a/src/de/schildbach/pte/Station.java b/src/de/schildbach/pte/Station.java new file mode 100644 index 00000000..ec60d649 --- /dev/null +++ b/src/de/schildbach/pte/Station.java @@ -0,0 +1,66 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.util.Date; + +/** + * @author Andreas Schildbach + */ +public final class Station +{ + // data + public final int id; + public final String name; + public final double latitude, longitude; + public float distance; + public final String[] lines; + public final int[][] lineColors; + + // transient + public transient Date lastUpdatedDepartures; + + public Station(final int id, final String name, final double latitude, final double longitude, final float distance, final String[] lines, + final int[][] lineColors) + { + this.id = id; + this.name = name; + this.latitude = latitude; + this.longitude = longitude; + this.distance = distance; + this.lines = lines; + this.lineColors = lineColors; + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (!(o instanceof Station)) + return false; + final Station other = (Station) o; + return this.id == other.id; + } + + @Override + public int hashCode() + { + return id; + } +} diff --git a/src/de/schildbach/pte/VbbProvider.java b/src/de/schildbach/pte/VbbProvider.java new file mode 100644 index 00000000..46c5bfa1 --- /dev/null +++ b/src/de/schildbach/pte/VbbProvider.java @@ -0,0 +1,641 @@ +/* + * Copyright 2010 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.schildbach.pte; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Andreas Schildbach + */ +public final class VbbProvider implements NetworkProvider +{ + public static final String NETWORK_ID = "mobil.bvg.de"; + + private static final long PARSER_DAY_ROLLOVER_THRESHOLD_MS = 12 * 60 * 60 * 1000; + private static final long PARSER_DAY_ROLLDOWN_THRESHOLD_MS = 6 * 60 * 60 * 1000; + + private static final String BVG_BASE_URL = "http://mobil.bvg.de"; + + public boolean hasCapabilities(Capability... capabilities) + { + for (final Capability capability : capabilities) + if (capability == Capability.NEARBY_STATIONS) + return false; + + return true; + } + + private static final Pattern P_AUTOCOMPLETE_IS_MAST = Pattern.compile("\\d{6}"); + private static final String AUTOCOMPLETE_NAME_URL = "http://mobil.bvg.de/Fahrinfo/bin/stboard.bin/dox/dox?input="; + private static final Pattern P_SINGLE_NAME = Pattern.compile(".*Haltestelleninfo.*?(.*?).*", Pattern.DOTALL); + private static final Pattern P_MULTI_NAME = Pattern.compile("\\s*(.*?)\\s*", Pattern.DOTALL); + private static final String AUTOCOMPLETE_MASTID_URL = "http://mobil.bvg.de/IstAbfahrtzeiten/index/mobil?input="; + private static final Pattern P_SINGLE_MASTID = Pattern.compile(".*Ist-Abfahrtzeiten.*?(.*?).*", Pattern.DOTALL); + + public List autoCompleteStationName(CharSequence constraint) throws IOException + { + final List names = new ArrayList(); + + if (P_AUTOCOMPLETE_IS_MAST.matcher(constraint).matches()) + { + final CharSequence page = ParserUtils.scrape(AUTOCOMPLETE_MASTID_URL + ParserUtils.urlEncode(constraint.toString())); + + final Matcher mSingle = P_SINGLE_MASTID.matcher(page); + if (mSingle.matches()) + { + names.add(ParserUtils.resolveEntities(mSingle.group(1))); + } + } + else + { + final CharSequence page = ParserUtils.scrape(AUTOCOMPLETE_NAME_URL + ParserUtils.urlEncode(constraint.toString())); + + final Matcher mSingle = P_SINGLE_NAME.matcher(page); + if (mSingle.matches()) + { + names.add(ParserUtils.resolveEntities(mSingle.group(1))); + } + else + { + final Matcher mMulti = P_MULTI_NAME.matcher(page); + while (mMulti.find()) + names.add(ParserUtils.resolveEntities(mMulti.group(1))); + } + } + + return names; + } + + public List nearbyStations(final double lat, final double lon, final int maxDistance, final int maxStations) throws IOException + { + throw new UnsupportedOperationException(); + } + + public static final String STATION_URL_CONNECTION = "http://mobil.bvg.de/Fahrinfo/bin/query.bin/dox"; + + public String connectionsQueryUri(final String from, final String via, final String to, final Date date, final boolean dep) + { + final DateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yy"); + final DateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm"); + final StringBuilder uri = new StringBuilder(); + + uri.append("http://mobil.bvg.de/Fahrinfo/bin/query.bin/dox"); + uri.append("?REQ0HafasInitialSelection=0"); + uri.append("&REQ0JourneyStopsS0A=255"); + uri.append("&REQ0JourneyStopsS0G=").append(ParserUtils.urlEncode(from)); + if (via != null) + { + uri.append("&REQ0JourneyStops1A=1"); + uri.append("&REQ0JourneyStops1G=").append(ParserUtils.urlEncode(via)); + } + uri.append("&REQ0JourneyStopsZ0A=255"); + uri.append("&REQ0JourneyStopsZ0G=").append(ParserUtils.urlEncode(to)); + uri.append("&REQ0HafasSearchForw=").append(dep ? "1" : "0"); + uri.append("&REQ0JourneyDate=").append(ParserUtils.urlEncode(DATE_FORMAT.format(date))); + uri.append("&REQ0JourneyTime=").append(ParserUtils.urlEncode(TIME_FORMAT.format(date))); + uri.append("&start=Suchen"); + + return uri.toString(); + } + + private static final Pattern P_CHECK_ADDRESS = Pattern.compile("\\s*(.*?)\\s*", Pattern.DOTALL); + private static final Pattern P_CHECK_FROM = Pattern.compile("Von:"); + private static final Pattern P_CHECK_TO = Pattern.compile("Nach:"); + private static final Pattern P_CHECK_TOO_CLOSE = Pattern.compile("zu dicht beieinander"); + + public CheckConnectionsQueryResult checkConnectionsQuery(final String uri) throws IOException + { + final CharSequence page = ParserUtils.scrape(uri); + + if (P_CHECK_TOO_CLOSE.matcher(page).find()) + return CheckConnectionsQueryResult.TOO_CLOSE; + + final Matcher mAddress = P_CHECK_ADDRESS.matcher(page); + + final List addresses = new ArrayList(); + while (mAddress.find()) + { + final String address = ParserUtils.resolveEntities(mAddress.group(1)); + if (!addresses.contains(address)) + addresses.add(address); + } + + if (addresses.isEmpty()) + { + return CheckConnectionsQueryResult.OK; + } + else if (P_CHECK_FROM.matcher(page).find()) + { + if (P_CHECK_TO.matcher(page).find()) + return new CheckConnectionsQueryResult(CheckConnectionsQueryResult.Status.AMBIGUOUS, null, addresses, null); + else + return new CheckConnectionsQueryResult(CheckConnectionsQueryResult.Status.AMBIGUOUS, null, null, addresses); + } + else + { + return new CheckConnectionsQueryResult(CheckConnectionsQueryResult.Status.AMBIGUOUS, addresses, null, null); + } + } + + private static final Pattern P_CONNECTIONS_HEAD = Pattern.compile( + ".*Von: (.*?).*?Nach: (.*?).*?Datum: .., (.*?)
.*?" + + "(?:.*?)?" + + "(?:.*?)?", Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_COARSE = Pattern.compile("

(.+?)

", Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_FINE = Pattern.compile(".*?
" + + "(\\d\\d:\\d\\d)-(\\d\\d:\\d\\d)  (?:\\d+ Umst\\.|([\\w\\d ]+)).*?", Pattern.DOTALL); + + public QueryConnectionsResult queryConnections(final String uri) throws IOException + { + final CharSequence page = ParserUtils.scrape(uri); + + final Matcher mHead = P_CONNECTIONS_HEAD.matcher(page); + if (mHead.matches()) + { + final String from = ParserUtils.resolveEntities(mHead.group(1)); + final String to = ParserUtils.resolveEntities(mHead.group(2)); + final Date currentDate = ParserUtils.parseDate(mHead.group(3)); + final String linkEarlier = mHead.group(4) != null ? BVG_BASE_URL + ParserUtils.resolveEntities(mHead.group(4)) : null; + final String linkLater = mHead.group(5) != null ? BVG_BASE_URL + ParserUtils.resolveEntities(mHead.group(5)) : null; + final List connections = new ArrayList(); + + final Matcher mConCoarse = P_CONNECTIONS_COARSE.matcher(page); + while (mConCoarse.find()) + { + final Matcher mConFine = P_CONNECTIONS_FINE.matcher(mConCoarse.group(1)); + if (mConFine.matches()) + { + final String link = BVG_BASE_URL + ParserUtils.resolveEntities(mConFine.group(1)); + Date departure = ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mConFine.group(2))); + if (!connections.isEmpty()) + { + final long diff = ParserUtils.timeDiff(departure, + ((Connection.Trip) connections.get(connections.size() - 1).parts.get(0)).departureTime); + if (diff > PARSER_DAY_ROLLOVER_THRESHOLD_MS) + departure = ParserUtils.addDays(departure, -1); + else if (diff < -PARSER_DAY_ROLLDOWN_THRESHOLD_MS) + departure = ParserUtils.addDays(departure, 1); + } + Date arrival = ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mConFine.group(3))); + if (departure.after(arrival)) + arrival = ParserUtils.addDays(arrival, 1); + String line = mConFine.group(4); + if (line != null) + line = normalizeLine(ParserUtils.resolveEntities(line)); + final Connection connection = new Connection(link, departure, arrival, from, to, new ArrayList(1)); + connection.parts.add(new Connection.Trip(departure, arrival, line, LINES.get(line))); + connections.add(connection); + } + else + { + throw new IllegalArgumentException("cannot parse '" + mConCoarse.group(1) + "' on " + uri); + } + } + + return new QueryConnectionsResult(from, to, currentDate, linkEarlier, linkLater, connections); + } + else + { + throw new IOException(page.toString()); + } + } + + private static final Pattern P_CONNECTION_DETAILS_HEAD = Pattern.compile(".*(?:Datum|Abfahrt): (\\d\\d\\.\\d\\d\\.\\d\\d).*", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_COARSE = Pattern.compile("

\n?(.+?)\n?

", Pattern.DOTALL); + static final Pattern P_CONNECTION_DETAILS_FINE = Pattern.compile("(?:(?:\\n?)?(.+?)(?:\\n?)?)?.*?" // departure + + "(?:" // + + "ab (\\d+:\\d+)\n?" // departureTime + + "(Gl\\. \\d+)?.*?" // departurePosition + + "\\s*(.*?)\\s*.*?" // line + + "Ri\\. (.*?)[\n\\.]*<.*?" // destination + + "an (\\d+:\\d+)\n?" // arrivalTime + + "(Gl\\. \\d+)?.*?" // arrivalPosition + + "(.*?)" // arrival + + "|" // + + "(\\d+) Min\\.[\n\\s]?" // footway + + "Fussweg\n?" // + + ".*?(?:(.*?)|(\\w.*?)).*?" // arrival + + ").*?", Pattern.DOTALL); + + public GetConnectionDetailsResult getConnectionDetails(final String uri) throws IOException + { + final CharSequence page = ParserUtils.scrape(uri); + + final Matcher mHead = P_CONNECTION_DETAILS_HEAD.matcher(page); + if (mHead.matches()) + { + final Date currentDate = ParserUtils.parseDate(mHead.group(1)); + final List parts = new ArrayList(4); + + Date firstDepartureTime = null; + String firstDeparture = null; + Date lastArrivalTime = null; + String lastArrival = null; + + final Matcher mDetCoarse = P_CONNECTION_DETAILS_COARSE.matcher(page); + while (mDetCoarse.find()) + { + final Matcher mDetFine = P_CONNECTION_DETAILS_FINE.matcher(mDetCoarse.group(1)); + if (mDetFine.matches()) + { + String departure = ParserUtils.resolveEntities(mDetFine.group(1)); + if (departure == null) + departure = lastArrival; + if (departure != null && firstDeparture == null) + firstDeparture = departure; + + final String min = mDetFine.group(9); + if (min == null) + { + Date departureTime = ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mDetFine.group(2))); + if (lastArrivalTime != null && departureTime.before(lastArrivalTime)) + departureTime = ParserUtils.addDays(departureTime, 1); + + final String departurePosition = mDetFine.group(3); + + final String line = normalizeLine(ParserUtils.resolveEntities(mDetFine.group(4))); + + final String destination = ParserUtils.resolveEntities(mDetFine.group(5)); + + Date arrivalTime = ParserUtils.joinDateTime(currentDate, ParserUtils.parseTime(mDetFine.group(6))); + if (departureTime.after(arrivalTime)) + arrivalTime = ParserUtils.addDays(arrivalTime, 1); + + final String arrivalPosition = mDetFine.group(7); + + final String arrival = ParserUtils.resolveEntities(mDetFine.group(8)); + + parts.add(new Connection.Trip(line, LINES.get(line), destination, departureTime, departurePosition, departure, arrivalTime, + arrivalPosition, arrival)); + + if (firstDepartureTime == null) + firstDepartureTime = departureTime; + + lastArrival = arrival; + lastArrivalTime = arrivalTime; + } + else + { + final String arrival = ParserUtils.resolveEntities(selectNotNull(mDetFine.group(10), mDetFine.group(11))); + + if (parts.size() > 0 && parts.get(parts.size() - 1) instanceof Connection.Footway) + { + final Connection.Footway lastFootway = (Connection.Footway) parts.remove(parts.size() - 1); + parts.add(new Connection.Footway(lastFootway.min + Integer.parseInt(min), lastFootway.departure, arrival)); + } + else + { + parts.add(new Connection.Footway(Integer.parseInt(min), departure, arrival)); + } + + lastArrival = arrival; + } + } + else + { + throw new IllegalArgumentException("cannot parse '" + mDetCoarse.group(1) + "' on " + uri); + } + } + + if (firstDepartureTime != null && lastArrivalTime != null) + return new GetConnectionDetailsResult(currentDate, new Connection(uri, firstDepartureTime, lastArrivalTime, firstDeparture, + lastArrival, parts)); + else + return new GetConnectionDetailsResult(currentDate, null); + } + else + { + throw new IOException(page.toString()); + } + } + + private static String selectNotNull(final String... groups) + { + String selected = null; + for (final String group : groups) + { + if (group != null) + { + if (selected == null) + selected = group; + else + throw new IllegalStateException("ambiguous"); + } + } + return selected; + } + + private static final String DEPARTURE_URL_LIVE = "http://mobil.bvg.de/IstAbfahrtzeiten/index/mobil?input="; + private static final String DEPARTURE_URL_PLAN = "http://mobil.bvg.de/Fahrinfo/bin/stboard.bin/dox/dox?boardType=dep&start=yes&maxJourneys=12&input="; + + public String getDeparturesUri(String stationId) + { + final boolean live = stationId.length() == 6; + return live ? DEPARTURE_URL_LIVE + stationId : DEPARTURE_URL_PLAN + stationId; + } + + private static final Pattern P_DEPARTURES_HEAD = Pattern.compile(".*(.*?).*Datum:(.*?)
.*", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_COARSE = Pattern.compile( + "\\s*((?:|).+?)\\s*", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_LIVE_FINE = Pattern.compile("\\s*(.*?)[\\s\\*]*\\s*" // + + "\\s*(.*?)\\s*\\s*" // + + ".*?\\s*(.*?)\\s*.*?", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_PLAN_FINE = Pattern.compile("\\s*(.*?)[\\s\\*]*\\s*" // + + "\\s*\\s*(.*?)[\\s\\*]*.*?\\s*" // + + "\\s*\\s*(.*?)\\s*\\s*", Pattern.DOTALL); + private static final Pattern P_DEPARTURES_SERVICE_DOWN = Pattern.compile("Wartungsarbeiten"); + + public GetDeparturesResult getDepartures(final String stationId, final Product[] products, final int maxDepartures) throws IOException + { + final CharSequence page = ParserUtils.scrape(getDeparturesUri(stationId)); + + if (P_DEPARTURES_SERVICE_DOWN.matcher(page).find()) + return GetDeparturesResult.SERVICE_DOWN; + + // parse page + final Matcher mHead = P_DEPARTURES_HEAD.matcher(page); + if (mHead.matches()) + { + final String location = ParserUtils.resolveEntities(mHead.group(1)); + final Date currentTime = parseDate(mHead.group(2)); + final List departures = new ArrayList(maxDepartures); + + // choose matcher + final Matcher mDepCoarse = P_DEPARTURES_COARSE.matcher(page); + while (mDepCoarse.find() && (maxDepartures == 0 || departures.size() < maxDepartures)) + { + final boolean live = stationId.length() == 6; + final Matcher mDepFine = (live ? P_DEPARTURES_LIVE_FINE : P_DEPARTURES_PLAN_FINE).matcher(mDepCoarse.group(1)); + if (mDepFine.matches()) + { + final Departure dep = parseDeparture(mDepFine, currentTime); + if (products == null || filter(dep.line.charAt(0), products)) + if (!departures.contains(dep)) + departures.add(dep); + } + else + { + throw new IllegalArgumentException("cannot parse '" + mDepCoarse.group(1) + "' on " + stationId); + } + } + + return new GetDeparturesResult(location, currentTime, departures); + } + else + { + return GetDeparturesResult.NO_INFO; + } + } + + private static Departure parseDeparture(final Matcher mDep, final Date currentTime) + { + // time + final Calendar current = new GregorianCalendar(); + current.setTime(currentTime); + final Calendar parsed = new GregorianCalendar(); + parsed.setTime(ParserUtils.parseTime(mDep.group(1))); + parsed.set(Calendar.YEAR, current.get(Calendar.YEAR)); + parsed.set(Calendar.MONTH, current.get(Calendar.MONTH)); + parsed.set(Calendar.DAY_OF_MONTH, current.get(Calendar.DAY_OF_MONTH)); + if (ParserUtils.timeDiff(parsed.getTime(), currentTime) < -PARSER_DAY_ROLLOVER_THRESHOLD_MS) + parsed.add(Calendar.DAY_OF_MONTH, 1); + + // line + final String line = normalizeLine(ParserUtils.resolveEntities(mDep.group(2))); + + // destination + final String destination = ParserUtils.resolveEntities(mDep.group(3)); + + return new Departure(parsed.getTime(), line, LINES.get(line), destination); + } + + private static final Date parseDate(String str) + { + try + { + return new SimpleDateFormat("dd.MM.yyyy, HH:mm:ss").parse(str); + } + catch (ParseException x1) + { + try + { + return new SimpleDateFormat("dd.MM.yy").parse(str); + } + catch (ParseException x2) + { + throw new RuntimeException(x2); + } + } + } + + private boolean filter(final char line, final Product[] products) + { + final Product lineProduct = Product.fromCode(line); + + for (final Product p : products) + if (lineProduct == p) + return true; + + return false; + } + + private static final Pattern P_NORMALIZE_LINE = Pattern.compile("([A-Za-zÄÖÜäöüß]+)[\\s-]*(.*)"); + private static final Pattern P_NORMALIZE_LINE_SPECIAL_NUMBER = Pattern.compile("\\d{4,}"); + private static final Pattern P_NORMALIZE_LINE_SPECIAL_BUS = Pattern.compile("Bus[A-Z]"); + + private static String normalizeLine(final String line) + { + if (line.startsWith("RE") || line.startsWith("RB") || line.startsWith("NE") || line.startsWith("OE") || line.startsWith("MR") + || line.startsWith("PE")) + return "R" + line; + if (P_NORMALIZE_LINE_SPECIAL_NUMBER.matcher(line).matches()) + return "R" + line; + + final Matcher m = P_NORMALIZE_LINE.matcher(line); + if (m.matches()) + { + final String type = m.group(1); + final String number = m.group(2).replace(" ", ""); + + if (type.equals("ICE")) // InterCityExpress + return "IICE" + number; + if (type.equals("IC")) // InterCity + return "IIC" + number; + if (type.equals("EC")) // EuroCity + return "IEC" + number; + if (type.equals("EN")) // EuroNight + return "IEN" + number; + if (type.equals("CNL")) // CityNightLine + return "ICNL" + number; + if (type.equals("Zug")) + return "R" + number; + if (type.equals("D")) // D-Zug? + return "RD" + number; + if (type.equals("DNZ")) // unklar, aber vermutlich Russland + return "RDNZ" + (number.equals("DNZ") ? "" : number); + if (type.equals("KBS")) // Kursbuchstrecke + return "RKBS" + number; + if (type.equals("S")) + return "SS" + number; + if (type.equals("U")) + return "UU" + number; + if (type.equals("Tra") || type.equals("Tram")) + return "T" + number; + if (type.equals("Bus")) + return "B" + number; + if (P_NORMALIZE_LINE_SPECIAL_BUS.matcher(type).matches()) // workaround for weird scheme BusF/526 + return "B" + line.substring(3); + if (type.equals("Fäh")) + return "F" + number; + if (type.equals("F")) + return "FF" + number; + + throw new IllegalStateException("cannot normalize type " + type + " line " + line); + } + + throw new IllegalStateException("cannot normalize line " + line); + } + + public static final Map LINES = new HashMap(); + + static + { + LINES.put("I", new int[] { Color.WHITE, Color.RED, Color.RED }); // generic + LINES.put("R", new int[] { Color.WHITE, Color.RED, Color.RED }); // generic + LINES.put("S", new int[] { Color.parseColor("#006e34"), Color.WHITE }); // generic + LINES.put("U", new int[] { Color.parseColor("#003090"), Color.WHITE }); // generic + + LINES.put("SS1", new int[] { Color.rgb(221, 77, 174), Color.WHITE }); + LINES.put("SS2", new int[] { Color.rgb(16, 132, 73), Color.WHITE }); + LINES.put("SS25", new int[] { Color.rgb(16, 132, 73), Color.WHITE }); + LINES.put("SS3", new int[] { Color.rgb(22, 106, 184), Color.WHITE }); + LINES.put("SS41", new int[] { Color.rgb(162, 63, 48), Color.WHITE }); + LINES.put("SS42", new int[] { Color.rgb(191, 90, 42), Color.WHITE }); + LINES.put("SS45", new int[] { Color.rgb(191, 128, 55), Color.WHITE }); + LINES.put("SS46", new int[] { Color.rgb(191, 128, 55), Color.WHITE }); + LINES.put("SS47", new int[] { Color.rgb(191, 128, 55), Color.WHITE }); + LINES.put("SS5", new int[] { Color.rgb(243, 103, 23), Color.WHITE }); + LINES.put("SS7", new int[] { Color.rgb(119, 96, 176), Color.WHITE }); + LINES.put("SS75", new int[] { Color.rgb(119, 96, 176), Color.WHITE }); + LINES.put("SS8", new int[] { Color.rgb(85, 184, 49), Color.WHITE }); + LINES.put("SS85", new int[] { Color.rgb(85, 184, 49), Color.WHITE }); + LINES.put("SS9", new int[] { Color.rgb(148, 36, 64), Color.WHITE }); + + LINES.put("UU1", new int[] { Color.rgb(84, 131, 47), Color.WHITE }); + LINES.put("UU2", new int[] { Color.rgb(215, 25, 16), Color.WHITE }); + LINES.put("UU3", new int[] { Color.rgb(47, 152, 154), Color.WHITE }); + LINES.put("UU4", new int[] { Color.rgb(255, 233, 42), Color.BLACK }); + LINES.put("UU5", new int[] { Color.rgb(91, 31, 16), Color.WHITE }); + LINES.put("UU55", new int[] { Color.rgb(91, 31, 16), Color.WHITE }); + LINES.put("UU6", new int[] { Color.rgb(127, 57, 115), Color.WHITE }); + LINES.put("UU7", new int[] { Color.rgb(0, 153, 204), Color.WHITE }); + LINES.put("UU8", new int[] { Color.rgb(24, 25, 83), Color.WHITE }); + LINES.put("UU9", new int[] { Color.rgb(255, 90, 34), Color.WHITE }); + + LINES.put("TM1", new int[] { Color.rgb(204, 51, 0), Color.WHITE }); + LINES.put("TM2", new int[] { Color.rgb(116, 192, 67), Color.WHITE }); + LINES.put("TM4", new int[] { Color.rgb(208, 28, 34), Color.WHITE }); + LINES.put("TM5", new int[] { Color.rgb(204, 153, 51), Color.WHITE }); + LINES.put("TM6", new int[] { Color.rgb(0, 0, 255), Color.WHITE }); + LINES.put("TM8", new int[] { Color.rgb(255, 102, 0), Color.WHITE }); + LINES.put("TM10", new int[] { Color.rgb(0, 153, 51), Color.WHITE }); + LINES.put("TM13", new int[] { Color.rgb(51, 153, 102), Color.WHITE }); + LINES.put("TM17", new int[] { Color.rgb(153, 102, 51), Color.WHITE }); + + LINES.put("B12", new int[] { Color.rgb(153, 102, 255), Color.WHITE }); + LINES.put("B16", new int[] { Color.rgb(0, 0, 255), Color.WHITE }); + LINES.put("B18", new int[] { Color.rgb(255, 102, 0), Color.WHITE }); + LINES.put("B21", new int[] { Color.rgb(153, 102, 255), Color.WHITE }); + LINES.put("B27", new int[] { Color.rgb(153, 102, 51), Color.WHITE }); + LINES.put("B37", new int[] { Color.rgb(153, 102, 51), Color.WHITE }); + LINES.put("B50", new int[] { Color.rgb(51, 153, 102), Color.WHITE }); + LINES.put("B60", new int[] { Color.rgb(0, 153, 51), Color.WHITE }); + LINES.put("B61", new int[] { Color.rgb(0, 153, 51), Color.WHITE }); + LINES.put("B62", new int[] { Color.rgb(0, 102, 51), Color.WHITE }); + LINES.put("B63", new int[] { Color.rgb(51, 153, 102), Color.WHITE }); + LINES.put("B67", new int[] { Color.rgb(0, 102, 51), Color.WHITE }); + LINES.put("B68", new int[] { Color.rgb(0, 153, 51), Color.WHITE }); + + LINES.put("FF1", new int[] { Color.BLUE, Color.WHITE }); // Potsdam + LINES.put("FF10", new int[] { Color.BLUE, Color.WHITE }); + LINES.put("FF11", new int[] { Color.BLUE, Color.WHITE }); + LINES.put("FF12", new int[] { Color.BLUE, Color.WHITE }); + LINES.put("FF21", new int[] { Color.BLUE, Color.WHITE }); + LINES.put("FF23", new int[] { Color.BLUE, Color.WHITE }); + LINES.put("FF24", new int[] { Color.BLUE, Color.WHITE }); + + // Regional lines Brandenburg: + LINES.put("RRE1", new int[] { Color.parseColor("#EE1C23"), Color.WHITE }); + LINES.put("RRE2", new int[] { Color.parseColor("#FFD403"), Color.BLACK }); + LINES.put("RRE3", new int[] { Color.parseColor("#F57921"), Color.WHITE }); + LINES.put("RRE4", new int[] { Color.parseColor("#952D4F"), Color.WHITE }); + LINES.put("RRE5", new int[] { Color.parseColor("#0072BC"), Color.WHITE }); + LINES.put("RRE6", new int[] { Color.parseColor("#DB6EAB"), Color.WHITE }); + LINES.put("RRE7", new int[] { Color.parseColor("#00854A"), Color.WHITE }); + LINES.put("RRE10", new int[] { Color.parseColor("#A7653F"), Color.WHITE }); + LINES.put("RRE11", new int[] { Color.parseColor("#059EDB"), Color.WHITE }); + LINES.put("RRE11", new int[] { Color.parseColor("#EE1C23"), Color.WHITE }); + LINES.put("RRE15", new int[] { Color.parseColor("#FFD403"), Color.BLACK }); + LINES.put("RRE18", new int[] { Color.parseColor("#00A65E"), Color.WHITE }); + LINES.put("RRB10", new int[] { Color.parseColor("#60BB46"), Color.WHITE }); + LINES.put("RRB12", new int[] { Color.parseColor("#A3238E"), Color.WHITE }); + LINES.put("RRB13", new int[] { Color.parseColor("#F68B1F"), Color.WHITE }); + LINES.put("RRB13", new int[] { Color.parseColor("#00A65E"), Color.WHITE }); + LINES.put("RRB14", new int[] { Color.parseColor("#A3238E"), Color.WHITE }); + LINES.put("RRB20", new int[] { Color.parseColor("#00854A"), Color.WHITE }); + LINES.put("RRB21", new int[] { Color.parseColor("#5E6DB3"), Color.WHITE }); + LINES.put("RRB22", new int[] { Color.parseColor("#0087CB"), Color.WHITE }); + LINES.put("ROE25", new int[] { Color.parseColor("#0087CB"), Color.WHITE }); + LINES.put("RNE26", new int[] { Color.parseColor("#00A896"), Color.WHITE }); + LINES.put("RNE27", new int[] { Color.parseColor("#EE1C23"), Color.WHITE }); + LINES.put("RRB30", new int[] { Color.parseColor("#00A65E"), Color.WHITE }); + LINES.put("RRB31", new int[] { Color.parseColor("#60BB46"), Color.WHITE }); + LINES.put("RMR33", new int[] { Color.parseColor("#EE1C23"), Color.WHITE }); + LINES.put("ROE35", new int[] { Color.parseColor("#5E6DB3"), Color.WHITE }); + LINES.put("ROE36", new int[] { Color.parseColor("#A7653F"), Color.WHITE }); + LINES.put("RRB43", new int[] { Color.parseColor("#5E6DB3"), Color.WHITE }); + LINES.put("RRB45", new int[] { Color.parseColor("#FFD403"), Color.BLACK }); + LINES.put("ROE46", new int[] { Color.parseColor("#DB6EAB"), Color.WHITE }); + LINES.put("RMR51", new int[] { Color.parseColor("#DB6EAB"), Color.WHITE }); + LINES.put("RRB51", new int[] { Color.parseColor("#DB6EAB"), Color.WHITE }); + LINES.put("RRB54", new int[] { Color.parseColor("#FFD403"), Color.parseColor("#333333") }); + LINES.put("RRB55", new int[] { Color.parseColor("#F57921"), Color.WHITE }); + LINES.put("ROE60", new int[] { Color.parseColor("#60BB46"), Color.WHITE }); + LINES.put("ROE63", new int[] { Color.parseColor("#FFD403"), Color.BLACK }); + LINES.put("ROE65", new int[] { Color.parseColor("#0072BC"), Color.WHITE }); + LINES.put("RRB66", new int[] { Color.parseColor("#60BB46"), Color.WHITE }); + LINES.put("RPE70", new int[] { Color.parseColor("#FFD403"), Color.BLACK }); + LINES.put("RPE73", new int[] { Color.parseColor("#00A896"), Color.WHITE }); + LINES.put("RPE74", new int[] { Color.parseColor("#0072BC"), Color.WHITE }); + LINES.put("T89", new int[] { Color.parseColor("#EE1C23"), Color.WHITE }); + LINES.put("RRB91", new int[] { Color.parseColor("#A7653F"), Color.WHITE }); + LINES.put("RRB93", new int[] { Color.parseColor("#A7653F"), Color.WHITE }); + } +}