/*
* 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;
import de.schildbach.pte.QueryDeparturesResult.Status;
/**
* @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(final 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();
}
private static Pattern P_STATION_LOCATION = Pattern.compile(""
+ DATE_FORMAT.format(now)
+ ""
+ DATE_FORMAT.format(now) + "";
final String uri = "http://www.vbb-fahrinfo.de/hafas/extxml/extxml.exe/dn";
final CharSequence page = ParserUtils.scrape(uri, request);
final Matcher mError = P_STATION_LOCATION_ERROR.matcher(page);
if (mError.find())
{
if (mError.group(1) != null)
return null;
if (mError.group(2) != null)
throw new RuntimeException("timeout error");
}
final Matcher m = P_STATION_LOCATION.matcher(page);
if (m.find())
{
final String name = ParserUtils.resolveEntities(m.group(1));
final double lon = latLonToDouble(Integer.parseInt(m.group(2)));
final double lat = latLonToDouble(Integer.parseInt(m.group(3)));
return new StationLocationResult(lat, lon, name);
}
else
{
throw new IllegalArgumentException("cannot parse '" + page + "' on " + uri);
}
}
private static double latLonToDouble(int value)
{
return (double) value / 1000000;
}
public static final String STATION_URL_CONNECTION = "http://mobil.bvg.de/Fahrinfo/bin/query.bin/dox";
private String connectionsQueryUri(final LocationType fromType, final String from, final String via, final LocationType toType, 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");
// from
if (fromType == LocationType.ANY)
{
uri.append("&REQ0JourneyStopsS0A=255");
uri.append("&REQ0JourneyStopsS0G=").append(ParserUtils.urlEncode(from));
}
else
{
final String[] parts = from.split(",\\s*", 2);
final double lat = Double.parseDouble(parts[0]);
final double lon = Double.parseDouble(parts[1]);
uri.append("&SID=").append(ParserUtils.urlEncode("A=16@X=" + latLonToInt(lon) + "@Y=" + latLonToInt(lat)));
}
// via
if (via != null)
{
if (fromType == LocationType.ANY)
{
uri.append("&REQ0JourneyStops1A=1");
uri.append("&REQ0JourneyStops1G=").append(ParserUtils.urlEncode(via));
}
else
{
// FIXME
}
}
// to
if (toType == LocationType.ANY)
{
uri.append("&REQ0JourneyStopsZ0A=255");
uri.append("&REQ0JourneyStopsZ0G=").append(ParserUtils.urlEncode(to));
}
else
{
final String[] parts = to.split(",\\s*", 2);
final double lat = Double.parseDouble(parts[0]);
final double lon = Double.parseDouble(parts[1]);
uri.append("&ZID=").append(ParserUtils.urlEncode("A=16@X=" + latLonToInt(lon) + "@Y=" + latLonToInt(lat)));
}
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 int latLonToInt(double value)
{
return (int) (value * 1000000);
}
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_CONNECTIONS_ERROR = Pattern.compile("(zu dicht beieinander)|(keine Verbindung gefunden)");
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(fromType, from, via, toType, 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.TOO_CLOSE;
if (mError.group(2) != null)
return QueryConnectionsResult.NO_CONNECTIONS;
}
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 queryConnections(uri, page);
}
else if (P_CHECK_FROM.matcher(page).find())
{
if (P_CHECK_TO.matcher(page).find())
return new QueryConnectionsResult(QueryConnectionsResult.Status.AMBIGUOUS, null, addresses, null);
else
return new QueryConnectionsResult(QueryConnectionsResult.Status.AMBIGUOUS, null, null, addresses);
}
else
{
return new QueryConnectionsResult(QueryConnectionsResult.Status.AMBIGUOUS, addresses, null, null);
}
}
public QueryConnectionsResult queryMoreConnections(final String uri) throws IOException
{
final CharSequence page = ParserUtils.scrape(uri);
return queryConnections(uri, page);
}
private static final Pattern P_CONNECTIONS_HEAD = Pattern.compile(
".*Von: (.*?).*?Nach: (.*?).*?Datum: .., (.*?) .*?"
+ "(?:.*?)?"
+ "(?:.*?)?", Pattern.DOTALL);
private static final Pattern P_CONNECTIONS_COARSE = Pattern.compile("