From 008a3746961a10eaefead629501eb0c694b681df Mon Sep 17 00:00:00 2001 From: "andreas.schildbach" Date: Tue, 24 Aug 2010 20:53:35 +0000 Subject: [PATCH] connections for Austria git-svn-id: https://public-transport-enabler.googlecode.com/svn/trunk@101 0924bc21-9374-b0fa-ee44-9ff1593b38f0 --- src/de/schildbach/pte/OebbProvider.java | 290 +++++++++++++++++- .../pte/live/OebbProviderLiveTest.java | 64 ++++ 2 files changed, 347 insertions(+), 7 deletions(-) create mode 100644 test/de/schildbach/pte/live/OebbProviderLiveTest.java diff --git a/src/de/schildbach/pte/OebbProvider.java b/src/de/schildbach/pte/OebbProvider.java index 4e653d88..a3553736 100644 --- a/src/de/schildbach/pte/OebbProvider.java +++ b/src/de/schildbach/pte/OebbProvider.java @@ -1,6 +1,8 @@ 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; @@ -22,10 +24,10 @@ public class OebbProvider implements NetworkProvider public boolean hasCapabilities(final Capability... capabilities) { for (final Capability capability : capabilities) - if (capability != Capability.DEPARTURES) - return false; + if (capability == Capability.DEPARTURES || capability == Capability.CONNECTIONS) + return true; - return true; + return false; } private static final String NAME_URL = "http://fahrplan.oebb.at/bin/stboard.exe/dn?input="; @@ -64,15 +66,289 @@ public class OebbProvider implements NetworkProvider throw new UnsupportedOperationException(); } - public QueryConnectionsResult queryConnections(LocationType fromType, String from, LocationType viaType, String via, LocationType toType, - String to, Date date, boolean dep) throws IOException + private String connectionsQueryUri(final String from, final String via, final String to, final Date date, final boolean dep) { - throw new UnsupportedOperationException(); + 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://fahrplan.oebb.at/bin/query.exe/dn?ld=web25&OK"); + uri.append("&REQ0HafasSearchForw=").append(dep ? "1" : "0"); + uri.append("&REQ0JourneyDate=").append(ParserUtils.urlEncode(DATE_FORMAT.format(date))); + uri.append("&REQ0JourneyStopsS0G=").append(ParserUtils.urlEncode(from)); + uri.append("&REQ0JourneyStopsS0A=255"); // 1=station, 2=city/street, 255=any + uri.append("&REQ0JourneyStopsS0ID="); // "tupel"? + if (via != null) + { + uri.append("&REQ0JourneyStops1.0G=").append(ParserUtils.urlEncode(via)); + uri.append("&REQ0JourneyStops1.0A=255"); // 1=station, 2=city/street, 255=any + uri.append("&REQ0JourneyStops1.0ID="); + } + uri.append("&REQ0JourneyStopsZ0G=").append(ParserUtils.urlEncode(to)); + uri.append("&REQ0JourneyStopsZ0A=255"); // 1=station, 2=city/street, 255=any + uri.append("&REQ0JourneyStopsZ0ID="); + uri.append("&REQ0JourneyTime=").append(ParserUtils.urlEncode(TIME_FORMAT.format(date))); + uri.append("&REQ0JourneyProduct_list=0:1111111111010000-000000"); + uri.append("&existHafasAttrInc=yes"); + uri.append("&existHafasDemo3=yes"); + uri.append("&queryPageDisplayed=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("(keine Verbindung gefunden werden)"); + + public QueryConnectionsResult queryConnections(final LocationType fromType, final String from, final LocationType viaType, final String via, + final LocationType toType, final String to, final Date date, final boolean dep) throws IOException + { + final String uri = connectionsQueryUri(from, via, to, date, dep); + final CharSequence page = ParserUtils.scrape(uri); + + final Matcher mError = P_CHECK_CONNECTIONS_ERROR.matcher(page); + if (mError.find()) + { + if (mError.group(1) != null) + return QueryConnectionsResult.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 QueryConnectionsResult(QueryConnectionsResult.Status.AMBIGUOUS, fromAddresses, viaAddresses, toAddresses); + else + return queryConnections(uri, page); } public QueryConnectionsResult queryMoreConnections(final String uri) throws IOException { - throw new UnsupportedOperationException(); + final CharSequence page = ParserUtils.scrape(uri); + + return queryConnections(uri, page); + } + + private static final Pattern P_CONNECTIONS_FORM_ACTION = Pattern.compile("" // + + "
" // action + , Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_PAGE = Pattern.compile(".*?" // + + ".*?" // action + + "\n(.*?)\n
.*?" // header + + "\n" // + + "(.*?
(.*?)
.*?)\n" // connections overview + + ".*?" // + + "\n" // + + "(.*?)\n" // connection details + + "
\n\\s*(.*?)\\s*.*?" // from + + "Datum:.*?.., (\\d{2}\\.\\d{2}\\.\\d{2}).*?" // date + + "nach:.*?\\s*(.*?)\\s*.*?" // to + + "(?:\"(REQ0HafasScrollDir=2&guiVCtrl_connection_detailsOut_add_selection&)\".*?)?" // linkEarlier + + "(?:\"(REQ0HafasScrollDir=1&guiVCtrl_connection_detailsOut_add_selection&)\".*?)?" // linkLater + , Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_COARSE = Pattern.compile("\n(.*?)", Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_FINE = Pattern.compile(".*?" // + + "name=\"guiVCtrl_connection_detailsOut_select_([\\w-]+)\".*?" // id + + "(\\d{2}\\.\\d{2}\\.\\d{2})" // departureDate + + "(?:
(\\d{2}\\.\\d{2}\\.\\d{2}))?.*?" // arrivalDate + + "(\\d{1,2}:\\d{2})" // departureTime + + "
(\\d{1,2}:\\d{2}).*?" // arrivalTime + , Pattern.DOTALL); + private static final Pattern P_CONNECTIONS_DETAILS_COARSE = Pattern.compile("Detailansicht" // id + + "(.*?)" // + + "\nDauer:", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_COARSE = Pattern.compile("\n(.*?)\n\n" // + + "\n(.*?)\n\n\n(.*?)\n", Pattern.DOTALL); + private static final Pattern P_CONNECTION_DETAILS_FINE = Pattern.compile(".*?" // + + "\n" // + + "(?:)?" // departureId + + "([^<]*?)<.*?" // departure + + "\n(?:(\\d{2}\\.\\d{2}\\.\\d{2})| )\n.*?" // departureDate + + "(?:(\\d{2}:\\d{2})| ).*?" // departureTime + + "\\s*(?: |(.*?))\\s*.*?" // departurePosition + + "(.*?).*?)?" // line + + "\n" // + + "(?:)?" // arrivalId + + "([^<]*?)<.*?" // arrival + + "\n(?:(\\d{2}\\.\\d{2}\\.\\d{2})| )\n.*?" // arrivalDate + + "(?:(\\d{2}:\\d{2})| ).*?" // arrivalTime + + "\\s*(?: |(.*?))\\s*.*?" // arrivalPosition + + "(?:ca\\. (\\d+) Min\\.\n.*?)?" // min + , Pattern.DOTALL); + + private QueryConnectionsResult queryConnections(final String firstUri, final CharSequence firstPage) throws IOException + { + // ugly workaround to fetch all details + final Matcher mFormAction = P_CONNECTIONS_FORM_ACTION.matcher(firstPage); + if (!mFormAction.find()) + throw new IOException("cannot find form action in '" + firstPage + "' on " + firstUri); + final String uri = mFormAction.group(1) + "&guiVCtrl_connection_detailsOut_add_group_overviewOut=yes"; + final CharSequence page = ParserUtils.scrape(uri); + + // parse page + final Matcher mPage = P_CONNECTIONS_PAGE.matcher(page); + if (mPage.matches()) + { + final String action = mPage.group(1); + final String headSet = mPage.group(2) + mPage.group(4); + + final Matcher mHead = P_CONNECTIONS_HEAD.matcher(headSet); + if (mHead.matches()) + { + final String from = ParserUtils.resolveEntities(mHead.group(1)); + final Date currentDate = ParserUtils.parseDate(mHead.group(2)); + final String to = ParserUtils.resolveEntities(mHead.group(3)); + final String linkEarlier = mHead.group(4) != null ? action + "&REQ0HafasScrollDir=2" + ParserUtils.resolveEntities(mHead.group(4)) + : null; + final String linkLater = mHead.group(5) != null ? action + "&REQ0HafasScrollDir=1" + ParserUtils.resolveEntities(mHead.group(5)) + : null; + final List connections = new ArrayList(); + + final Matcher mConCoarse = P_CONNECTIONS_COARSE.matcher(mPage.group(3)); + while (mConCoarse.find()) + { + final Matcher mConFine = P_CONNECTIONS_FINE.matcher(mConCoarse.group(1)); + if (mConFine.matches()) + { + final String id = mConFine.group(1); + final Date departureDate = ParserUtils.parseDate(mConFine.group(2)); + final Date arrivalDate = mConFine.group(3) != null ? ParserUtils.parseDate(mConFine.group(3)) : null; + final Date departureTime = ParserUtils.joinDateTime(departureDate, ParserUtils.parseTime(mConFine.group(4))); + final Date arrivalTime = ParserUtils.joinDateTime(arrivalDate != null ? arrivalDate : departureDate, ParserUtils + .parseTime(mConFine.group(5))); + final String link = uri + "#" + id; // TODO use print link? + + final Connection connection = new Connection(id, link, departureTime, arrivalTime, null, null, 0, from, 0, to, + new ArrayList(1)); + connections.add(connection); + } + else + { + throw new IllegalArgumentException("cannot parse '" + mConCoarse.group(1) + "' on " + uri); + } + } + + final Matcher mConDetCoarse = P_CONNECTIONS_DETAILS_COARSE.matcher(mPage.group(5)); + while (mConDetCoarse.find()) + { + final String id = mConDetCoarse.group(1); + final Connection connection = findConnection(connections, id); + + Date lastDate = null; + + final Matcher mDetCoarse = P_CONNECTION_DETAILS_COARSE.matcher(mConDetCoarse.group(2)); + while (mDetCoarse.find()) + { + final String set = mDetCoarse.group(1) + mDetCoarse.group(2) + mDetCoarse.group(3); + + final Matcher mDetFine = P_CONNECTION_DETAILS_FINE.matcher(set); + if (mDetFine.matches()) + { + final String departure = ParserUtils.resolveEntities(mDetFine.group(2)); + + Date departureDate = mDetFine.group(3) != null ? ParserUtils.parseDate(mDetFine.group(3)) : null; + if (departureDate != null) + lastDate = departureDate; + else + departureDate = lastDate; + + final String lineType = mDetFine.group(6); + + final String arrival = ParserUtils.resolveEntities(mDetFine.group(9)); + + Date arrivalDate = mDetFine.group(10) != null ? ParserUtils.parseDate(mDetFine.group(10)) : null; + if (arrivalDate != null) + lastDate = arrivalDate; + else + arrivalDate = lastDate; + + if (!lineType.equals("fuss")) + { + final int departureId = Integer.parseInt(mDetFine.group(1)); + + final Date departureTime = ParserUtils.joinDateTime(departureDate, ParserUtils.parseTime(mDetFine.group(4))); + + final String departurePosition = mDetFine.group(5) != null ? ParserUtils.resolveEntities(mDetFine.group(5)) : null; + + final String line = normalizeLine(lineType, ParserUtils.resolveEntities(mDetFine.group(7))); + + final int arrivalId = Integer.parseInt(mDetFine.group(8)); + + final Date arrivalTime = ParserUtils.joinDateTime(arrivalDate, ParserUtils.parseTime(mDetFine.group(11))); + + final String arrivalPosition = mDetFine.group(12) != null ? ParserUtils.resolveEntities(mDetFine.group(12)) : null; + + final Connection.Trip trip = new Connection.Trip(line, LINES.get(line.charAt(0)), null, departureTime, + departurePosition, departureId, departure, arrivalTime, arrivalPosition, arrivalId, arrival); + connection.parts.add(trip); + } + else + { + final int min = Integer.parseInt(mDetFine.group(13)); + + final Connection.Footway footway = new Connection.Footway(min, departure, arrival); + connection.parts.add(footway); + } + } + else + { + throw new IllegalArgumentException("cannot parse '" + set + "' on " + uri); + } + } + } + + return new QueryConnectionsResult(uri, from, to, currentDate, linkEarlier, linkLater, connections); + } + else + { + throw new IllegalArgumentException("cannot parse '" + headSet + "' on " + uri); + } + } + else + { + throw new IOException(page.toString()); + } + } + + private Connection findConnection(final List connections, final String id) + { + for (final Connection connection : connections) + if (connection.id.equals(id)) + return connection; + + return null; } public GetConnectionDetailsResult getConnectionDetails(final String connectionUri) throws IOException diff --git a/test/de/schildbach/pte/live/OebbProviderLiveTest.java b/test/de/schildbach/pte/live/OebbProviderLiveTest.java new file mode 100644 index 00000000..f978b56e --- /dev/null +++ b/test/de/schildbach/pte/live/OebbProviderLiveTest.java @@ -0,0 +1,64 @@ +/* + * 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.live; + +import java.util.Date; + +import org.junit.Test; + +import de.schildbach.pte.OebbProvider; +import de.schildbach.pte.QueryConnectionsResult; +import de.schildbach.pte.NetworkProvider.LocationType; + +/** + * @author Andreas Schildbach + */ +public class OebbProviderLiveTest +{ + private OebbProvider provider = new OebbProvider(); + + @Test + public void shortConnection() throws Exception + { + final QueryConnectionsResult result = provider.queryConnections(LocationType.ANY, "Linz", null, null, LocationType.ANY, "Berlin", new Date(), + true); + System.out.println(result); + final QueryConnectionsResult moreResult = provider.queryMoreConnections(result.linkLater); + System.out.println(moreResult); + } + + @Test + public void slowConnection() throws Exception + { + final QueryConnectionsResult result = provider.queryConnections(LocationType.ANY, "Ufhusen, Zollhus", null, null, LocationType.ANY, "Azuga", + new Date(), true); + System.out.println(result); + final QueryConnectionsResult moreResult = provider.queryMoreConnections(result.linkLater); + System.out.println(moreResult); + } + + @Test + public void connectionWithFootway() throws Exception + { + final QueryConnectionsResult result = provider.queryConnections(LocationType.ANY, "Graz, Haselweg", null, null, LocationType.ANY, + "Innsbruck, Gumppstraße 69!", new Date(), true); + System.out.println(result); + final QueryConnectionsResult moreResult = provider.queryMoreConnections(result.linkLater); + System.out.println(moreResult); + } +}